Skip to content
This repository has been archived by the owner on Nov 2, 2018. It is now read-only.

Add support for offline transaction signing #2907

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d39e94f
add offline signing functionality
lukechampine Mar 27, 2018
1c7efd4
sync before reporting wallet height
lukechampine Mar 27, 2018
4fa416a
add api routes for unspent+sign
lukechampine Mar 27, 2018
e959025
add api docs for unspent+sign
lukechampine Mar 27, 2018
6332d01
change sign semantics
lukechampine Mar 28, 2018
b3741e7
add wallet sign command
lukechampine Mar 28, 2018
41c5410
generate keys incrementally
lukechampine Mar 28, 2018
32059e7
use new SpendableOutput type for /unspent
lukechampine Mar 28, 2018
277d93a
sign SiafundInputs as well
lukechampine Mar 28, 2018
873500b
Add siatest and client integration for offline signing
ChrisSchinnerl Mar 29, 2018
043674b
Merge pull request #2913 from NebulousLabs/offline-signing-siatest
lukechampine Mar 29, 2018
a2bcb24
Merge branch 'master' into offline-signing
Mar 29, 2018
90566ab
decode directly into toSign map
lukechampine Mar 29, 2018
d408cc1
add docstrings
lukechampine Mar 29, 2018
4050676
document tosign types
lukechampine Mar 29, 2018
64ff690
account for unconfirmed txns in SpendableOutputs
lukechampine Apr 12, 2018
78c2a13
add wallet sign -raw flag, JSON by default
lukechampine Apr 17, 2018
d2c89fc
try /wallet/sign before doing keygen
lukechampine Apr 17, 2018
c5098c8
include UnlockConditions in SpendableOutput
lukechampine Apr 17, 2018
c1c14d7
Revert "include UnlockConditions in SpendableOutput"
lukechampine Apr 18, 2018
0514348
Merge branch 'master' into offline-signing
lukechampine May 14, 2018
6b22c87
don't include unconfirmed outputs that may be spent
lukechampine May 16, 2018
477a497
add UnlockConditions to SpendableOutput
lukechampine May 30, 2018
c36c14e
Merge branch 'master' into offline-signing
lukechampine May 30, 2018
30a5854
Merge branch 'master' into offline-signing
lukechampine Jul 11, 2018
83967e1
fix TransactionPoolRawPost signature
lukechampine Jul 12, 2018
4fccafd
add wallet broadcast cmd
lukechampine Jul 12, 2018
c97a9d7
more helpful signature decoding error
lukechampine Jul 12, 2018
f174e41
overhaul SignTransaction
lukechampine Jul 12, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion Makefile
Expand Up @@ -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:
Expand All @@ -58,6 +58,11 @@ fmt:
vet: release-std
go vet $(pkgs)

# will always run on some packages for a while.
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/wallet
lint:
golint -min_confidence=1.0 -set_exit_status $(pkgs)

Expand Down
13 changes: 2 additions & 11 deletions cmd/siac/walletcmd.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions doc/API.md
Expand Up @@ -1322,6 +1322,7 @@ specified.
{
"transaction": { }, // types.Transaction
"tosign": {
// types.OutputID -> types.UnlockHash
"3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably mention that those are SiacoinOutputID: UnlockHash/Address pairs.

"132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf"
}
Expand Down
2 changes: 2 additions & 0 deletions modules/wallet/transactionbuilder.go
Expand Up @@ -719,6 +719,8 @@ func SignTransaction(txn *types.Transaction, seed modules.Seed, toSign map[types
return signTransaction(txn, keys, toSign)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that it is not required for unexported functions but I like to have comments everywhere :P

// 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 {
Expand Down
2 changes: 2 additions & 0 deletions modules/wallet/transactionbuilder_test.go
Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing comment

if testing.Short() {
t.SkipNow()
Expand Down
21 changes: 21 additions & 0 deletions 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
}
22 changes: 22 additions & 0 deletions node/api/client/wallet.go
@@ -1,6 +1,7 @@
package client

import (
"bytes"
"encoding/json"
"fmt"
"net/url"
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
1 change: 1 addition & 0 deletions siatest/wallet/wallet.go
@@ -0,0 +1 @@
package wallet
89 changes: 89 additions & 0 deletions 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")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to report err here, too

}
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")
}
}
12 changes: 11 additions & 1 deletion types/encoding.go
Expand Up @@ -651,14 +651,24 @@ 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.
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)
Expand Down