Skip to content

Commit d67f89b

Browse files
committed
Merge bitcoin#25625: test: add test for decoding PSBT with per-input preimage types
71a751f test: add test for decoding PSBT with per-input preimage types (Sebastian Falbesoner) faf4337 refactor: move helper `random_bytes` to util library (Sebastian Falbesoner) fdc1ca3 test: add constants for PSBT key types (BIP 174) (Sebastian Falbesoner) 1b035c0 refactor: move PSBT(Map) helpers from signet miner to test framework (Sebastian Falbesoner) 7c0dfec refactor: move `from_binary` helper from signet miner to test framework (Sebastian Falbesoner) 597a4b3 scripted-diff: rename `FromBinary` helper to `from_binary` (signet miner) (Sebastian Falbesoner) Pull request description: This PR adds missing test coverage for the `decodepsbt` RPC in the case that a PSBT with on of the per-input preimage types (`PSBT_IN_RIPEMD160`, `PSBT_IN_SHA256`, `PSBT_IN_HASH160`, `PSBT_IN_HASH256`; see [BIP 174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Specification)) is passed. As preparation, the first four commits move the already existing helpers for (de)serialization of PSBTs and PSBTMaps from the signet miner to the test framework (in a new module `psbt.py`), which should be quite useful for further tests to easily create PSBTs. ACKs for top commit: achow101: ACK 71a751f Tree-SHA512: 04f2671612d94029da2ac8dc15ff93c4c8fcb73fe0b8cf5970509208564df1f5e32319b53ae998dd6e544d37637a9b75609f27a3685da51f603f6ed0555635fb
2 parents 5c82ca3 + 71a751f commit d67f89b

File tree

6 files changed

+212
-98
lines changed

6 files changed

+212
-98
lines changed

contrib/signet/miner

Lines changed: 8 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55

66
import argparse
7-
import base64
87
import json
98
import logging
109
import math
@@ -15,14 +14,13 @@ import sys
1514
import time
1615
import subprocess
1716

18-
from io import BytesIO
19-
2017
PATH_BASE_CONTRIB_SIGNET = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
2118
PATH_BASE_TEST_FUNCTIONAL = os.path.abspath(os.path.join(PATH_BASE_CONTRIB_SIGNET, "..", "..", "test", "functional"))
2219
sys.path.insert(0, PATH_BASE_TEST_FUNCTIONAL)
2320

2421
from test_framework.blocktools import get_witness_script, script_BIP34_coinbase_height # noqa: E402
25-
from test_framework.messages import CBlock, CBlockHeader, COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, from_hex, deser_string, ser_compact_size, ser_string, ser_uint256, tx_from_hex # noqa: E402
22+
from test_framework.messages import CBlock, CBlockHeader, COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, from_binary, from_hex, ser_string, ser_uint256, tx_from_hex # noqa: E402
23+
from test_framework.psbt import PSBT, PSBTMap, PSBT_GLOBAL_UNSIGNED_TX, PSBT_IN_FINAL_SCRIPTSIG, PSBT_IN_FINAL_SCRIPTWITNESS, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_SIGHASH_TYPE # noqa: E402
2624
from test_framework.script import CScriptOp # noqa: E402
2725

