From f9f9360b8ffd0436e3184a9110ca5731937336da Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Mon, 7 Jun 2021 14:29:16 +0000 Subject: [PATCH] Add per-ticket treasury key and tspend policies The four JSON-RPC methods that deal with setting and returning approval policies for tspend transactions (settreasurypolicy, settspendpolicy, treasurypolicy, tspendpolicy) gain an additional optional parameter for a ticket hash. When this hash is provided, the policies set or returned by these methods are bound to a specific ticket. This functionality will be used by vspd to set per-ticket tspend approval policies on its voting wallets. Solo voters should never need to use the per-ticket policies. --- internal/rpc/jsonrpc/methods.go | 78 ++++++++++--- internal/rpc/jsonrpc/rpcserverhelp.go | 10 +- internal/rpchelp/helpdescs_en_US.go | 6 + rpc/jsonrpc/types/methods.go | 8 +- rpc/jsonrpc/types/results.go | 2 + wallet/chainntfns.go | 2 +- wallet/udb/treasury.go | 147 ++++++++++++++++++++++++- wallet/udb/upgrades.go | 39 ++++++- wallet/wallet.go | 152 +++++++++++++++++++++----- 9 files changed, 392 insertions(+), 52 deletions(-) diff --git a/internal/rpc/jsonrpc/methods.go b/internal/rpc/jsonrpc/methods.go index 6b408019d..e0def7cbc 100644 --- a/internal/rpc/jsonrpc/methods.go +++ b/internal/rpc/jsonrpc/methods.go @@ -3485,7 +3485,8 @@ func (s *Server) sendOutputsFromTreasury(ctx context.Context, w *wallet.Wallet, // treasuryPolicy returns voting policies for treasury spends by a particular // key. If a key is specified, that policy is returned; otherwise the policies -// for all keys are returned in an array. +// for all keys are returned in an array. If both a key and ticket hash are +// provided, the per-ticket key policy is returned. func (s *Server) treasuryPolicy(ctx context.Context, icmd interface{}) (interface{}, error) { cmd := icmd.(*types.TreasuryPolicyCmd) w, ok := s.walletLoader.LoadedWallet() @@ -3493,13 +3494,22 @@ func (s *Server) treasuryPolicy(ctx context.Context, icmd interface{}) (interfac return nil, errUnloadedWallet } + var ticketHash *chainhash.Hash + if cmd.Ticket != nil && *cmd.Ticket != "" { + var err error + ticketHash, err = chainhash.NewHashFromStr(*cmd.Ticket) + if err != nil { + return nil, rpcError(dcrjson.ErrRPCDecodeHexString, err) + } + } + if cmd.Key != nil && *cmd.Key != "" { pikey, err := hex.DecodeString(*cmd.Key) if err != nil { return nil, rpcError(dcrjson.ErrRPCDecodeHexString, err) } var policy string - switch w.TreasuryKeyPolicy(pikey) { + switch w.TreasuryKeyPolicy(pikey, ticketHash) { case stake.TreasuryVoteYes: policy = "yes" case stake.TreasuryVoteNo: @@ -3511,6 +3521,9 @@ func (s *Server) treasuryPolicy(ctx context.Context, icmd interface{}) (interfac Key: *cmd.Key, Policy: policy, } + if cmd.Ticket != nil { + res.Ticket = *cmd.Ticket + } return res, nil } @@ -3524,10 +3537,14 @@ func (s *Server) treasuryPolicy(ctx context.Context, icmd interface{}) (interfac case stake.TreasuryVoteNo: policy = "no" } - res = append(res, types.TreasuryPolicyResult{ + r := types.TreasuryPolicyResult{ Key: hex.EncodeToString(policies[i].PiKey), Policy: policy, - }) + } + if policies[i].Ticket != nil { + r.Ticket = policies[i].Ticket.String() + } + res = append(res, r) } return res, nil } @@ -3551,7 +3568,7 @@ func (s *Server) setDisapprovePercent(ctx context.Context, icmd interface{}) (in } // setTreasuryPolicy saves the voting policy for treasury spends by a particular -// key. +// key, and optionally, setting the key policy used by a specific ticket. func (s *Server) setTreasuryPolicy(ctx context.Context, icmd interface{}) (interface{}, error) { cmd := icmd.(*types.SetTreasuryPolicyCmd) w, ok := s.walletLoader.LoadedWallet() @@ -3559,6 +3576,15 @@ func (s *Server) setTreasuryPolicy(ctx context.Context, icmd interface{}) (inter return nil, errUnloadedWallet } + var ticketHash *chainhash.Hash + if cmd.Ticket != nil && *cmd.Ticket != "" { + var err error + ticketHash, err = chainhash.NewHashFromStr(*cmd.Ticket) + if err != nil { + return nil, rpcError(dcrjson.ErrRPCDecodeHexString, err) + } + } + pikey, err := hex.DecodeString(cmd.Key) if err != nil { return nil, rpcError(dcrjson.ErrRPCDecodeHexString, err) @@ -3580,14 +3606,15 @@ func (s *Server) setTreasuryPolicy(ctx context.Context, icmd interface{}) (inter return nil, rpcError(dcrjson.ErrRPCInvalidParameter, err) } - err = w.SetTreasuryKeyPolicy(ctx, pikey, policy) + err = w.SetTreasuryKeyPolicy(ctx, pikey, policy, ticketHash) return nil, err } // tspendPolicy returns voting policies for particular treasury spends // transactions. If a tspend transaction hash is specified, that policy is // returned; otherwise the policies for all known tspends are returned in an -// array. +// array. If both a tspend transaction hash and a ticket hash are provided, +// the per-ticket tspend policy is returned. func (s *Server) tspendPolicy(ctx context.Context, icmd interface{}) (interface{}, error) { cmd := icmd.(*types.TSpendPolicyCmd) w, ok := s.walletLoader.LoadedWallet() @@ -3595,13 +3622,22 @@ func (s *Server) tspendPolicy(ctx context.Context, icmd interface{}) (interface{ return nil, errUnloadedWallet } + var ticketHash *chainhash.Hash + if cmd.Ticket != nil && *cmd.Ticket != "" { + var err error + ticketHash, err = chainhash.NewHashFromStr(*cmd.Ticket) + if err != nil { + return nil, rpcError(dcrjson.ErrRPCDecodeHexString, err) + } + } + if cmd.Hash != nil && *cmd.Hash != "" { hash, err := chainhash.NewHashFromStr(*cmd.Hash) if err != nil { return nil, rpcError(dcrjson.ErrRPCDecodeHexString, err) } var policy string - switch w.TSpendPolicy(hash) { + switch w.TSpendPolicy(hash, ticketHash) { case stake.TreasuryVoteYes: policy = "yes" case stake.TreasuryVoteNo: @@ -3613,6 +3649,9 @@ func (s *Server) tspendPolicy(ctx context.Context, icmd interface{}) (interface{ Hash: *cmd.Hash, Policy: policy, } + if cmd.Ticket != nil { + res.Ticket = *cmd.Ticket + } return res, nil } @@ -3620,7 +3659,7 @@ func (s *Server) tspendPolicy(ctx context.Context, icmd interface{}) (interface{ res := make([]types.TSpendPolicyResult, 0, len(tspends)) for i := range tspends { tspendHash := tspends[i].TxHash() - p := w.TSpendPolicy(&tspendHash) + p := w.TSpendPolicy(&tspendHash, ticketHash) var policy string switch p { @@ -3629,16 +3668,20 @@ func (s *Server) tspendPolicy(ctx context.Context, icmd interface{}) (interface{ case stake.TreasuryVoteNo: policy = "no" } - res = append(res, types.TSpendPolicyResult{ + r := types.TSpendPolicyResult{ Hash: tspendHash.String(), Policy: policy, - }) + } + if cmd.Ticket != nil { + r.Ticket = *cmd.Ticket + } + res = append(res, r) } return res, nil } // setTSpendPolicy saves the voting policy for a particular tspend transaction -// hash. +// hash, and optionally, setting the tspend policy used by a specific ticket. func (s *Server) setTSpendPolicy(ctx context.Context, icmd interface{}) (interface{}, error) { cmd := icmd.(*types.SetTSpendPolicyCmd) w, ok := s.walletLoader.LoadedWallet() @@ -3651,6 +3694,15 @@ func (s *Server) setTSpendPolicy(ctx context.Context, icmd interface{}) (interfa return nil, rpcError(dcrjson.ErrRPCDecodeHexString, err) } + var ticketHash *chainhash.Hash + if cmd.Ticket != nil && *cmd.Ticket != "" { + var err error + ticketHash, err = chainhash.NewHashFromStr(*cmd.Ticket) + if err != nil { + return nil, rpcError(dcrjson.ErrRPCDecodeHexString, err) + } + } + var policy stake.TreasuryVoteT switch cmd.Policy { case "abstain", "invalid", "": @@ -3664,7 +3716,7 @@ func (s *Server) setTSpendPolicy(ctx context.Context, icmd interface{}) (interfa return nil, rpcError(dcrjson.ErrRPCInvalidParameter, err) } - err = w.SetTSpendPolicy(ctx, hash, policy) + err = w.SetTSpendPolicy(ctx, hash, policy, ticketHash) return nil, err } diff --git a/internal/rpc/jsonrpc/rpcserverhelp.go b/internal/rpc/jsonrpc/rpcserverhelp.go index dc2879032..e3ba44381 100644 --- a/internal/rpc/jsonrpc/rpcserverhelp.go +++ b/internal/rpc/jsonrpc/rpcserverhelp.go @@ -80,8 +80,8 @@ func helpDescsEnUS() map[string]string { "sendtotreasury": "sendtotreasury amount\n\nSend decred to treasury\n\nArguments:\n1. amount (numeric, required) Amount to send to treasury\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", "setaccountpassphrase": "setaccountpassphrase \"account\" \"passphrase\"\n\nIndividually encrypt or change per-account passphrase\n\nArguments:\n1. account (string, required) Account to modify\n2. passphrase (string, required) New passphrase to use.\nIf this is the empty string, the account passphrase is removed and the account becomes encrypted by the global wallet passhprase.\n\nResult:\nNothing\n", "setdisapprovepercent": "setdisapprovepercent percent\n\nSets the wallet's block disapprove percent per vote. The wallet will randomly disapprove blocks with this percent of votes. Only used for testing purposes and will fail on mainnet.\n\nArguments:\n1. percent (numeric, required) The percent of votes to disapprove blocks. i.e. 100 means that all votes disapprove the block they are called on. Must be between zero and one hundred.\n\nResult:\nNothing\n", - "settreasurypolicy": "settreasurypolicy \"key\" \"policy\"\n\nSet a voting policy for treasury spends by a particular key\n\nArguments:\n1. key (string, required) Treasury key to set policy for\n2. policy (string, required) Voting policy for a treasury key (invalid/abstain, yes, or no)\n\nResult:\nNothing\n", - "settspendpolicy": "settspendpolicy \"hash\" \"policy\"\n\nSet a voting policy for a treasury spend transaction\n\nArguments:\n1. hash (string, required) Hash of treasury spend transaction to set policy for\n2. policy (string, required) Voting policy for a tspend transaction (invalid/abstain, yes, or no)\n\nResult:\nNothing\n", + "settreasurypolicy": "settreasurypolicy \"key\" \"policy\" (\"ticket\")\n\nSet a voting policy for treasury spends by a particular key\n\nArguments:\n1. key (string, required) Treasury key to set policy for\n2. policy (string, required) Voting policy for a treasury key (invalid/abstain, yes, or no)\n3. ticket (string, optional) Ticket hash to set a per-ticket treasury key policy\n\nResult:\nNothing\n", + "settspendpolicy": "settspendpolicy \"hash\" \"policy\" (\"ticket\")\n\nSet a voting policy for a treasury spend transaction\n\nArguments:\n1. hash (string, required) Hash of treasury spend transaction to set policy for\n2. policy (string, required) Voting policy for a tspend transaction (invalid/abstain, yes, or no)\n3. ticket (string, optional) Ticket hash to set a per-ticket tspend approval policy\n\nResult:\nNothing\n", "settxfee": "settxfee amount\n\nModify the fee per kB of the serialized tx size used each time more fee is required for an authored transaction.\n\nArguments:\n1. amount (numeric, required) The new fee per kB of the serialized tx size valued in decred\n\nResult:\ntrue|false (boolean) The boolean 'true'\n", "setvotechoice": "setvotechoice \"agendaid\" \"choiceid\" (\"tickethash\")\n\nSets choices for defined agendas in the latest stake version supported by this software\n\nArguments:\n1. agendaid (string, required) The ID for the agenda to modify\n2. choiceid (string, required) The ID for the choice to choose\n3. tickethash (string, optional) The hash of the ticket to set choices for\n\nResult:\nNothing\n", "signmessage": "signmessage \"address\" \"message\"\n\nSigns a message using the private key of a payment address.\n\nArguments:\n1. address (string, required) Payment address of private key used to sign the message with\n2. message (string, required) Message to sign\n\nResult:\n\"value\" (string) The signed message encoded as a base64 string\n", @@ -92,8 +92,8 @@ func helpDescsEnUS() map[string]string { "syncstatus": "syncstatus\n\nReturns information about this wallet's synchronization to the network.\n\nArguments:\nNone\n\nResult:\n{\n \"synced\": true|false, (boolean) Whether or not the wallet is fully caught up to the network.\n \"initialblockdownload\": true|false, (boolean) Best guess of whether this wallet is in the initial block download mode used to catch up the blockchain when it is far behind.\n \"headersfetchprogress\": n.nnn, (numeric) Estimated progress of the headers fetching stage of the current sync process.\n} \n", "ticketinfo": "ticketinfo (startheight=0)\n\nReturns details of each wallet ticket transaction\n\nArguments:\n1. startheight (numeric, optional, default=0) Specify the starting block height to scan from\n\nResult:\n[{\n \"hash\": \"value\", (string) Transaction hash of the ticket\n \"cost\": n.nnn, (numeric) Amount paid to purchase the ticket; this may be greater than the ticket price at time of purchase\n \"votingaddress\": \"value\", (string) Address of 0th output, which describes the requirements to spend the ticket\n \"status\": \"value\", (string) Description of ticket status (unknown, unmined, immature, mature, live, voted, missed, expired, unspent, revoked)\n \"blockhash\": \"value\", (string) Hash of block ticket is mined in\n \"blockheight\": n, (numeric) Height of block ticket is mined in\n \"vote\": \"value\", (string) Transaction hash of vote which spends the ticket\n \"revocation\": \"value\", (string) Transaction hash of revocation which spends the ticket\n \"choices\": [{ (array of object) Vote preferences set for the ticket\n \"agendaid\": \"value\", (string) The ID for the agenda the choice concerns\n \"agendadescription\": \"value\", (string) A description of the agenda the choice concerns\n \"choiceid\": \"value\", (string) The ID of the current choice for this agenda\n \"choicedescription\": \"value\", (string) A description of the current choice for this agenda\n },...], \n},...]\n", "ticketsforaddress": "ticketsforaddress \"address\"\n\nRequest all the tickets for an address.\n\nArguments:\n1. address (string, required) Address to look for.\n\nResult:\ntrue|false (boolean) Tickets owned by the specified address.\n", - "treasurypolicy": "treasurypolicy (\"key\")\n\nReturn voting policies for treasury spend transactions by key\n\nArguments:\n1. key (string, optional) Return the policy for a particular key\n\nResult (no key provided):\n[{\n \"key\": \"value\", (string) Treasury key associated with a policy\n \"policy\": \"value\", (string) Voting policy description (abstain, yes, or no)\n},...]\n\nResult (key specified):\n{\n \"key\": \"value\", (string) Treasury key associated with a policy\n \"policy\": \"value\", (string) Voting policy description (abstain, yes, or no)\n} \n", - "tspendpolicy": "tspendpolicy (\"hash\")\n\nReturn voting policies for treasury spend transactions\n\nArguments:\n1. hash (string, optional) Return the policy for a particular tspend hash\n\nResult (no tspend hash provided):\n[{\n \"hash\": \"value\", (string) Treasury spend transaction hash\n \"policy\": \"value\", (string) Voting policy description (abstain, yes, or no)\n},...]\n\nResult (tspend hash specified):\n{\n \"hash\": \"value\", (string) Treasury spend transaction hash\n \"policy\": \"value\", (string) Voting policy description (abstain, yes, or no)\n} \n", + "treasurypolicy": "treasurypolicy (\"key\" \"ticket\")\n\nReturn voting policies for treasury spend transactions by key\n\nArguments:\n1. key (string, optional) Return the policy for a particular key\n2. ticket (string, optional) Return policies used by a specific ticket hash\n\nResult (no key provided):\n[{\n \"key\": \"value\", (string) Treasury key associated with a policy\n \"policy\": \"value\", (string) Voting policy description (abstain, yes, or no)\n \"ticket\": \"value\", (string) Ticket hash of a per-ticket treasury key approval policy\n},...]\n\nResult (key specified):\n{\n \"key\": \"value\", (string) Treasury key associated with a policy\n \"policy\": \"value\", (string) Voting policy description (abstain, yes, or no)\n \"ticket\": \"value\", (string) Ticket hash of a per-ticket treasury key approval policy\n} \n", + "tspendpolicy": "tspendpolicy (\"hash\" \"ticket\")\n\nReturn voting policies for treasury spend transactions\n\nArguments:\n1. hash (string, optional) Return the policy for a particular tspend hash\n2. ticket (string, optional) Return policies used by a specific ticket hash\n\nResult (no tspend hash provided):\n[{\n \"hash\": \"value\", (string) Treasury spend transaction hash\n \"policy\": \"value\", (string) Voting policy description (abstain, yes, or no)\n \"ticket\": \"value\", (string) Ticket hash of a per-ticket tspend approval policy\n},...]\n\nResult (tspend hash specified):\n{\n \"hash\": \"value\", (string) Treasury spend transaction hash\n \"policy\": \"value\", (string) Voting policy description (abstain, yes, or no)\n \"ticket\": \"value\", (string) Ticket hash of a per-ticket tspend approval policy\n} \n", "unlockaccount": "unlockaccount \"account\" \"passphrase\"\n\nUnlock an individually-encrypted account\n\nArguments:\n1. account (string, required) Account to unlock\n2. passphrase (string, required) Account passphrase\n\nResult:\nNothing\n", "validateaddress": "validateaddress \"address\"\n\nVerify that an address is valid.\nExtra details are returned if the address is controlled by this wallet.\nThe following fields are valid only when the address is controlled by this wallet (ismine=true): isscript, pubkey, iscompressed, account, addresses, hex, script, and sigsrequired.\nThe following fields are only valid when address has an associated public key: pubkey, iscompressed.\nThe following fields are only valid when address is a pay-to-script-hash address: addresses, hex, and script.\nIf the address is a multisig address controlled by this wallet, the multisig fields will be left unset if the wallet is locked since the redeem script cannot be decrypted.\n\nArguments:\n1. address (string, required) Address to validate\n\nResult:\n{\n \"isvalid\": true|false, (boolean) Whether or not the address is valid\n \"address\": \"value\", (string) The payment address (only when isvalid is true)\n \"ismine\": true|false, (boolean) Whether this address is controlled by the wallet (only when isvalid is true)\n \"iswatchonly\": true|false, (boolean) Unset\n \"isscript\": true|false, (boolean) Whether the payment address is a pay-to-script-hash address (only when isvalid is true)\n \"pubkeyaddr\": \"value\", (string) The pubkey for this payment address (only when isvalid is true)\n \"pubkey\": \"value\", (string) The associated public key of the payment address, if any (only when isvalid is true)\n \"iscompressed\": true|false, (boolean) Whether the address was created by hashing a compressed public key, if any (only when isvalid is true)\n \"account\": \"value\", (string) The account this payment address belongs to (only when isvalid is true)\n \"addresses\": [\"value\",...], (array of string) All associated payment addresses of the script if address is a multisig address (only when isvalid is true)\n \"hex\": \"value\", (string) The redeem script \n \"script\": \"value\", (string) The class of redeem script for a multisig address\n \"sigsrequired\": n, (numeric) The number of required signatures to redeem outputs to the multisig address\n} \n", "validatepredcp0005cf": "validatepredcp0005cf\n\nValidate whether all stored cfilters from before DCP0005 activation are correct according to the expected hardcoded hash\n\nArguments:\nNone\n\nResult:\ntrue|false (boolean) Whether the cfilters are valid\n", @@ -112,4 +112,4 @@ var localeHelpDescs = map[string]func() map[string]string{ "en_US": helpDescsEnUS, } -var requestUsages = "abandontransaction \"hash\"\naccountaddressindex \"account\" branch\naccountsyncaddressindex \"account\" branch index\naccountunlocked \"account\"\naddmultisigaddress nrequired [\"key\",...] (\"account\")\naddtransaction \"blockhash\" \"transaction\"\nauditreuse (since)\nconsolidate inputs (\"account\" \"address\")\ncreatemultisig nrequired [\"key\",...]\ncreatenewaccount \"account\"\ncreaterawtransaction [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...] {\"address\":amount,...} (locktime expiry)\ncreatesignature \"address\" inputindex hashtype \"previouspkscript\" \"serializedtransaction\"\ndisapprovepercent\ndiscoverusage (\"startblock\" discoveraccounts gaplimit)\ndumpprivkey \"address\"\nfundrawtransaction \"hexstring\" \"fundaccount\" ({\"changeaddress\":changeaddress,\"feerate\":feerate,\"conftarget\":conftarget})\ngeneratevote \"blockhash\" height \"tickethash\" votebits \"votebitsext\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblock\ngetbestblockhash\ngetblockcount\ngetblockhash index\ngetblock \"hash\" (verbose=true verbosetx=false)\ngetcoinjoinsbyacct\ngetinfo\ngetmasterpubkey (\"account\")\ngetmultisigoutinfo \"hash\" index\ngetnewaddress (\"account\" \"gappolicy\")\ngetpeerinfo\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngetstakeinfo\ngettickets includeimmature\ngettransaction \"txid\" (includewatchonly=false)\ngettxout \"txid\" vout tree (includemempool=true)\ngetunconfirmedbalance (\"account\")\ngetvotechoices (\"tickethash\")\ngetwalletfee\ngetcfilterv2 \"blockhash\"\nhelp (\"command\")\nimportcfiltersv2 startheight [\"filter\",...]\nimportprivkey \"privkey\" (\"label\" rescan=true scanfrom)\nimportscript \"hex\" (rescan=true scanfrom)\nimportxpub \"name\" \"xpub\"\nlistaccounts (minconf=1)\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nlistlockunspent (\"account\")\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...] \"account\")\nlockaccount \"account\"\nlockunspent unlock [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\nmixaccount\nmixoutput \"outpoint\"\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" numtickets=1 \"pooladdress\" poolfees expiry \"comment\" dontsigntx)\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nrenameaccount \"oldaccount\" \"newaccount\"\nrescanwallet (beginheight=0)\nrevoketickets\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendfromtreasury \"key\" amounts\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendrawtransaction \"hextx\" (allowhighfees=false)\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsendtotreasury amount\nsetaccountpassphrase \"account\" \"passphrase\"\nsetdisapprovepercent percent\nsettreasurypolicy \"key\" \"policy\"\nsettspendpolicy \"hash\" \"policy\"\nsettxfee amount\nsetvotechoice \"agendaid\" \"choiceid\" (\"tickethash\")\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nstakepooluserinfo \"user\"\nsweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\nsyncstatus\nticketinfo (startheight=0)\nticketsforaddress \"address\"\ntreasurypolicy (\"key\")\ntspendpolicy (\"hash\")\nunlockaccount \"account\" \"passphrase\"\nvalidateaddress \"address\"\nvalidatepredcp0005cf\nverifymessage \"address\" \"signature\" \"message\"\nversion\nwalletinfo\nwalletislocked\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\nwalletpubpassphrasechange \"oldpassphrase\" \"newpassphrase\"" +var requestUsages = "abandontransaction \"hash\"\naccountaddressindex \"account\" branch\naccountsyncaddressindex \"account\" branch index\naccountunlocked \"account\"\naddmultisigaddress nrequired [\"key\",...] (\"account\")\naddtransaction \"blockhash\" \"transaction\"\nauditreuse (since)\nconsolidate inputs (\"account\" \"address\")\ncreatemultisig nrequired [\"key\",...]\ncreatenewaccount \"account\"\ncreaterawtransaction [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...] {\"address\":amount,...} (locktime expiry)\ncreatesignature \"address\" inputindex hashtype \"previouspkscript\" \"serializedtransaction\"\ndisapprovepercent\ndiscoverusage (\"startblock\" discoveraccounts gaplimit)\ndumpprivkey \"address\"\nfundrawtransaction \"hexstring\" \"fundaccount\" ({\"changeaddress\":changeaddress,\"feerate\":feerate,\"conftarget\":conftarget})\ngeneratevote \"blockhash\" height \"tickethash\" votebits \"votebitsext\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblock\ngetbestblockhash\ngetblockcount\ngetblockhash index\ngetblock \"hash\" (verbose=true verbosetx=false)\ngetcoinjoinsbyacct\ngetinfo\ngetmasterpubkey (\"account\")\ngetmultisigoutinfo \"hash\" index\ngetnewaddress (\"account\" \"gappolicy\")\ngetpeerinfo\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngetstakeinfo\ngettickets includeimmature\ngettransaction \"txid\" (includewatchonly=false)\ngettxout \"txid\" vout tree (includemempool=true)\ngetunconfirmedbalance (\"account\")\ngetvotechoices (\"tickethash\")\ngetwalletfee\ngetcfilterv2 \"blockhash\"\nhelp (\"command\")\nimportcfiltersv2 startheight [\"filter\",...]\nimportprivkey \"privkey\" (\"label\" rescan=true scanfrom)\nimportscript \"hex\" (rescan=true scanfrom)\nimportxpub \"name\" \"xpub\"\nlistaccounts (minconf=1)\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nlistlockunspent (\"account\")\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...] \"account\")\nlockaccount \"account\"\nlockunspent unlock [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\nmixaccount\nmixoutput \"outpoint\"\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" numtickets=1 \"pooladdress\" poolfees expiry \"comment\" dontsigntx)\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nrenameaccount \"oldaccount\" \"newaccount\"\nrescanwallet (beginheight=0)\nrevoketickets\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendfromtreasury \"key\" amounts\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendrawtransaction \"hextx\" (allowhighfees=false)\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsendtotreasury amount\nsetaccountpassphrase \"account\" \"passphrase\"\nsetdisapprovepercent percent\nsettreasurypolicy \"key\" \"policy\" (\"ticket\")\nsettspendpolicy \"hash\" \"policy\" (\"ticket\")\nsettxfee amount\nsetvotechoice \"agendaid\" \"choiceid\" (\"tickethash\")\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nstakepooluserinfo \"user\"\nsweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\nsyncstatus\nticketinfo (startheight=0)\nticketsforaddress \"address\"\ntreasurypolicy (\"key\" \"ticket\")\ntspendpolicy (\"hash\" \"ticket\")\nunlockaccount \"account\" \"passphrase\"\nvalidateaddress \"address\"\nvalidatepredcp0005cf\nverifymessage \"address\" \"signature\" \"message\"\nversion\nwalletinfo\nwalletislocked\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\nwalletpubpassphrasechange \"oldpassphrase\" \"newpassphrase\"" diff --git a/internal/rpchelp/helpdescs_en_US.go b/internal/rpchelp/helpdescs_en_US.go index 4e9585476..7ec4df6ac 100644 --- a/internal/rpchelp/helpdescs_en_US.go +++ b/internal/rpchelp/helpdescs_en_US.go @@ -774,11 +774,13 @@ var helpDescsEnUS = map[string]string{ "settreasurypolicy--synopsis": "Set a voting policy for treasury spends by a particular key", "settreasurypolicy-key": "Treasury key to set policy for", "settreasurypolicy-policy": "Voting policy for a treasury key (invalid/abstain, yes, or no)", + "settreasurypolicy-ticket": "Ticket hash to set a per-ticket treasury key policy", // SetTSpendPolicyCmd help. "settspendpolicy--synopsis": "Set a voting policy for a treasury spend transaction", "settspendpolicy-hash": "Hash of treasury spend transaction to set policy for", "settspendpolicy-policy": "Voting policy for a tspend transaction (invalid/abstain, yes, or no)", + "settspendpolicy-ticket": "Ticket hash to set a per-ticket tspend approval policy", // SetTxFeeCmd help. "settxfee--synopsis": "Modify the fee per kB of the serialized tx size used each time more fee is required for an authored transaction.", @@ -882,6 +884,7 @@ var helpDescsEnUS = map[string]string{ // TreasuryPolicyCmd help. "treasurypolicy--synopsis": "Return voting policies for treasury spend transactions by key", "treasurypolicy-key": "Return the policy for a particular key", + "treasurypolicy-ticket": "Return policies used by a specific ticket hash", "treasurypolicy--condition0": "no key provided", "treasurypolicy--condition1": "key specified", "treasurypolicy--result0": "Array of all non-abstaining voting policies", @@ -889,10 +892,12 @@ var helpDescsEnUS = map[string]string{ "treasurypolicyresult-key": "Treasury key associated with a policy", "treasurypolicyresult-policy": "Voting policy description (abstain, yes, or no)", + "treasurypolicyresult-ticket": "Ticket hash of a per-ticket treasury key approval policy", // TSpendPolicyCmd help. "tspendpolicy--synopsis": "Return voting policies for treasury spend transactions", "tspendpolicy-hash": "Return the policy for a particular tspend hash", + "tspendpolicy-ticket": "Return policies used by a specific ticket hash", "tspendpolicy--condition0": "no tspend hash provided", "tspendpolicy--condition1": "tspend hash specified", "tspendpolicy--result0": "Array of all non-abstaining policies for known tspends", @@ -900,6 +905,7 @@ var helpDescsEnUS = map[string]string{ "tspendpolicyresult-hash": "Treasury spend transaction hash", "tspendpolicyresult-policy": "Voting policy description (abstain, yes, or no)", + "tspendpolicyresult-ticket": "Ticket hash of a per-ticket tspend approval policy", // UnlockAccountCmd help. "unlockaccount--synopsis": "Unlock an individually-encrypted account", diff --git a/rpc/jsonrpc/types/methods.go b/rpc/jsonrpc/types/methods.go index 23c5325a2..b63e15e27 100644 --- a/rpc/jsonrpc/types/methods.go +++ b/rpc/jsonrpc/types/methods.go @@ -915,7 +915,8 @@ type SetDisapprovePercentCmd struct { // TreasuryPolicyCmd defines the parameters for the treasurypolicy JSON-RPC // command. type TreasuryPolicyCmd struct { - Key *string + Key *string + Ticket *string } // SetTreasuryPolicyCmd defines the parameters for the settreasurypolicy @@ -923,12 +924,14 @@ type TreasuryPolicyCmd struct { type SetTreasuryPolicyCmd struct { Key string Policy string + Ticket *string } // TSpendPolicyCmd defines the parameters for the tspendpolicy JSON-RPC // command. type TSpendPolicyCmd struct { - Hash *string + Hash *string + Ticket *string } // SetTSpendPolicyCmd defines the parameters for the settspendpolicy @@ -936,6 +939,7 @@ type TSpendPolicyCmd struct { type SetTSpendPolicyCmd struct { Hash string Policy string + Ticket *string } // SetTxFeeCmd defines the settxfee JSON-RPC command. diff --git a/rpc/jsonrpc/types/results.go b/rpc/jsonrpc/types/results.go index 8d6aa34c5..4258da9e2 100644 --- a/rpc/jsonrpc/types/results.go +++ b/rpc/jsonrpc/types/results.go @@ -378,12 +378,14 @@ type TicketInfoResult struct { type TreasuryPolicyResult struct { Key string `json:"key"` Policy string `json:"policy"` + Ticket string `json:"ticket,omitempty"` } // TSpendPolicyResult models objects returned by the tspendpolicy command. type TSpendPolicyResult struct { Hash string `json:"hash"` Policy string `json:"policy"` + Ticket string `json:"ticket,omitempty"` } // ValidateAddressResult models the data returned by the wallet server diff --git a/wallet/chainntfns.go b/wallet/chainntfns.go index 8d2c48749..bc7b4dcb6 100644 --- a/wallet/chainntfns.go +++ b/wallet/chainntfns.go @@ -942,7 +942,7 @@ func (w *Wallet) VoteOnOwnedTickets(ctx context.Context, winningTicketHashes []* // Get policy for tspend, falling back to any // policy for the Pi key. tspendHash := v.TxHash() - tspendVote := w.TSpendPolicy(&tspendHash) + tspendVote := w.TSpendPolicy(&tspendHash, ticketHash) if tspendVote == stake.TreasuryVoteInvalid { continue } diff --git a/wallet/udb/treasury.go b/wallet/udb/treasury.go index 2fc62dee5..ceb929aa6 100644 --- a/wallet/udb/treasury.go +++ b/wallet/udb/treasury.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 The Decred developers +// Copyright (c) 2020-2021 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -14,8 +14,20 @@ import ( var ( tspendPolicyBucketKey = []byte("tspendpolicy") // by tspend hash treasuryPolicyBucketKey = []byte("treasurypolicy") // by treasury key + + vspTspendPolicyBucketKey = []byte("vsptspendpolicy") // by ticket | tspend hash + vspTreasuryPolicyBucketKey = []byte("vsptreasurypolicy") // by ticket | treasury key ) +type VSPTSpend struct { + Ticket, TSpend chainhash.Hash +} + +type VSPTreasuryKey struct { + Ticket chainhash.Hash + TreasuryKey string +} + // SetTSpendPolicy sets a tspend vote policy for a specific tspend transaction // hash. func SetTSpendPolicy(dbtx walletdb.ReadWriteTx, hash *chainhash.Hash, @@ -27,10 +39,31 @@ func SetTSpendPolicy(dbtx walletdb.ReadWriteTx, hash *chainhash.Hash, if err != nil { return errors.E(errors.IO, err) } + return nil } return b.Put(hash[:], []byte{byte(policy)}) } +// SetVSPTSpendPolicy sets a tspend vote policy for a specific tspend +// transaction hash for a VSP customer's ticket hash. +func SetVSPTSpendPolicy(dbtx walletdb.ReadWriteTx, ticketHash, tspendHash *chainhash.Hash, + policy stake.TreasuryVoteT) error { + + k := make([]byte, 64) + copy(k, ticketHash[:]) + copy(k[32:], tspendHash[:]) + + b := dbtx.ReadWriteBucket(vspTspendPolicyBucketKey) + if policy == stake.TreasuryVoteInvalid { + err := b.Delete(k) + if err != nil { + return errors.E(errors.IO, err) + } + return nil + } + return b.Put(k, []byte{byte(policy)}) +} + // TSpendPolicy returns the tspend vote policy for a specific tspend transaction // hash. func TSpendPolicy(dbtx walletdb.ReadTx, hash *chainhash.Hash) (stake.TreasuryVoteT, error) { @@ -47,6 +80,28 @@ func TSpendPolicy(dbtx walletdb.ReadTx, hash *chainhash.Hash) (stake.TreasuryVot return stake.TreasuryVoteT(v[0]), nil } +// VSPTSpendPolicy returns the tspend vote policy for a specific tspend +// transaction hash for a VSP customer's ticket hash. +func VSPTSpendPolicy(dbtx walletdb.ReadTx, ticketHash, + tspendHash *chainhash.Hash) (stake.TreasuryVoteT, error) { + + key := make([]byte, 64) + copy(key, ticketHash[:]) + copy(key[32:], tspendHash[:]) + + b := dbtx.ReadBucket(vspTspendPolicyBucketKey) + v := b.Get(key) + if v == nil { + return stake.TreasuryVoteInvalid, nil + } + if len(v) != 1 { + err := errors.Errorf("invalid length %v for tspend "+ + "vote policy", len(v)) + return 0, errors.E(errors.IO, err) + } + return stake.TreasuryVoteT(v[0]), nil +} + // TSpendPolicies returns all tspend vote policies keyed by a tspend hash. // Abstaining policies are excluded. func TSpendPolicies(dbtx walletdb.ReadTx) (map[chainhash.Hash]stake.TreasuryVoteT, error) { @@ -68,6 +123,29 @@ func TSpendPolicies(dbtx walletdb.ReadTx) (map[chainhash.Hash]stake.TreasuryVote return policies, err } +// VSPTSpendPolicies returns all tspend vote policies keyed by a tspend hash and +// a VSP customer's ticket hash. Abstaining policies are excluded. +func VSPTSpendPolicies(dbtx walletdb.ReadTx) (map[VSPTSpend]stake.TreasuryVoteT, error) { + b := dbtx.ReadBucket(vspTspendPolicyBucketKey) + policies := make(map[VSPTSpend]stake.TreasuryVoteT) + err := b.ForEach(func(k, _ []byte) error { + var key VSPTSpend + copy(key.Ticket[:], k) + copy(key.TSpend[:], k[32:]) + + policy, err := VSPTSpendPolicy(dbtx, &key.Ticket, &key.TSpend) + if err != nil { + return err + } + if policy == stake.TreasuryVoteInvalid { + return nil + } + policies[key] = policy + return nil + }) + return policies, err +} + // SetTreasuryKeyPolicy sets a tspend vote policy for a specific Politeia // instance key. func SetTreasuryKeyPolicy(dbtx walletdb.ReadWriteTx, pikey []byte, @@ -79,10 +157,31 @@ func SetTreasuryKeyPolicy(dbtx walletdb.ReadWriteTx, pikey []byte, if err != nil { return errors.E(errors.IO, err) } + return nil } return b.Put(pikey, []byte{byte(policy)}) } +// SetVSPTreasuryKeyPolicy sets a tspend vote policy for a specific Politeia +// instance key for a VSP customer's ticket. +func SetVSPTreasuryKeyPolicy(dbtx walletdb.ReadWriteTx, ticket *chainhash.Hash, + pikey []byte, policy stake.TreasuryVoteT) error { + + k := make([]byte, 0, chainhash.HashSize+len(pikey)) + k = append(k, ticket[:]...) + k = append(k, pikey...) + + b := dbtx.ReadWriteBucket(vspTreasuryPolicyBucketKey) + if policy == stake.TreasuryVoteInvalid { + err := b.Delete(k) + if err != nil { + return errors.E(errors.IO, err) + } + return nil + } + return b.Put(k, []byte{byte(policy)}) +} + // TreasuryKeyPolicy returns the tspend vote policy for a specific Politeia // instance key. func TreasuryKeyPolicy(dbtx walletdb.ReadTx, pikey []byte) (stake.TreasuryVoteT, error) { @@ -99,6 +198,28 @@ func TreasuryKeyPolicy(dbtx walletdb.ReadTx, pikey []byte) (stake.TreasuryVoteT, return stake.TreasuryVoteT(v[0]), nil } +// VSPTreasuryKeyPolicy returns the tspend vote policy for a specific Politeia +// instance key for a VSP customer's ticket. +func VSPTreasuryKeyPolicy(dbtx walletdb.ReadTx, ticket *chainhash.Hash, + pikey []byte) (stake.TreasuryVoteT, error) { + + k := make([]byte, 0, chainhash.HashSize+len(pikey)) + k = append(k, ticket[:]...) + k = append(k, pikey...) + + b := dbtx.ReadBucket(vspTreasuryPolicyBucketKey) + v := b.Get(k) + if v == nil { + return stake.TreasuryVoteInvalid, nil + } + if len(v) != 1 { + err := errors.Errorf("invalid length %v for treasury "+ + "key policy", len(v)) + return 0, errors.E(errors.IO, err) + } + return stake.TreasuryVoteT(v[0]), nil +} + // TreasuryKeyPolicies returns all tspend vote policies keyed by a Politeia // instance key. Abstaining policies are excluded. func TreasuryKeyPolicies(dbtx walletdb.ReadTx) (map[string]stake.TreasuryVoteT, error) { @@ -117,3 +238,27 @@ func TreasuryKeyPolicies(dbtx walletdb.ReadTx) (map[string]stake.TreasuryVoteT, }) return policies, err } + +// VSPTreasuryKeyPolicies returns all tspend vote policies keyed by a Politeia +// instance key for a VSP customer's ticket. Abstaining policies are excluded. +func VSPTreasuryKeyPolicies(dbtx walletdb.ReadTx) (map[VSPTreasuryKey]stake.TreasuryVoteT, error) { + b := dbtx.ReadBucket(vspTreasuryPolicyBucketKey) + policies := make(map[VSPTreasuryKey]stake.TreasuryVoteT) + err := b.ForEach(func(k, _ []byte) error { + var key VSPTreasuryKey + pikey := k[chainhash.HashSize:] + copy(key.Ticket[:], k) + key.TreasuryKey = string(pikey) + + policy, err := VSPTreasuryKeyPolicy(dbtx, &key.Ticket, pikey) + if err != nil { + return err + } + if policy == stake.TreasuryVoteInvalid { + return nil + } + policies[key] = policy + return nil + }) + return policies, err +} diff --git a/wallet/udb/upgrades.go b/wallet/udb/upgrades.go index e5dd10e5f..aea523b14 100644 --- a/wallet/udb/upgrades.go +++ b/wallet/udb/upgrades.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 The Decred developers +// Copyright (c) 2017-2021 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -183,10 +183,16 @@ const ( // transactions by transaction hash, rather than by pi key. tspendHashPolicyVersion = 22 + // vspTreasuryPoliciesVersion is the 23rd version of the database. It + // adds top-level buckets for recording the voting policies + // treasury-spending transactions for specific customer's tickets served + // by a VSP. + vspTreasuryPoliciesVersion = 23 + // DBVersion is the latest version of the database that is understood by the // program. Databases with recorded versions higher than this will fail to // open (meaning any upgrades prevent reverting to older software). - DBVersion = tspendHashPolicyVersion + DBVersion = vspTreasuryPoliciesVersion ) // upgrades maps between old database versions and the upgrade function to @@ -214,6 +220,7 @@ var upgrades = [...]func(walletdb.ReadWriteTx, []byte, *chaincfg.Params) error{ vspBucketVersion - 1: vspBucketUpgrade, vspStatusVersion - 1: vspStatusUpgrade, tspendHashPolicyVersion - 1: tspendHashPolicyUpgrade, + vspTreasuryPoliciesVersion - 1: vspTreasuryPoliciesUpgrade, } func lastUsedAddressIndexUpgrade(tx walletdb.ReadWriteTx, publicPassphrase []byte, params *chaincfg.Params) error { @@ -1534,6 +1541,34 @@ func tspendHashPolicyUpgrade(tx walletdb.ReadWriteTx, publicPassphrase []byte, p return unifiedDBMetadata{}.putVersion(metadataBucket, newVersion) } +func vspTreasuryPoliciesUpgrade(tx walletdb.ReadWriteTx, publicPassphrase []byte, params *chaincfg.Params) error { + const oldVersion = 22 + const newVersion = 23 + + metadataBucket := tx.ReadWriteBucket(unifiedDBMetadata{}.rootBucketKey()) + + // Assert that this function is only called on version 22 databases. + dbVersion, err := unifiedDBMetadata{}.getVersion(metadataBucket) + if err != nil { + return err + } + if dbVersion != oldVersion { + return errors.E(errors.Invalid, "vspTreasuryPoliciesUpgrade inappropriately called") + } + + _, err = tx.CreateTopLevelBucket(vspTreasuryPolicyBucketKey) + if err != nil { + return errors.E(errors.IO, err) + } + _, err = tx.CreateTopLevelBucket(vspTspendPolicyBucketKey) + if err != nil { + return errors.E(errors.IO, err) + } + + // Write the new database version. + return unifiedDBMetadata{}.putVersion(metadataBucket, newVersion) +} + // Upgrade checks whether the any upgrades are necessary before the database is // ready for application usage. If any are, they are performed. func Upgrade(ctx context.Context, db walletdb.DB, publicPassphrase []byte, params *chaincfg.Params) error { diff --git a/wallet/wallet.go b/wallet/wallet.go index 91cdd3d96..aacd9bf2a 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -114,7 +114,9 @@ type Wallet struct { subsidyCache *blockchain.SubsidyCache tspends map[chainhash.Hash]wire.MsgTx tspendPolicy map[chainhash.Hash]stake.TreasuryVoteT - tspendKeyPolicy map[string]stake.TreasuryVoteT // [pikey]vote_policy + tspendKeyPolicy map[string]stake.TreasuryVoteT // keyed by politeia key + vspTSpendPolicy map[udb.VSPTSpend]stake.TreasuryVoteT + vspTSpendKeyPolicy map[udb.VSPTreasuryKey]stake.TreasuryVoteT // Start up flags/settings gapLimit uint32 @@ -338,12 +340,24 @@ func (w *Wallet) readDBTicketVoteBits(dbtx walletdb.ReadTx, ticketHash *chainhas return tvb, hasSavedPrefs } -func (w *Wallet) readDBTreasuryPolicies(dbtx walletdb.ReadTx) (map[chainhash.Hash]stake.TreasuryVoteT, error) { - return udb.TSpendPolicies(dbtx) +func (w *Wallet) readDBTreasuryPolicies(dbtx walletdb.ReadTx) ( + map[chainhash.Hash]stake.TreasuryVoteT, map[udb.VSPTSpend]stake.TreasuryVoteT, error) { + a, err := udb.TSpendPolicies(dbtx) + if err != nil { + return nil, nil, err + } + b, err := udb.VSPTSpendPolicies(dbtx) + return a, b, err } -func (w *Wallet) readDBTreasuryKeyPolicies(dbtx walletdb.ReadTx) (map[string]stake.TreasuryVoteT, error) { - return udb.TreasuryKeyPolicies(dbtx) +func (w *Wallet) readDBTreasuryKeyPolicies(dbtx walletdb.ReadTx) ( + map[string]stake.TreasuryVoteT, map[udb.VSPTreasuryKey]stake.TreasuryVoteT, error) { + a, err := udb.TreasuryKeyPolicies(dbtx) + if err != nil { + return nil, nil, err + } + b, err := udb.VSPTreasuryKeyPolicies(dbtx) + return a, b, err } // VoteBits returns the vote bits that are described by the currently set agenda @@ -527,12 +541,19 @@ func (w *Wallet) SetAgendaChoices(ctx context.Context, ticketHash *chainhash.Has } // TreasuryKeyPolicy returns a vote policy for provided Pi key. If there is -// no policy return TreasuryVoteInvalid. -func (w *Wallet) TreasuryKeyPolicy(pikey []byte) stake.TreasuryVoteT { +// no policy this method returns TreasuryVoteInvalid. +// A non-nil ticket hash may be used by a VSP to return per-ticket policies. +func (w *Wallet) TreasuryKeyPolicy(pikey []byte, ticket *chainhash.Hash) stake.TreasuryVoteT { w.stakeSettingsLock.Lock() defer w.stakeSettingsLock.Unlock() - // Zero value means abstain, just return as is. + // Zero value is abstain/invalid, just return as is. + if ticket != nil { + return w.vspTSpendKeyPolicy[udb.VSPTreasuryKey{ + Ticket: *ticket, + TreasuryKey: string(pikey), + }] + } return w.tspendKeyPolicy[string(pikey)] } @@ -540,30 +561,52 @@ func (w *Wallet) TreasuryKeyPolicy(pikey []byte) stake.TreasuryVoteT { // particular tspend transaction, that policy is returned. Otherwise, if the // tspend is known, any policy for the treasury key which signs the tspend is // returned. -func (w *Wallet) TSpendPolicy(hash *chainhash.Hash) stake.TreasuryVoteT { +// A non-nil ticket hash may be used by a VSP to return per-ticket policies. +func (w *Wallet) TSpendPolicy(tspendHash, ticketHash *chainhash.Hash) stake.TreasuryVoteT { w.stakeSettingsLock.Lock() defer w.stakeSettingsLock.Unlock() - if policy, ok := w.tspendPolicy[*hash]; ok { + // Policy preferences for specific tspends override key policies. + if ticketHash != nil { + policy, ok := w.vspTSpendPolicy[udb.VSPTSpend{ + Ticket: *ticketHash, + TSpend: *tspendHash, + }] + if ok { + return policy + } + } + if policy, ok := w.tspendPolicy[*tspendHash]; ok { return policy } // If this tspend is known, the pi key can be extracted from it and its // policy is returned. - tspend, ok := w.tspends[*hash] + tspend, ok := w.tspends[*tspendHash] if !ok { return 0 // invalid/abstain } pikey := tspend.TxIn[0].SignatureScript[66 : 66+secp256k1.PubKeyBytesLenCompressed] // Zero value means abstain, just return as is. + if ticketHash != nil { + policy, ok := w.vspTSpendKeyPolicy[udb.VSPTreasuryKey{ + Ticket: *ticketHash, + TreasuryKey: string(pikey), + }] + if ok { + return policy + } + } return w.tspendKeyPolicy[string(pikey)] } // TreasuryKeyPolicy records the voting policy for treasury spend transactions -// by a particular key. +// by a particular key, and possibly for a particular ticket being voted on by a +// VSP. type TreasuryKeyPolicy struct { PiKey []byte + Ticket *chainhash.Hash // nil unless for per-ticket VSP policies Policy stake.TreasuryVoteT } @@ -579,13 +622,23 @@ func (w *Wallet) TreasuryKeyPolicies() []TreasuryKeyPolicy { Policy: policy, }) } + for tuple, policy := range w.vspTSpendKeyPolicy { + ticketHash := tuple.Ticket // copy + pikey := []byte(tuple.TreasuryKey) + policies = append(policies, TreasuryKeyPolicy{ + PiKey: pikey, + Ticket: &ticketHash, + Policy: policy, + }) + } return policies } // SetTreasuryKeyPolicy sets a tspend vote policy for a specific Politeia // instance key. +// A non-nil ticket hash may be used by a VSP to set per-ticket policies. func (w *Wallet) SetTreasuryKeyPolicy(ctx context.Context, pikey []byte, - policy stake.TreasuryVoteT) error { + policy stake.TreasuryVoteT, ticketHash *chainhash.Hash) error { switch policy { case stake.TreasuryVoteInvalid, stake.TreasuryVoteNo, stake.TreasuryVoteYes: @@ -598,12 +651,30 @@ func (w *Wallet) SetTreasuryKeyPolicy(ctx context.Context, pikey []byte, w.stakeSettingsLock.Lock() err := walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { + if ticketHash != nil { + return udb.SetVSPTreasuryKeyPolicy(dbtx, ticketHash, + pikey, policy) + } return udb.SetTreasuryKeyPolicy(dbtx, pikey, policy) }) if err != nil { return err } + if ticketHash != nil { + k := udb.VSPTreasuryKey{ + Ticket: *ticketHash, + TreasuryKey: string(pikey), + } + if policy == stake.TreasuryVoteInvalid { + delete(w.vspTSpendKeyPolicy, k) + return nil + } + + w.vspTSpendKeyPolicy[k] = policy + return nil + } + if policy == stake.TreasuryVoteInvalid { delete(w.tspendKeyPolicy, string(pikey)) return nil @@ -615,8 +686,9 @@ func (w *Wallet) SetTreasuryKeyPolicy(ctx context.Context, pikey []byte, // SetTSpendPolicy sets a tspend vote policy for a specific tspend transaction // hash. -func (w *Wallet) SetTSpendPolicy(ctx context.Context, hash *chainhash.Hash, - policy stake.TreasuryVoteT) error { +// A non-nil ticket hash may be used by a VSP to set per-ticket policies. +func (w *Wallet) SetTSpendPolicy(ctx context.Context, tspendHash *chainhash.Hash, + policy stake.TreasuryVoteT, ticketHash *chainhash.Hash) error { switch policy { case stake.TreasuryVoteInvalid, stake.TreasuryVoteNo, stake.TreasuryVoteYes: @@ -629,18 +701,36 @@ func (w *Wallet) SetTSpendPolicy(ctx context.Context, hash *chainhash.Hash, w.stakeSettingsLock.Lock() err := walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { - return udb.SetTSpendPolicy(dbtx, hash, policy) + if ticketHash != nil { + return udb.SetVSPTSpendPolicy(dbtx, ticketHash, + tspendHash, policy) + } + return udb.SetTSpendPolicy(dbtx, tspendHash, policy) }) if err != nil { return err } + if ticketHash != nil { + k := udb.VSPTSpend{ + Ticket: *ticketHash, + TSpend: *tspendHash, + } + if policy == stake.TreasuryVoteInvalid { + delete(w.vspTSpendPolicy, k) + return nil + } + + w.vspTSpendPolicy[k] = policy + return nil + } + if policy == stake.TreasuryVoteInvalid { - delete(w.tspendPolicy, *hash) + delete(w.tspendPolicy, *tspendHash) return nil } - w.tspendPolicy[*hash] = policy + w.tspendPolicy[*tspendHash] = policy return nil } @@ -5295,14 +5385,16 @@ func Open(ctx context.Context, cfg *Config) (*Wallet, error) { db: db, // StakeOptions - votingEnabled: cfg.VotingEnabled, - addressReuse: cfg.AddressReuse, - ticketAddress: cfg.VotingAddress, - poolAddress: cfg.PoolAddress, - poolFees: cfg.PoolFees, - tspends: make(map[chainhash.Hash]wire.MsgTx), - tspendPolicy: make(map[chainhash.Hash]stake.TreasuryVoteT), - tspendKeyPolicy: make(map[string]stake.TreasuryVoteT), + votingEnabled: cfg.VotingEnabled, + addressReuse: cfg.AddressReuse, + ticketAddress: cfg.VotingAddress, + poolAddress: cfg.PoolAddress, + poolFees: cfg.PoolFees, + tspends: make(map[chainhash.Hash]wire.MsgTx), + tspendPolicy: make(map[chainhash.Hash]stake.TreasuryVoteT), + tspendKeyPolicy: make(map[string]stake.TreasuryVoteT), + vspTSpendPolicy: make(map[udb.VSPTSpend]stake.TreasuryVoteT), + vspTSpendKeyPolicy: make(map[udb.VSPTreasuryKey]stake.TreasuryVoteT), // LoaderOptions gapLimit: cfg.GapLimit, @@ -5332,6 +5424,8 @@ func Open(ctx context.Context, cfg *Config) (*Wallet, error) { var vb stake.VoteBits var tspendPolicy map[chainhash.Hash]stake.TreasuryVoteT var treasuryKeyPolicy map[string]stake.TreasuryVoteT + var vspTSpendPolicy map[udb.VSPTSpend]stake.TreasuryVoteT + var vspTreasuryKeyPolicy map[udb.VSPTreasuryKey]stake.TreasuryVoteT err = walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) lastAcct, err := w.manager.LastAccount(ns) @@ -5383,11 +5477,11 @@ func Open(ctx context.Context, cfg *Config) (*Wallet, error) { vb = w.readDBVoteBits(tx) - tspendPolicy, err = w.readDBTreasuryPolicies(tx) + tspendPolicy, vspTSpendPolicy, err = w.readDBTreasuryPolicies(tx) if err != nil { return err } - treasuryKeyPolicy, err = w.readDBTreasuryKeyPolicies(tx) + treasuryKeyPolicy, vspTreasuryKeyPolicy, err = w.readDBTreasuryKeyPolicies(tx) if err != nil { return err } @@ -5402,6 +5496,8 @@ func Open(ctx context.Context, cfg *Config) (*Wallet, error) { w.defaultVoteBits = vb w.tspendPolicy = tspendPolicy w.tspendKeyPolicy = treasuryKeyPolicy + w.vspTSpendPolicy = vspTSpendPolicy + w.vspTSpendKeyPolicy = vspTreasuryKeyPolicy w.stakePoolColdAddrs, err = decodeStakePoolColdExtKey(cfg.StakePoolColdExtKey, cfg.Params)