From d39e94ff7dcd745e71ada5cf603a52cd2c154c63 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 27 Mar 2018 01:53:46 -0400 Subject: [PATCH 01/24] add offline signing functionality --- modules/wallet.go | 12 ++++ modules/wallet/transactionbuilder.go | 84 +++++++++++++++++++++++ modules/wallet/transactionbuilder_test.go | 58 ++++++++++++++++ 3 files changed, 154 insertions(+) diff --git a/modules/wallet.go b/modules/wallet.go index 3994a9f167..5d90bfee08 100644 --- a/modules/wallet.go +++ b/modules/wallet.go @@ -408,6 +408,18 @@ type ( // DustThreshold returns the quantity per byte below which a Currency is // considered to be Dust. DustThreshold() types.Currency + + // SpendableOutputs returns the outputs spendable by the wallet. For + // each output, MaturityHeight is the height of the block containing + // the output. + SpendableOutputs() []ProcessedOutput + + // SignTransaction signs txn using secret keys controlled by w, which + // must be unlocked. For each SiacoinInput whose UnlockConditions are + // not set, SignTransaction attempts to fill in the UnlockConditions + // and adds a corresponding signature. It returns the indices of each + // signed input. + SignTransaction(txn *types.Transaction) []int } // WalletSettings control the behavior of the Wallet. diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index 2b7af3855e..c1bf61b638 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -634,3 +634,87 @@ func (w *Wallet) RegisterTransaction(t types.Transaction, parents []types.Transa func (w *Wallet) StartTransaction() modules.TransactionBuilder { return w.RegisterTransaction(types.Transaction{}, nil) } + +// SpendableOutputs returns the outputs spendable by the wallet. For each +// output, MaturityHeight is the height of the block containing the output. +func (w *Wallet) SpendableOutputs() []modules.ProcessedOutput { + w.mu.Lock() + defer w.mu.Unlock() + // ensure durability of reported outputs + w.syncDB() + + var outputs []modules.ProcessedOutput + dbForEachSiacoinOutput(w.dbTx, func(scoid types.SiacoinOutputID, sco types.SiacoinOutput) { + outputs = append(outputs, modules.ProcessedOutput{ + FundType: types.SpecifierSiacoinOutput, + ID: types.OutputID(scoid), + RelatedAddress: sco.UnlockHash, + Value: sco.Value, + WalletAddress: true, + }) + }) + dbForEachSiafundOutput(w.dbTx, func(sfoid types.SiafundOutputID, sfo types.SiafundOutput) { + outputs = append(outputs, modules.ProcessedOutput{ + FundType: types.SpecifierSiafundOutput, + ID: types.OutputID(sfoid), + RelatedAddress: sfo.UnlockHash, + Value: sfo.Value, + WalletAddress: true, + }) + }) + + // lookup the confirmation height of each output + // TODO: would be much better to store this alongside outputs + for i, o := range outputs { + txnIndices, _ := dbGetAddrTransactions(w.dbTx, o.RelatedAddress) + for _, j := range txnIndices { + pt, err := dbGetProcessedTransaction(w.dbTx, j) + if err != nil { + continue + } + for _, sco := range pt.Outputs { + if sco.ID == o.ID { + outputs[i].MaturityHeight = pt.ConfirmationHeight + break + } + } + } + } + + return outputs +} + +// SignTransaction signs txn using secret keys controlled by w, which must be +// unlocked. For each SiacoinInput whose UnlockConditions are not set, +// SignTransaction attempts to fill in the UnlockConditions and adds a +// corresponding signature. It returns the indices of each signed input. +func (w *Wallet) SignTransaction(txn *types.Transaction) []int { + w.mu.Lock() + defer w.mu.Unlock() + if !w.unlocked { + return nil + } + + var signed []int + for i, sci := range txn.SiacoinInputs { + // identify inputs with unset UnlockConditions + if sci.UnlockConditions.SignaturesRequired == 0 { + // locate output corresponding to this input + sco, err := dbGetSiacoinOutput(w.dbTx, sci.ParentID) + if err != nil { + continue + } + // lookup the signing key(s) + sk, ok := w.keys[sco.UnlockHash] + if !ok { + w.log.Critical("wallet is missing a signing key") + continue + } + txn.SiacoinInputs[i].UnlockConditions = sk.UnlockConditions + cf := types.CoveredFields{WholeTransaction: true} + addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sci.ParentID), sk) + signed = append(signed, i) + } + } + return signed +} diff --git a/modules/wallet/transactionbuilder_test.go b/modules/wallet/transactionbuilder_test.go index 7447b5fb8a..f682692e80 100644 --- a/modules/wallet/transactionbuilder_test.go +++ b/modules/wallet/transactionbuilder_test.go @@ -450,3 +450,61 @@ func TestParallelBuilders(t *testing.T) { t.Fatal("did not get the expected ending balance", expected, endingSCConfirmed, startingSCConfirmed) } } + +func TestSignTransaction(t *testing.T) { + if testing.Short() { + t.SkipNow() + } + wt, err := createWalletTester(t.Name(), modules.ProdDependencies) + if err != nil { + t.Fatal(err) + } + defer wt.closeWt() + + // get an output to spend + outputs := wt.wallet.SpendableOutputs() + + // create a transaction that sends an output to the void + txn := types.Transaction{ + SiacoinInputs: []types.SiacoinInput{{ + ParentID: types.SiacoinOutputID(outputs[0].ID), + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Value: outputs[0].Value, + UnlockHash: types.UnlockHash{}, + }}, + } + // sign the transaction + signed := wt.wallet.SignTransaction(&txn) + if len(signed) != 1 || signed[0] != 0 { + t.Fatal("expected signed to equal [0]; got", signed) + } + // txn should now have unlock condictions and a signature + if txn.SiacoinInputs[0].UnlockConditions.SignaturesRequired == 0 { + t.Fatal("unlock conditions are still unset") + } + if len(txn.TransactionSignatures) == 0 { + t.Fatal("transaction was not signed") + } + + // the resulting transaction should be valid; submit it to the tpool and + // mine a block to confirm it + err = txn.StandaloneValid(wt.wallet.Height()) + if err != nil { + t.Fatal(err) + } + err = wt.tpool.AcceptTransactionSet([]types.Transaction{txn}) + if err != nil { + t.Fatal(err) + } + wt.addBlockNoPayout() + + // the wallet should no longer list the resulting output as spendable + outputs = wt.wallet.SpendableOutputs() + if len(outputs) != 1 { + t.Fatal("expected one output") + } + if outputs[0].ID == types.OutputID(txn.SiacoinInputs[0].ParentID) { + t.Fatal("spent output still listed as spendable") + } +} From 1c7efd4eeedb846fb8e1952e9f9c2ec618a11e67 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 27 Mar 2018 01:54:01 -0400 Subject: [PATCH 02/24] sync before reporting wallet height --- modules/wallet/wallet.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/wallet/wallet.go b/modules/wallet/wallet.go index d62c63f3a1..0869e83be1 100644 --- a/modules/wallet/wallet.go +++ b/modules/wallet/wallet.go @@ -111,6 +111,7 @@ type Wallet struct { func (w *Wallet) Height() types.BlockHeight { w.mu.Lock() defer w.mu.Unlock() + w.syncDB() var height uint64 err := w.db.View(func(tx *bolt.Tx) error { From 4fa416ab72756a630a72d5b4a92b7643fe3a0cd6 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 27 Mar 2018 19:19:22 -0400 Subject: [PATCH 03/24] add api routes for unspent+sign --- node/api/routes.go | 2 ++ node/api/wallet.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/node/api/routes.go b/node/api/routes.go index 6cb9e70100..285a93646d 100644 --- a/node/api/routes.go +++ b/node/api/routes.go @@ -124,6 +124,8 @@ func (api *API) buildHTTPRoutes(requiredUserAgent string, requiredPassword strin router.GET("/wallet/verify/address/:addr", api.walletVerifyAddressHandler) router.POST("/wallet/unlock", RequirePassword(api.walletUnlockHandler, requiredPassword)) router.POST("/wallet/changepassword", RequirePassword(api.walletChangePasswordHandler, requiredPassword)) + router.GET("/wallet/unspent", RequirePassword(api.walletUnspentHandler, requiredPassword)) + router.POST("/wallet/sign", RequirePassword(api.walletSignHandler, requiredPassword)) } // Apply UserAgent middleware and return the Router diff --git a/node/api/wallet.go b/node/api/wallet.go index 3edeb92103..1080eb9c8f 100644 --- a/node/api/wallet.go +++ b/node/api/wallet.go @@ -1,6 +1,7 @@ package api import ( + "encoding/base64" "encoding/json" "net/http" "path/filepath" @@ -8,6 +9,7 @@ import ( "strings" "github.com/NebulousLabs/Sia/crypto" + "github.com/NebulousLabs/Sia/encoding" "github.com/NebulousLabs/Sia/modules" "github.com/NebulousLabs/Sia/types" @@ -63,6 +65,13 @@ type ( TransactionIDs []types.TransactionID `json:"transactionids"` } + // WalletSignPOST contains the signed transaction and a list of the inputs + // that were signed. + WalletSignPOST struct { + Transaction []byte `json:"transaction"` + SignedInputs []int `json:"signedinputs"` + } + // WalletSeedsGET contains the seeds used by the wallet. WalletSeedsGET struct { PrimarySeed string `json:"primaryseed"` @@ -98,6 +107,13 @@ type ( UnconfirmedTransactions []modules.ProcessedTransaction `json:"unconfirmedtransactions"` } + // WalletUnspentGET contains the unspent outputs of the wallet. The + // MaturityHeight field of each output indicates the height of the block + // that the output appeared in. + WalletUnspentGET struct { + Outputs []modules.ProcessedOutput `json:"outputs"` + } + // WalletVerifyAddressGET contains a bool indicating if the address passed to // /wallet/verify/address/:addr is a valid address. WalletVerifyAddressGET struct { @@ -596,3 +612,35 @@ func (api *API) walletVerifyAddressHandler(w http.ResponseWriter, req *http.Requ err := new(types.UnlockHash).LoadString(addrString) WriteJSON(w, WalletVerifyAddressGET{Valid: err == nil}) } + +// walletUnspentHandler handles API calls to /wallet/unspent. +func (api *API) walletUnspentHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { + WriteJSON(w, WalletUnspentGET{ + Outputs: api.wallet.SpendableOutputs(), + }) +} + +// walletSignHandler handles API calls to /wallet/sign. +func (api *API) walletSignHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { + txnBytes, err := base64.StdEncoding.DecodeString(req.FormValue("transaction")) + if err != nil { + WriteError(w, Error{"invalid transaction: " + err.Error()}, http.StatusBadRequest) + return + } + var txn types.Transaction + err = encoding.Unmarshal(txnBytes, &txn) + if err != nil { + WriteError(w, Error{"invalid transaction: " + err.Error()}, http.StatusBadRequest) + return + } + if !api.wallet.Unlocked() { + WriteError(w, Error{"wallet must be unlocked"}, http.StatusBadRequest) + return + } + signed := api.wallet.SignTransaction(&txn) + + WriteJSON(w, WalletSignPOST{ + Transaction: encoding.Marshal(txn), + SignedInputs: signed, + }) +} From e959025c3564a025a8f1f826e1e0711caf9f052e Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 27 Mar 2018 19:40:02 -0400 Subject: [PATCH 04/24] add api docs for unspent+sign --- doc/API.md | 71 +++++++++++++++++++++++------ doc/api/Transactionpool.md | 2 +- doc/api/Wallet.md | 93 +++++++++++++++++++++++++++++++------- 3 files changed, 133 insertions(+), 33 deletions(-) diff --git a/doc/API.md b/doc/API.md index ca25716b6a..c5ac3846f5 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1041,6 +1041,7 @@ Wallet | [/wallet/address](#walletaddress-get) | GET | | [/wallet/addresses](#walletaddresses-get) | GET | | [/wallet/backup](#walletbackup-get) | GET | +| [/wallet/changepassword](#walletchangepassword-post) | POST | | [/wallet/init](#walletinit-post) | POST | | [/wallet/init/seed](#walletinitseed-post) | POST | | [/wallet/lock](#walletlock-post) | POST | @@ -1049,13 +1050,14 @@ Wallet | [/wallet/siacoins](#walletsiacoins-post) | POST | | [/wallet/siafunds](#walletsiafunds-post) | POST | | [/wallet/siagkey](#walletsiagkey-post) | POST | +| [/wallet/sign](#walletsign-post) | POST | | [/wallet/sweep/seed](#walletsweepseed-post) | POST | | [/wallet/transaction/:___id___](#wallettransactionid-get) | GET | | [/wallet/transactions](#wallettransactions-get) | GET | | [/wallet/transactions/:___addr___](#wallettransactionsaddr-get) | GET | | [/wallet/unlock](#walletunlock-post) | POST | -| [/wallet/verify/address/:___addr___](#walletverifyaddressaddr-get) | GET | -| [/wallet/changepassword](#walletchangepassword-post) | POST | +| [/wallet/unspent](#walletunspent-get) | GET | +| [/wallet/verify/address/:___addr___](#walletverifyaddress-get) | GET | For examples and detailed descriptions of request and response parameters, refer to [Wallet.md](/doc/api/Wallet.md). @@ -1145,6 +1147,20 @@ destination standard success or error response. See [#standard-responses](#standard-responses). +#### /wallet/changepassword [POST] + +changes the wallet's encryption key. + +###### Query String Parameters [(with comments)](/doc/api/Wallet.md#query-string-parameters-12) +``` +encryptionpassword +newpassword +``` + +###### Response +standard success or error response. See +[#standard-responses](#standard-responses). + #### /wallet/init [POST] initializes the wallet. After the wallet has been initialized once, it does @@ -1296,6 +1312,24 @@ keyfiles standard success or error response. See [#standard-responses](#standard-responses). +#### /wallet/sign [POST] + +Function: Sign a transaction. The wallet will attempt to sign any SiacoinInput +in the transaction whose UnlockConditions are unset. + +###### Query String Parameters +``` +transaction string +``` + +###### Response [(with comments)](/doc/api/Wallet.md#json-response-7) +```javascript +{ + "transaction": "AQAAAAAAAADBM1ca", + "signedinputs": [0, 1, 6] +} +``` + #### /wallet/sweep/seed [POST] Function: Scan the blockchain for outputs belonging to a seed and send them to @@ -1428,28 +1462,35 @@ encryptionpassword standard success or error response. See [#standard-responses](#standard-responses). -#### /wallet/verify/address/:addr [GET] -takes the address specified by :addr and returns a JSON response indicating if the address is valid. +#### /wallet/unspent [GET] + +returns a list of outputs that the wallet can spend. ###### JSON Response [(with comments)](/doc/api/Wallet.md#json-response-11) ```javascript { - "valid": true + "outputs": [ + { + "id": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "fundtype": "siacoin output", + "maturityheight": 50000, + "walletaddress": true, + "relatedaddress": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", + "value": "1234" // big int + } + ] } ``` -#### /wallet/changepassword [POST] +#### /wallet/verify/address/:addr [GET] -changes the wallet's encryption key. +takes the address specified by :addr and returns a JSON response indicating if the address is valid. -###### Query String Parameters [(with comments)](/doc/api/Wallet.md#query-string-parameters-12) -``` -encryptionpassword -newpassword +###### JSON Response [(with comments)](/doc/api/Wallet.md#json-response-11) +```javascript +{ + "valid": true +} ``` -###### Response -standard success or error response. See -[#standard-responses](#standard-responses). - diff --git a/doc/api/Transactionpool.md b/doc/api/Transactionpool.md index 4a9eecc07e..645852abf8 100644 --- a/doc/api/Transactionpool.md +++ b/doc/api/Transactionpool.md @@ -71,7 +71,7 @@ returns the ID for the requested transaction and its raw encoded parents and tra submits a raw transaction to the transaction pool, broadcasting it to the transaction pool's peers. -###### Query String Parameters [(with comments)](/doc/api/Transactionpool.md#query-string-parameters) +###### Query String Parameters ``` parents string // raw base64 encoded transaction parents diff --git a/doc/api/Wallet.md b/doc/api/Wallet.md index 75e0833339..ec871ba08e 100644 --- a/doc/api/Wallet.md +++ b/doc/api/Wallet.md @@ -34,6 +34,7 @@ Index | [/wallet/address](#walletaddress-get) | GET | | [/wallet/addresses](#walletaddresses-get) | GET | | [/wallet/backup](#walletbackup-get) | GET | +| [/wallet/changepassword](#walletchangepassword-post) | POST | | [/wallet/init](#walletinit-post) | POST | | [/wallet/init/seed](#walletinitseed-post) | POST | | [/wallet/lock](#walletlock-post) | POST | @@ -42,13 +43,14 @@ Index | [/wallet/siacoins](#walletsiacoins-post) | POST | | [/wallet/siafunds](#walletsiafunds-post) | POST | | [/wallet/siagkey](#walletsiagkey-post) | POST | +| [/wallet/sign](#walletsign-post) | POST | | [/wallet/sweep/seed](#walletsweepseed-post) | POST | | [/wallet/transaction/___:id___](#wallettransactionid-get) | GET | | [/wallet/transactions](#wallettransactions-get) | GET | | [/wallet/transactions/___:addr___](#wallettransactionsaddr-get) | GET | | [/wallet/unlock](#walletunlock-post) | POST | +| [/wallet/unspent](#walletunspent-get) | GET | | [/wallet/verify/address/:___addr___](#walletverifyaddress-get) | GET | -| [/wallet/changepassword](#walletchangepassword-post) | POST | #### /wallet [GET] @@ -180,6 +182,22 @@ destination standard success or error response. See [API.md#standard-responses](/doc/API.md#standard-responses). +#### /wallet/changepassword [POST] + +changes the wallet's encryption password. + +###### Query String Parameter +``` +// encryptionpassword is the wallet's current encryption password. +encryptionpassword +// newpassword is the new password for the wallet. +newpassword +``` + +###### Response +standard success or error response. See +[#standard-responses](#standard-responses). + #### /wallet/init [POST] initializes the wallet. After the wallet has been initialized once, it does not @@ -483,6 +501,28 @@ keyfiles standard success or error response. See [API.md#standard-responses](/doc/API.md#standard-responses). +#### /wallet/sign [POST] + +Function: Sign a transaction. The wallet will attempt to sign any SiacoinInput +in the transaction whose UnlockConditions are unset. + +###### Query String Parameters +``` +// base64-encoded transaction to be signed +transaction string +``` + +###### Response +```javascript +{ + // raw, base64 encoded transaction data + "transaction": "AQAAAAAAAADBM1ca", + + // indices of inputs that were signed + "signedinputs": [0, 1, 6] +} +``` + #### /wallet/sweep/seed [POST] Function: Scan the blockchain for outputs belonging to a seed and send them to @@ -693,6 +733,41 @@ encryptionpassword string standard success or error response. See [API.md#standard-responses](/doc/API.md#standard-responses). +#### /wallet/unspent [GET] + +returns a list of outputs that the wallet can spend. + +###### Response +```javascript +{ + // Array of outputs that the wallet can spend. + "outputs": [ + { + // The id of the output. + "id": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + + // Type of output, either 'siacoin output' or 'siafund output'. + "fundtype": "siacoin output", + + // Height of block in which the output appeared. To calculate the + // number of confirmations, subtract this number from the current + // block height. + "maturityheight": 50000, + + // Irrelevant field shared by ProcessedOutput; always true. + "walletaddress": true, + + // UnlockHash of the output. + "relatedaddress": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", + + // Amount of funds in the output; hastings for siacoin outputs, and + // siafunds for siafund outputs. + "value": "1234" // big int + } + ] +} +``` + #### /wallet/verify/address/:addr [GET] takes the address specified by :addr and returns a JSON response indicating if the address is valid. @@ -704,19 +779,3 @@ takes the address specified by :addr and returns a JSON response indicating if t "valid": true } ``` - -#### /wallet/changepassword [POST] - -changes the wallet's encryption password. - -###### Query String Parameter -``` -// encryptionpassword is the wallet's current encryption password. -encryptionpassword -// newpassword is the new password for the wallet. -newpassword -``` - -###### Response -standard success or error response. See -[#standard-responses](#standard-responses). From 6332d01b58eba3c40f8b1b86d05d3395c053d5e5 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 27 Mar 2018 22:17:56 -0400 Subject: [PATCH 05/24] change sign semantics --- doc/API.md | 19 ++++--- doc/api/Wallet.md | 25 +++++---- modules/wallet.go | 11 ++-- modules/wallet/transactionbuilder.go | 65 ++++++++++++++--------- modules/wallet/transactionbuilder_test.go | 9 ++-- node/api/wallet.go | 38 ++++++------- 6 files changed, 95 insertions(+), 72 deletions(-) diff --git a/doc/API.md b/doc/API.md index c5ac3846f5..e19145ee3f 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1314,19 +1314,24 @@ standard success or error response. See #### /wallet/sign [POST] -Function: Sign a transaction. The wallet will attempt to sign any SiacoinInput -in the transaction whose UnlockConditions are unset. +Function: Sign a transaction. The wallet will attempt to sign each input +specified. -###### Query String Parameters +###### Request Body ``` -transaction string +{ + "transaction": { }, // types.Transaction + "tosign": { + "3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678", + "132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf" + } +} ``` -###### Response [(with comments)](/doc/api/Wallet.md#json-response-7) +###### Response ```javascript { - "transaction": "AQAAAAAAAADBM1ca", - "signedinputs": [0, 1, 6] + "transaction": { } // types.Transaction } ``` diff --git a/doc/api/Wallet.md b/doc/api/Wallet.md index ec871ba08e..2a029396a7 100644 --- a/doc/api/Wallet.md +++ b/doc/api/Wallet.md @@ -503,23 +503,28 @@ standard success or error response. See #### /wallet/sign [POST] -Function: Sign a transaction. The wallet will attempt to sign any SiacoinInput -in the transaction whose UnlockConditions are unset. +Function: Sign a transaction. The wallet will attempt to sign each input +specified. -###### Query String Parameters +###### Request Body ``` -// base64-encoded transaction to be signed -transaction string +{ + // unsigned transaction + "transaction": { }, // types.Transaction + + // inputs to sign; a mapping from OutputID to UnlockHash + "tosign": { + "3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678", + "132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf" + } +} ``` ###### Response ```javascript { - // raw, base64 encoded transaction data - "transaction": "AQAAAAAAAADBM1ca", - - // indices of inputs that were signed - "signedinputs": [0, 1, 6] + // signed transaction + "transaction": { } // types.Transaction } ``` diff --git a/modules/wallet.go b/modules/wallet.go index 5d90bfee08..9b53cd34f3 100644 --- a/modules/wallet.go +++ b/modules/wallet.go @@ -414,12 +414,11 @@ type ( // the output. SpendableOutputs() []ProcessedOutput - // SignTransaction signs txn using secret keys controlled by w, which - // must be unlocked. For each SiacoinInput whose UnlockConditions are - // not set, SignTransaction attempts to fill in the UnlockConditions - // and adds a corresponding signature. It returns the indices of each - // signed input. - SignTransaction(txn *types.Transaction) []int + // SignTransaction signs txn using secret keys known to the wallet. toSign + // maps the ParentID of each unsigned input to the UnlockHash of that input's + // desired UnlockConditions. SignTransaction fills in the UnlockConditions for + // each such input and adds a corresponding signature. + SignTransaction(txn *types.Transaction, toSign map[types.OutputID]types.UnlockHash) error } // WalletSettings control the behavior of the Wallet. diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index c1bf61b638..f65cd19bf1 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -684,37 +684,52 @@ func (w *Wallet) SpendableOutputs() []modules.ProcessedOutput { return outputs } -// SignTransaction signs txn using secret keys controlled by w, which must be -// unlocked. For each SiacoinInput whose UnlockConditions are not set, -// SignTransaction attempts to fill in the UnlockConditions and adds a -// corresponding signature. It returns the indices of each signed input. -func (w *Wallet) SignTransaction(txn *types.Transaction) []int { +// SignTransaction signs txn using secret keys known to the wallet. toSign +// maps the ParentID of each unsigned input to the UnlockHash of that input's +// desired UnlockConditions. SignTransaction fills in the UnlockConditions for +// each such input and adds a corresponding signature. +func (w *Wallet) SignTransaction(txn *types.Transaction, toSign map[types.OutputID]types.UnlockHash) error { w.mu.Lock() defer w.mu.Unlock() if !w.unlocked { - return nil + return modules.ErrLockedWallet } + return signTransaction(txn, w.keys, toSign) +} + +// SignTransaction signs txn using secret keys derived from seed. toSign maps +// the ParentID of each unsigned input to the UnlockHash of that input's +// desired UnlockConditions. SignTransaction fills in the UnlockConditions for +// each such input and adds a corresponding signature. +func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign map[types.OutputID]types.UnlockHash) error { + // generate 1M keys + keys := make(map[types.UnlockHash]spendableKey, 1e6) + for _, sk := range generateKeys(seed, 0, 1e6) { + keys[sk.UnlockConditions.UnlockHash()] = sk + } + return signTransaction(txn, keys, toSign) +} - var signed []int +func signTransaction(txn *types.Transaction, keys map[types.UnlockHash]spendableKey, toSign map[types.OutputID]types.UnlockHash) error { + signed := 0 for i, sci := range txn.SiacoinInputs { - // identify inputs with unset UnlockConditions - if sci.UnlockConditions.SignaturesRequired == 0 { - // locate output corresponding to this input - sco, err := dbGetSiacoinOutput(w.dbTx, sci.ParentID) - if err != nil { - continue - } - // lookup the signing key(s) - sk, ok := w.keys[sco.UnlockHash] - if !ok { - w.log.Critical("wallet is missing a signing key") - continue - } - txn.SiacoinInputs[i].UnlockConditions = sk.UnlockConditions - cf := types.CoveredFields{WholeTransaction: true} - addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sci.ParentID), sk) - signed = append(signed, i) + uh, ok := toSign[types.OutputID(sci.ParentID)] + if !ok { + // not signing this input + continue } + // lookup the signing key(s) + sk, ok := keys[uh] + if !ok { + return errors.New("could not locate signing key for " + uh.String()) + } + txn.SiacoinInputs[i].UnlockConditions = sk.UnlockConditions + cf := types.CoveredFields{WholeTransaction: true} + addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sci.ParentID), sk) + signed++ + } + if signed != len(toSign) { + return errors.New("toSign references OutputIDs not present in transaction") } - return signed + return nil } diff --git a/modules/wallet/transactionbuilder_test.go b/modules/wallet/transactionbuilder_test.go index f682692e80..86dc354859 100644 --- a/modules/wallet/transactionbuilder_test.go +++ b/modules/wallet/transactionbuilder_test.go @@ -474,10 +474,13 @@ func TestSignTransaction(t *testing.T) { UnlockHash: types.UnlockHash{}, }}, } + // sign the transaction - signed := wt.wallet.SignTransaction(&txn) - if len(signed) != 1 || signed[0] != 0 { - t.Fatal("expected signed to equal [0]; got", signed) + err = wt.wallet.SignTransaction(&txn, map[types.OutputID]types.UnlockHash{ + outputs[0].ID: outputs[0].RelatedAddress, + }) + if err != nil { + t.Fatal(err) } // txn should now have unlock condictions and a signature if txn.SiacoinInputs[0].UnlockConditions.SignaturesRequired == 0 { diff --git a/node/api/wallet.go b/node/api/wallet.go index 1080eb9c8f..34ba3109e6 100644 --- a/node/api/wallet.go +++ b/node/api/wallet.go @@ -1,7 +1,6 @@ package api import ( - "encoding/base64" "encoding/json" "net/http" "path/filepath" @@ -9,7 +8,6 @@ import ( "strings" "github.com/NebulousLabs/Sia/crypto" - "github.com/NebulousLabs/Sia/encoding" "github.com/NebulousLabs/Sia/modules" "github.com/NebulousLabs/Sia/types" @@ -65,11 +63,16 @@ type ( TransactionIDs []types.TransactionID `json:"transactionids"` } - // WalletSignPOST contains the signed transaction and a list of the inputs - // that were signed. - WalletSignPOST struct { - Transaction []byte `json:"transaction"` - SignedInputs []int `json:"signedinputs"` + // WalletSignPOSTParams contains the unsigned transaction and a set of + // inputs to sign. + WalletSignPOSTParams struct { + Transaction types.Transaction `json:"transaction"` + ToSign map[types.OutputID]types.UnlockHash `json:"tosign"` + } + + // WalletSignPOSTResp contains the signed transaction. + WalletSignPOSTResp struct { + Transaction types.Transaction `json:"transaction"` } // WalletSeedsGET contains the seeds used by the wallet. @@ -622,25 +625,18 @@ func (api *API) walletUnspentHandler(w http.ResponseWriter, req *http.Request, _ // walletSignHandler handles API calls to /wallet/sign. func (api *API) walletSignHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { - txnBytes, err := base64.StdEncoding.DecodeString(req.FormValue("transaction")) + var params WalletSignPOSTParams + err := json.NewDecoder(req.Body).Decode(¶ms) if err != nil { - WriteError(w, Error{"invalid transaction: " + err.Error()}, http.StatusBadRequest) + WriteError(w, Error{"invalid parameters: " + err.Error()}, http.StatusBadRequest) return } - var txn types.Transaction - err = encoding.Unmarshal(txnBytes, &txn) + err = api.wallet.SignTransaction(¶ms.Transaction, params.ToSign) if err != nil { - WriteError(w, Error{"invalid transaction: " + err.Error()}, http.StatusBadRequest) + WriteError(w, Error{"failed to sign transaction: " + err.Error()}, http.StatusBadRequest) return } - if !api.wallet.Unlocked() { - WriteError(w, Error{"wallet must be unlocked"}, http.StatusBadRequest) - return - } - signed := api.wallet.SignTransaction(&txn) - - WriteJSON(w, WalletSignPOST{ - Transaction: encoding.Marshal(txn), - SignedInputs: signed, + WriteJSON(w, WalletSignPOSTResp{ + Transaction: params.Transaction, }) } From b3741e7504ae1c71e3878e31b9ca8a5699e3ad14 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 27 Mar 2018 22:36:20 -0400 Subject: [PATCH 06/24] add wallet sign command --- cmd/siac/main.go | 2 +- cmd/siac/walletcmd.go | 52 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/cmd/siac/main.go b/cmd/siac/main.go index 54bd3448cf..f860dd503d 100644 --- a/cmd/siac/main.go +++ b/cmd/siac/main.go @@ -277,7 +277,7 @@ func main() { root.AddCommand(walletCmd) walletCmd.AddCommand(walletAddressCmd, walletAddressesCmd, walletChangepasswordCmd, walletInitCmd, walletInitSeedCmd, - walletLoadCmd, walletLockCmd, walletSeedsCmd, walletSendCmd, walletSweepCmd, + walletLoadCmd, walletLockCmd, walletSeedsCmd, walletSendCmd, walletSweepCmd, walletSignCmd, walletBalanceCmd, walletTransactionsCmd, walletUnlockCmd) walletInitCmd.Flags().BoolVarP(&initPassword, "password", "p", false, "Prompt for a custom password") walletInitCmd.Flags().BoolVarP(&initForce, "force", "", false, "destroy the existing wallet and re-encrypt") diff --git a/cmd/siac/walletcmd.go b/cmd/siac/walletcmd.go index d89fafa95e..583fa916b5 100644 --- a/cmd/siac/walletcmd.go +++ b/cmd/siac/walletcmd.go @@ -1,6 +1,8 @@ package main import ( + "encoding/base64" + "encoding/json" "errors" "fmt" "math/big" @@ -11,8 +13,13 @@ import ( "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" + "github.com/NebulousLabs/Sia/crypto" + "github.com/NebulousLabs/Sia/encoding" + "github.com/NebulousLabs/Sia/modules" + "github.com/NebulousLabs/Sia/modules/wallet" "github.com/NebulousLabs/Sia/node/api" "github.com/NebulousLabs/Sia/types" + "github.com/NebulousLabs/entropy-mnemonics" ) var ( @@ -148,6 +155,14 @@ Run 'wallet send --help' to see a list of available units.`, Run: wrap(walletsendsiafundscmd), } + walletSignCmd = &cobra.Command{ + Use: "sign [txn] [tosign]", + Short: "Sign a transaction", + Long: `Sign the specified inputs of a transaction using one or more keys +derived from the supplied seed.`, + Run: wrap(walletsigncmd), + } + walletSweepCmd = &cobra.Command{ Use: "sweep", Short: "Sweep siacoins and siafunds from a seed.", @@ -466,6 +481,43 @@ func walletsweepcmd() { fmt.Printf("Swept %v and %v SF from seed.\n", currencyUnits(swept.Coins), swept.Funds) } +// walletsigncmd signs a transaction. +func walletsigncmd(txnJSON, toSignJSON string) { + var txn types.Transaction + err := json.Unmarshal([]byte(txnJSON), &txn) + if err != nil { + die("Invalid transaction:", err) + } + + var toSignStrings map[string]string + err = json.Unmarshal([]byte(toSignJSON), &toSignStrings) + if err != nil { + die("Invalid transaction:", err) + } + toSign := make(map[types.OutputID]types.UnlockHash) + for k, v := range toSignStrings { + var oid crypto.Hash + oid.LoadString(k) + var uh types.UnlockHash + uh.LoadString(v) + toSign[types.OutputID(oid)] = uh + } + + seedString, err := passwordPrompt("Seed: ") + if err != nil { + die("Reading seed failed:", err) + } + seed, err := modules.StringToSeed(seedString, mnemonics.English) + if err != nil { + die("Invalid seed:", err) + } + err = wallet.SignTransaction(&txn, seed, toSign) + if err != nil { + die("Failed to sign transaction:", err) + } + fmt.Println(base64.StdEncoding.EncodeToString(encoding.Marshal(txn))) +} + // wallettransactionscmd lists all of the transactions related to the wallet, // providing a net flow of siacoins and siafunds for each. func wallettransactionscmd() { From 41c541084e76b20c3b6580584a4d593d942cf161 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 27 Mar 2018 22:46:45 -0400 Subject: [PATCH 07/24] generate keys incrementally --- modules/wallet/transactionbuilder.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index f65cd19bf1..52402fd456 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -701,11 +701,23 @@ func (w *Wallet) SignTransaction(txn *types.Transaction, toSign map[types.Output // the ParentID of each unsigned input to the UnlockHash of that input's // desired UnlockConditions. SignTransaction fills in the UnlockConditions for // each such input and adds a corresponding signature. +// +// SignTransaction must derive all of the keys from scratch, so it is +// appreciably slower than calling the Wallet.SignTransaction method. Only the +// first 1 million keys are derived. func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign map[types.OutputID]types.UnlockHash) error { - // generate 1M keys + // generate keys in batches up to 1e6 before giving up keys := make(map[types.UnlockHash]spendableKey, 1e6) - for _, sk := range generateKeys(seed, 0, 1e6) { - keys[sk.UnlockConditions.UnlockHash()] = sk + var keyIndex uint64 + const keysPerBatch = 1000 + for len(keys) < 1e6 { + for _, sk := range generateKeys(seed, keyIndex, keyIndex+keysPerBatch) { + keys[sk.UnlockConditions.UnlockHash()] = sk + } + keyIndex += keysPerBatch + if err := signTransaction(txn, keys, toSign); err == nil { + return nil + } } return signTransaction(txn, keys, toSign) } From 32059e7c8aaf2d16ed825f5c2a5419d9962c8cdb Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 27 Mar 2018 23:36:06 -0400 Subject: [PATCH 08/24] use new SpendableOutput type for /unspent --- doc/API.md | 5 ++- doc/api/Wallet.md | 7 ++-- modules/wallet.go | 16 +++++++--- modules/wallet/transactionbuilder.go | 39 +++++++++++------------ modules/wallet/transactionbuilder_test.go | 2 +- node/api/wallet.go | 2 +- 6 files changed, 36 insertions(+), 35 deletions(-) diff --git a/doc/API.md b/doc/API.md index e19145ee3f..ad7a0d9496 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1479,9 +1479,8 @@ returns a list of outputs that the wallet can spend. { "id": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "fundtype": "siacoin output", - "maturityheight": 50000, - "walletaddress": true, - "relatedaddress": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", + "confirmationheight": 50000, + "unlockhash": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", "value": "1234" // big int } ] diff --git a/doc/api/Wallet.md b/doc/api/Wallet.md index 2a029396a7..26c089b038 100644 --- a/doc/api/Wallet.md +++ b/doc/api/Wallet.md @@ -757,13 +757,10 @@ returns a list of outputs that the wallet can spend. // Height of block in which the output appeared. To calculate the // number of confirmations, subtract this number from the current // block height. - "maturityheight": 50000, - - // Irrelevant field shared by ProcessedOutput; always true. - "walletaddress": true, + "confirmationheight": 50000, // UnlockHash of the output. - "relatedaddress": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", + "unlockhash": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", // Amount of funds in the output; hastings for siacoin outputs, and // siafunds for siafund outputs. diff --git a/modules/wallet.go b/modules/wallet.go index 9b53cd34f3..ec063a748a 100644 --- a/modules/wallet.go +++ b/modules/wallet.go @@ -100,6 +100,16 @@ type ( Outputs []ProcessedOutput `json:"outputs"` } + // A SpendableOutput is a SiacoinOutput or SiafundOutput that the wallet + // can spend. + SpendableOutput struct { + ID types.OutputID `json:"id"` + FundType types.Specifier `json:"fundtype"` + UnlockHash types.UnlockHash `json:"unlockhash"` + Value types.Currency `json:"value"` + ConfirmationHeight types.BlockHeight `json:"confirmationheight"` + } + // TransactionBuilder is used to construct custom transactions. A transaction // builder is initialized via 'RegisterTransaction' and then can be modified by // adding funds or other fields. The transaction is completed by calling @@ -409,10 +419,8 @@ type ( // considered to be Dust. DustThreshold() types.Currency - // SpendableOutputs returns the outputs spendable by the wallet. For - // each output, MaturityHeight is the height of the block containing - // the output. - SpendableOutputs() []ProcessedOutput + // SpendableOutputs returns the outputs spendable by the wallet. + SpendableOutputs() []SpendableOutput // SignTransaction signs txn using secret keys known to the wallet. toSign // maps the ParentID of each unsigned input to the UnlockHash of that input's diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index 52402fd456..e354f2bfaf 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -635,38 +635,35 @@ func (w *Wallet) StartTransaction() modules.TransactionBuilder { return w.RegisterTransaction(types.Transaction{}, nil) } -// SpendableOutputs returns the outputs spendable by the wallet. For each -// output, MaturityHeight is the height of the block containing the output. -func (w *Wallet) SpendableOutputs() []modules.ProcessedOutput { +// SpendableOutputs returns the outputs spendable by the wallet. +func (w *Wallet) SpendableOutputs() []modules.SpendableOutput { w.mu.Lock() defer w.mu.Unlock() // ensure durability of reported outputs w.syncDB() - var outputs []modules.ProcessedOutput + var outputs []modules.SpendableOutput dbForEachSiacoinOutput(w.dbTx, func(scoid types.SiacoinOutputID, sco types.SiacoinOutput) { - outputs = append(outputs, modules.ProcessedOutput{ - FundType: types.SpecifierSiacoinOutput, - ID: types.OutputID(scoid), - RelatedAddress: sco.UnlockHash, - Value: sco.Value, - WalletAddress: true, + outputs = append(outputs, modules.SpendableOutput{ + FundType: types.SpecifierSiacoinOutput, + ID: types.OutputID(scoid), + UnlockHash: sco.UnlockHash, + Value: sco.Value, }) }) dbForEachSiafundOutput(w.dbTx, func(sfoid types.SiafundOutputID, sfo types.SiafundOutput) { - outputs = append(outputs, modules.ProcessedOutput{ - FundType: types.SpecifierSiafundOutput, - ID: types.OutputID(sfoid), - RelatedAddress: sfo.UnlockHash, - Value: sfo.Value, - WalletAddress: true, + outputs = append(outputs, modules.SpendableOutput{ + FundType: types.SpecifierSiafundOutput, + ID: types.OutputID(sfoid), + UnlockHash: sfo.UnlockHash, + Value: sfo.Value, }) }) - // lookup the confirmation height of each output - // TODO: would be much better to store this alongside outputs + // set the confirmation height for each output +outer: for i, o := range outputs { - txnIndices, _ := dbGetAddrTransactions(w.dbTx, o.RelatedAddress) + txnIndices, _ := dbGetAddrTransactions(w.dbTx, o.UnlockHash) for _, j := range txnIndices { pt, err := dbGetProcessedTransaction(w.dbTx, j) if err != nil { @@ -674,8 +671,8 @@ func (w *Wallet) SpendableOutputs() []modules.ProcessedOutput { } for _, sco := range pt.Outputs { if sco.ID == o.ID { - outputs[i].MaturityHeight = pt.ConfirmationHeight - break + outputs[i].ConfirmationHeight = pt.ConfirmationHeight + continue outer } } } diff --git a/modules/wallet/transactionbuilder_test.go b/modules/wallet/transactionbuilder_test.go index 86dc354859..290bfedeb1 100644 --- a/modules/wallet/transactionbuilder_test.go +++ b/modules/wallet/transactionbuilder_test.go @@ -477,7 +477,7 @@ func TestSignTransaction(t *testing.T) { // sign the transaction err = wt.wallet.SignTransaction(&txn, map[types.OutputID]types.UnlockHash{ - outputs[0].ID: outputs[0].RelatedAddress, + outputs[0].ID: outputs[0].UnlockHash, }) if err != nil { t.Fatal(err) diff --git a/node/api/wallet.go b/node/api/wallet.go index 34ba3109e6..92247ccbf2 100644 --- a/node/api/wallet.go +++ b/node/api/wallet.go @@ -114,7 +114,7 @@ type ( // MaturityHeight field of each output indicates the height of the block // that the output appeared in. WalletUnspentGET struct { - Outputs []modules.ProcessedOutput `json:"outputs"` + Outputs []modules.SpendableOutput `json:"outputs"` } // WalletVerifyAddressGET contains a bool indicating if the address passed to From 277d93a828ffc9ac825f27c4518cb55b6c9925ae Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 27 Mar 2018 23:36:17 -0400 Subject: [PATCH 09/24] sign SiafundInputs as well --- modules/wallet/transactionbuilder.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index e354f2bfaf..9600671e52 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -737,6 +737,22 @@ func signTransaction(txn *types.Transaction, keys map[types.UnlockHash]spendable addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sci.ParentID), sk) signed++ } + for i, sfi := range txn.SiafundInputs { + uh, ok := toSign[types.OutputID(sfi.ParentID)] + if !ok { + // not signing this input + continue + } + // lookup the signing key(s) + sk, ok := keys[uh] + if !ok { + return errors.New("could not locate signing key for " + uh.String()) + } + txn.SiafundInputs[i].UnlockConditions = sk.UnlockConditions + cf := types.CoveredFields{WholeTransaction: true} + addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sfi.ParentID), sk) + signed++ + } if signed != len(toSign) { return errors.New("toSign references OutputIDs not present in transaction") } From 873500b505259e1d7ac1cc78bf0462108f40dfbb Mon Sep 17 00:00:00 2001 From: Christopher Schinnerl Date: Thu, 29 Mar 2018 12:29:53 -0400 Subject: [PATCH 10/24] Add siatest and client integration for offline signing --- Makefile | 4 +- node/api/client/transactionpool.go | 21 +++++++ node/api/client/wallet.go | 22 ++++++++ siatest/wallet/wallet.go | 1 + siatest/wallet/wallet_test.go | 89 ++++++++++++++++++++++++++++++ types/encoding.go | 12 +++- 6 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 node/api/client/transactionpool.go create mode 100644 siatest/wallet/wallet.go create mode 100644 siatest/wallet/wallet_test.go diff --git a/Makefile b/Makefile index 0822a934fb..c99c39acd9 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ pkgs = ./build ./cmd/siac ./cmd/siad ./compatibility ./crypto ./encoding ./modul ./modules/gateway ./modules/host ./modules/host/contractmanager ./modules/renter ./modules/renter/contractor \ ./modules/renter/hostdb ./modules/renter/hostdb/hosttree ./modules/renter/proto ./modules/miner ./modules/wallet \ ./modules/transactionpool ./node ./node/api ./persist ./siatest ./siatest/consensus ./siatest/renter \ - ./node/api/server ./sync ./types + ./siatest/wallet ./node/api/server ./sync ./types # fmt calls go fmt on all packages. fmt: @@ -62,7 +62,7 @@ vet: release-std lintpkgs = ./build ./cmd/siac ./cmd/siad ./compatibility ./crypto ./encoding ./modules ./modules/consensus ./modules/explorer \ ./modules/gateway ./modules/host ./modules/miner ./modules/host/contractmanager ./modules/renter ./modules/renter/contractor ./modules/renter/hostdb \ ./modules/renter/hostdb/hosttree ./modules/renter/proto ./modules/wallet ./modules/transactionpool ./node ./node/api ./node/api/server ./persist \ - ./siatest ./siatest/consensus ./siatest/renter + ./siatest ./siatest/consensus ./siatest/renter ./siatest/wallet lint: golint -min_confidence=1.0 -set_exit_status $(lintpkgs) diff --git a/node/api/client/transactionpool.go b/node/api/client/transactionpool.go new file mode 100644 index 0000000000..6262e52ad9 --- /dev/null +++ b/node/api/client/transactionpool.go @@ -0,0 +1,21 @@ +package client + +import ( + "encoding/base64" + "net/url" + + "github.com/NebulousLabs/Sia/encoding" + "github.com/NebulousLabs/Sia/types" +) + +// TransactionpoolRawPost uses the /tpool/raw endpoint to broadcast a +// transaction by adding it to the transactionpool. +func (c *Client) TransactionpoolRawPost(parents []types.Transaction, txn types.Transaction) (err error) { + parentsBytes := encoding.Marshal(parents) + txnBytes := encoding.Marshal(txn) + values := url.Values{} + values.Set("parents", base64.StdEncoding.EncodeToString(parentsBytes)) + values.Set("transaction", base64.StdEncoding.EncodeToString(txnBytes)) + err = c.post("/tpool/raw", values.Encode(), nil) + return +} diff --git a/node/api/client/wallet.go b/node/api/client/wallet.go index dff2fdfbd2..026977058a 100644 --- a/node/api/client/wallet.go +++ b/node/api/client/wallet.go @@ -1,6 +1,7 @@ package client import ( + "bytes" "encoding/json" "fmt" "net/url" @@ -55,6 +56,20 @@ func (c *Client) WalletSiacoinsPost(amount types.Currency, destination types.Unl return } +// WalletSignPost uses the /wallet/sign api endpoint to sign a transaction. +func (c *Client) WalletSignPost(txn types.Transaction, toSign map[types.OutputID]types.UnlockHash) (wspr api.WalletSignPOSTResp, err error) { + buf := new(bytes.Buffer) + err = json.NewEncoder(buf).Encode(api.WalletSignPOSTParams{ + Transaction: txn, + ToSign: toSign, + }) + if err != nil { + return + } + err = c.post("/wallet/sign", string(buf.Bytes()), &wspr) + return +} + // WalletTransactionsGet requests the/wallet/transactions api resource for a // certain startheight and endheight func (c *Client) WalletTransactionsGet(startHeight types.BlockHeight, endHeight types.BlockHeight) (wtg api.WalletTransactionsGET, err error) { @@ -71,3 +86,10 @@ func (c *Client) WalletUnlockPost(password string) (err error) { err = c.post("/wallet/unlock", values.Encode(), nil) return } + +// WalletUnspentGet requests the /wallet/unspent endpoint and returns all of +// the unspent outputs related to the wallet. +func (c *Client) WalletUnspentGet() (wug api.WalletUnspentGET, err error) { + err = c.get("/wallet/unspent", &wug) + return +} diff --git a/siatest/wallet/wallet.go b/siatest/wallet/wallet.go new file mode 100644 index 0000000000..23a7507327 --- /dev/null +++ b/siatest/wallet/wallet.go @@ -0,0 +1 @@ +package wallet diff --git a/siatest/wallet/wallet_test.go b/siatest/wallet/wallet_test.go new file mode 100644 index 0000000000..1de4d0f1b8 --- /dev/null +++ b/siatest/wallet/wallet_test.go @@ -0,0 +1,89 @@ +package wallet + +import ( + "testing" + + "github.com/NebulousLabs/Sia/node" + "github.com/NebulousLabs/Sia/siatest" + "github.com/NebulousLabs/Sia/types" +) + +// TestSignTransaction is a integration test for signing transaction offline +// using the API. +func TestSignTransaction(t *testing.T) { + if testing.Short() { + t.SkipNow() + } + testdir, err := siatest.TestDir(t.Name()) + if err != nil { + t.Fatal(err) + } + + // Create a new server + testNode, err := siatest.NewNode(node.AllModules(testdir)) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := testNode.Close(); err != nil { + t.Fatal(err) + } + }() + + // get an output to spend + unspentResp, err := testNode.WalletUnspentGet() + if err != nil { + t.Fatal("failed to get spendable outputs") + } + outputs := unspentResp.Outputs + + // create a transaction that sends an output to the void + txn := types.Transaction{ + SiacoinInputs: []types.SiacoinInput{{ + ParentID: types.SiacoinOutputID(outputs[0].ID), + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Value: outputs[0].Value, + UnlockHash: types.UnlockHash{}, + }}, + } + + // sign the transaction + signResp, err := testNode.WalletSignPost(txn, map[types.OutputID]types.UnlockHash{ + outputs[0].ID: outputs[0].UnlockHash, + }) + if err != nil { + t.Fatal("failed to sign the transaction", err) + } + txn = signResp.Transaction + + // txn should now have unlock condictions and a signature + if txn.SiacoinInputs[0].UnlockConditions.SignaturesRequired == 0 { + t.Fatal("unlock conditions are still unset") + } + if len(txn.TransactionSignatures) == 0 { + t.Fatal("transaction was not signed") + } + + // the resulting transaction should be valid; submit it to the tpool and + // mine a block to confirm it + if err := testNode.TransactionpoolRawPost(nil, txn); err != nil { + t.Fatal("failed to add transaction to pool", err) + } + if err := testNode.MineBlock(); err != nil { + t.Fatal("failed to mine block", err) + } + + // the wallet should no longer list the resulting output as spendable + unspentResp, err = testNode.WalletUnspentGet() + if err != nil { + t.Fatal("failed to get spendable outputs") + } + outputs = unspentResp.Outputs + if len(outputs) != 1 { + t.Fatal("expected one output") + } + if outputs[0].ID == types.OutputID(txn.SiacoinInputs[0].ParentID) { + t.Fatal("spent output still listed as spendable") + } +} diff --git a/types/encoding.go b/types/encoding.go index ea55d9ac56..b248e3302b 100644 --- a/types/encoding.go +++ b/types/encoding.go @@ -646,7 +646,7 @@ func (fcid *FileContractID) UnmarshalJSON(b []byte) error { // MarshalJSON marshals an id as a hex string. func (oid OutputID) MarshalJSON() ([]byte, error) { - return json.Marshal(oid.String()) + return (*crypto.Hash)(&oid).MarshalJSON() } // String prints the id in hex. @@ -654,6 +654,16 @@ func (oid OutputID) String() string { return fmt.Sprintf("%x", oid[:]) } +// MarshalText marshals an OutputID to text. +func (oid OutputID) MarshalText() (text []byte, err error) { + return []byte(oid.String()), nil +} + +// UnmarshalText unmarshals an OutputID from text. +func (oid *OutputID) UnmarshalText(text []byte) error { + return (*crypto.Hash)(oid).LoadString(string(text)) +} + // UnmarshalJSON decodes the json hex string of the id. func (oid *OutputID) UnmarshalJSON(b []byte) error { return (*crypto.Hash)(oid).UnmarshalJSON(b) From 90566ab720efdc6747e6e9236e8f5dfa6a35cddd Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 29 Mar 2018 17:51:42 -0400 Subject: [PATCH 11/24] decode directly into toSign map --- cmd/siac/walletcmd.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/cmd/siac/walletcmd.go b/cmd/siac/walletcmd.go index 583fa916b5..e9a468bd8a 100644 --- a/cmd/siac/walletcmd.go +++ b/cmd/siac/walletcmd.go @@ -13,7 +13,6 @@ import ( "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" - "github.com/NebulousLabs/Sia/crypto" "github.com/NebulousLabs/Sia/encoding" "github.com/NebulousLabs/Sia/modules" "github.com/NebulousLabs/Sia/modules/wallet" @@ -489,19 +488,11 @@ func walletsigncmd(txnJSON, toSignJSON string) { die("Invalid transaction:", err) } - var toSignStrings map[string]string - err = json.Unmarshal([]byte(toSignJSON), &toSignStrings) + var toSign map[types.OutputID]types.UnlockHash + err = json.Unmarshal([]byte(toSignJSON), &toSign) if err != nil { die("Invalid transaction:", err) } - toSign := make(map[types.OutputID]types.UnlockHash) - for k, v := range toSignStrings { - var oid crypto.Hash - oid.LoadString(k) - var uh types.UnlockHash - uh.LoadString(v) - toSign[types.OutputID(oid)] = uh - } seedString, err := passwordPrompt("Seed: ") if err != nil { From d408cc1a432b8cb91a756bfdf6a45cde7d0f5292 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 29 Mar 2018 17:52:36 -0400 Subject: [PATCH 12/24] add docstrings --- modules/wallet/transactionbuilder.go | 2 ++ modules/wallet/transactionbuilder_test.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index 9600671e52..1b68d1c360 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -719,6 +719,8 @@ func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign map[types return signTransaction(txn, keys, toSign) } +// signTransaction signs the specified inputs of txn using the specified keys. +// It returns an error if any of the specified inputs cannot be signed. func signTransaction(txn *types.Transaction, keys map[types.UnlockHash]spendableKey, toSign map[types.OutputID]types.UnlockHash) error { signed := 0 for i, sci := range txn.SiacoinInputs { diff --git a/modules/wallet/transactionbuilder_test.go b/modules/wallet/transactionbuilder_test.go index 290bfedeb1..d4b548686b 100644 --- a/modules/wallet/transactionbuilder_test.go +++ b/modules/wallet/transactionbuilder_test.go @@ -451,6 +451,8 @@ func TestParallelBuilders(t *testing.T) { } } +// TestSignTransaction constructs a valid, signed transaction using the +// wallet's SpendableOutputs and SignTransaction methods. func TestSignTransaction(t *testing.T) { if testing.Short() { t.SkipNow() From 40506765c0e70a4428374acd6223589ade164d50 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 29 Mar 2018 17:54:39 -0400 Subject: [PATCH 13/24] document tosign types --- doc/API.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/API.md b/doc/API.md index ad7a0d9496..f22b753b4f 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1322,6 +1322,7 @@ specified. { "transaction": { }, // types.Transaction "tosign": { + // types.OutputID -> types.UnlockHash "3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678", "132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf" } From 64ff690b0407837cf374ab8d09266bc8546270f1 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 12 Apr 2018 01:46:57 -0400 Subject: [PATCH 14/24] account for unconfirmed txns in SpendableOutputs --- modules/wallet/transactionbuilder.go | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index 1b68d1c360..59e4773e8b 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -3,6 +3,7 @@ package wallet import ( "bytes" "errors" + "math" "sort" "github.com/NebulousLabs/Sia/crypto" @@ -642,6 +643,7 @@ func (w *Wallet) SpendableOutputs() []modules.SpendableOutput { // ensure durability of reported outputs w.syncDB() + // build initial list of confirmed outputs var outputs []modules.SpendableOutput dbForEachSiacoinOutput(w.dbTx, func(scoid types.SiacoinOutputID, sco types.SiacoinOutput) { outputs = append(outputs, modules.SpendableOutput{ @@ -660,6 +662,23 @@ func (w *Wallet) SpendableOutputs() []modules.SpendableOutput { }) }) + // don't include outputs marked as spent in pending transactions + pending := make(map[types.OutputID]struct{}) + for _, pt := range w.unconfirmedProcessedTransactions { + for _, input := range pt.Inputs { + if input.WalletAddress { + pending[input.ParentID] = struct{}{} + } + } + } + filtered := outputs[:0] + for _, o := range outputs { + if _, ok := pending[o.ID]; !ok { + filtered = append(filtered, o) + } + } + outputs = filtered + // set the confirmation height for each output outer: for i, o := range outputs { @@ -678,6 +697,21 @@ outer: } } + // add unconfirmed outputs + for _, pt := range w.unconfirmedProcessedTransactions { + for _, o := range pt.Outputs { + if o.WalletAddress { + outputs = append(outputs, modules.SpendableOutput{ + FundType: types.SpecifierSiacoinOutput, + ID: o.ID, + UnlockHash: o.RelatedAddress, + Value: o.Value, + ConfirmationHeight: types.BlockHeight(math.MaxUint64), // unconfirmed + }) + } + } + } + return outputs } From 78c2a13464a39e6c6503c40899c71f3d54d72cf3 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 17 Apr 2018 18:17:55 -0400 Subject: [PATCH 15/24] add wallet sign -raw flag, JSON by default --- cmd/siac/main.go | 2 ++ cmd/siac/walletcmd.go | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/siac/main.go b/cmd/siac/main.go index f860dd503d..2acb9026f9 100644 --- a/cmd/siac/main.go +++ b/cmd/siac/main.go @@ -23,6 +23,7 @@ var ( initPassword bool // supply a custom password when creating a wallet renterListVerbose bool // Show additional info about uploaded files. renterShowHistory bool // Show download history in addition to download queue. + walletSignRaw bool // Print signed transaction in base64-encoded binary. ) var ( @@ -285,6 +286,7 @@ func main() { walletLoadCmd.AddCommand(walletLoad033xCmd, walletLoadSeedCmd, walletLoadSiagCmd) walletSendCmd.AddCommand(walletSendSiacoinsCmd, walletSendSiafundsCmd) walletUnlockCmd.Flags().BoolVarP(&initPassword, "password", "p", false, "Display interactive password prompt even if SIA_WALLET_PASSWORD is set") + walletSignCmd.Flags().BoolVarP(&walletSignRaw, "raw", "", false, "Encode signed transaction as base64 instead of JSON") root.AddCommand(renterCmd) renterCmd.AddCommand(renterFilesDeleteCmd, renterFilesDownloadCmd, diff --git a/cmd/siac/walletcmd.go b/cmd/siac/walletcmd.go index e9a468bd8a..14ddc7d346 100644 --- a/cmd/siac/walletcmd.go +++ b/cmd/siac/walletcmd.go @@ -506,7 +506,12 @@ func walletsigncmd(txnJSON, toSignJSON string) { if err != nil { die("Failed to sign transaction:", err) } - fmt.Println(base64.StdEncoding.EncodeToString(encoding.Marshal(txn))) + if walletSignRaw { + base64.NewEncoder(base64.StdEncoding, os.Stdout).Write(encoding.Marshal(txn)) + } else { + json.NewEncoder(os.Stdout).Encode(txn) + } + fmt.Println() } // wallettransactionscmd lists all of the transactions related to the wallet, From d2c89fcd15d6949286e7419ac4521b88ca5861d2 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 17 Apr 2018 18:33:03 -0400 Subject: [PATCH 16/24] try /wallet/sign before doing keygen --- cmd/siac/walletcmd.go | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/cmd/siac/walletcmd.go b/cmd/siac/walletcmd.go index 14ddc7d346..2bddbf2bd2 100644 --- a/cmd/siac/walletcmd.go +++ b/cmd/siac/walletcmd.go @@ -157,8 +157,9 @@ Run 'wallet send --help' to see a list of available units.`, walletSignCmd = &cobra.Command{ Use: "sign [txn] [tosign]", Short: "Sign a transaction", - Long: `Sign the specified inputs of a transaction using one or more keys -derived from the supplied seed.`, + Long: `Sign the specified inputs of a transaction. If siad is running with an +unlocked wallet, the /wallet/sign API call will be used. Otherwise, sign will +prompt for the wallet seed, and the signing key(s) will be regenerated.`, Run: wrap(walletsigncmd), } @@ -494,18 +495,30 @@ func walletsigncmd(txnJSON, toSignJSON string) { die("Invalid transaction:", err) } - seedString, err := passwordPrompt("Seed: ") - if err != nil { - die("Reading seed failed:", err) - } - seed, err := modules.StringToSeed(seedString, mnemonics.English) - if err != nil { - die("Invalid seed:", err) - } - err = wallet.SignTransaction(&txn, seed, toSign) - if err != nil { - die("Failed to sign transaction:", err) + // try API first + params, _ := json.Marshal(api.WalletSignPOSTParams{Transaction: txn, ToSign: toSign}) + var wspr api.WalletSignPOSTResp + err = postResp("/wallet/sign", string(params), &wspr) + if err == nil { + txn = wspr.Transaction + } else { + // fallback to offline keygen + fmt.Println("Signing via API failed: either siad is not running, or your wallet is locked.") + fmt.Println("Enter your wallet seed to generate the signing key(s) now and sign without siad.") + seedString, err := passwordPrompt("Seed: ") + if err != nil { + die("Reading seed failed:", err) + } + seed, err := modules.StringToSeed(seedString, mnemonics.English) + if err != nil { + die("Invalid seed:", err) + } + err = wallet.SignTransaction(&txn, seed, toSign) + if err != nil { + die("Failed to sign transaction:", err) + } } + if walletSignRaw { base64.NewEncoder(base64.StdEncoding, os.Stdout).Write(encoding.Marshal(txn)) } else { From c5098c88f20f5e25cd60a6883c1091d26b3c7f6b Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 17 Apr 2018 19:56:05 -0400 Subject: [PATCH 17/24] include UnlockConditions in SpendableOutput --- cmd/siac/walletcmd.go | 2 +- doc/API.md | 18 ++++-- doc/api/Wallet.md | 21 ++++--- modules/wallet.go | 18 +++--- modules/wallet/transactionbuilder.go | 69 +++++++++++------------ modules/wallet/transactionbuilder_test.go | 7 +-- node/api/client/wallet.go | 2 +- node/api/wallet.go | 4 +- 8 files changed, 73 insertions(+), 68 deletions(-) diff --git a/cmd/siac/walletcmd.go b/cmd/siac/walletcmd.go index 2bddbf2bd2..a2a0f9a590 100644 --- a/cmd/siac/walletcmd.go +++ b/cmd/siac/walletcmd.go @@ -489,7 +489,7 @@ func walletsigncmd(txnJSON, toSignJSON string) { die("Invalid transaction:", err) } - var toSign map[types.OutputID]types.UnlockHash + var toSign []types.OutputID err = json.Unmarshal([]byte(toSignJSON), &toSign) if err != nil { die("Invalid transaction:", err) diff --git a/doc/API.md b/doc/API.md index f22b753b4f..3da3723dc7 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1321,11 +1321,10 @@ specified. ``` { "transaction": { }, // types.Transaction - "tosign": { - // types.OutputID -> types.UnlockHash - "3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678", - "132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf" - } + "tosign": [ + "3689bd3489679aabcde02e01345abcde", // types.OutputID + "132cee478a9bb98bdd23cf05376cdf2a" + ] } ``` @@ -1481,7 +1480,14 @@ returns a list of outputs that the wallet can spend. "id": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "fundtype": "siacoin output", "confirmationheight": 50000, - "unlockhash": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", + "unlockconditions": { + "timelock": 0, + "publickeys": [{ + "algorithm": "ed25519", + "key": "AADBM1ca/FyURfizmSukoUQ2S0GwXMit1iNSeYgrnhXOPAAA", + }], + "signaturesrequired": 1 + } "value": "1234" // big int } ] diff --git a/doc/api/Wallet.md b/doc/api/Wallet.md index 26c089b038..e5807fbf0b 100644 --- a/doc/api/Wallet.md +++ b/doc/api/Wallet.md @@ -512,11 +512,11 @@ specified. // unsigned transaction "transaction": { }, // types.Transaction - // inputs to sign; a mapping from OutputID to UnlockHash - "tosign": { - "3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678", - "132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf" - } + // inputs to sign; must correspond to ParentIDs of inputs in transaction + "tosign": [ + "3689bd3489679aabcde02e01345abcde", + "132cee478a9bb98bdd23cf05376cdf2a" + ] } ``` @@ -759,8 +759,15 @@ returns a list of outputs that the wallet can spend. // block height. "confirmationheight": 50000, - // UnlockHash of the output. - "unlockhash": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", + // UnlockConditions that must be satisfied to spend the output. + "unlockconditions": { + "timelock": 0, + "publickeys": [{ + "algorithm": "ed25519", + "key": "AADBM1ca/FyURfizmSukoUQ2S0GwXMit1iNSeYgrnhXOPAAA", + }], + "signaturesrequired": 1 + }, // Amount of funds in the output; hastings for siacoin outputs, and // siafunds for siafund outputs. diff --git a/modules/wallet.go b/modules/wallet.go index ec063a748a..a43047c19f 100644 --- a/modules/wallet.go +++ b/modules/wallet.go @@ -103,11 +103,11 @@ type ( // A SpendableOutput is a SiacoinOutput or SiafundOutput that the wallet // can spend. SpendableOutput struct { - ID types.OutputID `json:"id"` - FundType types.Specifier `json:"fundtype"` - UnlockHash types.UnlockHash `json:"unlockhash"` - Value types.Currency `json:"value"` - ConfirmationHeight types.BlockHeight `json:"confirmationheight"` + ID types.OutputID `json:"id"` + FundType types.Specifier `json:"fundtype"` + UnlockConditions types.UnlockConditions `json:"unlockconditions"` + Value types.Currency `json:"value"` + ConfirmationHeight types.BlockHeight `json:"confirmationheight"` } // TransactionBuilder is used to construct custom transactions. A transaction @@ -422,11 +422,9 @@ type ( // SpendableOutputs returns the outputs spendable by the wallet. SpendableOutputs() []SpendableOutput - // SignTransaction signs txn using secret keys known to the wallet. toSign - // maps the ParentID of each unsigned input to the UnlockHash of that input's - // desired UnlockConditions. SignTransaction fills in the UnlockConditions for - // each such input and adds a corresponding signature. - SignTransaction(txn *types.Transaction, toSign map[types.OutputID]types.UnlockHash) error + // SignTransaction signs txn using secret keys known to the wallet, adding a + // TransactionSignature for each input whose ParentID is specified in toSign. + SignTransaction(txn *types.Transaction, toSign []types.OutputID) error } // WalletSettings control the behavior of the Wallet. diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index 59e4773e8b..3c277bce46 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -647,18 +647,18 @@ func (w *Wallet) SpendableOutputs() []modules.SpendableOutput { var outputs []modules.SpendableOutput dbForEachSiacoinOutput(w.dbTx, func(scoid types.SiacoinOutputID, sco types.SiacoinOutput) { outputs = append(outputs, modules.SpendableOutput{ - FundType: types.SpecifierSiacoinOutput, - ID: types.OutputID(scoid), - UnlockHash: sco.UnlockHash, - Value: sco.Value, + FundType: types.SpecifierSiacoinOutput, + ID: types.OutputID(scoid), + UnlockConditions: w.keys[sco.UnlockHash].UnlockConditions, + Value: sco.Value, }) }) dbForEachSiafundOutput(w.dbTx, func(sfoid types.SiafundOutputID, sfo types.SiafundOutput) { outputs = append(outputs, modules.SpendableOutput{ - FundType: types.SpecifierSiafundOutput, - ID: types.OutputID(sfoid), - UnlockHash: sfo.UnlockHash, - Value: sfo.Value, + FundType: types.SpecifierSiafundOutput, + ID: types.OutputID(sfoid), + UnlockConditions: w.keys[sfo.UnlockHash].UnlockConditions, + Value: sfo.Value, }) }) @@ -682,7 +682,7 @@ func (w *Wallet) SpendableOutputs() []modules.SpendableOutput { // set the confirmation height for each output outer: for i, o := range outputs { - txnIndices, _ := dbGetAddrTransactions(w.dbTx, o.UnlockHash) + txnIndices, _ := dbGetAddrTransactions(w.dbTx, o.UnlockConditions.UnlockHash()) for _, j := range txnIndices { pt, err := dbGetProcessedTransaction(w.dbTx, j) if err != nil { @@ -704,7 +704,7 @@ outer: outputs = append(outputs, modules.SpendableOutput{ FundType: types.SpecifierSiacoinOutput, ID: o.ID, - UnlockHash: o.RelatedAddress, + UnlockConditions: w.keys[o.RelatedAddress].UnlockConditions, Value: o.Value, ConfirmationHeight: types.BlockHeight(math.MaxUint64), // unconfirmed }) @@ -715,11 +715,9 @@ outer: return outputs } -// SignTransaction signs txn using secret keys known to the wallet. toSign -// maps the ParentID of each unsigned input to the UnlockHash of that input's -// desired UnlockConditions. SignTransaction fills in the UnlockConditions for -// each such input and adds a corresponding signature. -func (w *Wallet) SignTransaction(txn *types.Transaction, toSign map[types.OutputID]types.UnlockHash) error { +// SignTransaction signs txn using secret keys known to the wallet, adding a +// TransactionSignature for each input whose ParentID is specified in toSign. +func (w *Wallet) SignTransaction(txn *types.Transaction, toSign []types.OutputID) error { w.mu.Lock() defer w.mu.Unlock() if !w.unlocked { @@ -728,15 +726,13 @@ func (w *Wallet) SignTransaction(txn *types.Transaction, toSign map[types.Output return signTransaction(txn, w.keys, toSign) } -// SignTransaction signs txn using secret keys derived from seed. toSign maps -// the ParentID of each unsigned input to the UnlockHash of that input's -// desired UnlockConditions. SignTransaction fills in the UnlockConditions for -// each such input and adds a corresponding signature. +// SignTransaction signs txn using secret keys known to the wallet, adding a +// TransactionSignature for each input whose ParentID is specified in toSign. // // SignTransaction must derive all of the keys from scratch, so it is // appreciably slower than calling the Wallet.SignTransaction method. Only the // first 1 million keys are derived. -func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign map[types.OutputID]types.UnlockHash) error { +func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign []types.OutputID) error { // generate keys in batches up to 1e6 before giving up keys := make(map[types.UnlockHash]spendableKey, 1e6) var keyIndex uint64 @@ -755,41 +751,40 @@ func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign map[types // signTransaction signs the specified inputs of txn using the specified keys. // It returns an error if any of the specified inputs cannot be signed. -func signTransaction(txn *types.Transaction, keys map[types.UnlockHash]spendableKey, toSign map[types.OutputID]types.UnlockHash) error { +func signTransaction(txn *types.Transaction, keys map[types.UnlockHash]spendableKey, toSign []types.OutputID) error { + signing := make(map[types.OutputID]bool) + for _, oid := range toSign { + signing[oid] = true + } + signed := 0 - for i, sci := range txn.SiacoinInputs { - uh, ok := toSign[types.OutputID(sci.ParentID)] - if !ok { - // not signing this input + for _, sci := range txn.SiacoinInputs { + if !signing[types.OutputID(sci.ParentID)] { continue } // lookup the signing key(s) - sk, ok := keys[uh] + sk, ok := keys[sci.UnlockConditions.UnlockHash()] if !ok { - return errors.New("could not locate signing key for " + uh.String()) + return errors.New("could not locate signing key for input" + sci.ParentID.String()) } - txn.SiacoinInputs[i].UnlockConditions = sk.UnlockConditions cf := types.CoveredFields{WholeTransaction: true} - addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sci.ParentID), sk) + addSignatures(txn, cf, sci.UnlockConditions, crypto.Hash(sci.ParentID), sk) signed++ } - for i, sfi := range txn.SiafundInputs { - uh, ok := toSign[types.OutputID(sfi.ParentID)] - if !ok { - // not signing this input + for _, sfi := range txn.SiafundInputs { + if !signing[types.OutputID(sfi.ParentID)] { continue } // lookup the signing key(s) - sk, ok := keys[uh] + sk, ok := keys[sfi.UnlockConditions.UnlockHash()] if !ok { - return errors.New("could not locate signing key for " + uh.String()) + return errors.New("could not locate signing key for input" + sfi.ParentID.String()) } - txn.SiafundInputs[i].UnlockConditions = sk.UnlockConditions cf := types.CoveredFields{WholeTransaction: true} addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sfi.ParentID), sk) signed++ } - if signed != len(toSign) { + if signed != len(signing) { return errors.New("toSign references OutputIDs not present in transaction") } return nil diff --git a/modules/wallet/transactionbuilder_test.go b/modules/wallet/transactionbuilder_test.go index d4b548686b..bca5094a80 100644 --- a/modules/wallet/transactionbuilder_test.go +++ b/modules/wallet/transactionbuilder_test.go @@ -469,7 +469,8 @@ func TestSignTransaction(t *testing.T) { // create a transaction that sends an output to the void txn := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{ - ParentID: types.SiacoinOutputID(outputs[0].ID), + ParentID: types.SiacoinOutputID(outputs[0].ID), + UnlockConditions: outputs[0].UnlockConditions, }}, SiacoinOutputs: []types.SiacoinOutput{{ Value: outputs[0].Value, @@ -478,9 +479,7 @@ func TestSignTransaction(t *testing.T) { } // sign the transaction - err = wt.wallet.SignTransaction(&txn, map[types.OutputID]types.UnlockHash{ - outputs[0].ID: outputs[0].UnlockHash, - }) + err = wt.wallet.SignTransaction(&txn, []types.OutputID{outputs[0].ID}) if err != nil { t.Fatal(err) } diff --git a/node/api/client/wallet.go b/node/api/client/wallet.go index 026977058a..2bf34c4527 100644 --- a/node/api/client/wallet.go +++ b/node/api/client/wallet.go @@ -57,7 +57,7 @@ func (c *Client) WalletSiacoinsPost(amount types.Currency, destination types.Unl } // WalletSignPost uses the /wallet/sign api endpoint to sign a transaction. -func (c *Client) WalletSignPost(txn types.Transaction, toSign map[types.OutputID]types.UnlockHash) (wspr api.WalletSignPOSTResp, err error) { +func (c *Client) WalletSignPost(txn types.Transaction, toSign []types.OutputID) (wspr api.WalletSignPOSTResp, err error) { buf := new(bytes.Buffer) err = json.NewEncoder(buf).Encode(api.WalletSignPOSTParams{ Transaction: txn, diff --git a/node/api/wallet.go b/node/api/wallet.go index 92247ccbf2..546bbb3760 100644 --- a/node/api/wallet.go +++ b/node/api/wallet.go @@ -66,8 +66,8 @@ type ( // WalletSignPOSTParams contains the unsigned transaction and a set of // inputs to sign. WalletSignPOSTParams struct { - Transaction types.Transaction `json:"transaction"` - ToSign map[types.OutputID]types.UnlockHash `json:"tosign"` + Transaction types.Transaction `json:"transaction"` + ToSign []types.OutputID `json:"tosign"` } // WalletSignPOSTResp contains the signed transaction. From c1c14d72f29e5f6ba78699b3dc8a92775cf8bd49 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Wed, 18 Apr 2018 16:23:37 -0400 Subject: [PATCH 18/24] Revert "include UnlockConditions in SpendableOutput" This reverts commit c5098c88f20f5e25cd60a6883c1091d26b3c7f6b. --- cmd/siac/walletcmd.go | 2 +- doc/API.md | 18 ++---- doc/api/Wallet.md | 21 +++---- modules/wallet.go | 18 +++--- modules/wallet/transactionbuilder.go | 69 ++++++++++++----------- modules/wallet/transactionbuilder_test.go | 7 ++- node/api/client/wallet.go | 2 +- node/api/wallet.go | 4 +- 8 files changed, 68 insertions(+), 73 deletions(-) diff --git a/cmd/siac/walletcmd.go b/cmd/siac/walletcmd.go index a2a0f9a590..2bddbf2bd2 100644 --- a/cmd/siac/walletcmd.go +++ b/cmd/siac/walletcmd.go @@ -489,7 +489,7 @@ func walletsigncmd(txnJSON, toSignJSON string) { die("Invalid transaction:", err) } - var toSign []types.OutputID + var toSign map[types.OutputID]types.UnlockHash err = json.Unmarshal([]byte(toSignJSON), &toSign) if err != nil { die("Invalid transaction:", err) diff --git a/doc/API.md b/doc/API.md index 3da3723dc7..f22b753b4f 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1321,10 +1321,11 @@ specified. ``` { "transaction": { }, // types.Transaction - "tosign": [ - "3689bd3489679aabcde02e01345abcde", // types.OutputID - "132cee478a9bb98bdd23cf05376cdf2a" - ] + "tosign": { + // types.OutputID -> types.UnlockHash + "3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678", + "132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf" + } } ``` @@ -1480,14 +1481,7 @@ returns a list of outputs that the wallet can spend. "id": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "fundtype": "siacoin output", "confirmationheight": 50000, - "unlockconditions": { - "timelock": 0, - "publickeys": [{ - "algorithm": "ed25519", - "key": "AADBM1ca/FyURfizmSukoUQ2S0GwXMit1iNSeYgrnhXOPAAA", - }], - "signaturesrequired": 1 - } + "unlockhash": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", "value": "1234" // big int } ] diff --git a/doc/api/Wallet.md b/doc/api/Wallet.md index e5807fbf0b..26c089b038 100644 --- a/doc/api/Wallet.md +++ b/doc/api/Wallet.md @@ -512,11 +512,11 @@ specified. // unsigned transaction "transaction": { }, // types.Transaction - // inputs to sign; must correspond to ParentIDs of inputs in transaction - "tosign": [ - "3689bd3489679aabcde02e01345abcde", - "132cee478a9bb98bdd23cf05376cdf2a" - ] + // inputs to sign; a mapping from OutputID to UnlockHash + "tosign": { + "3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678", + "132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf" + } } ``` @@ -759,15 +759,8 @@ returns a list of outputs that the wallet can spend. // block height. "confirmationheight": 50000, - // UnlockConditions that must be satisfied to spend the output. - "unlockconditions": { - "timelock": 0, - "publickeys": [{ - "algorithm": "ed25519", - "key": "AADBM1ca/FyURfizmSukoUQ2S0GwXMit1iNSeYgrnhXOPAAA", - }], - "signaturesrequired": 1 - }, + // UnlockHash of the output. + "unlockhash": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", // Amount of funds in the output; hastings for siacoin outputs, and // siafunds for siafund outputs. diff --git a/modules/wallet.go b/modules/wallet.go index a43047c19f..ec063a748a 100644 --- a/modules/wallet.go +++ b/modules/wallet.go @@ -103,11 +103,11 @@ type ( // A SpendableOutput is a SiacoinOutput or SiafundOutput that the wallet // can spend. SpendableOutput struct { - ID types.OutputID `json:"id"` - FundType types.Specifier `json:"fundtype"` - UnlockConditions types.UnlockConditions `json:"unlockconditions"` - Value types.Currency `json:"value"` - ConfirmationHeight types.BlockHeight `json:"confirmationheight"` + ID types.OutputID `json:"id"` + FundType types.Specifier `json:"fundtype"` + UnlockHash types.UnlockHash `json:"unlockhash"` + Value types.Currency `json:"value"` + ConfirmationHeight types.BlockHeight `json:"confirmationheight"` } // TransactionBuilder is used to construct custom transactions. A transaction @@ -422,9 +422,11 @@ type ( // SpendableOutputs returns the outputs spendable by the wallet. SpendableOutputs() []SpendableOutput - // SignTransaction signs txn using secret keys known to the wallet, adding a - // TransactionSignature for each input whose ParentID is specified in toSign. - SignTransaction(txn *types.Transaction, toSign []types.OutputID) error + // SignTransaction signs txn using secret keys known to the wallet. toSign + // maps the ParentID of each unsigned input to the UnlockHash of that input's + // desired UnlockConditions. SignTransaction fills in the UnlockConditions for + // each such input and adds a corresponding signature. + SignTransaction(txn *types.Transaction, toSign map[types.OutputID]types.UnlockHash) error } // WalletSettings control the behavior of the Wallet. diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index 3c277bce46..59e4773e8b 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -647,18 +647,18 @@ func (w *Wallet) SpendableOutputs() []modules.SpendableOutput { var outputs []modules.SpendableOutput dbForEachSiacoinOutput(w.dbTx, func(scoid types.SiacoinOutputID, sco types.SiacoinOutput) { outputs = append(outputs, modules.SpendableOutput{ - FundType: types.SpecifierSiacoinOutput, - ID: types.OutputID(scoid), - UnlockConditions: w.keys[sco.UnlockHash].UnlockConditions, - Value: sco.Value, + FundType: types.SpecifierSiacoinOutput, + ID: types.OutputID(scoid), + UnlockHash: sco.UnlockHash, + Value: sco.Value, }) }) dbForEachSiafundOutput(w.dbTx, func(sfoid types.SiafundOutputID, sfo types.SiafundOutput) { outputs = append(outputs, modules.SpendableOutput{ - FundType: types.SpecifierSiafundOutput, - ID: types.OutputID(sfoid), - UnlockConditions: w.keys[sfo.UnlockHash].UnlockConditions, - Value: sfo.Value, + FundType: types.SpecifierSiafundOutput, + ID: types.OutputID(sfoid), + UnlockHash: sfo.UnlockHash, + Value: sfo.Value, }) }) @@ -682,7 +682,7 @@ func (w *Wallet) SpendableOutputs() []modules.SpendableOutput { // set the confirmation height for each output outer: for i, o := range outputs { - txnIndices, _ := dbGetAddrTransactions(w.dbTx, o.UnlockConditions.UnlockHash()) + txnIndices, _ := dbGetAddrTransactions(w.dbTx, o.UnlockHash) for _, j := range txnIndices { pt, err := dbGetProcessedTransaction(w.dbTx, j) if err != nil { @@ -704,7 +704,7 @@ outer: outputs = append(outputs, modules.SpendableOutput{ FundType: types.SpecifierSiacoinOutput, ID: o.ID, - UnlockConditions: w.keys[o.RelatedAddress].UnlockConditions, + UnlockHash: o.RelatedAddress, Value: o.Value, ConfirmationHeight: types.BlockHeight(math.MaxUint64), // unconfirmed }) @@ -715,9 +715,11 @@ outer: return outputs } -// SignTransaction signs txn using secret keys known to the wallet, adding a -// TransactionSignature for each input whose ParentID is specified in toSign. -func (w *Wallet) SignTransaction(txn *types.Transaction, toSign []types.OutputID) error { +// SignTransaction signs txn using secret keys known to the wallet. toSign +// maps the ParentID of each unsigned input to the UnlockHash of that input's +// desired UnlockConditions. SignTransaction fills in the UnlockConditions for +// each such input and adds a corresponding signature. +func (w *Wallet) SignTransaction(txn *types.Transaction, toSign map[types.OutputID]types.UnlockHash) error { w.mu.Lock() defer w.mu.Unlock() if !w.unlocked { @@ -726,13 +728,15 @@ func (w *Wallet) SignTransaction(txn *types.Transaction, toSign []types.OutputID return signTransaction(txn, w.keys, toSign) } -// SignTransaction signs txn using secret keys known to the wallet, adding a -// TransactionSignature for each input whose ParentID is specified in toSign. +// SignTransaction signs txn using secret keys derived from seed. toSign maps +// the ParentID of each unsigned input to the UnlockHash of that input's +// desired UnlockConditions. SignTransaction fills in the UnlockConditions for +// each such input and adds a corresponding signature. // // SignTransaction must derive all of the keys from scratch, so it is // appreciably slower than calling the Wallet.SignTransaction method. Only the // first 1 million keys are derived. -func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign []types.OutputID) error { +func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign map[types.OutputID]types.UnlockHash) error { // generate keys in batches up to 1e6 before giving up keys := make(map[types.UnlockHash]spendableKey, 1e6) var keyIndex uint64 @@ -751,40 +755,41 @@ func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign []types.O // signTransaction signs the specified inputs of txn using the specified keys. // It returns an error if any of the specified inputs cannot be signed. -func signTransaction(txn *types.Transaction, keys map[types.UnlockHash]spendableKey, toSign []types.OutputID) error { - signing := make(map[types.OutputID]bool) - for _, oid := range toSign { - signing[oid] = true - } - +func signTransaction(txn *types.Transaction, keys map[types.UnlockHash]spendableKey, toSign map[types.OutputID]types.UnlockHash) error { signed := 0 - for _, sci := range txn.SiacoinInputs { - if !signing[types.OutputID(sci.ParentID)] { + for i, sci := range txn.SiacoinInputs { + uh, ok := toSign[types.OutputID(sci.ParentID)] + if !ok { + // not signing this input continue } // lookup the signing key(s) - sk, ok := keys[sci.UnlockConditions.UnlockHash()] + sk, ok := keys[uh] if !ok { - return errors.New("could not locate signing key for input" + sci.ParentID.String()) + return errors.New("could not locate signing key for " + uh.String()) } + txn.SiacoinInputs[i].UnlockConditions = sk.UnlockConditions cf := types.CoveredFields{WholeTransaction: true} - addSignatures(txn, cf, sci.UnlockConditions, crypto.Hash(sci.ParentID), sk) + addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sci.ParentID), sk) signed++ } - for _, sfi := range txn.SiafundInputs { - if !signing[types.OutputID(sfi.ParentID)] { + for i, sfi := range txn.SiafundInputs { + uh, ok := toSign[types.OutputID(sfi.ParentID)] + if !ok { + // not signing this input continue } // lookup the signing key(s) - sk, ok := keys[sfi.UnlockConditions.UnlockHash()] + sk, ok := keys[uh] if !ok { - return errors.New("could not locate signing key for input" + sfi.ParentID.String()) + return errors.New("could not locate signing key for " + uh.String()) } + txn.SiafundInputs[i].UnlockConditions = sk.UnlockConditions cf := types.CoveredFields{WholeTransaction: true} addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sfi.ParentID), sk) signed++ } - if signed != len(signing) { + if signed != len(toSign) { return errors.New("toSign references OutputIDs not present in transaction") } return nil diff --git a/modules/wallet/transactionbuilder_test.go b/modules/wallet/transactionbuilder_test.go index bca5094a80..d4b548686b 100644 --- a/modules/wallet/transactionbuilder_test.go +++ b/modules/wallet/transactionbuilder_test.go @@ -469,8 +469,7 @@ func TestSignTransaction(t *testing.T) { // create a transaction that sends an output to the void txn := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{ - ParentID: types.SiacoinOutputID(outputs[0].ID), - UnlockConditions: outputs[0].UnlockConditions, + ParentID: types.SiacoinOutputID(outputs[0].ID), }}, SiacoinOutputs: []types.SiacoinOutput{{ Value: outputs[0].Value, @@ -479,7 +478,9 @@ func TestSignTransaction(t *testing.T) { } // sign the transaction - err = wt.wallet.SignTransaction(&txn, []types.OutputID{outputs[0].ID}) + err = wt.wallet.SignTransaction(&txn, map[types.OutputID]types.UnlockHash{ + outputs[0].ID: outputs[0].UnlockHash, + }) if err != nil { t.Fatal(err) } diff --git a/node/api/client/wallet.go b/node/api/client/wallet.go index 2bf34c4527..026977058a 100644 --- a/node/api/client/wallet.go +++ b/node/api/client/wallet.go @@ -57,7 +57,7 @@ func (c *Client) WalletSiacoinsPost(amount types.Currency, destination types.Unl } // WalletSignPost uses the /wallet/sign api endpoint to sign a transaction. -func (c *Client) WalletSignPost(txn types.Transaction, toSign []types.OutputID) (wspr api.WalletSignPOSTResp, err error) { +func (c *Client) WalletSignPost(txn types.Transaction, toSign map[types.OutputID]types.UnlockHash) (wspr api.WalletSignPOSTResp, err error) { buf := new(bytes.Buffer) err = json.NewEncoder(buf).Encode(api.WalletSignPOSTParams{ Transaction: txn, diff --git a/node/api/wallet.go b/node/api/wallet.go index 546bbb3760..92247ccbf2 100644 --- a/node/api/wallet.go +++ b/node/api/wallet.go @@ -66,8 +66,8 @@ type ( // WalletSignPOSTParams contains the unsigned transaction and a set of // inputs to sign. WalletSignPOSTParams struct { - Transaction types.Transaction `json:"transaction"` - ToSign []types.OutputID `json:"tosign"` + Transaction types.Transaction `json:"transaction"` + ToSign map[types.OutputID]types.UnlockHash `json:"tosign"` } // WalletSignPOSTResp contains the signed transaction. From 6b22c8744d37c99d7a76f612d5d25d8721ab8af2 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 15 May 2018 23:01:52 -0400 Subject: [PATCH 19/24] don't include unconfirmed outputs that may be spent --- modules/wallet/transactionbuilder.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index 33f33938c9..6ccce86ada 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -745,10 +745,11 @@ outer: } } - // add unconfirmed outputs + // add unconfirmed outputs, except those that are spent in pending + // transactions for _, pt := range w.unconfirmedProcessedTransactions { for _, o := range pt.Outputs { - if o.WalletAddress { + if _, ok := pending[o.ID]; !ok && o.WalletAddress { outputs = append(outputs, modules.SpendableOutput{ FundType: types.SpecifierSiacoinOutput, ID: o.ID, From 477a4972924251058b5f0ec55d515c183faa96cc Mon Sep 17 00:00:00 2001 From: lukechampine Date: Wed, 30 May 2018 19:37:22 -0400 Subject: [PATCH 20/24] add UnlockConditions to SpendableOutput --- doc/API.md | 4 ++++ doc/api/Wallet.md | 13 ++++++++++++- modules/wallet.go | 11 ++++++----- modules/wallet/transactionbuilder.go | 11 +++++++++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/doc/API.md b/doc/API.md index f2dcd47254..0fcef4738e 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1659,6 +1659,10 @@ returns a list of outputs that the wallet can spend. "id": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "fundtype": "siacoin output", "confirmationheight": 50000, + "unlockconditions": { + "publickeys": [{"algorithm":"ed25519","key":"/XUGj8PxMDkqdae6Js6ubcERxfxnXN7XPjZyANBZH1I="}], + "signaturesrequired": 1 + }, "unlockhash": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", "value": "1234" // big int } diff --git a/doc/api/Wallet.md b/doc/api/Wallet.md index 26c089b038..eea88357f2 100644 --- a/doc/api/Wallet.md +++ b/doc/api/Wallet.md @@ -759,7 +759,18 @@ returns a list of outputs that the wallet can spend. // block height. "confirmationheight": 50000, - // UnlockHash of the output. + // Unlock conditions of the output. These conditions must be met in + // order to spend the output. + "unlockconditions": { + "timelock": 0, + "publickeys": [{ + "algorithm": "ed25519", + "key": "/XUGj8PxMDkqdae6Js6ubcERxfxnXN7XPjZyANBZH1I=" + }], + "signaturesrequired": 1 + }, + + // Hash of the unlock conditions, commonly known as the "address". "unlockhash": "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", // Amount of funds in the output; hastings for siacoin outputs, and diff --git a/modules/wallet.go b/modules/wallet.go index 67aa503aa8..603c92984c 100644 --- a/modules/wallet.go +++ b/modules/wallet.go @@ -107,11 +107,12 @@ type ( // A SpendableOutput is a SiacoinOutput or SiafundOutput that the wallet // can spend. SpendableOutput struct { - ID types.OutputID `json:"id"` - FundType types.Specifier `json:"fundtype"` - UnlockHash types.UnlockHash `json:"unlockhash"` - Value types.Currency `json:"value"` - ConfirmationHeight types.BlockHeight `json:"confirmationheight"` + ID types.OutputID `json:"id"` + FundType types.Specifier `json:"fundtype"` + UnlockConditions types.UnlockConditions `json:"unlockconditions"` + UnlockHash types.UnlockHash `json:"unlockhash"` + Value types.Currency `json:"value"` + ConfirmationHeight types.BlockHeight `json:"confirmationheight"` } // TransactionBuilder is used to construct custom transactions. A transaction diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index 6ccce86ada..83a213c970 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -761,6 +761,17 @@ outer: } } + // add UnlockConditions for each output. If we don't know the + // UnlockConditions, they aren't actually spendable. + filtered = outputs[:0] + for _, o := range outputs { + if sk, ok := w.keys[o.UnlockHash]; ok { + o.UnlockConditions = sk.UnlockConditions + filtered = append(filtered, o) + } + } + outputs = filtered + return outputs } From 83967e1b12d663652e947fd74a9550e343d86041 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 12 Jul 2018 13:10:15 -0400 Subject: [PATCH 21/24] fix TransactionPoolRawPost signature --- node/api/client/transactionpool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/api/client/transactionpool.go b/node/api/client/transactionpool.go index dd78a52e39..19464cd95d 100644 --- a/node/api/client/transactionpool.go +++ b/node/api/client/transactionpool.go @@ -29,7 +29,7 @@ func (c *Client) TransactionPoolFeeGet() (tfg api.TpoolFeeGET, err error) { // TransactionPoolRawPost uses the /tpool/raw endpoint to send a raw // transaction to the transaction pool. -func (c *Client) TransactionPoolRawPost(txn types.Transaction, parents types.Transaction) (err error) { +func (c *Client) TransactionPoolRawPost(txn types.Transaction, parents []types.Transaction) (err error) { values := url.Values{} values.Set("transaction", string(encoding.Marshal(txn))) values.Set("parents", string(encoding.Marshal(parents))) From 4fccafdf166b82eec231c39dc9a5b4f2ce5e8ae9 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 12 Jul 2018 13:11:07 -0400 Subject: [PATCH 22/24] add wallet broadcast cmd --- cmd/siac/main.go | 7 ++++--- cmd/siac/walletcmd.go | 32 +++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/cmd/siac/main.go b/cmd/siac/main.go index d56532b92d..c916f75b22 100644 --- a/cmd/siac/main.go +++ b/cmd/siac/main.go @@ -21,7 +21,7 @@ var ( renterDownloadAsync bool // Downloads files asynchronously renterListVerbose bool // Show additional info about uploaded files. renterShowHistory bool // Show download history in addition to download queue. - walletSignRaw bool // Print signed transaction in base64-encoded binary. + walletRawTxn bool // Encode/decode transactions in base64-encoded binary. ) var ( @@ -108,14 +108,15 @@ func main() { root.AddCommand(walletCmd) walletCmd.AddCommand(walletAddressCmd, walletAddressesCmd, walletChangepasswordCmd, walletInitCmd, walletInitSeedCmd, walletLoadCmd, walletLockCmd, walletSeedsCmd, walletSendCmd, walletSweepCmd, walletSignCmd, - walletBalanceCmd, walletTransactionsCmd, walletUnlockCmd) + walletBalanceCmd, walletBroadcastCmd, walletTransactionsCmd, walletUnlockCmd) walletInitCmd.Flags().BoolVarP(&initPassword, "password", "p", false, "Prompt for a custom password") walletInitCmd.Flags().BoolVarP(&initForce, "force", "", false, "destroy the existing wallet and re-encrypt") walletInitSeedCmd.Flags().BoolVarP(&initForce, "force", "", false, "destroy the existing wallet") walletLoadCmd.AddCommand(walletLoad033xCmd, walletLoadSeedCmd, walletLoadSiagCmd) walletSendCmd.AddCommand(walletSendSiacoinsCmd, walletSendSiafundsCmd) walletUnlockCmd.Flags().BoolVarP(&initPassword, "password", "p", false, "Display interactive password prompt even if SIA_WALLET_PASSWORD is set") - walletSignCmd.Flags().BoolVarP(&walletSignRaw, "raw", "", false, "Encode signed transaction as base64 instead of JSON") + walletBroadcastCmd.Flags().BoolVarP(&walletRawTxn, "raw", "", false, "Decode transaction as base64 instead of JSON") + walletSignCmd.Flags().BoolVarP(&walletRawTxn, "raw", "", false, "Encode signed transaction as base64 instead of JSON") root.AddCommand(renterCmd) renterCmd.AddCommand(renterFilesDeleteCmd, renterFilesDownloadCmd, diff --git a/cmd/siac/walletcmd.go b/cmd/siac/walletcmd.go index 6b06078ea8..4f7b3fe811 100644 --- a/cmd/siac/walletcmd.go +++ b/cmd/siac/walletcmd.go @@ -43,6 +43,13 @@ var ( Run: wrap(walletbalancecmd), } + walletBroadcastCmd = &cobra.Command{ + Use: "broadcast [txn]", + Short: "Broadcast a transaction", + Long: "Broadcast a transaction to connected peers. The transaction must be valid.", + Run: wrap(walletbroadcastcmd), + } + walletChangepasswordCmd = &cobra.Command{ Use: "change-password", Short: "Change the wallet password", @@ -465,6 +472,29 @@ Estimated Fee: %v / KB fees.Maximum.Mul64(1e3).HumanString()) } +// walletbroadcastcmd broadcasts a transaction. +func walletbroadcastcmd(txnStr string) { + var txn types.Transaction + var err error + if walletRawTxn { + var txnBytes []byte + txnBytes, err = base64.StdEncoding.DecodeString(txnStr) + if err == nil { + err = encoding.Unmarshal(txnBytes, &txn) + } + } else { + err = json.Unmarshal([]byte(txnStr), &txn) + } + if err != nil { + die("Could not decode transaction:", err) + } + err = httpClient.TransactionPoolRawPost(txn, nil) + if err != nil { + die("Could not broadcast transaction:", err) + } + fmt.Println("Transaction broadcast successfully") +} + // walletsweepcmd sweeps coins and funds from a seed. func walletsweepcmd() { seed, err := passwordPrompt("Seed: ") @@ -515,7 +545,7 @@ func walletsigncmd(txnJSON, toSignJSON string) { } } - if walletSignRaw { + if walletRawTxn { base64.NewEncoder(base64.StdEncoding, os.Stdout).Write(encoding.Marshal(txn)) } else { json.NewEncoder(os.Stdout).Encode(txn) From c97a9d755e30d886ee1446fb4cf4032ab2e03d0f Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 12 Jul 2018 13:51:53 -0400 Subject: [PATCH 23/24] more helpful signature decoding error --- types/signatures.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/types/signatures.go b/types/signatures.go index da2441e656..0a3a45766f 100644 --- a/types/signatures.go +++ b/types/signatures.go @@ -382,19 +382,12 @@ func (t *Transaction) validSignatures(currentHeight BlockHeight) error { case SignatureEd25519: // Decode the public key and signature. var edPK crypto.PublicKey - err := encoding.Unmarshal([]byte(publicKey.Key), &edPK) - if err != nil { - return err - } - var edSig [crypto.SignatureSize]byte - err = encoding.Unmarshal([]byte(sig.Signature), &edSig) - if err != nil { - return err - } - cryptoSig := crypto.Signature(edSig) + copy(edPK[:], publicKey.Key) + var edSig crypto.Signature + copy(edSig[:], sig.Signature) sigHash := t.SigHash(i) - err = crypto.VerifyHash(sigHash, edPK, cryptoSig) + err = crypto.VerifyHash(sigHash, edPK, edSig) if err != nil { return err } From f174e413dfae3802b3ae0bf9e3b9580ccb36855e Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 12 Jul 2018 15:02:15 -0400 Subject: [PATCH 24/24] overhaul SignTransaction In a nutshell, SignTransaction now does less work: it requires the user to fill out the UnlockConditions and TransactionSignatures of the transaction, whereas before it would fill them in itself. This is a better approach because it makes the most common operation -- signing all the inputs that the wallet controls -- dead simple. And requiring the user to fill out the unlock conditions isn't a big deal, because they can get those from /wallet/unspent. Lastly, if the user is responsible for filling out the TransactionSignature fields, they can control precisely what gets signed. --- cmd/siac/walletcmd.go | 22 ++-- doc/API.md | 9 +- doc/api/Wallet.md | 4 +- modules/wallet.go | 9 +- modules/wallet/transactionbuilder.go | 127 +++++++++++++++------- modules/wallet/transactionbuilder_test.go | 11 +- node/api/client/wallet.go | 3 +- node/api/wallet.go | 4 +- siatest/wallet/wallet_test.go | 11 +- 9 files changed, 131 insertions(+), 69 deletions(-) diff --git a/cmd/siac/walletcmd.go b/cmd/siac/walletcmd.go index 4f7b3fe811..acc2219899 100644 --- a/cmd/siac/walletcmd.go +++ b/cmd/siac/walletcmd.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" + "github.com/NebulousLabs/Sia/crypto" "github.com/NebulousLabs/Sia/encoding" "github.com/NebulousLabs/Sia/modules" "github.com/NebulousLabs/Sia/modules/wallet" @@ -167,7 +168,7 @@ Run 'wallet send --help' to see a list of available units.`, Long: `Sign the specified inputs of a transaction. If siad is running with an unlocked wallet, the /wallet/sign API call will be used. Otherwise, sign will prompt for the wallet seed, and the signing key(s) will be regenerated.`, - Run: wrap(walletsigncmd), + Run: walletsigncmd, } walletSweepCmd = &cobra.Command{ @@ -510,17 +511,24 @@ func walletsweepcmd() { } // walletsigncmd signs a transaction. -func walletsigncmd(txnJSON, toSignJSON string) { +func walletsigncmd(cmd *cobra.Command, args []string) { + if len(args) < 1 || len(args) > 2 { + cmd.UsageFunc()(cmd) + os.Exit(exitCodeUsage) + } + var txn types.Transaction - err := json.Unmarshal([]byte(txnJSON), &txn) + err := json.Unmarshal([]byte(args[0]), &txn) if err != nil { die("Invalid transaction:", err) } - var toSign map[types.OutputID]types.UnlockHash - err = json.Unmarshal([]byte(toSignJSON), &toSign) - if err != nil { - die("Invalid transaction:", err) + var toSign []crypto.Hash + if len(args) == 2 { + err = json.Unmarshal([]byte(args[1]), &toSign) + if err != nil { + die("Invalid transaction:", err) + } } // try API first diff --git a/doc/API.md b/doc/API.md index 1c4569fdd5..6e13dfcc7e 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1543,11 +1543,10 @@ specified. ``` { "transaction": { }, // types.Transaction - "tosign": { - // types.OutputID -> types.UnlockHash - "3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678", - "132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf" - } + "tosign": [ + "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "abcdef0123456789abcdef0123456789abcd1234567890ef0123456789abcdef" + ] } ``` diff --git a/doc/api/Wallet.md b/doc/api/Wallet.md index cdea8ce151..359aaa4146 100644 --- a/doc/api/Wallet.md +++ b/doc/api/Wallet.md @@ -514,8 +514,8 @@ specified. // inputs to sign; a mapping from OutputID to UnlockHash "tosign": { - "3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678", - "132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf" + "1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "abcdef0123456789abcdef0123456789abcd1234567890ef0123456789abcdef" } } ``` diff --git a/modules/wallet.go b/modules/wallet.go index 603c92984c..b2bda70bd2 100644 --- a/modules/wallet.go +++ b/modules/wallet.go @@ -431,11 +431,10 @@ type ( // SpendableOutputs returns the outputs spendable by the wallet. SpendableOutputs() []SpendableOutput - // SignTransaction signs txn using secret keys known to the wallet. toSign - // maps the ParentID of each unsigned input to the UnlockHash of that input's - // desired UnlockConditions. SignTransaction fills in the UnlockConditions for - // each such input and adds a corresponding signature. - SignTransaction(txn *types.Transaction, toSign map[types.OutputID]types.UnlockHash) error + // SignTransaction signs txn using secret keys known to the wallet. + // The transaction should be complete with the exception of the + // Signature fields of each TransactionSignature referenced by toSign. + SignTransaction(txn *types.Transaction, toSign []crypto.Hash) error } // WalletSettings control the behavior of the Wallet. diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index 83a213c970..25f1360027 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -775,28 +775,46 @@ outer: return outputs } -// SignTransaction signs txn using secret keys known to the wallet. toSign -// maps the ParentID of each unsigned input to the UnlockHash of that input's -// desired UnlockConditions. SignTransaction fills in the UnlockConditions for -// each such input and adds a corresponding signature. -func (w *Wallet) SignTransaction(txn *types.Transaction, toSign map[types.OutputID]types.UnlockHash) error { +// SignTransaction signs txn using secret keys known to the wallet. The +// transaction should be complete with the exception of the Signature fields +// of each TransactionSignature referenced by toSign. For convenience, if +// toSign is empty, SignTransaction signs everything that it can. +func (w *Wallet) SignTransaction(txn *types.Transaction, toSign []crypto.Hash) error { w.mu.Lock() defer w.mu.Unlock() if !w.unlocked { return modules.ErrLockedWallet } + // if toSign is empty, sign all inputs that we have keys for + if len(toSign) == 0 { + for _, sci := range txn.SiacoinInputs { + if _, ok := w.keys[sci.UnlockConditions.UnlockHash()]; ok { + toSign = append(toSign, crypto.Hash(sci.ParentID)) + } + } + for _, sfi := range txn.SiafundInputs { + if _, ok := w.keys[sfi.UnlockConditions.UnlockHash()]; ok { + toSign = append(toSign, crypto.Hash(sfi.ParentID)) + } + } + } return signTransaction(txn, w.keys, toSign) } -// SignTransaction signs txn using secret keys derived from seed. toSign maps -// the ParentID of each unsigned input to the UnlockHash of that input's -// desired UnlockConditions. SignTransaction fills in the UnlockConditions for -// each such input and adds a corresponding signature. +// SignTransaction signs txn using secret keys derived from seed. The +// transaction should be complete with the exception of the Signature fields +// of each TransactionSignature referenced by toSign, which must not be empty. // // SignTransaction must derive all of the keys from scratch, so it is // appreciably slower than calling the Wallet.SignTransaction method. Only the // first 1 million keys are derived. -func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign map[types.OutputID]types.UnlockHash) error { +func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign []crypto.Hash) error { + if len(toSign) == 0 { + // unlike the wallet method, we can't simply "sign all inputs we have + // keys for," because without generating all of the keys up front, we + // don't know how many inputs we actually have keys for. + return errors.New("toSign cannot be empty") + } // generate keys in batches up to 1e6 before giving up keys := make(map[types.UnlockHash]spendableKey, 1e6) var keyIndex uint64 @@ -815,42 +833,73 @@ func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign map[types // signTransaction signs the specified inputs of txn using the specified keys. // It returns an error if any of the specified inputs cannot be signed. -func signTransaction(txn *types.Transaction, keys map[types.UnlockHash]spendableKey, toSign map[types.OutputID]types.UnlockHash) error { - signed := 0 - for i, sci := range txn.SiacoinInputs { - uh, ok := toSign[types.OutputID(sci.ParentID)] - if !ok { - // not signing this input - continue +func signTransaction(txn *types.Transaction, keys map[types.UnlockHash]spendableKey, toSign []crypto.Hash) error { + // helper function to lookup unlock conditions in the txn associated with + // a transaction signature's ParentID + findUnlockConditions := func(id crypto.Hash) (types.UnlockConditions, bool) { + for _, sci := range txn.SiacoinInputs { + if crypto.Hash(sci.ParentID) == id { + return sci.UnlockConditions, true + } } - // lookup the signing key(s) - sk, ok := keys[uh] - if !ok { - return errors.New("could not locate signing key for " + uh.String()) + for _, sfi := range txn.SiafundInputs { + if crypto.Hash(sfi.ParentID) == id { + return sfi.UnlockConditions, true + } } - txn.SiacoinInputs[i].UnlockConditions = sk.UnlockConditions - cf := types.CoveredFields{WholeTransaction: true} - addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sci.ParentID), sk) - signed++ + return types.UnlockConditions{}, false } - for i, sfi := range txn.SiafundInputs { - uh, ok := toSign[types.OutputID(sfi.ParentID)] + // helper function to lookup the secret key that can sign + findSigningKey := func(uc types.UnlockConditions, pubkeyIndex uint64) (crypto.SecretKey, bool) { + if pubkeyIndex >= uint64(len(uc.PublicKeys)) { + return crypto.SecretKey{}, false + } + pk := uc.PublicKeys[pubkeyIndex] + sk, ok := keys[uc.UnlockHash()] if !ok { - // not signing this input - continue + return crypto.SecretKey{}, false + } + for _, key := range sk.SecretKeys { + pubKey := key.PublicKey() + if bytes.Equal(pk.Key, pubKey[:]) { + return key, true + } + } + return crypto.SecretKey{}, false + } + + for _, id := range toSign { + // find associated txn signature + // + // NOTE: it's possible that the Signature field will already be filled + // out. Although we could save a bit of work by not signing it, in + // practice it's probably best to overwrite any existing signatures, + // since we know that ours will be valid. + sigIndex := -1 + for i, sig := range txn.TransactionSignatures { + if sig.ParentID == id { + sigIndex = i + break + } + } + if sigIndex == -1 { + return errors.New("toSign references signatures not present in transaction") } - // lookup the signing key(s) - sk, ok := keys[uh] + // find associated input + uc, ok := findUnlockConditions(id) if !ok { - return errors.New("could not locate signing key for " + uh.String()) + return errors.New("toSign references IDs not present in transaction") } - txn.SiafundInputs[i].UnlockConditions = sk.UnlockConditions - cf := types.CoveredFields{WholeTransaction: true} - addSignatures(txn, cf, sk.UnlockConditions, crypto.Hash(sfi.ParentID), sk) - signed++ - } - if signed != len(toSign) { - return errors.New("toSign references OutputIDs not present in transaction") + // lookup the signing key + sk, ok := findSigningKey(uc, txn.TransactionSignatures[sigIndex].PublicKeyIndex) + if !ok { + return errors.New("could not locate signing key for " + id.String()) + } + // add signature + sigHash := txn.SigHash(sigIndex) + encodedSig := crypto.SignHash(sigHash, sk) + txn.TransactionSignatures[sigIndex].Signature = encodedSig[:] } + return nil } diff --git a/modules/wallet/transactionbuilder_test.go b/modules/wallet/transactionbuilder_test.go index b1e5c834b4..33d4526ef1 100644 --- a/modules/wallet/transactionbuilder_test.go +++ b/modules/wallet/transactionbuilder_test.go @@ -4,6 +4,7 @@ import ( "sync" "testing" + "github.com/NebulousLabs/Sia/crypto" "github.com/NebulousLabs/Sia/modules" "github.com/NebulousLabs/Sia/types" ) @@ -526,18 +527,20 @@ func TestSignTransaction(t *testing.T) { // create a transaction that sends an output to the void txn := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{ - ParentID: types.SiacoinOutputID(outputs[0].ID), + ParentID: types.SiacoinOutputID(outputs[0].ID), + UnlockConditions: outputs[0].UnlockConditions, }}, SiacoinOutputs: []types.SiacoinOutput{{ Value: outputs[0].Value, UnlockHash: types.UnlockHash{}, }}, + TransactionSignatures: []types.TransactionSignature{{ + ParentID: crypto.Hash(outputs[0].ID), + }}, } // sign the transaction - err = wt.wallet.SignTransaction(&txn, map[types.OutputID]types.UnlockHash{ - outputs[0].ID: outputs[0].UnlockHash, - }) + err = wt.wallet.SignTransaction(&txn, nil) if err != nil { t.Fatal(err) } diff --git a/node/api/client/wallet.go b/node/api/client/wallet.go index 8c7a85a990..2a8fb7a1e9 100644 --- a/node/api/client/wallet.go +++ b/node/api/client/wallet.go @@ -7,6 +7,7 @@ import ( "net/url" "strconv" + "github.com/NebulousLabs/Sia/crypto" "github.com/NebulousLabs/Sia/node/api" "github.com/NebulousLabs/Sia/types" ) @@ -108,7 +109,7 @@ func (c *Client) WalletSiacoinsPost(amount types.Currency, destination types.Unl } // WalletSignPost uses the /wallet/sign api endpoint to sign a transaction. -func (c *Client) WalletSignPost(txn types.Transaction, toSign map[types.OutputID]types.UnlockHash) (wspr api.WalletSignPOSTResp, err error) { +func (c *Client) WalletSignPost(txn types.Transaction, toSign []crypto.Hash) (wspr api.WalletSignPOSTResp, err error) { buf := new(bytes.Buffer) err = json.NewEncoder(buf).Encode(api.WalletSignPOSTParams{ Transaction: txn, diff --git a/node/api/wallet.go b/node/api/wallet.go index 71a1376919..60fb1c4ddd 100644 --- a/node/api/wallet.go +++ b/node/api/wallet.go @@ -68,8 +68,8 @@ type ( // WalletSignPOSTParams contains the unsigned transaction and a set of // inputs to sign. WalletSignPOSTParams struct { - Transaction types.Transaction `json:"transaction"` - ToSign map[types.OutputID]types.UnlockHash `json:"tosign"` + Transaction types.Transaction `json:"transaction"` + ToSign []crypto.Hash `json:"tosign"` } // WalletSignPOSTResp contains the signed transaction. diff --git a/siatest/wallet/wallet_test.go b/siatest/wallet/wallet_test.go index 27eca8586d..f3214f105b 100644 --- a/siatest/wallet/wallet_test.go +++ b/siatest/wallet/wallet_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/NebulousLabs/Sia/build" + "github.com/NebulousLabs/Sia/crypto" "github.com/NebulousLabs/Sia/node" "github.com/NebulousLabs/Sia/siatest" "github.com/NebulousLabs/Sia/types" @@ -145,18 +146,20 @@ func TestSignTransaction(t *testing.T) { // create a transaction that sends an output to the void txn := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{ - ParentID: types.SiacoinOutputID(outputs[0].ID), + ParentID: types.SiacoinOutputID(outputs[0].ID), + UnlockConditions: outputs[0].UnlockConditions, }}, SiacoinOutputs: []types.SiacoinOutput{{ Value: outputs[0].Value, UnlockHash: types.UnlockHash{}, }}, + TransactionSignatures: []types.TransactionSignature{{ + ParentID: crypto.Hash(outputs[0].ID), + }}, } // sign the transaction - signResp, err := testNode.WalletSignPost(txn, map[types.OutputID]types.UnlockHash{ - outputs[0].ID: outputs[0].UnlockHash, - }) + signResp, err := testNode.WalletSignPost(txn, nil) if err != nil { t.Fatal("failed to sign the transaction", err) }