2826
logging.basicConfig(
@@ -34,89 +32,6 @@ SIGNET_HEADER = b"\xec\xc7\xda\xa2"
3432
PSBT_SIGNET_BLOCK = b"\xfc\x06signetb" # proprietary PSBT global field holding the block being signed
3533
RE_MULTIMINER = re.compile("^(\d+)(-(\d+))?/(\d+)$")
3634

37-
# #### some helpers that could go into test_framework
38-
39-
# like from_hex, but without the hex part
40-
def FromBinary(cls, stream):
41-
"""deserialize a binary stream (or bytes object) into an object"""
42-
# handle bytes object by turning it into a stream
43-
was_bytes = isinstance(stream, bytes)
44-
if was_bytes:
45-
stream = BytesIO(stream)
46-
obj = cls()
47-
obj.deserialize(stream)
48-
if was_bytes:
49-
assert len(stream.read()) == 0
50-
return obj
51-
52-
class PSBTMap:
53-
"""Class for serializing and deserializing PSBT maps"""
54-
55-
def __init__(self, map=None):
56-
self.map = map if map is not None else {}
57-
58-
def deserialize(self, f):
59-
m = {}
60-
while True:
61-
k = deser_string(f)
62-
if len(k) == 0:
63-
break
64-
v = deser_string(f)
65-
if len(k) == 1:
66-
k = k[0]
67-
assert k not in m
68-
m[k] = v
69-
self.map = m
70-
71-
def serialize(self):
72-
m = b""
73-
for k,v in self.map.items():
74-
if isinstance(k, int) and 0 <= k and k <= 255:
75-
k = bytes([k])
76-
m += ser_compact_size(len(k)) + k
77-
m += ser_compact_size(len(v)) + v
78-
m += b"\x00"
79-
return m
80-
81-
class PSBT:
82-
"""Class for serializing and deserializing PSBTs"""
83-
84-
def __init__(self):
85-
self.g = PSBTMap()
86-
self.i = []
87-
self.o = []
88-
self.tx = None
89-
90-
def deserialize(self, f):
91-
assert f.read(5) == b"psbt\xff"
92-
self.g = FromBinary(PSBTMap, f)
93-
assert 0 in self.g.map
94-
self.tx = FromBinary(CTransaction, self.g.map[0])
95-
self.i = [FromBinary(PSBTMap, f) for _ in self.tx.vin]
96-
self.o = [FromBinary(PSBTMap, f) for _ in self.tx.vout]
97-
return self
98-
99-
def serialize(self):
100-
assert isinstance(self.g, PSBTMap)
101-
assert isinstance(self.i, list) and all(isinstance(x, PSBTMap) for x in self.i)
102-
assert isinstance(self.o, list) and all(isinstance(x, PSBTMap) for x in self.o)
103-
assert 0 in self.g.map
104-
tx = FromBinary(CTransaction, self.g.map[0])
105-
assert len(tx.vin) == len(self.i)
106-
assert len(tx.vout) == len(self.o)
107-
108-
psbt = [x.serialize() for x in [self.g] + self.i + self.o]
109-
return b"psbt\xff" + b"".join(psbt)
110-
111-
def to_base64(self):
112-
return base64.b64encode(self.serialize()).decode("utf8")
113-
114-
@classmethod
115-
def from_base64(cls, b64psbt):
116-
return FromBinary(cls, base64.b64decode(b64psbt))
117-
118-
# #####
119-
12035
def create_coinbase(height, value, spk):
12136
cb = CTransaction()
12237
cb.vin = [CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), 0xffffffff)]
@@ -159,11 +74,11 @@ def signet_txs(block, challenge):
15974

