Dash: Support spending DIP2 special transaction inputs (#351)

This implements support for spending of TX outputs which are part of a [DIP2](https://github.com/dashpay/dips/blob/master/dip-0002.md) special transaction, especially [DIP4 coinbases](https://github.com/dashpay/dips/blob/master/dip-0004.md#coinbase-special-transaction).

This is for Dash only and thus required the addition of `is_dash` in `tx_api.py`.

Support on the firmware side is not required for this to work as it reuses the logic from Zcash's `extra_data` and `extra_data_len` fields.
pull/25/head
Alexander Block 5 years ago committed by matejcik
parent 6fa8ccfeed
commit d131b70bed

@ -0,0 +1,208 @@
# This file is part of the Trezor project.
#
# Copyright (C) 2012-2018 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>.
from trezorlib import btc, messages as proto
from trezorlib.tools import parse_path
from ..support.tx_cache import tx_cache
from .common import TrezorTest
TX_API = tx_cache("Dash")
class TestMsgSigntxDash(TrezorTest):
def test_send_dash(self):
self.setup_mnemonic_allallall()
inp1 = proto.TxInputType(
address_n=parse_path("44'/5'/0'/0/0"),
# dash:XdTw4G5AWW4cogGd7ayybyBNDbuB45UpgH
amount=1000000000,
prev_hash=bytes.fromhex(
"5579eaa64b2a0233e7d8d037f5a5afc957cedf48f1c4067e9e33ca6df22ab04f"
),
prev_index=1,
script_type=proto.InputScriptType.SPENDADDRESS,
)
out1 = proto.TxOutputType(
address="XpTc36DPAeWmaueNBA9JqCg2GC8XDLKSYe",
amount=999999000,
script_type=proto.OutputScriptType.PAYTOADDRESS,
)
with self.client:
self.client.set_expected_responses(
[
proto.TxRequest(
request_type=proto.RequestType.TXINPUT,
details=proto.TxRequestDetailsType(request_index=0),
),
proto.TxRequest(
request_type=proto.RequestType.TXMETA,
details=proto.TxRequestDetailsType(tx_hash=inp1.prev_hash),
),
proto.TxRequest(
request_type=proto.RequestType.TXINPUT,
details=proto.TxRequestDetailsType(
request_index=0, tx_hash=inp1.prev_hash
),
),
proto.TxRequest(
request_type=proto.RequestType.TXINPUT,
details=proto.TxRequestDetailsType(
request_index=1, tx_hash=inp1.prev_hash
),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(
request_index=0, tx_hash=inp1.prev_hash
),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(
request_index=1, tx_hash=inp1.prev_hash
),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(request_index=0),
),
proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput),
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
proto.TxRequest(
request_type=proto.RequestType.TXINPUT,
details=proto.TxRequestDetailsType(request_index=0),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(request_index=0),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(request_index=0),
),
proto.TxRequest(request_type=proto.RequestType.TXFINISHED),
]
)
_, serialized_tx = btc.sign_tx(
self.client, "Dash", [inp1], [out1], prev_txes=TX_API
)
assert (
serialized_tx.hex()
== "01000000014fb02af26dca339e7e06c4f148dfce57c9afa5f537d0d8e733022a4ba6ea7955010000006a4730440220387be4d1e4b5e355614091416373e99e1a3532b8cc9a8629368060aff2681bdb02200a0c4a5e9eb2ce6adb6c2e01ec8f954463dcc04f531ed8a89a2b40019d5aeb0b012102936f80cac2ba719ddb238646eb6b78a170a55a52a9b9f08c43523a4a6bd5c896ffffffff0118c69a3b000000001976a9149710d6545407e78c326aa8c8ae386ec7f883b0af88ac00000000"
)
def test_send_dash_dip2_input(self):
self.setup_mnemonic_allallall()
inp1 = proto.TxInputType(
address_n=parse_path("44'/5'/0'/0/0"),
# dash:XdTw4G5AWW4cogGd7ayybyBNDbuB45UpgH
amount=4095000260,
prev_hash=bytes.fromhex(
"15575a1c874bd60a819884e116c42e6791c8283ce1fc3b79f0d18531a61bbb8a"
),
prev_index=1,
script_type=proto.InputScriptType.SPENDADDRESS,
)
out1 = proto.TxOutputType(
address_n=parse_path("44'/5'/0'/1/0"),
amount=4000000000,
script_type=proto.OutputScriptType.PAYTOADDRESS,
)
out2 = proto.TxOutputType(
address="XrEFMNkxeipYHgEQKiJuqch8XzwrtfH5fm",
amount=95000000,
script_type=proto.OutputScriptType.PAYTOADDRESS,
)
with self.client:
self.client.set_expected_responses(
[
proto.TxRequest(
request_type=proto.RequestType.TXINPUT,
details=proto.TxRequestDetailsType(request_index=0),
),
proto.TxRequest(
request_type=proto.RequestType.TXMETA,
details=proto.TxRequestDetailsType(tx_hash=inp1.prev_hash),
),
proto.TxRequest(
request_type=proto.RequestType.TXINPUT,
details=proto.TxRequestDetailsType(
request_index=0, tx_hash=inp1.prev_hash
),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(
request_index=0, tx_hash=inp1.prev_hash
),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(
request_index=1, tx_hash=inp1.prev_hash
),
),
proto.TxRequest(
request_type=proto.RequestType.TXEXTRADATA,
details=proto.TxRequestDetailsType(
extra_data_len=39,
extra_data_offset=0,
tx_hash=inp1.prev_hash,
),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(request_index=0),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(request_index=1),
),
proto.ButtonRequest(code=proto.ButtonRequestType.ConfirmOutput),
proto.ButtonRequest(code=proto.ButtonRequestType.SignTx),
proto.TxRequest(
request_type=proto.RequestType.TXINPUT,
details=proto.TxRequestDetailsType(request_index=0),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(request_index=0),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(request_index=1),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(request_index=0),
),
proto.TxRequest(
request_type=proto.RequestType.TXOUTPUT,
details=proto.TxRequestDetailsType(request_index=1),
),
proto.TxRequest(request_type=proto.RequestType.TXFINISHED),
]
)
_, serialized_tx = btc.sign_tx(
self.client, "Dash", [inp1], [out1, out2], prev_txes=TX_API
)
assert (
serialized_tx.hex()
== "01000000018abb1ba63185d1f0793bfce13c28c891672ec416e18498810ad64b871c5a5715010000006b483045022100f0442b6d9c7533cd6f74afa993b280ed9475276d69df4dac631bc3b5591ba71b022051daf125372c1c477681bbd804a6445d8ff6840901854fb0b485b1c6c7866c44012102936f80cac2ba719ddb238646eb6b78a170a55a52a9b9f08c43523a4a6bd5c896ffffffff0200286bee000000001976a914fd61dd017dad1f505c0511142cc9ac51ef3a5beb88acc095a905000000001976a914aa7a6a1f43dfc34d17e562ce1845b804b73fc31e88ac00000000"
)

@ -0,0 +1 @@
{"txid":"15575a1c874bd60a819884e116c42e6791c8283ce1fc3b79f0d18531a61bbb8a","version":3,"type":5,"locktime":0,"extraPayloadSize":38,"extraPayload":"01003a5200009e8ecb69a1493e3d573cc9ce8460b9a0d7b05e77ca17878c9faf73959392cef3","vin":[{"coinbase":"023a52042ab0355c08fabe6d6d0000000000000000000000000000000000000000000000000000000000000000010000000000000010000017cd0000000d2f6e6f64655374726174756d2f","sequence":0,"n":0}],"vout":[{"value":"40.95000262","n":0,"scriptPubKey":{"hex":"76a914cb594917ad4e5849688ec63f29a0f7f3badb5da688ac","asm":"OP_DUP OP_HASH160 cb594917ad4e5849688ec63f29a0f7f3badb5da6 OP_EQUALVERIFY OP_CHECKSIG","addresses":["yereyozxENB9jbhqpbg1coE5c39ExqLSaG"],"type":"pubkeyhash"},"spentTxId":null,"spentIndex":null,"spentHeight":null},{"value":"40.95000260","n":1,"scriptPubKey":{"hex":"76a9141e7754b21a4afe7d229b8ff46e613d1117a6515588ac","asm":"OP_DUP OP_HASH160 1e7754b21a4afe7d229b8ff46e613d1117a65155 OP_EQUALVERIFY OP_CHECKSIG","addresses":["yP6Y5D9bx3ih9RCAgSJNdzbiVtPYXNUE8v"],"type":"pubkeyhash"},"spentTxId":null,"spentIndex":null,"spentHeight":null}],"blockhash":"000000000c1f70ed43e42571dc18c6930de267d63f82640aa13ecee1b2508ed0","blockheight":21050,"confirmations":-635,"time":1547022378,"blocktime":1547022378,"isCoinBase":true,"valueOut":81.90000522,"size":233,"txlock":false,"cbTx":{"version":1,"height":21050,"merkleRootMNList":"f3ce92939573af9f8c8717ca775eb0d7a0b96084cec93c573d3e49a169cb8e9e"}}

@ -0,0 +1 @@
{"txid":"5579eaa64b2a0233e7d8d037f5a5afc957cedf48f1c4067e9e33ca6df22ab04f","version":3,"locktime":21064,"vin":[{"txid":"4f5a33d74e1d2aea6a0d6c374e382a9bc4d5a045dff1969ed62b887558f7a230","vout":0,"sequence":4294967294,"n":0,"scriptSig":{"hex":"473044022003057fa600b72bf970fe56cf8fbaba613ec705563adcb34c4a18eb6a268d722f022070fa9e1f999dacf2b16de9887d5e37374b09fddb7e8856f250de918def31fd0d01210380137151f0cdacdd072ec5fd3b11c68c4509be9d5676d24ca2ba852173c6a899","asm":"3044022003057fa600b72bf970fe56cf8fbaba613ec705563adcb34c4a18eb6a268d722f022070fa9e1f999dacf2b16de9887d5e37374b09fddb7e8856f250de918def31fd0d[ALL] 0380137151f0cdacdd072ec5fd3b11c68c4509be9d5676d24ca2ba852173c6a899"},"addr":"yY8hmtiBVyw74vKmBhdhNxvCfiakuUUBkb","valueSat":1000593,"value":0.01000593,"doubleSpentTxID":null},{"txid":"4f5a33d74e1d2aea6a0d6c374e382a9bc4d5a045dff1969ed62b887558f7a230","vout":1,"sequence":4294967294,"n":1,"scriptSig":{"hex":"483045022100babd26c449eb9718d3906328a392873630df77c5cafd715cfdb880f832f5470c02201a8dce2a0e784696a2bcbd64415e523e8c545773881764e7100354eaddd7337401210219a6894f4d5dd3e78c2215f9feb0d57fcce287bd0f0078ae0d246a58b3e554fb","asm":"3045022100babd26c449eb9718d3906328a392873630df77c5cafd715cfdb880f832f5470c02201a8dce2a0e784696a2bcbd64415e523e8c545773881764e7100354eaddd73374[ALL] 0219a6894f4d5dd3e78c2215f9feb0d57fcce287bd0f0078ae0d246a58b3e554fb"},"addr":"ySCCMgodnW999ehuQt8pe4qivwhfZaduSy","valueSat":1000000000,"value":10,"doubleSpentTxID":null}],"vout":[{"value":"0.01000210","n":0,"scriptPubKey":{"hex":"76a9144f61517912cf3fb9a19a14912bea23792b18bdcc88ac","asm":"OP_DUP OP_HASH160 4f61517912cf3fb9a19a14912bea23792b18bdcc OP_EQUALVERIFY OP_CHECKSIG","addresses":["yTZAp9ZhVcNDVa8ZUiJByTKKeTNHwFpasG"],"type":"pubkeyhash"},"spentTxId":null,"spentIndex":null,"spentHeight":null},{"value":"10.00000000","n":1,"scriptPubKey":{"hex":"76a9141e7754b21a4afe7d229b8ff46e613d1117a6515588ac","asm":"OP_DUP OP_HASH160 1e7754b21a4afe7d229b8ff46e613d1117a65155 OP_EQUALVERIFY OP_CHECKSIG","addresses":["yP6Y5D9bx3ih9RCAgSJNdzbiVtPYXNUE8v"],"type":"pubkeyhash"},"spentTxId":null,"spentIndex":null,"spentHeight":null}],"blockheight":-1,"confirmations":0,"time":1547025147,"valueOut":10.0100021,"size":373,"valueIn":10.01000593,"fees":0.00000383,"txlock":false}

@ -23,6 +23,7 @@ from ..support.tx_cache import tx_cache
TxApiBitcoin = coins.tx_api["Bitcoin"]
TxApiTestnet = tx_cache("Testnet", allow_fetch=False)
TxApiZencash = coins.tx_api["Zencash"]
TxApiDash = tx_cache("Dash", allow_fetch=False)
tests_dir = os.path.dirname(os.path.abspath(__file__))
@ -82,3 +83,26 @@ def test_tx_api_get_block_hash():
assert (
hash.hex() == "000000003f5d6ba1385c6cd2d4f836dfc5adf7f98834309ad67e26faef462454"
)
def test_tx_api_dash_dip2():
# Test if pre-DIP2 TXs are still working as expected
tx = TxApiDash.get_tx(
"acb3b7f259429989fc9c51ae4a5e3e3eab0723dceb21577533ac7c4b4ba4db5d"
)
assert tx.version == 2 # pre-DIP2
assert tx.extra_data is None and tx.extra_data_len is None
# Test if version 3 TX with type=0 is treated as normal TX
tx = TxApiDash.get_tx(
"5579eaa64b2a0233e7d8d037f5a5afc957cedf48f1c4067e9e33ca6df22ab04f"
)
assert tx.version == 3
assert tx.extra_data is None and tx.extra_data_len is None
# Test if DIP2 payloads are initialized correctly
tx = TxApiDash.get_tx(
"15575a1c874bd60a819884e116c42e6791c8283ce1fc3b79f0d18531a61bbb8a"
)
assert tx.version == (3 | (5 << 16)) # DIP2 type 1 (ProRegTx)
assert len(tx.extra_data) == (38 + 1) # real length + varint size

@ -15,6 +15,7 @@
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
import random
import struct
from decimal import Decimal
import requests
@ -32,6 +33,21 @@ def is_capricoin(coin):
return coin["coin_name"].lower().startswith("capricoin")
def is_dash(coin):
return coin["coin_name"].lower().startswith("dash")
def pack_varint(n):
if n < 253:
return struct.pack("<B", n)
elif n <= 0xFFFF:
return struct.pack("<BH", 253, n)
elif n <= 0xFFFFFFFF:
return struct.pack("<BL", 254, n)
else:
return struct.pack("<BQ", 255, n)
def _json_to_input(coin, vin):
i = messages.TxInputType()
if "coinbase" in vin:
@ -104,6 +120,25 @@ def json_to_tx(coin, data):
extra_data_len = 1 + joinsplit_cnt * 1802 + 32 + 64
t.extra_data = rawtx[-extra_data_len:]
if is_dash(coin):
dip2_type = data.get("type", 0)
if t.version == 3 and dip2_type != 0:
# It's a DIP2 special TX with payload
if "extraPayloadSize" not in data or "extraPayload" not in data:
raise ValueError("Payload data missing in DIP2 transaction")
if data["extraPayloadSize"] * 2 != len(data["extraPayload"]):
raise ValueError("length mismatch")
t.extra_data = pack_varint(data["extraPayloadSize"]) + bytes.fromhex(
data["extraPayload"]
)
# Trezor firmware doesn't understand the split of version and type, so let's mimic the
# old serialization format
t.version |= dip2_type << 16
return t

Loading…
Cancel
Save