parent
8f2b22a8f5
commit
65bb8cdf4e
@ -0,0 +1,301 @@
|
||||
# trezorctl Bitcoin transaction JSON format
|
||||
|
||||
Since version 0.11.2, `trezorctl` allows fully offline signing of Bitcoin and
|
||||
Bitcoin-like altcoin transactions encoded in a custom JSON structure. Starting with
|
||||
version 0.11.6, this is the only supported format for signing.
|
||||
|
||||
## Structure
|
||||
|
||||
The structure of the JSON matches the shape of the relevant protobuf messages. See
|
||||
file [messages-bitcoin.proto] for up-to-date structure.
|
||||
|
||||
The root is an object with the following attributes:
|
||||
|
||||
* __`coin_name`__: string representing the coin name as listed in [coin defs]. If
|
||||
missing, `"Bitcoin"` is used.
|
||||
* __`inputs`__: array of `TxInputType` objects. Must be present.
|
||||
* __`outputs`__: array of `TxOutputType` objects. Must be present.
|
||||
* __`details`__: object of type `SignTx`, specifying transaction metadata. Can be
|
||||
omitted.
|
||||
* __`prev_txes`__: object whose keys are hex-encoded transaction hashes, and values are
|
||||
objects of type `TransactionType`. When signing a transaction with non-SegWit inputs,
|
||||
each previous transaction must have an entry in `prev_txes`. With pure SegWit
|
||||
transactions, this field can be omitted.
|
||||
|
||||
See definition of the respective object types in [messages-bitcoin.proto] for
|
||||
descriptions of individual fields.
|
||||
|
||||
[messages-bitcoin.proto]: ../../common/protob/messages-bitcoin.proto
|
||||
[coin defs]: ../../common/defs/bitcoin
|
||||
|
||||
**Please note** that the `optional` keyword in the protobuf definition does _not_
|
||||
indicate that the field can be omitted, nor does the `default` extension mean that the
|
||||
default value will be used if missing.
|
||||
|
||||
### Derivation paths
|
||||
|
||||
A derivation path in the field `address_n` is encoded as an array of numbers according
|
||||
to the BIP-32 specification. Use `trezorlib.tools.parse_path` to convert a string
|
||||
derivation path to the corresponding array.
|
||||
|
||||
### Inputs
|
||||
|
||||
```protobuf
|
||||
enum InputScriptType {
|
||||
SPENDADDRESS = 0; // standard P2PKH address
|
||||
SPENDMULTISIG = 1; // P2SH multisig address
|
||||
EXTERNAL = 2; // reserved for external inputs (coinjoin)
|
||||
SPENDWITNESS = 3; // native SegWit
|
||||
SPENDP2SHWITNESS = 4; // SegWit over P2SH (backward compatible)
|
||||
}
|
||||
|
||||
message TxInputType {
|
||||
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
|
||||
required bytes prev_hash = 2; // hash of previous transaction output to spend by this input
|
||||
required uint32 prev_index = 3; // index of previous output to spend
|
||||
optional bytes script_sig = 4; // script signature, unset for tx to sign
|
||||
optional uint32 sequence = 5; // sequence (default=0xffffffff)
|
||||
optional InputScriptType script_type = 6 ; // defines template of input script
|
||||
optional MultisigRedeemScriptType multisig = 7; // Filled if input is going to spend multisig tx
|
||||
optional uint64 amount = 8; // amount of previous transaction output
|
||||
optional uint32 decred_tree = 9; // only for Decred
|
||||
optional uint32 decred_script_version = 10; // only for Decred
|
||||
optional bytes prev_block_hash_bip115 = 11; // block hash of previous transaction output (for bip115 implementation)
|
||||
optional uint32 prev_block_height_bip115 = 12; // block height of previous transaction output (for bip115 implementation)
|
||||
}
|
||||
```
|
||||
|
||||
Each input must have a derivation path (`address_n`), `prev_hash` and `prev_index`
|
||||
refering to the output being spent, `sequence` number, `script_type` corresponding to
|
||||
the desired signature type, and `amount`.
|
||||
|
||||
The field `script_sig` must not be set.
|
||||
|
||||
The field `multisig` can be used for multisig inputs. Documenting the multisig structure is TBD. With regular inputs, `multisig` must not be set.
|
||||
|
||||
`decred` and `bip115` fields must only be set when relevant to your currency.
|
||||
|
||||
### Outputs
|
||||
|
||||
```protobuf
|
||||
enum OutputScriptType {
|
||||
PAYTOADDRESS = 0; // string address output; change is a P2PKH address
|
||||
PAYTOMULTISIG = 2; // change output is a multisig address
|
||||
PAYTOOPRETURN = 3; // op_return
|
||||
PAYTOWITNESS = 4; // change output is native SegWit
|
||||
PAYTOP2SHWITNESS = 5; // change output is SegWit over P2SH
|
||||
}
|
||||
|
||||
message TxOutputType {
|
||||
optional string address = 1; // destination address in Base58 encoding
|
||||
repeated uint32 address_n = 2; // derivation path for change address
|
||||
required uint64 amount = 3; // amount to spend in satoshis
|
||||
required OutputScriptType script_type = 4; // output script type
|
||||
optional MultisigRedeemScriptType multisig = 5; // multisig output definition
|
||||
optional bytes op_return_data = 6; // defines op_return data
|
||||
optional uint32 decred_script_version = 7; // only for Decred
|
||||
optional bytes block_hash_bip115 = 8; // block hash of existing block (recommended current_block - 300) (for bip115 implementation)
|
||||
optional uint32 block_height_bip115 = 9; // block height of existing block (recommended current_block - 300) (for bip115 implementation)
|
||||
|
||||
```
|
||||
|
||||
All outputs must have an `amount` and a `script_type`.
|
||||
|
||||
For normal (non-change) outputs, the field `address` must be set to an address string,
|
||||
and the `script_type` must be set to `"PAYTOADDRESS"`. `address_n` must not be set.
|
||||
|
||||
For outputs returning change, `address` must not be set, and `address_n` must be a
|
||||
derivation path of the desired change address. `script_type` indicates the desired
|
||||
address type of the change output.
|
||||
|
||||
For `OP_RETURN` outputs, `script_type` must be set to `"PAYTOOPRETURN"` and
|
||||
`op_return_data` must be filled appropriately. `address_n` and `address` must not be
|
||||
set.
|
||||
|
||||
`decred` and `bip115` fields must only be set when relevant to your currency.
|
||||
|
||||
### Transaction metadata
|
||||
|
||||
The following is a shortened definition of the `SignTx` protobuf message. Note that it
|
||||
is possible to set fields `outputs_count`, `inputs_count` and `coin_name`, but their
|
||||
values will be ignored. Instead, the number of elements in `outputs`, `inputs`, and the
|
||||
value of `coin_name` from root object will be used.
|
||||
|
||||
All fields are optional unless required by your currency.
|
||||
|
||||
```protobuf
|
||||
message SignTx {
|
||||
optional uint32 version = 4; // transaction version
|
||||
optional uint32 lock_time = 5; // transaction lock_time
|
||||
optional uint32 expiry = 6; // only for Decred and Zcash
|
||||
optional bool overwintered = 7; // only for Zcash
|
||||
optional uint32 version_group_id = 8; // only for Zcash, nVersionGroupId when overwintered is set
|
||||
optional uint32 timestamp = 9; // only for Capricoin, transaction timestamp
|
||||
optional uint32 branch_id = 10; // only for Zcash, BRANCH_ID when overwintered is set
|
||||
}
|
||||
```
|
||||
|
||||
### Previous transactions
|
||||
|
||||
For inputs that do not use BIP-143 (SegWit) signing, each input transaction must have an
|
||||
entry in the `prev_txes` object. The following object definitions are used:
|
||||
|
||||
```protobuf
|
||||
message TxInputType {
|
||||
required bytes prev_hash = 2; // hash of previous transaction output to spend by this input
|
||||
required uint32 prev_index = 3; // index of previous output to spend
|
||||
optional bytes script_sig = 4; // script signature, unset for tx to sign
|
||||
optional uint32 sequence = 5; // sequence (default=0xffffffff)
|
||||
optional uint32 decred_tree = 9; // only for Decred
|
||||
}
|
||||
|
||||
message TxOutputBinType {
|
||||
required uint64 amount = 1;
|
||||
required bytes script_pubkey = 2;
|
||||
optional uint32 decred_script_version = 3; // only for Decred
|
||||
}
|
||||
|
||||
message TransactionType {
|
||||
optional uint32 version = 1;
|
||||
repeated TxInputType inputs = 2;
|
||||
repeated TxOutputBinType bin_outputs = 3;
|
||||
optional uint32 lock_time = 4;
|
||||
optional bytes extra_data = 8; // only for Zcash
|
||||
optional uint32 expiry = 10; // only for Decred and Zcash
|
||||
optional bool overwintered = 11; // only for Zcash
|
||||
optional uint32 version_group_id = 12; // only for Zcash, nVersionGroupId when overwintered is set
|
||||
optional uint32 timestamp = 13; // only for Capricoin, transaction timestamp
|
||||
optional uint32 branch_id = 14; // only for Zcash, BRANCH_ID when overwintered is set
|
||||
}
|
||||
```
|
||||
|
||||
## Encoding
|
||||
|
||||
Object types are encoded by a variant of [proto3 JSON mapping](https://developers.google.com/protocol-buffers/docs/proto3#json).
|
||||
The following notable differences exist:
|
||||
|
||||
1. due to the fact that Trezor protocol uses proto2, the logic for omitted fields is
|
||||
different. If a value is missing or null in JSON, it is considered unset for the
|
||||
corresponding protobuf.
|
||||
2. proto3 JSON mapping encodes `bytes` as Base64. The transaction format encodes them as
|
||||
hexadecimal strings. This will be changed in a future revision, but the hex strings
|
||||
will still be understood.
|
||||
3. Field names are expected in `snake_case`, identical to the protobuf definition. In
|
||||
the future, support for `camelCase` field names will be added.
|
||||
|
||||
Otherwise the encoding is identical:
|
||||
|
||||
* numeric fields (`uint32`, `uint64`) are encoded as JSON numbers
|
||||
* `bool` fields are encoded as JSON booleans (`true`, `false`)
|
||||
* `string` fields are encoded as JSON strings
|
||||
* `bytes` fields are encoded as JSON strings with hex representation of the bytes content
|
||||
* `repeated` fields are JSON arrays of the inner type
|
||||
* `enum` fields can be either a JSON number of the value, or a JSON string of the name
|
||||
* nested objects are JSON objects
|
||||
|
||||
## Example
|
||||
|
||||
The JSON below encodes a transaction with the following inputs:
|
||||
|
||||
* [e9cec1644db8fa95fe639a9b503a63ea587d2f4e480d3847703e3ec73adf6b5a](https://btc5.trezor.io/tx/e9cec1644db8fa95fe639a9b503a63ea587d2f4e480d3847703e3ec73adf6b5a)
|
||||
output **0** (P2PKH address 1Jw5FrKhi2aWbbF4h3QRWLog5AjsJYGswv)
|
||||
at derivation path **m/44'/0'/0'/0/282**
|
||||
amount **85 170** sat
|
||||
* [1f545c0ca1f2c055e199c70457025c1e393edd013a274a976187115a5c601155](https://btc5.trezor.io/tx/1f545c0ca1f2c055e199c70457025c1e393edd013a274a976187115a5c601155)
|
||||
output **0** (P2SH-SegWit address 3DEAk9KGrgvj2gHQ1hyfCXus9hZr9K8Beh)
|
||||
at derivation path **m/49'/0'/0'/0/55**
|
||||
amount **500 000** sat
|
||||
|
||||
And the following outputs:
|
||||
|
||||
* **12 345** sat to address **3DDEgt7quAq7XqoG6PjVXi1eeAea4rfWck**
|
||||
* **562 825** sat to a P2SH-SegWit change address at derivation path **m/49'/0'/0'/1/99**
|
||||
* fee of 10 000 sat
|
||||
|
||||
(Note that Trezor does not support change addresses when mixing input types. The example
|
||||
is designed purely to showcase the JSON structure. Usually, all inputs should have the
|
||||
same `script_type`.)
|
||||
|
||||
Transaction version is **2**, other metadata is not set.
|
||||
|
||||
```json
|
||||
{
|
||||
"coin_name": "Bitcoin",
|
||||
"details": {
|
||||
"version": 2
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"address_n": [
|
||||
2147483692,
|
||||
2147483648,
|
||||
2147483648,
|
||||
0,
|
||||
282
|
||||
],
|
||||
"amount": 85170,
|
||||
"prev_hash": "e9cec1644db8fa95fe639a9b503a63ea587d2f4e480d3847703e3ec73adf6b5a",
|
||||
"prev_index": 0,
|
||||
"script_type": "SPENDADDRESS",
|
||||
"sequence": 4294967293
|
||||
},
|
||||
{
|
||||
"address_n": [
|
||||
2147483697,
|
||||
2147483648,
|
||||
2147483648,
|
||||
0,
|
||||
55
|
||||
],
|
||||
"amount": 500000,
|
||||
"prev_hash": "1f545c0ca1f2c055e199c70457025c1e393edd013a274a976187115a5c601155",
|
||||
"prev_index": 0,
|
||||
"script_type": "SPENDP2SHWITNESS",
|
||||
"sequence": 4294967293
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"address": "3DDEgt7quAq7XqoG6PjVXi1eeAea4rfWck",
|
||||
"amount": 12345,
|
||||
"script_type": "PAYTOADDRESS"
|
||||
},
|
||||
{
|
||||
"address_n": [
|
||||
2147483697,
|
||||
2147483648,
|
||||
2147483648,
|
||||
1,
|
||||
99
|
||||
],
|
||||
"amount": 562825,
|
||||
"script_type": "PAYTOP2SHWITNESS"
|
||||
}
|
||||
],
|
||||
"prev_txes": {
|
||||
"e9cec1644db8fa95fe639a9b503a63ea587d2f4e480d3847703e3ec73adf6b5a": {
|
||||
"bin_outputs": [
|
||||
{
|
||||
"amount": 85170,
|
||||
"script_pubkey": "76a914c4b4272ca6d3b069dcf7afdda172a7dae677d4c988ac"
|
||||
},
|
||||
{
|
||||
"amount": 2375277,
|
||||
"script_pubkey": "a914115125511fa9f301ecdda8bb73401644c260c61b87"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"prev_hash": "59ef8b5633c2a8bf0a21edcbc4b9f271572061f81d42b366fe3b8bc0ec68014e",
|
||||
"prev_index": 1,
|
||||
"script_sig": "1600149043ed42ab198d95067d8760c247f164c4933f3f",
|
||||
"sequence": 4294967295
|
||||
}
|
||||
],
|
||||
"lock_time": 0,
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
# This file is part of the Trezor project.
|
||||
#
|
||||
# Copyright (C) 2012-2019 SatoshiLabs and contributors
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3
|
||||
# as published by the Free Software Foundation.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the License along with this library.
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
from trezorlib import coins, messages, tools
|
||||
from trezorlib.cli import ChoiceType
|
||||
from trezorlib.cli.btc import INPUT_SCRIPTS, OUTPUT_SCRIPTS
|
||||
from trezorlib.protobuf import to_dict
|
||||
|
||||
|
||||
def echo(*args, **kwargs):
|
||||
return click.echo(*args, err=True, **kwargs)
|
||||
|
||||
|
||||
def prompt(*args, **kwargs):
|
||||
return click.prompt(*args, err=True, **kwargs)
|
||||
|
||||
|
||||
def _default_script_type(address_n, script_types):
|
||||
script_type = "address"
|
||||
|
||||
if address_n is None:
|
||||
pass
|
||||
elif address_n[0] == tools.H_(49):
|
||||
script_type = "p2shsegwit"
|
||||
elif address_n[0] == tools.H_(84):
|
||||
script_type = "segwit"
|
||||
|
||||
return script_type
|
||||
# return script_types[script_type]
|
||||
|
||||
|
||||
def parse_vin(s):
|
||||
txid, vout = s.split(":")
|
||||
return bytes.fromhex(txid), int(vout)
|
||||
|
||||
|
||||
def _get_inputs_interactive(coin_data, txapi):
|
||||
inputs = []
|
||||
txes = {}
|
||||
while True:
|
||||
echo()
|
||||
prev = prompt(
|
||||
"Previous output to spend (txid:vout)", type=parse_vin, default=""
|
||||
)
|
||||
if not prev:
|
||||
break
|
||||
prev_hash, prev_index = prev
|
||||
address_n = prompt("BIP-32 path to derive the key", type=tools.parse_path)
|
||||
try:
|
||||
tx = txapi[prev_hash]
|
||||
txes[prev_hash] = tx
|
||||
amount = tx.bin_outputs[prev_index].amount
|
||||
echo("Prefilling input amount: {}".format(amount))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
echo("Failed to fetch transation. This might bite you later.")
|
||||
amount = prompt("Input amount (satoshis)", type=int, default=0)
|
||||
|
||||
sequence = prompt(
|
||||
"Sequence Number to use (RBF opt-in enabled by default)",
|
||||
type=int,
|
||||
default=0xFFFFFFFD,
|
||||
)
|
||||
script_type = prompt(
|
||||
"Input type",
|
||||
type=ChoiceType(INPUT_SCRIPTS),
|
||||
default=_default_script_type(address_n, INPUT_SCRIPTS),
|
||||
)
|
||||
if isinstance(script_type, str):
|
||||
script_type = INPUT_SCRIPTS[script_type]
|
||||
|
||||
new_input = messages.TxInputType(
|
||||
address_n=address_n,
|
||||
prev_hash=prev_hash,
|
||||
prev_index=prev_index,
|
||||
amount=amount,
|
||||
script_type=script_type,
|
||||
sequence=sequence,
|
||||
)
|
||||
if coin_data["bip115"]:
|
||||
prev_output = txapi.get_tx(prev_hash.hex()).bin_outputs[prev_index]
|
||||
new_input.prev_block_hash_bip115 = prev_output.block_hash
|
||||
new_input.prev_block_height_bip115 = prev_output.block_height
|
||||
|
||||
inputs.append(new_input)
|
||||
|
||||
return inputs, txes
|
||||
|
||||
|
||||
def _get_outputs_interactive():
|
||||
outputs = []
|
||||
while True:
|
||||
echo()
|
||||
address = prompt("Output address (for non-change output)", default="")
|
||||
if address:
|
||||
address_n = None
|
||||
script_type = messages.OutputScriptType.PAYTOADDRESS
|
||||
else:
|
||||
address = None
|
||||
address_n = prompt(
|
||||
"BIP-32 path (for change output)", type=tools.parse_path, default=""
|
||||
)
|
||||
if not address_n:
|
||||
break
|
||||
script_type = prompt(
|
||||
"Output type",
|
||||
type=ChoiceType(OUTPUT_SCRIPTS),
|
||||
default=_default_script_type(address_n, OUTPUT_SCRIPTS),
|
||||
)
|
||||
if isinstance(script_type, str):
|
||||
script_type = OUTPUT_SCRIPTS[script_type]
|
||||
|
||||
amount = prompt("Amount to spend (satoshis)", type=int)
|
||||
|
||||
outputs.append(
|
||||
messages.TxOutputType(
|
||||
address_n=address_n,
|
||||
address=address,
|
||||
amount=amount,
|
||||
script_type=script_type,
|
||||
)
|
||||
)
|
||||
|
||||
return outputs
|
||||
|
||||
|
||||
@click.command()
|
||||
def sign_interactive():
|
||||
coin = prompt("Coin name", default="Bitcoin")
|
||||
if coin in coins.tx_api:
|
||||
coin_data = coins.by_name[coin]
|
||||
txapi = coins.tx_api[coin]
|
||||
else:
|
||||
echo('Coin "%s" is not recognized.' % coin, err=True)
|
||||
echo("Supported coin types: %s" % ", ".join(coins.tx_api.keys()), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
inputs, txes = _get_inputs_interactive(coin_data, txapi)
|
||||
outputs = _get_outputs_interactive()
|
||||
|
||||
if coin_data["bip115"]:
|
||||
current_block_height = txapi.current_height()
|
||||
# Zencash recommendation for the better protection
|
||||
block_height = current_block_height - 300
|
||||
block_hash = txapi.get_block_hash(block_height)
|
||||
# Blockhash passed in reverse order
|
||||
block_hash = block_hash[::-1]
|
||||
|
||||
for output in outputs:
|
||||
output.block_hash_bip115 = block_hash
|
||||
output.block_height_bip115 = block_height
|
||||
|
||||
signtx = messages.SignTx()
|
||||
signtx.version = prompt("Transaction version", type=int, default=2)
|
||||
signtx.lock_time = prompt("Transaction locktime", type=int, default=0)
|
||||
if coin == "Capricoin":
|
||||
signtx.timestamp = prompt("Transaction timestamp", type=int)
|
||||
|
||||
result = {
|
||||
"coin_name": coin,
|
||||
"inputs": [to_dict(i, hexlify_bytes=True) for i in inputs],
|
||||
"outputs": [to_dict(o, hexlify_bytes=True) for o in outputs],
|
||||
"details": to_dict(signtx, hexlify_bytes=True),
|
||||
"prev_txes": {
|
||||
txhash.hex(): to_dict(txdata, hexlify_bytes=True)
|
||||
for txhash, txdata in txes.items()
|
||||
},
|
||||
}
|
||||
|
||||
print(json.dumps(result, sort_keys=True, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sign_interactive()
|
Loading…
Reference in new issue