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 15 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
2 changes: 1 addition & 1 deletion cmd/siac/main.go
Expand Up @@ -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")
Expand Down
43 changes: 43 additions & 0 deletions cmd/siac/walletcmd.go
@@ -1,6 +1,8 @@
package main

import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/big"
Expand All @@ -11,8 +13,12 @@ import (
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"

"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 (
Expand Down Expand Up @@ -148,6 +154,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.",
Expand Down Expand Up @@ -466,6 +480,35 @@ 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 toSign map[types.OutputID]types.UnlockHash
err = json.Unmarshal([]byte(toSignJSON), &toSign)
if err != nil {
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)
}
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() {
Expand Down
76 changes: 61 additions & 15 deletions doc/API.md
Expand Up @@ -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 |
Expand All @@ -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).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1296,6 +1312,30 @@ keyfiles
standard success or error response. See
[#standard-responses](#standard-responses).

#### /wallet/sign [POST]

Function: Sign a transaction. The wallet will attempt to sign each input
specified.

###### Request Body
```
{
"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"
}
}
```

###### Response
```javascript
{
"transaction": { } // types.Transaction
}
```

#### /wallet/sweep/seed [POST]

Function: Scan the blockchain for outputs belonging to a seed and send them to
Expand Down Expand Up @@ -1428,28 +1468,34 @@ 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",
"confirmationheight": 50000,
"unlockhash": "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).

2 changes: 1 addition & 1 deletion doc/api/Transactionpool.md
Expand Up @@ -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
Expand Down
95 changes: 78 additions & 17 deletions doc/api/Wallet.md
Expand Up @@ -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 |
Expand All @@ -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]

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -483,6 +501,33 @@ 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 each input
specified.

###### Request Body
```
{
// unsigned transaction
"transaction": { }, // types.Transaction

// inputs to sign; a mapping from OutputID to UnlockHash
"tosign": {
"3689bd3489679aabcde02e01345abcde": "138950f0129d74acd4eade3453b45678",
"132cee478a9bb98bdd23cf05376cdf2a": "7cbcd123578234ce0f12fe01a68ba9bf"
}
}
```

###### Response
```javascript
{
// signed transaction
"transaction": { } // types.Transaction
}
```

#### /wallet/sweep/seed [POST]

Function: Scan the blockchain for outputs belonging to a seed and send them to
Expand Down Expand Up @@ -693,6 +738,38 @@ 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.
"confirmationheight": 50000,

// UnlockHash of the output.
"unlockhash": "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.
Expand All @@ -704,19 +781,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).