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 all 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: 5 additions & 2 deletions cmd/siac/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +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.
walletRawTxn bool // Encode/decode transactions in base64-encoded binary.
)

var (
Expand Down Expand Up @@ -106,14 +107,16 @@ func main() {

root.AddCommand(walletCmd)
walletCmd.AddCommand(walletAddressCmd, walletAddressesCmd, walletChangepasswordCmd, walletInitCmd, walletInitSeedCmd,
walletLoadCmd, walletLockCmd, walletSeedsCmd, walletSendCmd, walletSweepCmd,
walletBalanceCmd, walletTransactionsCmd, walletUnlockCmd)
walletLoadCmd, walletLockCmd, walletSeedsCmd, walletSendCmd, walletSweepCmd, walletSignCmd,
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")
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,
Expand Down
97 changes: 97 additions & 0 deletions cmd/siac/walletcmd.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math"
Expand All @@ -12,7 +14,12 @@ 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/types"
"github.com/NebulousLabs/entropy-mnemonics"
)

var (
Expand All @@ -37,6 +44,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",
Expand Down Expand Up @@ -148,6 +162,15 @@ 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. 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: walletsigncmd,
}

walletSweepCmd = &cobra.Command{
Use: "sweep",
Short: "Sweep siacoins and siafunds from a seed.",
Expand Down Expand Up @@ -450,6 +473,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: ")
Expand All @@ -464,6 +510,57 @@ func walletsweepcmd() {
fmt.Printf("Swept %v and %v SF from seed.\n", currencyUnits(swept.Coins), swept.Funds)
}

// walletsigncmd signs a transaction.
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(args[0]), &txn)
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
wspr, err := httpClient.WalletSignPost(txn, toSign)
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 walletRawTxn {
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,
// providing a net flow of siacoins and siafunds for each.
func wallettransactionscmd() {
Expand Down
79 changes: 64 additions & 15 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,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 @@ -1271,13 +1272,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 @@ -1367,6 +1369,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 @@ -1518,6 +1534,29 @@ 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": [
"1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"abcdef0123456789abcdef0123456789abcd1234567890ef0123456789abcdef"
]
}
```

###### 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 @@ -1650,28 +1689,38 @@ 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,
"unlockconditions": {
"publickeys": [{"algorithm":"ed25519","key":"/XUGj8PxMDkqdae6Js6ubcERxfxnXN7XPjZyANBZH1I="}],
"signaturesrequired": 1
},
"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
Original file line number Diff line number Diff line change
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