16075
def do_createpsbt(block, signme, spendme):
16176
psbt = PSBT()
162-
psbt.g = PSBTMap( {0: signme.serialize(),
77+
psbt.g = PSBTMap( {PSBT_GLOBAL_UNSIGNED_TX: signme.serialize(),
16378
PSBT_SIGNET_BLOCK: block.serialize()
16479
} )
165-
psbt.i = [ PSBTMap( {0: spendme.serialize(),
166-
3: bytes([1,0,0,0])})
80+
psbt.i = [ PSBTMap( {PSBT_IN_NON_WITNESS_UTXO: spendme.serialize(),
81+
PSBT_IN_SIGHASH_TYPE: bytes([1,0,0,0])})
16782
]
16883
psbt.o = [ PSBTMap() ]
16984
return psbt.to_base64()
@@ -175,10 +90,10 @@ def do_decode_psbt(b64psbt):
17590
assert len(psbt.tx.vout) == 1
17691
assert PSBT_SIGNET_BLOCK in psbt.g.map
17792

178-
scriptSig = psbt.i[0].map.get(7, b"")
179-
scriptWitness = psbt.i[0].map.get(8, b"\x00")
93+
scriptSig = psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTSIG, b"")
94+
scriptWitness = psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTWITNESS, b"\x00")
18095

181-
return FromBinary(CBlock, psbt.g.map[PSBT_SIGNET_BLOCK]), ser_string(scriptSig) + scriptWitness
96+
return from_binary(CBlock, psbt.g.map[PSBT_SIGNET_BLOCK]), ser_string(scriptSig) + scriptWitness
18297

18398
def finish_block(block, signet_solution, grind_cmd):
18499
block.vtx[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER + signet_solution)

test/functional/feature_taproot.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@
9191
script_to_p2wsh_script,
9292
)
9393
from test_framework.test_framework import BitcoinTestFramework
94-
from test_framework.util import assert_raises_rpc_error, assert_equal
94+
from test_framework.util import (
95+
assert_raises_rpc_error,
96+
assert_equal,
97+
random_bytes,
98+
)
9599
from test_framework.key import generate_privkey, compute_xonly_pubkey, sign_schnorr, tweak_add_privkey, ECKey
96100
from test_framework.address import (
97101
hash160,
@@ -566,10 +570,6 @@ def random_checksig_style(pubkey):
566570
ret = CScript([pubkey, opcode])
567571
return bytes(ret)
568572

569-
def random_bytes(n):
570-
"""Return a random bytes object of length n."""
571-
return bytes(random.getrandbits(8) for i in range(n))
572-
573573
def bitflipper(expr):
574574
"""Return a callable that evaluates expr and returns it with a random bitflip."""
575575
def fn(ctx):

test/functional/rpc_psbt.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,23 @@
1111
from test_framework.descriptors import descsum_create
1212
from test_framework.key import ECKey, H_POINT
1313
from test_framework.messages import (
14+
COutPoint,
15+
CTransaction,
16+
CTxIn,
17+
CTxOut,
1418
MAX_BIP125_RBF_SEQUENCE,
1519
WITNESS_SCALE_FACTOR,
1620
ser_compact_size,
1721
)
22+
from test_framework.psbt import (
23+
PSBT,
24+
PSBTMap,
25+
PSBT_GLOBAL_UNSIGNED_TX,
26+
PSBT_IN_RIPEMD160,
27+
PSBT_IN_SHA256,
28+
PSBT_IN_HASH160,
29+
PSBT_IN_HASH256,
30+
)
1831
from test_framework.test_framework import BitcoinTestFramework
1932
from test_framework.util import (
2033
assert_approx,
@@ -23,6 +36,7 @@
2336
assert_raises_rpc_error,
2437
find_output,
2538
find_vout_for_address,
39+
random_bytes,
2640
)
2741
from test_framework.wallet_util import bytes_to_wif
2842

@@ -775,5 +789,37 @@ def test_psbt_input_keys(psbt_input, keys):
775789
self.nodes[0].sendrawtransaction(rawtx)
776790
self.generate(self.nodes[0], 1)
777791

792+
self.log.info("Test decoding PSBT with per-input preimage types")
793+
# note that the decodepsbt RPC doesn't check whether preimages and hashes match
794+
hash_ripemd160, preimage_ripemd160 = random_bytes(20), random_bytes(50)
795+
hash_sha256, preimage_sha256 = random_bytes(32), random_bytes(50)
796+
hash_hash160, preimage_hash160 = random_bytes(20), random_bytes(50)
797+
hash_hash256, preimage_hash256 = random_bytes(32), random_bytes(50)
798+
799+
tx = CTransaction()
800+
tx.vin = [CTxIn(outpoint=COutPoint(hash=int('aa' * 32, 16), n=0), scriptSig=b""),
801+
CTxIn(outpoint=COutPoint(hash=int('bb' * 32, 16), n=0), scriptSig=b""),
802+
CTxIn(outpoint=COutPoint(hash=int('cc' * 32, 16), n=0), scriptSig=b""),
803+
CTxIn(outpoint=COutPoint(hash=int('dd' * 32, 16), n=0), scriptSig=b"")]
804+
tx.vout = [CTxOut(nValue=0, scriptPubKey=b"")]
805+
psbt = PSBT()
806+
psbt.g = PSBTMap({PSBT_GLOBAL_UNSIGNED_TX: tx.serialize()})
807+
psbt.i = [PSBTMap({bytes([PSBT_IN_RIPEMD160]) + hash_ripemd160: preimage_ripemd160}),
808+
PSBTMap({bytes([PSBT_IN_SHA256]) + hash_sha256: preimage_sha256}),
809+
PSBTMap({bytes([PSBT_IN_HASH160]) + hash_hash160: preimage_hash160}),
810+
PSBTMap({bytes([PSBT_IN_HASH256]) + hash_hash256: preimage_hash256})]
811+
psbt.o = [PSBTMap()]
812+
res_inputs = self.nodes[0].decodepsbt(psbt.to_base64())["inputs"]
813+
assert_equal(len(res_inputs), 4)
814+
preimage_keys = ["ripemd160_preimages", "sha256_preimages", "hash160_preimages", "hash256_preimages"]
815+
expected_hashes = [hash_ripemd160, hash_sha256, hash_hash160, hash_hash256]
816+
expected_preimages = [preimage_ripemd160, preimage_sha256, preimage_hash160, preimage_hash256]
817+
for res_input, preimage_key, hash, preimage in zip(res_inputs, preimage_keys, expected_hashes, expected_preimages):
818+
assert preimage_key in res_input
819+
assert_equal(len(res_input[preimage_key]), 1)
820+
assert hash.hex() in res_input[preimage_key]
821+
assert_equal(res_input[preimage_key][hash.hex()], preimage.hex())
822+
823+
778824
if __name__ == '__main__':
779825
PSBTTest().main()

test/functional/test_framework/messages.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,20 @@ def tx_from_hex(hex_string):
208208
return from_hex(CTransaction(), hex_string)
209209

210210

211+
# like from_hex, but without the hex part
212+
def from_binary(cls, stream):
213+
"""deserialize a binary stream (or bytes object) into an object"""
214+
# handle bytes object by turning it into a stream
215+
was_bytes = isinstance(stream, bytes)
216+
if was_bytes:
217+
stream = BytesIO(stream)
218+
obj = cls()
219+
obj.deserialize(stream)
220+
if was_bytes:
221+
assert len(stream.read()) == 0
222+
return obj
223+
224+
211225
# Objects that map to bitcoind objects, which can be serialized/deserialized
212226

213227

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
import base64
7+
8+
from .messages import (
9+
CTransaction,
10+
deser_string,
11+
from_binary,
12+
ser_compact_size,
13+
)
14+
15+
16+
# global types
17+
PSBT_GLOBAL_UNSIGNED_TX = 0x00
18+
PSBT_GLOBAL_XPUB = 0x01
19+
PSBT_GLOBAL_TX_VERSION = 0x02
20+
PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03
21+
PSBT_GLOBAL_INPUT_COUNT = 0x04
22+
PSBT_GLOBAL_OUTPUT_COUNT = 0x05
23+
PSBT_GLOBAL_TX_MODIFIABLE = 0x06
24+
PSBT_GLOBAL_VERSION = 0xfb
25+
PSBT_GLOBAL_PROPRIETARY = 0xfc
26+
27+
# per-input types
28+
PSBT_IN_NON_WITNESS_UTXO = 0x00
29+
PSBT_IN_WITNESS_UTXO = 0x01
30+
PSBT_IN_PARTIAL_SIG = 0x02
31+
PSBT_IN_SIGHASH_TYPE = 0x03
32+
PSBT_IN_REDEEM_SCRIPT = 0x04
33+
PSBT_IN_WITNESS_SCRIPT = 0x05
34+
PSBT_IN_BIP32_DERIVATION = 0x06
35+
PSBT_IN_FINAL_SCRIPTSIG = 0x07
36+
PSBT_IN_FINAL_SCRIPTWITNESS = 0x08
37+
PSBT_IN_POR_COMMITMENT = 0x09
38+
PSBT_IN_RIPEMD160 = 0x0a
39+
PSBT_IN_SHA256 = 0x0b
40+
PSBT_IN_HASH160 = 0x0c
41+
PSBT_IN_HASH256 = 0x0d
42+
PSBT_IN_PREVIOUS_TXID = 0x0e
43+
PSBT_IN_OUTPUT_INDEX = 0x0f
44+
PSBT_IN_SEQUENCE = 0x10
45+
PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11
46+
PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12
47+
PSBT_IN_TAP_KEY_SIG = 0x13
48+
PSBT_IN_TAP_SCRIPT_SIG = 0x14
49+
PSBT_IN_TAP_LEAF_SCRIPT = 0x15
50+
PSBT_IN_TAP_BIP32_DERIVATION = 0x16
51+
PSBT_IN_TAP_INTERNAL_KEY = 0x17
52+
PSBT_IN_TAP_MERKLE_ROOT = 0x18
53+
PSBT_IN_PROPRIETARY = 0xfc
54+
55+
# per-output types
56+
PSBT_OUT_REDEEM_SCRIPT = 0x00
57+
PSBT_OUT_WITNESS_SCRIPT = 0x01
58+
PSBT_OUT_BIP32_DERIVATION = 0x02
59+
PSBT_OUT_AMOUNT = 0x03
60+
PSBT_OUT_SCRIPT = 0x04
61+
PSBT_OUT_TAP_INTERNAL_KEY = 0x05
62+
PSBT_OUT_TAP_TREE = 0x06
63+
PSBT_OUT_TAP_BIP32_DERIVATION = 0x07
64+
PSBT_OUT_PROPRIETARY = 0xfc
65+
66+
67+
class PSBTMap:
68+
"""Class for serializing and deserializing PSBT maps"""
69+
70+
def __init__(self, map=None):
71+
self.map = map if map is not None else {}
72+
73+
def deserialize(self, f):
74+
m = {}
75+
while True:
76+
k = deser_string(f)
77+
if len(k) == 0:
78+
break
79+
v = deser_string(f)
80+
if len(k) == 1:
81+
k = k[0]
82+
assert k not in m
83+
m[k] = v
84+
self.map = m
85+
86+
def serialize(self):
87+
m = b""
88+
for k,v in self.map.items():
89+
if isinstance(k, int) and 0 <= k and k <= 255:
90+
k = bytes([k])
91+
m += ser_compact_size(len(k)) + k
92+
m += ser_compact_size(len(v)) + v
93+
m += b"\x00"
94+
return m
95+
96+
class PSBT:
97+
"""Class for serializing and deserializing PSBTs"""
98+
99+
def __init__(self):
100+
self.g = PSBTMap()
101+
self.i = []
102+
self.o = []
103+
self.tx = None
104+
105+
def deserialize(self, f):
106+
assert f.read(5) == b"psbt\xff"
107+
self.g = from_binary(PSBTMap, f)
108+
assert 0 in self.g.map
109+
self.tx = from_binary(CTransaction, self.g.map[0])
110+
self.i = [from_binary(PSBTMap, f) for _ in self.tx.vin]
111+
self.o = [from_binary(PSBTMap, f) for _ in self.tx.vout]
112+
return self
113+
114+
def serialize(self):
115+
assert isinstance(self.g, PSBTMap)
116+
assert isinstance(self.i, list) and all(isinstance(x, PSBTMap) for x in self.i)
117+
assert isinstance(self.o, list) and all(isinstance(x, PSBTMap) for x in self.o)
118+
assert 0 in self.g.map
119+
tx = from_binary(CTransaction, self.g.map[0])
120+
assert len(tx.vin) == len(self.i)
121+
assert len(tx.vout) == len(self.o)
122+
123+
psbt = [x.serialize() for x in [self.g] + self.i + self.o]
124+
return b"psbt\xff" + b"".join(psbt)
125+
126+
def to_base64(self):
127+
return base64.b64encode(self.serialize()).decode("utf8")
128+
129+
@classmethod
130+
def from_base64(cls, b64psbt):
131+
return from_binary(cls, base64.b64decode(b64psbt))

0 commit comments

Comments
 (0)