Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc-schnorr.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2015-2016 The Bitcoin Core developers | # Copyright (c) 2019 The Bitcoin developers | ||||
# Copyright (c) 2017-2019 The Bitcoin developers | |||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
""" | """ | ||||
This tests the treatment of Schnorr transaction signatures: | This tests the treatment of Schnorr transaction signatures: | ||||
- acceptance both in mempool and blocks. | - acceptance both in mempool and blocks. | ||||
- check banning for peers who send txns with 64 byte ECDSA DER sigs. | - check banning for peers who send txns with 64 byte ECDSA DER sigs. | ||||
Derived from abc-replay-protection.py with improvements borrowed from | Derived from a variety of functional tests. | ||||
abc-segwit-recovery-activation.py. Later reduced down to this feature test. | |||||
""" | """ | ||||
import time | import time | ||||
from test_framework.blocktools import ( | from test_framework.blocktools import ( | ||||
create_block, | create_block, | ||||
create_coinbase, | create_coinbase, | ||||
create_transaction, | create_transaction, | ||||
make_conform_to_ctor, | make_conform_to_ctor, | ||||
) | ) | ||||
from test_framework.comptool import RejectResult, TestInstance, TestManager | |||||
from test_framework.key import CECKey | from test_framework.key import CECKey | ||||
from test_framework.messages import ( | from test_framework.messages import ( | ||||
COIN, | COIN, | ||||
COutPoint, | COutPoint, | ||||
CTransaction, | CTransaction, | ||||
CTxIn, | CTxIn, | ||||
CTxOut, | CTxOut, | ||||
msg_tx, | |||||
ToHex, | ToHex, | ||||
) | ) | ||||
from test_framework.mininode import ( | from test_framework.mininode import ( | ||||
network_thread_join, | |||||
network_thread_start, | network_thread_start, | ||||
P2PInterface, | P2PDataStore, | ||||
) | ) | ||||
from test_framework import schnorr | from test_framework import schnorr | ||||
from test_framework.script import ( | from test_framework.script import ( | ||||
CScript, | CScript, | ||||
OP_1, | OP_1, | ||||
OP_CHECKMULTISIG, | OP_CHECKMULTISIG, | ||||
OP_CHECKSIG, | OP_CHECKSIG, | ||||
OP_TRUE, | OP_TRUE, | ||||
SIGHASH_ALL, | SIGHASH_ALL, | ||||
SIGHASH_FORKID, | SIGHASH_FORKID, | ||||
SignatureHashForkId, | SignatureHashForkId, | ||||
) | ) | ||||
from test_framework.test_framework import BitcoinTestFramework | from test_framework.test_framework import BitcoinTestFramework | ||||
from test_framework.util import assert_raises_rpc_error, sync_blocks | from test_framework.util import assert_raises_rpc_error | ||||
# A mandatory (bannable) error occurs when people pass Schnorr signatures into OP_CHECKMULTISIG. | # A mandatory (bannable) error occurs when people pass Schnorr signatures into OP_CHECKMULTISIG. | ||||
RPC_SCHNORR_MULTISIG_ERROR = 'mandatory-script-verify-flag-failed (Signature cannot be 65 bytes in CHECKMULTISIG) (code 16)' | RPC_SCHNORR_MULTISIG_ERROR = 'mandatory-script-verify-flag-failed (Signature cannot be 65 bytes in CHECKMULTISIG) (code 16)' | ||||
# A mandatory (bannable) error occurs when people send invalid Schnorr sigs into OP_CHECKSIG. | # A mandatory (bannable) error occurs when people send invalid Schnorr sigs into OP_CHECKSIG. | ||||
RPC_NULLFAIL_ERROR = 'mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation) (code 16)' | RPC_NULLFAIL_ERROR = 'mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation) (code 16)' | ||||
# This 64-byte signature is used to test exclusion & banning according to | # This 64-byte signature is used to test exclusion & banning according to | ||||
# the above error messages. | # the above error messages. | ||||
# Tests of real 64 byte ECDSA signatures can be found in script_tests. | # Tests of real 64 byte ECDSA signatures can be found in script_tests. | ||||
sig64 = b'\0'*64 | sig64 = b'\0'*64 | ||||
class PreviousSpendableOutput(object): | class PreviousSpendableOutput(object): | ||||
def __init__(self, tx=CTransaction(), n=-1): | def __init__(self, tx=CTransaction(), n=-1): | ||||
self.tx = tx | self.tx = tx | ||||
self.n = n | self.n = n | ||||
class SchnorrTest(BitcoinTestFramework): | class SchnorrTest(BitcoinTestFramework): | ||||
def set_test_params(self): | def set_test_params(self): | ||||
self.num_nodes = 2 | self.num_nodes = 1 | ||||
self.setup_clean_chain = True | self.setup_clean_chain = True | ||||
self.block_heights = {} | self.block_heights = {} | ||||
self.tip = None | self.tip = None | ||||
self.blocks = {} | self.blocks = {} | ||||
self.extra_args = [['-whitelist=127.0.0.1'], | |||||
[]] | |||||
def run_test(self): | |||||
test = TestManager(self, self.options.tmpdir) | |||||
test.add_all_connections([self.nodes[0]]) | |||||
# We have made a second node for ban-testing, to which we connect | |||||
# the mininode (but not test framework). We make multiple connections | |||||
# since each disconnect event consumes a connection (and, after we | |||||
# run network_thread_start() we can't make any more connections). | |||||
for _ in range(3): | |||||
self.nodes[1].add_p2p_connection(P2PInterface()) | |||||
network_thread_start() | |||||
test.run() | |||||
def next_block(self, number, transactions=None, nTime=None): | def next_block(self, number, transactions=None, nTime=None): | ||||
if self.tip == None: | if self.tip == None: | ||||
base_block_hash = self.genesis_hash | base_block_hash = self.genesis_hash | ||||
block_time = int(time.time()) + 1 | block_time = int(time.time()) + 1 | ||||
else: | else: | ||||
base_block_hash = self.tip.sha256 | base_block_hash = self.tip.sha256 | ||||
block_time = self.tip.nTime + 1 | block_time = self.tip.nTime + 1 | ||||
Show All 14 Lines | def next_block(self, number, transactions=None, nTime=None): | ||||
# Do PoW, which is cheap on regnet | # Do PoW, which is cheap on regnet | ||||
block.solve() | block.solve() | ||||
self.tip = block | self.tip = block | ||||
self.block_heights[block.sha256] = height | self.block_heights[block.sha256] = height | ||||
assert number not in self.blocks | assert number not in self.blocks | ||||
self.blocks[number] = block | self.blocks[number] = block | ||||
return block | return block | ||||
def get_tests(self): | def bootstrap_p2p(self, *, num_connections=1): | ||||
"""Add a P2P connection to the node. | |||||
Helper to connect and wait for version handshake.""" | |||||
for _ in range(num_connections): | |||||
self.nodes[0].add_p2p_connection(P2PDataStore()) | |||||
network_thread_start() | |||||
self.nodes[0].p2p.wait_for_verack() | |||||
def reconnect_p2p(self, **kwargs): | |||||
"""Tear down and bootstrap the P2P connection to the node. | |||||
The node gets disconnected several times in this test. This helper | |||||
method reconnects the p2p and restarts the network thread.""" | |||||
self.nodes[0].disconnect_p2ps() | |||||
network_thread_join() | |||||
self.bootstrap_p2p(**kwargs) | |||||
def run_test(self): | |||||
self.bootstrap_p2p() | |||||
self.genesis_hash = int(self.nodes[0].getbestblockhash(), 16) | self.genesis_hash = int(self.nodes[0].getbestblockhash(), 16) | ||||
self.block_heights[self.genesis_hash] = 0 | self.block_heights[self.genesis_hash] = 0 | ||||
spendable_outputs = [] | spendable_outputs = [] | ||||
# shorthand | # shorthand | ||||
block = self.next_block | block = self.next_block | ||||
node = self.nodes[0] | node = self.nodes[0] | ||||
node_ban = self.nodes[1] | |||||
# save the current tip so its coinbase can be spent by a later block | # save the current tip so its coinbase can be spent by a later block | ||||
def save_spendable_output(): | def save_spendable_output(): | ||||
spendable_outputs.append(self.tip) | spendable_outputs.append(self.tip) | ||||
# get a coinbase that we previously marked as spendable | # get a coinbase that we previously marked as spendable | ||||
def get_spendable_output(): | def get_spendable_output(): | ||||
return PreviousSpendableOutput(spendable_outputs.pop(0).vtx[0], 0) | return PreviousSpendableOutput(spendable_outputs.pop(0).vtx[0], 0) | ||||
# returns a test case that asserts that the current tip was accepted | # submit current tip and check it was accepted | ||||
def accepted(): | def accepted(): | ||||
return TestInstance([[self.tip, True]]) | node.p2p.send_blocks_and_test([self.tip], node) | ||||
# returns a test case that asserts that the current tip was rejected | # submit current tip and check it was rejected (and we are banned) | ||||
def rejected(reject=None): | def rejected(reject_code, reject_reason): | ||||
if reject is None: | node.p2p.send_blocks_and_test( | ||||
return TestInstance([[self.tip, False]]) | [self.tip], node, success=False, reject_code=reject_code, reject_reason=reject_reason) | ||||
else: | node.p2p.wait_for_disconnect() | ||||
return TestInstance([[self.tip, reject]]) | self.reconnect_p2p() | ||||
# move the tip back to a previous block | # move the tip back to a previous block | ||||
def tip(number): | def tip(number): | ||||
self.tip = self.blocks[number] | self.tip = self.blocks[number] | ||||
self.log.info("Create some blocks with OP_1 coinbase for spending.") | |||||
# Create a new block | # Create a new block | ||||
block(0) | block(0) | ||||
save_spendable_output() | save_spendable_output() | ||||
yield accepted() | accepted() | ||||
# Now we need that block to mature so we can spend the coinbase. | # Now we need that block to mature so we can spend the coinbase. | ||||
test = TestInstance(sync_every_block=False) | matureblocks = [] | ||||
for i in range(199): | for i in range(199): | ||||
block(5000 + i) | block(5000 + i) | ||||
test.blocks_and_transactions.append([self.tip, True]) | matureblocks.append(self.tip) | ||||
save_spendable_output() | save_spendable_output() | ||||
yield test | node.p2p.send_blocks_and_test(matureblocks, node) | ||||
# collect spendable outputs now to avoid cluttering the code later on | # collect spendable outputs now to avoid cluttering the code later on | ||||
out = [] | out = [] | ||||
for i in range(100): | for i in range(100): | ||||
out.append(get_spendable_output()) | out.append(get_spendable_output()) | ||||
# Generate a key pair to test P2SH sigops count | self.log.info("Setting up spends to test and mining the fundings.") | ||||
# Generate a key pair | |||||
privkeybytes = b"Schnorr!" * 4 | privkeybytes = b"Schnorr!" * 4 | ||||
private_key = CECKey() | private_key = CECKey() | ||||
private_key.set_secretbytes(privkeybytes) | private_key.set_secretbytes(privkeybytes) | ||||
# get uncompressed public key serialization | # get uncompressed public key serialization | ||||
public_key = private_key.get_pubkey() | public_key = private_key.get_pubkey() | ||||
def create_fund_and_spend_tx(spend, multi=False, sig='schnorr'): | def create_fund_and_spend_tx(spend, multi=False, sig='schnorr'): | ||||
if multi: | if multi: | ||||
Show All 35 Lines | def run_test(self): | ||||
def send_transaction_to_mempool(tx): | def send_transaction_to_mempool(tx): | ||||
tx_id = node.sendrawtransaction(ToHex(tx)) | tx_id = node.sendrawtransaction(ToHex(tx)) | ||||
assert(tx_id in set(node.getrawmempool())) | assert(tx_id in set(node.getrawmempool())) | ||||
return tx_id | return tx_id | ||||
# Check we are disconnected when sending a txn that node_ban rejects. | # Check we are disconnected when sending a txn that node_ban rejects. | ||||
# (Can't actually get banned, since bitcoind won't ban local peers.) | # (Can't actually get banned, since bitcoind won't ban local peers.) | ||||
def check_for_ban_on_rejected_tx(tx): | def check_for_ban_on_rejected_tx(tx): | ||||
# Take a connection | self.nodes[0].p2p.send_txs_and_test( | ||||
p2p = node_ban.p2ps.pop() | [tx], self.nodes[0], success=False, expect_disconnect=True) | ||||
assert(p2p.state == 'connected') | self.reconnect_p2p() | ||||
# make sure we can ping | |||||
p2p.sync_with_ping() | |||||
# send the naughty transaction | |||||
p2p.send_message(msg_tx(tx)) | |||||
# if not "banned", this will timeout and raise exception. | |||||
p2p.wait_for_disconnect() | |||||
# Setup fundings | # Setup fundings | ||||
fundings = [] | fundings = [] | ||||
fund, schnorrchecksigtx = create_fund_and_spend_tx(out[0]) | fund, schnorrchecksigtx = create_fund_and_spend_tx(out[0]) | ||||
fundings.append(fund) | fundings.append(fund) | ||||
fund, schnorrmultisigtx = create_fund_and_spend_tx(out[1], multi=True) | fund, schnorrmultisigtx = create_fund_and_spend_tx(out[1], multi=True) | ||||
fundings.append(fund) | fundings.append(fund) | ||||
fund, ecdsachecksigtx = create_fund_and_spend_tx(out[2], sig='ecdsa') | fund, ecdsachecksigtx = create_fund_and_spend_tx(out[2], sig='ecdsa') | ||||
fundings.append(fund) | fundings.append(fund) | ||||
fund, sig64checksigtx = create_fund_and_spend_tx( | fund, sig64checksigtx = create_fund_and_spend_tx( | ||||
out[5], sig=sig64) | out[5], sig=sig64) | ||||
fundings.append(fund) | fundings.append(fund) | ||||
fund, sig64multisigtx = create_fund_and_spend_tx( | fund, sig64multisigtx = create_fund_and_spend_tx( | ||||
out[6], multi=True, sig=sig64) | out[6], multi=True, sig=sig64) | ||||
fundings.append(fund) | fundings.append(fund) | ||||
for fund in fundings: | for fund in fundings: | ||||
send_transaction_to_mempool(fund) | send_transaction_to_mempool(fund) | ||||
block(1, transactions=fundings) | block(1, transactions=fundings) | ||||
yield accepted() | accepted() | ||||
# we're now set up for the various spends; make sure the other node | |||||
# is set up, too. | |||||
sync_blocks(self.nodes) | |||||
# Typical ECDSA and Schnorr CHECKSIG are valid | self.log.info("Typical ECDSA and Schnorr CHECKSIG are valid.") | ||||
schnorr_tx_id = send_transaction_to_mempool(schnorrchecksigtx) | schnorr_tx_id = send_transaction_to_mempool(schnorrchecksigtx) | ||||
ecdsa_tx_id = send_transaction_to_mempool(ecdsachecksigtx) | ecdsa_tx_id = send_transaction_to_mempool(ecdsachecksigtx) | ||||
# It can also be mined | # It can also be mined | ||||
block(2, transactions=[schnorrchecksigtx, ecdsachecksigtx]) | block(2, transactions=[schnorrchecksigtx, ecdsachecksigtx]) | ||||
yield accepted() | accepted() | ||||
assert schnorr_tx_id not in set(node.getrawmempool()) | assert schnorr_tx_id not in set(node.getrawmempool()) | ||||
assert ecdsa_tx_id not in set(node.getrawmempool()) | assert ecdsa_tx_id not in set(node.getrawmempool()) | ||||
# Schnorr in multisig is rejected with mandatory error. | self.log.info("Schnorr in multisig is rejected with mandatory error.") | ||||
assert_raises_rpc_error(-26, RPC_SCHNORR_MULTISIG_ERROR, | assert_raises_rpc_error(-26, RPC_SCHNORR_MULTISIG_ERROR, | ||||
node.sendrawtransaction, ToHex(schnorrmultisigtx)) | node.sendrawtransaction, ToHex(schnorrmultisigtx)) | ||||
# And it is banworthy. | # And it is banworthy. | ||||
check_for_ban_on_rejected_tx(schnorrmultisigtx) | check_for_ban_on_rejected_tx(schnorrmultisigtx) | ||||
# And it can't be mined | # And it can't be mined | ||||
block(13, transactions=[schnorrmultisigtx]) | block(13, transactions=[schnorrmultisigtx]) | ||||
yield rejected(RejectResult(16, b'blk-bad-inputs')) | rejected(16, b'blk-bad-inputs') | ||||
# Rewind bad block | # Rewind bad block | ||||
tip(2) | tip(2) | ||||
# If we try to submit a bad 64-byte sig, we fail with mandatory errors. | self.log.info("Bad 64-byte sig is rejected with mandatory error.") | ||||
# In CHECKSIG it's invalid Schnorr and hence NULLFAIL. | # In CHECKSIG it's invalid Schnorr and hence NULLFAIL. | ||||
assert_raises_rpc_error(-26, RPC_NULLFAIL_ERROR, | assert_raises_rpc_error(-26, RPC_NULLFAIL_ERROR, | ||||
node.sendrawtransaction, ToHex(sig64checksigtx)) | node.sendrawtransaction, ToHex(sig64checksigtx)) | ||||
# In CHECKMULTISIG it's invalid length and hence BAD_LENGTH. | # In CHECKMULTISIG it's invalid length and hence BAD_LENGTH. | ||||
assert_raises_rpc_error(-26, RPC_SCHNORR_MULTISIG_ERROR, | assert_raises_rpc_error(-26, RPC_SCHNORR_MULTISIG_ERROR, | ||||
node.sendrawtransaction, ToHex(sig64multisigtx)) | node.sendrawtransaction, ToHex(sig64multisigtx)) | ||||
# Getting sent these transactions is banworthy. | # Getting sent these transactions is banworthy. | ||||
check_for_ban_on_rejected_tx(sig64checksigtx) | check_for_ban_on_rejected_tx(sig64checksigtx) | ||||
check_for_ban_on_rejected_tx(sig64multisigtx) | check_for_ban_on_rejected_tx(sig64multisigtx) | ||||
# And they can't be mined either... | # And they can't be mined either... | ||||
block(14, transactions=[sig64checksigtx]) | block(14, transactions=[sig64checksigtx]) | ||||
yield rejected(RejectResult(16, b'blk-bad-inputs')) | rejected(16, b'blk-bad-inputs') | ||||
# Rewind bad block | # Rewind bad block | ||||
tip(2) | tip(2) | ||||
block(15, transactions=[sig64multisigtx]) | block(15, transactions=[sig64multisigtx]) | ||||
yield rejected(RejectResult(16, b'blk-bad-inputs')) | rejected(16, b'blk-bad-inputs') | ||||
# Rewind bad block | # Rewind bad block | ||||
tip(2) | tip(2) | ||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
SchnorrTest().main() | SchnorrTest().main() |