Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc-schnorrmultisig.py
- This file was moved from test/functional/abc-minimaldata-activation.py.
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2019 The Bitcoin developers | # Copyright (c) 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 activation of MINIMALDATA rule to consensus (from standard). | This tests the CHECKMULTISIG mode that uses Schnorr transaction signatures and | ||||
markblundeberg: hmm that's annoying, phab thinks this is renamed from minimaldata.
as a workaround we can keep… | |||||
FabienAuthorUnsubmitted Done Inline ActionsMeh, that's really annoying... I will keep the file names as suggested and update them in another diff to avoid this mess. Fabien: Meh, that's really annoying... I will keep the file names as suggested and update them in… | |||||
- test rejection in mempool, with error changing before/after activation. | repurposes the dummy element to indicate which signatures are being checked. | ||||
- test acceptance in blocks before activation, and rejection after. | - acceptance both in mempool and blocks. | ||||
- check non-banning for peers who send invalid txns that would have been valid | - check non-banning for peers who send invalid txns that would have been valid | ||||
on the other side of the upgrade. | on the other side of the upgrade. | ||||
- check banning of peers for some fully-invalid transactions. | |||||
Derived from abc-schnorr.py | Derived from abc-schnorr.py | ||||
""" | """ | ||||
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.key import CECKey | |||||
from test_framework.messages import ( | from test_framework.messages import ( | ||||
CBlock, | CBlock, | ||||
COutPoint, | COutPoint, | ||||
CTransaction, | CTransaction, | ||||
CTxIn, | CTxIn, | ||||
CTxOut, | CTxOut, | ||||
FromHex, | FromHex, | ||||
ToHex, | ToHex, | ||||
) | ) | ||||
from test_framework.mininode import ( | from test_framework.mininode import ( | ||||
P2PDataStore, | P2PDataStore, | ||||
) | ) | ||||
from test_framework import schnorr | |||||
from test_framework.script import ( | from test_framework.script import ( | ||||
CScript, | CScript, | ||||
OP_ADD, | OP_0, | ||||
OP_1, | |||||
OP_CHECKMULTISIG, | |||||
OP_TRUE, | OP_TRUE, | ||||
SIGHASH_ALL, | |||||
SIGHASH_FORKID, | |||||
SignatureHashForkId, | |||||
) | ) | ||||
from test_framework.test_framework import BitcoinTestFramework | from test_framework.test_framework import BitcoinTestFramework | ||||
from test_framework.txtools import pad_tx | |||||
from test_framework.util import assert_equal, assert_raises_rpc_error | from test_framework.util import assert_equal, assert_raises_rpc_error | ||||
# the upgrade activation time, which we artificially set far into the future | |||||
GRAVITON_START_TIME = 2000000000 | |||||
# If we don't do this, autoreplay protection will activate before graviton and | # ECDSA checkmultisig with non-null dummy are invalid since the new mode | ||||
# all our sigs will mysteriously fail. | # refuses ECDSA. | ||||
REPLAY_PROTECTION_START_TIME = GRAVITON_START_TIME * 2 | ECDSA_NULLDUMMY_ERROR = 'mandatory-script-verify-flag-failed (Only Schnorr signatures allowed in this operation)' | ||||
# A mandatory (bannable) error occurs when people pass Schnorr signatures into | |||||
# Both before and after the upgrade, minimal push violations in mempool are | # legacy OP_CHECKMULTISIG. | ||||
# rejected with a bannable error. | SCHNORR_LEGACY_MULTISIG_ERROR = 'mandatory-script-verify-flag-failed (Signature cannot be 65 bytes in CHECKMULTISIG)' | ||||
MINIMALPUSH_ERROR = 'mandatory-script-verify-flag-failed (Data push larger than necessary)' | |||||
# Blocks with invalid scripts give this error: | # Blocks with invalid scripts give this error: | ||||
BADINPUTS_ERROR = 'blk-bad-inputs' | BADINPUTS_ERROR = 'blk-bad-inputs' | ||||
class SchnorrTest(BitcoinTestFramework): | # This 64-byte signature is used to test exclusion & banning according to | ||||
# the above error messages. | |||||
# Tests of real 64 byte ECDSA signatures can be found in script_tests. | |||||
sig64 = b'\0'*64 | |||||
class SchnorrMultisigTest(BitcoinTestFramework): | |||||
def set_test_params(self): | def set_test_params(self): | ||||
self.num_nodes = 1 | self.num_nodes = 1 | ||||
self.block_heights = {} | self.block_heights = {} | ||||
self.extra_args = [["-gravitonactivationtime={}".format( | |||||
GRAVITON_START_TIME), | |||||
"-replayprotectionactivationtime={}".format( | |||||
REPLAY_PROTECTION_START_TIME)]] | |||||
def bootstrap_p2p(self, *, num_connections=1): | def bootstrap_p2p(self, *, num_connections=1): | ||||
"""Add a P2P connection to the node. | """Add a P2P connection to the node. | ||||
Helper to connect and wait for version handshake.""" | Helper to connect and wait for version handshake.""" | ||||
for _ in range(num_connections): | for _ in range(num_connections): | ||||
self.nodes[0].add_p2p_connection(P2PDataStore()) | self.nodes[0].add_p2p_connection(P2PDataStore()) | ||||
Show All 34 Lines | class SchnorrMultisigTest(BitcoinTestFramework): | ||||
def check_for_ban_on_rejected_tx(self, tx, reject_reason=None): | def check_for_ban_on_rejected_tx(self, tx, reject_reason=None): | ||||
"""Check we are disconnected when sending a txn that the node rejects. | """Check we are disconnected when sending a txn that the node 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.)""" | ||||
self.nodes[0].p2p.send_txs_and_test( | self.nodes[0].p2p.send_txs_and_test( | ||||
[tx], self.nodes[0], success=False, expect_disconnect=True, reject_reason=reject_reason) | [tx], self.nodes[0], success=False, expect_disconnect=True, reject_reason=reject_reason) | ||||
self.reconnect_p2p() | self.reconnect_p2p() | ||||
def check_for_no_ban_on_rejected_tx(self, tx, reject_reason): | |||||
"""Check we are not disconnected when sending a txn that the node rejects.""" | |||||
self.nodes[0].p2p.send_txs_and_test( | |||||
[tx], self.nodes[0], success=False, reject_reason=reject_reason) | |||||
def check_for_ban_on_rejected_block(self, block, reject_reason=None): | def check_for_ban_on_rejected_block(self, block, reject_reason=None): | ||||
"""Check we are disconnected when sending a block that the node rejects. | """Check we are disconnected when sending a block that the node 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.)""" | ||||
self.nodes[0].p2p.send_blocks_and_test( | self.nodes[0].p2p.send_blocks_and_test( | ||||
[block], self.nodes[0], success=False, reject_reason=reject_reason, expect_disconnect=True) | [block], self.nodes[0], success=False, reject_reason=reject_reason, expect_disconnect=True) | ||||
self.reconnect_p2p() | self.reconnect_p2p() | ||||
Show All 15 Lines | def run_test(self): | ||||
self.log.info("Mature the blocks and get out of IBD.") | self.log.info("Mature the blocks and get out of IBD.") | ||||
node.generate(100) | node.generate(100) | ||||
tip = self.getbestblock(node) | tip = self.getbestblock(node) | ||||
self.log.info("Setting up spends to test and mining the fundings.") | self.log.info("Setting up spends to test and mining the fundings.") | ||||
fundings = [] | fundings = [] | ||||
def create_fund_and_spend_tx(): | # Generate a key pair | ||||
privkeybytes = b"Schnorr!" * 4 | |||||
private_key = CECKey() | |||||
private_key.set_secretbytes(privkeybytes) | |||||
# get uncompressed public key serialization | |||||
public_key = private_key.get_pubkey() | |||||
def create_fund_and_spend_tx(dummy=OP_0, sigtype='ecdsa'): | |||||
spendfrom = spendable_outputs.pop() | spendfrom = spendable_outputs.pop() | ||||
script = CScript([OP_ADD]) | script = CScript([OP_1, public_key, OP_1, OP_CHECKMULTISIG]) | ||||
value = spendfrom.vout[0].nValue | value = spendfrom.vout[0].nValue | ||||
# Fund transaction | # Fund transaction | ||||
txfund = create_transaction(spendfrom, 0, b'', value, script) | txfund = create_transaction(spendfrom, 0, b'', value, script) | ||||
txfund.rehash() | txfund.rehash() | ||||
fundings.append(txfund) | fundings.append(txfund) | ||||
# Spend transaction | # Spend transaction | ||||
txspend = CTransaction() | txspend = CTransaction() | ||||
txspend.vout.append( | txspend.vout.append( | ||||
CTxOut(value-1000, CScript([OP_TRUE]))) | CTxOut(value-1000, CScript([OP_TRUE]))) | ||||
txspend.vin.append( | txspend.vin.append( | ||||
CTxIn(COutPoint(txfund.sha256, 0), b'')) | CTxIn(COutPoint(txfund.sha256, 0), b'')) | ||||
# Sign the transaction | # Sign the transaction | ||||
txspend.vin[0].scriptSig = CScript( | sighashtype = SIGHASH_ALL | SIGHASH_FORKID | ||||
b'\x01\x01\x51') # PUSH1(0x01) OP_1 | hashbyte = bytes([sighashtype & 0xff]) | ||||
pad_tx(txspend) | sighash = SignatureHashForkId( | ||||
script, txspend, 0, sighashtype, value) | |||||
if sigtype == 'schnorr': | |||||
txsig = schnorr.sign(privkeybytes, sighash) + hashbyte | |||||
elif sigtype == 'ecdsa': | |||||
txsig = private_key.sign(sighash) + hashbyte | |||||
txspend.vin[0].scriptSig = CScript([dummy, txsig]) | |||||
txspend.rehash() | txspend.rehash() | ||||
return txspend | return txspend | ||||
# make a few of these, which are nonstandard before upgrade and invalid after. | # This is valid. | ||||
nonminimaltx = create_fund_and_spend_tx() | ecdsa0tx = create_fund_and_spend_tx(OP_0, 'ecdsa') | ||||
nonminimaltx_2 = create_fund_and_spend_tx() | |||||
nonminimaltx_3 = create_fund_and_spend_tx() | # This is invalid. | ||||
ecdsa1tx = create_fund_and_spend_tx(OP_1, 'ecdsa') | |||||
# This is invalid. | |||||
schnorr0tx = create_fund_and_spend_tx(OP_0, 'schnorr') | |||||
# This is valid. | |||||
schnorr1tx = create_fund_and_spend_tx(OP_1, 'schnorr') | |||||
tip = self.build_block(tip, fundings) | tip = self.build_block(tip, fundings) | ||||
node.p2p.send_blocks_and_test([tip], node) | node.p2p.send_blocks_and_test([tip], node) | ||||
self.log.info("Start preupgrade tests") | self.log.info("Send a legacy ECDSA multisig into mempool.") | ||||
node.p2p.send_txs_and_test([ecdsa0tx], node) | |||||
self.log.info("Sending rejected transactions via RPC") | assert_equal(node.getrawmempool(), [ecdsa0tx.hash]) | ||||
assert_raises_rpc_error(-26, MINIMALPUSH_ERROR, | |||||
node.sendrawtransaction, ToHex(nonminimaltx)) | |||||
assert_raises_rpc_error(-26, MINIMALPUSH_ERROR, | |||||
node.sendrawtransaction, ToHex(nonminimaltx_2)) | |||||
assert_raises_rpc_error(-26, MINIMALPUSH_ERROR, | |||||
node.sendrawtransaction, ToHex(nonminimaltx_3)) | |||||
self.log.info("Trying to mine a non-null-dummy ECDSA.") | |||||
self.check_for_ban_on_rejected_block( | |||||
self.build_block(tip, [ecdsa1tx]), BADINPUTS_ERROR) | |||||
self.log.info( | self.log.info( | ||||
"Sending rejected transactions via net (banning)") | "If we try to submit it by mempool or RPC, it is rejected and we are banned") | ||||
assert_raises_rpc_error(-26, ECDSA_NULLDUMMY_ERROR, | |||||
node.sendrawtransaction, ToHex(ecdsa1tx)) | |||||
self.check_for_ban_on_rejected_tx( | self.check_for_ban_on_rejected_tx( | ||||
nonminimaltx, MINIMALPUSH_ERROR) | ecdsa1tx, ECDSA_NULLDUMMY_ERROR) | ||||
self.check_for_ban_on_rejected_tx( | |||||
nonminimaltx_2, MINIMALPUSH_ERROR) | |||||
self.check_for_ban_on_rejected_tx( | |||||
nonminimaltx_3, MINIMALPUSH_ERROR) | |||||
assert_equal(node.getrawmempool(), []) | self.log.info( | ||||
"Submitting a Schnorr-multisig via net, and mining it in a block") | |||||
self.log.info("Successfully mine nonstandard transaction") | node.p2p.send_txs_and_test([schnorr1tx], node) | ||||
tip = self.build_block(tip, [nonminimaltx]) | assert_equal(set(node.getrawmempool()), { | ||||
ecdsa0tx.hash, schnorr1tx.hash}) | |||||
tip = self.build_block(tip, [schnorr1tx]) | |||||
node.p2p.send_blocks_and_test([tip], node) | node.p2p.send_blocks_and_test([tip], node) | ||||
# Activation tests | |||||
self.log.info("Approach to just before upgrade activation") | |||||
# Move our clock to the uprade time so we will accept such future-timestamped blocks. | |||||
node.setmocktime(GRAVITON_START_TIME) | |||||
# Mine six blocks with timestamp starting at GRAVITON_START_TIME-1 | |||||
blocks = [] | |||||
for i in range(-1, 5): | |||||
tip = self.build_block(tip, nTime=GRAVITON_START_TIME + i) | |||||
blocks.append(tip) | |||||
node.p2p.send_blocks_and_test(blocks, node) | |||||
assert_equal(node.getblockchaininfo()[ | |||||
'mediantime'], GRAVITON_START_TIME - 1) | |||||
self.log.info( | self.log.info( | ||||
"Mine the activation block itself, including a minimaldata violation at the last possible moment") | "That legacy ECDSA multisig is still in mempool, let's mine it") | ||||
tip = self.build_block(tip, [nonminimaltx_2]) | assert_equal(node.getrawmempool(), [ecdsa0tx.hash]) | ||||
tip = self.build_block(tip, [ecdsa0tx]) | |||||
node.p2p.send_blocks_and_test([tip], node) | node.p2p.send_blocks_and_test([tip], node) | ||||
assert_equal(node.getrawmempool(), []) | |||||
self.log.info("We have activated!") | |||||
assert_equal(node.getblockchaininfo()[ | |||||
'mediantime'], GRAVITON_START_TIME) | |||||
self.log.info( | |||||
"Trying to mine a minimaldata violation, but we are just barely too late") | |||||
self.check_for_ban_on_rejected_block( | |||||
self.build_block(tip, [nonminimaltx_3]), BADINPUTS_ERROR) | |||||
self.log.info( | self.log.info( | ||||
"If we try to submit it by mempool or RPC we still aren't banned") | "Trying Schnorr in legacy multisig is invalid and banworthy.") | ||||
assert_raises_rpc_error(-26, MINIMALPUSH_ERROR, | |||||
node.sendrawtransaction, ToHex(nonminimaltx_3)) | |||||
self.check_for_ban_on_rejected_tx( | self.check_for_ban_on_rejected_tx( | ||||
nonminimaltx_3, MINIMALPUSH_ERROR) | schnorr0tx, SCHNORR_LEGACY_MULTISIG_ERROR) | ||||
self.check_for_ban_on_rejected_block( | |||||
self.log.info("Mine a normal block") | self.build_block(tip, [schnorr0tx]), BADINPUTS_ERROR) | ||||
tip = self.build_block(tip) | |||||
node.p2p.send_blocks_and_test([tip], node) | |||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
SchnorrTest().main() | SchnorrMultisigTest().main() |
hmm that's annoying, phab thinks this is renamed from minimaldata.
as a workaround we can keep the file named -activation for now.