From 65bb8cdf4e86704f25021dbf54f31d8789742f44 Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 14 Nov 2019 16:19:47 +0100 Subject: [PATCH] python/trezorctl: only accept json for sign-tx, document the format --- python/docs/EXAMPLES.rst | 65 ++++--- python/docs/transaction-format.md | 301 ++++++++++++++++++++++++++++++ python/src/trezorlib/cli/btc.py | 174 ++--------------- python/tools/build_tx.py | 193 +++++++++++++++++++ 4 files changed, 543 insertions(+), 190 deletions(-) create mode 100644 python/docs/transaction-format.md create mode 100755 python/tools/build_tx.py diff --git a/python/docs/EXAMPLES.rst b/python/docs/EXAMPLES.rst index c6b22600f..86a4ed756 100644 --- a/python/docs/EXAMPLES.rst +++ b/python/docs/EXAMPLES.rst @@ -5,14 +5,14 @@ Show all available `options `_: .. code:: - trezorctl --help + $ trezorctl --help Retrieve features, settings and coin types supported by your device: .. code:: - trezorctl get-features + $ trezorctl get-features Bitcoin examples @@ -22,37 +22,46 @@ Get first receiving address of first account for Bitcoin (Legacy / non-SegWit): .. code:: - trezorctl get-address --coin Bitcoin --script-type address --address "m/44'/0'/0'/0/0" + $ trezorctl btc get-address --script-type address --address m/44h/0h/0h/0/0 Get first receiving address of first account for Bitcoin (SegWit-in-P2SH): .. code:: - trezorctl get-address --coin Bitcoin --script-type p2shsegwit --address "m/49'/0'/0'/0/0" + $ trezorctl btc get-address --script-type p2shsegwit --address m/49h/0h/0h/0/0 Get first receiving address of first account for Bitcoin (Bech32 native SegWit P2WPKH): .. code:: - trezorctl get-address --coin Bitcoin --script-type segwit --address "m/84'/0'/0'/0/0" + trezorctl btc get-address --script-type segwit --address m/84h/0h/0h/0/0 Get Legacy Bitcoin ``xpub`` (can be used to create a watch-only wallet): .. code:: - trezorctl get-public-node --coin Bitcoin --address "m/44'/0'/0'" + trezorctl btc get-public-node --address m/44h/0h/0h Transaction signing ------------------- -You can use ``trezorctl`` to sign a transaction without it automatically being broadcast to the Bitcoin network. +``trezorctl`` supports offline signing of transactions in a `custom JSON format `_. +If you have transaction data in file ``tx.json``, use the following call to get the signature: -You will need the following pieces of info: +.. code:: + + $ trezorctl btc sign-tx tx.json + Please confirm action on your Trezor device + + Signed transaction: + 02000000025a6bdf3ac73e3e7047380d484e2f7d58ea633a509b9a63fe95fab84d64(...) + +A provided script can be used to sign transactions interactively. You will need the following pieces of info: 1) Transaction ID containing the Output we want to spend (aka ``prevhash`` or ``a5ea715a...d201e64e`` in example below). 2) Index number of the Output being spent from the above tx (aka ``previndex`` or ``0`` in example below). -3) BIP32 path to the Node which can spend the above UTXO (eg ``Bitcoin/0'/0/0`` for the first). +3) BIP32 path to the Node which can spend the above UTXO (eg ``Bitcoin/0h/0/0`` for the first). 4) Destination address where you want to send funds (eg ``3M8XGFBKwkf7miBzpkU3x2DoWwAVrD1mhk`` below). 5) Amount to send in satoshis - ``91305`` in the example below (multiply BTC amount 0.00091305 by 100,000,000). 6) Expected fee (``0.00019695`` BTC in example below). Note: the miner receives all satoshis left unspent from a transaction. If you want to receive some change, you need to send it to an address you own (otherwise it will go to miner). Fee is not needed below, we just want it as a sanity check. @@ -63,31 +72,29 @@ After authenticating, open the "Send" tab, fill-out all details, then open the " .. code:: - trezorctl sign-tx -c Bitcoin + $ python3 tools/build_tx.py | trezorctl btc sign-tx - - Input (prevhash:previndex, empty to move on): a5ea715aa99ca30516f3af6f622dfe7399d883d49ad74b1fe33fdf73d201e64e:0 - Node path to sign with (e.g.- Bitcoin/0'/0/0): Bitcoin/0'/0/0 + Coin name [Bitcoin]: Bitcoin - Input (prevhash:previndex, empty to move on): + Previous output to spend (txid:vout) []: a5ea715aa99ca30516f3af6f622dfe7399d883d49ad74b1fe33fdf73d201e64e:0 + BIP-32 path to derive the key: Bitcoin/0h/0/0 + Prefilling input amount: 111000 + Sequence Number to use (RBF opt-in enabled by default) [4294967293]: 4294967293 + Input type (address, segwit, p2shsegwit) [address]: - Pay to address (empty to move on): 3M8XGFBKwkf7miBzpkU3x2DoWwAVrD1mhk - Amount (in satoshis): 91305 + Previous output to spend (txid:vout) []: - Pay to address (empty to move on): - Passphrase required: + Output address (for non-change output) []: 3M8XGFBKwkf7miBzpkU3x2DoWwAVrD1mhk + Amount to spend (satoshis): 91305 - Confirm your Passphrase: - - RECEIVED PART OF SERIALIZED TX (152 BYTES) - RECEIVED PART OF SERIALIZED TX (37 BYTES) - SIGNED IN 52.538 SECONDS, CALLED 10 MESSAGES, 189 BYTES + Output address (for non-change output) []: + BIP-32 path (for change output) []: + Transaction version [2]: + Transaction locktime [0]: + Please confirm action on your Trezor device Signed Transaction: - 01000000014ee601d273df3fe31f4bd79ad483d89973fe2d626faff31605a39ca95a71eaa5000000006a47304402206386a0ad0f0b196d375a0805eee2aebe4644032c2998aaf00e43ce68a293986702202ad25964844657e10130f81201b7d87eb8047cf0c09dfdcbbe68a1a732e80ded012103b375a0dd50c8dbc4a6156a55e31274ee0537191e1bc824a09278a220fafba2dbffffffff01a96401000000000017a914d53d47ccd1579b93c284e9caf3c81f3f417871698700000000 - - Use the following form to broadcast it to the network: - https://btc-bitcore1.trezor.io/tx/send - + 02000000014ee601d273df3fe31f4bd79ad483d89973fe2d626faff316(...) The signed transaction text can then be inspected in Electrum (`screenshot `_), `coinb.in `_ or another tool. If all info is correct, you can then broadcast the tx to the Bitcoin network via the URL provided by ``trezorctl`` or Electrum (Tools → Load transaction → From text. Here is a `screenshot `_). TIP: Electrum will only show the transaction fee if you previously imported the spending address (eg ``16ijWp48xn8hj6deD5ZHSJcgNjtYbpiki8`` from example tx above). Also, the final tx size (and therefore satoshis / byte) might be slightly different than the estimate shown on beta-wallet.trezor.io @@ -101,13 +108,13 @@ Get first receiving address of first account for Litecoin (SegWit-in-P2SH): .. code:: - trezorctl get-address --coin Litecoin --script-type p2shsegwit --address "m/49'/2'/0'/0/0" + $ trezorctl ltc get-address --script-type p2shsegwit --address m/49h/2h/0h/0/0 Get first receiving address of first account for Litecoin (Bech32 native SegWit P2WPKH): .. code:: - trezorctl get-address --coin Litecoin --script-type segwit --address "m/84'/2'/0'/0/0" + $ trezorctl ltc get-address --script-type segwit --address m/84h/2h/0h/0/0 Notes ----- diff --git a/python/docs/transaction-format.md b/python/docs/transaction-format.md new file mode 100644 index 000000000..126761bfe --- /dev/null +++ b/python/docs/transaction-format.md @@ -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 + } + } +} + +``` diff --git a/python/src/trezorlib/cli/btc.py b/python/src/trezorlib/cli/btc.py index f66becdb4..24c10ada3 100644 --- a/python/src/trezorlib/cli/btc.py +++ b/python/src/trezorlib/cli/btc.py @@ -16,11 +16,10 @@ import base64 import json -import sys import click -from .. import btc, coins, messages, protobuf, tools +from .. import btc, messages, protobuf, tools from . import ChoiceType INPUT_SCRIPTS = { @@ -100,19 +99,22 @@ def get_public_node(connect, coin, address, curve, script_type, show_display): @cli.command() -@click.option("-c", "--coin") -@click.argument("json_file", type=click.File(), required=False) +@click.option("-c", "--coin", "_ignore", is_flag=True, hidden=True, expose_value=False) +@click.argument("json_file", type=click.File()) @click.pass_obj -def sign_tx(connect, coin, json_file): - """Sign transaction.""" - client = connect() - coin = coin or DEFAULT_COIN +def sign_tx(connect, json_file): + """Sign transaction. - if json_file is None: - return _sign_interactive(client, coin) + Transaction data must be provided in a JSON file. See `transaction-format.md` for + description. You can use `tools/build_tx.py` from the source distribution to build + the required JSON file interactively: + + $ python3 tools/build_tx.py | trezorctl btc sign-tx - + """ + client = connect() data = json.load(json_file) - coin = data.get("coin_name", coin) + coin = data.get("coin_name", DEFAULT_COIN) details = protobuf.dict_to_proto(messages.SignTx, data.get("details", {})) inputs = [ protobuf.dict_to_proto(messages.TxInputType, i) for i in data.get("inputs", ()) @@ -172,153 +174,3 @@ def verify_message(connect, coin, address, signature, message): # # deprecated interactive signing # ALL BELOW is legacy code and will be dropped - - -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_types[script_type] - - -def _get_inputs_interactive(coin_data, txapi): - def outpoint(s): - txid, vout = s.split(":") - return bytes.fromhex(txid), int(vout) - - inputs = [] - txes = {} - while True: - click.echo() - prev = click.prompt( - "Previous output to spend (txid:vout)", type=outpoint, default="" - ) - if not prev: - break - prev_hash, prev_index = prev - address_n = click.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 - click.echo("Prefilling input amount: {}".format(amount)) - except Exception as e: - print(e) - click.echo("Failed to fetch transation. This might bite you later.") - amount = click.prompt("Input amount (satoshis)", type=int, default=0) - - sequence = click.prompt( - "Sequence Number to use (RBF opt-in enabled by default)", - type=int, - default=0xFFFFFFFD, - ) - script_type = click.prompt( - "Input type", - type=ChoiceType(INPUT_SCRIPTS), - default=_default_script_type(address_n, INPUT_SCRIPTS), - ) - - 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: - click.echo() - address = click.prompt("Output address (for non-change output)", default="") - if address: - address_n = None - else: - address = None - address_n = click.prompt( - "BIP-32 path (for change output)", type=tools.parse_path, default="" - ) - if not address_n: - break - amount = click.prompt("Amount to spend (satoshis)", type=int) - script_type = click.prompt( - "Output type", - type=ChoiceType(OUTPUT_SCRIPTS), - default=_default_script_type(address_n, OUTPUT_SCRIPTS), - ) - outputs.append( - messages.TxOutputType( - address_n=address_n, - address=address, - amount=amount, - script_type=script_type, - ) - ) - - return outputs - - -def _sign_interactive(client, coin): - click.echo("Warning: interactive sign-tx mode is deprecated.", err=True) - click.echo( - "Instead, you should format your transaction data as JSON and " - "supply the file as an argument to sign-tx", - err=True, - ) - if coin in coins.tx_api: - coin_data = coins.by_name[coin] - txapi = coins.tx_api[coin] - else: - click.echo('Coin "%s" is not recognized.' % coin, err=True) - click.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 = click.prompt("Transaction version", type=int, default=2) - signtx.lock_time = click.prompt("Transaction locktime", type=int, default=0) - if coin == "Capricoin": - signtx.timestamp = click.prompt("Transaction timestamp", type=int) - - _, serialized_tx = btc.sign_tx( - client, coin, inputs, outputs, details=signtx, prev_txes=txes - ) - - click.echo() - click.echo("Signed Transaction:") - click.echo(serialized_tx.hex()) - click.echo() - click.echo("Use the following form to broadcast it to the network:") - click.echo(txapi.pushtx_url) diff --git a/python/tools/build_tx.py b/python/tools/build_tx.py new file mode 100755 index 000000000..3d0931c6b --- /dev/null +++ b/python/tools/build_tx.py @@ -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 . + +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()