Skip to content

Commit 28bf5ec

Browse files
committed
tests: Add test feature_sighash_rangeproof.py
1 parent 4c84e34 commit 28bf5ec

File tree

6 files changed

+416
-10
lines changed

6 files changed

+416
-10
lines changed
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2019 The Elements 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+
"""
7+
Test the post-dynafed elements-only SIGHASH_RANGEPROOF sighash flag.
8+
"""
9+
10+
import struct
11+
from test_framework.test_framework import BitcoinTestFramework
12+
from test_framework.script import (
13+
hash160,
14+
SignatureHash,
15+
SegwitVersion1SignatureHash,
16+
SIGHASH_ALL,
17+
SIGHASH_SINGLE,
18+
SIGHASH_NONE,
19+
SIGHASH_ANYONECANPAY,
20+
SIGHASH_RANGEPROOF,
21+
CScript,
22+
CScriptOp,
23+
FindAndDelete,
24+
OP_CODESEPARATOR,
25+
OP_CHECKSIG,
26+
OP_DUP,
27+
OP_EQUALVERIFY,
28+
OP_HASH160,
29+
)
30+
from test_framework.key import ECKey
31+
32+
from test_framework.messages import (
33+
CBlock,
34+
CTransaction,
35+
CTxOut,
36+
FromHex,
37+
WitToHex,
38+
hash256, uint256_from_str, ser_uint256, ser_string, ser_vector
39+
)
40+
41+
from test_framework import util
42+
from test_framework.util import (
43+
assert_equal,
44+
hex_str_to_bytes,
45+
assert_raises_rpc_error,
46+
)
47+
48+
from test_framework.blocktools import add_witness_commitment
49+
50+
def get_p2pkh_script(pubkeyhash):
51+
"""Get the script associated with a P2PKH."""
52+
return CScript([CScriptOp(OP_DUP), CScriptOp(OP_HASH160), pubkeyhash, CScriptOp(OP_EQUALVERIFY), CScriptOp(OP_CHECKSIG)])
53+
54+
def SignatureHash_legacy(script, txTo, inIdx, hashtype):
55+
"""
56+
This method is identical to the regular `SignatureHash` method,
57+
but without support for SIGHASH_RANGEPROOF.
58+
So basically it's the old version of the method from before the
59+
new sighash flag was added.
60+
"""
61+
HASH_ONE = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
62+
63+
if inIdx >= len(txTo.vin):
64+
return (HASH_ONE, "inIdx %d out of range (%d)" % (inIdx, len(txTo.vin)))
65+
txtmp = CTransaction(txTo)
66+
67+
for txin in txtmp.vin:
68+
txin.scriptSig = b''
69+
txtmp.vin[inIdx].scriptSig = FindAndDelete(script, CScript([OP_CODESEPARATOR]))
70+
71+
if (hashtype & 0x1f) == SIGHASH_NONE:
72+
txtmp.vout = []
73+
74+
for i in range(len(txtmp.vin)):
75+
if i != inIdx:
76+
txtmp.vin[i].nSequence = 0
77+
78+
elif (hashtype & 0x1f) == SIGHASH_SINGLE:
79+
outIdx = inIdx
80+
if outIdx >= len(txtmp.vout):
81+
return (HASH_ONE, "outIdx %d out of range (%d)" % (outIdx, len(txtmp.vout)))
82+
83+
tmp = txtmp.vout[outIdx]
84+
txtmp.vout = []
85+
for i in range(outIdx):
86+
txtmp.vout.append(CTxOut(-1))
87+
txtmp.vout.append(tmp)
88+
89+
for i in range(len(txtmp.vin)):
90+
if i != inIdx:
91+
txtmp.vin[i].nSequence = 0
92+
93+
if hashtype & SIGHASH_ANYONECANPAY:
94+
tmp = txtmp.vin[inIdx]
95+
txtmp.vin = []
96+
txtmp.vin.append(tmp)
97+
98+
# sighash serialization is different from non-witness serialization
99+
# do manual sighash serialization:
100+
s = b""
101+
s += struct.pack("<i", txtmp.nVersion)
102+
s += ser_vector(txtmp.vin)
103+
s += ser_vector(txtmp.vout)
104+
s += struct.pack("<I", txtmp.nLockTime)
105+
106+
# add sighash type
107+
s += struct.pack(b"<I", hashtype)
108+
109+
hash = hash256(s)
110+
111+
return (hash, None)
112+
113+
def SegwitVersion1SignatureHash_legacy(script, txTo, inIdx, hashtype, amount):
114+
"""
115+
This method is identical to the regular `SegwitVersion1SignatureHash`
116+
method, but without support for SIGHASH_RANGEPROOF.
117+
So basically it's the old version of the method from before the
118+
new sighash flag was added.
119+
"""
120+
121+
hashPrevouts = 0
122+
hashSequence = 0
123+
hashIssuance = 0
124+
hashOutputs = 0
125+
126+
if not (hashtype & SIGHASH_ANYONECANPAY):
127+
serialize_prevouts = bytes()
128+
for i in txTo.vin:
129+
serialize_prevouts += i.prevout.serialize()
130+
hashPrevouts = uint256_from_str(hash256(serialize_prevouts))
131+
132+
if (not (hashtype & SIGHASH_ANYONECANPAY) and (hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
133+
serialize_sequence = bytes()
134+
for i in txTo.vin:
135+
serialize_sequence += struct.pack("<I", i.nSequence)
136+
hashSequence = uint256_from_str(hash256(serialize_sequence))
137+
138+
if not (hashtype & SIGHASH_ANYONECANPAY):
139+
serialize_issuance = bytes()
140+
# TODO actually serialize issuances
141+
for _ in txTo.vin:
142+
serialize_issuance += b'\x00'
143+
hashIssuance = uint256_from_str(hash256(serialize_issuance))
144+
145+
if ((hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
146+
serialize_outputs = bytes()
147+
for o in txTo.vout:
148+
serialize_outputs += o.serialize()
149+
hashOutputs = uint256_from_str(hash256(serialize_outputs))
150+
elif ((hashtype & 0x1f) == SIGHASH_SINGLE and inIdx < len(txTo.vout)):
151+
serialize_outputs = txTo.vout[inIdx].serialize()
152+
hashOutputs = uint256_from_str(hash256(serialize_outputs))
153+
154+
ss = bytes()
155+
ss += struct.pack("<i", txTo.nVersion)
156+
ss += ser_uint256(hashPrevouts)
157+
ss += ser_uint256(hashSequence)
158+
ss += ser_uint256(hashIssuance)
159+
ss += txTo.vin[inIdx].prevout.serialize()
160+
ss += ser_string(script)
161+
ss += amount.serialize()
162+
ss += struct.pack("<I", txTo.vin[inIdx].nSequence)
163+
ss += ser_uint256(hashOutputs)
164+
ss += struct.pack("<i", txTo.nLockTime)
165+
ss += struct.pack("<I", hashtype)
166+
167+
return hash256(ss)
168+
169+
170+
class SighashRangeproofTest(BitcoinTestFramework):
171+
def set_test_params(self):
172+
self.setup_clean_chain = True
173+
self.num_nodes = 3
174+
# We want to test activation of dynafed
175+
args = ["-con_dyna_deploy_start=1000", "-blindedaddresses=1", "-initialfreecoins=2100000000000000", "-con_blocksubsidy=0", "-con_connect_genesis_outputs=1", "-txindex=1"]
176+
self.extra_args = [args] * self.num_nodes
177+
self.extra_args[0].append("-anyonecanspendaremine=1") # first node gets the coins
178+
179+
def skip_test_if_missing_module(self):
180+
self.skip_if_no_wallet()
181+
182+
def prepare_tx_signed_with_sighash(self, address_type, sighash_rangeproof_aware):
183+
# Create a tx that is signed with a specific version of the sighash
184+
# method.
185+
# If `sighash_rangeproof_aware` is
186+
# true, the sighash will contain the rangeproofs if SIGHASH_RANGEPROOF is set
187+
# false, the sighash will NOT contain the rangeproofs if SIGHASH_RANGEPROOF is set
188+
189+
addr = self.nodes[1].getnewaddress("", address_type)
190+
assert len(self.nodes[1].getaddressinfo(addr)["confidential_key"]) > 0
191+
self.nodes[0].generate(1)
192+
self.sync_all()
193+
utxo = self.nodes[1].listunspent(1, 1, [addr])[0]
194+
utxo_tx = FromHex(CTransaction(), self.nodes[1].getrawtransaction(utxo["txid"]))
195+
utxo_spk = CScript(hex_str_to_bytes(utxo["scriptPubKey"]))
196+
utxo_value = utxo_tx.vout[utxo["vout"]].nValue
197+
198+
assert len(utxo["amountblinder"]) > 0
199+
sink_addr = self.nodes[2].getnewaddress()
200+
unsigned_hex = self.nodes[1].createrawtransaction(
201+
[{"txid": utxo["txid"], "vout": utxo["vout"]}],
202+
{sink_addr: 0.9, "fee": 0.1}
203+
)
204+
blinded_hex = self.nodes[1].blindrawtransaction(unsigned_hex)
205+
blinded_tx = FromHex(CTransaction(), blinded_hex)
206+
signed_hex = self.nodes[1].signrawtransactionwithwallet(blinded_hex)["hex"]
207+
signed_tx = FromHex(CTransaction(), signed_hex)
208+
209+
# Make sure that the tx the node produced is always valid.
210+
test_accept = self.nodes[0].testmempoolaccept([signed_hex])[0]
211+
assert test_accept["allowed"], "not accepted: {}".format(test_accept["reject-reason"])
212+
213+
# Prepare the keypair we need to re-sign the tx.
214+
wif = self.nodes[1].dumpprivkey(addr)
215+
privkey = ECKey()
216+
privkey.set_wif(wif)
217+
pubkey = privkey.get_pubkey()
218+
219+
# Now we need to replace the signature with an equivalent one with the new sighash set.
220+
hashtype = SIGHASH_ALL | SIGHASH_RANGEPROOF
221+
if address_type == "legacy":
222+
if sighash_rangeproof_aware:
223+
(sighash, _) = SignatureHash(utxo_spk, blinded_tx, 0, hashtype)
224+
else:
225+
(sighash, _) = SignatureHash_legacy(utxo_spk, blinded_tx, 0, hashtype)
226+
signature = privkey.sign_ecdsa(sighash) + chr(hashtype).encode('latin-1')
227+
assert len(signature) <= 0xfc
228+
assert len(pubkey.get_bytes()) <= 0xfc
229+
signed_tx.vin[0].scriptSig = CScript(
230+
struct.pack("<B", len(signature)) + signature
231+
+ struct.pack("<B", len(pubkey.get_bytes())) + pubkey.get_bytes()
232+
)
233+
elif address_type == "bech32" or address_type == "p2sh-segwit":
234+
assert signed_tx.wit.vtxinwit[0].scriptWitness.stack[1] == pubkey.get_bytes()
235+
pubkeyhash = hash160(pubkey.get_bytes())
236+
script = get_p2pkh_script(pubkeyhash)
237+
if sighash_rangeproof_aware:
238+
sighash = SegwitVersion1SignatureHash(script, blinded_tx, 0, hashtype, utxo_value)
239+
else:
240+
sighash = SegwitVersion1SignatureHash_legacy(script, blinded_tx, 0, hashtype, utxo_value)
241+
signature = privkey.sign_ecdsa(sighash) + chr(hashtype).encode('latin-1')
242+
signed_tx.wit.vtxinwit[0].scriptWitness.stack[0] = signature
243+
else:
244+
assert False
245+
246+
signed_tx.rehash()
247+
return signed_tx
248+
249+
def assert_tx_standard(self, tx, assert_standard=True):
250+
# Test the standardness of the tx by submitting it to the mempool.
251+
252+
test_accept = self.nodes[0].testmempoolaccept([WitToHex(tx)])[0]
253+
if assert_standard:
254+
assert test_accept["allowed"], "tx was not accepted: {}".format(test_accept["reject-reason"])
255+
else:
256+
assert not test_accept["allowed"], "tx was accepted"
257+
258+
def assert_tx_valid(self, tx, assert_valid=True):
259+
# Test the validity of the transaction by manually mining a block that contains the tx.
260+
261+
block = FromHex(CBlock(), self.nodes[2].getnewblockhex())
262+
assert len(block.vtx) > 0
263+
block.vtx.append(tx)
264+
block.hashMerkleRoot = block.calc_merkle_root()
265+
add_witness_commitment(block)
266+
block.solve()
267+
block_hex = WitToHex(block)
268+
269+
# First test the testproposed block RPC.
270+
if assert_valid:
271+
self.nodes[0].testproposedblock(block_hex)
272+
else:
273+
assert_raises_rpc_error(-25, "block-validation-failed", self.nodes[0].testproposedblock, block_hex)
274+
275+
# Then try submit the block and check if it was accepted or not.
276+
pre = self.nodes[0].getblockcount()
277+
self.nodes[0].submitblock(block_hex)
278+
post = self.nodes[0].getblockcount()
279+
280+
if assert_valid:
281+
# assert block was accepted
282+
assert pre < post
283+
else:
284+
# assert block was not accepted
285+
assert pre == post
286+
287+
def run_test(self):
288+
util.node_fastmerkle = self.nodes[0]
289+
ADDRESS_TYPES = ["legacy", "bech32", "p2sh-segwit"]
290+
291+
# Different test scenarios.
292+
# - before activation, using the flag is non-standard
293+
# - before activation, using the flag but a non-flag-aware signature is legal
294+
# - after activation, using the flag but a non-flag-aware signature is illegal
295+
# - after activation, using the flag is standard (and thus also legal)
296+
297+
# Mine come coins for node 0.
298+
self.nodes[0].generate(200)
299+
self.sync_all()
300+
301+
# Ensure that if we use the SIGHASH_RANGEPROOF flag before it's activated,
302+
# - the tx is not accepted in the mempool and
303+
# - the tx is accepted if manually mined in a block
304+
for address_type in ADDRESS_TYPES:
305+
self.log.info("Pre-activation for {} address".format(address_type))
306+
tx = self.prepare_tx_signed_with_sighash(address_type, False)
307+
self.assert_tx_standard(tx, False)
308+
self.assert_tx_valid(tx, True)
309+
310+
# Activate dynafed (nb of blocks taken from dynafed activation test)
311+
self.nodes[0].generate(1006 + 1 + 144 + 144)
312+
assert_equal(self.nodes[0].getblockchaininfo()["bip9_softforks"]["dynafed"]["status"], "active")
313+
self.sync_all()
314+
315+
# Test that the use of SIGHASH_RANGEPROOF is legal and standard
316+
# after activation.
317+
for address_type in ADDRESS_TYPES:
318+
self.log.info("Post-activation for {} address".format(address_type))
319+
tx = self.prepare_tx_signed_with_sighash(address_type, True)
320+
self.assert_tx_standard(tx, True)
321+
self.assert_tx_valid(tx, True)
322+
323+
# Ensure that if we then use the old sighash algorith that doesn't hash
324+
# the rangeproofs, the signature is no longer valid.
325+
for address_type in ADDRESS_TYPES:
326+
self.log.info("Post-activation invalid sighash for {} address".format(address_type))
327+
tx = self.prepare_tx_signed_with_sighash(address_type, False)
328+
self.assert_tx_standard(tx, False)
329+
self.assert_tx_valid(tx, False)
330+
331+
if __name__ == '__main__':
332+
SighashRangeproofTest().main()
333+

test/functional/feature_txwitness.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
1515
"""
1616

17-
from test_framework.messages import CTransaction, CBlock, ser_uint256, FromHex, uint256_from_str, CTxOut, ToHex, CTxIn, COutPoint, OUTPOINT_ISSUANCE_FLAG, ser_string
17+
from test_framework.messages import CTransaction, CBlock, ser_uint256, FromHex, uint256_from_str, CTxOut, ToHex, WitToHex, CTxIn, COutPoint, OUTPOINT_ISSUANCE_FLAG, ser_string
1818
from test_framework.test_framework import BitcoinTestFramework
1919
from test_framework.util import assert_equal, bytes_to_hex_str, hex_str_to_bytes, assert_raises_rpc_error, assert_greater_than
2020
from test_framework import util
@@ -126,9 +126,6 @@ def test_transaction_serialization(self):
126126

127127
def test_coinbase_witness(self):
128128

129-
def WitToHex(obj):
130-
return bytes_to_hex_str(obj.serialize(with_witness=True))
131-
132129
block = self.nodes[0].getnewblockhex()
133130
block_struct = FromHex(CBlock(), block)
134131

test/functional/test_framework/address.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""Encode and decode BASE58, P2PKH and P2SH addresses."""
66

77
from .script import hash256, hash160, sha256, CScript, OP_0
8-
from .util import bytes_to_hex_str, hex_str_to_bytes
8+
from .util import bytes_to_hex_str, hex_str_to_bytes, assert_equal
99

1010
from . import segwit_addr
1111

@@ -29,7 +29,36 @@ def byte_to_base58(b, version):
2929
str = str[2:]
3030
return result
3131

32-
# TODO: def base58_decode
32+
33+
def base58_to_byte(s):
34+
"""Converts a base58-encoded string to its data and version.
35+
36+
Throws if the base58 checksum is invalid."""
37+
if not s:
38+
return b''
39+
n = 0
40+
for c in s:
41+
n *= 58
42+
assert c in chars
43+
digit = chars.index(c)
44+
n += digit
45+
h = '%x' % n
46+
if len(h) % 2:
47+
h = '0' + h
48+
res = n.to_bytes((n.bit_length() + 7) // 8, 'big')
49+
pad = 0
50+
for c in s:
51+
if c == chars[0]:
52+
pad += 1
53+
else:
54+
break
55+
res = b'\x00' * pad + res
56+
57+
# Assert if the checksum is invalid
58+
assert_equal(hash256(res[:-4])[:4], res[-4:])
59+
60+
return res[1:-4], int(res[0])
61+
3362

3463
def keyhash_to_p2pkh(hash, main = False):
3564
assert (len(hash) == 20)

0 commit comments

Comments
 (0)