diff options
Diffstat (limited to 'test/functional')
201 files changed, 11827 insertions, 4429 deletions
diff --git a/test/functional/README.md b/test/functional/README.md index d830ba0334..926810cf03 100644 --- a/test/functional/README.md +++ b/test/functional/README.md @@ -188,5 +188,5 @@ perf report -i /path/to/datadir/send-big-msgs.perf.data.xxxx --stdio | c++filt | #### See also: - [Installing perf](https://askubuntu.com/q/50145) -- [Perf examples](http://www.brendangregg.com/perf.html) +- [Perf examples](https://www.brendangregg.com/perf.html) - [Hotspot](https://github.com/KDAB/hotspot): a GUI for perf output analysis diff --git a/test/functional/combine_logs.py b/test/functional/combine_logs.py index 00f2833f55..33c81bde13 100755 --- a/test/functional/combine_logs.py +++ b/test/functional/combine_logs.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2019 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Combine logs from multiple bitcoin nodes as well as the test_framework log. @@ -188,7 +188,7 @@ def print_logs_plain(log_events, colors): def print_logs_html(log_events): """Renders the iterator of log events into html.""" try: - import jinja2 + import jinja2 #type:ignore except ImportError: print("jinja2 not found. Try `pip install jinja2`") sys.exit(1) diff --git a/test/functional/data/__init__.py b/test/functional/data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/test/functional/data/__init__.py diff --git a/test/functional/data/invalid_txs.py b/test/functional/data/invalid_txs.py index fab921ef19..3747b2a98d 100644 --- a/test/functional/data/invalid_txs.py +++ b/test/functional/data/invalid_txs.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ @@ -28,28 +28,34 @@ from test_framework.messages import ( CTxIn, CTxOut, MAX_MONEY, + SEQUENCE_FINAL, ) -from test_framework import script as sc from test_framework.blocktools import create_tx_with_script, MAX_BLOCK_SIGOPS from test_framework.script import ( CScript, + OP_0, + OP_2DIV, + OP_2MUL, + OP_AND, OP_CAT, - OP_SUBSTR, - OP_LEFT, - OP_RIGHT, + OP_CHECKSIG, + OP_DIV, OP_INVERT, - OP_AND, + OP_LEFT, + OP_LSHIFT, + OP_MOD, + OP_MUL, OP_OR, + OP_RIGHT, + OP_RSHIFT, + OP_SUBSTR, + OP_TRUE, OP_XOR, - OP_2MUL, - OP_2DIV, - OP_MUL, - OP_DIV, - OP_MOD, - OP_LSHIFT, - OP_RSHIFT ) -basic_p2sh = sc.CScript([sc.OP_HASH160, sc.hash160(sc.CScript([sc.OP_0])), sc.OP_EQUAL]) +from test_framework.script_util import ( + script_to_p2sh_script, +) +basic_p2sh = script_to_p2sh_script(CScript([OP_0])) class BadTxTemplate: @@ -72,7 +78,7 @@ class BadTxTemplate: def __init__(self, *, spend_tx=None, spend_block=None): self.spend_tx = spend_block.vtx[0] if spend_block else spend_tx self.spend_avail = sum(o.nValue for o in self.spend_tx.vout) - self.valid_txin = CTxIn(COutPoint(self.spend_tx.sha256, 0), b"", 0xffffffff) + self.valid_txin = CTxIn(COutPoint(self.spend_tx.sha256, 0), b"", SEQUENCE_FINAL) @abc.abstractmethod def get_tx(self, *args, **kwargs): @@ -116,7 +122,7 @@ class SizeTooSmall(BadTxTemplate): def get_tx(self): tx = CTransaction() tx.vin.append(self.valid_txin) - tx.vout.append(CTxOut(0, sc.CScript([sc.OP_TRUE]))) + tx.vout.append(CTxOut(0, CScript([OP_TRUE]))) tx.calc_sha256() return tx @@ -132,7 +138,7 @@ class BadInputOutpointIndex(BadTxTemplate): bad_idx = num_indices + 100 tx = CTransaction() - tx.vin.append(CTxIn(COutPoint(self.spend_tx.sha256, bad_idx), b"", 0xffffffff)) + tx.vin.append(CTxIn(COutPoint(self.spend_tx.sha256, bad_idx), b"", SEQUENCE_FINAL)) tx.vout.append(CTxOut(0, basic_p2sh)) tx.calc_sha256() return tx @@ -151,13 +157,26 @@ class DuplicateInput(BadTxTemplate): return tx +class PrevoutNullInput(BadTxTemplate): + reject_reason = 'bad-txns-prevout-null' + expect_disconnect = True + + def get_tx(self): + tx = CTransaction() + tx.vin.append(self.valid_txin) + tx.vin.append(CTxIn(COutPoint(hash=0, n=0xffffffff))) + tx.vout.append(CTxOut(1, basic_p2sh)) + tx.calc_sha256() + return tx + + class NonexistentInput(BadTxTemplate): reject_reason = None # Added as an orphan tx. expect_disconnect = False def get_tx(self): tx = CTransaction() - tx.vin.append(CTxIn(COutPoint(self.spend_tx.sha256 + 1, 0), b"", 0xffffffff)) + tx.vin.append(CTxIn(COutPoint(self.spend_tx.sha256 + 1, 0), b"", SEQUENCE_FINAL)) tx.vin.append(self.valid_txin) tx.vout.append(CTxOut(1, basic_p2sh)) tx.calc_sha256() @@ -217,7 +236,7 @@ class TooManySigops(BadTxTemplate): expect_disconnect = False def get_tx(self): - lotsa_checksigs = sc.CScript([sc.OP_CHECKSIG] * (MAX_BLOCK_SIGOPS)) + lotsa_checksigs = CScript([OP_CHECKSIG] * (MAX_BLOCK_SIGOPS)) return create_tx_with_script( self.spend_tx, 0, script_pub_key=lotsa_checksigs, diff --git a/test/functional/data/rpc_decodescript.json b/test/functional/data/rpc_decodescript.json new file mode 100644 index 0000000000..8903f5efac --- /dev/null +++ b/test/functional/data/rpc_decodescript.json @@ -0,0 +1,96 @@ +[ + [ + "5120eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + { + "asm": "1 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "address": "bcrt1pamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhqz6nvlh", + "desc": "addr(bcrt1pamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhqz6nvlh)#v52jnujz", + "type": "witness_v1_taproot" + } + ], + [ + "5102eeee", + { + "asm": "1 -28398", + "address": "bcrt1pamhqk96edn", + "desc": "addr(bcrt1pamhqk96edn)#vkh8uj5a", + "type": "witness_unknown" + } + ], + [ + "0020eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + { + "asm": "0 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "address": "bcrt1qamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhqgdn98t", + "desc": "addr(bcrt1qamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhqgdn98t)#afaecevx", + "type": "witness_v0_scripthash", + "p2sh": "2MwGk8mw1GBP6U9D5X8gTvgvXpuknmAK3fo" + } + ], + [ + "a914eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee87", + { + "asm": "OP_HASH160 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee OP_EQUAL", + "address": "2NF2b3KS8xXb9XHvbRMXdZh8s5g92rUZHtp", + "desc": "addr(2NF2b3KS8xXb9XHvbRMXdZh8s5g92rUZHtp)#ywfcpmh9", + "type": "scripthash" + } + ], + [ + "6a00", + { + "asm": "OP_RETURN 0", + "desc": "raw(6a00)#ncfmkl43", + "type": "nulldata" + } + ], + [ + "6aee", + { + "asm": "OP_RETURN OP_UNKNOWN", + "desc": "raw(6aee)#vsyzgqdt", + "type": "nonstandard" + } + ], + [ + "6a02ee", + { + "asm": "OP_RETURN [error]", + "desc": "raw(6a02ee)#gvdwnlzl", + "type": "nonstandard" + } + ], + [ + "02eeee", + { + "asm": "-28398", + "desc": "raw(02eeee)#5xzck7pr", + "type": "nonstandard", + "p2sh": "2N34iiGoUUkVSPiaaTFpJjB1FR9TXQu3PGM", + "segwit": { + "asm": "0 96c2368fc30514a438a8bd909f93c49a1549d77198ccbdb792043b666cb24f42", + "desc": "addr(bcrt1qjmprdr7rq522gw9ghkgfly7yng25n4m3nrxtmdujqsakvm9jfapqk795l5)#5akkdska", + "hex": "002096c2368fc30514a438a8bd909f93c49a1549d77198ccbdb792043b666cb24f42", + "address": "bcrt1qjmprdr7rq522gw9ghkgfly7yng25n4m3nrxtmdujqsakvm9jfapqk795l5", + "type": "witness_v0_scripthash", + "p2sh-segwit": "2MtoejEictTQ6XtmHYzoYttt35Ec6krqFKN" + } + } + ], + [ + "ba", + { + "asm": "OP_CHECKSIGADD", + "desc": "raw(ba)#yy0eg44l", + "type": "nonstandard" + } + ], + [ + "50", + { + "asm": "OP_RESERVED", + "desc": "raw(50)#a7tu03xf", + "type": "nonstandard" + } + ] +] diff --git a/test/functional/data/rpc_psbt.json b/test/functional/data/rpc_psbt.json index 0f6cd97fd8..8672400a92 100644 --- a/test/functional/data/rpc_psbt.json +++ b/test/functional/data/rpc_psbt.json @@ -19,6 +19,7 @@ "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAgAAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A", "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A", "cHNidP8BAHMCAAAAAbiWoY6pOQepFsEGhUPXaulX9rvye2NH+NrdlAHg+WgpAQAAAAD/////AkBLTAAAAAAAF6kUqWwXCcLM5BN2zoNqMNT5qMlIi7+HQEtMAAAAAAAXqRSVF/in2XNxAlN1OSxkyp0z+Wtg2YcAAAAAAAEBIBNssgAAAAAAF6kUamsvautR8hRlMRY6OKNTx03DK96HAQcXFgAUo8u1LWpHprjt/uENAwBpGZD0UH0BCGsCRzBEAiAONfH3DYiw67ZbylrsxCF/XXpVwyWBRgofyRbPslzvwgIgIKCsWw5sHSIPh1icNvcVLZLHWj6NA7Dk+4Os2pOnMbQBIQPGStfYHPtyhpV7zIWtn0Q4GXv5gK1zy/tnJ+cBXu4iiwABABYAFMwmJQEz+HDpBEEabxJ5PogPsqZRAAEAFgAUyCrGc3h3FYCmiIspbv2pSTKZ5jU", + "cHNidP8B+wQBAAAAAQB1AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAAAA/v///wLT3/UFAAAAABl2qRTQxZkDxbrChodg6Q/VIaRmWqdlIIisAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4ezLhMAAAEA/aUBAQAAAAABAomjxx6rTSDgNxu7pMxpj6KVyUY6+i45f4UzzLYvlWflAQAAABcWABS+GNFSqbASA52vPafeT1M0nuy5hf////+G+KpDpx3/FEiJOlMKcjfva0YIu7LdLQFx5jrsakiQtAEAAAAXFgAU/j6e8adF6XTZAsQ1WUOryzS9U1P/////AgDC6wsAAAAAGXapFIXP8Ql/2eAIuzSvcJxiGXs4l4pIiKxy/vhOLAAAABepFDOXJboh79Yqx1OpvNBn1semo50FhwJHMEQCICcSviLgJw85T1aDEdx8qaaJcLgCX907JAIp8H+KXzokAiABizjX3NMU5zTJJ2vW+0D2czJbxLqhRMgA0vLwLbJ2XAEhA9LhVnSUG61KmWNyy4fhhW02UmBtmFYv45xenn5BPyEFAkgwRQIhANErhS2F3Nlh0vX0q2YGVN9u7cx5TAwzzlzDCf+1/OWNAiBnM4qODhclwZf7GoivWfUeROQlWyAWfIaEAxwF0fJZKgEhAiO3K+7wll0Qvgd47+zWH8rG95pOoWk5M4BzRGT4TyqzAAAAAAAAAA==", "cHNidP8BACoCAAAAAAFAQg8AAAAAABepFG6Rty1Vk+fUOR4v9E6R6YXDFkHwhwAAAAAAAQEAAQEBagA=", "cHNidP8BACoCAAAAAAFAQg8AAAAAABepFG6Rty1Vk+fUOR4v9E6R6YXDFkHwhwAAAAAAAQAAAQABagA=", "cHNidP8BADMBAAAAAREREREREREREREREREREREREfrK3hERERERERERERERfwAAAAD/////AAAAAAAAAQEJ//////////8AAQEJAADK/gAAAAAAAA==", @@ -34,7 +35,15 @@ "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==", "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=", "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=", - "cHNidP8BACoCAAAAAAFAQg8AAAAAABepFG6Rty1Vk+fUOR4v9E6R6YXDFkHwhwAAAAAAAA==" + "cHNidP8BACoCAAAAAAFAQg8AAAAAABepFG6Rty1Vk+fUOR4v9E6R6YXDFkHwhwAAAAAAAA==", + "cHNidP8B+wQAAAAAAQB1AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAAAA/v///wLT3/UFAAAAABl2qRTQxZkDxbrChodg6Q/VIaRmWqdlIIisAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4ezLhMAAAEA/aUBAQAAAAABAomjxx6rTSDgNxu7pMxpj6KVyUY6+i45f4UzzLYvlWflAQAAABcWABS+GNFSqbASA52vPafeT1M0nuy5hf////+G+KpDpx3/FEiJOlMKcjfva0YIu7LdLQFx5jrsakiQtAEAAAAXFgAU/j6e8adF6XTZAsQ1WUOryzS9U1P/////AgDC6wsAAAAAGXapFIXP8Ql/2eAIuzSvcJxiGXs4l4pIiKxy/vhOLAAAABepFDOXJboh79Yqx1OpvNBn1semo50FhwJHMEQCICcSviLgJw85T1aDEdx8qaaJcLgCX907JAIp8H+KXzokAiABizjX3NMU5zTJJ2vW+0D2czJbxLqhRMgA0vLwLbJ2XAEhA9LhVnSUG61KmWNyy4fhhW02UmBtmFYv45xenn5BPyEFAkgwRQIhANErhS2F3Nlh0vX0q2YGVN9u7cx5TAwzzlzDCf+1/OWNAiBnM4qODhclwZf7GoivWfUeROQlWyAWfIaEAxwF0fJZKgEhAiO3K+7wll0Qvgd47+zWH8rG95pOoWk5M4BzRGT4TyqzAAAAAAAAAA==", + "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAD/AAAAaoF/AKqqgABqgABAP2lAQEAAAAAAQKJo8ceq00g4Dcbu6TMaY+ilclGOvouOX+FM8y2L5Vn5QEAAAAXFgAUvhjRUqmwEgOdrz2n3k9TNJ7suYX/////hviqQ6cd/xRIiTpTCnI372tGCLuy3S0BceY67GpIkLQBAAAAFxYAFP4+nvGnRel02QLENVlDq8s0vVNT/////wIAwusLAAAAABl2qRSFz/EJf9ngCLs0r3CcYhl7OJeKSIiscv74TiwAAAAXqRQzlyW6Ie/WKsdTqbzQZ9bHpqOdBYcCRzBEAiAnEr4i4CcPOU9WgxHcfKmmiXC4Al/dOyQCKfB/il86JAIgAYs419zTFOc0ySdr1vtA9nMyW8S6oUTIANLy8C2ydlwBIQPS4VZ0lButSpljcsuH4YVtNlJgbZhWL+OcXp5+QT8hBQJIMEUCIQDRK4UthdzZYdL19KtmBlTfbu3MeUwMM85cwwn/tfzljQIgZzOKjg4XJcGX+xqIr1n1HkTkJVsgFnyGhAMcBdHyWSoBIQIjtyvu8JZdEL4HeO/s1h/KxveaTqFpOTOAc0Rk+E8qswAAAAAF/AKqqgEBqwAABfwCqqoCAawA", + "cHNidP8BAFICAAAAAZ38ZijCbFiZ/hvT3DOGZb/VXXraEPYiCXPfLTht7BJ2AQAAAAD/////AfA9zR0AAAAAFgAUezoAv9wU0neVwrdJAdCdpu8TNXkAAAAATwEENYfPAto/0AiAAAAAlwSLGtBEWx7IJ1UXcnyHtOTrwYogP/oPlMAVZr046QADUbdDiH7h1A3DKmBDck8tZFmztaTXPa7I+64EcvO8Q+IM2QxqT64AAIAAAACATwEENYfPAto/0AiAAAABuQRSQnE5zXjCz/JES+NTzVhgXj5RMoXlKLQH+uP2FzUD0wpel8itvFV9rCrZp+OcFyLrrGnmaLbyZnzB1nHIPKsM2QxqT64AAIABAACAAAEBKwBlzR0AAAAAIgAgLFSGEmxJeAeagU4TcV1l82RZ5NbMre0mbQUIZFuvpjIBBUdSIQKdoSzbWyNWkrkVNq/v5ckcOrlHPY5DtTODarRWKZyIcSEDNys0I07Xz5wf6l0F1EFVeSe+lUKxYusC4ass6AIkwAtSriIGAp2hLNtbI1aSuRU2r+/lyRw6uUc9jkO1M4NqtFYpnIhxENkMak+uAACAAAAAgAAAAAAiBgM3KzQjTtfPnB/qXQXUQVV5J76VQrFi6wLhqyzoAiTACxDZDGpPrgAAgAEAAIAAAAAAACICA57/H1R6HV+S36K6evaslxpL0DukpzSwMVaiVritOh75EO3kXMUAAACAAAAAgAEAAIAA", + "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAATwEENYfPAAAAAAAAAAAAG3t93NmzqdwlifBjtWBRnFrHYkoMdmriSG1s74PiZ8ID3+4wNJ18fPeMDsRRe9iTAopsKogDQfxLmL6Kgj07xScE2QxqTwAAAAAA", + "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAFQoYn3yLGjhv/o7tkbODDHp7zR53jAIBAhUK8pG6UBXfNIyAhT+luw95RvXJ4bMBAQAAAA==", + "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAIQtL9RIvNEVUxTveLruM0rfj0WAK1jHDhaXXzOI8d4VFmgEBIQuhKHH+4hD7hhkpHq6hlFgcvSUx5LI3WdIl9oBpI/YyIgIBAgAAAA==", + "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAFQwVzEnhkcvFINkZRGAKXLd69qoykQIBAhUMxRtmvO1eRJEAG9cCZpdw3M9ECYIBAQAAAA==", + "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAIQ12pWrO2RXSUT3NhMLDeLLoqlzWMrW3HKLyrFsOOmSb2wIBAiENnBLP3ATHRYTXh6w9I3chMsGFJLx6so3sQhm4/FtCX3ABAQAAAA==" ], "creator" : [ { @@ -136,4 +145,4 @@ "result" : "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000" } ] -}
\ No newline at end of file +} diff --git a/test/functional/example_test.py b/test/functional/example_test.py index a0eb213a78..2ad96da854 100755 --- a/test/functional/example_test.py +++ b/test/functional/example_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """An example functional test @@ -141,8 +141,7 @@ class ExampleTest(BitcoinTestFramework): peer_messaging = self.nodes[0].add_p2p_connection(BaseNode()) # Generating a block on one of the nodes will get us out of IBD - blocks = [int(self.nodes[0].generate(nblocks=1)[0], 16)] - self.sync_all(self.nodes[0:2]) + blocks = [int(self.generate(self.nodes[0], sync_fun=lambda: self.sync_all(self.nodes[0:2]), nblocks=1)[0], 16)] # Notice above how we called an RPC by calling a method with the same # name on the node object. Notice also how we used a keyword argument diff --git a/test/functional/feature_abortnode.py b/test/functional/feature_abortnode.py index 8abfdef3a1..32cf4a47f4 100755 --- a/test/functional/feature_abortnode.py +++ b/test/functional/feature_abortnode.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test bitcoind aborts if can't disconnect a block. @@ -26,7 +26,7 @@ class AbortNodeTest(BitcoinTestFramework): # We'll connect the nodes later def run_test(self): - self.nodes[0].generate(3) + self.generate(self.nodes[0], 3, sync_fun=self.no_op) datadir = get_datadir_path(self.options.tmpdir, 0) # Deleting the undo file will result in reorg failure @@ -34,10 +34,10 @@ class AbortNodeTest(BitcoinTestFramework): # Connecting to a node with a more work chain will trigger a reorg # attempt. - self.nodes[1].generate(3) + self.generate(self.nodes[1], 3, sync_fun=self.no_op) with self.nodes[0].assert_debug_log(["Failed to disconnect block"]): self.connect_nodes(0, 1) - self.nodes[1].generate(1) + self.generate(self.nodes[1], 1, sync_fun=self.no_op) # Check that node0 aborted self.log.info("Waiting for crash") diff --git a/test/functional/feature_addrman.py b/test/functional/feature_addrman.py new file mode 100755 index 0000000000..14a4f8abb7 --- /dev/null +++ b/test/functional/feature_addrman.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test addrman functionality""" + +import os +import re +import struct + +from test_framework.messages import ser_uint256, hash256 +from test_framework.p2p import MAGIC_BYTES +from test_framework.test_framework import BitcoinTestFramework +from test_framework.test_node import ErrorMatch +from test_framework.util import assert_equal + + +def serialize_addrman( + *, + format=1, + lowest_compatible=4, + net_magic="regtest", + bucket_key=1, + len_new=None, + len_tried=None, + mock_checksum=None, +): + new = [] + tried = [] + INCOMPATIBILITY_BASE = 32 + r = MAGIC_BYTES[net_magic] + r += struct.pack("B", format) + r += struct.pack("B", INCOMPATIBILITY_BASE + lowest_compatible) + r += ser_uint256(bucket_key) + r += struct.pack("i", len_new or len(new)) + r += struct.pack("i", len_tried or len(tried)) + ADDRMAN_NEW_BUCKET_COUNT = 1 << 10 + r += struct.pack("i", ADDRMAN_NEW_BUCKET_COUNT ^ (1 << 30)) + for _ in range(ADDRMAN_NEW_BUCKET_COUNT): + r += struct.pack("i", 0) + checksum = hash256(r) + r += mock_checksum or checksum + return r + + +def write_addrman(peers_dat, **kwargs): + with open(peers_dat, "wb") as f: + f.write(serialize_addrman(**kwargs)) + + +class AddrmanTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + peers_dat = os.path.join(self.nodes[0].datadir, self.chain, "peers.dat") + init_error = lambda reason: ( + f"Error: Invalid or corrupt peers.dat \\({reason}\\). If you believe this " + f"is a bug, please report it to {self.config['environment']['PACKAGE_BUGREPORT']}. " + f'As a workaround, you can move the file \\("{re.escape(peers_dat)}"\\) out of the way \\(rename, ' + "move, or delete\\) to have a new one created on the next start." + ) + + self.log.info("Check that mocked addrman is valid") + self.stop_node(0) + write_addrman(peers_dat) + with self.nodes[0].assert_debug_log(["Loaded 0 addresses from peers.dat"]): + self.start_node(0, extra_args=["-checkaddrman=1"]) + assert_equal(self.nodes[0].getnodeaddresses(), []) + + self.log.info("Check that addrman from future cannot be read") + self.stop_node(0) + write_addrman(peers_dat, lowest_compatible=111) + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error( + "Unsupported format of addrman database: 1. It is compatible with " + "formats >=111, but the maximum supported by this version of " + f"{self.config['environment']['PACKAGE_NAME']} is 4.: (.+)" + ), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that corrupt addrman cannot be read (EOF)") + self.stop_node(0) + with open(peers_dat, "wb") as f: + f.write(serialize_addrman()[:-1]) + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("CAutoFile::read: end of file.*"), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that corrupt addrman cannot be read (magic)") + self.stop_node(0) + write_addrman(peers_dat, net_magic="signet") + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("Invalid network magic number"), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that corrupt addrman cannot be read (checksum)") + self.stop_node(0) + write_addrman(peers_dat, mock_checksum=b"ab" * 32) + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("Checksum mismatch, data corrupted"), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that corrupt addrman cannot be read (len_tried)") + self.stop_node(0) + write_addrman(peers_dat, len_tried=-1) + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("Corrupt AddrMan serialization: nTried=-1, should be in \\[0, 16384\\]:.*"), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that corrupt addrman cannot be read (len_new)") + self.stop_node(0) + write_addrman(peers_dat, len_new=-1) + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("Corrupt AddrMan serialization: nNew=-1, should be in \\[0, 65536\\]:.*"), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that corrupt addrman cannot be read (failed check)") + self.stop_node(0) + write_addrman(peers_dat, bucket_key=0) + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("Corrupt data. Consistency check failed with code -16: .*"), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that missing addrman is recreated") + self.stop_node(0) + os.remove(peers_dat) + with self.nodes[0].assert_debug_log([ + f'Creating peers.dat because the file was not found ("{peers_dat}")', + ]): + self.start_node(0) + assert_equal(self.nodes[0].getnodeaddresses(), []) + + +if __name__ == "__main__": + AddrmanTest().main() diff --git a/test/functional/feature_anchors.py b/test/functional/feature_anchors.py new file mode 100755 index 0000000000..713c0826d3 --- /dev/null +++ b/test/functional/feature_anchors.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test block-relay-only anchors functionality""" + +import os + +from test_framework.p2p import P2PInterface +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import check_node_connections + +INBOUND_CONNECTIONS = 5 +BLOCK_RELAY_CONNECTIONS = 2 + + +class AnchorsTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.disable_autoconnect = False + + def run_test(self): + node_anchors_path = os.path.join( + self.nodes[0].datadir, "regtest", "anchors.dat" + ) + + self.log.info("When node starts, check if anchors.dat doesn't exist") + assert not os.path.exists(node_anchors_path) + + self.log.info(f"Add {BLOCK_RELAY_CONNECTIONS} block-relay-only connections to node") + for i in range(BLOCK_RELAY_CONNECTIONS): + self.log.debug(f"block-relay-only: {i}") + self.nodes[0].add_outbound_p2p_connection( + P2PInterface(), p2p_idx=i, connection_type="block-relay-only" + ) + + self.log.info(f"Add {INBOUND_CONNECTIONS} inbound connections to node") + for i in range(INBOUND_CONNECTIONS): + self.log.debug(f"inbound: {i}") + self.nodes[0].add_p2p_connection(P2PInterface()) + + self.log.info("Check node connections") + check_node_connections(node=self.nodes[0], num_in=5, num_out=2) + + # 127.0.0.1 + ip = "7f000001" + + # Since the ip is always 127.0.0.1 for this case, + # we store only the port to identify the peers + block_relay_nodes_port = [] + inbound_nodes_port = [] + for p in self.nodes[0].getpeerinfo(): + addr_split = p["addr"].split(":") + if p["connection_type"] == "block-relay-only": + block_relay_nodes_port.append(hex(int(addr_split[1]))[2:]) + else: + inbound_nodes_port.append(hex(int(addr_split[1]))[2:]) + + self.log.info("Stop node 0") + self.stop_node(0) + + # It should contain only the block-relay-only addresses + self.log.info("Check the addresses in anchors.dat") + + with open(node_anchors_path, "rb") as file_handler: + anchors = file_handler.read().hex() + + for port in block_relay_nodes_port: + ip_port = ip + port + assert ip_port in anchors + for port in inbound_nodes_port: + ip_port = ip + port + assert ip_port not in anchors + + self.log.info("Start node") + self.start_node(0) + + self.log.info("When node starts, check if anchors.dat doesn't exist anymore") + assert not os.path.exists(node_anchors_path) + + +if __name__ == "__main__": + AnchorsTest().main() diff --git a/test/functional/feature_asmap.py b/test/functional/feature_asmap.py index 5fcecb4882..9440ba11f5 100755 --- a/test/functional/feature_asmap.py +++ b/test/functional/feature_asmap.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test asmap config argument for ASN-based IP bucketing. @@ -14,9 +14,11 @@ Verify node behaviour and debug log when launching bitcoind in these cases: 4. `bitcoind -asmap/-asmap=` with no file specified, using the default asmap -5. `bitcoind -asmap` with no file specified and a missing default asmap file +5. `bitcoind -asmap` restart with an addrman containing new and tried entries -6. `bitcoind -asmap` with an empty (unparsable) default asmap file +6. `bitcoind -asmap` with no file specified and a missing default asmap file + +7. `bitcoind -asmap` with an empty (unparsable) default asmap file The tests are order-independent. @@ -31,12 +33,18 @@ ASMAP = '../../src/test/data/asmap.raw' # path to unit test skeleton asmap VERSION = 'fec61fa21a9f46f3b17bdcd660d7f4cd90b966aad3aec593c99b35f0aca15853' def expected_messages(filename): - return ['Opened asmap file "{}" (59 bytes) from disk'.format(filename), - 'Using asmap version {} for IP bucketing'.format(VERSION)] + return [f'Opened asmap file "{filename}" (59 bytes) from disk', + f'Using asmap version {VERSION} for IP bucketing'] class AsmapTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 + self.extra_args = [["-checkaddrman=1"]] # Do addrman checks on all operations. + + def fill_addrman(self, node_id): + """Add 1 tried address to the addrman, followed by 1 new address.""" + for addr, tried in [[0, True], [1, False]]: + self.nodes[node_id].addpeeraddress(address=f"101.{addr}.0.0", tried=tried, port=8333) def test_without_asmap_arg(self): self.log.info('Test bitcoind with no -asmap arg passed') @@ -50,7 +58,7 @@ class AsmapTest(BitcoinTestFramework): filename = os.path.join(self.datadir, 'my-map-file.map') shutil.copyfile(self.asmap_raw, filename) with self.node.assert_debug_log(expected_messages(filename)): - self.start_node(0, ['-asmap={}'.format(filename)]) + self.start_node(0, [f'-asmap={filename}']) os.remove(filename) def test_asmap_with_relative_path(self): @@ -60,22 +68,38 @@ class AsmapTest(BitcoinTestFramework): filename = os.path.join(self.datadir, name) shutil.copyfile(self.asmap_raw, filename) with self.node.assert_debug_log(expected_messages(filename)): - self.start_node(0, ['-asmap={}'.format(name)]) + self.start_node(0, [f'-asmap={name}']) os.remove(filename) def test_default_asmap(self): shutil.copyfile(self.asmap_raw, self.default_asmap) for arg in ['-asmap', '-asmap=']: - self.log.info('Test bitcoind {} (using default map file)'.format(arg)) + self.log.info(f'Test bitcoind {arg} (using default map file)') self.stop_node(0) with self.node.assert_debug_log(expected_messages(self.default_asmap)): self.start_node(0, [arg]) os.remove(self.default_asmap) + def test_asmap_interaction_with_addrman_containing_entries(self): + self.log.info("Test bitcoind -asmap restart with addrman containing new and tried entries") + self.stop_node(0) + shutil.copyfile(self.asmap_raw, self.default_asmap) + self.start_node(0, ["-asmap", "-checkaddrman=1"]) + self.fill_addrman(node_id=0) + self.restart_node(0, ["-asmap", "-checkaddrman=1"]) + with self.node.assert_debug_log( + expected_msgs=[ + "CheckAddrman: new 1, tried 1, total 2 started", + "CheckAddrman: completed", + ] + ): + self.node.getnodeaddresses() # getnodeaddresses re-runs the addrman checks + os.remove(self.default_asmap) + def test_default_asmap_with_missing_file(self): self.log.info('Test bitcoind -asmap with missing default map file') self.stop_node(0) - msg = "Error: Could not find asmap file \"{}\"".format(self.default_asmap) + msg = f"Error: Could not find asmap file \"{self.default_asmap}\"" self.node.assert_start_raises_init_error(extra_args=['-asmap'], expected_msg=msg) def test_empty_asmap(self): @@ -83,7 +107,7 @@ class AsmapTest(BitcoinTestFramework): self.stop_node(0) with open(self.default_asmap, "w", encoding="utf-8") as f: f.write("") - msg = "Error: Could not parse asmap file \"{}\"".format(self.default_asmap) + msg = f"Error: Could not parse asmap file \"{self.default_asmap}\"" self.node.assert_start_raises_init_error(extra_args=['-asmap'], expected_msg=msg) os.remove(self.default_asmap) @@ -97,6 +121,7 @@ class AsmapTest(BitcoinTestFramework): self.test_asmap_with_absolute_path() self.test_asmap_with_relative_path() self.test_default_asmap() + self.test_asmap_interaction_with_addrman_containing_entries() self.test_default_asmap_with_missing_file() self.test_empty_asmap() diff --git a/test/functional/feature_assumevalid.py b/test/functional/feature_assumevalid.py index 1a148f04f4..67cacaa9ce 100755 --- a/test/functional/feature_assumevalid.py +++ b/test/functional/feature_assumevalid.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test logic for skipping signature validation on old blocks. @@ -31,6 +31,7 @@ Start three nodes: """ from test_framework.blocktools import ( + COINBASE_MATURITY, create_block, create_coinbase, ) @@ -121,11 +122,8 @@ class AssumeValidTest(BitcoinTestFramework): tx.vout.append(CTxOut(49 * 100000000, CScript([OP_TRUE]))) tx.calc_sha256() - block102 = create_block(self.tip, create_coinbase(height), self.block_time) + block102 = create_block(self.tip, create_coinbase(height), self.block_time, txlist=[tx]) self.block_time += 1 - block102.vtx.extend([tx]) - block102.hashMerkleRoot = block102.calc_merkle_root() - block102.rehash() block102.solve() self.blocks.append(block102) self.tip = block102.sha256 @@ -135,7 +133,6 @@ class AssumeValidTest(BitcoinTestFramework): # Bury the assumed valid block 2100 deep for _ in range(2100): block = create_block(self.tip, create_coinbase(height), self.block_time) - block.nVersion = 4 block.solve() self.blocks.append(block) self.tip = block.sha256 @@ -161,8 +158,8 @@ class AssumeValidTest(BitcoinTestFramework): # Send blocks to node0. Block 102 will be rejected. self.send_blocks_until_disconnected(p2p0) - self.wait_until(lambda: self.nodes[0].getblockcount() >= 101) - assert_equal(self.nodes[0].getblockcount(), 101) + self.wait_until(lambda: self.nodes[0].getblockcount() >= COINBASE_MATURITY + 1) + assert_equal(self.nodes[0].getblockcount(), COINBASE_MATURITY + 1) # Send all blocks to node1. All blocks will be accepted. for i in range(2202): @@ -173,8 +170,8 @@ class AssumeValidTest(BitcoinTestFramework): # Send blocks to node2. Block 102 will be rejected. self.send_blocks_until_disconnected(p2p2) - self.wait_until(lambda: self.nodes[2].getblockcount() >= 101) - assert_equal(self.nodes[2].getblockcount(), 101) + self.wait_until(lambda: self.nodes[2].getblockcount() >= COINBASE_MATURITY + 1) + assert_equal(self.nodes[2].getblockcount(), COINBASE_MATURITY + 1) if __name__ == '__main__': diff --git a/test/functional/feature_backwards_compatibility.py b/test/functional/feature_backwards_compatibility.py index e6a53b52db..476a6a0c14 100755 --- a/test/functional/feature_backwards_compatibility.py +++ b/test/functional/feature_backwards_compatibility.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Backwards compatibility functional test -Test various backwards compatibility scenarios. Download the previous node binaries: - -test/get_previous_releases.py -b v0.19.1 v0.18.1 v0.17.2 v0.16.3 v0.15.2 +Test various backwards compatibility scenarios. Requires previous releases binaries, +see test/README.md. v0.15.2 is not required by this test, but it is used in wallet_upgradewallet.py. Due to a hardfork in regtest, it can't be used to sync nodes. @@ -22,6 +21,7 @@ needs an older patch version. import os import shutil +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.descriptors import descsum_create @@ -64,13 +64,11 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): self.import_deterministic_coinbase_privkeys() def run_test(self): - self.nodes[0].generatetoaddress(101, self.nodes[0].getnewaddress()) - - self.sync_blocks() + self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, self.nodes[0].getnewaddress()) # Sanity check the test framework: res = self.nodes[self.num_nodes - 1].getblockchaininfo() - assert_equal(res['blocks'], 101) + assert_equal(res['blocks'], COINBASE_MATURITY + 1) node_master = self.nodes[self.num_nodes - 5] node_v19 = self.nodes[self.num_nodes - 4] @@ -92,16 +90,14 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): address = wallet.getnewaddress() self.nodes[0].sendtoaddress(address, 10) self.sync_mempools() - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) # Create a conflicting transaction using RBF return_address = self.nodes[0].getnewaddress() tx1_id = self.nodes[1].sendtoaddress(return_address, 1) tx2_id = self.nodes[1].bumpfee(tx1_id)["txid"] # Confirm the transaction self.sync_mempools() - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) # Create another conflicting transaction using RBF tx3_id = self.nodes[1].sendtoaddress(return_address, 1) tx4_id = self.nodes[1].bumpfee(tx3_id)["txid"] @@ -366,7 +362,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): assert_equal(load_res['warning'], '') wallet = node_master.get_wallet_rpc("u1_v16") info = wallet.getaddressinfo(v16_addr) - descriptor = "wpkh([" + info["hdmasterfingerprint"] + hdkeypath[1:] + "]" + v16_pubkey + ")" + descriptor = f"wpkh([{info['hdmasterfingerprint']}{hdkeypath[1:]}]{v16_pubkey})" assert_equal(info["desc"], descsum_create(descriptor)) # Now copy that same wallet back to 0.16 to make sure no automatic upgrade breaks it @@ -389,7 +385,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): node_master.loadwallet("u1_v17") wallet = node_master.get_wallet_rpc("u1_v17") info = wallet.getaddressinfo(address) - descriptor = "wpkh([" + info["hdmasterfingerprint"] + hdkeypath[1:] + "]" + pubkey + ")" + descriptor = f"wpkh([{info['hdmasterfingerprint']}{hdkeypath[1:]}]{pubkey})" assert_equal(info["desc"], descsum_create(descriptor)) # Now copy that same wallet back to 0.17 to make sure no automatic upgrade breaks it diff --git a/test/functional/feature_bind_extra.py b/test/functional/feature_bind_extra.py new file mode 100755 index 0000000000..6802da8d48 --- /dev/null +++ b/test/functional/feature_bind_extra.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Test starting bitcoind with -bind and/or -bind=...=onion and confirm +that bind happens on the expected ports. +""" + +import sys + +from test_framework.netutil import ( + addr_to_hex, + get_bind_addrs, +) +from test_framework.test_framework import ( + BitcoinTestFramework, + SkipTest, +) +from test_framework.util import ( + PORT_MIN, + PORT_RANGE, + assert_equal, + rpc_port, +) + +class BindExtraTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + # Avoid any -bind= on the command line. Force the framework to avoid + # adding -bind=127.0.0.1. + self.bind_to_localhost_only = False + self.num_nodes = 2 + + def setup_network(self): + # Override setup_network() because we want to put the result of + # p2p_port() in self.extra_args[], before the nodes are started. + # p2p_port() is not usable in set_test_params() because PortSeed.n is + # not set at that time. + + # Due to OS-specific network stats queries, we only run on Linux. + self.log.info("Checking for Linux") + if not sys.platform.startswith('linux'): + raise SkipTest("This test can only be run on Linux.") + + loopback_ipv4 = addr_to_hex("127.0.0.1") + + # Start custom ports after p2p and rpc ports. + port = PORT_MIN + 2 * PORT_RANGE + + # Array of tuples [command line arguments, expected bind addresses]. + self.expected = [] + + # Node0, no normal -bind=... with -bind=...=onion, thus only the tor target. + self.expected.append( + [ + [f"-bind=127.0.0.1:{port}=onion"], + [(loopback_ipv4, port)] + ], + ) + port += 1 + + # Node1, both -bind=... and -bind=...=onion. + self.expected.append( + [ + [f"-bind=127.0.0.1:{port}", f"-bind=127.0.0.1:{port + 1}=onion"], + [(loopback_ipv4, port), (loopback_ipv4, port + 1)] + ], + ) + port += 2 + + self.extra_args = list(map(lambda e: e[0], self.expected)) + self.add_nodes(self.num_nodes, self.extra_args) + # Don't start the nodes, as some of them would collide trying to bind on the same port. + + def run_test(self): + for i in range(len(self.expected)): + self.log.info(f"Starting node {i} with {self.expected[i][0]}") + self.start_node(i) + pid = self.nodes[i].process.pid + binds = set(get_bind_addrs(pid)) + # Remove IPv6 addresses because on some CI environments "::1" is not configured + # on the system (so our test_ipv6_local() would return False), but it is + # possible to bind on "::". This makes it unpredictable whether to expect + # that bitcoind has bound on "::1" (for RPC) and "::" (for P2P). + ipv6_addr_len_bytes = 32 + binds = set(filter(lambda e: len(e[0]) != ipv6_addr_len_bytes, binds)) + # Remove RPC ports. They are not relevant for this test. + binds = set(filter(lambda e: e[1] != rpc_port(i), binds)) + assert_equal(binds, set(self.expected[i][1])) + self.stop_node(i) + self.log.info(f"Stopped node {i}") + +if __name__ == '__main__': + BindExtraTest().main() diff --git a/test/functional/feature_bip68_sequence.py b/test/functional/feature_bip68_sequence.py index 6c5857c5ce..05d274a9fe 100755 --- a/test/functional/feature_bip68_sequence.py +++ b/test/functional/feature_bip68_sequence.py @@ -1,19 +1,29 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test BIP68 implementation.""" import time -from test_framework.blocktools import create_block, NORMAL_GBT_REQUEST_PARAMS, add_witness_commitment -from test_framework.messages import COIN, COutPoint, CTransaction, CTxIn, CTxOut, FromHex, ToHex +from test_framework.blocktools import ( + NORMAL_GBT_REQUEST_PARAMS, + add_witness_commitment, + create_block, +) +from test_framework.messages import ( + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxOut, + tx_from_hex, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than, assert_raises_rpc_error, - satoshi_round, softfork_active, ) from test_framework.script_util import DUMMY_P2WPKH_SCRIPT @@ -31,10 +41,13 @@ class BIP68Test(BitcoinTestFramework): self.num_nodes = 2 self.extra_args = [ [ + '-testactivationheight=csv@432', "-acceptnonstdtxn=1", - "-peertimeout=9999", # bump because mocktime might cause a disconnect otherwise ], - ["-acceptnonstdtxn=0"], + [ + '-testactivationheight=csv@432', + "-acceptnonstdtxn=0", + ], ] def skip_test_if_missing_module(self): @@ -44,7 +57,7 @@ class BIP68Test(BitcoinTestFramework): self.relayfee = self.nodes[0].getnetworkinfo()["relayfee"] # Generate some coins - self.nodes[0].generate(110) + self.generate(self.nodes[0], 110) self.log.info("Running test disable flag") self.test_disable_flag() @@ -80,7 +93,7 @@ class BIP68Test(BitcoinTestFramework): utxo = utxos[0] tx1 = CTransaction() - value = int(satoshi_round(utxo["amount"] - self.relayfee)*COIN) + value = int((utxo["amount"] - self.relayfee) * COIN) # Check that the disable flag disables relative locktime. # If sequence locks were used, this would require 1 block for the @@ -89,7 +102,7 @@ class BIP68Test(BitcoinTestFramework): tx1.vin = [CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]), nSequence=sequence_value)] tx1.vout = [CTxOut(value, DUMMY_P2WPKH_SCRIPT)] - tx1_signed = self.nodes[0].signrawtransactionwithwallet(ToHex(tx1))["hex"] + tx1_signed = self.nodes[0].signrawtransactionwithwallet(tx1.serialize().hex())["hex"] tx1_id = self.nodes[0].sendrawtransaction(tx1_signed) tx1_id = int(tx1_id, 16) @@ -102,13 +115,13 @@ class BIP68Test(BitcoinTestFramework): tx2.vout = [CTxOut(int(value - self.relayfee * COIN), DUMMY_P2WPKH_SCRIPT)] tx2.rehash() - assert_raises_rpc_error(-26, NOT_FINAL_ERROR, self.nodes[0].sendrawtransaction, ToHex(tx2)) + assert_raises_rpc_error(-26, NOT_FINAL_ERROR, self.nodes[0].sendrawtransaction, tx2.serialize().hex()) # Setting the version back down to 1 should disable the sequence lock, # so this should be accepted. tx2.nVersion = 1 - self.nodes[0].sendrawtransaction(ToHex(tx2)) + self.nodes[0].sendrawtransaction(tx2.serialize().hex()) # Calculate the median time past of a prior block ("confirmations" before # the current tip). @@ -132,7 +145,7 @@ class BIP68Test(BitcoinTestFramework): for i in range(num_outputs): outputs[addresses[i]] = random.randint(1, 20)*0.01 self.nodes[0].sendmany("", outputs) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) utxos = self.nodes[0].listunspent() @@ -193,9 +206,9 @@ class BIP68Test(BitcoinTestFramework): tx.vin.append(CTxIn(COutPoint(int(utxos[j]["txid"], 16), utxos[j]["vout"]), nSequence=sequence_value)) value += utxos[j]["amount"]*COIN # Overestimate the size of the tx - signatures should be less than 120 bytes, and leave 50 for the output - tx_size = len(ToHex(tx))//2 + 120*num_inputs + 50 + tx_size = len(tx.serialize().hex())//2 + 120*num_inputs + 50 tx.vout.append(CTxOut(int(value-self.relayfee*tx_size*COIN/1000), DUMMY_P2WPKH_SCRIPT)) - rawtx = self.nodes[0].signrawtransactionwithwallet(ToHex(tx))["hex"] + rawtx = self.nodes[0].signrawtransactionwithwallet(tx.serialize().hex())["hex"] if (using_sequence_locks and not should_pass): # This transaction should be rejected @@ -215,7 +228,7 @@ class BIP68Test(BitcoinTestFramework): # Create a mempool tx. txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 2) - tx1 = FromHex(CTransaction(), self.nodes[0].getrawtransaction(txid)) + tx1 = tx_from_hex(self.nodes[0].getrawtransaction(txid)) tx1.rehash() # Anyone-can-spend mempool tx. @@ -224,8 +237,8 @@ class BIP68Test(BitcoinTestFramework): tx2.nVersion = 2 tx2.vin = [CTxIn(COutPoint(tx1.sha256, 0), nSequence=0)] tx2.vout = [CTxOut(int(tx1.vout[0].nValue - self.relayfee*COIN), DUMMY_P2WPKH_SCRIPT)] - tx2_raw = self.nodes[0].signrawtransactionwithwallet(ToHex(tx2))["hex"] - tx2 = FromHex(tx2, tx2_raw) + tx2_raw = self.nodes[0].signrawtransactionwithwallet(tx2.serialize().hex())["hex"] + tx2 = tx_from_hex(tx2_raw) tx2.rehash() self.nodes[0].sendrawtransaction(tx2_raw) @@ -246,10 +259,10 @@ class BIP68Test(BitcoinTestFramework): if (orig_tx.hash in node.getrawmempool()): # sendrawtransaction should fail if the tx is in the mempool - assert_raises_rpc_error(-26, NOT_FINAL_ERROR, node.sendrawtransaction, ToHex(tx)) + assert_raises_rpc_error(-26, NOT_FINAL_ERROR, node.sendrawtransaction, tx.serialize().hex()) else: # sendrawtransaction should succeed if the tx is not in the mempool - node.sendrawtransaction(ToHex(tx)) + node.sendrawtransaction(tx.serialize().hex()) return tx @@ -262,7 +275,7 @@ class BIP68Test(BitcoinTestFramework): cur_time = int(time.time()) for _ in range(10): self.nodes[0].setmocktime(cur_time + 600) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) cur_time += 600 assert tx2.hash in self.nodes[0].getrawmempool() @@ -277,7 +290,7 @@ class BIP68Test(BitcoinTestFramework): self.nodes[0].setmocktime(cur_time+600) # Save block template now to use for the reorg later tmpl = self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) assert tx2.hash not in self.nodes[0].getrawmempool() # Now that tx2 is not in the mempool, a sequence locked spend should @@ -285,7 +298,7 @@ class BIP68Test(BitcoinTestFramework): tx3 = test_nonzero_locks(tx2, self.nodes[0], self.relayfee, use_height_lock=False) assert tx3.hash in self.nodes[0].getrawmempool() - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) assert tx3.hash not in self.nodes[0].getrawmempool() # One more test, this time using height locks @@ -299,7 +312,7 @@ class BIP68Test(BitcoinTestFramework): utxos = self.nodes[0].listunspent() tx5.vin.append(CTxIn(COutPoint(int(utxos[0]["txid"], 16), utxos[0]["vout"]), nSequence=1)) tx5.vout[0].nValue += int(utxos[0]["amount"]*COIN) - raw_tx5 = self.nodes[0].signrawtransactionwithwallet(ToHex(tx5))["hex"] + raw_tx5 = self.nodes[0].signrawtransactionwithwallet(tx5.serialize().hex())["hex"] assert_raises_rpc_error(-26, NOT_FINAL_ERROR, self.nodes[0].sendrawtransaction, raw_tx5) @@ -322,10 +335,9 @@ class BIP68Test(BitcoinTestFramework): # tx3 to be removed. for i in range(2): block = create_block(tmpl=tmpl, ntime=cur_time) - block.rehash() block.solve() tip = block.sha256 - assert_equal(None if i == 1 else 'inconclusive', self.nodes[0].submitblock(ToHex(block))) + assert_equal(None if i == 1 else 'inconclusive', self.nodes[0].submitblock(block.serialize().hex())) tmpl = self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) tmpl['previousblockhash'] = '%x' % tip tmpl['transactions'] = [] @@ -338,7 +350,7 @@ class BIP68Test(BitcoinTestFramework): # Reset the chain and get rid of the mocktimed-blocks self.nodes[0].setmocktime(0) self.nodes[0].invalidateblock(self.nodes[0].getblockhash(cur_height+1)) - self.nodes[0].generate(10) + self.generate(self.nodes[0], 10, sync_fun=self.no_op) # Make sure that BIP68 isn't being used to validate blocks prior to # activation height. If more blocks are mined prior to this test @@ -348,7 +360,7 @@ class BIP68Test(BitcoinTestFramework): assert not softfork_active(self.nodes[0], 'csv') txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 2) - tx1 = FromHex(CTransaction(), self.nodes[0].getrawtransaction(txid)) + tx1 = tx_from_hex(self.nodes[0].getrawtransaction(txid)) tx1.rehash() # Make an anyone-can-spend transaction @@ -358,11 +370,11 @@ class BIP68Test(BitcoinTestFramework): tx2.vout = [CTxOut(int(tx1.vout[0].nValue - self.relayfee*COIN), DUMMY_P2WPKH_SCRIPT)] # sign tx2 - tx2_raw = self.nodes[0].signrawtransactionwithwallet(ToHex(tx2))["hex"] - tx2 = FromHex(tx2, tx2_raw) + tx2_raw = self.nodes[0].signrawtransactionwithwallet(tx2.serialize().hex())["hex"] + tx2 = tx_from_hex(tx2_raw) tx2.rehash() - self.nodes[0].sendrawtransaction(ToHex(tx2)) + self.nodes[0].sendrawtransaction(tx2.serialize().hex()) # Now make an invalid spend of tx2 according to BIP68 sequence_value = 100 # 100 block relative locktime @@ -373,13 +385,10 @@ class BIP68Test(BitcoinTestFramework): tx3.vout = [CTxOut(int(tx2.vout[0].nValue - self.relayfee * COIN), DUMMY_P2WPKH_SCRIPT)] tx3.rehash() - assert_raises_rpc_error(-26, NOT_FINAL_ERROR, self.nodes[0].sendrawtransaction, ToHex(tx3)) + assert_raises_rpc_error(-26, NOT_FINAL_ERROR, self.nodes[0].sendrawtransaction, tx3.serialize().hex()) # make a block that violates bip68; ensure that the tip updates - block = create_block(tmpl=self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) - block.vtx.extend([tx1, tx2, tx3]) - block.hashMerkleRoot = block.calc_merkle_root() - block.rehash() + block = create_block(tmpl=self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS), txlist=[tx1, tx2, tx3]) add_witness_commitment(block) block.solve() @@ -392,9 +401,9 @@ class BIP68Test(BitcoinTestFramework): min_activation_height = 432 height = self.nodes[0].getblockcount() assert_greater_than(min_activation_height - height, 2) - self.nodes[0].generate(min_activation_height - height - 2) + self.generate(self.nodes[0], min_activation_height - height - 2, sync_fun=self.no_op) assert not softfork_active(self.nodes[0], 'csv') - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) assert softfork_active(self.nodes[0], 'csv') self.sync_blocks() @@ -404,9 +413,9 @@ class BIP68Test(BitcoinTestFramework): outputs = { self.nodes[1].getnewaddress() : 1.0 } rawtx = self.nodes[1].createrawtransaction(inputs, outputs) rawtxfund = self.nodes[1].fundrawtransaction(rawtx)['hex'] - tx = FromHex(CTransaction(), rawtxfund) + tx = tx_from_hex(rawtxfund) tx.nVersion = 2 - tx_signed = self.nodes[1].signrawtransactionwithwallet(ToHex(tx))["hex"] + tx_signed = self.nodes[1].signrawtransactionwithwallet(tx.serialize().hex())["hex"] self.nodes[1].sendrawtransaction(tx_signed) if __name__ == '__main__': diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 158efb52c9..462deeae32 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test block processing.""" @@ -22,7 +22,8 @@ from test_framework.messages import ( CTransaction, CTxIn, CTxOut, - MAX_BLOCK_BASE_SIZE, + MAX_BLOCK_WEIGHT, + SEQUENCE_FINAL, uint256_from_compact, uint256_from_str, ) @@ -37,22 +38,26 @@ from test_framework.script import ( OP_CHECKSIGVERIFY, OP_ELSE, OP_ENDIF, - OP_EQUAL, OP_DROP, OP_FALSE, - OP_HASH160, OP_IF, OP_INVALIDOPCODE, OP_RETURN, OP_TRUE, SIGHASH_ALL, LegacySignatureHash, - hash160, +) +from test_framework.script_util import ( + script_to_p2sh_script, ) from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal +from test_framework.util import ( + assert_equal, + assert_greater_than, +) from data import invalid_txs + # Use this class for tests that require behavior other than normal p2p behavior. # For now, it is used to serialize a bloated varint (b64). class CBrokenBlock(CBlock): @@ -82,7 +87,10 @@ class FullBlockTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True - self.extra_args = [['-acceptnonstdtxn=1']] # This is a consensus block test, we don't care about tx policy + self.extra_args = [[ + '-acceptnonstdtxn=1', # This is a consensus block test, we don't care about tx policy + '-testactivationheight=bip34@2', + ]] def run_test(self): node = self.nodes[0] # convenience reference to the node @@ -307,33 +315,33 @@ class FullBlockTest(BitcoinTestFramework): b22 = self.next_block(22, spend=out[5]) self.send_blocks([b22], success=False, reject_reason='bad-txns-premature-spend-of-coinbase', reconnect=True) - # Create a block on either side of MAX_BLOCK_BASE_SIZE and make sure its accepted/rejected + # Create a block on either side of MAX_BLOCK_WEIGHT and make sure its accepted/rejected # genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6 (3) # \-> b12 (3) -> b13 (4) -> b15 (5) -> b23 (6) # \-> b24 (6) -> b25 (7) # \-> b3 (1) -> b4 (2) - self.log.info("Accept a block of size MAX_BLOCK_BASE_SIZE") + self.log.info("Accept a block of weight MAX_BLOCK_WEIGHT") self.move_tip(15) b23 = self.next_block(23, spend=out[6]) tx = CTransaction() - script_length = MAX_BLOCK_BASE_SIZE - len(b23.serialize()) - 69 + script_length = (MAX_BLOCK_WEIGHT - b23.get_weight() - 276) // 4 script_output = CScript([b'\x00' * script_length]) tx.vout.append(CTxOut(0, script_output)) tx.vin.append(CTxIn(COutPoint(b23.vtx[1].sha256, 0))) b23 = self.update_block(23, [tx]) - # Make sure the math above worked out to produce a max-sized block - assert_equal(len(b23.serialize()), MAX_BLOCK_BASE_SIZE) + # Make sure the math above worked out to produce a max-weighted block + assert_equal(b23.get_weight(), MAX_BLOCK_WEIGHT) self.send_blocks([b23], True) self.save_spendable_output() - self.log.info("Reject a block of size MAX_BLOCK_BASE_SIZE + 1") + self.log.info("Reject a block of weight MAX_BLOCK_WEIGHT + 4") self.move_tip(15) b24 = self.next_block(24, spend=out[6]) - script_length = MAX_BLOCK_BASE_SIZE - len(b24.serialize()) - 69 + script_length = (MAX_BLOCK_WEIGHT - b24.get_weight() - 276) // 4 script_output = CScript([b'\x00' * (script_length + 1)]) tx.vout = [CTxOut(0, script_output)] b24 = self.update_block(24, [tx]) - assert_equal(len(b24.serialize()), MAX_BLOCK_BASE_SIZE + 1) + assert_equal(b24.get_weight(), MAX_BLOCK_WEIGHT + 1 * 4) self.send_blocks([b24], success=False, reject_reason='bad-blk-length', reconnect=True) b25 = self.next_block(25, spend=out[7]) @@ -373,7 +381,9 @@ class FullBlockTest(BitcoinTestFramework): # b30 has a max-sized coinbase scriptSig. self.move_tip(23) b30 = self.next_block(30) - b30.vtx[0].vin[0].scriptSig = b'\x00' * 100 + b30.vtx[0].vin[0].scriptSig = bytes(b30.vtx[0].vin[0].scriptSig) # Convert CScript to raw bytes + b30.vtx[0].vin[0].scriptSig += b'\x00' * (100 - len(b30.vtx[0].vin[0].scriptSig)) # Fill with 0s + assert_equal(len(b30.vtx[0].vin[0].scriptSig), 100) b30.vtx[0].rehash() b30 = self.update_block(30, []) self.send_blocks([b30], True) @@ -469,8 +479,7 @@ class FullBlockTest(BitcoinTestFramework): # Build the redeem script, hash it, use hash to create the p2sh script redeem_script = CScript([self.coinbase_pubkey] + [OP_2DUP, OP_CHECKSIGVERIFY] * 5 + [OP_CHECKSIG]) - redeem_script_hash = hash160(redeem_script) - p2sh_script = CScript([OP_HASH160, redeem_script_hash, OP_EQUAL]) + p2sh_script = script_to_p2sh_script(redeem_script) # Create a transaction that spends one satoshi to the p2sh_script, the rest to OP_TRUE # This must be signed because it is spending a coinbase @@ -485,13 +494,13 @@ class FullBlockTest(BitcoinTestFramework): # Until block is full, add tx's with 1 satoshi to p2sh_script, the rest to OP_TRUE tx_new = None tx_last = tx - total_size = len(b39.serialize()) - while(total_size < MAX_BLOCK_BASE_SIZE): + total_weight = b39.get_weight() + while total_weight < MAX_BLOCK_WEIGHT: tx_new = self.create_tx(tx_last, 1, 1, p2sh_script) tx_new.vout.append(CTxOut(tx_last.vout[1].nValue - 1, CScript([OP_TRUE]))) tx_new.rehash() - total_size += len(tx_new.serialize()) - if total_size >= MAX_BLOCK_BASE_SIZE: + total_weight += tx_new.get_weight() + if total_weight >= MAX_BLOCK_WEIGHT: break b39.vtx.append(tx_new) # add tx to block tx_last = tx_new @@ -502,7 +511,7 @@ class FullBlockTest(BitcoinTestFramework): # Make sure we didn't accidentally make too big a block. Note that the # size of the block has non-determinism due to the ECDSA signature in # the first transaction. - while (len(b39.serialize()) >= MAX_BLOCK_BASE_SIZE): + while b39.get_weight() >= MAX_BLOCK_WEIGHT: del b39.vtx[-1] b39 = self.update_block(39, []) @@ -591,6 +600,8 @@ class FullBlockTest(BitcoinTestFramework): b44.hashPrevBlock = self.tip.sha256 b44.nBits = 0x207fffff b44.vtx.append(coinbase) + tx = self.create_and_sign_transaction(out[14], 1) + b44.vtx.append(tx) b44.hashMerkleRoot = b44.calc_merkle_root() b44.solve() self.tip = b44 @@ -606,7 +617,6 @@ class FullBlockTest(BitcoinTestFramework): b45.nBits = 0x207fffff b45.vtx.append(non_coinbase) b45.hashMerkleRoot = b45.calc_merkle_root() - b45.calc_sha256() b45.solve() self.block_heights[b45.sha256] = self.block_heights[self.tip.sha256] + 1 self.tip = b45 @@ -678,7 +688,7 @@ class FullBlockTest(BitcoinTestFramework): # Test block timestamps # -> b31 (8) -> b33 (9) -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) # \-> b54 (15) - # + # -> b44 (14)\-> b48 () self.move_tip(43) b53 = self.next_block(53, spend=out[14]) self.send_blocks([b53], False) @@ -698,6 +708,21 @@ class FullBlockTest(BitcoinTestFramework): self.send_blocks([b55], True) self.save_spendable_output() + # The block which was previously rejected because of being "too far(3 hours)" must be accepted 2 hours later. + # The new block is only 1 hour into future now and we must reorg onto to the new longer chain. + # The new bestblock b48p is invalidated manually. + # -> b31 (8) -> b33 (9) -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) + # \-> b54 (15) + # -> b44 (14)\-> b48 () -> b48p () + self.log.info("Accept a previously rejected future block at a later time") + node.setmocktime(int(time.time()) + 2*60*60) + self.move_tip(48) + self.block_heights[b48.sha256] = self.block_heights[b44.sha256] + 1 # b48 is a parent of b44 + b48p = self.next_block("48p") + self.send_blocks([b48, b48p], success=True) # Reorg to the longer chain + node.invalidateblock(b48p.hash) # mark b48p as invalid + node.setmocktime(0) + # Test Merkle tree malleability # # -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57p2 (16) @@ -781,7 +806,7 @@ class FullBlockTest(BitcoinTestFramework): b58 = self.next_block(58, spend=out[17]) tx = CTransaction() assert len(out[17].vout) < 42 - tx.vin.append(CTxIn(COutPoint(out[17].sha256, 42), CScript([OP_TRUE]), 0xffffffff)) + tx.vin.append(CTxIn(COutPoint(out[17].sha256, 42), CScript([OP_TRUE]), SEQUENCE_FINAL)) tx.vout.append(CTxOut(0, b"")) tx.calc_sha256() b58 = self.update_block(58, [tx]) @@ -817,6 +842,7 @@ class FullBlockTest(BitcoinTestFramework): b61.vtx[0].rehash() b61 = self.update_block(61, []) assert_equal(duplicate_tx.serialize(), b61.vtx[0].serialize()) + # BIP30 is always checked on regtest, regardless of the BIP34 activation height self.send_blocks([b61], success=False, reject_reason='bad-txns-BIP30', reconnect=True) # Test BIP30 (allow duplicate if spent) @@ -855,7 +881,7 @@ class FullBlockTest(BitcoinTestFramework): tx.nLockTime = 0xffffffff # this locktime is non-final tx.vin.append(CTxIn(COutPoint(out[18].sha256, 0))) # don't set nSequence tx.vout.append(CTxOut(0, CScript([OP_TRUE]))) - assert tx.vin[0].nSequence < 0xffffffff + assert_greater_than(SEQUENCE_FINAL, tx.vin[0].nSequence) tx.calc_sha256() b62 = self.update_block(62, [tx]) self.send_blocks([b62], success=False, reject_reason='bad-txns-nonfinal', reconnect=True) @@ -875,7 +901,7 @@ class FullBlockTest(BitcoinTestFramework): self.send_blocks([b63], success=False, reject_reason='bad-txns-nonfinal', reconnect=True) # This checks that a block with a bloated VARINT between the block_header and the array of tx such that - # the block is > MAX_BLOCK_BASE_SIZE with the bloated varint, but <= MAX_BLOCK_BASE_SIZE without the bloated varint, + # the block is > MAX_BLOCK_WEIGHT with the bloated varint, but <= MAX_BLOCK_WEIGHT without the bloated varint, # does not cause a subsequent, identical block with canonical encoding to be rejected. The test does not # care whether the bloated block is accepted or rejected; it only cares that the second block is accepted. # @@ -900,12 +926,12 @@ class FullBlockTest(BitcoinTestFramework): tx = CTransaction() # use canonical serialization to calculate size - script_length = MAX_BLOCK_BASE_SIZE - len(b64a.normal_serialize()) - 69 + script_length = (MAX_BLOCK_WEIGHT - 4 * len(b64a.normal_serialize()) - 276) // 4 script_output = CScript([b'\x00' * script_length]) tx.vout.append(CTxOut(0, script_output)) tx.vin.append(CTxIn(COutPoint(b64a.vtx[1].sha256, 0))) b64a = self.update_block("64a", [tx]) - assert_equal(len(b64a.serialize()), MAX_BLOCK_BASE_SIZE + 8) + assert_equal(b64a.get_weight(), MAX_BLOCK_WEIGHT + 8 * 4) self.send_blocks([b64a], success=False, reject_reason='non-canonical ReadCompactSize()') # bitcoind doesn't disconnect us for sending a bloated block, but if we subsequently @@ -919,7 +945,7 @@ class FullBlockTest(BitcoinTestFramework): b64 = CBlock(b64a) b64.vtx = copy.deepcopy(b64a.vtx) assert_equal(b64.hash, b64a.hash) - assert_equal(len(b64.serialize()), MAX_BLOCK_BASE_SIZE) + assert_equal(b64.get_weight(), MAX_BLOCK_WEIGHT) self.blocks[64] = b64 b64 = self.update_block(64, []) self.send_blocks([b64], True) @@ -1003,7 +1029,7 @@ class FullBlockTest(BitcoinTestFramework): bogus_tx = CTransaction() bogus_tx.sha256 = uint256_from_str(b"23c70ed7c0506e9178fc1a987f40a33946d4ad4c962b5ae3a52546da53af0c5c") tx = CTransaction() - tx.vin.append(CTxIn(COutPoint(bogus_tx.sha256, 0), b"", 0xffffffff)) + tx.vin.append(CTxIn(COutPoint(bogus_tx.sha256, 0), b"", SEQUENCE_FINAL)) tx.vout.append(CTxOut(1, b"")) b70 = self.update_block(70, [tx]) self.send_blocks([b70], success=False, reject_reason='bad-txns-inputs-missingorspent', reconnect=True) @@ -1253,12 +1279,12 @@ class FullBlockTest(BitcoinTestFramework): for i in range(89, LARGE_REORG_SIZE + 89): b = self.next_block(i, spend) tx = CTransaction() - script_length = MAX_BLOCK_BASE_SIZE - len(b.serialize()) - 69 + script_length = (MAX_BLOCK_WEIGHT - b.get_weight() - 276) // 4 script_output = CScript([b'\x00' * script_length]) tx.vout.append(CTxOut(0, script_output)) tx.vin.append(CTxIn(COutPoint(b.vtx[1].sha256, 0))) b = self.update_block(i, [tx]) - assert_equal(len(b.serialize()), MAX_BLOCK_BASE_SIZE) + assert_equal(b.get_weight(), MAX_BLOCK_WEIGHT) blocks.append(b) self.save_spendable_output() spend = self.get_spendable_output() @@ -1308,7 +1334,7 @@ class FullBlockTest(BitcoinTestFramework): return create_tx_with_script(spend_tx, n, amount=value, script_pub_key=script) # sign a transaction, using the key we know about - # this signs input 0 in tx, which is assumed to be spending output n in spend_tx + # this signs input 0 in tx, which is assumed to be spending output 0 in spend_tx def sign_tx(self, tx, spend_tx): scriptPubKey = bytearray(spend_tx.vout[0].scriptPubKey) if (scriptPubKey[0] == OP_TRUE): # an anyone-can-spend @@ -1340,11 +1366,10 @@ class FullBlockTest(BitcoinTestFramework): else: coinbase.vout[0].nValue += spend.vout[0].nValue - 1 # all but one satoshi to fees coinbase.rehash() - block = create_block(base_block_hash, coinbase, block_time, version=version) tx = self.create_tx(spend, 0, 1, script) # spend 1 satoshi self.sign_tx(tx, spend) - self.add_transactions_to_block(block, [tx]) - block.hashMerkleRoot = block.calc_merkle_root() + tx.rehash() + block = create_block(base_block_hash, coinbase, block_time, version=version, txlist=[tx]) # Block is created. Find a valid nonce. block.solve() self.tip = block diff --git a/test/functional/feature_blockfilterindex_prune.py b/test/functional/feature_blockfilterindex_prune.py index d13d191b20..2451988135 100755 --- a/test/functional/feature_blockfilterindex_prune.py +++ b/test/functional/feature_blockfilterindex_prune.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test blockfilterindex in conjunction with prune.""" @@ -24,16 +24,14 @@ class FeatureBlockfilterindexPruneTest(BitcoinTestFramework): self.log.info("check if we can access a blockfilter when pruning is enabled but no blocks are actually pruned") self.sync_index(height=200) assert_greater_than(len(self.nodes[0].getblockfilter(self.nodes[0].getbestblockhash())['filter']), 0) - # Mine two batches of blocks to avoid hitting NODE_NETWORK_LIMITED_MIN_BLOCKS disconnection - self.nodes[0].generate(250) - self.sync_all() - self.nodes[0].generate(250) - self.sync_all() + self.generate(self.nodes[0], 500) self.sync_index(height=700) self.log.info("prune some blocks") pruneheight = self.nodes[0].pruneblockchain(400) - assert_equal(pruneheight, 250) + # the prune heights used here and below are magic numbers that are determined by the + # thresholds at which block files wrap, so they depend on disk serialization and default block file size. + assert_equal(pruneheight, 248) self.log.info("check if we can access the tips blockfilter when we have pruned some blocks") assert_greater_than(len(self.nodes[0].getblockfilter(self.nodes[0].getbestblockhash())['filter']), 0) @@ -41,24 +39,39 @@ class FeatureBlockfilterindexPruneTest(BitcoinTestFramework): self.log.info("check if we can access the blockfilter of a pruned block") assert_greater_than(len(self.nodes[0].getblockfilter(self.nodes[0].getblockhash(2))['filter']), 0) + # mine and sync index up to a height that will later be the pruneheight + self.generate(self.nodes[0], 298) + self.sync_index(height=998) + self.log.info("start node without blockfilterindex") self.restart_node(0, extra_args=["-fastprune", "-prune=1"]) self.log.info("make sure accessing the blockfilters throws an error") assert_raises_rpc_error(-1, "Index is not enabled for filtertype basic", self.nodes[0].getblockfilter, self.nodes[0].getblockhash(2)) - self.nodes[0].generate(1000) + self.generate(self.nodes[0], 502) + + self.log.info("prune exactly up to the blockfilterindexes best block while blockfilters are disabled") + pruneheight_2 = self.nodes[0].pruneblockchain(1000) + assert_equal(pruneheight_2, 998) + self.restart_node(0, extra_args=["-fastprune", "-prune=1", "-blockfilterindex=1"]) + self.log.info("make sure that we can continue with the partially synced index after having pruned up to the index height") + self.sync_index(height=1500) self.log.info("prune below the blockfilterindexes best block while blockfilters are disabled") - pruneheight_new = self.nodes[0].pruneblockchain(1000) - assert_greater_than(pruneheight_new, pruneheight) + self.restart_node(0, extra_args=["-fastprune", "-prune=1"]) + self.generate(self.nodes[0], 1000) + pruneheight_3 = self.nodes[0].pruneblockchain(2000) + assert_greater_than(pruneheight_3, pruneheight_2) self.stop_node(0) self.log.info("make sure we get an init error when starting the node again with block filters") - with self.nodes[0].assert_debug_log(["basic block filter index best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again)"]): - self.nodes[0].assert_start_raises_init_error(extra_args=["-fastprune", "-prune=1", "-blockfilterindex=1"]) + self.nodes[0].assert_start_raises_init_error( + extra_args=["-fastprune", "-prune=1", "-blockfilterindex=1"], + expected_msg="Error: basic block filter index best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again)", + ) self.log.info("make sure the node starts again with the -reindex arg") - self.start_node(0, extra_args = ["-fastprune", "-prune=1", "-blockfilterindex", "-reindex"]) + self.start_node(0, extra_args=["-fastprune", "-prune=1", "-blockfilterindex", "-reindex"]) if __name__ == '__main__': diff --git a/test/functional/feature_blocksdir.py b/test/functional/feature_blocksdir.py index 7bfad52c24..e8d2ec3676 100755 --- a/test/functional/feature_blocksdir.py +++ b/test/functional/feature_blocksdir.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2019 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the blocksdir option. @@ -24,12 +24,12 @@ class BlocksdirTest(BitcoinTestFramework): initialize_datadir(self.options.tmpdir, 0, self.chain) self.log.info("Starting with nonexistent blocksdir ...") blocksdir_path = os.path.join(self.options.tmpdir, 'blocksdir') - self.nodes[0].assert_start_raises_init_error(["-blocksdir=" + blocksdir_path], 'Error: Specified blocks directory "{}" does not exist.'.format(blocksdir_path)) + self.nodes[0].assert_start_raises_init_error([f"-blocksdir={blocksdir_path}"], f'Error: Specified blocks directory "{blocksdir_path}" does not exist.') os.mkdir(blocksdir_path) self.log.info("Starting with existing blocksdir ...") - self.start_node(0, ["-blocksdir=" + blocksdir_path]) + self.start_node(0, [f"-blocksdir={blocksdir_path}"]) self.log.info("mining blocks..") - self.nodes[0].generatetoaddress(10, self.nodes[0].get_deterministic_priv_key().address) + self.generatetoaddress(self.nodes[0], 10, self.nodes[0].get_deterministic_priv_key().address) assert os.path.isfile(os.path.join(blocksdir_path, self.chain, "blocks", "blk00000.dat")) assert os.path.isdir(os.path.join(self.nodes[0].datadir, self.chain, "blocks", "index")) diff --git a/test/functional/feature_cltv.py b/test/functional/feature_cltv.py index b7c2887ee8..9d32749a08 100755 --- a/test/functional/feature_cltv.py +++ b/test/functional/feature_cltv.py @@ -1,60 +1,89 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test BIP65 (CHECKLOCKTIMEVERIFY). -Test that the CHECKLOCKTIMEVERIFY soft-fork activates at (regtest) block height -1351. +Test that the CHECKLOCKTIMEVERIFY soft-fork activates. """ -from test_framework.blocktools import create_coinbase, create_block, create_transaction -from test_framework.messages import CTransaction, msg_block, ToHex +from test_framework.blocktools import ( + create_block, + create_coinbase, +) +from test_framework.messages import ( + CTransaction, + SEQUENCE_FINAL, + msg_block, +) from test_framework.p2p import P2PInterface -from test_framework.script import CScript, OP_1NEGATE, OP_CHECKLOCKTIMEVERIFY, OP_DROP, CScriptNum +from test_framework.script import ( + CScript, + CScriptNum, + OP_1NEGATE, + OP_CHECKLOCKTIMEVERIFY, + OP_DROP, +) from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import ( - assert_equal, - hex_str_to_bytes, +from test_framework.util import assert_equal +from test_framework.wallet import ( + MiniWallet, + MiniWalletMode, ) -from io import BytesIO -CLTV_HEIGHT = 1351 +# Helper function to modify a transaction by +# 1) prepending a given script to the scriptSig of vin 0 and +# 2) (optionally) modify the nSequence of vin 0 and the tx's nLockTime +def cltv_modify_tx(tx, prepend_scriptsig, nsequence=None, nlocktime=None): + assert_equal(len(tx.vin), 1) + if nsequence is not None: + tx.vin[0].nSequence = nsequence + tx.nLockTime = nlocktime + + tx.vin[0].scriptSig = CScript(prepend_scriptsig + list(CScript(tx.vin[0].scriptSig))) + tx.rehash() + +def cltv_invalidate(tx, failure_reason): + # Modify the signature in vin 0 and nSequence/nLockTime of the tx to fail CLTV + # + # According to BIP65, OP_CHECKLOCKTIMEVERIFY can fail due the following reasons: + # 1) the stack is empty + # 2) the top item on the stack is less than 0 + # 3) the lock-time type (height vs. timestamp) of the top stack item and the + # nLockTime field are not the same + # 4) the top stack item is greater than the transaction's nLockTime field + # 5) the nSequence field of the txin is 0xffffffff (SEQUENCE_FINAL) + assert failure_reason in range(5) + scheme = [ + # | Script to prepend to scriptSig | nSequence | nLockTime | + # +-------------------------------------------------+------------+--------------+ + [[OP_CHECKLOCKTIMEVERIFY], None, None], + [[OP_1NEGATE, OP_CHECKLOCKTIMEVERIFY, OP_DROP], None, None], + [[CScriptNum(100), OP_CHECKLOCKTIMEVERIFY, OP_DROP], 0, 1296688602], # timestamp of genesis block + [[CScriptNum(100), OP_CHECKLOCKTIMEVERIFY, OP_DROP], 0, 50], + [[CScriptNum(50), OP_CHECKLOCKTIMEVERIFY, OP_DROP], SEQUENCE_FINAL, 50], + ][failure_reason] -def cltv_invalidate(tx): - '''Modify the signature in vin 0 of the tx to fail CLTV + cltv_modify_tx(tx, prepend_scriptsig=scheme[0], nsequence=scheme[1], nlocktime=scheme[2]) - Prepends -1 CLTV DROP in the scriptSig itself. - TODO: test more ways that transactions using CLTV could be invalid (eg - locktime requirements fail, sequence time requirements fail, etc). - ''' - tx.vin[0].scriptSig = CScript([OP_1NEGATE, OP_CHECKLOCKTIMEVERIFY, OP_DROP] + - list(CScript(tx.vin[0].scriptSig))) +def cltv_validate(tx, height): + # Modify the signature in vin 0 and nSequence/nLockTime of the tx to pass CLTV + scheme = [[CScriptNum(height), OP_CHECKLOCKTIMEVERIFY, OP_DROP], 0, height] -def cltv_validate(node, tx, height): - '''Modify the signature in vin 0 of the tx to pass CLTV - Prepends <height> CLTV DROP in the scriptSig, and sets - the locktime to height''' - tx.vin[0].nSequence = 0 - tx.nLockTime = height + cltv_modify_tx(tx, prepend_scriptsig=scheme[0], nsequence=scheme[1], nlocktime=scheme[2]) - # Need to re-sign, since nSequence and nLockTime changed - signed_result = node.signrawtransactionwithwallet(ToHex(tx)) - new_tx = CTransaction() - new_tx.deserialize(BytesIO(hex_str_to_bytes(signed_result['hex']))) - new_tx.vin[0].scriptSig = CScript([CScriptNum(height), OP_CHECKLOCKTIMEVERIFY, OP_DROP] + - list(CScript(new_tx.vin[0].scriptSig))) - return new_tx +CLTV_HEIGHT = 111 class BIP65Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [[ + f'-testactivationheight=cltv@{CLTV_HEIGHT}', '-whitelist=noban@127.0.0.1', '-par=1', # Use only one script thread to get the exact reject reason for testing '-acceptnonstdtxn=1', # cltv_invalidate is nonstandard @@ -62,12 +91,8 @@ class BIP65Test(BitcoinTestFramework): self.setup_clean_chain = True self.rpc_timeout = 480 - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def test_cltv_info(self, *, is_active): - assert_equal(self.nodes[0].getblockchaininfo()['softforks']['bip65'], - { + assert_equal(self.nodes[0].getdeploymentinfo()['deployments']['bip65'], { "active": is_active, "height": CLTV_HEIGHT, "type": "buried", @@ -76,26 +101,27 @@ class BIP65Test(BitcoinTestFramework): def run_test(self): peer = self.nodes[0].add_p2p_connection(P2PInterface()) + wallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_OP_TRUE) self.test_cltv_info(is_active=False) self.log.info("Mining %d blocks", CLTV_HEIGHT - 2) - self.coinbase_txids = [self.nodes[0].getblock(b)['tx'][0] for b in self.nodes[0].generate(CLTV_HEIGHT - 2)] - self.nodeaddress = self.nodes[0].getnewaddress() + self.generate(wallet, 10) + self.generate(self.nodes[0], CLTV_HEIGHT - 2 - 10) + assert_equal(self.nodes[0].getblockcount(), CLTV_HEIGHT - 2) - self.log.info("Test that an invalid-according-to-CLTV transaction can still appear in a block") + self.log.info("Test that invalid-according-to-CLTV transactions can still appear in a block") - spendtx = create_transaction(self.nodes[0], self.coinbase_txids[0], - self.nodeaddress, amount=1.0) - cltv_invalidate(spendtx) - spendtx.rehash() + # create one invalid tx per CLTV failure reason (5 in total) and collect them + invalid_cltv_txs = [] + for i in range(5): + spendtx = wallet.create_self_transfer()['tx'] + cltv_invalidate(spendtx, i) + invalid_cltv_txs.append(spendtx) tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblockheader(tip)['mediantime'] + 1 - block = create_block(int(tip, 16), create_coinbase(CLTV_HEIGHT - 1), block_time) - block.nVersion = 3 - block.vtx.append(spendtx) - block.hashMerkleRoot = block.calc_merkle_root() + block = create_block(int(tip, 16), create_coinbase(CLTV_HEIGHT - 1), block_time, version=3, txlist=invalid_cltv_txs) block.solve() self.test_cltv_info(is_active=False) # Not active as of current tip and next block does not need to obey rules @@ -106,48 +132,54 @@ class BIP65Test(BitcoinTestFramework): self.log.info("Test that blocks must now be at least version 4") tip = block.sha256 block_time += 1 - block = create_block(tip, create_coinbase(CLTV_HEIGHT), block_time) - block.nVersion = 3 + block = create_block(tip, create_coinbase(CLTV_HEIGHT), block_time, version=3) block.solve() - with self.nodes[0].assert_debug_log(expected_msgs=['{}, bad-version(0x00000003)'.format(block.hash)]): + with self.nodes[0].assert_debug_log(expected_msgs=[f'{block.hash}, bad-version(0x00000003)']): peer.send_and_ping(msg_block(block)) assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) peer.sync_with_ping() - self.log.info("Test that invalid-according-to-cltv transactions cannot appear in a block") + self.log.info("Test that invalid-according-to-CLTV transactions cannot appear in a block") block.nVersion = 4 - - spendtx = create_transaction(self.nodes[0], self.coinbase_txids[1], - self.nodeaddress, amount=1.0) - cltv_invalidate(spendtx) - spendtx.rehash() - - # First we show that this tx is valid except for CLTV by getting it - # rejected from the mempool for exactly that reason. - assert_equal( - [{ - 'txid': spendtx.hash, - 'wtxid': spendtx.getwtxid(), - 'allowed': False, - 'reject-reason': 'non-mandatory-script-verify-flag (Negative locktime)', - }], - self.nodes[0].testmempoolaccept(rawtxs=[spendtx.serialize().hex()], maxfeerate=0), - ) - - # Now we verify that a block with this transaction is also invalid. - block.vtx.append(spendtx) - block.hashMerkleRoot = block.calc_merkle_root() - block.solve() - - with self.nodes[0].assert_debug_log(expected_msgs=['CheckInputScripts on {} failed with non-mandatory-script-verify-flag (Negative locktime)'.format(block.vtx[-1].hash)]): - peer.send_and_ping(msg_block(block)) - assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) - peer.sync_with_ping() + block.vtx.append(CTransaction()) # dummy tx after coinbase that will be replaced later + + # create and test one invalid tx per CLTV failure reason (5 in total) + for i in range(5): + spendtx = wallet.create_self_transfer()['tx'] + cltv_invalidate(spendtx, i) + + expected_cltv_reject_reason = [ + "non-mandatory-script-verify-flag (Operation not valid with the current stack size)", + "non-mandatory-script-verify-flag (Negative locktime)", + "non-mandatory-script-verify-flag (Locktime requirement not satisfied)", + "non-mandatory-script-verify-flag (Locktime requirement not satisfied)", + "non-mandatory-script-verify-flag (Locktime requirement not satisfied)", + ][i] + # First we show that this tx is valid except for CLTV by getting it + # rejected from the mempool for exactly that reason. + assert_equal( + [{ + 'txid': spendtx.hash, + 'wtxid': spendtx.getwtxid(), + 'allowed': False, + 'reject-reason': expected_cltv_reject_reason, + }], + self.nodes[0].testmempoolaccept(rawtxs=[spendtx.serialize().hex()], maxfeerate=0), + ) + + # Now we verify that a block with this transaction is also invalid. + block.vtx[1] = spendtx + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + + with self.nodes[0].assert_debug_log(expected_msgs=[f'CheckInputScripts on {block.vtx[-1].hash} failed with {expected_cltv_reject_reason}']): + peer.send_and_ping(msg_block(block)) + assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) + peer.sync_with_ping() self.log.info("Test that a version 4 block with a valid-according-to-CLTV transaction is accepted") - spendtx = cltv_validate(self.nodes[0], spendtx, CLTV_HEIGHT - 1) - spendtx.rehash() + cltv_validate(spendtx, CLTV_HEIGHT - 1) block.vtx.pop(1) block.vtx.append(spendtx) diff --git a/test/functional/feature_coinstatsindex.py b/test/functional/feature_coinstatsindex.py new file mode 100755 index 0000000000..c70f8a83db --- /dev/null +++ b/test/functional/feature_coinstatsindex.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test coinstatsindex across nodes. + +Test that the values returned by gettxoutsetinfo are consistent +between a node running the coinstatsindex and a node without +the index. +""" + +from decimal import Decimal + +from test_framework.blocktools import ( + COINBASE_MATURITY, + create_block, + create_coinbase, +) +from test_framework.messages import ( + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxOut, +) +from test_framework.script import ( + CScript, + OP_FALSE, + OP_RETURN, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) + +class CoinStatsIndexTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 2 + self.supports_cli = False + self.extra_args = [ + # Explicitly set the output type in order to have consistent tx vsize / fees + # for both legacy and descriptor wallets (disables the change address type detection algorithm) + ["-addresstype=bech32", "-changetype=bech32"], + ["-coinstatsindex"] + ] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + self._test_coin_stats_index() + self._test_use_index_option() + self._test_reorg_index() + self._test_index_rejects_hash_serialized() + + def block_sanity_check(self, block_info): + block_subsidy = 50 + assert_equal( + block_info['prevout_spent'] + block_subsidy, + block_info['new_outputs_ex_coinbase'] + block_info['coinbase'] + block_info['unspendable'] + ) + + def _test_coin_stats_index(self): + node = self.nodes[0] + index_node = self.nodes[1] + # Both none and muhash options allow the usage of the index + index_hash_options = ['none', 'muhash'] + + # Generate a normal transaction and mine it + self.generate(node, COINBASE_MATURITY + 1) + address = self.nodes[0].get_deterministic_priv_key().address + node.sendtoaddress(address=address, amount=10, subtractfeefromamount=True) + self.generate(node, 1) + + self.log.info("Test that gettxoutsetinfo() output is consistent with or without coinstatsindex option") + res0 = node.gettxoutsetinfo('none') + + # The fields 'disk_size' and 'transactions' do not exist on the index + del res0['disk_size'], res0['transactions'] + + for hash_option in index_hash_options: + res1 = index_node.gettxoutsetinfo(hash_option) + # The fields 'block_info' and 'total_unspendable_amount' only exist on the index + del res1['block_info'], res1['total_unspendable_amount'] + res1.pop('muhash', None) + + # Everything left should be the same + assert_equal(res1, res0) + + self.log.info("Test that gettxoutsetinfo() can get fetch data on specific heights with index") + + # Generate a new tip + self.generate(node, 5) + + for hash_option in index_hash_options: + # Fetch old stats by height + res2 = index_node.gettxoutsetinfo(hash_option, 102) + del res2['block_info'], res2['total_unspendable_amount'] + res2.pop('muhash', None) + assert_equal(res0, res2) + + # Fetch old stats by hash + res3 = index_node.gettxoutsetinfo(hash_option, res0['bestblock']) + del res3['block_info'], res3['total_unspendable_amount'] + res3.pop('muhash', None) + assert_equal(res0, res3) + + # It does not work without coinstatsindex + assert_raises_rpc_error(-8, "Querying specific block heights requires coinstatsindex", node.gettxoutsetinfo, hash_option, 102) + + self.log.info("Test gettxoutsetinfo() with index and verbose flag") + + for hash_option in index_hash_options: + # Genesis block is unspendable + res4 = index_node.gettxoutsetinfo(hash_option, 0) + assert_equal(res4['total_unspendable_amount'], 50) + assert_equal(res4['block_info'], { + 'unspendable': 50, + 'prevout_spent': 0, + 'new_outputs_ex_coinbase': 0, + 'coinbase': 0, + 'unspendables': { + 'genesis_block': 50, + 'bip30': 0, + 'scripts': 0, + 'unclaimed_rewards': 0 + } + }) + self.block_sanity_check(res4['block_info']) + + # Test an older block height that included a normal tx + res5 = index_node.gettxoutsetinfo(hash_option, 102) + assert_equal(res5['total_unspendable_amount'], 50) + assert_equal(res5['block_info'], { + 'unspendable': 0, + 'prevout_spent': 50, + 'new_outputs_ex_coinbase': Decimal('49.99995560'), + 'coinbase': Decimal('50.00004440'), + 'unspendables': { + 'genesis_block': 0, + 'bip30': 0, + 'scripts': 0, + 'unclaimed_rewards': 0 + } + }) + self.block_sanity_check(res5['block_info']) + + # Generate and send a normal tx with two outputs + tx1_inputs = [] + tx1_outputs = {self.nodes[0].getnewaddress(): 21, self.nodes[0].getnewaddress(): 42} + raw_tx1 = self.nodes[0].createrawtransaction(tx1_inputs, tx1_outputs) + funded_tx1 = self.nodes[0].fundrawtransaction(raw_tx1) + signed_tx1 = self.nodes[0].signrawtransactionwithwallet(funded_tx1['hex']) + tx1_txid = self.nodes[0].sendrawtransaction(signed_tx1['hex']) + + # Find the right position of the 21 BTC output + tx1_final = self.nodes[0].gettransaction(tx1_txid) + for output in tx1_final['details']: + if output['amount'] == Decimal('21.00000000') and output['category'] == 'receive': + n = output['vout'] + + # Generate and send another tx with an OP_RETURN output (which is unspendable) + tx2 = CTransaction() + tx2.vin.append(CTxIn(COutPoint(int(tx1_txid, 16), n), b'')) + tx2.vout.append(CTxOut(int(Decimal('20.99') * COIN), CScript([OP_RETURN] + [OP_FALSE]*30))) + tx2_hex = self.nodes[0].signrawtransactionwithwallet(tx2.serialize().hex())['hex'] + self.nodes[0].sendrawtransaction(tx2_hex) + + # Include both txs in a block + self.generate(self.nodes[0], 1) + + for hash_option in index_hash_options: + # Check all amounts were registered correctly + res6 = index_node.gettxoutsetinfo(hash_option, 108) + assert_equal(res6['total_unspendable_amount'], Decimal('70.99000000')) + assert_equal(res6['block_info'], { + 'unspendable': Decimal('20.99000000'), + 'prevout_spent': 111, + 'new_outputs_ex_coinbase': Decimal('89.99993620'), + 'coinbase': Decimal('50.01006380'), + 'unspendables': { + 'genesis_block': 0, + 'bip30': 0, + 'scripts': Decimal('20.99000000'), + 'unclaimed_rewards': 0 + } + }) + self.block_sanity_check(res6['block_info']) + + # Create a coinbase that does not claim full subsidy and also + # has two outputs + cb = create_coinbase(109, nValue=35) + cb.vout.append(CTxOut(5 * COIN, CScript([OP_FALSE]))) + cb.rehash() + + # Generate a block that includes previous coinbase + tip = self.nodes[0].getbestblockhash() + block_time = self.nodes[0].getblock(tip)['time'] + 1 + block = create_block(int(tip, 16), cb, block_time) + block.solve() + self.nodes[0].submitblock(block.serialize().hex()) + self.sync_all() + + for hash_option in index_hash_options: + res7 = index_node.gettxoutsetinfo(hash_option, 109) + assert_equal(res7['total_unspendable_amount'], Decimal('80.99000000')) + assert_equal(res7['block_info'], { + 'unspendable': 10, + 'prevout_spent': 0, + 'new_outputs_ex_coinbase': 0, + 'coinbase': 40, + 'unspendables': { + 'genesis_block': 0, + 'bip30': 0, + 'scripts': 0, + 'unclaimed_rewards': 10 + } + }) + self.block_sanity_check(res7['block_info']) + + self.log.info("Test that the index is robust across restarts") + + res8 = index_node.gettxoutsetinfo('muhash') + self.restart_node(1, extra_args=self.extra_args[1]) + res9 = index_node.gettxoutsetinfo('muhash') + assert_equal(res8, res9) + + self.generate(index_node, 1, sync_fun=self.no_op) + res10 = index_node.gettxoutsetinfo('muhash') + assert(res8['txouts'] < res10['txouts']) + + def _test_use_index_option(self): + self.log.info("Test use_index option for nodes running the index") + + self.connect_nodes(0, 1) + self.nodes[0].waitforblockheight(110) + res = self.nodes[0].gettxoutsetinfo('muhash') + option_res = self.nodes[1].gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False) + del res['disk_size'], option_res['disk_size'] + assert_equal(res, option_res) + + def _test_reorg_index(self): + self.log.info("Test that index can handle reorgs") + + # Generate two block, let the index catch up, then invalidate the blocks + index_node = self.nodes[1] + reorg_blocks = self.generatetoaddress(index_node, 2, index_node.getnewaddress()) + reorg_block = reorg_blocks[1] + res_invalid = index_node.gettxoutsetinfo('muhash') + index_node.invalidateblock(reorg_blocks[0]) + assert_equal(index_node.gettxoutsetinfo('muhash')['height'], 110) + + # Add two new blocks + block = self.generate(index_node, 2, sync_fun=self.no_op)[1] + res = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False) + + # Test that the result of the reorged block is not returned for its old block height + res2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112) + assert_equal(res["bestblock"], block) + assert_equal(res["muhash"], res2["muhash"]) + assert(res["muhash"] != res_invalid["muhash"]) + + # Test that requesting reorged out block by hash is still returning correct results + res_invalid2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=reorg_block) + assert_equal(res_invalid2["muhash"], res_invalid["muhash"]) + assert(res["muhash"] != res_invalid2["muhash"]) + + # Add another block, so we don't depend on reconsiderblock remembering which + # blocks were touched by invalidateblock + self.generate(index_node, 1) + + # Ensure that removing and re-adding blocks yields consistent results + block = index_node.getblockhash(99) + index_node.invalidateblock(block) + index_node.reconsiderblock(block) + res3 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112) + assert_equal(res2, res3) + + def _test_index_rejects_hash_serialized(self): + self.log.info("Test that the rpc raises if the legacy hash is passed with the index") + + msg = "hash_serialized_2 hash type cannot be queried for a specific block" + assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_2', hash_or_height=111) + + for use_index in {True, False, None}: + assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_2', hash_or_height=111, use_index=use_index) + + +if __name__ == '__main__': + CoinStatsIndexTest().main() diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py index a0bcd9f12a..eea5fa24ee 100755 --- a/test/functional/feature_config_args.py +++ b/test/functional/feature_config_args.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test various command line arguments and configuration file parameters.""" @@ -17,13 +17,14 @@ class ConfArgsTest(BitcoinTestFramework): self.num_nodes = 1 self.supports_cli = False self.wallet_names = [] + self.disable_autoconnect = False def test_config_file_parser(self): self.stop_node(0) inc_conf_file_path = os.path.join(self.nodes[0].datadir, 'include.conf') with open(os.path.join(self.nodes[0].datadir, 'bitcoin.conf'), 'a', encoding='utf-8') as conf: - conf.write('includeconf={}\n'.format(inc_conf_file_path)) + conf.write(f'includeconf={inc_conf_file_path}\n') self.nodes[0].assert_start_raises_init_error( expected_msg='Error: Error parsing command line arguments: Invalid parameter -dash_cli=1', @@ -42,13 +43,13 @@ class ConfArgsTest(BitcoinTestFramework): if self.is_wallet_compiled(): with open(inc_conf_file_path, 'w', encoding='utf8') as conf: conf.write("wallet=foo\n") - self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Config setting for -wallet only applied on %s network when in [%s] section.' % (self.chain, self.chain)) + self.nodes[0].assert_start_raises_init_error(expected_msg=f'Error: Config setting for -wallet only applied on {self.chain} network when in [{self.chain}] section.') main_conf_file_path = os.path.join(self.options.tmpdir, 'node0', 'bitcoin_main.conf') - util.write_config(main_conf_file_path, n=0, chain='', extra_config='includeconf={}\n'.format(inc_conf_file_path)) + util.write_config(main_conf_file_path, n=0, chain='', extra_config=f'includeconf={inc_conf_file_path}\n') with open(inc_conf_file_path, 'w', encoding='utf-8') as conf: conf.write('acceptnonstdtxn=1\n') - self.nodes[0].assert_start_raises_init_error(extra_args=["-conf={}".format(main_conf_file_path)], expected_msg='Error: acceptnonstdtxn is not currently supported for main chain') + self.nodes[0].assert_start_raises_init_error(extra_args=[f"-conf={main_conf_file_path}"], expected_msg='Error: acceptnonstdtxn is not currently supported for main chain') with open(inc_conf_file_path, 'w', encoding='utf-8') as conf: conf.write('nono\n') @@ -68,14 +69,14 @@ class ConfArgsTest(BitcoinTestFramework): inc_conf_file2_path = os.path.join(self.nodes[0].datadir, 'include2.conf') with open(os.path.join(self.nodes[0].datadir, 'bitcoin.conf'), 'a', encoding='utf-8') as conf: - conf.write('includeconf={}\n'.format(inc_conf_file2_path)) + conf.write(f'includeconf={inc_conf_file2_path}\n') with open(inc_conf_file_path, 'w', encoding='utf-8') as conf: conf.write('testnot.datadir=1\n') with open(inc_conf_file2_path, 'w', encoding='utf-8') as conf: conf.write('[testnet]\n') self.restart_node(0) - self.nodes[0].stop_node(expected_stderr='Warning: ' + inc_conf_file_path + ':1 Section [testnot] is not recognized.' + os.linesep + inc_conf_file2_path + ':1 Section [testnet] is not recognized.') + self.nodes[0].stop_node(expected_stderr=f'Warning: {inc_conf_file_path}:1 Section [testnot] is not recognized.{os.linesep}{inc_conf_file2_path}:1 Section [testnet] is not recognized.') with open(inc_conf_file_path, 'w', encoding='utf-8') as conf: conf.write('') # clear @@ -104,8 +105,8 @@ class ConfArgsTest(BitcoinTestFramework): 'Command-line arg: rpcpassword=****', 'Command-line arg: rpcuser=****', 'Command-line arg: torpassword=****', - 'Config file arg: %s="1"' % self.chain, - 'Config file arg: [%s] server="1"' % self.chain, + f'Config file arg: {self.chain}="1"', + f'Config file arg: [{self.chain}] server="1"', ], unexpected_msgs=[ 'alice:f7efda5c189b999524f151318c0c86$d5b51b3beffbc0', @@ -158,14 +159,19 @@ class ConfArgsTest(BitcoinTestFramework): self.stop_node(0) # No peers.dat exists and -dnsseed=1 - # We expect the node will use DNS Seeds, but Regtest mode has 0 DNS seeds - # So after 60 seconds, the node should fallback to fixed seeds (this is a slow test) + # We expect the node will use DNS Seeds, but Regtest mode does not have + # any valid DNS seeds. So after 60 seconds, the node should fallback to + # fixed seeds assert not os.path.exists(os.path.join(default_data_dir, "peers.dat")) start = int(time.time()) - with self.nodes[0].assert_debug_log(expected_msgs=[ - "Loaded 0 addresses from peers.dat", - "0 addresses found from DNS seeds", - ]): + with self.nodes[0].assert_debug_log( + expected_msgs=[ + "Loaded 0 addresses from peers.dat", + "0 addresses found from DNS seeds", + "opencon thread start", # Ensure ThreadOpenConnections::start time is properly set + ], + timeout=10, + ): self.start_node(0, extra_args=['-dnsseed=1', '-fixedseeds=1', f'-mocktime={start}']) with self.nodes[0].assert_debug_log(expected_msgs=[ "Adding fixed seeds as 60 seconds have passed and addrman is empty", @@ -203,10 +209,14 @@ class ConfArgsTest(BitcoinTestFramework): # We expect the node will allow 60 seconds prior to using fixed seeds assert not os.path.exists(os.path.join(default_data_dir, "peers.dat")) start = int(time.time()) - with self.nodes[0].assert_debug_log(expected_msgs=[ - "Loaded 0 addresses from peers.dat", - "DNS seeding disabled", - ]): + with self.nodes[0].assert_debug_log( + expected_msgs=[ + "Loaded 0 addresses from peers.dat", + "DNS seeding disabled", + "opencon thread start", # Ensure ThreadOpenConnections::start time is properly set + ], + timeout=10, + ): self.start_node(0, extra_args=['-dnsseed=0', '-fixedseeds=1', '-addnode=fakenodeaddr', f'-mocktime={start}']) with self.nodes[0].assert_debug_log(expected_msgs=[ "Adding fixed seeds as 60 seconds have passed and addrman is empty", @@ -231,7 +241,7 @@ class ConfArgsTest(BitcoinTestFramework): # Check that using -datadir argument on non-existent directory fails self.nodes[0].datadir = new_data_dir - self.nodes[0].assert_start_raises_init_error(['-datadir=' + new_data_dir], 'Error: Specified data directory "' + new_data_dir + '" does not exist.') + self.nodes[0].assert_start_raises_init_error([f'-datadir={new_data_dir}'], f'Error: Specified data directory "{new_data_dir}" does not exist.') # Check that using non-existent datadir in conf file fails conf_file = os.path.join(default_data_dir, "bitcoin.conf") @@ -239,21 +249,25 @@ class ConfArgsTest(BitcoinTestFramework): # datadir needs to be set before [chain] section conf_file_contents = open(conf_file, encoding='utf8').read() with open(conf_file, 'w', encoding='utf8') as f: - f.write("datadir=" + new_data_dir + "\n") + f.write(f"datadir={new_data_dir}\n") f.write(conf_file_contents) - self.nodes[0].assert_start_raises_init_error(['-conf=' + conf_file], 'Error: Error reading configuration file: specified data directory "' + new_data_dir + '" does not exist.') + self.nodes[0].assert_start_raises_init_error([f'-conf={conf_file}'], f'Error: Error reading configuration file: specified data directory "{new_data_dir}" does not exist.') + + # Check that an explicitly specified config file that cannot be opened fails + none_existent_conf_file = os.path.join(default_data_dir, "none_existent_bitcoin.conf") + self.nodes[0].assert_start_raises_init_error(['-conf=' + none_existent_conf_file], 'Error: Error reading configuration file: specified config file "' + none_existent_conf_file + '" could not be opened.') # Create the directory and ensure the config file now works os.mkdir(new_data_dir) - self.start_node(0, ['-conf='+conf_file]) + self.start_node(0, [f'-conf={conf_file}']) self.stop_node(0) assert os.path.exists(os.path.join(new_data_dir, self.chain, 'blocks')) # Ensure command line argument overrides datadir in conf os.mkdir(new_data_dir_2) self.nodes[0].datadir = new_data_dir_2 - self.start_node(0, ['-datadir='+new_data_dir_2, '-conf='+conf_file]) + self.start_node(0, [f'-datadir={new_data_dir_2}', f'-conf={conf_file}']) assert os.path.exists(os.path.join(new_data_dir_2, self.chain, 'blocks')) diff --git a/test/functional/feature_csv_activation.py b/test/functional/feature_csv_activation.py index 46ba18b9b5..6470c1c5eb 100755 --- a/test/functional/feature_csv_activation.py +++ b/test/functional/feature_csv_activation.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test CSV soft fork activation. @@ -9,8 +9,8 @@ BIP 68 - nSequence relative lock times BIP 112 - CHECKSEQUENCEVERIFY BIP 113 - MedianTimePast semantics for nLockTime -mine 82 blocks whose coinbases will be used to generate inputs for our tests -mine 345 blocks and seed block chain with the 82 inputs will use for our tests at height 427 +mine 83 blocks whose coinbases will be used to generate inputs for our tests +mine 344 blocks and seed block chain with the 83 inputs used for our tests at height 427 mine 2 blocks and verify soft fork not yet activated mine 1 block and test that soft fork is activated (rules enforced for next block) Test BIP 113 is enforced @@ -37,13 +37,13 @@ bip112txs_vary_OP_CSV_9 - 16 txs with nSequence = 9 evaluated against varying {r bip112tx_special - test negative argument to OP_CSV bip112tx_emptystack - test empty stack (= no argument) OP_CSV """ -from decimal import Decimal from itertools import product -from io import BytesIO import time -from test_framework.blocktools import create_coinbase, create_block, create_transaction -from test_framework.messages import ToHex, CTransaction +from test_framework.blocktools import ( + create_block, + create_coinbase, +) from test_framework.p2p import P2PDataStore from test_framework.script import ( CScript, @@ -53,19 +53,22 @@ from test_framework.script import ( from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - hex_str_to_bytes, softfork_active, ) +from test_framework.wallet import ( + MiniWallet, + MiniWalletMode, +) TESTING_TX_COUNT = 83 # Number of testing transactions: 1 BIP113 tx, 16 BIP68 txs, 66 BIP112 txs (see comments above) COINBASE_BLOCK_COUNT = TESTING_TX_COUNT # Number of coinbase blocks we need to generate as inputs for our txs BASE_RELATIVE_LOCKTIME = 10 -CSV_ACTIVATION_HEIGHT = 432 SEQ_DISABLE_FLAG = 1 << 31 SEQ_RANDOM_HIGH_BIT = 1 << 25 SEQ_TYPE_FLAG = 1 << 22 SEQ_RANDOM_LOW_BIT = 1 << 18 + def relative_locktime(sdf, srhb, stf, srlb): """Returns a locktime with certain bits set.""" @@ -80,69 +83,13 @@ def relative_locktime(sdf, srhb, stf, srlb): locktime |= SEQ_RANDOM_LOW_BIT return locktime + def all_rlt_txs(txs): return [tx['tx'] for tx in txs] -def sign_transaction(node, unsignedtx): - rawtx = ToHex(unsignedtx) - signresult = node.signrawtransactionwithwallet(rawtx) - tx = CTransaction() - f = BytesIO(hex_str_to_bytes(signresult['hex'])) - tx.deserialize(f) - return tx - -def create_bip112special(node, input, txversion, address): - tx = create_transaction(node, input, address, amount=Decimal("49.98")) - tx.nVersion = txversion - signtx = sign_transaction(node, tx) - signtx.vin[0].scriptSig = CScript([-1, OP_CHECKSEQUENCEVERIFY, OP_DROP] + list(CScript(signtx.vin[0].scriptSig))) - return signtx - -def create_bip112emptystack(node, input, txversion, address): - tx = create_transaction(node, input, address, amount=Decimal("49.98")) - tx.nVersion = txversion - signtx = sign_transaction(node, tx) - signtx.vin[0].scriptSig = CScript([OP_CHECKSEQUENCEVERIFY] + list(CScript(signtx.vin[0].scriptSig))) - return signtx - -def send_generic_input_tx(node, coinbases, address): - return node.sendrawtransaction(ToHex(sign_transaction(node, create_transaction(node, node.getblock(coinbases.pop())['tx'][0], address, amount=Decimal("49.99"))))) - -def create_bip68txs(node, bip68inputs, txversion, address, locktime_delta=0): - """Returns a list of bip68 transactions with different bits set.""" - txs = [] - assert len(bip68inputs) >= 16 - for i, (sdf, srhb, stf, srlb) in enumerate(product(*[[True, False]] * 4)): - locktime = relative_locktime(sdf, srhb, stf, srlb) - tx = create_transaction(node, bip68inputs[i], address, amount=Decimal("49.98")) - tx.nVersion = txversion - tx.vin[0].nSequence = locktime + locktime_delta - tx = sign_transaction(node, tx) - tx.rehash() - txs.append({'tx': tx, 'sdf': sdf, 'stf': stf}) - - return txs - -def create_bip112txs(node, bip112inputs, varyOP_CSV, txversion, address, locktime_delta=0): - """Returns a list of bip68 transactions with different bits set.""" - txs = [] - assert len(bip112inputs) >= 16 - for i, (sdf, srhb, stf, srlb) in enumerate(product(*[[True, False]] * 4)): - locktime = relative_locktime(sdf, srhb, stf, srlb) - tx = create_transaction(node, bip112inputs[i], address, amount=Decimal("49.98")) - if (varyOP_CSV): # if varying OP_CSV, nSequence is fixed - tx.vin[0].nSequence = BASE_RELATIVE_LOCKTIME + locktime_delta - else: # vary nSequence instead, OP_CSV is fixed - tx.vin[0].nSequence = locktime + locktime_delta - tx.nVersion = txversion - signtx = sign_transaction(node, tx) - if (varyOP_CSV): - signtx.vin[0].scriptSig = CScript([locktime, OP_CHECKSEQUENCEVERIFY, OP_DROP] + list(CScript(signtx.vin[0].scriptSig))) - else: - signtx.vin[0].scriptSig = CScript([BASE_RELATIVE_LOCKTIME, OP_CHECKSEQUENCEVERIFY, OP_DROP] + list(CScript(signtx.vin[0].scriptSig))) - tx.rehash() - txs.append({'tx': signtx, 'sdf': sdf, 'stf': stf}) - return txs + +CSV_ACTIVATION_HEIGHT = 432 + class BIP68_112_113Test(BitcoinTestFramework): def set_test_params(self): @@ -150,13 +97,70 @@ class BIP68_112_113Test(BitcoinTestFramework): self.setup_clean_chain = True self.extra_args = [[ '-whitelist=noban@127.0.0.1', - '-addresstype=legacy', + f'-testactivationheight=csv@{CSV_ACTIVATION_HEIGHT}', '-par=1', # Use only one script thread to get the exact reject reason for testing ]] self.supports_cli = False - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() + def create_self_transfer_from_utxo(self, input_tx): + utxo = self.miniwallet.get_utxo(txid=input_tx.rehash(), mark_as_spent=False) + tx = self.miniwallet.create_self_transfer(utxo_to_spend=utxo)['tx'] + return tx + + def create_bip112special(self, input, txversion): + tx = self.create_self_transfer_from_utxo(input) + tx.nVersion = txversion + self.miniwallet.sign_tx(tx) + tx.vin[0].scriptSig = CScript([-1, OP_CHECKSEQUENCEVERIFY, OP_DROP] + list(CScript(tx.vin[0].scriptSig))) + return tx + + def create_bip112emptystack(self, input, txversion): + tx = self.create_self_transfer_from_utxo(input) + tx.nVersion = txversion + self.miniwallet.sign_tx(tx) + tx.vin[0].scriptSig = CScript([OP_CHECKSEQUENCEVERIFY] + list(CScript(tx.vin[0].scriptSig))) + return tx + + def send_generic_input_tx(self, coinbases): + input_txid = self.nodes[0].getblock(coinbases.pop(), 2)['tx'][0]['txid'] + utxo_to_spend = self.miniwallet.get_utxo(txid=input_txid) + return self.miniwallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_to_spend)['tx'] + + def create_bip68txs(self, bip68inputs, txversion, locktime_delta=0): + """Returns a list of bip68 transactions with different bits set.""" + txs = [] + assert len(bip68inputs) >= 16 + for i, (sdf, srhb, stf, srlb) in enumerate(product(*[[True, False]] * 4)): + locktime = relative_locktime(sdf, srhb, stf, srlb) + tx = self.create_self_transfer_from_utxo(bip68inputs[i]) + tx.nVersion = txversion + tx.vin[0].nSequence = locktime + locktime_delta + self.miniwallet.sign_tx(tx) + tx.rehash() + txs.append({'tx': tx, 'sdf': sdf, 'stf': stf}) + + return txs + + def create_bip112txs(self, bip112inputs, varyOP_CSV, txversion, locktime_delta=0): + """Returns a list of bip68 transactions with different bits set.""" + txs = [] + assert len(bip112inputs) >= 16 + for i, (sdf, srhb, stf, srlb) in enumerate(product(*[[True, False]] * 4)): + locktime = relative_locktime(sdf, srhb, stf, srlb) + tx = self.create_self_transfer_from_utxo(bip112inputs[i]) + if varyOP_CSV: # if varying OP_CSV, nSequence is fixed + tx.vin[0].nSequence = BASE_RELATIVE_LOCKTIME + locktime_delta + else: # vary nSequence instead, OP_CSV is fixed + tx.vin[0].nSequence = locktime + locktime_delta + tx.nVersion = txversion + self.miniwallet.sign_tx(tx) + if varyOP_CSV: + tx.vin[0].scriptSig = CScript([locktime, OP_CHECKSEQUENCEVERIFY, OP_DROP] + list(CScript(tx.vin[0].scriptSig))) + else: + tx.vin[0].scriptSig = CScript([BASE_RELATIVE_LOCKTIME, OP_CHECKSEQUENCEVERIFY, OP_DROP] + list(CScript(tx.vin[0].scriptSig))) + tx.rehash() + txs.append({'tx': tx, 'sdf': sdf, 'stf': stf}) + return txs def generate_blocks(self, number): test_blocks = [] @@ -169,11 +173,7 @@ class BIP68_112_113Test(BitcoinTestFramework): return test_blocks def create_test_block(self, txs): - block = create_block(self.tip, create_coinbase(self.tipheight + 1), self.last_block_time + 600) - block.nVersion = 4 - block.vtx.extend(txs) - block.hashMerkleRoot = block.calc_merkle_root() - block.rehash() + block = create_block(self.tip, create_coinbase(self.tipheight + 1), self.last_block_time + 600, txlist=txs) block.solve() return block @@ -185,20 +185,20 @@ class BIP68_112_113Test(BitcoinTestFramework): def run_test(self): self.helper_peer = self.nodes[0].add_p2p_connection(P2PDataStore()) + self.miniwallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK) self.log.info("Generate blocks in the past for coinbase outputs.") long_past_time = int(time.time()) - 600 * 1000 # enough to build up to 1000 blocks 10 minutes apart without worrying about getting into the future self.nodes[0].setmocktime(long_past_time - 100) # enough so that the generated blocks will still all be before long_past_time - self.coinbase_blocks = self.nodes[0].generate(COINBASE_BLOCK_COUNT) # blocks generated for inputs + self.coinbase_blocks = self.generate(self.miniwallet, COINBASE_BLOCK_COUNT) # blocks generated for inputs self.nodes[0].setmocktime(0) # set time back to present so yielded blocks aren't in the future as we advance last_block_time self.tipheight = COINBASE_BLOCK_COUNT # height of the next block to build self.last_block_time = long_past_time self.tip = int(self.nodes[0].getbestblockhash(), 16) - self.nodeaddress = self.nodes[0].getnewaddress() # Activation height is hardcoded # We advance to block height five below BIP112 activation for the following tests - test_blocks = self.generate_blocks(CSV_ACTIVATION_HEIGHT-5 - COINBASE_BLOCK_COUNT) + test_blocks = self.generate_blocks(CSV_ACTIVATION_HEIGHT - 5 - COINBASE_BLOCK_COUNT) self.send_blocks(test_blocks) assert not softfork_active(self.nodes[0], 'csv') @@ -209,14 +209,14 @@ class BIP68_112_113Test(BitcoinTestFramework): # 16 normal inputs bip68inputs = [] for _ in range(16): - bip68inputs.append(send_generic_input_tx(self.nodes[0], self.coinbase_blocks, self.nodeaddress)) + bip68inputs.append(self.send_generic_input_tx(self.coinbase_blocks)) # 2 sets of 16 inputs with 10 OP_CSV OP_DROP (actually will be prepended to spending scriptSig) bip112basicinputs = [] for _ in range(2): inputs = [] for _ in range(16): - inputs.append(send_generic_input_tx(self.nodes[0], self.coinbase_blocks, self.nodeaddress)) + inputs.append(self.send_generic_input_tx(self.coinbase_blocks)) bip112basicinputs.append(inputs) # 2 sets of 16 varied inputs with (relative_lock_time) OP_CSV OP_DROP (actually will be prepended to spending scriptSig) @@ -224,19 +224,19 @@ class BIP68_112_113Test(BitcoinTestFramework): for _ in range(2): inputs = [] for _ in range(16): - inputs.append(send_generic_input_tx(self.nodes[0], self.coinbase_blocks, self.nodeaddress)) + inputs.append(self.send_generic_input_tx(self.coinbase_blocks)) bip112diverseinputs.append(inputs) # 1 special input with -1 OP_CSV OP_DROP (actually will be prepended to spending scriptSig) - bip112specialinput = send_generic_input_tx(self.nodes[0], self.coinbase_blocks, self.nodeaddress) + bip112specialinput = self.send_generic_input_tx(self.coinbase_blocks) # 1 special input with (empty stack) OP_CSV (actually will be prepended to spending scriptSig) - bip112emptystackinput = send_generic_input_tx(self.nodes[0],self.coinbase_blocks, self.nodeaddress) + bip112emptystackinput = self.send_generic_input_tx(self.coinbase_blocks) # 1 normal input - bip113input = send_generic_input_tx(self.nodes[0], self.coinbase_blocks, self.nodeaddress) + bip113input = self.send_generic_input_tx(self.coinbase_blocks) self.nodes[0].setmocktime(self.last_block_time + 600) - inputblockhash = self.nodes[0].generate(1)[0] # 1 block generated for inputs to be in chain at height 431 + inputblockhash = self.generate(self.nodes[0], 1)[0] # 1 block generated for inputs to be in chain at height 431 self.nodes[0].setmocktime(0) self.tip = int(inputblockhash, 16) self.tipheight += 1 @@ -248,41 +248,41 @@ class BIP68_112_113Test(BitcoinTestFramework): self.send_blocks(test_blocks) assert_equal(self.tipheight, CSV_ACTIVATION_HEIGHT - 2) - self.log.info("Height = {}, CSV not yet active (will activate for block {}, not {})".format(self.tipheight, CSV_ACTIVATION_HEIGHT, CSV_ACTIVATION_HEIGHT - 1)) + self.log.info(f"Height = {self.tipheight}, CSV not yet active (will activate for block {CSV_ACTIVATION_HEIGHT}, not {CSV_ACTIVATION_HEIGHT - 1})") assert not softfork_active(self.nodes[0], 'csv') # Test both version 1 and version 2 transactions for all tests # BIP113 test transaction will be modified before each use to put in appropriate block time - bip113tx_v1 = create_transaction(self.nodes[0], bip113input, self.nodeaddress, amount=Decimal("49.98")) + bip113tx_v1 = self.create_self_transfer_from_utxo(bip113input) bip113tx_v1.vin[0].nSequence = 0xFFFFFFFE bip113tx_v1.nVersion = 1 - bip113tx_v2 = create_transaction(self.nodes[0], bip113input, self.nodeaddress, amount=Decimal("49.98")) + bip113tx_v2 = self.create_self_transfer_from_utxo(bip113input) bip113tx_v2.vin[0].nSequence = 0xFFFFFFFE bip113tx_v2.nVersion = 2 # For BIP68 test all 16 relative sequence locktimes - bip68txs_v1 = create_bip68txs(self.nodes[0], bip68inputs, 1, self.nodeaddress) - bip68txs_v2 = create_bip68txs(self.nodes[0], bip68inputs, 2, self.nodeaddress) + bip68txs_v1 = self.create_bip68txs(bip68inputs, 1) + bip68txs_v2 = self.create_bip68txs(bip68inputs, 2) # For BIP112 test: # 16 relative sequence locktimes of 10 against 10 OP_CSV OP_DROP inputs - bip112txs_vary_nSequence_v1 = create_bip112txs(self.nodes[0], bip112basicinputs[0], False, 1, self.nodeaddress) - bip112txs_vary_nSequence_v2 = create_bip112txs(self.nodes[0], bip112basicinputs[0], False, 2, self.nodeaddress) + bip112txs_vary_nSequence_v1 = self.create_bip112txs(bip112basicinputs[0], False, 1) + bip112txs_vary_nSequence_v2 = self.create_bip112txs(bip112basicinputs[0], False, 2) # 16 relative sequence locktimes of 9 against 10 OP_CSV OP_DROP inputs - bip112txs_vary_nSequence_9_v1 = create_bip112txs(self.nodes[0], bip112basicinputs[1], False, 1, self.nodeaddress, -1) - bip112txs_vary_nSequence_9_v2 = create_bip112txs(self.nodes[0], bip112basicinputs[1], False, 2, self.nodeaddress, -1) + bip112txs_vary_nSequence_9_v1 = self.create_bip112txs(bip112basicinputs[1], False, 1, -1) + bip112txs_vary_nSequence_9_v2 = self.create_bip112txs(bip112basicinputs[1], False, 2, -1) # sequence lock time of 10 against 16 (relative_lock_time) OP_CSV OP_DROP inputs - bip112txs_vary_OP_CSV_v1 = create_bip112txs(self.nodes[0], bip112diverseinputs[0], True, 1, self.nodeaddress) - bip112txs_vary_OP_CSV_v2 = create_bip112txs(self.nodes[0], bip112diverseinputs[0], True, 2, self.nodeaddress) + bip112txs_vary_OP_CSV_v1 = self.create_bip112txs(bip112diverseinputs[0], True, 1) + bip112txs_vary_OP_CSV_v2 = self.create_bip112txs(bip112diverseinputs[0], True, 2) # sequence lock time of 9 against 16 (relative_lock_time) OP_CSV OP_DROP inputs - bip112txs_vary_OP_CSV_9_v1 = create_bip112txs(self.nodes[0], bip112diverseinputs[1], True, 1, self.nodeaddress, -1) - bip112txs_vary_OP_CSV_9_v2 = create_bip112txs(self.nodes[0], bip112diverseinputs[1], True, 2, self.nodeaddress, -1) + bip112txs_vary_OP_CSV_9_v1 = self.create_bip112txs(bip112diverseinputs[1], True, 1, -1) + bip112txs_vary_OP_CSV_9_v2 = self.create_bip112txs(bip112diverseinputs[1], True, 2, -1) # -1 OP_CSV OP_DROP input - bip112tx_special_v1 = create_bip112special(self.nodes[0], bip112specialinput, 1, self.nodeaddress) - bip112tx_special_v2 = create_bip112special(self.nodes[0], bip112specialinput, 2, self.nodeaddress) + bip112tx_special_v1 = self.create_bip112special(bip112specialinput, 1) + bip112tx_special_v2 = self.create_bip112special(bip112specialinput, 2) # (empty stack) OP_CSV input - bip112tx_emptystack_v1 = create_bip112emptystack(self.nodes[0], bip112emptystackinput, 1, self.nodeaddress) - bip112tx_emptystack_v2 = create_bip112emptystack(self.nodes[0], bip112emptystackinput, 2, self.nodeaddress) + bip112tx_emptystack_v1 = self.create_bip112emptystack(bip112emptystackinput, 1) + bip112tx_emptystack_v2 = self.create_bip112emptystack(bip112emptystackinput, 2) self.log.info("TESTING") @@ -292,8 +292,8 @@ class BIP68_112_113Test(BitcoinTestFramework): success_txs = [] # BIP113 tx, -1 CSV tx and empty stack CSV tx should succeed bip113tx_v1.nLockTime = self.last_block_time - 600 * 5 # = MTP of prior block (not <) but < time put on current block - bip113signed1 = sign_transaction(self.nodes[0], bip113tx_v1) - success_txs.append(bip113signed1) + self.miniwallet.sign_tx(bip113tx_v1) + success_txs.append(bip113tx_v1) success_txs.append(bip112tx_special_v1) success_txs.append(bip112tx_emptystack_v1) # add BIP 68 txs @@ -312,8 +312,8 @@ class BIP68_112_113Test(BitcoinTestFramework): success_txs = [] # BIP113 tx, -1 CSV tx and empty stack CSV tx should succeed bip113tx_v2.nLockTime = self.last_block_time - 600 * 5 # = MTP of prior block (not <) but < time put on current block - bip113signed2 = sign_transaction(self.nodes[0], bip113tx_v2) - success_txs.append(bip113signed2) + self.miniwallet.sign_tx(bip113tx_v2) + success_txs.append(bip113tx_v2) success_txs.append(bip112tx_special_v2) success_txs.append(bip112tx_emptystack_v2) # add BIP 68 txs @@ -338,17 +338,22 @@ class BIP68_112_113Test(BitcoinTestFramework): self.log.info("BIP 113 tests") # BIP 113 tests should now fail regardless of version number if nLockTime isn't satisfied by new rules bip113tx_v1.nLockTime = self.last_block_time - 600 * 5 # = MTP of prior block (not <) but < time put on current block - bip113signed1 = sign_transaction(self.nodes[0], bip113tx_v1) + self.miniwallet.sign_tx(bip113tx_v1) + bip113tx_v1.rehash() bip113tx_v2.nLockTime = self.last_block_time - 600 * 5 # = MTP of prior block (not <) but < time put on current block - bip113signed2 = sign_transaction(self.nodes[0], bip113tx_v2) - for bip113tx in [bip113signed1, bip113signed2]: + self.miniwallet.sign_tx(bip113tx_v2) + bip113tx_v2.rehash() + for bip113tx in [bip113tx_v1, bip113tx_v2]: self.send_blocks([self.create_test_block([bip113tx])], success=False, reject_reason='bad-txns-nonfinal') + # BIP 113 tests should now pass if the locktime is < MTP bip113tx_v1.nLockTime = self.last_block_time - 600 * 5 - 1 # < MTP of prior block - bip113signed1 = sign_transaction(self.nodes[0], bip113tx_v1) + self.miniwallet.sign_tx(bip113tx_v1) + bip113tx_v1.rehash() bip113tx_v2.nLockTime = self.last_block_time - 600 * 5 - 1 # < MTP of prior block - bip113signed2 = sign_transaction(self.nodes[0], bip113tx_v2) - for bip113tx in [bip113signed1, bip113signed2]: + self.miniwallet.sign_tx(bip113tx_v2) + bip113tx_v2.rehash() + for bip113tx in [bip113tx_v1, bip113tx_v2]: self.send_blocks([self.create_test_block([bip113tx])]) self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) @@ -471,11 +476,13 @@ class BIP68_112_113Test(BitcoinTestFramework): time_txs = [] for tx in [tx['tx'] for tx in bip112txs_vary_OP_CSV_v2 if not tx['sdf'] and tx['stf']]: tx.vin[0].nSequence = BASE_RELATIVE_LOCKTIME | SEQ_TYPE_FLAG - signtx = sign_transaction(self.nodes[0], tx) - time_txs.append(signtx) + self.miniwallet.sign_tx(tx) + tx.rehash() + time_txs.append(tx) self.send_blocks([self.create_test_block(time_txs)]) self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) + if __name__ == '__main__': BIP68_112_113Test().main() diff --git a/test/functional/feature_dbcrash.py b/test/functional/feature_dbcrash.py index 2b56bc78f5..3e60efbb3c 100755 --- a/test/functional/feature_dbcrash.py +++ b/test/functional/feature_dbcrash.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test recovery from a crash during chainstate writing. @@ -36,13 +36,11 @@ from test_framework.messages import ( CTransaction, CTxIn, CTxOut, - ToHex, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, create_confirmed_utxos, - hex_str_to_bytes, ) @@ -104,7 +102,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # perhaps we generated a test case that blew up our cache? # TODO: If this happens a lot, we should try to restart without -dbcrashratio # and make sure that recovery happens. - raise AssertionError("Unable to successfully restart node %d in allotted time", node_index) + raise AssertionError(f"Unable to successfully restart node {node_index} in allotted time") def submit_block_catch_error(self, node_index, block): """Try submitting a block to the given node. @@ -116,10 +114,10 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): self.nodes[node_index].submitblock(block) return True except (http.client.CannotSendRequest, http.client.RemoteDisconnected) as e: - self.log.debug("node %d submitblock raised exception: %s", node_index, e) + self.log.debug(f"node {node_index} submitblock raised exception: {e}") return False except OSError as e: - self.log.debug("node %d submitblock raised OSError exception: errno=%s", node_index, e.errno) + self.log.debug(f"node {node_index} submitblock raised OSError exception: errno={e.errno}") if e.errno in [errno.EPIPE, errno.ECONNREFUSED, errno.ECONNRESET]: # The node has likely crashed return False @@ -144,15 +142,15 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Deliver each block to each other node for i in range(3): nodei_utxo_hash = None - self.log.debug("Syncing blocks to node %d", i) + self.log.debug(f"Syncing blocks to node {i}") for (block_hash, block) in blocks: # Get the block from node3, and submit to node_i - self.log.debug("submitting block %s", block_hash) + self.log.debug(f"submitting block {block_hash}") if not self.submit_block_catch_error(i, block): # TODO: more carefully check that the crash is due to -dbcrashratio # (change the exit code perhaps, and check that here?) self.wait_for_node_exit(i, timeout=30) - self.log.debug("Restarting node %d after block hash %s", i, block_hash) + self.log.debug(f"Restarting node {i} after block hash {block_hash}") nodei_utxo_hash = self.restart_node(i, block_hash) assert nodei_utxo_hash is not None self.restart_counts[i] += 1 @@ -169,7 +167,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # - we only update the utxo cache after a node restart, since flushing # the cache is a no-op at that point if nodei_utxo_hash is not None: - self.log.debug("Checking txoutsetinfo matches for node %d", i) + self.log.debug(f"Checking txoutsetinfo matches for node {i}") assert_equal(nodei_utxo_hash, node3_utxo_hash) def verify_utxo_hash(self): @@ -205,10 +203,10 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): continue for _ in range(3): - tx.vout.append(CTxOut(output_amount, hex_str_to_bytes(utxo['scriptPubKey']))) + tx.vout.append(CTxOut(output_amount, bytes.fromhex(utxo['scriptPubKey']))) # Sign and send the transaction to get into the mempool - tx_signed_hex = node.signrawtransactionwithwallet(ToHex(tx))['hex'] + tx_signed_hex = node.signrawtransactionwithwallet(tx.serialize().hex())['hex'] node.sendrawtransaction(tx_signed_hex) num_transactions += 1 @@ -219,15 +217,15 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Start by creating a lot of utxos on node3 initial_height = self.nodes[3].getblockcount() - utxo_list = create_confirmed_utxos(self.nodes[3].getnetworkinfo()['relayfee'], self.nodes[3], 5000) - self.log.info("Prepped %d utxo entries", len(utxo_list)) + utxo_list = create_confirmed_utxos(self, self.nodes[3].getnetworkinfo()['relayfee'], self.nodes[3], 5000, sync_fun=self.no_op) + self.log.info(f"Prepped {len(utxo_list)} utxo entries") # Sync these blocks with the other nodes block_hashes_to_sync = [] for height in range(initial_height + 1, self.nodes[3].getblockcount() + 1): block_hashes_to_sync.append(self.nodes[3].getblockhash(height)) - self.log.debug("Syncing %d blocks with other nodes", len(block_hashes_to_sync)) + self.log.debug(f"Syncing {len(block_hashes_to_sync)} blocks with other nodes") # Syncing the blocks could cause nodes to crash, so the test begins here. self.sync_node3blocks(block_hashes_to_sync) @@ -237,33 +235,35 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # each time through the loop, generate a bunch of transactions, # and then either mine a single new block on the tip, or some-sized reorg. for i in range(40): - self.log.info("Iteration %d, generating 2500 transactions %s", i, self.restart_counts) + self.log.info(f"Iteration {i}, generating 2500 transactions {self.restart_counts}") # Generate a bunch of small-ish transactions self.generate_small_transactions(self.nodes[3], 2500, utxo_list) # Pick a random block between current tip, and starting tip current_height = self.nodes[3].getblockcount() random_height = random.randint(starting_tip_height, current_height) - self.log.debug("At height %d, considering height %d", current_height, random_height) + self.log.debug(f"At height {current_height}, considering height {random_height}") if random_height > starting_tip_height: # Randomly reorg from this point with some probability (1/4 for # tip, 1/5 for tip-1, ...) if random.random() < 1.0 / (current_height + 4 - random_height): - self.log.debug("Invalidating block at height %d", random_height) + self.log.debug(f"Invalidating block at height {random_height}") self.nodes[3].invalidateblock(self.nodes[3].getblockhash(random_height)) # Now generate new blocks until we pass the old tip height self.log.debug("Mining longer tip") block_hashes = [] while current_height + 1 > self.nodes[3].getblockcount(): - block_hashes.extend(self.nodes[3].generatetoaddress( + block_hashes.extend(self.generatetoaddress( + self.nodes[3], nblocks=min(10, current_height + 1 - self.nodes[3].getblockcount()), # new address to avoid mining a block that has just been invalidated address=self.nodes[3].getnewaddress(), + sync_fun=self.no_op, )) - self.log.debug("Syncing %d new blocks...", len(block_hashes)) + self.log.debug(f"Syncing {len(block_hashes)} new blocks...") self.sync_node3blocks(block_hashes) utxo_list = self.nodes[3].listunspent() - self.log.debug("Node3 utxo count: %d", len(utxo_list)) + self.log.debug(f"Node3 utxo count: {len(utxo_list)}") # Check that the utxo hashes agree with node3 # Useful side effect: each utxo cache gets flushed here, so that we @@ -271,7 +271,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): self.verify_utxo_hash() # Check the test coverage - self.log.info("Restarted nodes: %s; crashes on restart: %d", self.restart_counts, self.crashed_on_restart) + self.log.info(f"Restarted nodes: {self.restart_counts}; crashes on restart: {self.crashed_on_restart}") # If no nodes were restarted, we didn't test anything. assert self.restart_counts != [0, 0, 0] @@ -282,7 +282,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Warn if any of the nodes escaped restart. for i in range(3): if self.restart_counts[i] == 0: - self.log.warning("Node %d never crashed during utxo flush!", i) + self.log.warning(f"Node {i} never crashed during utxo flush!") if __name__ == "__main__": diff --git a/test/functional/feature_dersig.py b/test/functional/feature_dersig.py index 3b430139b1..9a46839969 100755 --- a/test/functional/feature_dersig.py +++ b/test/functional/feature_dersig.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test BIP66 (DER SIG). -Test that the DERSIG soft-fork activates at (regtest) height 1251. +Test the DERSIG soft-fork activation on regtest. """ -from test_framework.blocktools import create_coinbase, create_block, create_transaction +from test_framework.blocktools import ( + create_block, + create_coinbase, +) from test_framework.messages import msg_block from test_framework.p2p import P2PInterface from test_framework.script import CScript @@ -15,8 +18,10 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, ) - -DERSIG_HEIGHT = 1251 +from test_framework.wallet import ( + MiniWallet, + MiniWalletMode, +) # A canonical signature consists of: @@ -36,21 +41,26 @@ def unDERify(tx): tx.vin[0].scriptSig = CScript(newscript) +DERSIG_HEIGHT = 102 + + class BIP66Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [[ + f'-testactivationheight=dersig@{DERSIG_HEIGHT}', '-whitelist=noban@127.0.0.1', '-par=1', # Use only one script thread to get the exact log msg for testing ]] self.setup_clean_chain = True self.rpc_timeout = 240 - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() + def create_tx(self, input_txid): + utxo_to_spend = self.miniwallet.get_utxo(txid=input_txid, mark_as_spent=False) + return self.miniwallet.create_self_transfer(utxo_to_spend=utxo_to_spend)['tx'] def test_dersig_info(self, *, is_active): - assert_equal(self.nodes[0].getblockchaininfo()['softforks']['bip66'], + assert_equal(self.nodes[0].getdeploymentinfo()['deployments']['bip66'], { "active": is_active, "height": DERSIG_HEIGHT, @@ -60,52 +70,46 @@ class BIP66Test(BitcoinTestFramework): def run_test(self): peer = self.nodes[0].add_p2p_connection(P2PInterface()) + self.miniwallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK) self.test_dersig_info(is_active=False) self.log.info("Mining %d blocks", DERSIG_HEIGHT - 2) - self.coinbase_txids = [self.nodes[0].getblock(b)['tx'][0] for b in self.nodes[0].generate(DERSIG_HEIGHT - 2)] - self.nodeaddress = self.nodes[0].getnewaddress() + self.coinbase_txids = [self.nodes[0].getblock(b)['tx'][0] for b in self.generate(self.miniwallet, DERSIG_HEIGHT - 2)] self.log.info("Test that a transaction with non-DER signature can still appear in a block") - spendtx = create_transaction(self.nodes[0], self.coinbase_txids[0], - self.nodeaddress, amount=1.0) + spendtx = self.create_tx(self.coinbase_txids[0]) unDERify(spendtx) spendtx.rehash() tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblockheader(tip)['mediantime'] + 1 - block = create_block(int(tip, 16), create_coinbase(DERSIG_HEIGHT - 1), block_time) - block.nVersion = 2 - block.vtx.append(spendtx) - block.hashMerkleRoot = block.calc_merkle_root() - block.rehash() + block = create_block(int(tip, 16), create_coinbase(DERSIG_HEIGHT - 1), block_time, txlist=[spendtx]) block.solve() + assert_equal(self.nodes[0].getblockcount(), DERSIG_HEIGHT - 2) self.test_dersig_info(is_active=False) # Not active as of current tip and next block does not need to obey rules peer.send_and_ping(msg_block(block)) + assert_equal(self.nodes[0].getblockcount(), DERSIG_HEIGHT - 1) self.test_dersig_info(is_active=True) # Not active as of current tip, but next block must obey rules assert_equal(self.nodes[0].getbestblockhash(), block.hash) self.log.info("Test that blocks must now be at least version 3") tip = block.sha256 block_time += 1 - block = create_block(tip, create_coinbase(DERSIG_HEIGHT), block_time) - block.nVersion = 2 - block.rehash() + block = create_block(tip, create_coinbase(DERSIG_HEIGHT), block_time, version=2) block.solve() - with self.nodes[0].assert_debug_log(expected_msgs=['{}, bad-version(0x00000002)'.format(block.hash)]): + with self.nodes[0].assert_debug_log(expected_msgs=[f'{block.hash}, bad-version(0x00000002)']): peer.send_and_ping(msg_block(block)) assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) peer.sync_with_ping() self.log.info("Test that transactions with non-DER signatures cannot appear in a block") - block.nVersion = 3 + block.nVersion = 4 - spendtx = create_transaction(self.nodes[0], self.coinbase_txids[1], - self.nodeaddress, amount=1.0) + spendtx = self.create_tx(self.coinbase_txids[1]) unDERify(spendtx) spendtx.rehash() @@ -124,18 +128,16 @@ class BIP66Test(BitcoinTestFramework): # Now we verify that a block with this transaction is also invalid. block.vtx.append(spendtx) block.hashMerkleRoot = block.calc_merkle_root() - block.rehash() block.solve() - with self.nodes[0].assert_debug_log(expected_msgs=['CheckInputScripts on {} failed with non-mandatory-script-verify-flag (Non-canonical DER signature)'.format(block.vtx[-1].hash)]): + with self.nodes[0].assert_debug_log(expected_msgs=[f'CheckInputScripts on {block.vtx[-1].hash} failed with non-mandatory-script-verify-flag (Non-canonical DER signature)']): peer.send_and_ping(msg_block(block)) assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) peer.sync_with_ping() - self.log.info("Test that a version 3 block with a DERSIG-compliant transaction is accepted") - block.vtx[1] = create_transaction(self.nodes[0], self.coinbase_txids[1], self.nodeaddress, amount=1.0) + self.log.info("Test that a block with a DERSIG-compliant transaction is accepted") + block.vtx[1] = self.create_tx(self.coinbase_txids[1]) block.hashMerkleRoot = block.calc_merkle_root() - block.rehash() block.solve() self.test_dersig_info(is_active=True) # Not active as of current tip, but next block must obey rules diff --git a/test/functional/feature_dirsymlinks.py b/test/functional/feature_dirsymlinks.py new file mode 100755 index 0000000000..85c8e27600 --- /dev/null +++ b/test/functional/feature_dirsymlinks.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test successful startup with symlinked directories. +""" + +import os +import sys + +from test_framework.test_framework import BitcoinTestFramework, SkipTest + + +def rename_and_link(*, from_name, to_name): + os.rename(from_name, to_name) + os.symlink(to_name, from_name) + assert os.path.islink(from_name) and os.path.isdir(from_name) + +class SymlinkTest(BitcoinTestFramework): + def skip_test_if_missing_module(self): + if sys.platform == 'win32': + raise SkipTest("Symlinks test skipped on Windows") + + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + self.stop_node(0) + + rename_and_link(from_name=os.path.join(self.nodes[0].datadir, self.chain, "blocks"), + to_name=os.path.join(self.nodes[0].datadir, self.chain, "newblocks")) + rename_and_link(from_name=os.path.join(self.nodes[0].datadir, self.chain, "chainstate"), + to_name=os.path.join(self.nodes[0].datadir, self.chain, "newchainstate")) + + self.start_node(0) + + +if __name__ == '__main__': + SymlinkTest().main() diff --git a/test/functional/feature_fee_estimation.py b/test/functional/feature_fee_estimation.py index 8f522aee66..233ffd60da 100755 --- a/test/functional/feature_fee_estimation.py +++ b/test/functional/feature_fee_estimation.py @@ -1,13 +1,28 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test fee estimation code.""" from decimal import Decimal +import os import random -from test_framework.messages import CTransaction, CTxIn, CTxOut, COutPoint, ToHex, COIN -from test_framework.script import CScript, OP_1, OP_DROP, OP_2, OP_HASH160, OP_EQUAL, hash160, OP_TRUE +from test_framework.messages import ( + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxOut, +) +from test_framework.script import ( + CScript, + OP_1, + OP_DROP, + OP_TRUE, +) +from test_framework.script_util import ( + script_to_p2sh_script, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -20,16 +35,14 @@ from test_framework.util import ( # Construct 2 trivial P2SH's and the ScriptSigs that spend them # So we can create many transactions without needing to spend # time signing. -REDEEM_SCRIPT_1 = CScript([OP_1, OP_DROP]) -REDEEM_SCRIPT_2 = CScript([OP_2, OP_DROP]) -P2SH_1 = CScript([OP_HASH160, hash160(REDEEM_SCRIPT_1), OP_EQUAL]) -P2SH_2 = CScript([OP_HASH160, hash160(REDEEM_SCRIPT_2), OP_EQUAL]) - -# Associated ScriptSig's to spend satisfy P2SH_1 and P2SH_2 -SCRIPT_SIG = [CScript([OP_TRUE, REDEEM_SCRIPT_1]), CScript([OP_TRUE, REDEEM_SCRIPT_2])] +SCRIPT = CScript([OP_1, OP_DROP]) +P2SH = script_to_p2sh_script(SCRIPT) +REDEEM_SCRIPT = CScript([OP_TRUE, SCRIPT]) -def small_txpuzzle_randfee(from_node, conflist, unconflist, amount, min_fee, fee_increment): +def small_txpuzzle_randfee( + from_node, conflist, unconflist, amount, min_fee, fee_increment +): """Create and send a transaction with a random fee. The transaction pays to a trivial P2SH script, and assumes that its inputs @@ -50,54 +63,21 @@ def small_txpuzzle_randfee(from_node, conflist, unconflist, amount, min_fee, fee while total_in <= (amount + fee) and len(conflist) > 0: t = conflist.pop(0) total_in += t["amount"] - tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), b"")) + tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), REDEEM_SCRIPT)) + while total_in <= (amount + fee) and len(unconflist) > 0: + t = unconflist.pop(0) + total_in += t["amount"] + tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), REDEEM_SCRIPT)) if total_in <= amount + fee: - while total_in <= (amount + fee) and len(unconflist) > 0: - t = unconflist.pop(0) - total_in += t["amount"] - tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), b"")) - if total_in <= amount + fee: - raise RuntimeError("Insufficient funds: need %d, have %d" % (amount + fee, total_in)) - tx.vout.append(CTxOut(int((total_in - amount - fee) * COIN), P2SH_1)) - tx.vout.append(CTxOut(int(amount * COIN), P2SH_2)) - # These transactions don't need to be signed, but we still have to insert - # the ScriptSig that will satisfy the ScriptPubKey. - for inp in tx.vin: - inp.scriptSig = SCRIPT_SIG[inp.prevout.n] - txid = from_node.sendrawtransaction(hexstring=ToHex(tx), maxfeerate=0) + raise RuntimeError(f"Insufficient funds: need {amount + fee}, have {total_in}") + tx.vout.append(CTxOut(int((total_in - amount - fee) * COIN), P2SH)) + tx.vout.append(CTxOut(int(amount * COIN), P2SH)) + txid = from_node.sendrawtransaction(hexstring=tx.serialize().hex(), maxfeerate=0) unconflist.append({"txid": txid, "vout": 0, "amount": total_in - amount - fee}) unconflist.append({"txid": txid, "vout": 1, "amount": amount}) - return (ToHex(tx), fee) - - -def split_inputs(from_node, txins, txouts, initial_split=False): - """Generate a lot of inputs so we can generate a ton of transactions. - - This function takes an input from txins, and creates and sends a transaction - which splits the value into 2 outputs which are appended to txouts. - Previously this was designed to be small inputs so they wouldn't have - a high coin age when the notion of priority still existed.""" + return (tx.serialize().hex(), fee) - prevtxout = txins.pop() - tx = CTransaction() - tx.vin.append(CTxIn(COutPoint(int(prevtxout["txid"], 16), prevtxout["vout"]), b"")) - - half_change = satoshi_round(prevtxout["amount"] / 2) - rem_change = prevtxout["amount"] - half_change - Decimal("0.00001000") - tx.vout.append(CTxOut(int(half_change * COIN), P2SH_1)) - tx.vout.append(CTxOut(int(rem_change * COIN), P2SH_2)) - - # If this is the initial split we actually need to sign the transaction - # Otherwise we just need to insert the proper ScriptSig - if (initial_split): - completetx = from_node.signrawtransactionwithwallet(ToHex(tx))["hex"] - else: - tx.vin[0].scriptSig = SCRIPT_SIG[prevtxout["vout"]] - completetx = ToHex(tx) - txid = from_node.sendrawtransaction(hexstring=completetx, maxfeerate=0) - txouts.append({"txid": txid, "vout": 0, "amount": half_change}) - txouts.append({"txid": txid, "vout": 1, "amount": rem_change}) def check_raw_estimates(node, fees_seen): """Call estimaterawfee and verify that the estimates meet certain invariants.""" @@ -109,8 +89,10 @@ def check_raw_estimates(node, fees_seen): assert_greater_than(feerate, 0) if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen): - raise AssertionError("Estimated fee (%f) out of range (%f,%f)" - % (feerate, min(fees_seen), max(fees_seen))) + raise AssertionError( + f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})" + ) + def check_smart_estimates(node, fees_seen): """Call estimatesmartfee and verify that the estimates meet certain invariants.""" @@ -118,16 +100,22 @@ def check_smart_estimates(node, fees_seen): delta = 1.0e-6 # account for rounding error last_feerate = float(max(fees_seen)) all_smart_estimates = [node.estimatesmartfee(i) for i in range(1, 26)] + mempoolMinFee = node.getmempoolinfo()["mempoolminfee"] + minRelaytxFee = node.getmempoolinfo()["minrelaytxfee"] for i, e in enumerate(all_smart_estimates): # estimate is for i+1 feerate = float(e["feerate"]) assert_greater_than(feerate, 0) + assert_greater_than_or_equal(feerate, float(mempoolMinFee)) + assert_greater_than_or_equal(feerate, float(minRelaytxFee)) if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen): - raise AssertionError("Estimated fee (%f) out of range (%f,%f)" - % (feerate, min(fees_seen), max(fees_seen))) + raise AssertionError( + f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})" + ) if feerate - delta > last_feerate: - raise AssertionError("Estimated fee (%f) larger than last fee (%f) for lower number of confirms" - % (feerate, last_feerate)) + raise AssertionError( + f"Estimated fee ({feerate}) larger than last fee ({last_feerate}) for lower number of confirms" + ) last_feerate = feerate if i == 0: @@ -135,19 +123,33 @@ def check_smart_estimates(node, fees_seen): else: assert_greater_than_or_equal(i + 1, e["blocks"]) + def check_estimates(node, fees_seen): check_raw_estimates(node, fees_seen) check_smart_estimates(node, fees_seen) + +def send_tx(node, utxo, feerate): + """Broadcast a 1in-1out transaction with a specific input and feerate (sat/vb).""" + tx = CTransaction() + tx.vin = [CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]), REDEEM_SCRIPT)] + tx.vout = [CTxOut(int(utxo["amount"] * COIN), P2SH)] + + # vbytes == bytes as we are using legacy transactions + fee = tx.get_vsize() * feerate + tx.vout[0].nValue -= fee + + return node.sendrawtransaction(tx.serialize().hex()) + + class EstimateFeeTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 - # mine non-standard txs (e.g. txs with "dust" outputs) # Force fSendTrickle to true (via whitelist.noban) self.extra_args = [ - ["-acceptnonstdtxn", "-whitelist=noban@127.0.0.1"], - ["-acceptnonstdtxn", "-whitelist=noban@127.0.0.1", "-blockmaxweight=68000"], - ["-acceptnonstdtxn", "-whitelist=noban@127.0.0.1", "-blockmaxweight=32000"], + ["-whitelist=noban@127.0.0.1"], + ["-whitelist=noban@127.0.0.1", "-blockmaxweight=68000"], + ["-whitelist=noban@127.0.0.1", "-blockmaxweight=32000"], ] def skip_test_if_missing_module(self): @@ -180,13 +182,18 @@ class EstimateFeeTest(BitcoinTestFramework): random.shuffle(self.confutxo) for _ in range(random.randrange(100 - 50, 100 + 50)): from_index = random.randint(1, 2) - (txhex, fee) = small_txpuzzle_randfee(self.nodes[from_index], self.confutxo, - self.memutxo, Decimal("0.005"), min_fee, min_fee) + (txhex, fee) = small_txpuzzle_randfee( + self.nodes[from_index], + self.confutxo, + self.memutxo, + Decimal("0.005"), + min_fee, + min_fee, + ) tx_kbytes = (len(txhex) // 2) / 1000.0 self.fees_per_kb.append(float(fee) / tx_kbytes) - self.sync_mempools(wait=.1) - mined = mining_node.getblock(mining_node.generate(1)[0], True)["tx"] - self.sync_blocks(wait=.1) + self.sync_mempools(wait=0.1) + mined = mining_node.getblock(self.generate(mining_node, 1)[0], True)["tx"] # update which txouts are confirmed newmem = [] for utx in self.memutxo: @@ -196,60 +203,47 @@ class EstimateFeeTest(BitcoinTestFramework): newmem.append(utx) self.memutxo = newmem - def run_test(self): - self.log.info("This test is time consuming, please be patient") - self.log.info("Splitting inputs so we can generate tx's") - - # Start node0 - self.start_node(0) - self.txouts = [] - self.txouts2 = [] - # Split a coinbase into two transaction puzzle outputs - split_inputs(self.nodes[0], self.nodes[0].listunspent(0), self.txouts, True) - - # Mine - while len(self.nodes[0].getrawmempool()) > 0: - self.nodes[0].generate(1) - - # Repeatedly split those 2 outputs, doubling twice for each rep - # Use txouts to monitor the available utxo, since these won't be tracked in wallet - reps = 0 - while reps < 5: - # Double txouts to txouts2 - while len(self.txouts) > 0: - split_inputs(self.nodes[0], self.txouts, self.txouts2) - while len(self.nodes[0].getrawmempool()) > 0: - self.nodes[0].generate(1) - # Double txouts2 to txouts - while len(self.txouts2) > 0: - split_inputs(self.nodes[0], self.txouts2, self.txouts) - while len(self.nodes[0].getrawmempool()) > 0: - self.nodes[0].generate(1) - reps += 1 - self.log.info("Finished splitting") - - # Now we can connect the other nodes, didn't want to connect them earlier - # so the estimates would not be affected by the splitting transactions - self.start_node(1) - self.start_node(2) - self.connect_nodes(1, 0) - self.connect_nodes(0, 2) - self.connect_nodes(2, 1) - - self.sync_all() + def initial_split(self, node): + """Split two coinbase UTxOs into many small coins""" + utxo_count = 2048 + self.confutxo = [] + splitted_amount = Decimal("0.04") + fee = Decimal("0.1") + change = Decimal("100") - splitted_amount * utxo_count - fee + tx = CTransaction() + tx.vin = [ + CTxIn(COutPoint(int(cb["txid"], 16), cb["vout"])) + for cb in node.listunspent()[:2] + ] + tx.vout = [CTxOut(int(splitted_amount * COIN), P2SH) for _ in range(utxo_count)] + tx.vout.append(CTxOut(int(change * COIN), P2SH)) + txhex = node.signrawtransactionwithwallet(tx.serialize().hex())["hex"] + txid = node.sendrawtransaction(txhex) + self.confutxo = [ + {"txid": txid, "vout": i, "amount": splitted_amount} + for i in range(utxo_count) + ] + while len(node.getrawmempool()) > 0: + self.generate(node, 1, sync_fun=self.no_op) + def sanity_check_estimates_range(self): + """Populate estimation buckets, assert estimates are in a sane range and + are strictly increasing as the target decreases.""" self.fees_per_kb = [] self.memutxo = [] - self.confutxo = self.txouts # Start with the set of confirmed txouts after splitting self.log.info("Will output estimates for 1/2/3/6/15/25 blocks") for _ in range(2): - self.log.info("Creating transactions and mining them with a block size that can't keep up") + self.log.info( + "Creating transactions and mining them with a block size that can't keep up" + ) # Create transactions and mine 10 small blocks with node 2, but create txs faster than we can mine self.transact_and_mine(10, self.nodes[2]) check_estimates(self.nodes[1], self.fees_per_kb) - self.log.info("Creating transactions and mining them at a block size that is just big enough") + self.log.info( + "Creating transactions and mining them at a block size that is just big enough" + ) # Generate transactions while mining 10 more blocks, this time with node1 # which mines blocks with capacity just above the rate that transactions are being created self.transact_and_mine(10, self.nodes[1]) @@ -257,17 +251,109 @@ class EstimateFeeTest(BitcoinTestFramework): # Finish by mining a normal-sized block: while len(self.nodes[1].getrawmempool()) > 0: - self.nodes[1].generate(1) + self.generate(self.nodes[1], 1) - self.sync_blocks(self.nodes[0:3], wait=.1) self.log.info("Final estimates after emptying mempools") check_estimates(self.nodes[1], self.fees_per_kb) + def test_feerate_mempoolminfee(self): + high_val = 3 * self.nodes[1].estimatesmartfee(1)["feerate"] + self.restart_node(1, extra_args=[f"-minrelaytxfee={high_val}"]) + check_estimates(self.nodes[1], self.fees_per_kb) + self.restart_node(1) + + def sanity_check_rbf_estimates(self, utxos): + """During 5 blocks, broadcast low fee transactions. Only 10% of them get + confirmed and the remaining ones get RBF'd with a high fee transaction at + the next block. + The block policy estimator should return the high feerate. + """ + # The broadcaster and block producer + node = self.nodes[0] + miner = self.nodes[1] + # In sat/vb + low_feerate = 1 + high_feerate = 10 + # Cache the utxos of which to replace the spender after it failed to get + # confirmed + utxos_to_respend = [] + txids_to_replace = [] + + assert_greater_than_or_equal(len(utxos), 250) + for _ in range(5): + # Broadcast 45 low fee transactions that will need to be RBF'd + for _ in range(45): + u = utxos.pop(0) + txid = send_tx(node, u, low_feerate) + utxos_to_respend.append(u) + txids_to_replace.append(txid) + # Broadcast 5 low fee transaction which don't need to + for _ in range(5): + send_tx(node, utxos.pop(0), low_feerate) + # Mine the transactions on another node + self.sync_mempools(wait=0.1, nodes=[node, miner]) + for txid in txids_to_replace: + miner.prioritisetransaction(txid=txid, fee_delta=-COIN) + self.generate(miner, 1) + # RBF the low-fee transactions + while len(utxos_to_respend) > 0: + u = utxos_to_respend.pop(0) + send_tx(node, u, high_feerate) + + # Mine the last replacement txs + self.sync_mempools(wait=0.1, nodes=[node, miner]) + self.generate(miner, 1) + + # Only 10% of the transactions were really confirmed with a low feerate, + # the rest needed to be RBF'd. We must return the 90% conf rate feerate. + high_feerate_kvb = Decimal(high_feerate) / COIN * 10 ** 3 + est_feerate = node.estimatesmartfee(2)["feerate"] + assert_equal(est_feerate, high_feerate_kvb) + + def run_test(self): + self.log.info("This test is time consuming, please be patient") + self.log.info("Splitting inputs so we can generate tx's") + + # Split two coinbases into many small utxos + self.start_node(0) + self.initial_split(self.nodes[0]) + self.log.info("Finished splitting") + + # Now we can connect the other nodes, didn't want to connect them earlier + # so the estimates would not be affected by the splitting transactions + self.start_node(1) + self.start_node(2) + self.connect_nodes(1, 0) + self.connect_nodes(0, 2) + self.connect_nodes(2, 1) + self.sync_all() + + self.log.info("Testing estimates with single transactions.") + self.sanity_check_estimates_range() + + # check that the effective feerate is greater than or equal to the mempoolminfee even for high mempoolminfee + self.log.info( + "Test fee rate estimation after restarting node with high MempoolMinFee" + ) + self.test_feerate_mempoolminfee() + + self.log.info("Restarting node with fresh estimation") + self.stop_node(0) + fee_dat = os.path.join(self.nodes[0].datadir, self.chain, "fee_estimates.dat") + os.remove(fee_dat) + self.start_node(0) + self.connect_nodes(0, 1) + self.connect_nodes(0, 2) + + self.log.info("Testing estimates with RBF.") + self.sanity_check_rbf_estimates(self.confutxo + self.memutxo) + self.log.info("Testing that fee estimation is disabled in blocksonly.") self.restart_node(0, ["-blocksonly"]) - assert_raises_rpc_error(-32603, "Fee estimation disabled", - self.nodes[0].estimatesmartfee, 2) + assert_raises_rpc_error( + -32603, "Fee estimation disabled", self.nodes[0].estimatesmartfee, 2 + ) -if __name__ == '__main__': +if __name__ == "__main__": EstimateFeeTest().main() diff --git a/test/functional/feature_filelock.py b/test/functional/feature_filelock.py index 2798d11b0a..945ece6a33 100755 --- a/test/functional/feature_filelock.py +++ b/test/functional/feature_filelock.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Check that it's not possible to start a second bitcoind instance using the same datadir or wallet.""" @@ -22,11 +22,11 @@ class FilelockTest(BitcoinTestFramework): def run_test(self): datadir = os.path.join(self.nodes[0].datadir, self.chain) - self.log.info("Using datadir {}".format(datadir)) + self.log.info(f"Using datadir {datadir}") self.log.info("Check that we can't start a second bitcoind instance using the same datadir") - expected_msg = "Error: Cannot obtain a lock on data directory {0}. {1} is probably already running.".format(datadir, self.config['environment']['PACKAGE_NAME']) - self.nodes[1].assert_start_raises_init_error(extra_args=['-datadir={}'.format(self.nodes[0].datadir), '-noserver'], expected_msg=expected_msg) + expected_msg = f"Error: Cannot obtain a lock on data directory {datadir}. {self.config['environment']['PACKAGE_NAME']} is probably already running." + self.nodes[1].assert_start_raises_init_error(extra_args=[f'-datadir={self.nodes[0].datadir}', '-noserver'], expected_msg=expected_msg) if self.is_wallet_compiled(): def check_wallet_filelock(descriptors): @@ -35,10 +35,10 @@ class FilelockTest(BitcoinTestFramework): wallet_dir = os.path.join(datadir, 'wallets') self.log.info("Check that we can't start a second bitcoind instance using the same wallet") if descriptors: - expected_msg = "Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?" + expected_msg = f"Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of {self.config['environment']['PACKAGE_NAME']}?" else: expected_msg = "Error: Error initializing wallet database environment" - self.nodes[1].assert_start_raises_init_error(extra_args=['-walletdir={}'.format(wallet_dir), '-wallet=' + wallet_name, '-noserver'], expected_msg=expected_msg, match=ErrorMatch.PARTIAL_REGEX) + self.nodes[1].assert_start_raises_init_error(extra_args=[f'-walletdir={wallet_dir}', f'-wallet={wallet_name}', '-noserver'], expected_msg=expected_msg, match=ErrorMatch.PARTIAL_REGEX) if self.is_bdb_compiled(): check_wallet_filelock(False) diff --git a/test/functional/feature_help.py b/test/functional/feature_help.py index babe9bfc80..4b66030b47 100755 --- a/test/functional/feature_help.py +++ b/test/functional/feature_help.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Verify that starting bitcoin with -h works as expected.""" @@ -40,14 +40,14 @@ class HelpTest(BitcoinTestFramework): # Node should exit immediately and output help to stdout. output, _ = self.get_node_output(ret_code_expected=0) assert b'Options' in output - self.log.info("Help text received: {} (...)".format(output[0:60])) + self.log.info(f"Help text received: {output[0:60]} (...)") self.log.info("Start bitcoin with -version for version information") self.nodes[0].start(extra_args=['-version']) # Node should exit immediately and output version to stdout. output, _ = self.get_node_output(ret_code_expected=0) assert b'version' in output - self.log.info("Version text received: {} (...)".format(output[0:60])) + self.log.info(f"Version text received: {output[0:60]} (...)") # Test that arguments not in the help results in an error self.log.info("Start bitcoind with -fakearg to make sure it does not start") @@ -55,7 +55,7 @@ class HelpTest(BitcoinTestFramework): # Node should exit immediately and output an error to stderr _, output = self.get_node_output(ret_code_expected=1) assert b'Error parsing command line arguments' in output - self.log.info("Error message received: {} (...)".format(output[0:60])) + self.log.info(f"Error message received: {output[0:60]} (...)") if __name__ == '__main__': diff --git a/test/functional/feature_includeconf.py b/test/functional/feature_includeconf.py index f22b7f266a..818e4c923b 100755 --- a/test/functional/feature_includeconf.py +++ b/test/functional/feature_includeconf.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2019 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Tests the includeconf argument @@ -42,7 +42,14 @@ class IncludeConfTest(BitcoinTestFramework): self.log.info("-includeconf cannot be used as command-line arg") self.stop_node(0) - self.nodes[0].assert_start_raises_init_error(extra_args=["-includeconf=relative2.conf"], expected_msg="Error: Error parsing command line arguments: -includeconf cannot be used from commandline; -includeconf=relative2.conf") + self.nodes[0].assert_start_raises_init_error( + extra_args=['-noincludeconf=0'], + expected_msg='Error: Error parsing command line arguments: -includeconf cannot be used from commandline; -includeconf=true', + ) + self.nodes[0].assert_start_raises_init_error( + extra_args=['-includeconf=relative2.conf', '-includeconf=no_warn.conf'], + expected_msg='Error: Error parsing command line arguments: -includeconf cannot be used from commandline; -includeconf="relative2.conf"', + ) self.log.info("-includeconf cannot be used recursively. subversion should end with 'main; relative)/'") with open(os.path.join(self.options.tmpdir, "node0", "relative.conf"), "a", encoding="utf8") as f: diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py new file mode 100755 index 0000000000..d0cb1e10e2 --- /dev/null +++ b/test/functional/feature_init.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Stress tests related to node initialization.""" +import os +from pathlib import Path + +from test_framework.test_framework import BitcoinTestFramework, SkipTest +from test_framework.test_node import ErrorMatch +from test_framework.util import assert_equal + + +class InitStressTest(BitcoinTestFramework): + """ + Ensure that initialization can be interrupted at a number of points and not impair + subsequent starts. + """ + + def set_test_params(self): + self.setup_clean_chain = False + self.num_nodes = 1 + + def run_test(self): + """ + - test terminating initialization after seeing a certain log line. + - test removing certain essential files to test startup error paths. + """ + # TODO: skip Windows for now since it isn't clear how to SIGTERM. + # + # Windows doesn't support `process.terminate()`. + # and other approaches (like below) don't work: + # + # os.kill(node.process.pid, signal.CTRL_C_EVENT) + if os.name == 'nt': + raise SkipTest("can't SIGTERM on Windows") + + self.stop_node(0) + node = self.nodes[0] + + def sigterm_node(): + node.process.terminate() + node.process.wait() + + def check_clean_start(): + """Ensure that node restarts successfully after various interrupts.""" + node.start() + node.wait_for_rpc_connection() + assert_equal(200, node.getblockcount()) + + lines_to_terminate_after = [ + 'Validating signatures for all blocks', + 'scheduler thread start', + 'Starting HTTP server', + 'Loading P2P addresses', + 'Loading banlist', + 'Loading block index', + 'Switching active chainstate', + 'Checking all blk files are present', + 'Loaded best chain:', + 'init message: Verifying blocks', + 'init message: Starting network threads', + 'net thread start', + 'addcon thread start', + 'loadblk thread start', + 'txindex thread start', + 'block filter index thread start', + 'coinstatsindex thread start', + 'msghand thread start', + 'net thread start', + 'addcon thread start', + ] + if self.is_wallet_compiled(): + lines_to_terminate_after.append('Verifying wallet') + + for terminate_line in lines_to_terminate_after: + self.log.info(f"Starting node and will exit after line '{terminate_line}'") + with node.wait_for_debug_log([terminate_line], ignore_case=True): + node.start(extra_args=['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1']) + self.log.debug("Terminating node after terminate line was found") + sigterm_node() + + check_clean_start() + self.stop_node(0) + + self.log.info("Test startup errors after removing certain essential files") + + files_to_disturb = { + 'blocks/index/*.ldb': 'Error opening block database.', + 'chainstate/*.ldb': 'Error opening block database.', + 'blocks/blk*.dat': 'Error loading block database.', + } + + for file_patt, err_fragment in files_to_disturb.items(): + target_files = list(node.chain_path.glob(file_patt)) + + for target_file in target_files: + self.log.info(f"Tweaking file to ensure failure {target_file}") + bak_path = str(target_file) + ".bak" + target_file.rename(bak_path) + + # TODO: at some point, we should test perturbing the files instead of removing + # them, e.g. + # + # contents = target_file.read_bytes() + # tweaked_contents = bytearray(contents) + # tweaked_contents[50:250] = b'1' * 200 + # target_file.write_bytes(bytes(tweaked_contents)) + # + # At the moment I can't get this to work (bitcoind loads successfully?) so + # investigate doing this later. + + node.assert_start_raises_init_error( + extra_args=['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1'], + expected_msg=err_fragment, + match=ErrorMatch.PARTIAL_REGEX, + ) + + for target_file in target_files: + bak_path = str(target_file) + ".bak" + self.log.debug(f"Restoring file from {bak_path} and restarting") + Path(bak_path).rename(target_file) + + check_clean_start() + self.stop_node(0) + + +if __name__ == '__main__': + InitStressTest().main() diff --git a/test/functional/feature_loadblock.py b/test/functional/feature_loadblock.py index 0a457ca17f..7f030c6773 100755 --- a/test/functional/feature_loadblock.py +++ b/test/functional/feature_loadblock.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test loadblock option @@ -16,6 +16,7 @@ import sys import tempfile import urllib +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal @@ -28,7 +29,7 @@ class LoadblockTest(BitcoinTestFramework): def run_test(self): self.nodes[1].setnetworkactive(state=False) - self.nodes[0].generate(100) + self.generate(self.nodes[0], COINBASE_MATURITY, sync_fun=self.no_op) # Parsing the url of our node to get settings for config file data_dir = self.nodes[0].datadir @@ -44,17 +45,17 @@ class LoadblockTest(BitcoinTestFramework): self.log.info("Create linearization config file") with open(cfg_file, "a", encoding="utf-8") as cfg: - cfg.write("datadir={}\n".format(data_dir)) - cfg.write("rpcuser={}\n".format(node_url.username)) - cfg.write("rpcpassword={}\n".format(node_url.password)) - cfg.write("port={}\n".format(node_url.port)) - cfg.write("host={}\n".format(node_url.hostname)) - cfg.write("output_file={}\n".format(bootstrap_file)) - cfg.write("max_height=100\n") - cfg.write("netmagic=fabfb5da\n") - cfg.write("input={}\n".format(blocks_dir)) - cfg.write("genesis={}\n".format(genesis_block)) - cfg.write("hashlist={}\n".format(hash_list.name)) + cfg.write(f"datadir={data_dir}\n") + cfg.write(f"rpcuser={node_url.username}\n") + cfg.write(f"rpcpassword={node_url.password}\n") + cfg.write(f"port={node_url.port}\n") + cfg.write(f"host={node_url.hostname}\n") + cfg.write(f"output_file={bootstrap_file}\n") + cfg.write(f"max_height=100\n") + cfg.write(f"netmagic=fabfb5da\n") + cfg.write(f"input={blocks_dir}\n") + cfg.write(f"genesis={genesis_block}\n") + cfg.write(f"hashlist={hash_list.name}\n") base_dir = self.config["environment"]["SRCDIR"] linearize_dir = os.path.join(base_dir, "contrib", "linearize") @@ -71,7 +72,7 @@ class LoadblockTest(BitcoinTestFramework): check=True) self.log.info("Restart second, unsynced node with bootstrap file") - self.restart_node(1, extra_args=["-loadblock=" + bootstrap_file]) + self.restart_node(1, extra_args=[f"-loadblock={bootstrap_file}"]) assert_equal(self.nodes[1].getblockcount(), 100) # start_node is blocking on all block files being imported assert_equal(self.nodes[1].getblockchaininfo()['blocks'], 100) diff --git a/test/functional/feature_logging.py b/test/functional/feature_logging.py index afcbcf099a..fe4f02dfe6 100755 --- a/test/functional/feature_logging.py +++ b/test/functional/feature_logging.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test debug logging.""" @@ -29,7 +29,7 @@ class LoggingTest(BitcoinTestFramework): # test alternative log file name outside datadir tempname = os.path.join(self.options.tmpdir, "foo.log") - self.restart_node(0, ["-debuglogfile=%s" % tempname]) + self.restart_node(0, [f"-debuglogfile={tempname}"]) assert os.path.isfile(tempname) # check that invalid log (relative) will cause error @@ -37,26 +37,26 @@ class LoggingTest(BitcoinTestFramework): invalidname = os.path.join("foo", "foo.log") self.stop_node(0) exp_stderr = r"Error: Could not open debug log file \S+$" - self.nodes[0].assert_start_raises_init_error(["-debuglogfile=%s" % (invalidname)], exp_stderr, match=ErrorMatch.FULL_REGEX) + self.nodes[0].assert_start_raises_init_error([f"-debuglogfile={invalidname}"], exp_stderr, match=ErrorMatch.FULL_REGEX) assert not os.path.isfile(os.path.join(invdir, "foo.log")) # check that invalid log (relative) works after path exists self.stop_node(0) os.mkdir(invdir) - self.start_node(0, ["-debuglogfile=%s" % (invalidname)]) + self.start_node(0, [f"-debuglogfile={invalidname}"]) assert os.path.isfile(os.path.join(invdir, "foo.log")) # check that invalid log (absolute) will cause error self.stop_node(0) invdir = os.path.join(self.options.tmpdir, "foo") invalidname = os.path.join(invdir, "foo.log") - self.nodes[0].assert_start_raises_init_error(["-debuglogfile=%s" % invalidname], exp_stderr, match=ErrorMatch.FULL_REGEX) + self.nodes[0].assert_start_raises_init_error([f"-debuglogfile={invalidname}"], exp_stderr, match=ErrorMatch.FULL_REGEX) assert not os.path.isfile(os.path.join(invdir, "foo.log")) # check that invalid log (absolute) works after path exists self.stop_node(0) os.mkdir(invdir) - self.start_node(0, ["-debuglogfile=%s" % (invalidname)]) + self.start_node(0, [f"-debuglogfile={invalidname}"]) assert os.path.isfile(os.path.join(invdir, "foo.log")) # check that -nodebuglogfile disables logging @@ -67,7 +67,7 @@ class LoggingTest(BitcoinTestFramework): assert not os.path.isfile(default_log_path) # just sanity check no crash here - self.restart_node(0, ["-debuglogfile=%s" % os.devnull]) + self.restart_node(0, [f"-debuglogfile={os.devnull}"]) if __name__ == '__main__': diff --git a/test/functional/feature_maxtipage.py b/test/functional/feature_maxtipage.py new file mode 100755 index 0000000000..87f9d6962d --- /dev/null +++ b/test/functional/feature_maxtipage.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test logic for setting nMaxTipAge on command line. + +Nodes don't consider themselves out of "initial block download" as long as +their best known block header time is more than nMaxTipAge in the past. +""" + +import time + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + + +DEFAULT_MAX_TIP_AGE = 24 * 60 * 60 + + +class MaxTipAgeTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 2 + + def test_maxtipage(self, maxtipage, set_parameter=True): + node_miner = self.nodes[0] + node_ibd = self.nodes[1] + + self.restart_node(1, [f'-maxtipage={maxtipage}'] if set_parameter else None) + self.connect_nodes(0, 1) + + # tips older than maximum age -> stay in IBD + cur_time = int(time.time()) + node_ibd.setmocktime(cur_time) + for delta in [5, 4, 3, 2, 1]: + node_miner.setmocktime(cur_time - maxtipage - delta) + self.generate(node_miner, 1) + assert_equal(node_ibd.getblockchaininfo()['initialblockdownload'], True) + + # tip within maximum age -> leave IBD + node_miner.setmocktime(cur_time - maxtipage) + self.generate(node_miner, 1) + assert_equal(node_ibd.getblockchaininfo()['initialblockdownload'], False) + + def run_test(self): + self.log.info("Test IBD with maximum tip age of 24 hours (default).") + self.test_maxtipage(DEFAULT_MAX_TIP_AGE, set_parameter=False) + + for hours in [20, 10, 5, 2, 1]: + maxtipage = hours * 60 * 60 + self.log.info(f"Test IBD with maximum tip age of {hours} hours (-maxtipage={maxtipage}).") + self.test_maxtipage(maxtipage) + + +if __name__ == '__main__': + MaxTipAgeTest().main() diff --git a/test/functional/feature_maxuploadtarget.py b/test/functional/feature_maxuploadtarget.py index d0a94658ff..24f79dda67 100755 --- a/test/functional/feature_maxuploadtarget.py +++ b/test/functional/feature_maxuploadtarget.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test behavior of -maxuploadtarget. @@ -36,9 +36,8 @@ class MaxUploadTest(BitcoinTestFramework): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [[ - "-maxuploadtarget=800", + "-maxuploadtarget=800M", "-acceptnonstdtxn=1", - "-peertimeout=9999", # bump because mocktime might cause a disconnect otherwise ]] self.supports_cli = False @@ -56,7 +55,7 @@ class MaxUploadTest(BitcoinTestFramework): self.nodes[0].setmocktime(old_time) # Generate some old blocks - self.nodes[0].generate(130) + self.generate(self.nodes[0], 130) # p2p_conns[0] will only request old blocks # p2p_conns[1] will only request new blocks @@ -67,7 +66,7 @@ class MaxUploadTest(BitcoinTestFramework): p2p_conns.append(self.nodes[0].add_p2p_connection(TestP2PConn())) # Now mine a big block - mine_large_block(self.nodes[0], self.utxo_cache) + mine_large_block(self, self.nodes[0], self.utxo_cache) # Store the hash; we'll request this later big_old_block = self.nodes[0].getbestblockhash() @@ -78,7 +77,7 @@ class MaxUploadTest(BitcoinTestFramework): self.nodes[0].setmocktime(int(time.time()) - 2*60*60*24) # Mine one more block, so that the prior block looks old - mine_large_block(self.nodes[0], self.utxo_cache) + mine_large_block(self, self.nodes[0], self.utxo_cache) # We'll be requesting this new block too big_new_block = self.nodes[0].getbestblockhash() diff --git a/test/functional/feature_minchainwork.py b/test/functional/feature_minchainwork.py index 803feb509a..489a729cfc 100755 --- a/test/functional/feature_minchainwork.py +++ b/test/functional/feature_minchainwork.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test logic for setting nMinimumChainWork on command line. @@ -45,16 +45,15 @@ class MinimumChainWorkTest(BitcoinTestFramework): # Start building a chain on node0. node2 shouldn't be able to sync until node1's # minchainwork is exceeded starting_chain_work = REGTEST_WORK_PER_BLOCK # Genesis block's work - self.log.info("Testing relay across node %d (minChainWork = %d)", 1, self.node_min_work[1]) + self.log.info(f"Testing relay across node 1 (minChainWork = {self.node_min_work[1]})") starting_blockcount = self.nodes[2].getblockcount() num_blocks_to_generate = int((self.node_min_work[1] - starting_chain_work) / REGTEST_WORK_PER_BLOCK) - self.log.info("Generating %d blocks on node0", num_blocks_to_generate) - hashes = self.nodes[0].generatetoaddress(num_blocks_to_generate, - self.nodes[0].get_deterministic_priv_key().address) + self.log.info(f"Generating {num_blocks_to_generate} blocks on node0") + hashes = self.generate(self.nodes[0], num_blocks_to_generate, sync_fun=self.no_op) - self.log.info("Node0 current chain work: %s", self.nodes[0].getblockheader(hashes[-1])['chainwork']) + self.log.info(f"Node0 current chain work: {self.nodes[0].getblockheader(hashes[-1])['chainwork']}") # Sleep a few seconds and verify that node2 didn't get any new blocks # or headers. We sleep, rather than sync_blocks(node0, node1) because @@ -63,7 +62,7 @@ class MinimumChainWorkTest(BitcoinTestFramework): time.sleep(3) self.log.info("Verifying node 2 has no more blocks than before") - self.log.info("Blockcounts: %s", [n.getblockcount() for n in self.nodes]) + self.log.info(f"Blockcounts: {[n.getblockcount() for n in self.nodes]}") # Node2 shouldn't have any new headers yet, because node1 should not # have relayed anything. assert_equal(len(self.nodes[2].getchaintips()), 1) @@ -73,7 +72,7 @@ class MinimumChainWorkTest(BitcoinTestFramework): assert_equal(self.nodes[2].getblockcount(), starting_blockcount) self.log.info("Generating one more block") - self.nodes[0].generatetoaddress(1, self.nodes[0].get_deterministic_priv_key().address) + self.generate(self.nodes[0], 1) self.log.info("Verifying nodes are all synced") @@ -84,7 +83,7 @@ class MinimumChainWorkTest(BitcoinTestFramework): # continue the test. self.sync_all() - self.log.info("Blockcounts: %s", [n.getblockcount() for n in self.nodes]) + self.log.info(f"Blockcounts: {[n.getblockcount() for n in self.nodes]}") if __name__ == '__main__': MinimumChainWorkTest().main() diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index b068ce612c..e038afa1ad 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the -alertnotify, -blocknotify and -walletnotify options.""" @@ -17,16 +17,19 @@ from test_framework.util import ( FILE_CHAR_START = 32 if os.name == 'nt' else 1 FILE_CHAR_END = 128 FILE_CHARS_DISALLOWED = '/\\?%*:|"<>' if os.name == 'nt' else '/' - +UNCONFIRMED_HASH_STRING = 'unconfirmed' def notify_outputname(walletname, txid): - return txid if os.name == 'nt' else '{}_{}'.format(walletname, txid) + return txid if os.name == 'nt' else f'{walletname}_{txid}' class NotificationsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True + # The experimental syscall sandbox feature (-sandbox) is not compatible with -alertnotify, + # -blocknotify or -walletnotify (which all invoke execve). + self.disable_syscall_sandbox = True def setup_network(self): self.wallet = ''.join(chr(i) for i in range(FILE_CHAR_START, FILE_CHAR_END) if chr(i) not in FILE_CHARS_DISALLOWED) @@ -39,11 +42,10 @@ class NotificationsTest(BitcoinTestFramework): # -alertnotify and -blocknotify on node0, walletnotify on node1 self.extra_args = [[ - "-alertnotify=echo > {}".format(os.path.join(self.alertnotify_dir, '%s')), - "-blocknotify=echo > {}".format(os.path.join(self.blocknotify_dir, '%s')), + f"-alertnotify=echo > {os.path.join(self.alertnotify_dir, '%s')}", + f"-blocknotify=echo > {os.path.join(self.blocknotify_dir, '%s')}", ], [ - "-rescan", - "-walletnotify=echo > {}".format(os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))), + f"-walletnotify=echo %h_%b > {os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))}", ]] self.wallet_names = [self.default_wallet_name, self.wallet] super().setup_network() @@ -54,12 +56,12 @@ class NotificationsTest(BitcoinTestFramework): seed = "cTdGmKFWpbvpKQ7ejrdzqYT2hhjyb3GPHnLAK7wdi5Em67YLwSm9" xpriv = "tprv8ZgxMBicQKsPfHCsTwkiM1KT56RXbGGTqvc2hgqzycpwbHqqpcajQeMRZoBD35kW4RtyCemu6j34Ku5DEspmgjKdt2qe4SvRch5Kk8B8A2v" desc_imports = [{ - "desc": descsum_create("wpkh(" + xpriv + "/0/*)"), + "desc": descsum_create(f"wpkh({xpriv}/0/*)"), "timestamp": 0, "active": True, "keypool": True, },{ - "desc": descsum_create("wpkh(" + xpriv + "/1/*)"), + "desc": descsum_create(f"wpkh({xpriv}/1/*)"), "timestamp": 0, "active": True, "keypool": True, @@ -76,7 +78,7 @@ class NotificationsTest(BitcoinTestFramework): self.log.info("test -blocknotify") block_count = 10 - blocks = self.nodes[1].generatetoaddress(block_count, self.nodes[1].getnewaddress() if self.is_wallet_compiled() else ADDRESS_BCRT1_UNSPENDABLE) + blocks = self.generatetoaddress(self.nodes[1], block_count, self.nodes[1].getnewaddress() if self.is_wallet_compiled() else ADDRESS_BCRT1_UNSPENDABLE) # wait at most 10 seconds for expected number of files before reading the content self.wait_until(lambda: len(os.listdir(self.blocknotify_dir)) == block_count, timeout=10) @@ -90,39 +92,33 @@ class NotificationsTest(BitcoinTestFramework): self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) == block_count, timeout=10) # directory content should equal the generated transaction hashes - txids_rpc = list(map(lambda t: notify_outputname(self.wallet, t['txid']), self.nodes[1].listtransactions("*", block_count))) - assert_equal(sorted(txids_rpc), sorted(os.listdir(self.walletnotify_dir))) - self.stop_node(1) - for tx_file in os.listdir(self.walletnotify_dir): - os.remove(os.path.join(self.walletnotify_dir, tx_file)) + tx_details = list(map(lambda t: (t['txid'], t['blockheight'], t['blockhash']), self.nodes[1].listtransactions("*", block_count))) + self.expect_wallet_notify(tx_details) self.log.info("test -walletnotify after rescan") - # restart node to rescan to force wallet notifications - self.start_node(1) - self.connect_nodes(0, 1) - + # rescan to force wallet notifications + self.nodes[1].rescanblockchain() self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) == block_count, timeout=10) + self.connect_nodes(0, 1) + # directory content should equal the generated transaction hashes - txids_rpc = list(map(lambda t: notify_outputname(self.wallet, t['txid']), self.nodes[1].listtransactions("*", block_count))) - assert_equal(sorted(txids_rpc), sorted(os.listdir(self.walletnotify_dir))) - for tx_file in os.listdir(self.walletnotify_dir): - os.remove(os.path.join(self.walletnotify_dir, tx_file)) + tx_details = list(map(lambda t: (t['txid'], t['blockheight'], t['blockhash']), self.nodes[1].listtransactions("*", block_count))) + self.expect_wallet_notify(tx_details) # Conflicting transactions tests. # Generate spends from node 0, and check notifications # triggered by node 1 self.log.info("test -walletnotify with conflicting transactions") self.nodes[0].rescanblockchain() - self.nodes[0].generatetoaddress(100, ADDRESS_BCRT1_UNSPENDABLE) - self.sync_blocks() + self.generatetoaddress(self.nodes[0], 100, ADDRESS_BCRT1_UNSPENDABLE) # Generate transaction on node 0, sync mempools, and check for # notification on node 1. tx1 = self.nodes[0].sendtoaddress(address=ADDRESS_BCRT1_UNSPENDABLE, amount=1, replaceable=True) assert_equal(tx1 in self.nodes[0].getrawmempool(), True) self.sync_mempools() - self.expect_wallet_notify([tx1]) + self.expect_wallet_notify([(tx1, -1, UNCONFIRMED_HASH_STRING)]) # Generate bump transaction, sync mempools, and check for bump1 # notification. In the future, per @@ -131,39 +127,59 @@ class NotificationsTest(BitcoinTestFramework): bump1 = self.nodes[0].bumpfee(tx1)["txid"] assert_equal(bump1 in self.nodes[0].getrawmempool(), True) self.sync_mempools() - self.expect_wallet_notify([bump1]) + self.expect_wallet_notify([(bump1, -1, UNCONFIRMED_HASH_STRING)]) # Add bump1 transaction to new block, checking for a notification # and the correct number of confirmations. - self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) + blockhash1 = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE)[0] + blockheight1 = self.nodes[0].getblockcount() self.sync_blocks() - self.expect_wallet_notify([bump1]) + self.expect_wallet_notify([(bump1, blockheight1, blockhash1)]) assert_equal(self.nodes[1].gettransaction(bump1)["confirmations"], 1) # Generate a second transaction to be bumped. tx2 = self.nodes[0].sendtoaddress(address=ADDRESS_BCRT1_UNSPENDABLE, amount=1, replaceable=True) assert_equal(tx2 in self.nodes[0].getrawmempool(), True) self.sync_mempools() - self.expect_wallet_notify([tx2]) + self.expect_wallet_notify([(tx2, -1, UNCONFIRMED_HASH_STRING)]) # Bump tx2 as bump2 and generate a block on node 0 while # disconnected, then reconnect and check for notifications on node 1 # about newly confirmed bump2 and newly conflicted tx2. self.disconnect_nodes(0, 1) bump2 = self.nodes[0].bumpfee(tx2)["txid"] - self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) + blockhash2 = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE, sync_fun=self.no_op)[0] + blockheight2 = self.nodes[0].getblockcount() assert_equal(self.nodes[0].gettransaction(bump2)["confirmations"], 1) assert_equal(tx2 in self.nodes[1].getrawmempool(), True) self.connect_nodes(0, 1) self.sync_blocks() - self.expect_wallet_notify([bump2, tx2]) + self.expect_wallet_notify([(bump2, blockheight2, blockhash2), (tx2, -1, UNCONFIRMED_HASH_STRING)]) assert_equal(self.nodes[1].gettransaction(bump2)["confirmations"], 1) # TODO: add test for `-alertnotify` large fork notifications - def expect_wallet_notify(self, tx_ids): - self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) >= len(tx_ids), timeout=10) - assert_equal(sorted(notify_outputname(self.wallet, tx_id) for tx_id in tx_ids), sorted(os.listdir(self.walletnotify_dir))) + def expect_wallet_notify(self, tx_details): + self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) >= len(tx_details), timeout=10) + # Should have no more and no less files than expected + assert_equal(sorted(notify_outputname(self.wallet, tx_id) for tx_id, _, _ in tx_details), sorted(os.listdir(self.walletnotify_dir))) + # Should now verify contents of each file + for tx_id, blockheight, blockhash in tx_details: + fname = os.path.join(self.walletnotify_dir, notify_outputname(self.wallet, tx_id)) + # Wait for the cached writes to hit storage + self.wait_until(lambda: os.path.getsize(fname) > 0, timeout=10) + with open(fname, 'rt', encoding='utf-8') as f: + text = f.read() + # Universal newline ensures '\n' on 'nt' + assert_equal(text[-1], '\n') + text = text[:-1] + if os.name == 'nt': + # On Windows, echo as above will append a whitespace + assert_equal(text[-1], ' ') + text = text[:-1] + expected = str(blockheight) + '_' + blockhash + assert_equal(text, expected) + for tx_file in os.listdir(self.walletnotify_dir): os.remove(os.path.join(self.walletnotify_dir, tx_file)) diff --git a/test/functional/feature_nulldummy.py b/test/functional/feature_nulldummy.py index bdbfa5aed1..7a84098a83 100755 --- a/test/functional/feature_nulldummy.py +++ b/test/functional/feature_nulldummy.py @@ -1,51 +1,59 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test NULLDUMMY softfork. Connect to a single node. Generate 2 blocks (save the coinbases for later). -Generate 427 more blocks. -[Policy/Consensus] Check that NULLDUMMY compliant transactions are accepted in the 430th block. +Generate COINBASE_MATURITY (CB) more blocks to ensure the coinbases are mature. +[Policy/Consensus] Check that NULLDUMMY compliant transactions are accepted in block CB + 3. [Policy] Check that non-NULLDUMMY transactions are rejected before activation. -[Consensus] Check that the new NULLDUMMY rules are not enforced on the 431st block. -[Policy/Consensus] Check that the new NULLDUMMY rules are enforced on the 432nd block. +[Consensus] Check that the new NULLDUMMY rules are not enforced on block CB + 4. +[Policy/Consensus] Check that the new NULLDUMMY rules are enforced on block CB + 5. """ import time -from test_framework.blocktools import NORMAL_GBT_REQUEST_PARAMS, create_block, create_transaction, add_witness_commitment +from test_framework.blocktools import ( + COINBASE_MATURITY, + NORMAL_GBT_REQUEST_PARAMS, + add_witness_commitment, + create_block, + create_transaction, +) from test_framework.messages import CTransaction -from test_framework.script import CScript +from test_framework.script import ( + OP_0, + OP_TRUE, +) from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) NULLDUMMY_ERROR = "non-mandatory-script-verify-flag (Dummy CHECKMULTISIG argument must be zero)" -def trueDummy(tx): - scriptSig = CScript(tx.vin[0].scriptSig) - newscript = [] - for i in scriptSig: - if (len(newscript) == 0): - assert len(i) == 0 - newscript.append(b'\x51') - else: - newscript.append(i) - tx.vin[0].scriptSig = CScript(newscript) + +def invalidate_nulldummy_tx(tx): + """Transform a NULLDUMMY compliant tx (i.e. scriptSig starts with OP_0) + to be non-NULLDUMMY compliant by replacing the dummy with OP_TRUE""" + assert_equal(tx.vin[0].scriptSig[0], OP_0) + tx.vin[0].scriptSig = bytes([OP_TRUE]) + tx.vin[0].scriptSig[1:] tx.rehash() -class NULLDUMMYTest(BitcoinTestFramework): +class NULLDUMMYTest(BitcoinTestFramework): def set_test_params(self): - # Need two nodes only so GBT doesn't complain that it's not connected - self.num_nodes = 2 + self.num_nodes = 1 self.setup_clean_chain = True # This script tests NULLDUMMY activation, which is part of the 'segwit' deployment, so we go through # normal segwit activation here (and don't use the default always-on behaviour). self.extra_args = [[ - '-segwitheight=432', + f'-testactivationheight=segwit@{COINBASE_MATURITY + 5}', '-addresstype=legacy', - ]] * 2 + '-par=1', # Use only one script thread to get the exact reject reason for testing + ]] def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -64,65 +72,61 @@ class NULLDUMMYTest(BitcoinTestFramework): wmulti.importaddress(self.ms_address) wmulti.importaddress(self.wit_ms_address) - self.coinbase_blocks = self.nodes[0].generate(2) # Block 2 + self.coinbase_blocks = self.generate(self.nodes[0], 2) # block height = 2 coinbase_txid = [] for i in self.coinbase_blocks: coinbase_txid.append(self.nodes[0].getblock(i)['tx'][0]) - self.nodes[0].generate(427) # Block 429 + self.generate(self.nodes[0], COINBASE_MATURITY) # block height = COINBASE_MATURITY + 2 self.lastblockhash = self.nodes[0].getbestblockhash() - self.lastblockheight = 429 - self.lastblocktime = int(time.time()) + 429 + self.lastblockheight = COINBASE_MATURITY + 2 + self.lastblocktime = int(time.time()) + self.lastblockheight - self.log.info("Test 1: NULLDUMMY compliant base transactions should be accepted to mempool and mined before activation [430]") + self.log.info(f"Test 1: NULLDUMMY compliant base transactions should be accepted to mempool and mined before activation [{COINBASE_MATURITY + 3}]") test1txs = [create_transaction(self.nodes[0], coinbase_txid[0], self.ms_address, amount=49)] txid1 = self.nodes[0].sendrawtransaction(test1txs[0].serialize_with_witness().hex(), 0) test1txs.append(create_transaction(self.nodes[0], txid1, self.ms_address, amount=48)) txid2 = self.nodes[0].sendrawtransaction(test1txs[1].serialize_with_witness().hex(), 0) test1txs.append(create_transaction(self.nodes[0], coinbase_txid[1], self.wit_ms_address, amount=49)) txid3 = self.nodes[0].sendrawtransaction(test1txs[2].serialize_with_witness().hex(), 0) - self.block_submit(self.nodes[0], test1txs, False, True) + self.block_submit(self.nodes[0], test1txs, accept=True) self.log.info("Test 2: Non-NULLDUMMY base multisig transaction should not be accepted to mempool before activation") test2tx = create_transaction(self.nodes[0], txid2, self.ms_address, amount=47) - trueDummy(test2tx) + invalidate_nulldummy_tx(test2tx) assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test2tx.serialize_with_witness().hex(), 0) - self.log.info("Test 3: Non-NULLDUMMY base transactions should be accepted in a block before activation [431]") - self.block_submit(self.nodes[0], [test2tx], False, True) + self.log.info(f"Test 3: Non-NULLDUMMY base transactions should be accepted in a block before activation [{COINBASE_MATURITY + 4}]") + self.block_submit(self.nodes[0], [test2tx], accept=True) self.log.info("Test 4: Non-NULLDUMMY base multisig transaction is invalid after activation") test4tx = create_transaction(self.nodes[0], test2tx.hash, self.address, amount=46) test6txs = [CTransaction(test4tx)] - trueDummy(test4tx) + invalidate_nulldummy_tx(test4tx) assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test4tx.serialize_with_witness().hex(), 0) - self.block_submit(self.nodes[0], [test4tx]) + self.block_submit(self.nodes[0], [test4tx], accept=False) self.log.info("Test 5: Non-NULLDUMMY P2WSH multisig transaction invalid after activation") test5tx = create_transaction(self.nodes[0], txid3, self.wit_address, amount=48) test6txs.append(CTransaction(test5tx)) test5tx.wit.vtxinwit[0].scriptWitness.stack[0] = b'\x01' assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test5tx.serialize_with_witness().hex(), 0) - self.block_submit(self.nodes[0], [test5tx], True) + self.block_submit(self.nodes[0], [test5tx], with_witness=True, accept=False) - self.log.info("Test 6: NULLDUMMY compliant base/witness transactions should be accepted to mempool and in block after activation [432]") + self.log.info(f"Test 6: NULLDUMMY compliant base/witness transactions should be accepted to mempool and in block after activation [{COINBASE_MATURITY + 5}]") for i in test6txs: self.nodes[0].sendrawtransaction(i.serialize_with_witness().hex(), 0) - self.block_submit(self.nodes[0], test6txs, True, True) + self.block_submit(self.nodes[0], test6txs, with_witness=True, accept=True) - def block_submit(self, node, txs, witness=False, accept=False): + def block_submit(self, node, txs, *, with_witness=False, accept): tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) assert_equal(tmpl['previousblockhash'], self.lastblockhash) assert_equal(tmpl['height'], self.lastblockheight + 1) - block = create_block(tmpl=tmpl, ntime=self.lastblocktime + 1) - for tx in txs: - tx.rehash() - block.vtx.append(tx) - block.hashMerkleRoot = block.calc_merkle_root() - witness and add_witness_commitment(block) - block.rehash() + block = create_block(tmpl=tmpl, ntime=self.lastblocktime + 1, txlist=txs) + if with_witness: + add_witness_commitment(block) block.solve() - assert_equal(None if accept else 'block-validation-failed', node.submitblock(block.serialize().hex())) - if (accept): + assert_equal(None if accept else NULLDUMMY_ERROR, node.submitblock(block.serialize().hex())) + if accept: assert_equal(node.getbestblockhash(), block.hash) self.lastblockhash = block.hash self.lastblocktime += 1 @@ -130,5 +134,6 @@ class NULLDUMMYTest(BitcoinTestFramework): else: assert_equal(node.getbestblockhash(), self.lastblockhash) + if __name__ == '__main__': NULLDUMMYTest().main() diff --git a/test/functional/feature_presegwit_node_upgrade.py b/test/functional/feature_presegwit_node_upgrade.py new file mode 100755 index 0000000000..3d762c8197 --- /dev/null +++ b/test/functional/feature_presegwit_node_upgrade.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test a pre-segwit node upgrading to segwit consensus""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + softfork_active, +) +import os + + +class SegwitUpgradeTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [["-testactivationheight=segwit@10"]] + + def run_test(self): + """A pre-segwit node with insufficiently validated blocks needs to redownload blocks""" + + self.log.info("Testing upgrade behaviour for pre-segwit node to segwit rules") + node = self.nodes[0] + + # Node hasn't been used or connected yet + assert_equal(node.getblockcount(), 0) + + assert not softfork_active(node, "segwit") + + # Generate 8 blocks without witness data + self.generate(node, 8) + assert_equal(node.getblockcount(), 8) + + self.stop_node(0) + # Restarting the node (with segwit activation height set to 5) should result in a shutdown + # because the blockchain consists of 3 insufficiently validated blocks per segwit consensus rules. + node.assert_start_raises_init_error( + extra_args=["-testactivationheight=segwit@5"], + expected_msg=": Witness data for blocks after height 5 requires " + f"validation. Please restart with -reindex..{os.linesep}" + "Please restart with -reindex or -reindex-chainstate to recover.", + ) + + # As directed, the user restarts the node with -reindex + self.start_node(0, extra_args=["-reindex", "-testactivationheight=segwit@5"]) + + # With the segwit consensus rules, the node is able to validate only up to block 4 + assert_equal(node.getblockcount(), 4) + + # The upgraded node should now have segwit activated + assert softfork_active(node, "segwit") + + +if __name__ == '__main__': + SegwitUpgradeTest().main() diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index 8bee43b8ad..7d9e5b70fc 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test bitcoind with different proxy configuration. @@ -12,6 +12,7 @@ Test plan: - `-proxy` (proxy everything) - `-onion` (proxy just onions) - `-proxyrandomize` Circuit randomization + - `-cjdnsreachable` - Proxy configurations to test on proxy side, - support no authentication (other proxy) - support no authentication + user/pass authentication (Tor) @@ -26,6 +27,7 @@ addnode connect to IPv4 addnode connect to IPv6 addnode connect to onion addnode connect to generic DNS name +addnode connect to a CJDNS address - Test getnetworkinfo for each node """ @@ -50,14 +52,15 @@ NET_IPV4 = "ipv4" NET_IPV6 = "ipv6" NET_ONION = "onion" NET_I2P = "i2p" +NET_CJDNS = "cjdns" # Networks returned by RPC getnetworkinfo, defined in src/rpc/net.cpp::GetNetworksInfo() -NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION, NET_I2P}) +NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION, NET_I2P, NET_CJDNS}) class ProxyTest(BitcoinTestFramework): def set_test_params(self): - self.num_nodes = 4 + self.num_nodes = 5 self.setup_clean_chain = True def setup_nodes(self): @@ -97,14 +100,16 @@ class ProxyTest(BitcoinTestFramework): # Note: proxies are not used to connect to local nodes. This is because the proxy to # use is based on CService.GetNetwork(), which returns NET_UNROUTABLE for localhost. args = [ - ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-proxyrandomize=1'], - ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-onion=%s:%i' % (self.conf2.addr), - '-i2psam=%s:%i' % (self.i2p_sam), '-i2pacceptincoming=0', '-proxyrandomize=0'], - ['-listen', '-proxy=%s:%i' % (self.conf2.addr),'-proxyrandomize=1'], - [] + ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}','-proxyrandomize=1'], + ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}',f'-onion={self.conf2.addr[0]}:{self.conf2.addr[1]}', + f'-i2psam={self.i2p_sam[0]}:{self.i2p_sam[1]}', '-i2pacceptincoming=0', '-proxyrandomize=0'], + ['-listen', f'-proxy={self.conf2.addr[0]}:{self.conf2.addr[1]}','-proxyrandomize=1'], + [], + ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}','-proxyrandomize=1', + '-cjdnsreachable'] ] if self.have_ipv6: - args[3] = ['-listen', '-proxy=[%s]:%i' % (self.conf3.addr),'-proxyrandomize=0', '-noonion'] + args[3] = ['-listen', f'-proxy=[{self.conf3.addr[0]}]:{self.conf3.addr[1]}','-proxyrandomize=0', '-noonion'] self.add_nodes(self.num_nodes, extra_args=args) self.start_nodes() @@ -113,10 +118,10 @@ class ProxyTest(BitcoinTestFramework): if peer["addr"] == addr: assert_equal(peer["network"], network) - def node_test(self, node, proxies, auth, test_onion=True): + def node_test(self, node, *, proxies, auth, test_onion, test_cjdns): rv = [] addr = "15.61.23.23:1234" - self.log.debug("Test: outgoing IPv4 connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing IPv4 connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[0].queue.get() assert isinstance(cmd, Socks5Command) @@ -132,7 +137,7 @@ class ProxyTest(BitcoinTestFramework): if self.have_ipv6: addr = "[1233:3432:2434:2343:3234:2345:6546:4534]:5443" - self.log.debug("Test: outgoing IPv6 connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing IPv6 connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[1].queue.get() assert isinstance(cmd, Socks5Command) @@ -147,13 +152,13 @@ class ProxyTest(BitcoinTestFramework): self.network_test(node, addr, network=NET_IPV6) if test_onion: - addr = "bitcoinostk4e4re.onion:8333" - self.log.debug("Test: outgoing onion connection through node for address {}".format(addr)) + addr = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:8333" + self.log.debug(f"Test: outgoing onion connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[2].queue.get() assert isinstance(cmd, Socks5Command) assert_equal(cmd.atyp, AddressType.DOMAINNAME) - assert_equal(cmd.addr, b"bitcoinostk4e4re.onion") + assert_equal(cmd.addr, b"pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion") assert_equal(cmd.port, 8333) if not auth: assert_equal(cmd.username, None) @@ -161,8 +166,23 @@ class ProxyTest(BitcoinTestFramework): rv.append(cmd) self.network_test(node, addr, network=NET_ONION) + if test_cjdns: + addr = "[fc00:1:2:3:4:5:6:7]:8888" + self.log.debug(f"Test: outgoing CJDNS connection through node for address {addr}") + node.addnode(addr, "onetry") + cmd = proxies[1].queue.get() + assert isinstance(cmd, Socks5Command) + assert_equal(cmd.atyp, AddressType.DOMAINNAME) + assert_equal(cmd.addr, b"fc00:1:2:3:4:5:6:7") + assert_equal(cmd.port, 8888) + if not auth: + assert_equal(cmd.username, None) + assert_equal(cmd.password, None) + rv.append(cmd) + self.network_test(node, addr, network=NET_CJDNS) + addr = "node.noumenon:8333" - self.log.debug("Test: outgoing DNS name connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing DNS name connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[3].queue.get() assert isinstance(cmd, Socks5Command) @@ -179,20 +199,33 @@ class ProxyTest(BitcoinTestFramework): def run_test(self): # basic -proxy - self.node_test(self.nodes[0], [self.serv1, self.serv1, self.serv1, self.serv1], False) + self.node_test(self.nodes[0], + proxies=[self.serv1, self.serv1, self.serv1, self.serv1], + auth=False, test_onion=True, test_cjdns=False) # -proxy plus -onion - self.node_test(self.nodes[1], [self.serv1, self.serv1, self.serv2, self.serv1], False) + self.node_test(self.nodes[1], + proxies=[self.serv1, self.serv1, self.serv2, self.serv1], + auth=False, test_onion=True, test_cjdns=False) # -proxy plus -onion, -proxyrandomize - rv = self.node_test(self.nodes[2], [self.serv2, self.serv2, self.serv2, self.serv2], True) + rv = self.node_test(self.nodes[2], + proxies=[self.serv2, self.serv2, self.serv2, self.serv2], + auth=True, test_onion=True, test_cjdns=False) # Check that credentials as used for -proxyrandomize connections are unique credentials = set((x.username,x.password) for x in rv) assert_equal(len(credentials), len(rv)) if self.have_ipv6: # proxy on IPv6 localhost - self.node_test(self.nodes[3], [self.serv3, self.serv3, self.serv3, self.serv3], False, False) + self.node_test(self.nodes[3], + proxies=[self.serv3, self.serv3, self.serv3, self.serv3], + auth=False, test_onion=False, test_cjdns=False) + + # -proxy=unauth -proxyrandomize=1 -cjdnsreachable + self.node_test(self.nodes[4], + proxies=[self.serv1, self.serv1, self.serv1, self.serv1], + auth=False, test_onion=True, test_cjdns=True) def networks_dict(d): r = {} @@ -214,16 +247,17 @@ class ProxyTest(BitcoinTestFramework): assert_equal(n0[net]['proxy_randomize_credentials'], expected_randomize) assert_equal(n0['onion']['reachable'], True) assert_equal(n0['i2p']['reachable'], False) + assert_equal(n0['cjdns']['reachable'], False) n1 = networks_dict(self.nodes[1].getnetworkinfo()) assert_equal(NETWORKS, n1.keys()) for net in ['ipv4', 'ipv6']: - assert_equal(n1[net]['proxy'], '%s:%i' % (self.conf1.addr)) + assert_equal(n1[net]['proxy'], f'{self.conf1.addr[0]}:{self.conf1.addr[1]}') assert_equal(n1[net]['proxy_randomize_credentials'], False) - assert_equal(n1['onion']['proxy'], '%s:%i' % (self.conf2.addr)) + assert_equal(n1['onion']['proxy'], f'{self.conf2.addr[0]}:{self.conf2.addr[1]}') assert_equal(n1['onion']['proxy_randomize_credentials'], False) assert_equal(n1['onion']['reachable'], True) - assert_equal(n1['i2p']['proxy'], '%s:%i' % (self.i2p_sam)) + assert_equal(n1['i2p']['proxy'], f'{self.i2p_sam[0]}:{self.i2p_sam[1]}') assert_equal(n1['i2p']['proxy_randomize_credentials'], False) assert_equal(n1['i2p']['reachable'], True) @@ -234,12 +268,13 @@ class ProxyTest(BitcoinTestFramework): expected_proxy = '' expected_randomize = False else: - expected_proxy = '%s:%i' % (self.conf2.addr) + expected_proxy = f'{self.conf2.addr[0]}:{self.conf2.addr[1]}' expected_randomize = True assert_equal(n2[net]['proxy'], expected_proxy) assert_equal(n2[net]['proxy_randomize_credentials'], expected_randomize) assert_equal(n2['onion']['reachable'], True) assert_equal(n2['i2p']['reachable'], False) + assert_equal(n2['cjdns']['reachable'], False) if self.have_ipv6: n3 = networks_dict(self.nodes[3].getnetworkinfo()) @@ -248,11 +283,27 @@ class ProxyTest(BitcoinTestFramework): if net == NET_I2P: expected_proxy = '' else: - expected_proxy = '[%s]:%i' % (self.conf3.addr) + expected_proxy = f'[{self.conf3.addr[0]}]:{self.conf3.addr[1]}' assert_equal(n3[net]['proxy'], expected_proxy) assert_equal(n3[net]['proxy_randomize_credentials'], False) assert_equal(n3['onion']['reachable'], False) assert_equal(n3['i2p']['reachable'], False) + assert_equal(n3['cjdns']['reachable'], False) + + n4 = networks_dict(self.nodes[4].getnetworkinfo()) + assert_equal(NETWORKS, n4.keys()) + for net in NETWORKS: + if net == NET_I2P: + expected_proxy = '' + expected_randomize = False + else: + expected_proxy = '%s:%i' % (self.conf1.addr) + expected_randomize = True + assert_equal(n4[net]['proxy'], expected_proxy) + assert_equal(n4[net]['proxy_randomize_credentials'], expected_randomize) + assert_equal(n4['onion']['reachable'], True) + assert_equal(n4['i2p']['reachable'], False) + assert_equal(n4['cjdns']['reachable'], True) if __name__ == '__main__': diff --git a/test/functional/feature_pruning.py b/test/functional/feature_pruning.py index f09bffe2d4..ba3c5053cb 100755 --- a/test/functional/feature_pruning.py +++ b/test/functional/feature_pruning.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the pruning code. @@ -11,8 +11,12 @@ This test takes 30 mins or more (up to 2 hours) import os from test_framework.blocktools import create_coinbase -from test_framework.messages import CBlock, ToHex -from test_framework.script import CScript, OP_RETURN, OP_NOP +from test_framework.messages import CBlock +from test_framework.script import ( + CScript, + OP_NOP, + OP_RETURN, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -62,7 +66,7 @@ def mine_large_blocks(node, n): block.solve() # Submit to the node - node.submitblock(ToHex(block)) + node.submitblock(block.serialize().hex()) previousblockhash = block.sha256 height += 1 @@ -92,9 +96,6 @@ class PruneTest(BitcoinTestFramework): ] self.rpc_timeout = 120 - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def setup_network(self): self.setup_nodes() @@ -110,23 +111,41 @@ class PruneTest(BitcoinTestFramework): def setup_nodes(self): self.add_nodes(self.num_nodes, self.extra_args) self.start_nodes() - self.import_deterministic_coinbase_privkeys() + if self.is_wallet_compiled(): + self.import_deterministic_coinbase_privkeys() def create_big_chain(self): # Start by creating some coinbases we can spend later - self.nodes[1].generate(200) - self.sync_blocks(self.nodes[0:2]) - self.nodes[0].generate(150) + self.generate(self.nodes[1], 200, sync_fun=lambda: self.sync_blocks(self.nodes[0:2])) + self.generate(self.nodes[0], 150, sync_fun=self.no_op) # Then mine enough full blocks to create more than 550MiB of data mine_large_blocks(self.nodes[0], 645) self.sync_blocks(self.nodes[0:5]) + def test_invalid_command_line_options(self): + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Prune cannot be configured with a negative value.', + extra_args=['-prune=-1'], + ) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Prune configured below the minimum of 550 MiB. Please use a higher number.', + extra_args=['-prune=549'], + ) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Prune mode is incompatible with -txindex.', + extra_args=['-prune=550', '-txindex'], + ) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Prune mode is incompatible with -coinstatsindex.', + extra_args=['-prune=550', '-coinstatsindex'], + ) + def test_height_min(self): assert os.path.isfile(os.path.join(self.prunedir, "blk00000.dat")), "blk00000.dat is missing, pruning too early" self.log.info("Success") - self.log.info("Though we're already using more than 550MiB, current usage: %d" % calc_usage(self.prunedir)) + self.log.info(f"Though we're already using more than 550MiB, current usage: {calc_usage(self.prunedir)}") self.log.info("Mining 25 more blocks should cause the first block file to be pruned") # Pruning doesn't run until we're allocating another chunk, 20 full blocks past the height cutoff will ensure this mine_large_blocks(self.nodes[0], 25) @@ -136,7 +155,7 @@ class PruneTest(BitcoinTestFramework): self.log.info("Success") usage = calc_usage(self.prunedir) - self.log.info("Usage should be below target: %d" % usage) + self.log.info(f"Usage should be below target: {usage}") assert_greater_than(550, usage) def create_chain_with_staleblocks(self): @@ -159,18 +178,18 @@ class PruneTest(BitcoinTestFramework): self.connect_nodes(0, 2) self.sync_blocks(self.nodes[0:3]) - self.log.info("Usage can be over target because of high stale rate: %d" % calc_usage(self.prunedir)) + self.log.info(f"Usage can be over target because of high stale rate: {calc_usage(self.prunedir)}") def reorg_test(self): # Node 1 will mine a 300 block chain starting 287 blocks back from Node 0 and Node 2's tip # This will cause Node 2 to do a reorg requiring 288 blocks of undo data to the reorg_test chain height = self.nodes[1].getblockcount() - self.log.info("Current block height: %d" % height) + self.log.info(f"Current block height: {height}") self.forkheight = height - 287 self.forkhash = self.nodes[1].getblockhash(self.forkheight) - self.log.info("Invalidating block %s at height %d" % (self.forkhash, self.forkheight)) + self.log.info(f"Invalidating block {self.forkhash} at height {self.forkheight}") self.nodes[1].invalidateblock(self.forkhash) # We've now switched to our previously mined-24 block fork on node 1, but that's not what we want @@ -182,22 +201,22 @@ class PruneTest(BitcoinTestFramework): curhash = self.nodes[1].getblockhash(self.forkheight - 1) assert self.nodes[1].getblockcount() == self.forkheight - 1 - self.log.info("New best height: %d" % self.nodes[1].getblockcount()) + self.log.info(f"New best height: {self.nodes[1].getblockcount()}") # Disconnect node1 and generate the new chain self.disconnect_nodes(0, 1) self.disconnect_nodes(1, 2) self.log.info("Generating new longer chain of 300 more blocks") - self.nodes[1].generate(300) + self.generate(self.nodes[1], 300, sync_fun=self.no_op) self.log.info("Reconnect nodes") self.connect_nodes(0, 1) self.connect_nodes(1, 2) self.sync_blocks(self.nodes[0:3], timeout=120) - self.log.info("Verify height on node 2: %d" % self.nodes[2].getblockcount()) - self.log.info("Usage possibly still high because of stale blocks in block files: %d" % calc_usage(self.prunedir)) + self.log.info(f"Verify height on node 2: {self.nodes[2].getblockcount()}") + self.log.info(f"Usage possibly still high because of stale blocks in block files: {calc_usage(self.prunedir)}") self.log.info("Mine 220 more large blocks so we have requisite history") @@ -205,7 +224,7 @@ class PruneTest(BitcoinTestFramework): self.sync_blocks(self.nodes[0:3], timeout=120) usage = calc_usage(self.prunedir) - self.log.info("Usage should be below target: %d" % usage) + self.log.info(f"Usage should be below target: {usage}") assert_greater_than(550, usage) def reorg_back(self): @@ -213,7 +232,7 @@ class PruneTest(BitcoinTestFramework): assert_raises_rpc_error(-1, "Block not available (pruned data)", self.nodes[2].getblock, self.forkhash) with self.nodes[2].assert_debug_log(expected_msgs=['block verification stopping at height', '(pruning, no data)']): self.nodes[2].verifychain(checklevel=4, nblocks=0) - self.log.info("Will need to redownload block %d" % self.forkheight) + self.log.info(f"Will need to redownload block {self.forkheight}") # Verify that we have enough history to reorg back to the fork point # Although this is more than 288 blocks, because this chain was written more recently @@ -237,11 +256,11 @@ class PruneTest(BitcoinTestFramework): # At this point node 2 is within 288 blocks of the fork point so it will preserve its ability to reorg if self.nodes[2].getblockcount() < self.mainchainheight: blocks_to_mine = first_reorg_height + 1 - self.mainchainheight - self.log.info("Rewind node 0 to prev main chain to mine longer chain to trigger redownload. Blocks needed: %d" % blocks_to_mine) + self.log.info(f"Rewind node 0 to prev main chain to mine longer chain to trigger redownload. Blocks needed: {blocks_to_mine}") self.nodes[0].invalidateblock(curchainhash) assert_equal(self.nodes[0].getblockcount(), self.mainchainheight) assert_equal(self.nodes[0].getbestblockhash(), self.mainchainhash2) - goalbesthash = self.nodes[0].generate(blocks_to_mine)[-1] + goalbesthash = self.generate(self.nodes[0], blocks_to_mine, sync_fun=self.no_op)[-1] goalbestheight = first_reorg_height + 1 self.log.info("Verify node 2 reorged back to the main chain, some blocks of which it had to redownload") @@ -256,7 +275,7 @@ class PruneTest(BitcoinTestFramework): self.start_node(node_number) node = self.nodes[node_number] assert_equal(node.getblockcount(), 995) - assert_raises_rpc_error(-1, "not in prune mode", node.pruneblockchain, 500) + assert_raises_rpc_error(-1, "Cannot prune blocks because node is not in prune mode", node.pruneblockchain, 500) # now re-start in manual pruning mode self.restart_node(node_number, extra_args=["-prune=1"]) @@ -274,7 +293,7 @@ class PruneTest(BitcoinTestFramework): assert_equal(ret, node.getblockchaininfo()['pruneheight']) def has_block(index): - return os.path.isfile(os.path.join(self.nodes[node_number].datadir, self.chain, "blocks", "blk{:05}.dat".format(index))) + return os.path.isfile(os.path.join(self.nodes[node_number].datadir, self.chain, "blocks", f"blk{index:05}.dat")) # should not prune because chain tip of node 3 (995) < PruneAfterHeight (1000) assert_raises_rpc_error(-1, "Blockchain is too short for pruning", node.pruneblockchain, height(500)) @@ -284,14 +303,21 @@ class PruneTest(BitcoinTestFramework): assert_equal(block1_details["nTx"], len(block1_details["tx"])) # mine 6 blocks so we are at height 1001 (i.e., above PruneAfterHeight) - node.generate(6) + self.generate(node, 6, sync_fun=self.no_op) assert_equal(node.getblockchaininfo()["blocks"], 1001) + # prune parameter in the future (block or timestamp) should raise an exception + future_parameter = height(1001) + 5 + if use_timestamp: + assert_raises_rpc_error(-8, "Could not find block with at least the specified timestamp", node.pruneblockchain, future_parameter) + else: + assert_raises_rpc_error(-8, "Blockchain is shorter than the attempted prune height", node.pruneblockchain, future_parameter) + # Pruned block should still know the number of transactions assert_equal(node.getblockheader(node.getblockhash(1))["nTx"], block1_details["nTx"]) # negative heights should raise an exception - assert_raises_rpc_error(-8, "Negative", node.pruneblockchain, -10) + assert_raises_rpc_error(-8, "Negative block height", node.pruneblockchain, -10) # height=100 too low to prune first block file so this is a no-op prune(100) @@ -315,7 +341,7 @@ class PruneTest(BitcoinTestFramework): assert has_block(2), "blk00002.dat is still there, should be pruned by now" # advance the tip so blk00002.dat and blk00003.dat can be pruned (the last 288 blocks should now be in blk00004.dat) - node.generate(288) + self.generate(node, 288, sync_fun=self.no_op) prune(1000) assert not has_block(2), "blk00002.dat is still there, should be pruned by now" assert not has_block(3), "blk00003.dat is still there, should be pruned by now" @@ -446,8 +472,12 @@ class PruneTest(BitcoinTestFramework): self.log.info("Test manual pruning with timestamps") self.manual_test(4, use_timestamp=True) - self.log.info("Test wallet re-scan") - self.wallet_test() + if self.is_wallet_compiled(): + self.log.info("Test wallet re-scan") + self.wallet_test() + + self.log.info("Test invalid pruning command line options") + self.test_invalid_command_line_options() self.log.info("Done") diff --git a/test/functional/feature_rbf.py b/test/functional/feature_rbf.py index c6f55c62b4..f0ed914461 100755 --- a/test/functional/feature_rbf.py +++ b/test/functional/feature_rbf.py @@ -1,68 +1,35 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the RBF code.""" +from copy import deepcopy from decimal import Decimal -from test_framework.messages import COIN, COutPoint, CTransaction, CTxIn, CTxOut +from test_framework.messages import ( + BIP125_SEQUENCE_NUMBER, + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxOut, + SEQUENCE_FINAL, +) from test_framework.script import CScript, OP_DROP from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_raises_rpc_error, satoshi_round -from test_framework.script_util import DUMMY_P2WPKH_SCRIPT, DUMMY_2_P2WPKH_SCRIPT +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) +from test_framework.script_util import ( + DUMMY_P2WPKH_SCRIPT, + DUMMY_2_P2WPKH_SCRIPT, +) +from test_framework.wallet import MiniWallet +from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE MAX_REPLACEMENT_LIMIT = 100 - -def txToHex(tx): - return tx.serialize().hex() - -def make_utxo(node, amount, confirmed=True, scriptPubKey=DUMMY_P2WPKH_SCRIPT): - """Create a txout with a given amount and scriptPubKey - - Mines coins as needed. - - confirmed - txouts created will be confirmed in the blockchain; - unconfirmed otherwise. - """ - fee = 1*COIN - while node.getbalance() < satoshi_round((amount + fee)/COIN): - node.generate(100) - - new_addr = node.getnewaddress() - txid = node.sendtoaddress(new_addr, satoshi_round((amount+fee)/COIN)) - tx1 = node.getrawtransaction(txid, 1) - txid = int(txid, 16) - i = None - - for i, txout in enumerate(tx1['vout']): - if txout['scriptPubKey']['addresses'] == [new_addr]: - break - assert i is not None - - tx2 = CTransaction() - tx2.vin = [CTxIn(COutPoint(txid, i))] - tx2.vout = [CTxOut(amount, scriptPubKey)] - tx2.rehash() - - signed_tx = node.signrawtransactionwithwallet(txToHex(tx2)) - - txid = node.sendrawtransaction(signed_tx['hex'], 0) - - # If requested, ensure txouts are confirmed. - if confirmed: - mempool_size = len(node.getrawmempool()) - while mempool_size > 0: - node.generate(1) - new_size = len(node.getrawmempool()) - # Error out if we have something stuck in the mempool, as this - # would likely be a bug. - assert new_size < mempool_size - mempool_size = new_size - - return COutPoint(int(txid, 16), 0) - - class ReplaceByFeeTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -78,17 +45,12 @@ class ReplaceByFeeTest(BitcoinTestFramework): ] self.supports_cli = False - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def run_test(self): - # Leave IBD - self.nodes[0].generate(1) - - make_utxo(self.nodes[0], 1*COIN) - - # Ensure nodes are synced - self.sync_all() + self.wallet = MiniWallet(self.nodes[0]) + # the pre-mined test framework chain contains coinbase outputs to the + # MiniWallet's default address in blocks 76-100 (see method + # BitcoinTestFramework._initialize_chain()) + self.wallet.rescan_utxos() self.log.info("Running test simple doublespend...") self.test_simple_doublespend() @@ -120,39 +82,57 @@ class ReplaceByFeeTest(BitcoinTestFramework): self.log.info("Running test prioritised transactions...") self.test_prioritised_transactions() + self.log.info("Running test no inherited signaling...") + self.test_no_inherited_signaling() + + self.log.info("Running test replacement relay fee...") + self.test_replacement_relay_fee() + self.log.info("Passed") + def make_utxo(self, node, amount, confirmed=True, scriptPubKey=DUMMY_P2WPKH_SCRIPT): + """Create a txout with a given amount and scriptPubKey + + confirmed - txouts created will be confirmed in the blockchain; + unconfirmed otherwise. + """ + txid, n = self.wallet.send_to(from_node=node, scriptPubKey=scriptPubKey, amount=amount) + + # If requested, ensure txouts are confirmed. + if confirmed: + mempool_size = len(node.getrawmempool()) + while mempool_size > 0: + self.generate(node, 1) + new_size = len(node.getrawmempool()) + # Error out if we have something stuck in the mempool, as this + # would likely be a bug. + assert new_size < mempool_size + mempool_size = new_size + + return COutPoint(int(txid, 16), n) + def test_simple_doublespend(self): """Simple doublespend""" - tx0_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) - - # make_utxo may have generated a bunch of blocks, so we need to sync - # before we can spend the coins generated, or else the resulting - # transactions might not be accepted by our peers. - self.sync_all() + # we use MiniWallet to create a transaction template with inputs correctly set, + # and modify the output (amount, scriptPubKey) according to our needs + tx_template = self.wallet.create_self_transfer()['tx'] - tx1a = CTransaction() - tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0)] + tx1a = deepcopy(tx_template) tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] - tx1a_hex = txToHex(tx1a) + tx1a_hex = tx1a.serialize().hex() tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) - self.sync_all() - # Should fail because we haven't changed the fee - tx1b = CTransaction() - tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] + tx1b = deepcopy(tx_template) tx1b.vout = [CTxOut(1 * COIN, DUMMY_2_P2WPKH_SCRIPT)] - tx1b_hex = txToHex(tx1b) + tx1b_hex = tx1b.serialize().hex() # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) # Extra 0.1 BTC fee - tx1b = CTransaction() - tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] - tx1b.vout = [CTxOut(int(0.9 * COIN), DUMMY_P2WPKH_SCRIPT)] - tx1b_hex = txToHex(tx1b) + tx1b.vout[0].nValue -= int(0.1 * COIN) + tx1b_hex = tx1b.serialize().hex() # Works when enabled tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) @@ -166,28 +146,28 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_doublespend_chain(self): """Doublespend of a long chain""" - initial_nValue = 50*COIN - tx0_outpoint = make_utxo(self.nodes[0], initial_nValue) + initial_nValue = 5 * COIN + tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) prevout = tx0_outpoint remaining_value = initial_nValue chain_txids = [] - while remaining_value > 10*COIN: - remaining_value -= 1*COIN + while remaining_value > 1 * COIN: + remaining_value -= int(0.1 * COIN) tx = CTransaction() tx.vin = [CTxIn(prevout, nSequence=0)] tx.vout = [CTxOut(remaining_value, CScript([1, OP_DROP] * 15 + [1]))] - tx_hex = txToHex(tx) + tx_hex = tx.serialize().hex() txid = self.nodes[0].sendrawtransaction(tx_hex, 0) chain_txids.append(txid) prevout = COutPoint(int(txid, 16), 0) # Whether the double-spend is allowed is evaluated by including all - # child fees - 40 BTC - so this attempt is rejected. + # child fees - 4 BTC - so this attempt is rejected. dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] - dbl_tx.vout = [CTxOut(initial_nValue - 30 * COIN, DUMMY_P2WPKH_SCRIPT)] - dbl_tx_hex = txToHex(dbl_tx) + dbl_tx.vout = [CTxOut(initial_nValue - 3 * COIN, DUMMY_P2WPKH_SCRIPT)] + dbl_tx_hex = dbl_tx.serialize().hex() # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) @@ -195,8 +175,8 @@ class ReplaceByFeeTest(BitcoinTestFramework): # Accepted with sufficient fee dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] - dbl_tx.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] - dbl_tx_hex = txToHex(dbl_tx) + dbl_tx.vout = [CTxOut(int(0.1 * COIN), DUMMY_P2WPKH_SCRIPT)] + dbl_tx_hex = dbl_tx.serialize().hex() self.nodes[0].sendrawtransaction(dbl_tx_hex, 0) mempool = self.nodes[0].getrawmempool() @@ -206,10 +186,10 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_doublespend_tree(self): """Doublespend of a big tree of transactions""" - initial_nValue = 50*COIN - tx0_outpoint = make_utxo(self.nodes[0], initial_nValue) + initial_nValue = 5 * COIN + tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) - def branch(prevout, initial_value, max_txs, tree_width=5, fee=0.0001*COIN, _total_txs=None): + def branch(prevout, initial_value, max_txs, tree_width=5, fee=0.00001 * COIN, _total_txs=None): if _total_txs is None: _total_txs = [0] if _total_txs[0] >= max_txs: @@ -224,7 +204,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): tx = CTransaction() tx.vin = [CTxIn(prevout, nSequence=0)] tx.vout = vout - tx_hex = txToHex(tx) + tx_hex = tx.serialize().hex() assert len(tx.serialize()) < 100000 txid = self.nodes[0].sendrawtransaction(tx_hex, 0) @@ -240,7 +220,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): _total_txs=_total_txs): yield x - fee = int(0.0001*COIN) + fee = int(0.00001 * COIN) n = MAX_REPLACEMENT_LIMIT tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) assert_equal(len(tree_txs), n) @@ -249,15 +229,15 @@ class ReplaceByFeeTest(BitcoinTestFramework): dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] dbl_tx.vout = [CTxOut(initial_nValue - fee * n, DUMMY_P2WPKH_SCRIPT)] - dbl_tx_hex = txToHex(dbl_tx) + dbl_tx_hex = dbl_tx.serialize().hex() # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) - # 1 BTC fee is enough + # 0.1 BTC fee is enough dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] - dbl_tx.vout = [CTxOut(initial_nValue - fee * n - 1 * COIN, DUMMY_P2WPKH_SCRIPT)] - dbl_tx_hex = txToHex(dbl_tx) + dbl_tx.vout = [CTxOut(initial_nValue - fee * n - int(0.1 * COIN), DUMMY_P2WPKH_SCRIPT)] + dbl_tx_hex = dbl_tx.serialize().hex() self.nodes[0].sendrawtransaction(dbl_tx_hex, 0) mempool = self.nodes[0].getrawmempool() @@ -268,16 +248,16 @@ class ReplaceByFeeTest(BitcoinTestFramework): # Try again, but with more total transactions than the "max txs # double-spent at once" anti-DoS limit. - for n in (MAX_REPLACEMENT_LIMIT+1, MAX_REPLACEMENT_LIMIT*2): - fee = int(0.0001*COIN) - tx0_outpoint = make_utxo(self.nodes[0], initial_nValue) + for n in (MAX_REPLACEMENT_LIMIT + 1, MAX_REPLACEMENT_LIMIT * 2): + fee = int(0.00001 * COIN) + tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) assert_equal(len(tree_txs), n) dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] dbl_tx.vout = [CTxOut(initial_nValue - 2 * fee * n, DUMMY_P2WPKH_SCRIPT)] - dbl_tx_hex = txToHex(dbl_tx) + dbl_tx_hex = dbl_tx.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) @@ -287,33 +267,33 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_replacement_feeperkb(self): """Replacement requires fee-per-KB to be higher""" - tx0_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx1a = CTransaction() tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0)] tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] - tx1a_hex = txToHex(tx1a) + tx1a_hex = tx1a.serialize().hex() self.nodes[0].sendrawtransaction(tx1a_hex, 0) # Higher fee, but the fee per KB is much lower, so the replacement is # rejected. tx1b = CTransaction() tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] - tx1b.vout = [CTxOut(int(0.001*COIN), CScript([b'a'*999000]))] - tx1b_hex = txToHex(tx1b) + tx1b.vout = [CTxOut(int(0.001 * COIN), CScript([b'a' * 999000]))] + tx1b_hex = tx1b.serialize().hex() # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) def test_spends_of_conflicting_outputs(self): """Replacements that spend conflicting tx outputs are rejected""" - utxo1 = make_utxo(self.nodes[0], int(1.2*COIN)) - utxo2 = make_utxo(self.nodes[0], 3*COIN) + utxo1 = self.make_utxo(self.nodes[0], int(1.2 * COIN)) + utxo2 = self.make_utxo(self.nodes[0], 3 * COIN) tx1a = CTransaction() tx1a.vin = [CTxIn(utxo1, nSequence=0)] tx1a.vout = [CTxOut(int(1.1 * COIN), DUMMY_P2WPKH_SCRIPT)] - tx1a_hex = txToHex(tx1a) + tx1a_hex = tx1a.serialize().hex() tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) tx1a_txid = int(tx1a_txid, 16) @@ -323,7 +303,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): tx2.vin = [CTxIn(utxo1, nSequence=0), CTxIn(utxo2, nSequence=0)] tx2.vin.append(CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)) tx2.vout = tx1a.vout - tx2_hex = txToHex(tx2) + tx2_hex = tx2.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0) @@ -332,7 +312,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): tx1b = CTransaction() tx1b.vin = [CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)] tx1b.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] - tx1b_hex = txToHex(tx1b) + tx1b_hex = tx1b.serialize().hex() tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) tx1b_txid = int(tx1b_txid, 16) @@ -340,26 +320,26 @@ class ReplaceByFeeTest(BitcoinTestFramework): tx2.vin = [CTxIn(utxo1, nSequence=0), CTxIn(utxo2, nSequence=0), CTxIn(COutPoint(tx1b_txid, 0))] tx2.vout = tx1a.vout - tx2_hex = txToHex(tx2) + tx2_hex = tx2.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0) def test_new_unconfirmed_inputs(self): """Replacements that add new unconfirmed inputs are rejected""" - confirmed_utxo = make_utxo(self.nodes[0], int(1.1*COIN)) - unconfirmed_utxo = make_utxo(self.nodes[0], int(0.1*COIN), False) + confirmed_utxo = self.make_utxo(self.nodes[0], int(1.1 * COIN)) + unconfirmed_utxo = self.make_utxo(self.nodes[0], int(0.1 * COIN), False) tx1 = CTransaction() tx1.vin = [CTxIn(confirmed_utxo)] tx1.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] - tx1_hex = txToHex(tx1) + tx1_hex = tx1.serialize().hex() self.nodes[0].sendrawtransaction(tx1_hex, 0) tx2 = CTransaction() tx2.vin = [CTxIn(confirmed_utxo), CTxIn(unconfirmed_utxo)] tx2.vout = tx1.vout - tx2_hex = txToHex(tx2) + tx2_hex = tx2.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "replacement-adds-unconfirmed", self.nodes[0].sendrawtransaction, tx2_hex, 0) @@ -370,42 +350,42 @@ class ReplaceByFeeTest(BitcoinTestFramework): # transactions # Start by creating a single transaction with many outputs - initial_nValue = 10*COIN - utxo = make_utxo(self.nodes[0], initial_nValue) - fee = int(0.0001*COIN) - split_value = int((initial_nValue-fee)/(MAX_REPLACEMENT_LIMIT+1)) + initial_nValue = 10 * COIN + utxo = self.make_utxo(self.nodes[0], initial_nValue) + fee = int(0.0001 * COIN) + split_value = int((initial_nValue - fee) / (MAX_REPLACEMENT_LIMIT + 1)) outputs = [] - for _ in range(MAX_REPLACEMENT_LIMIT+1): + for _ in range(MAX_REPLACEMENT_LIMIT + 1): outputs.append(CTxOut(split_value, CScript([1]))) splitting_tx = CTransaction() splitting_tx.vin = [CTxIn(utxo, nSequence=0)] splitting_tx.vout = outputs - splitting_tx_hex = txToHex(splitting_tx) + splitting_tx_hex = splitting_tx.serialize().hex() txid = self.nodes[0].sendrawtransaction(splitting_tx_hex, 0) txid = int(txid, 16) # Now spend each of those outputs individually - for i in range(MAX_REPLACEMENT_LIMIT+1): + for i in range(MAX_REPLACEMENT_LIMIT + 1): tx_i = CTransaction() tx_i.vin = [CTxIn(COutPoint(txid, i), nSequence=0)] tx_i.vout = [CTxOut(split_value - fee, DUMMY_P2WPKH_SCRIPT)] - tx_i_hex = txToHex(tx_i) + tx_i_hex = tx_i.serialize().hex() self.nodes[0].sendrawtransaction(tx_i_hex, 0) # Now create doublespend of the whole lot; should fail. # Need a big enough fee to cover all spending transactions and have # a higher fee rate - double_spend_value = (split_value-100*fee)*(MAX_REPLACEMENT_LIMIT+1) + double_spend_value = (split_value - 100 * fee) * (MAX_REPLACEMENT_LIMIT + 1) inputs = [] - for i in range(MAX_REPLACEMENT_LIMIT+1): + for i in range(MAX_REPLACEMENT_LIMIT + 1): inputs.append(CTxIn(COutPoint(txid, i), nSequence=0)) double_tx = CTransaction() double_tx.vin = inputs double_tx.vout = [CTxOut(double_spend_value, CScript([b'a']))] - double_tx_hex = txToHex(double_tx) + double_tx_hex = double_tx.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, double_tx_hex, 0) @@ -414,18 +394,18 @@ class ReplaceByFeeTest(BitcoinTestFramework): double_tx = CTransaction() double_tx.vin = inputs[0:-1] double_tx.vout = [CTxOut(double_spend_value, CScript([b'a']))] - double_tx_hex = txToHex(double_tx) + double_tx_hex = double_tx.serialize().hex() self.nodes[0].sendrawtransaction(double_tx_hex, 0) def test_opt_in(self): """Replacing should only work if orig tx opted in""" - tx0_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) # Create a non-opting in transaction tx1a = CTransaction() - tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0xffffffff)] + tx1a.vin = [CTxIn(tx0_outpoint, nSequence=SEQUENCE_FINAL)] tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] - tx1a_hex = txToHex(tx1a) + tx1a_hex = tx1a.serialize().hex() tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) # This transaction isn't shown as replaceable @@ -435,25 +415,25 @@ class ReplaceByFeeTest(BitcoinTestFramework): tx1b = CTransaction() tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] tx1b.vout = [CTxOut(int(0.9 * COIN), DUMMY_P2WPKH_SCRIPT)] - tx1b_hex = txToHex(tx1b) + tx1b_hex = tx1b.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx1b_hex, 0) - tx1_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) # Create a different non-opting in transaction tx2a = CTransaction() tx2a.vin = [CTxIn(tx1_outpoint, nSequence=0xfffffffe)] tx2a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] - tx2a_hex = txToHex(tx2a) + tx2a_hex = tx2a.serialize().hex() tx2a_txid = self.nodes[0].sendrawtransaction(tx2a_hex, 0) # Still shouldn't be able to double-spend tx2b = CTransaction() tx2b.vin = [CTxIn(tx1_outpoint, nSequence=0)] tx2b.vout = [CTxOut(int(0.9 * COIN), DUMMY_P2WPKH_SCRIPT)] - tx2b_hex = txToHex(tx2b) + tx2b_hex = tx2b.serialize().hex() # This will raise an exception assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx2b_hex, 0) @@ -466,10 +446,10 @@ class ReplaceByFeeTest(BitcoinTestFramework): tx2a_txid = int(tx2a_txid, 16) tx3a = CTransaction() - tx3a.vin = [CTxIn(COutPoint(tx1a_txid, 0), nSequence=0xffffffff), + tx3a.vin = [CTxIn(COutPoint(tx1a_txid, 0), nSequence=SEQUENCE_FINAL), CTxIn(COutPoint(tx2a_txid, 0), nSequence=0xfffffffd)] - tx3a.vout = [CTxOut(int(0.9*COIN), CScript([b'c'])), CTxOut(int(0.9*COIN), CScript([b'd']))] - tx3a_hex = txToHex(tx3a) + tx3a.vout = [CTxOut(int(0.9 * COIN), CScript([b'c'])), CTxOut(int(0.9 * COIN), CScript([b'd']))] + tx3a_hex = tx3a.serialize().hex() tx3a_txid = self.nodes[0].sendrawtransaction(tx3a_hex, 0) @@ -479,12 +459,12 @@ class ReplaceByFeeTest(BitcoinTestFramework): tx3b = CTransaction() tx3b.vin = [CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)] tx3b.vout = [CTxOut(int(0.5 * COIN), DUMMY_P2WPKH_SCRIPT)] - tx3b_hex = txToHex(tx3b) + tx3b_hex = tx3b.serialize().hex() tx3c = CTransaction() tx3c.vin = [CTxIn(COutPoint(tx2a_txid, 0), nSequence=0)] tx3c.vout = [CTxOut(int(0.5 * COIN), DUMMY_P2WPKH_SCRIPT)] - tx3c_hex = txToHex(tx3c) + tx3c_hex = tx3c.serialize().hex() self.nodes[0].sendrawtransaction(tx3b_hex, 0) # If tx3b was accepted, tx3c won't look like a replacement, @@ -496,25 +476,25 @@ class ReplaceByFeeTest(BitcoinTestFramework): # correctly used by replacement logic # 1. Check that feeperkb uses modified fees - tx0_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx1a = CTransaction() tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0)] tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] - tx1a_hex = txToHex(tx1a) + tx1a_hex = tx1a.serialize().hex() tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) # Higher fee, but the actual fee per KB is much lower. tx1b = CTransaction() tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] - tx1b.vout = [CTxOut(int(0.001*COIN), CScript([b'a'*740000]))] - tx1b_hex = txToHex(tx1b) + tx1b.vout = [CTxOut(int(0.001 * COIN), CScript([b'a' * 740000]))] + tx1b_hex = tx1b.serialize().hex() # Verify tx1b cannot replace tx1a. assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) # Use prioritisetransaction to set tx1a's fee to 0. - self.nodes[0].prioritisetransaction(txid=tx1a_txid, fee_delta=int(-0.1*COIN)) + self.nodes[0].prioritisetransaction(txid=tx1a_txid, fee_delta=int(-0.1 * COIN)) # Now tx1b should be able to replace tx1a tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) @@ -522,12 +502,12 @@ class ReplaceByFeeTest(BitcoinTestFramework): assert tx1b_txid in self.nodes[0].getrawmempool() # 2. Check that absolute fee checks use modified fee. - tx1_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx2a = CTransaction() tx2a.vin = [CTxIn(tx1_outpoint, nSequence=0)] tx2a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] - tx2a_hex = txToHex(tx2a) + tx2a_hex = tx2a.serialize().hex() self.nodes[0].sendrawtransaction(tx2a_hex, 0) # Lower fee, but we'll prioritise it @@ -535,13 +515,13 @@ class ReplaceByFeeTest(BitcoinTestFramework): tx2b.vin = [CTxIn(tx1_outpoint, nSequence=0)] tx2b.vout = [CTxOut(int(1.01 * COIN), DUMMY_P2WPKH_SCRIPT)] tx2b.rehash() - tx2b_hex = txToHex(tx2b) + tx2b_hex = tx2b.serialize().hex() # Verify tx2b cannot replace tx2a. assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx2b_hex, 0) # Now prioritise tx2b to have a higher modified fee - self.nodes[0].prioritisetransaction(txid=tx2b.hash, fee_delta=int(0.1*COIN)) + self.nodes[0].prioritisetransaction(txid=tx2b.hash, fee_delta=int(0.1 * COIN)) # tx2b should now be accepted tx2b_txid = self.nodes[0].sendrawtransaction(tx2b_hex, 0) @@ -549,24 +529,98 @@ class ReplaceByFeeTest(BitcoinTestFramework): assert tx2b_txid in self.nodes[0].getrawmempool() def test_rpc(self): - us0 = self.nodes[0].listunspent()[0] + us0 = self.wallet.get_utxo() ins = [us0] - outs = {self.nodes[0].getnewaddress() : Decimal(1.0000000)} + outs = {ADDRESS_BCRT1_UNSPENDABLE: Decimal(1.0000000)} rawtx0 = self.nodes[0].createrawtransaction(ins, outs, 0, True) rawtx1 = self.nodes[0].createrawtransaction(ins, outs, 0, False) - json0 = self.nodes[0].decoderawtransaction(rawtx0) - json1 = self.nodes[0].decoderawtransaction(rawtx1) + json0 = self.nodes[0].decoderawtransaction(rawtx0) + json1 = self.nodes[0].decoderawtransaction(rawtx1) assert_equal(json0["vin"][0]["sequence"], 4294967293) assert_equal(json1["vin"][0]["sequence"], 4294967295) - rawtx2 = self.nodes[0].createrawtransaction([], outs) - frawtx2a = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": True}) - frawtx2b = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": False}) - - json0 = self.nodes[0].decoderawtransaction(frawtx2a['hex']) - json1 = self.nodes[0].decoderawtransaction(frawtx2b['hex']) - assert_equal(json0["vin"][0]["sequence"], 4294967293) - assert_equal(json1["vin"][0]["sequence"], 4294967294) + if self.is_specified_wallet_compiled(): + self.init_wallet(node=0) + rawtx2 = self.nodes[0].createrawtransaction([], outs) + frawtx2a = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": True}) + frawtx2b = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": False}) + + json0 = self.nodes[0].decoderawtransaction(frawtx2a['hex']) + json1 = self.nodes[0].decoderawtransaction(frawtx2b['hex']) + assert_equal(json0["vin"][0]["sequence"], 4294967293) + assert_equal(json1["vin"][0]["sequence"], 4294967294) + + def test_no_inherited_signaling(self): + confirmed_utxo = self.wallet.get_utxo() + + # Create an explicitly opt-in parent transaction + optin_parent_tx = self.wallet.send_self_transfer( + from_node=self.nodes[0], + utxo_to_spend=confirmed_utxo, + sequence=BIP125_SEQUENCE_NUMBER, + fee_rate=Decimal('0.01'), + ) + assert_equal(True, self.nodes[0].getmempoolentry(optin_parent_tx['txid'])['bip125-replaceable']) + + replacement_parent_tx = self.wallet.create_self_transfer( + utxo_to_spend=confirmed_utxo, + sequence=BIP125_SEQUENCE_NUMBER, + fee_rate=Decimal('0.02'), + ) + + # Test if parent tx can be replaced. + res = self.nodes[0].testmempoolaccept(rawtxs=[replacement_parent_tx['hex']])[0] + + # Parent can be replaced. + assert_equal(res['allowed'], True) + + # Create an opt-out child tx spending the opt-in parent + parent_utxo = self.wallet.get_utxo(txid=optin_parent_tx['txid']) + optout_child_tx = self.wallet.send_self_transfer( + from_node=self.nodes[0], + utxo_to_spend=parent_utxo, + sequence=SEQUENCE_FINAL, + fee_rate=Decimal('0.01'), + ) + + # Reports true due to inheritance + assert_equal(True, self.nodes[0].getmempoolentry(optout_child_tx['txid'])['bip125-replaceable']) + + replacement_child_tx = self.wallet.create_self_transfer( + utxo_to_spend=parent_utxo, + sequence=SEQUENCE_FINAL, + fee_rate=Decimal('0.02'), + mempool_valid=False, + ) + + # Broadcast replacement child tx + # BIP 125 : + # 1. The original transactions signal replaceability explicitly or through inheritance as described in the above + # Summary section. + # The original transaction (`optout_child_tx`) doesn't signal RBF but its parent (`optin_parent_tx`) does. + # The replacement transaction (`replacement_child_tx`) should be able to replace the original transaction. + # See CVE-2021-31876 for further explanations. + assert_equal(True, self.nodes[0].getmempoolentry(optin_parent_tx['txid'])['bip125-replaceable']) + assert_raises_rpc_error(-26, 'txn-mempool-conflict', self.nodes[0].sendrawtransaction, replacement_child_tx["hex"], 0) + + self.log.info('Check that the child tx can still be replaced (via a tx that also replaces the parent)') + replacement_parent_tx = self.wallet.send_self_transfer( + from_node=self.nodes[0], + utxo_to_spend=confirmed_utxo, + sequence=SEQUENCE_FINAL, + fee_rate=Decimal('0.03'), + ) + # Check that child is removed and update wallet utxo state + assert_raises_rpc_error(-5, 'Transaction not in mempool', self.nodes[0].getmempoolentry, optout_child_tx['txid']) + self.wallet.get_utxo(txid=optout_child_tx['txid']) + + def test_replacement_relay_fee(self): + tx = self.wallet.send_self_transfer(from_node=self.nodes[0])['tx'] + + # Higher fee, higher feerate, different txid, but the replacement does not provide a relay + # fee conforming to node's `incrementalrelayfee` policy of 1000 sat per KB. + tx.vout[0].nValue -= 1 + assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex()) if __name__ == '__main__': ReplaceByFeeTest().main() diff --git a/test/functional/feature_reindex.py b/test/functional/feature_reindex.py index 68585b7475..44040f426f 100755 --- a/test/functional/feature_reindex.py +++ b/test/functional/feature_reindex.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test running bitcoind with -reindex and -reindex-chainstate options. @@ -19,7 +19,7 @@ class ReindexTest(BitcoinTestFramework): self.num_nodes = 1 def reindex(self, justchainstate=False): - self.nodes[0].generatetoaddress(3, self.nodes[0].get_deterministic_priv_key().address) + self.generatetoaddress(self.nodes[0], 3, self.nodes[0].get_deterministic_priv_key().address) blockcount = self.nodes[0].getblockcount() self.stop_nodes() extra_args = [["-reindex-chainstate" if justchainstate else "-reindex"]] diff --git a/test/functional/feature_segwit.py b/test/functional/feature_segwit.py index 7bd2fc7847..6d7f1def88 100755 --- a/test/functional/feature_segwit.py +++ b/test/functional/feature_segwit.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the SegWit changeover logic.""" from decimal import Decimal -from io import BytesIO from test_framework.address import ( key_to_p2pkh, @@ -14,37 +13,69 @@ from test_framework.address import ( script_to_p2sh_p2wsh, script_to_p2wsh, ) -from test_framework.blocktools import witness_script, send_to_witness -from test_framework.messages import COIN, COutPoint, CTransaction, CTxIn, CTxOut, FromHex, sha256, ToHex -from test_framework.script import CScript, OP_HASH160, OP_CHECKSIG, OP_0, hash160, OP_EQUAL, OP_DUP, OP_EQUALVERIFY, OP_1, OP_2, OP_CHECKMULTISIG, OP_TRUE, OP_DROP +from test_framework.blocktools import ( + send_to_witness, + witness_script, +) +from test_framework.descriptors import descsum_create +from test_framework.messages import ( + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxOut, + tx_from_hex, +) +from test_framework.script import ( + CScript, + OP_0, + OP_1, + OP_DROP, + OP_TRUE, +) +from test_framework.script_util import ( + key_to_p2pk_script, + key_to_p2pkh_script, + key_to_p2wpkh_script, + keys_to_multisig_script, + script_to_p2sh_script, + script_to_p2wsh_script, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than_or_equal, assert_is_hex_string, assert_raises_rpc_error, - hex_str_to_bytes, try_rpc, ) +from test_framework.wallet_util import ( + get_generate_key, +) NODE_0 = 0 NODE_2 = 2 P2WPKH = 0 P2WSH = 1 + def getutxo(txid): utxo = {} utxo["vout"] = 0 utxo["txid"] = txid return utxo + def find_spendable_utxo(node, min_value): for utxo in node.listunspent(query_options={'minimumAmount': min_value}): if utxo['spendable']: return utxo - raise AssertionError("Unspent output equal or higher than %s not found" % min_value) + raise AssertionError(f"Unspent output equal or higher than {min_value} not found") + + +txs_mined = {} # txindex from txid to blockhash -txs_mined = {} # txindex from txid to blockhash class SegWitTest(BitcoinTestFramework): def set_test_params(self): @@ -55,18 +86,18 @@ class SegWitTest(BitcoinTestFramework): [ "-acceptnonstdtxn=1", "-rpcserialversion=0", - "-segwitheight=432", + "-testactivationheight=segwit@432", "-addresstype=legacy", ], [ "-acceptnonstdtxn=1", "-rpcserialversion=1", - "-segwitheight=432", + "-testactivationheight=segwit@432", "-addresstype=legacy", ], [ "-acceptnonstdtxn=1", - "-segwitheight=432", + "-testactivationheight=segwit@432", "-addresstype=legacy", ], ] @@ -82,13 +113,13 @@ class SegWitTest(BitcoinTestFramework): def success_mine(self, node, txid, sign, redeem_script=""): send_to_witness(1, node, getutxo(txid), self.pubkey[0], False, Decimal("49.998"), sign, redeem_script) - block = node.generate(1) + block = self.generate(node, 1) assert_equal(len(node.getblock(block[0])["tx"]), 2) self.sync_blocks() def skip_mine(self, node, txid, sign, redeem_script=""): send_to_witness(1, node, getutxo(txid), self.pubkey[0], False, Decimal("49.998"), sign, redeem_script) - block = node.generate(1) + block = self.generate(node, 1) assert_equal(len(node.getblock(block[0])["tx"]), 1) self.sync_blocks() @@ -96,31 +127,57 @@ class SegWitTest(BitcoinTestFramework): assert_raises_rpc_error(-26, error_msg, send_to_witness, use_p2wsh=1, node=node, utxo=getutxo(txid), pubkey=self.pubkey[0], encode_p2sh=False, amount=Decimal("49.998"), sign=sign, insert_redeem_script=redeem_script) def run_test(self): - self.nodes[0].generate(161) # block 161 + self.generate(self.nodes[0], 161) # block 161 self.log.info("Verify sigops are counted in GBT with pre-BIP141 rules before the fork") txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) tmpl = self.nodes[0].getblocktemplate({'rules': ['segwit']}) - assert tmpl['sizelimit'] == 1000000 + assert_equal(tmpl['sizelimit'], 1000000) assert 'weightlimit' not in tmpl - assert tmpl['sigoplimit'] == 20000 - assert tmpl['transactions'][0]['hash'] == txid - assert tmpl['transactions'][0]['sigops'] == 2 + assert_equal(tmpl['sigoplimit'], 20000) + assert_equal(tmpl['transactions'][0]['hash'], txid) + assert_equal(tmpl['transactions'][0]['sigops'], 2) assert '!segwit' not in tmpl['rules'] - self.nodes[0].generate(1) # block 162 + self.generate(self.nodes[0], 1) # block 162 balance_presetup = self.nodes[0].getbalance() self.pubkey = [] - p2sh_ids = [] # p2sh_ids[NODE][TYPE] is an array of txids that spend to P2WPKH (TYPE=0) or P2WSH (TYPE=1) scripts to an address for NODE embedded in p2sh - wit_ids = [] # wit_ids[NODE][TYPE] is an array of txids that spend to P2WPKH (TYPE=0) or P2WSH (TYPE=1) scripts to an address for NODE via bare witness + p2sh_ids = [] # p2sh_ids[NODE][TYPE] is an array of txids that spend to P2WPKH (TYPE=0) or P2WSH (TYPE=1) scripts to an address for NODE embedded in p2sh + wit_ids = [] # wit_ids[NODE][TYPE] is an array of txids that spend to P2WPKH (TYPE=0) or P2WSH (TYPE=1) scripts to an address for NODE via bare witness for i in range(3): - newaddress = self.nodes[i].getnewaddress() - self.pubkey.append(self.nodes[i].getaddressinfo(newaddress)["pubkey"]) - multiscript = CScript([OP_1, hex_str_to_bytes(self.pubkey[-1]), OP_1, OP_CHECKMULTISIG]) - p2sh_ms_addr = self.nodes[i].addmultisigaddress(1, [self.pubkey[-1]], '', 'p2sh-segwit')['address'] - bip173_ms_addr = self.nodes[i].addmultisigaddress(1, [self.pubkey[-1]], '', 'bech32')['address'] + key = get_generate_key() + self.pubkey.append(key.pubkey) + + multiscript = keys_to_multisig_script([self.pubkey[-1]]) + p2sh_ms_addr = self.nodes[i].createmultisig(1, [self.pubkey[-1]], 'p2sh-segwit')['address'] + bip173_ms_addr = self.nodes[i].createmultisig(1, [self.pubkey[-1]], 'bech32')['address'] assert_equal(p2sh_ms_addr, script_to_p2sh_p2wsh(multiscript)) assert_equal(bip173_ms_addr, script_to_p2wsh(multiscript)) + + p2sh_ms_desc = descsum_create(f"sh(wsh(multi(1,{key.privkey})))") + bip173_ms_desc = descsum_create(f"wsh(multi(1,{key.privkey}))") + assert_equal(self.nodes[i].deriveaddresses(p2sh_ms_desc)[0], p2sh_ms_addr) + assert_equal(self.nodes[i].deriveaddresses(bip173_ms_desc)[0], bip173_ms_addr) + + sh_wpkh_desc = descsum_create(f"sh(wpkh({key.privkey}))") + wpkh_desc = descsum_create(f"wpkh({key.privkey})") + assert_equal(self.nodes[i].deriveaddresses(sh_wpkh_desc)[0], key.p2sh_p2wpkh_addr) + assert_equal(self.nodes[i].deriveaddresses(wpkh_desc)[0], key.p2wpkh_addr) + + if self.options.descriptors: + res = self.nodes[i].importdescriptors([ + {"desc": p2sh_ms_desc, "timestamp": "now"}, + {"desc": bip173_ms_desc, "timestamp": "now"}, + {"desc": sh_wpkh_desc, "timestamp": "now"}, + {"desc": wpkh_desc, "timestamp": "now"}, + ]) + else: + # The nature of the legacy wallet is that this import results in also adding all of the necessary scripts + res = self.nodes[i].importmulti([ + {"desc": p2sh_ms_desc, "timestamp": "now"}, + ]) + assert all([r["success"] for r in res]) + p2sh_ids.append([]) wit_ids.append([]) for _ in range(2): @@ -133,16 +190,14 @@ class SegWitTest(BitcoinTestFramework): wit_ids[n][v].append(send_to_witness(v, self.nodes[0], find_spendable_utxo(self.nodes[0], 50), self.pubkey[n], False, Decimal("49.999"))) p2sh_ids[n][v].append(send_to_witness(v, self.nodes[0], find_spendable_utxo(self.nodes[0], 50), self.pubkey[n], True, Decimal("49.999"))) - self.nodes[0].generate(1) # block 163 - self.sync_blocks() + self.generate(self.nodes[0], 1) # block 163 # Make sure all nodes recognize the transactions as theirs assert_equal(self.nodes[0].getbalance(), balance_presetup - 60 * 50 + 20 * Decimal("49.999") + 50) assert_equal(self.nodes[1].getbalance(), 20 * Decimal("49.999")) assert_equal(self.nodes[2].getbalance(), 20 * Decimal("49.999")) - self.nodes[0].generate(260) # block 423 - self.sync_blocks() + self.generate(self.nodes[0], 260) # block 423 self.log.info("Verify witness txs are skipped for mining before the fork") self.skip_mine(self.nodes[2], wit_ids[NODE_2][P2WPKH][0], True) # block 424 @@ -154,12 +209,11 @@ class SegWitTest(BitcoinTestFramework): self.fail_accept(self.nodes[2], "mandatory-script-verify-flag-failed (Operation not valid with the current stack size)", p2sh_ids[NODE_2][P2WPKH][1], sign=False) self.fail_accept(self.nodes[2], "mandatory-script-verify-flag-failed (Operation not valid with the current stack size)", p2sh_ids[NODE_2][P2WSH][1], sign=False) - self.nodes[2].generate(4) # blocks 428-431 + self.generate(self.nodes[2], 4) # blocks 428-431 self.log.info("Verify previous witness txs skipped for mining can now be mined") assert_equal(len(self.nodes[2].getrawmempool()), 4) - blockhash = self.nodes[2].generate(1)[0] # block 432 (first block with new rules; 432 = 144 * 3) - self.sync_blocks() + blockhash = self.generate(self.nodes[2], 1)[0] # block 432 (first block with new rules; 432 = 144 * 3) assert_equal(len(self.nodes[2].getrawmempool()), 0) segwit_tx_list = self.nodes[2].getblock(blockhash)["tx"] assert_equal(len(segwit_tx_list), 5) @@ -179,7 +233,7 @@ class SegWitTest(BitcoinTestFramework): assert self.nodes[1].getblock(blockhash, False) == self.nodes[2].getblock(blockhash, False) for tx_id in segwit_tx_list: - tx = FromHex(CTransaction(), self.nodes[2].gettransaction(tx_id)["hex"]) + tx = tx_from_hex(self.nodes[2].gettransaction(tx_id)["hex"]) assert self.nodes[2].getrawtransaction(tx_id, False, blockhash) != self.nodes[0].getrawtransaction(tx_id, False, blockhash) assert self.nodes[1].getrawtransaction(tx_id, False, blockhash) == self.nodes[2].getrawtransaction(tx_id, False, blockhash) assert self.nodes[0].getrawtransaction(tx_id, False, blockhash) != self.nodes[2].gettransaction(tx_id)["hex"] @@ -192,7 +246,7 @@ class SegWitTest(BitcoinTestFramework): witnesses = coinbase_tx["decoded"]["vin"][0]["txinwitness"] assert_equal(len(witnesses), 1) assert_is_hex_string(witnesses[0]) - assert_equal(witnesses[0], '00'*32) + assert_equal(witnesses[0], '00' * 32) self.log.info("Verify witness txs without witness data are invalid after the fork") self.fail_accept(self.nodes[2], 'non-mandatory-script-verify-flag (Witness program hash mismatch)', wit_ids[NODE_2][P2WPKH][2], sign=False) @@ -208,15 +262,17 @@ class SegWitTest(BitcoinTestFramework): self.log.info("Verify sigops are counted in GBT with BIP141 rules after the fork") txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) + raw_tx = self.nodes[0].getrawtransaction(txid, True) tmpl = self.nodes[0].getblocktemplate({'rules': ['segwit']}) - assert tmpl['sizelimit'] >= 3999577 # actual maximum size is lower due to minimum mandatory non-witness data - assert tmpl['weightlimit'] == 4000000 - assert tmpl['sigoplimit'] == 80000 - assert tmpl['transactions'][0]['txid'] == txid - assert tmpl['transactions'][0]['sigops'] == 8 + assert_greater_than_or_equal(tmpl['sizelimit'], 3999577) # actual maximum size is lower due to minimum mandatory non-witness data + assert_equal(tmpl['weightlimit'], 4000000) + assert_equal(tmpl['sigoplimit'], 80000) + assert_equal(tmpl['transactions'][0]['txid'], txid) + expected_sigops = 9 if 'txinwitness' in raw_tx["vin"][0] else 8 + assert_equal(tmpl['transactions'][0]['sigops'], expected_sigops) assert '!segwit' in tmpl['rules'] - self.nodes[0].generate(1) # Mine a block to clear the gbt cache + self.generate(self.nodes[0], 1) # Mine a block to clear the gbt cache self.log.info("Non-segwit miners are able to use GBT response after activation.") # Create a 3-tx chain: tx1 (non-segwit input, paying to a segwit output) -> @@ -225,42 +281,42 @@ class SegWitTest(BitcoinTestFramework): # tx1 is allowed to appear in the block, but no others. txid1 = send_to_witness(1, self.nodes[0], find_spendable_utxo(self.nodes[0], 50), self.pubkey[0], False, Decimal("49.996")) hex_tx = self.nodes[0].gettransaction(txid)['hex'] - tx = FromHex(CTransaction(), hex_tx) + tx = tx_from_hex(hex_tx) assert tx.wit.is_null() # This should not be a segwit input assert txid1 in self.nodes[0].getrawmempool() tx1_hex = self.nodes[0].gettransaction(txid1)['hex'] - tx1 = FromHex(CTransaction(), tx1_hex) + tx1 = tx_from_hex(tx1_hex) # Check that wtxid is properly reported in mempool entry (txid1) assert_equal(int(self.nodes[0].getmempoolentry(txid1)["wtxid"], 16), tx1.calc_sha256(True)) # Check that weight and vsize are properly reported in mempool entry (txid1) - assert_equal(self.nodes[0].getmempoolentry(txid1)["vsize"], (self.nodes[0].getmempoolentry(txid1)["weight"] + 3) // 4) - assert_equal(self.nodes[0].getmempoolentry(txid1)["weight"], len(tx1.serialize_without_witness())*3 + len(tx1.serialize_with_witness())) + assert_equal(self.nodes[0].getmempoolentry(txid1)["vsize"], tx1.get_vsize()) + assert_equal(self.nodes[0].getmempoolentry(txid1)["weight"], tx1.get_weight()) # Now create tx2, which will spend from txid1. tx = CTransaction() tx.vin.append(CTxIn(COutPoint(int(txid1, 16), 0), b'')) tx.vout.append(CTxOut(int(49.99 * COIN), CScript([OP_TRUE, OP_DROP] * 15 + [OP_TRUE]))) - tx2_hex = self.nodes[0].signrawtransactionwithwallet(ToHex(tx))['hex'] + tx2_hex = self.nodes[0].signrawtransactionwithwallet(tx.serialize().hex())['hex'] txid2 = self.nodes[0].sendrawtransaction(tx2_hex) - tx = FromHex(CTransaction(), tx2_hex) + tx = tx_from_hex(tx2_hex) assert not tx.wit.is_null() # Check that wtxid is properly reported in mempool entry (txid2) assert_equal(int(self.nodes[0].getmempoolentry(txid2)["wtxid"], 16), tx.calc_sha256(True)) # Check that weight and vsize are properly reported in mempool entry (txid2) - assert_equal(self.nodes[0].getmempoolentry(txid2)["vsize"], (self.nodes[0].getmempoolentry(txid2)["weight"] + 3) // 4) - assert_equal(self.nodes[0].getmempoolentry(txid2)["weight"], len(tx.serialize_without_witness())*3 + len(tx.serialize_with_witness())) + assert_equal(self.nodes[0].getmempoolentry(txid2)["vsize"], tx.get_vsize()) + assert_equal(self.nodes[0].getmempoolentry(txid2)["weight"], tx.get_weight()) # Now create tx3, which will spend from txid2 tx = CTransaction() tx.vin.append(CTxIn(COutPoint(int(txid2, 16), 0), b"")) tx.vout.append(CTxOut(int(49.95 * COIN), CScript([OP_TRUE, OP_DROP] * 15 + [OP_TRUE]))) # Huge fee tx.calc_sha256() - txid3 = self.nodes[0].sendrawtransaction(hexstring=ToHex(tx), maxfeerate=0) + txid3 = self.nodes[0].sendrawtransaction(hexstring=tx.serialize().hex(), maxfeerate=0) assert tx.wit.is_null() assert txid3 in self.nodes[0].getrawmempool() @@ -275,290 +331,291 @@ class SegWitTest(BitcoinTestFramework): assert_equal(int(self.nodes[0].getmempoolentry(txid3)["wtxid"], 16), tx.calc_sha256(True)) # Check that weight and vsize are properly reported in mempool entry (txid3) - assert_equal(self.nodes[0].getmempoolentry(txid3)["vsize"], (self.nodes[0].getmempoolentry(txid3)["weight"] + 3) // 4) - assert_equal(self.nodes[0].getmempoolentry(txid3)["weight"], len(tx.serialize_without_witness())*3 + len(tx.serialize_with_witness())) + assert_equal(self.nodes[0].getmempoolentry(txid3)["vsize"], tx.get_vsize()) + assert_equal(self.nodes[0].getmempoolentry(txid3)["weight"], tx.get_weight()) # Mine a block to clear the gbt cache again. - self.nodes[0].generate(1) - - self.log.info("Verify behaviour of importaddress and listunspent") - - # Some public keys to be used later - pubkeys = [ - "0363D44AABD0F1699138239DF2F042C3282C0671CC7A76826A55C8203D90E39242", # cPiM8Ub4heR9NBYmgVzJQiUH1if44GSBGiqaeJySuL2BKxubvgwb - "02D3E626B3E616FC8662B489C123349FECBFC611E778E5BE739B257EAE4721E5BF", # cPpAdHaD6VoYbW78kveN2bsvb45Q7G5PhaPApVUGwvF8VQ9brD97 - "04A47F2CBCEFFA7B9BCDA184E7D5668D3DA6F9079AD41E422FA5FD7B2D458F2538A62F5BD8EC85C2477F39650BD391EA6250207065B2A81DA8B009FC891E898F0E", # 91zqCU5B9sdWxzMt1ca3VzbtVm2YM6Hi5Rxn4UDtxEaN9C9nzXV - "02A47F2CBCEFFA7B9BCDA184E7D5668D3DA6F9079AD41E422FA5FD7B2D458F2538", # cPQFjcVRpAUBG8BA9hzr2yEzHwKoMgLkJZBBtK9vJnvGJgMjzTbd - "036722F784214129FEB9E8129D626324F3F6716555B603FFE8300BBCB882151228", # cQGtcm34xiLjB1v7bkRa4V3aAc9tS2UTuBZ1UnZGeSeNy627fN66 - "0266A8396EE936BF6D99D17920DB21C6C7B1AB14C639D5CD72B300297E416FD2EC", # cTW5mR5M45vHxXkeChZdtSPozrFwFgmEvTNnanCW6wrqwaCZ1X7K - "0450A38BD7F0AC212FEBA77354A9B036A32E0F7C81FC4E0C5ADCA7C549C4505D2522458C2D9AE3CEFD684E039194B72C8A10F9CB9D4764AB26FCC2718D421D3B84", # 92h2XPssjBpsJN5CqSP7v9a7cf2kgDunBC6PDFwJHMACM1rrVBJ - ] - - # Import a compressed key and an uncompressed key, generate some multisig addresses - self.nodes[0].importprivkey("92e6XLo5jVAVwrQKPNTs93oQco8f8sDNBcpv73Dsrs397fQtFQn") - uncompressed_spendable_address = ["mvozP4UwyGD2mGZU4D2eMvMLPB9WkMmMQu"] - self.nodes[0].importprivkey("cNC8eQ5dg3mFAVePDX4ddmPYpPbw41r9bm2jd1nLJT77e6RrzTRR") - compressed_spendable_address = ["mmWQubrDomqpgSYekvsU7HWEVjLFHAakLe"] - assert not self.nodes[0].getaddressinfo(uncompressed_spendable_address[0])['iscompressed'] - assert self.nodes[0].getaddressinfo(compressed_spendable_address[0])['iscompressed'] - - self.nodes[0].importpubkey(pubkeys[0]) - compressed_solvable_address = [key_to_p2pkh(pubkeys[0])] - self.nodes[0].importpubkey(pubkeys[1]) - compressed_solvable_address.append(key_to_p2pkh(pubkeys[1])) - self.nodes[0].importpubkey(pubkeys[2]) - uncompressed_solvable_address = [key_to_p2pkh(pubkeys[2])] - - spendable_anytime = [] # These outputs should be seen anytime after importprivkey and addmultisigaddress - spendable_after_importaddress = [] # These outputs should be seen after importaddress - solvable_after_importaddress = [] # These outputs should be seen after importaddress but not spendable - unsolvable_after_importaddress = [] # These outputs should be unsolvable after importaddress - solvable_anytime = [] # These outputs should be solvable after importpubkey - unseen_anytime = [] # These outputs should never be seen - - uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], compressed_spendable_address[0]])['address']) - uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], uncompressed_spendable_address[0]])['address']) - compressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_spendable_address[0]])['address']) - uncompressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], uncompressed_solvable_address[0]])['address']) - compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_solvable_address[0]])['address']) - compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_solvable_address[0], compressed_solvable_address[1]])['address']) - - # Test multisig_without_privkey - # We have 2 public keys without private keys, use addmultisigaddress to add to wallet. - # Money sent to P2SH of multisig of this should only be seen after importaddress with the BASE58 P2SH address. - - multisig_without_privkey_address = self.nodes[0].addmultisigaddress(2, [pubkeys[3], pubkeys[4]])['address'] - script = CScript([OP_2, hex_str_to_bytes(pubkeys[3]), hex_str_to_bytes(pubkeys[4]), OP_2, OP_CHECKMULTISIG]) - solvable_after_importaddress.append(CScript([OP_HASH160, hash160(script), OP_EQUAL])) - - for i in compressed_spendable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - # p2sh multisig with compressed keys should always be spendable - spendable_anytime.extend([p2sh]) - # bare multisig can be watched and signed, but is not treated as ours - solvable_after_importaddress.extend([bare]) - # P2WSH and P2SH(P2WSH) multisig with compressed keys are spendable after direct importaddress - spendable_after_importaddress.extend([p2wsh, p2sh_p2wsh]) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # normal P2PKH and P2PK with compressed keys should always be spendable - spendable_anytime.extend([p2pkh, p2pk]) - # P2SH_P2PK, P2SH_P2PKH with compressed keys are spendable after direct importaddress - spendable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) - # P2WPKH and P2SH_P2WPKH with compressed keys should always be spendable - spendable_anytime.extend([p2wpkh, p2sh_p2wpkh]) - - for i in uncompressed_spendable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - # p2sh multisig with uncompressed keys should always be spendable - spendable_anytime.extend([p2sh]) - # bare multisig can be watched and signed, but is not treated as ours - solvable_after_importaddress.extend([bare]) - # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen - unseen_anytime.extend([p2wsh, p2sh_p2wsh]) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # normal P2PKH and P2PK with uncompressed keys should always be spendable - spendable_anytime.extend([p2pkh, p2pk]) - # P2SH_P2PK and P2SH_P2PKH are spendable after direct importaddress - spendable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh]) - # Witness output types with uncompressed keys are never seen - unseen_anytime.extend([p2wpkh, p2sh_p2wpkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) - - for i in compressed_solvable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - # Multisig without private is not seen after addmultisigaddress, but seen after importaddress - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - solvable_after_importaddress.extend([bare, p2sh, p2wsh, p2sh_p2wsh]) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # normal P2PKH, P2PK, P2WPKH and P2SH_P2WPKH with compressed keys should always be seen - solvable_anytime.extend([p2pkh, p2pk, p2wpkh, p2sh_p2wpkh]) - # P2SH_P2PK, P2SH_P2PKH with compressed keys are seen after direct importaddress - solvable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) - - for i in uncompressed_solvable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - # Base uncompressed multisig without private is not seen after addmultisigaddress, but seen after importaddress - solvable_after_importaddress.extend([bare, p2sh]) - # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen - unseen_anytime.extend([p2wsh, p2sh_p2wsh]) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # normal P2PKH and P2PK with uncompressed keys should always be seen - solvable_anytime.extend([p2pkh, p2pk]) - # P2SH_P2PK, P2SH_P2PKH with uncompressed keys are seen after direct importaddress - solvable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh]) - # Witness output types with uncompressed keys are never seen - unseen_anytime.extend([p2wpkh, p2sh_p2wpkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) - - op1 = CScript([OP_1]) - op0 = CScript([OP_0]) - # 2N7MGY19ti4KDMSzRfPAssP6Pxyuxoi6jLe is the P2SH(P2PKH) version of mjoE3sSrb8ByYEvgnC3Aox86u1CHnfJA4V - unsolvable_address_key = hex_str_to_bytes("02341AEC7587A51CDE5279E0630A531AEA2615A9F80B17E8D9376327BAEAA59E3D") - unsolvablep2pkh = CScript([OP_DUP, OP_HASH160, hash160(unsolvable_address_key), OP_EQUALVERIFY, OP_CHECKSIG]) - unsolvablep2wshp2pkh = CScript([OP_0, sha256(unsolvablep2pkh)]) - p2shop0 = CScript([OP_HASH160, hash160(op0), OP_EQUAL]) - p2wshop1 = CScript([OP_0, sha256(op1)]) - unsolvable_after_importaddress.append(unsolvablep2pkh) - unsolvable_after_importaddress.append(unsolvablep2wshp2pkh) - unsolvable_after_importaddress.append(op1) # OP_1 will be imported as script - unsolvable_after_importaddress.append(p2wshop1) - unseen_anytime.append(op0) # OP_0 will be imported as P2SH address with no script provided - unsolvable_after_importaddress.append(p2shop0) - - spendable_txid = [] - solvable_txid = [] - spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime, 2)) - solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime, 1)) - self.mine_and_test_listunspent(spendable_after_importaddress + solvable_after_importaddress + unseen_anytime + unsolvable_after_importaddress, 0) - - importlist = [] - for i in compressed_spendable_address + uncompressed_spendable_address + compressed_solvable_address + uncompressed_solvable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - bare = hex_str_to_bytes(v['hex']) - importlist.append(bare.hex()) - importlist.append(CScript([OP_0, sha256(bare)]).hex()) - else: - pubkey = hex_str_to_bytes(v['pubkey']) - p2pk = CScript([pubkey, OP_CHECKSIG]) - p2pkh = CScript([OP_DUP, OP_HASH160, hash160(pubkey), OP_EQUALVERIFY, OP_CHECKSIG]) - importlist.append(p2pk.hex()) - importlist.append(p2pkh.hex()) - importlist.append(CScript([OP_0, hash160(pubkey)]).hex()) - importlist.append(CScript([OP_0, sha256(p2pk)]).hex()) - importlist.append(CScript([OP_0, sha256(p2pkh)]).hex()) - - importlist.append(unsolvablep2pkh.hex()) - importlist.append(unsolvablep2wshp2pkh.hex()) - importlist.append(op1.hex()) - importlist.append(p2wshop1.hex()) - - for i in importlist: - # import all generated addresses. The wallet already has the private keys for some of these, so catch JSON RPC - # exceptions and continue. - try_rpc(-4, "The wallet already contains the private key for this address or script", self.nodes[0].importaddress, i, "", False, True) - - self.nodes[0].importaddress(script_to_p2sh(op0)) # import OP_0 as address only - self.nodes[0].importaddress(multisig_without_privkey_address) # Test multisig_without_privkey - - spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime + spendable_after_importaddress, 2)) - solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime + solvable_after_importaddress, 1)) - self.mine_and_test_listunspent(unsolvable_after_importaddress, 1) - self.mine_and_test_listunspent(unseen_anytime, 0) - - spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime + spendable_after_importaddress, 2)) - solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime + solvable_after_importaddress, 1)) - self.mine_and_test_listunspent(unsolvable_after_importaddress, 1) - self.mine_and_test_listunspent(unseen_anytime, 0) - - # Repeat some tests. This time we don't add witness scripts with importaddress - # Import a compressed key and an uncompressed key, generate some multisig addresses - self.nodes[0].importprivkey("927pw6RW8ZekycnXqBQ2JS5nPyo1yRfGNN8oq74HeddWSpafDJH") - uncompressed_spendable_address = ["mguN2vNSCEUh6rJaXoAVwY3YZwZvEmf5xi"] - self.nodes[0].importprivkey("cMcrXaaUC48ZKpcyydfFo8PxHAjpsYLhdsp6nmtB3E2ER9UUHWnw") - compressed_spendable_address = ["n1UNmpmbVUJ9ytXYXiurmGPQ3TRrXqPWKL"] - - self.nodes[0].importpubkey(pubkeys[5]) - compressed_solvable_address = [key_to_p2pkh(pubkeys[5])] - self.nodes[0].importpubkey(pubkeys[6]) - uncompressed_solvable_address = [key_to_p2pkh(pubkeys[6])] - - unseen_anytime = [] # These outputs should never be seen - solvable_anytime = [] # These outputs should be solvable after importpubkey - unseen_anytime = [] # These outputs should never be seen - - uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], compressed_spendable_address[0]])['address']) - uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], uncompressed_spendable_address[0]])['address']) - compressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_spendable_address[0]])['address']) - uncompressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_solvable_address[0], uncompressed_solvable_address[0]])['address']) - compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_solvable_address[0]])['address']) - - premature_witaddress = [] - - for i in compressed_spendable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - premature_witaddress.append(script_to_p2sh(p2wsh)) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # P2WPKH, P2SH_P2WPKH are always spendable - spendable_anytime.extend([p2wpkh, p2sh_p2wpkh]) - - for i in uncompressed_spendable_address + uncompressed_solvable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen - unseen_anytime.extend([p2wsh, p2sh_p2wsh]) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # P2WPKH, P2SH_P2WPKH with uncompressed keys are never seen - unseen_anytime.extend([p2wpkh, p2sh_p2wpkh]) - - for i in compressed_solvable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - premature_witaddress.append(script_to_p2sh(p2wsh)) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # P2SH_P2PK, P2SH_P2PKH with compressed keys are always solvable - solvable_anytime.extend([p2wpkh, p2sh_p2wpkh]) - - self.mine_and_test_listunspent(spendable_anytime, 2) - self.mine_and_test_listunspent(solvable_anytime, 1) - self.mine_and_test_listunspent(unseen_anytime, 0) - - # Check that createrawtransaction/decoderawtransaction with non-v0 Bech32 works - v1_addr = program_to_witness(1, [3, 5]) - v1_tx = self.nodes[0].createrawtransaction([getutxo(spendable_txid[0])], {v1_addr: 1}) - v1_decoded = self.nodes[1].decoderawtransaction(v1_tx) - assert_equal(v1_decoded['vout'][0]['scriptPubKey']['addresses'][0], v1_addr) - assert_equal(v1_decoded['vout'][0]['scriptPubKey']['hex'], "51020305") - - # Check that spendable outputs are really spendable - self.create_and_mine_tx_from_txids(spendable_txid) - - # import all the private keys so solvable addresses become spendable - self.nodes[0].importprivkey("cPiM8Ub4heR9NBYmgVzJQiUH1if44GSBGiqaeJySuL2BKxubvgwb") - self.nodes[0].importprivkey("cPpAdHaD6VoYbW78kveN2bsvb45Q7G5PhaPApVUGwvF8VQ9brD97") - self.nodes[0].importprivkey("91zqCU5B9sdWxzMt1ca3VzbtVm2YM6Hi5Rxn4UDtxEaN9C9nzXV") - self.nodes[0].importprivkey("cPQFjcVRpAUBG8BA9hzr2yEzHwKoMgLkJZBBtK9vJnvGJgMjzTbd") - self.nodes[0].importprivkey("cQGtcm34xiLjB1v7bkRa4V3aAc9tS2UTuBZ1UnZGeSeNy627fN66") - self.nodes[0].importprivkey("cTW5mR5M45vHxXkeChZdtSPozrFwFgmEvTNnanCW6wrqwaCZ1X7K") - self.create_and_mine_tx_from_txids(solvable_txid) - - # Test that importing native P2WPKH/P2WSH scripts works - for use_p2wsh in [False, True]: - if use_p2wsh: - scriptPubKey = "00203a59f3f56b713fdcf5d1a57357f02c44342cbf306ffe0c4741046837bf90561a" - transaction = "01000000000100e1f505000000002200203a59f3f56b713fdcf5d1a57357f02c44342cbf306ffe0c4741046837bf90561a00000000" - else: - scriptPubKey = "a9142f8c469c2f0084c48e11f998ffbe7efa7549f26d87" - transaction = "01000000000100e1f5050000000017a9142f8c469c2f0084c48e11f998ffbe7efa7549f26d8700000000" - - self.nodes[1].importaddress(scriptPubKey, "", False) - rawtxfund = self.nodes[1].fundrawtransaction(transaction)['hex'] - rawtxfund = self.nodes[1].signrawtransactionwithwallet(rawtxfund)["hex"] - txid = self.nodes[1].sendrawtransaction(rawtxfund) - - assert_equal(self.nodes[1].gettransaction(txid, True)["txid"], txid) - assert_equal(self.nodes[1].listtransactions("*", 1, 0, True)[0]["txid"], txid) - - # Assert it is properly saved - self.restart_node(1) - assert_equal(self.nodes[1].gettransaction(txid, True)["txid"], txid) - assert_equal(self.nodes[1].listtransactions("*", 1, 0, True)[0]["txid"], txid) + self.generate(self.nodes[0], 1) + + if not self.options.descriptors: + self.log.info("Verify behaviour of importaddress and listunspent") + + # Some public keys to be used later + pubkeys = [ + "0363D44AABD0F1699138239DF2F042C3282C0671CC7A76826A55C8203D90E39242", # cPiM8Ub4heR9NBYmgVzJQiUH1if44GSBGiqaeJySuL2BKxubvgwb + "02D3E626B3E616FC8662B489C123349FECBFC611E778E5BE739B257EAE4721E5BF", # cPpAdHaD6VoYbW78kveN2bsvb45Q7G5PhaPApVUGwvF8VQ9brD97 + "04A47F2CBCEFFA7B9BCDA184E7D5668D3DA6F9079AD41E422FA5FD7B2D458F2538A62F5BD8EC85C2477F39650BD391EA6250207065B2A81DA8B009FC891E898F0E", # 91zqCU5B9sdWxzMt1ca3VzbtVm2YM6Hi5Rxn4UDtxEaN9C9nzXV + "02A47F2CBCEFFA7B9BCDA184E7D5668D3DA6F9079AD41E422FA5FD7B2D458F2538", # cPQFjcVRpAUBG8BA9hzr2yEzHwKoMgLkJZBBtK9vJnvGJgMjzTbd + "036722F784214129FEB9E8129D626324F3F6716555B603FFE8300BBCB882151228", # cQGtcm34xiLjB1v7bkRa4V3aAc9tS2UTuBZ1UnZGeSeNy627fN66 + "0266A8396EE936BF6D99D17920DB21C6C7B1AB14C639D5CD72B300297E416FD2EC", # cTW5mR5M45vHxXkeChZdtSPozrFwFgmEvTNnanCW6wrqwaCZ1X7K + "0450A38BD7F0AC212FEBA77354A9B036A32E0F7C81FC4E0C5ADCA7C549C4505D2522458C2D9AE3CEFD684E039194B72C8A10F9CB9D4764AB26FCC2718D421D3B84", # 92h2XPssjBpsJN5CqSP7v9a7cf2kgDunBC6PDFwJHMACM1rrVBJ + ] + + # Import a compressed key and an uncompressed key, generate some multisig addresses + self.nodes[0].importprivkey("92e6XLo5jVAVwrQKPNTs93oQco8f8sDNBcpv73Dsrs397fQtFQn") + uncompressed_spendable_address = ["mvozP4UwyGD2mGZU4D2eMvMLPB9WkMmMQu"] + self.nodes[0].importprivkey("cNC8eQ5dg3mFAVePDX4ddmPYpPbw41r9bm2jd1nLJT77e6RrzTRR") + compressed_spendable_address = ["mmWQubrDomqpgSYekvsU7HWEVjLFHAakLe"] + assert not self.nodes[0].getaddressinfo(uncompressed_spendable_address[0])['iscompressed'] + assert self.nodes[0].getaddressinfo(compressed_spendable_address[0])['iscompressed'] + + self.nodes[0].importpubkey(pubkeys[0]) + compressed_solvable_address = [key_to_p2pkh(pubkeys[0])] + self.nodes[0].importpubkey(pubkeys[1]) + compressed_solvable_address.append(key_to_p2pkh(pubkeys[1])) + self.nodes[0].importpubkey(pubkeys[2]) + uncompressed_solvable_address = [key_to_p2pkh(pubkeys[2])] + + spendable_anytime = [] # These outputs should be seen anytime after importprivkey and addmultisigaddress + spendable_after_importaddress = [] # These outputs should be seen after importaddress + solvable_after_importaddress = [] # These outputs should be seen after importaddress but not spendable + unsolvable_after_importaddress = [] # These outputs should be unsolvable after importaddress + solvable_anytime = [] # These outputs should be solvable after importpubkey + unseen_anytime = [] # These outputs should never be seen + + uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], compressed_spendable_address[0]])['address']) + uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], uncompressed_spendable_address[0]])['address']) + compressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_spendable_address[0]])['address']) + uncompressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], uncompressed_solvable_address[0]])['address']) + compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_solvable_address[0]])['address']) + compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_solvable_address[0], compressed_solvable_address[1]])['address']) + + # Test multisig_without_privkey + # We have 2 public keys without private keys, use addmultisigaddress to add to wallet. + # Money sent to P2SH of multisig of this should only be seen after importaddress with the BASE58 P2SH address. + + multisig_without_privkey_address = self.nodes[0].addmultisigaddress(2, [pubkeys[3], pubkeys[4]])['address'] + script = keys_to_multisig_script([pubkeys[3], pubkeys[4]]) + solvable_after_importaddress.append(script_to_p2sh_script(script)) + + for i in compressed_spendable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + # p2sh multisig with compressed keys should always be spendable + spendable_anytime.extend([p2sh]) + # bare multisig can be watched and signed, but is not treated as ours + solvable_after_importaddress.extend([bare]) + # P2WSH and P2SH(P2WSH) multisig with compressed keys are spendable after direct importaddress + spendable_after_importaddress.extend([p2wsh, p2sh_p2wsh]) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # normal P2PKH and P2PK with compressed keys should always be spendable + spendable_anytime.extend([p2pkh, p2pk]) + # P2SH_P2PK, P2SH_P2PKH with compressed keys are spendable after direct importaddress + spendable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) + # P2WPKH and P2SH_P2WPKH with compressed keys should always be spendable + spendable_anytime.extend([p2wpkh, p2sh_p2wpkh]) + + for i in uncompressed_spendable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + # p2sh multisig with uncompressed keys should always be spendable + spendable_anytime.extend([p2sh]) + # bare multisig can be watched and signed, but is not treated as ours + solvable_after_importaddress.extend([bare]) + # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen + unseen_anytime.extend([p2wsh, p2sh_p2wsh]) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # normal P2PKH and P2PK with uncompressed keys should always be spendable + spendable_anytime.extend([p2pkh, p2pk]) + # P2SH_P2PK and P2SH_P2PKH are spendable after direct importaddress + spendable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh]) + # Witness output types with uncompressed keys are never seen + unseen_anytime.extend([p2wpkh, p2sh_p2wpkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) + + for i in compressed_solvable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + # Multisig without private is not seen after addmultisigaddress, but seen after importaddress + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + solvable_after_importaddress.extend([bare, p2sh, p2wsh, p2sh_p2wsh]) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # normal P2PKH, P2PK, P2WPKH and P2SH_P2WPKH with compressed keys should always be seen + solvable_anytime.extend([p2pkh, p2pk, p2wpkh, p2sh_p2wpkh]) + # P2SH_P2PK, P2SH_P2PKH with compressed keys are seen after direct importaddress + solvable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) + + for i in uncompressed_solvable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + # Base uncompressed multisig without private is not seen after addmultisigaddress, but seen after importaddress + solvable_after_importaddress.extend([bare, p2sh]) + # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen + unseen_anytime.extend([p2wsh, p2sh_p2wsh]) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # normal P2PKH and P2PK with uncompressed keys should always be seen + solvable_anytime.extend([p2pkh, p2pk]) + # P2SH_P2PK, P2SH_P2PKH with uncompressed keys are seen after direct importaddress + solvable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh]) + # Witness output types with uncompressed keys are never seen + unseen_anytime.extend([p2wpkh, p2sh_p2wpkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) + + op1 = CScript([OP_1]) + op0 = CScript([OP_0]) + # 2N7MGY19ti4KDMSzRfPAssP6Pxyuxoi6jLe is the P2SH(P2PKH) version of mjoE3sSrb8ByYEvgnC3Aox86u1CHnfJA4V + unsolvable_address_key = bytes.fromhex("02341AEC7587A51CDE5279E0630A531AEA2615A9F80B17E8D9376327BAEAA59E3D") + unsolvablep2pkh = key_to_p2pkh_script(unsolvable_address_key) + unsolvablep2wshp2pkh = script_to_p2wsh_script(unsolvablep2pkh) + p2shop0 = script_to_p2sh_script(op0) + p2wshop1 = script_to_p2wsh_script(op1) + unsolvable_after_importaddress.append(unsolvablep2pkh) + unsolvable_after_importaddress.append(unsolvablep2wshp2pkh) + unsolvable_after_importaddress.append(op1) # OP_1 will be imported as script + unsolvable_after_importaddress.append(p2wshop1) + unseen_anytime.append(op0) # OP_0 will be imported as P2SH address with no script provided + unsolvable_after_importaddress.append(p2shop0) + + spendable_txid = [] + solvable_txid = [] + spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime, 2)) + solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime, 1)) + self.mine_and_test_listunspent(spendable_after_importaddress + solvable_after_importaddress + unseen_anytime + unsolvable_after_importaddress, 0) + + importlist = [] + for i in compressed_spendable_address + uncompressed_spendable_address + compressed_solvable_address + uncompressed_solvable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + bare = bytes.fromhex(v['hex']) + importlist.append(bare.hex()) + importlist.append(script_to_p2wsh_script(bare).hex()) + else: + pubkey = bytes.fromhex(v['pubkey']) + p2pk = key_to_p2pk_script(pubkey) + p2pkh = key_to_p2pkh_script(pubkey) + importlist.append(p2pk.hex()) + importlist.append(p2pkh.hex()) + importlist.append(key_to_p2wpkh_script(pubkey).hex()) + importlist.append(script_to_p2wsh_script(p2pk).hex()) + importlist.append(script_to_p2wsh_script(p2pkh).hex()) + + importlist.append(unsolvablep2pkh.hex()) + importlist.append(unsolvablep2wshp2pkh.hex()) + importlist.append(op1.hex()) + importlist.append(p2wshop1.hex()) + + for i in importlist: + # import all generated addresses. The wallet already has the private keys for some of these, so catch JSON RPC + # exceptions and continue. + try_rpc(-4, "The wallet already contains the private key for this address or script", self.nodes[0].importaddress, i, "", False, True) + + self.nodes[0].importaddress(script_to_p2sh(op0)) # import OP_0 as address only + self.nodes[0].importaddress(multisig_without_privkey_address) # Test multisig_without_privkey + + spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime + spendable_after_importaddress, 2)) + solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime + solvable_after_importaddress, 1)) + self.mine_and_test_listunspent(unsolvable_after_importaddress, 1) + self.mine_and_test_listunspent(unseen_anytime, 0) + + spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime + spendable_after_importaddress, 2)) + solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime + solvable_after_importaddress, 1)) + self.mine_and_test_listunspent(unsolvable_after_importaddress, 1) + self.mine_and_test_listunspent(unseen_anytime, 0) + + # Repeat some tests. This time we don't add witness scripts with importaddress + # Import a compressed key and an uncompressed key, generate some multisig addresses + self.nodes[0].importprivkey("927pw6RW8ZekycnXqBQ2JS5nPyo1yRfGNN8oq74HeddWSpafDJH") + uncompressed_spendable_address = ["mguN2vNSCEUh6rJaXoAVwY3YZwZvEmf5xi"] + self.nodes[0].importprivkey("cMcrXaaUC48ZKpcyydfFo8PxHAjpsYLhdsp6nmtB3E2ER9UUHWnw") + compressed_spendable_address = ["n1UNmpmbVUJ9ytXYXiurmGPQ3TRrXqPWKL"] + + self.nodes[0].importpubkey(pubkeys[5]) + compressed_solvable_address = [key_to_p2pkh(pubkeys[5])] + self.nodes[0].importpubkey(pubkeys[6]) + uncompressed_solvable_address = [key_to_p2pkh(pubkeys[6])] + + unseen_anytime = [] # These outputs should never be seen + solvable_anytime = [] # These outputs should be solvable after importpubkey + unseen_anytime = [] # These outputs should never be seen + + uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], compressed_spendable_address[0]])['address']) + uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], uncompressed_spendable_address[0]])['address']) + compressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_spendable_address[0]])['address']) + uncompressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_solvable_address[0], uncompressed_solvable_address[0]])['address']) + compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_solvable_address[0]])['address']) + + premature_witaddress = [] + + for i in compressed_spendable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + premature_witaddress.append(script_to_p2sh(p2wsh)) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # P2WPKH, P2SH_P2WPKH are always spendable + spendable_anytime.extend([p2wpkh, p2sh_p2wpkh]) + + for i in uncompressed_spendable_address + uncompressed_solvable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen + unseen_anytime.extend([p2wsh, p2sh_p2wsh]) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # P2WPKH, P2SH_P2WPKH with uncompressed keys are never seen + unseen_anytime.extend([p2wpkh, p2sh_p2wpkh]) + + for i in compressed_solvable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + premature_witaddress.append(script_to_p2sh(p2wsh)) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # P2SH_P2PK, P2SH_P2PKH with compressed keys are always solvable + solvable_anytime.extend([p2wpkh, p2sh_p2wpkh]) + + self.mine_and_test_listunspent(spendable_anytime, 2) + self.mine_and_test_listunspent(solvable_anytime, 1) + self.mine_and_test_listunspent(unseen_anytime, 0) + + # Check that createrawtransaction/decoderawtransaction with non-v0 Bech32 works + v1_addr = program_to_witness(1, [3, 5]) + v1_tx = self.nodes[0].createrawtransaction([getutxo(spendable_txid[0])], {v1_addr: 1}) + v1_decoded = self.nodes[1].decoderawtransaction(v1_tx) + assert_equal(v1_decoded['vout'][0]['scriptPubKey']['address'], v1_addr) + assert_equal(v1_decoded['vout'][0]['scriptPubKey']['hex'], "51020305") + + # Check that spendable outputs are really spendable + self.create_and_mine_tx_from_txids(spendable_txid) + + # import all the private keys so solvable addresses become spendable + self.nodes[0].importprivkey("cPiM8Ub4heR9NBYmgVzJQiUH1if44GSBGiqaeJySuL2BKxubvgwb") + self.nodes[0].importprivkey("cPpAdHaD6VoYbW78kveN2bsvb45Q7G5PhaPApVUGwvF8VQ9brD97") + self.nodes[0].importprivkey("91zqCU5B9sdWxzMt1ca3VzbtVm2YM6Hi5Rxn4UDtxEaN9C9nzXV") + self.nodes[0].importprivkey("cPQFjcVRpAUBG8BA9hzr2yEzHwKoMgLkJZBBtK9vJnvGJgMjzTbd") + self.nodes[0].importprivkey("cQGtcm34xiLjB1v7bkRa4V3aAc9tS2UTuBZ1UnZGeSeNy627fN66") + self.nodes[0].importprivkey("cTW5mR5M45vHxXkeChZdtSPozrFwFgmEvTNnanCW6wrqwaCZ1X7K") + self.create_and_mine_tx_from_txids(solvable_txid) + + # Test that importing native P2WPKH/P2WSH scripts works + for use_p2wsh in [False, True]: + if use_p2wsh: + scriptPubKey = "00203a59f3f56b713fdcf5d1a57357f02c44342cbf306ffe0c4741046837bf90561a" + transaction = "01000000000100e1f505000000002200203a59f3f56b713fdcf5d1a57357f02c44342cbf306ffe0c4741046837bf90561a00000000" + else: + scriptPubKey = "a9142f8c469c2f0084c48e11f998ffbe7efa7549f26d87" + transaction = "01000000000100e1f5050000000017a9142f8c469c2f0084c48e11f998ffbe7efa7549f26d8700000000" + + self.nodes[1].importaddress(scriptPubKey, "", False) + rawtxfund = self.nodes[1].fundrawtransaction(transaction)['hex'] + rawtxfund = self.nodes[1].signrawtransactionwithwallet(rawtxfund)["hex"] + txid = self.nodes[1].sendrawtransaction(rawtxfund) + + assert_equal(self.nodes[1].gettransaction(txid, True)["txid"], txid) + assert_equal(self.nodes[1].listtransactions("*", 1, 0, True)[0]["txid"], txid) + + # Assert it is properly saved + self.restart_node(1) + assert_equal(self.nodes[1].gettransaction(txid, True)["txid"], txid) + assert_equal(self.nodes[1].listtransactions("*", 1, 0, True)[0]["txid"], txid) def mine_and_test_listunspent(self, script_list, ismine): utxo = find_spendable_utxo(self.nodes[0], 50) @@ -569,18 +626,17 @@ class SegWitTest(BitcoinTestFramework): tx.rehash() signresults = self.nodes[0].signrawtransactionwithwallet(tx.serialize_without_witness().hex())['hex'] txid = self.nodes[0].sendrawtransaction(hexstring=signresults, maxfeerate=0) - txs_mined[txid] = self.nodes[0].generate(1)[0] - self.sync_blocks() + txs_mined[txid] = self.generate(self.nodes[0], 1)[0] watchcount = 0 spendcount = 0 for i in self.nodes[0].listunspent(): - if (i['txid'] == txid): + if i['txid'] == txid: watchcount += 1 if i['spendable']: spendcount += 1 - if (ismine == 2): + if ismine == 2: assert_equal(spendcount, len(script_list)) - elif (ismine == 1): + elif ismine == 1: assert_equal(watchcount, len(script_list)) assert_equal(spendcount, 0) else: @@ -588,41 +644,38 @@ class SegWitTest(BitcoinTestFramework): return txid def p2sh_address_to_script(self, v): - bare = CScript(hex_str_to_bytes(v['hex'])) - p2sh = CScript(hex_str_to_bytes(v['scriptPubKey'])) - p2wsh = CScript([OP_0, sha256(bare)]) - p2sh_p2wsh = CScript([OP_HASH160, hash160(p2wsh), OP_EQUAL]) - return([bare, p2sh, p2wsh, p2sh_p2wsh]) + bare = CScript(bytes.fromhex(v['hex'])) + p2sh = CScript(bytes.fromhex(v['scriptPubKey'])) + p2wsh = script_to_p2wsh_script(bare) + p2sh_p2wsh = script_to_p2sh_script(p2wsh) + return [bare, p2sh, p2wsh, p2sh_p2wsh] def p2pkh_address_to_script(self, v): - pubkey = hex_str_to_bytes(v['pubkey']) - p2wpkh = CScript([OP_0, hash160(pubkey)]) - p2sh_p2wpkh = CScript([OP_HASH160, hash160(p2wpkh), OP_EQUAL]) - p2pk = CScript([pubkey, OP_CHECKSIG]) - p2pkh = CScript(hex_str_to_bytes(v['scriptPubKey'])) - p2sh_p2pk = CScript([OP_HASH160, hash160(p2pk), OP_EQUAL]) - p2sh_p2pkh = CScript([OP_HASH160, hash160(p2pkh), OP_EQUAL]) - p2wsh_p2pk = CScript([OP_0, sha256(p2pk)]) - p2wsh_p2pkh = CScript([OP_0, sha256(p2pkh)]) - p2sh_p2wsh_p2pk = CScript([OP_HASH160, hash160(p2wsh_p2pk), OP_EQUAL]) - p2sh_p2wsh_p2pkh = CScript([OP_HASH160, hash160(p2wsh_p2pkh), OP_EQUAL]) + pubkey = bytes.fromhex(v['pubkey']) + p2wpkh = key_to_p2wpkh_script(pubkey) + p2sh_p2wpkh = script_to_p2sh_script(p2wpkh) + p2pk = key_to_p2pk_script(pubkey) + p2pkh = CScript(bytes.fromhex(v['scriptPubKey'])) + p2sh_p2pk = script_to_p2sh_script(p2pk) + p2sh_p2pkh = script_to_p2sh_script(p2pkh) + p2wsh_p2pk = script_to_p2wsh_script(p2pk) + p2wsh_p2pkh = script_to_p2wsh_script(p2pkh) + p2sh_p2wsh_p2pk = script_to_p2sh_script(p2wsh_p2pk) + p2sh_p2wsh_p2pkh = script_to_p2sh_script(p2wsh_p2pkh) return [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] def create_and_mine_tx_from_txids(self, txids, success=True): tx = CTransaction() for i in txids: - txtmp = CTransaction() txraw = self.nodes[0].getrawtransaction(i, 0, txs_mined[i]) - f = BytesIO(hex_str_to_bytes(txraw)) - txtmp.deserialize(f) + txtmp = tx_from_hex(txraw) for j in range(len(txtmp.vout)): tx.vin.append(CTxIn(COutPoint(int('0x' + i, 0), j))) tx.vout.append(CTxOut(0, CScript())) tx.rehash() signresults = self.nodes[0].signrawtransactionwithwallet(tx.serialize_without_witness().hex())['hex'] self.nodes[0].sendrawtransaction(hexstring=signresults, maxfeerate=0) - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) if __name__ == '__main__': diff --git a/test/functional/feature_settings.py b/test/functional/feature_settings.py index 5a0236401d..20018f010f 100755 --- a/test/functional/feature_settings.py +++ b/test/functional/feature_settings.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test various command line arguments and configuration file parameters.""" @@ -83,7 +83,7 @@ class SettingsTest(BitcoinTestFramework): with altsettings.open("w") as fp: fp.write('{"key": "value"}') with node.assert_debug_log(expected_msgs=['Setting file arg: key = "value"']): - self.start_node(0, extra_args=["-settings={}".format(altsettings)]) + self.start_node(0, extra_args=[f"-settings={altsettings}"]) self.stop_node(0) diff --git a/test/functional/feature_signet.py b/test/functional/feature_signet.py index 96c581dede..6578caee3f 100755 --- a/test/functional/feature_signet.py +++ b/test/functional/feature_signet.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test basic signet functionality""" @@ -51,7 +51,7 @@ class SignetBasicTest(BitcoinTestFramework): assert_equal(mining_info['networkhashps'], Decimal('0')) assert_equal(mining_info['pooledtx'], 0) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) self.log.info("pregenerated signet blocks check") diff --git a/test/functional/feature_startupnotify.py b/test/functional/feature_startupnotify.py new file mode 100755 index 0000000000..c6aa837768 --- /dev/null +++ b/test/functional/feature_startupnotify.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test -startupnotify.""" + +import os + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, +) + +NODE_DIR = "node0" +FILE_NAME = "test.txt" + + +class StartupNotifyTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.disable_syscall_sandbox = True + + def run_test(self): + tmpdir_file = os.path.join(self.options.tmpdir, NODE_DIR, FILE_NAME) + assert not os.path.exists(tmpdir_file) + + self.log.info("Test -startupnotify command is run when node starts") + self.restart_node(0, extra_args=[f"-startupnotify=echo '{FILE_NAME}' >> {NODE_DIR}/{FILE_NAME}"]) + self.wait_until(lambda: os.path.exists(tmpdir_file)) + + self.log.info("Test -startupnotify is executed once") + with open(tmpdir_file, "r", encoding="utf8") as f: + file_content = f.read() + assert_equal(file_content.count(FILE_NAME), 1) + + self.log.info("Test node is fully started") + assert_equal(self.nodes[0].getblockcount(), 200) + + +if __name__ == '__main__': + StartupNotifyTest().main() diff --git a/test/functional/feature_syscall_sandbox.py b/test/functional/feature_syscall_sandbox.py new file mode 100755 index 0000000000..e430542845 --- /dev/null +++ b/test/functional/feature_syscall_sandbox.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test bitcoind aborts if a disallowed syscall is used when compiled with the syscall sandbox.""" + +from test_framework.test_framework import BitcoinTestFramework, SkipTest + + +class SyscallSandboxTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def skip_test_if_missing_module(self): + if not self.is_syscall_sandbox_compiled(): + raise SkipTest("bitcoind has not been built with syscall sandbox enabled.") + if self.disable_syscall_sandbox: + raise SkipTest("--nosandbox passed to test runner.") + + def run_test(self): + disallowed_syscall_terminated_bitcoind = False + expected_log_entry = 'ERROR: The syscall "getgroups" (syscall number 115) is not allowed by the syscall sandbox' + with self.nodes[0].assert_debug_log([expected_log_entry]): + self.log.info("Invoking disallowed syscall") + try: + self.nodes[0].invokedisallowedsyscall() + except ConnectionError: + disallowed_syscall_terminated_bitcoind = True + assert disallowed_syscall_terminated_bitcoind + self.nodes = [] + + +if __name__ == "__main__": + SyscallSandboxTest().main() diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index 183a43abd4..3e3d4b3c77 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # Test Taproot softfork (BIPs 340-342) from test_framework.blocktools import ( + COINBASE_MATURITY, create_coinbase, create_block, add_witness_commitment, @@ -18,15 +19,21 @@ from test_framework.messages import ( CTxIn, CTxInWitness, CTxOut, - ToHex, + SEQUENCE_FINAL, ) from test_framework.script import ( ANNEX_TAG, + BIP341_sha_amounts, + BIP341_sha_outputs, + BIP341_sha_prevouts, + BIP341_sha_scriptpubkeys, + BIP341_sha_sequences, CScript, CScriptNum, CScriptOp, + hash256, LEAF_VERSION_TAPSCRIPT, - LegacySignatureHash, + LegacySignatureMsg, LOCKTIME_THRESHOLD, MAX_SCRIPT_ELEMENT_SIZE, OP_0, @@ -57,7 +64,6 @@ from test_framework.script import ( OP_ENDIF, OP_EQUAL, OP_EQUALVERIFY, - OP_HASH160, OP_IF, OP_NOP, OP_NOT, @@ -71,25 +77,38 @@ from test_framework.script import ( SIGHASH_NONE, SIGHASH_SINGLE, SIGHASH_ANYONECANPAY, - SegwitV0SignatureHash, - TaprootSignatureHash, + SegwitV0SignatureMsg, + TaggedHash, + TaprootSignatureMsg, is_op_success, taproot_construct, ) +from test_framework.script_util import ( + key_to_p2pk_script, + key_to_p2pkh_script, + key_to_p2wpkh_script, + keyhash_to_p2pkh_script, + script_to_p2sh_script, + script_to_p2wsh_script, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_raises_rpc_error, assert_equal from test_framework.key import generate_privkey, compute_xonly_pubkey, sign_schnorr, tweak_add_privkey, ECKey from test_framework.address import ( hash160, - sha256, + program_to_witness ) from collections import OrderedDict, namedtuple +from enum import Enum from io import BytesIO import json import hashlib import os import random +# Whether or not to output generated test vectors, in JSON format. +GEN_TEST_VECTORS = False + # === Framework for building spending transactions. === # # The computation is represented as a "context" dict, whose entries store potentially-unevaluated expressions that @@ -189,8 +208,8 @@ def default_controlblock(ctx): """Default expression for "controlblock": combine leafversion, negflag, pubkey_internal, merklebranch.""" return bytes([get(ctx, "leafversion") + get(ctx, "negflag")]) + get(ctx, "pubkey_internal") + get(ctx, "merklebranch") -def default_sighash(ctx): - """Default expression for "sighash": depending on mode, compute BIP341, BIP143, or legacy sighash.""" +def default_sigmsg(ctx): + """Default expression for "sigmsg": depending on mode, compute BIP341, BIP143, or legacy sigmsg.""" tx = get(ctx, "tx") idx = get(ctx, "idx") hashtype = get(ctx, "hashtype_actual") @@ -203,18 +222,30 @@ def default_sighash(ctx): codeseppos = get(ctx, "codeseppos") leaf_ver = get(ctx, "leafversion") script = get(ctx, "script_taproot") - return TaprootSignatureHash(tx, utxos, hashtype, idx, scriptpath=True, script=script, leaf_ver=leaf_ver, codeseparator_pos=codeseppos, annex=annex) + return TaprootSignatureMsg(tx, utxos, hashtype, idx, scriptpath=True, script=script, leaf_ver=leaf_ver, codeseparator_pos=codeseppos, annex=annex) else: - return TaprootSignatureHash(tx, utxos, hashtype, idx, scriptpath=False, annex=annex) + return TaprootSignatureMsg(tx, utxos, hashtype, idx, scriptpath=False, annex=annex) elif mode == "witv0": # BIP143 signature hash scriptcode = get(ctx, "scriptcode") utxos = get(ctx, "utxos") - return SegwitV0SignatureHash(scriptcode, tx, idx, hashtype, utxos[idx].nValue) + return SegwitV0SignatureMsg(scriptcode, tx, idx, hashtype, utxos[idx].nValue) else: # Pre-segwit signature hash scriptcode = get(ctx, "scriptcode") - return LegacySignatureHash(scriptcode, tx, idx, hashtype)[0] + return LegacySignatureMsg(scriptcode, tx, idx, hashtype)[0] + +def default_sighash(ctx): + """Default expression for "sighash": depending on mode, compute tagged hash or dsha256 of sigmsg.""" + msg = get(ctx, "sigmsg") + mode = get(ctx, "mode") + if mode == "taproot": + return TaggedHash("TapSighash", msg) + else: + if msg is None: + return (1).to_bytes(32, 'little') + else: + return hash256(msg) def default_tweak(ctx): """Default expression for "tweak": None if a leaf is specified, tap[0] otherwise.""" @@ -234,14 +265,18 @@ def default_key_tweaked(ctx): def default_signature(ctx): """Default expression for "signature": BIP340 signature or ECDSA signature depending on mode.""" sighash = get(ctx, "sighash") + deterministic = get(ctx, "deterministic") if get(ctx, "mode") == "taproot": key = get(ctx, "key_tweaked") flip_r = get(ctx, "flag_flip_r") flip_p = get(ctx, "flag_flip_p") - return sign_schnorr(key, sighash, flip_r=flip_r, flip_p=flip_p) + aux = bytes([0] * 32) + if not deterministic: + aux = random.getrandbits(256).to_bytes(32, 'big') + return sign_schnorr(key, sighash, flip_r=flip_r, flip_p=flip_p, aux=aux) else: key = get(ctx, "key") - return key.sign_ecdsa(sighash) + return key.sign_ecdsa(sighash, rfc6979=deterministic) def default_hashtype_actual(ctx): """Default expression for "hashtype_actual": hashtype, unless mismatching SIGHASH_SINGLE in taproot.""" @@ -335,6 +370,8 @@ DEFAULT_CONTEXT = { "key_tweaked": default_key_tweaked, # The tweak to use (None for script path spends, the actual tweak for key path spends). "tweak": default_tweak, + # The sigmsg value (preimage of sighash) + "sigmsg": default_sigmsg, # The sighash value (32 bytes) "sighash": default_sighash, # The information about the chosen script path spend (TaprootLeafInfo object). @@ -371,6 +408,8 @@ DEFAULT_CONTEXT = { "leaf": None, # The input arguments to provide to the executed script "inputs": [], + # Use deterministic signing nonces + "deterministic": False, # == Parameters to be set before evaluation: == # - mode: what spending style to use ("taproot", "witv0", or "legacy"). @@ -391,6 +430,7 @@ def flatten(lst): ret.append(elem) return ret + def spend(tx, idx, utxos, **kwargs): """Sign transaction input idx of tx, provided utxos is the list of outputs being spent. @@ -418,7 +458,7 @@ def spend(tx, idx, utxos, **kwargs): # Each spender is a tuple of: # - A scriptPubKey which is to be spent from (CScript) # - A comment describing the test (string) -# - Whether the spending (on itself) is expected to be standard (bool) +# - Whether the spending (on itself) is expected to be standard (Enum.Standard) # - A tx-signing lambda returning (scriptsig, witness_stack), taking as inputs: # - A transaction to sign (CTransaction) # - An input position (int) @@ -430,8 +470,14 @@ def spend(tx, idx, utxos, **kwargs): # - Whether this test demands being placed in a txin with no corresponding txout (for testing SIGHASH_SINGLE behavior) Spender = namedtuple("Spender", "script,comment,is_standard,sat_function,err_msg,sigops_weight,no_fail,need_vin_vout_mismatch") +# The full node versions that treat the tx standard. +# ALL means any version +# V23 means the major version 23.0 and any later version +# NONE means no version +Standard = Enum('Standard', 'ALL V23 NONE') + -def make_spender(comment, *, tap=None, witv0=False, script=None, pkh=None, p2sh=False, spk_mutate_pre_p2sh=None, failure=None, standard=True, err_msg=None, sigops_weight=0, need_vin_vout_mismatch=False, **kwargs): +def make_spender(comment, *, tap=None, witv0=False, script=None, pkh=None, p2sh=False, spk_mutate_pre_p2sh=None, failure=None, standard=Standard.ALL, err_msg=None, sigops_weight=0, need_vin_vout_mismatch=False, **kwargs): """Helper for constructing Spender objects using the context signing framework. * tap: a TaprootInfo object (see taproot_construct), for Taproot spends (cannot be combined with pkh, witv0, or script) @@ -441,13 +487,18 @@ def make_spender(comment, *, tap=None, witv0=False, script=None, pkh=None, p2sh= * p2sh: whether the output is P2SH wrapper (this is supported even for Taproot, where it makes the output unencumbered) * spk_mutate_pre_psh: a callable to be applied to the script (before potentially P2SH-wrapping it) * failure: a dict of entries to override in the context when intentionally failing to spend (if None, no_fail will be set) - * standard: whether the (valid version of) spending is expected to be standard + * standard: whether the (valid version of) spending is expected to be standard (True is mapped to Standard.ALL, False is mapped to Standard.NONE) * err_msg: a string with an expected error message for failure (or None, if not cared about) * sigops_weight: the pre-taproot sigops weight consumed by a successful spend * need_vin_vout_mismatch: whether this test requires being tested in a transaction input that has no corresponding transaction output. """ + if standard == True: + standard = Standard.ALL + elif standard == False: + standard = Standard.NONE + conf = dict() # Compute scriptPubKey and set useful defaults based on the inputs. @@ -458,13 +509,13 @@ def make_spender(comment, *, tap=None, witv0=False, script=None, pkh=None, p2sh= # P2WPKH assert script is None pubkeyhash = hash160(pkh) - spk = CScript([OP_0, pubkeyhash]) - conf["scriptcode"] = CScript([OP_DUP, OP_HASH160, pubkeyhash, OP_EQUALVERIFY, OP_CHECKSIG]) + spk = key_to_p2wpkh_script(pkh) + conf["scriptcode"] = keyhash_to_p2pkh_script(pubkeyhash) conf["script_witv0"] = None conf["inputs"] = [getter("sign"), pkh] elif script is not None: # P2WSH - spk = CScript([OP_0, sha256(script)]) + spk = script_to_p2wsh_script(script) conf["scriptcode"] = script conf["script_witv0"] = script else: @@ -475,7 +526,7 @@ def make_spender(comment, *, tap=None, witv0=False, script=None, pkh=None, p2sh= # P2PKH assert script is None pubkeyhash = hash160(pkh) - spk = CScript([OP_DUP, OP_HASH160, pubkeyhash, OP_EQUALVERIFY, OP_CHECKSIG]) + spk = keyhash_to_p2pkh_script(pubkeyhash) conf["scriptcode"] = spk conf["inputs"] = [getter("sign"), pkh] elif script is not None: @@ -496,7 +547,7 @@ def make_spender(comment, *, tap=None, witv0=False, script=None, pkh=None, p2sh= if p2sh: # P2SH wrapper can be combined with anything else conf["script_p2sh"] = spk - spk = CScript([OP_HASH160, hash160(spk), OP_EQUAL]) + spk = script_to_p2sh_script(spk) conf = {**conf, **kwargs} @@ -518,9 +569,9 @@ def add_spender(spenders, *args, **kwargs): def random_checksig_style(pubkey): """Creates a random CHECKSIG* tapscript that would succeed with only the valid signature on witness stack.""" opcode = random.choice([OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKSIGADD]) - if (opcode == OP_CHECKSIGVERIFY): + if opcode == OP_CHECKSIGVERIFY: ret = CScript([pubkey, opcode, OP_1]) - elif (opcode == OP_CHECKSIGADD): + elif opcode == OP_CHECKSIGADD: num = random.choice([0, 0x7fffffff, -0x7fffffff]) ret = CScript([num, pubkey, opcode, num + 1, OP_EQUAL]) else: @@ -1105,7 +1156,7 @@ def spenders_taproot_active(): for witv0 in [False, True]: for hashtype in VALID_SIGHASHES_ECDSA + [random.randrange(0x04, 0x80), random.randrange(0x84, 0x100)]: standard = (hashtype in VALID_SIGHASHES_ECDSA) and (compressed or not witv0) - add_spender(spenders, "legacy/pk-wrongkey", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, script=CScript([pubkey1, OP_CHECKSIG]), **SINGLE_SIG, key=eckey1, failure={"key": eckey2}, sigops_weight=4-3*witv0, **ERR_NO_SUCCESS) + add_spender(spenders, "legacy/pk-wrongkey", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, script=key_to_p2pk_script(pubkey1), **SINGLE_SIG, key=eckey1, failure={"key": eckey2}, sigops_weight=4-3*witv0, **ERR_NO_SUCCESS) add_spender(spenders, "legacy/pkh-sighashflip", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, pkh=pubkey1, key=eckey1, **SIGHASH_BITFLIP, sigops_weight=4-3*witv0, **ERR_NO_SUCCESS) # Verify that OP_CHECKSIGADD wasn't accidentally added to pre-taproot validation logic. @@ -1132,12 +1183,12 @@ def spenders_taproot_inactive(): tap = taproot_construct(pub, scripts) # Test that keypath spending is valid & non-standard, regardless of validity. - add_spender(spenders, "inactive/keypath_valid", key=sec, tap=tap, standard=False) + add_spender(spenders, "inactive/keypath_valid", key=sec, tap=tap, standard=Standard.V23) add_spender(spenders, "inactive/keypath_invalidsig", key=sec, tap=tap, standard=False, sighash=bitflipper(default_sighash)) add_spender(spenders, "inactive/keypath_empty", key=sec, tap=tap, standard=False, witness=[]) # Same for scriptpath spending (and features like annex, leaf versions, or OP_SUCCESS don't change this) - add_spender(spenders, "inactive/scriptpath_valid", key=sec, tap=tap, leaf="pk", standard=False, inputs=[getter("sign")]) + add_spender(spenders, "inactive/scriptpath_valid", key=sec, tap=tap, leaf="pk", standard=Standard.V23, inputs=[getter("sign")]) add_spender(spenders, "inactive/scriptpath_invalidsig", key=sec, tap=tap, leaf="pk", standard=False, inputs=[getter("sign")], sighash=bitflipper(default_sighash)) add_spender(spenders, "inactive/scriptpath_invalidcb", key=sec, tap=tap, leaf="pk", standard=False, inputs=[getter("sign")], controlblock=bitflipper(default_controlblock)) add_spender(spenders, "inactive/scriptpath_valid_unkleaf", key=sec, tap=tap, leaf="future_leaf", standard=False, inputs=[getter("sign")]) @@ -1167,7 +1218,7 @@ def dump_json_test(tx, input_utxos, idx, success, failure): # The "final" field indicates that a spend should be always valid, even with more validation flags enabled # than the listed ones. Use standardness as a proxy for this (which gives a conservative underestimate). - if spender.is_standard: + if spender.is_standard == Standard.ALL: fields.append(("final", True)) def dump_witness(wit): @@ -1189,19 +1240,36 @@ def dump_json_test(tx, input_utxos, idx, success, failure): # Data type to keep track of UTXOs, where they were created, and how to spend them. UTXOData = namedtuple('UTXOData', 'outpoint,output,spender') + class TaprootTest(BitcoinTestFramework): def add_options(self, parser): parser.add_argument("--dumptests", dest="dump_tests", default=False, action="store_true", help="Dump generated test cases to directory set by TEST_DUMP_DIR environment variable") + parser.add_argument("--previous_release", dest="previous_release", default=False, action="store_true", + help="Use a previous release as taproot-inactive node") def skip_test_if_missing_module(self): self.skip_if_no_wallet() + if self.options.previous_release: + self.skip_if_no_previous_releases() def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True # Node 0 has Taproot inactive, Node 1 active. - self.extra_args = [["-par=1", "-vbparams=taproot:1:1"], ["-par=1"]] + self.extra_args = [["-par=1"], ["-par=1"]] + if self.options.previous_release: + self.wallet_names = [None, self.default_wallet_name] + else: + self.extra_args[0].append("-vbparams=taproot:1:1") + + def setup_nodes(self): + self.add_nodes(self.num_nodes, self.extra_args, versions=[ + 200100 if self.options.previous_release else None, + None, + ]) + self.start_nodes() + self.import_deterministic_coinbase_privkeys() def block_submit(self, node, txs, msg, err_msg, cb_pubkey=None, fees=0, sigops_weight=0, witness=False, accept=False): @@ -1211,19 +1279,14 @@ class TaprootTest(BitcoinTestFramework): # transactions. extra_output_script = CScript([OP_CHECKSIG]*((MAX_BLOCK_SIGOPS_WEIGHT - sigops_weight) // WITNESS_SCALE_FACTOR)) - block = create_block(self.tip, create_coinbase(self.lastblockheight + 1, pubkey=cb_pubkey, extra_output_script=extra_output_script, fees=fees), self.lastblocktime + 1) - block.nVersion = 4 - for tx in txs: - tx.rehash() - block.vtx.append(tx) - block.hashMerkleRoot = block.calc_merkle_root() + coinbase_tx = create_coinbase(self.lastblockheight + 1, pubkey=cb_pubkey, extra_output_script=extra_output_script, fees=fees) + block = create_block(self.tip, coinbase_tx, self.lastblocktime + 1, txlist=txs) witness and add_witness_commitment(block) - block.rehash() block.solve() block_response = node.submitblock(block.serialize().hex()) if err_msg is not None: assert block_response is not None and err_msg in block_response, "Missing error message '%s' from block response '%s': %s" % (err_msg, "(None)" if block_response is None else block_response, msg) - if (accept): + if accept: assert node.getbestblockhash() == block.hash, "Failed to accept: %s (response: %s)" % (msg, block_response) self.tip = block.sha256 self.lastblockhash = block.hash @@ -1232,6 +1295,14 @@ class TaprootTest(BitcoinTestFramework): else: assert node.getbestblockhash() == self.lastblockhash, "Failed to reject: " + msg + def init_blockinfo(self, node): + # Initialize variables used by block_submit(). + self.lastblockhash = node.getbestblockhash() + self.tip = int(self.lastblockhash, 16) + block = node.getblock(self.lastblockhash) + self.lastblockheight = block['height'] + self.lastblocktime = block['time'] + def test_spenders(self, node, spenders, input_counts): """Run randomized tests with a number of "spenders". @@ -1258,12 +1329,7 @@ class TaprootTest(BitcoinTestFramework): host_spks.append(spk) host_pubkeys.append(bytes.fromhex(info['pubkey'])) - # Initialize variables used by block_submit(). - self.lastblockhash = node.getbestblockhash() - self.tip = int(self.lastblockhash, 16) - block = node.getblock(self.lastblockhash) - self.lastblockheight = block['height'] - self.lastblocktime = block['time'] + self.init_blockinfo(node) # Create transactions spending up to 50 of the wallet's inputs, with one output for each spender, and # one change output at the end. The transaction is constructed on the Python side to enable @@ -1305,7 +1371,7 @@ class TaprootTest(BitcoinTestFramework): # Add change fund_tx.vout.append(CTxOut(balance - 10000, random.choice(host_spks))) # Ask the wallet to sign - ss = BytesIO(bytes.fromhex(node.signrawtransactionwithwallet(ToHex(fund_tx))["hex"])) + ss = BytesIO(bytes.fromhex(node.signrawtransactionwithwallet(fund_tx.serialize().hex())["hex"])) fund_tx.deserialize(ss) # Construct UTXOData entries fund_tx.rehash() @@ -1417,8 +1483,13 @@ class TaprootTest(BitcoinTestFramework): for i in range(len(input_utxos)): tx.vin[i].scriptSig = input_data[i][i != fail_input][0] tx.wit.vtxinwit[i].scriptWitness.stack = input_data[i][i != fail_input][1] + taproot_spend_policy = Standard.V23 if node.version is None else Standard.ALL # Submit to mempool to check standardness - is_standard_tx = fail_input is None and all(utxo.spender.is_standard for utxo in input_utxos) and tx.nVersion >= 1 and tx.nVersion <= 2 + is_standard_tx = ( + fail_input is None # Must be valid to be standard + and (all(utxo.spender.is_standard == Standard.ALL or utxo.spender.is_standard == taproot_spend_policy for utxo in input_utxos)) # All inputs must be standard + and tx.nVersion >= 1 # The tx version must be standard + and tx.nVersion <= 2) tx.rehash() msg = ','.join(utxo.spender.comment + ("*" if n == fail_input else "") for n, utxo in enumerate(input_utxos)) if is_standard_tx: @@ -1437,10 +1508,239 @@ class TaprootTest(BitcoinTestFramework): assert len(mismatching_utxos) == 0 self.log.info(" - Done") + def gen_test_vectors(self): + """Run a scenario that corresponds (and optionally produces) to BIP341 test vectors.""" + + self.log.info("Unit test scenario...") + + # Deterministically mine coins to OP_TRUE in block 1 + assert self.nodes[1].getblockcount() == 0 + coinbase = CTransaction() + coinbase.nVersion = 1 + coinbase.vin = [CTxIn(COutPoint(0, 0xffffffff), CScript([OP_1, OP_1]), SEQUENCE_FINAL)] + coinbase.vout = [CTxOut(5000000000, CScript([OP_1]))] + coinbase.nLockTime = 0 + coinbase.rehash() + assert coinbase.hash == "f60c73405d499a956d3162e3483c395526ef78286458a4cb17b125aa92e49b20" + # Mine it + block = create_block(hashprev=int(self.nodes[1].getbestblockhash(), 16), coinbase=coinbase) + block.rehash() + block.solve() + self.nodes[1].submitblock(block.serialize().hex()) + assert self.nodes[1].getblockcount() == 1 + self.generate(self.nodes[1], COINBASE_MATURITY) + + SEED = 317 + VALID_LEAF_VERS = list(range(0xc0, 0x100, 2)) + [0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe] + # Generate private keys + prvs = [hashlib.sha256(SEED.to_bytes(2, 'big') + bytes([i])).digest() for i in range(100)] + # Generate corresponding public x-only pubkeys + pubs = [compute_xonly_pubkey(prv)[0] for prv in prvs] + # Generate taproot objects + inner_keys = [pubs[i] for i in range(7)] + + script_lists = [ + None, + [("0", CScript([pubs[50], OP_CHECKSIG]), 0xc0)], + [("0", CScript([pubs[51], OP_CHECKSIG]), 0xc0)], + [("0", CScript([pubs[52], OP_CHECKSIG]), 0xc0), ("1", CScript([b"BIP341"]), VALID_LEAF_VERS[pubs[99][0] % 41])], + [("0", CScript([pubs[53], OP_CHECKSIG]), 0xc0), ("1", CScript([b"Taproot"]), VALID_LEAF_VERS[pubs[99][1] % 41])], + [("0", CScript([pubs[54], OP_CHECKSIG]), 0xc0), [("1", CScript([pubs[55], OP_CHECKSIG]), 0xc0), ("2", CScript([pubs[56], OP_CHECKSIG]), 0xc0)]], + [("0", CScript([pubs[57], OP_CHECKSIG]), 0xc0), [("1", CScript([pubs[58], OP_CHECKSIG]), 0xc0), ("2", CScript([pubs[59], OP_CHECKSIG]), 0xc0)]], + ] + taps = [taproot_construct(inner_keys[i], script_lists[i]) for i in range(len(inner_keys))] + + # Require negated taps[0] + assert taps[0].negflag + # Require one negated and one non-negated in taps 1 and 2. + assert taps[1].negflag != taps[2].negflag + # Require one negated and one non-negated in taps 3 and 4. + assert taps[3].negflag != taps[4].negflag + # Require one negated and one non-negated in taps 5 and 6. + assert taps[5].negflag != taps[6].negflag + + cblks = [{leaf: get({**DEFAULT_CONTEXT, 'tap': taps[i], 'leaf': leaf}, 'controlblock') for leaf in taps[i].leaves} for i in range(7)] + # Require one swapped and one unswapped in taps 3 and 4. + assert (cblks[3]['0'][33:65] < cblks[3]['1'][33:65]) != (cblks[4]['0'][33:65] < cblks[4]['1'][33:65]) + # Require one swapped and one unswapped in taps 5 and 6, both at the top and child level. + assert (cblks[5]['0'][33:65] < cblks[5]['1'][65:]) != (cblks[6]['0'][33:65] < cblks[6]['1'][65:]) + assert (cblks[5]['1'][33:65] < cblks[5]['2'][33:65]) != (cblks[6]['1'][33:65] < cblks[6]['2'][33:65]) + # Require within taps 5 (and thus also 6) that one level is swapped and the other is not. + assert (cblks[5]['0'][33:65] < cblks[5]['1'][65:]) != (cblks[5]['1'][33:65] < cblks[5]['2'][33:65]) + + # Compute a deterministic set of scriptPubKeys + tap_spks = [] + old_spks = [] + spend_info = {} + # First, taproot scriptPubKeys, for the tap objects constructed above + for i, tap in enumerate(taps): + tap_spks.append(tap.scriptPubKey) + d = {'key': prvs[i], 'tap': tap, 'mode': 'taproot'} + spend_info[tap.scriptPubKey] = d + # Then, a number of deterministically generated (keys 0x1,0x2,0x3) with 2x P2PKH, 1x P2WPKH spks. + for i in range(1, 4): + prv = ECKey() + prv.set(i.to_bytes(32, 'big'), True) + pub = prv.get_pubkey().get_bytes() + d = {"key": prv} + d["scriptcode"] = key_to_p2pkh_script(pub) + d["inputs"] = [getter("sign"), pub] + if i < 3: + # P2PKH + d['spk'] = key_to_p2pkh_script(pub) + d['mode'] = 'legacy' + else: + # P2WPKH + d['spk'] = key_to_p2wpkh_script(pub) + d['mode'] = 'witv0' + old_spks.append(d['spk']) + spend_info[d['spk']] = d + + # Construct a deterministic chain of transactions creating UTXOs to the test's spk's (so that they + # come from distinct txids). + txn = [] + lasttxid = coinbase.sha256 + amount = 5000000000 + for i, spk in enumerate(old_spks + tap_spks): + val = 42000000 * (i + 7) + tx = CTransaction() + tx.nVersion = 1 + tx.vin = [CTxIn(COutPoint(lasttxid, i & 1), CScript([]), SEQUENCE_FINAL)] + tx.vout = [CTxOut(val, spk), CTxOut(amount - val, CScript([OP_1]))] + if i & 1: + tx.vout = list(reversed(tx.vout)) + tx.nLockTime = 0 + tx.rehash() + amount -= val + lasttxid = tx.sha256 + txn.append(tx) + spend_info[spk]['prevout'] = COutPoint(tx.sha256, i & 1) + spend_info[spk]['utxo'] = CTxOut(val, spk) + # Mine those transactions + self.init_blockinfo(self.nodes[1]) + self.block_submit(self.nodes[1], txn, "Crediting txn", None, sigops_weight=10, accept=True) + + # scriptPubKey computation + tests = {"version": 1} + spk_tests = tests.setdefault("scriptPubKey", []) + for i, tap in enumerate(taps): + test_case = {} + given = test_case.setdefault("given", {}) + given['internalPubkey'] = tap.internal_pubkey.hex() + + def pr(node): + if node is None: + return None + elif isinstance(node, tuple): + return {"id": int(node[0]), "script": node[1].hex(), "leafVersion": node[2]} + elif len(node) == 1: + return pr(node[0]) + elif len(node) == 2: + return [pr(node[0]), pr(node[1])] + else: + assert False + + given['scriptTree'] = pr(script_lists[i]) + intermediary = test_case.setdefault("intermediary", {}) + if len(tap.leaves): + leafhashes = intermediary.setdefault('leafHashes', [None] * len(tap.leaves)) + for leaf in tap.leaves: + leafhashes[int(leaf)] = tap.leaves[leaf].leaf_hash.hex() + intermediary['merkleRoot'] = tap.merkle_root.hex() if tap.merkle_root else None + intermediary['tweak'] = tap.tweak.hex() + intermediary['tweakedPubkey'] = tap.output_pubkey.hex() + expected = test_case.setdefault("expected", {}) + expected['scriptPubKey'] = tap.scriptPubKey.hex() + expected['bip350Address'] = program_to_witness(1, bytes(tap.output_pubkey), True) + if len(tap.leaves): + control_blocks = expected.setdefault("scriptPathControlBlocks", [None] * len(tap.leaves)) + for leaf in tap.leaves: + ctx = {**DEFAULT_CONTEXT, 'tap': tap, 'leaf': leaf} + control_blocks[int(leaf)] = get(ctx, "controlblock").hex() + spk_tests.append(test_case) + + # Construct a deterministic transaction spending all outputs created above. + tx = CTransaction() + tx.nVersion = 2 + tx.vin = [] + inputs = [] + input_spks = [tap_spks[0], tap_spks[1], old_spks[0], tap_spks[2], tap_spks[5], old_spks[2], tap_spks[6], tap_spks[3], tap_spks[4]] + sequences = [0, SEQUENCE_FINAL, SEQUENCE_FINAL, 0xfffffffe, 0xfffffffe, 0, 0, SEQUENCE_FINAL, SEQUENCE_FINAL] + hashtypes = [SIGHASH_SINGLE, SIGHASH_SINGLE|SIGHASH_ANYONECANPAY, SIGHASH_ALL, SIGHASH_ALL, SIGHASH_DEFAULT, SIGHASH_ALL, SIGHASH_NONE, SIGHASH_NONE|SIGHASH_ANYONECANPAY, SIGHASH_ALL|SIGHASH_ANYONECANPAY] + for i, spk in enumerate(input_spks): + tx.vin.append(CTxIn(spend_info[spk]['prevout'], CScript(), sequences[i])) + inputs.append(spend_info[spk]['utxo']) + tx.vout.append(CTxOut(1000000000, old_spks[1])) + tx.vout.append(CTxOut(3410000000, pubs[98])) + tx.nLockTime = 500000000 + precomputed = { + "hashAmounts": BIP341_sha_amounts(inputs), + "hashPrevouts": BIP341_sha_prevouts(tx), + "hashScriptPubkeys": BIP341_sha_scriptpubkeys(inputs), + "hashSequences": BIP341_sha_sequences(tx), + "hashOutputs": BIP341_sha_outputs(tx) + } + keypath_tests = tests.setdefault("keyPathSpending", []) + tx_test = {} + global_given = tx_test.setdefault("given", {}) + global_given['rawUnsignedTx'] = tx.serialize().hex() + utxos_spent = global_given.setdefault("utxosSpent", []) + for i in range(len(input_spks)): + utxos_spent.append({"scriptPubKey": inputs[i].scriptPubKey.hex(), "amountSats": inputs[i].nValue}) + global_intermediary = tx_test.setdefault("intermediary", {}) + for key in sorted(precomputed.keys()): + global_intermediary[key] = precomputed[key].hex() + test_list = tx_test.setdefault('inputSpending', []) + for i in range(len(input_spks)): + ctx = { + **DEFAULT_CONTEXT, + **spend_info[input_spks[i]], + 'tx': tx, + 'utxos': inputs, + 'idx': i, + 'hashtype': hashtypes[i], + 'deterministic': True + } + if ctx['mode'] == 'taproot': + test_case = {} + given = test_case.setdefault("given", {}) + given['txinIndex'] = i + given['internalPrivkey'] = get(ctx, 'key').hex() + if get(ctx, "tap").merkle_root != bytes(): + given['merkleRoot'] = get(ctx, "tap").merkle_root.hex() + else: + given['merkleRoot'] = None + given['hashType'] = get(ctx, "hashtype") + intermediary = test_case.setdefault("intermediary", {}) + intermediary['internalPubkey'] = get(ctx, "tap").internal_pubkey.hex() + intermediary['tweak'] = get(ctx, "tap").tweak.hex() + intermediary['tweakedPrivkey'] = get(ctx, "key_tweaked").hex() + sigmsg = get(ctx, "sigmsg") + intermediary['sigMsg'] = sigmsg.hex() + intermediary['precomputedUsed'] = [key for key in sorted(precomputed.keys()) if sigmsg.count(precomputed[key])] + intermediary['sigHash'] = get(ctx, "sighash").hex() + expected = test_case.setdefault("expected", {}) + expected['witness'] = [get(ctx, "sign").hex()] + test_list.append(test_case) + tx.wit.vtxinwit.append(CTxInWitness()) + tx.vin[i].scriptSig = CScript(flatten(get(ctx, "scriptsig"))) + tx.wit.vtxinwit[i].scriptWitness.stack = flatten(get(ctx, "witness")) + aux = tx_test.setdefault("auxiliary", {}) + aux['fullySignedTx'] = tx.serialize().hex() + keypath_tests.append(tx_test) + assert_equal(hashlib.sha256(tx.serialize()).hexdigest(), "24bab662cb55a7f3bae29b559f651674c62bcc1cd442d44715c0133939107b38") + # Mine the spending transaction + self.block_submit(self.nodes[1], [tx], "Spending txn", None, sigops_weight=10000, accept=True, witness=True) + + if GEN_TEST_VECTORS: + print(json.dumps(tests, indent=4, sort_keys=False)) + + def run_test(self): + self.gen_test_vectors() + # Post-taproot activation tests go first (pre-taproot tests' blocks are invalid post-taproot). self.log.info("Post-activation tests...") - self.nodes[1].generate(101) self.test_spenders(self.nodes[1], spenders_taproot_active(), input_counts=[1, 2, 2, 2, 2, 3]) # Re-connect nodes in case they have been disconnected @@ -1466,7 +1766,6 @@ class TaprootTest(BitcoinTestFramework): # Mine a block with the transaction block = create_block(tmpl=self.nodes[1].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS), txlist=[rawtx]) add_witness_commitment(block) - block.rehash() block.solve() assert_equal(None, self.nodes[1].submitblock(block.serialize().hex())) self.sync_blocks() diff --git a/test/functional/feature_txindex_compatibility.py b/test/functional/feature_txindex_compatibility.py new file mode 100755 index 0000000000..bbe1d1b537 --- /dev/null +++ b/test/functional/feature_txindex_compatibility.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test that legacy txindex will be disabled on upgrade. + +Previous releases are required by this test, see test/README.md. +""" + +import os +import shutil + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.wallet import MiniWallet + + +class MempoolCompatibilityTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 3 + self.extra_args = [ + ["-reindex", "-txindex"], + [], + [], + ] + + def skip_test_if_missing_module(self): + self.skip_if_no_previous_releases() + + def setup_network(self): + self.add_nodes( + self.num_nodes, + self.extra_args, + versions=[ + 160300, # Last release with legacy txindex + None, # For MiniWallet, without migration code + 200100, # Any release with migration code (0.17.x - 22.x) + ], + ) + self.start_nodes() + self.connect_nodes(0, 1) + self.connect_nodes(1, 2) + + def run_test(self): + mini_wallet = MiniWallet(self.nodes[1]) + mini_wallet.rescan_utxos() + spend_utxo = mini_wallet.get_utxo() + mini_wallet.send_self_transfer(from_node=self.nodes[1], utxo_to_spend=spend_utxo) + self.generate(self.nodes[1], 1) + + self.log.info("Check legacy txindex") + self.nodes[0].getrawtransaction(txid=spend_utxo["txid"]) # Requires -txindex + + self.stop_nodes() + legacy_chain_dir = os.path.join(self.nodes[0].datadir, self.chain) + + self.log.info("Migrate legacy txindex") + migrate_chain_dir = os.path.join(self.nodes[2].datadir, self.chain) + shutil.rmtree(migrate_chain_dir) + shutil.copytree(legacy_chain_dir, migrate_chain_dir) + with self.nodes[2].assert_debug_log([ + "Upgrading txindex database...", + "txindex is enabled at height 200", + ]): + self.start_node(2, extra_args=["-txindex"]) + self.nodes[2].getrawtransaction(txid=spend_utxo["txid"]) # Requires -txindex + + self.log.info("Drop legacy txindex") + drop_index_chain_dir = os.path.join(self.nodes[1].datadir, self.chain) + shutil.rmtree(drop_index_chain_dir) + shutil.copytree(legacy_chain_dir, drop_index_chain_dir) + self.nodes[1].assert_start_raises_init_error( + extra_args=["-txindex"], + expected_msg="Error: The block index db contains a legacy 'txindex'. To clear the occupied disk space, run a full -reindex, otherwise ignore this error. This error message will not be displayed again.", + ) + # Build txindex from scratch and check there is no error this time + self.start_node(1, extra_args=["-txindex"]) + self.nodes[2].getrawtransaction(txid=spend_utxo["txid"]) # Requires -txindex + + self.stop_nodes() + + self.log.info("Check migrated txindex can not be read by legacy node") + err_msg = f": You need to rebuild the database using -reindex to change -txindex.{os.linesep}Please restart with -reindex or -reindex-chainstate to recover." + shutil.rmtree(legacy_chain_dir) + shutil.copytree(migrate_chain_dir, legacy_chain_dir) + self.nodes[0].assert_start_raises_init_error(extra_args=["-txindex"], expected_msg=err_msg) + shutil.rmtree(legacy_chain_dir) + shutil.copytree(drop_index_chain_dir, legacy_chain_dir) + self.nodes[0].assert_start_raises_init_error(extra_args=["-txindex"], expected_msg=err_msg) + + +if __name__ == "__main__": + MempoolCompatibilityTest().main() diff --git a/test/functional/feature_utxo_set_hash.py b/test/functional/feature_utxo_set_hash.py index 6e6046d84d..75180e62a2 100755 --- a/test/functional/feature_utxo_set_hash.py +++ b/test/functional/feature_utxo_set_hash.py @@ -6,48 +6,39 @@ import struct -from test_framework.blocktools import create_transaction from test_framework.messages import ( CBlock, COutPoint, - FromHex, + from_hex, ) from test_framework.muhash import MuHash3072 from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet class UTXOSetHashTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - - def test_deterministic_hash_results(self): - self.log.info("Test deterministic UTXO set hash results") - - # These depend on the setup_clean_chain option, the chain loaded from the cache - assert_equal(self.nodes[0].gettxoutsetinfo()['hash_serialized_2'], "b32ec1dda5a53cd025b95387aad344a801825fe46a60ff952ce26528f01d3be8") - assert_equal(self.nodes[0].gettxoutsetinfo("muhash")['muhash'], "dd5ad2a105c2d29495f577245c357409002329b9f4d6182c0af3dc2f462555c8") - def test_muhash_implementation(self): self.log.info("Test MuHash implementation consistency") node = self.nodes[0] + wallet = MiniWallet(node) + mocktime = node.getblockheader(node.getblockhash(0))['time'] + 1 + node.setmocktime(mocktime) # Generate 100 blocks and remove the first since we plan to spend its # coinbase - block_hashes = node.generate(100) - blocks = list(map(lambda block: FromHex(CBlock(), node.getblock(block, False)), block_hashes)) - spending = blocks.pop(0) + block_hashes = self.generate(wallet, 1) + self.generate(node, 99) + blocks = list(map(lambda block: from_hex(CBlock(), node.getblock(block, False)), block_hashes)) + blocks.pop(0) # Create a spending transaction and mine a block which includes it - tx = create_transaction(node, spending.vtx[0].rehash(), node.getnewaddress(), amount=49) - txid = node.sendrawtransaction(hexstring=tx.serialize_with_witness().hex(), maxfeerate=0) - - tx_block = node.generateblock(output=node.getnewaddress(), transactions=[txid]) - blocks.append(FromHex(CBlock(), node.getblock(tx_block['hash'], False))) + txid = wallet.send_self_transfer(from_node=node)['txid'] + tx_block = self.generateblock(node, output=wallet.get_address(), transactions=[txid]) + blocks.append(from_hex(CBlock(), node.getblock(tx_block['hash'], False))) # Serialize the outputs that should be in the UTXO set and add them to # a MuHash object @@ -77,8 +68,11 @@ class UTXOSetHashTest(BitcoinTestFramework): assert_equal(finalized[::-1].hex(), node_muhash) + self.log.info("Test deterministic UTXO set hash results") + assert_equal(node.gettxoutsetinfo()['hash_serialized_2'], "3a570529b4c32e77268de1f81b903c75cc2da53c48df0d125c1e697ba7c8c7b7") + assert_equal(node.gettxoutsetinfo("muhash")['muhash'], "a13e0e70eb8acc786549596e3bc154623f1a5a622ba2f70715f6773ec745f435") + def run_test(self): - self.test_deterministic_hash_results() self.test_muhash_implementation() diff --git a/test/functional/feature_versionbits_warning.py b/test/functional/feature_versionbits_warning.py index 062a7affb5..e83dd7f446 100755 --- a/test/functional/feature_versionbits_warning.py +++ b/test/functional/feature_versionbits_warning.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test version bits warning system. @@ -21,20 +21,23 @@ VB_TOP_BITS = 0x20000000 VB_UNKNOWN_BIT = 27 # Choose a bit unassigned to any deployment VB_UNKNOWN_VERSION = VB_TOP_BITS | (1 << VB_UNKNOWN_BIT) -WARN_UNKNOWN_RULES_ACTIVE = "unknown new rules activated (versionbit {})".format(VB_UNKNOWN_BIT) -VB_PATTERN = re.compile("Warning: unknown new rules activated.*versionbit") +WARN_UNKNOWN_RULES_ACTIVE = f"Unknown new rules activated (versionbit {VB_UNKNOWN_BIT})" +VB_PATTERN = re.compile("Unknown new rules activated.*versionbit") class VersionBitsWarningTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 + # The experimental syscall sandbox feature (-sandbox) is not compatible with -alertnotify + # (which invokes execve). + self.disable_syscall_sandbox = True def setup_network(self): self.alert_filename = os.path.join(self.options.tmpdir, "alert.txt") # Open and close to create zero-length file with open(self.alert_filename, 'w', encoding='utf8'): pass - self.extra_args = [["-alertnotify=echo %s >> \"" + self.alert_filename + "\""]] + self.extra_args = [[f"-alertnotify=echo %s >> \"{self.alert_filename}\""]] self.setup_nodes() def send_blocks_with_version(self, peer, numblocks, version): @@ -45,8 +48,7 @@ class VersionBitsWarningTest(BitcoinTestFramework): tip = int(tip, 16) for _ in range(numblocks): - block = create_block(tip, create_coinbase(height + 1), block_time) - block.nVersion = version + block = create_block(tip, create_coinbase(height + 1), block_time, version=version) block.solve() peer.send_message(msg_block(block)) block_time += 1 @@ -65,12 +67,12 @@ class VersionBitsWarningTest(BitcoinTestFramework): node_deterministic_address = node.get_deterministic_priv_key().address # Mine one period worth of blocks - node.generatetoaddress(VB_PERIOD, node_deterministic_address) + self.generatetoaddress(node, VB_PERIOD, node_deterministic_address) self.log.info("Check that there is no warning if previous VB_BLOCKS have <VB_THRESHOLD blocks with unknown versionbits version.") # Build one period of blocks with < VB_THRESHOLD blocks signaling some unknown bit self.send_blocks_with_version(peer, VB_THRESHOLD - 1, VB_UNKNOWN_VERSION) - node.generatetoaddress(VB_PERIOD - VB_THRESHOLD + 1, node_deterministic_address) + self.generatetoaddress(node, VB_PERIOD - VB_THRESHOLD + 1, node_deterministic_address) # Check that we're not getting any versionbit-related errors in get*info() assert not VB_PATTERN.match(node.getmininginfo()["warnings"]) @@ -78,21 +80,21 @@ class VersionBitsWarningTest(BitcoinTestFramework): # Build one period of blocks with VB_THRESHOLD blocks signaling some unknown bit self.send_blocks_with_version(peer, VB_THRESHOLD, VB_UNKNOWN_VERSION) - node.generatetoaddress(VB_PERIOD - VB_THRESHOLD, node_deterministic_address) + self.generatetoaddress(node, VB_PERIOD - VB_THRESHOLD, node_deterministic_address) self.log.info("Check that there is a warning if previous VB_BLOCKS have >=VB_THRESHOLD blocks with unknown versionbits version.") # Mine a period worth of expected blocks so the generic block-version warning # is cleared. This will move the versionbit state to ACTIVE. - node.generatetoaddress(VB_PERIOD, node_deterministic_address) + self.generatetoaddress(node, VB_PERIOD, node_deterministic_address) # Stop-start the node. This is required because bitcoind will only warn once about unknown versions or unknown rules activating. self.restart_node(0) # Generating one block guarantees that we'll get out of IBD - node.generatetoaddress(1, node_deterministic_address) + self.generatetoaddress(node, 1, node_deterministic_address) self.wait_until(lambda: not node.getblockchaininfo()['initialblockdownload']) # Generating one more block will be enough to generate an error. - node.generatetoaddress(1, node_deterministic_address) + self.generatetoaddress(node, 1, node_deterministic_address) # Check that get*info() shows the versionbits unknown rules warning assert WARN_UNKNOWN_RULES_ACTIVE in node.getmininginfo()["warnings"] assert WARN_UNKNOWN_RULES_ACTIVE in node.getnetworkinfo()["warnings"] diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 2cf0ef2251..db5564ac50 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -1,22 +1,27 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test bitcoin-cli""" from decimal import Decimal +import re + +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than_or_equal, assert_raises_process_error, assert_raises_rpc_error, get_auth_cookie, ) +import time # The block reward of coinbaseoutput.nValue (50) BTC/block matures after # COINBASE_MATURITY (100) blocks. Therefore, after mining 101 blocks we expect # node 0 to have a balance of (BLOCKS - COINBASE_MATURITY) * 50 BTC/block. -BLOCKS = 101 +BLOCKS = COINBASE_MATURITY + 1 BALANCE = (BLOCKS - 100) * 50 JSON_PARSING_ERROR = 'error: Error parsing JSON: foo' @@ -25,11 +30,46 @@ TOO_MANY_ARGS = 'error: too many arguments (maximum 2 for nblocks and maxtries)' WALLET_NOT_LOADED = 'Requested wallet does not exist or is not loaded' WALLET_NOT_SPECIFIED = 'Wallet file not specified' + +def cli_get_info_string_to_dict(cli_get_info_string): + """Helper method to convert human-readable -getinfo into a dictionary""" + cli_get_info = {} + lines = cli_get_info_string.splitlines() + line_idx = 0 + ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]') + while line_idx < len(lines): + # Remove ansi colour code + line = ansi_escape.sub('', lines[line_idx]) + if "Balances" in line: + # When "Balances" appears in a line, all of the following lines contain "balance: wallet" until an empty line + cli_get_info["Balances"] = {} + while line_idx < len(lines) and not (lines[line_idx + 1] == ''): + line_idx += 1 + balance, wallet = lines[line_idx].strip().split(" ") + # Remove right justification padding + wallet = wallet.strip() + if wallet == '""': + # Set default wallet("") to empty string + wallet = '' + cli_get_info["Balances"][wallet] = balance.strip() + elif ": " in line: + key, value = line.split(": ") + if key == 'Wallet' and value == '""': + # Set default wallet("") to empty string + value = '' + if key == "Proxies" and value == "n/a": + # Set N/A to empty string to represent no proxy + value = '' + cli_get_info[key.strip()] = value.strip() + line_idx += 1 + return cli_get_info + + class TestBitcoinCli(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - if self.is_wallet_compiled(): + if self.is_specified_wallet_compiled(): self.requires_wallet = True def skip_test_if_missing_module(self): @@ -37,7 +77,7 @@ class TestBitcoinCli(BitcoinTestFramework): def run_test(self): """Main test logic""" - self.nodes[0].generate(BLOCKS) + self.generate(self.nodes[0], BLOCKS) self.log.info("Compare responses from getblockchaininfo RPC and `bitcoin-cli getblockchaininfo`") cli_response = self.nodes[0].cli.getblockchaininfo() @@ -47,12 +87,12 @@ class TestBitcoinCli(BitcoinTestFramework): user, password = get_auth_cookie(self.nodes[0].datadir, self.chain) self.log.info("Test -stdinrpcpass option") - assert_equal(BLOCKS, self.nodes[0].cli('-rpcuser={}'.format(user), '-stdinrpcpass', input=password).getblockcount()) - assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli('-rpcuser={}'.format(user), '-stdinrpcpass', input='foo').echo) + assert_equal(BLOCKS, self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input=password).getblockcount()) + assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input='foo').echo) self.log.info("Test -stdin and -stdinrpcpass") - assert_equal(['foo', 'bar'], self.nodes[0].cli('-rpcuser={}'.format(user), '-stdin', '-stdinrpcpass', input=password + '\nfoo\nbar').echo()) - assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli('-rpcuser={}'.format(user), '-stdin', '-stdinrpcpass', input='foo').echo) + assert_equal(['foo', 'bar'], self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input=f'{password}\nfoo\nbar').echo()) + assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input='foo').echo) self.log.info("Test connecting to a non-existing server") assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcport=1').echo) @@ -63,37 +103,53 @@ class TestBitcoinCli(BitcoinTestFramework): self.log.info("Test -getinfo with arguments fails") assert_raises_process_error(1, "-getinfo takes no arguments", self.nodes[0].cli('-getinfo').help) + self.log.info("Test -getinfo with -color=never does not return ANSI escape codes") + assert "\u001b[0m" not in self.nodes[0].cli('-getinfo', '-color=never').send_cli() + + self.log.info("Test -getinfo with -color=always returns ANSI escape codes") + assert "\u001b[0m" in self.nodes[0].cli('-getinfo', '-color=always').send_cli() + + self.log.info("Test -getinfo with invalid value for -color option") + assert_raises_process_error(1, "Invalid value for -color option. Valid values: always, auto, never.", self.nodes[0].cli('-getinfo', '-color=foo').send_cli) + self.log.info("Test -getinfo returns expected network and blockchain info") - if self.is_wallet_compiled(): + if self.is_specified_wallet_compiled(): self.nodes[0].encryptwallet(password) - cli_get_info = self.nodes[0].cli('-getinfo').send_cli() + cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() + cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) + network_info = self.nodes[0].getnetworkinfo() blockchain_info = self.nodes[0].getblockchaininfo() - assert_equal(cli_get_info['version'], network_info['version']) - assert_equal(cli_get_info['blocks'], blockchain_info['blocks']) - assert_equal(cli_get_info['headers'], blockchain_info['headers']) - assert_equal(cli_get_info['timeoffset'], network_info['timeoffset']) - assert_equal( - cli_get_info['connections'], - { - 'in': network_info['connections_in'], - 'out': network_info['connections_out'], - 'total': network_info['connections'] - } - ) - assert_equal(cli_get_info['proxy'], network_info['networks'][0]['proxy']) - assert_equal(cli_get_info['difficulty'], blockchain_info['difficulty']) - assert_equal(cli_get_info['chain'], blockchain_info['chain']) - - if self.is_wallet_compiled(): + assert_equal(int(cli_get_info['Version']), network_info['version']) + assert_equal(cli_get_info['Verification progress'], "%.4f%%" % (blockchain_info['verificationprogress'] * 100)) + assert_equal(int(cli_get_info['Blocks']), blockchain_info['blocks']) + assert_equal(int(cli_get_info['Headers']), blockchain_info['headers']) + assert_equal(int(cli_get_info['Time offset (s)']), network_info['timeoffset']) + expected_network_info = f"in {network_info['connections_in']}, out {network_info['connections_out']}, total {network_info['connections']}" + assert_equal(cli_get_info["Network"], expected_network_info) + assert_equal(cli_get_info['Proxies'], network_info['networks'][0]['proxy']) + assert_equal(Decimal(cli_get_info['Difficulty']), blockchain_info['difficulty']) + assert_equal(cli_get_info['Chain'], blockchain_info['chain']) + + self.log.info("Test -getinfo and bitcoin-cli return all proxies") + self.restart_node(0, extra_args=["-proxy=127.0.0.1:9050", "-i2psam=127.0.0.1:7656"]) + network_info = self.nodes[0].getnetworkinfo() + cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() + cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) + assert_equal(cli_get_info["Proxies"], "127.0.0.1:9050 (ipv4, ipv6, onion, cjdns), 127.0.0.1:7656 (i2p)") + + if self.is_specified_wallet_compiled(): self.log.info("Test -getinfo and bitcoin-cli getwalletinfo return expected wallet info") - assert_equal(cli_get_info['balance'], BALANCE) - assert 'balances' not in cli_get_info.keys() + # Explicitly set the output type in order to have consistent tx vsize / fees + # for both legacy and descriptor wallets (disables the change address type detection algorithm) + self.restart_node(0, extra_args=["-addresstype=bech32", "-changetype=bech32"]) + assert_equal(Decimal(cli_get_info['Balance']), BALANCE) + assert 'Balances' not in cli_get_info_string wallet_info = self.nodes[0].getwalletinfo() - assert_equal(cli_get_info['keypoolsize'], wallet_info['keypoolsize']) - assert_equal(cli_get_info['unlocked_until'], wallet_info['unlocked_until']) - assert_equal(cli_get_info['paytxfee'], wallet_info['paytxfee']) - assert_equal(cli_get_info['relayfee'], network_info['relayfee']) + assert_equal(int(cli_get_info['Keypool size']), wallet_info['keypoolsize']) + assert_equal(int(cli_get_info['Unlocked until']), wallet_info['unlocked_until']) + assert_equal(Decimal(cli_get_info['Transaction fee rate (-paytxfee) (BTC/kvB)']), wallet_info['paytxfee']) + assert_equal(Decimal(cli_get_info['Min tx relay fee rate (BTC/kvB)']), network_info['relayfee']) assert_equal(self.nodes[0].cli.getwalletinfo(), wallet_info) # Setup to test -getinfo, -generate, and -rpcwallet= with multiple wallets. @@ -104,56 +160,69 @@ class TestBitcoinCli(BitcoinTestFramework): w1 = self.nodes[0].get_wallet_rpc(wallets[0]) w2 = self.nodes[0].get_wallet_rpc(wallets[1]) w3 = self.nodes[0].get_wallet_rpc(wallets[2]) - rpcwallet2 = '-rpcwallet={}'.format(wallets[1]) - rpcwallet3 = '-rpcwallet={}'.format(wallets[2]) + rpcwallet2 = f'-rpcwallet={wallets[1]}' + rpcwallet3 = f'-rpcwallet={wallets[2]}' w1.walletpassphrase(password, self.rpc_timeout) w2.encryptwallet(password) w1.sendtoaddress(w2.getnewaddress(), amounts[1]) w1.sendtoaddress(w3.getnewaddress(), amounts[2]) # Mine a block to confirm; adds a block reward (50 BTC) to the default wallet. - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) self.log.info("Test -getinfo with multiple wallets and -rpcwallet returns specified wallet balance") for i in range(len(wallets)): - cli_get_info = self.nodes[0].cli('-getinfo', '-rpcwallet={}'.format(wallets[i])).send_cli() - assert 'balances' not in cli_get_info.keys() - assert_equal(cli_get_info['balance'], amounts[i]) + cli_get_info_string = self.nodes[0].cli('-getinfo', f'-rpcwallet={wallets[i]}').send_cli() + cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) + assert 'Balances' not in cli_get_info_string + assert_equal(cli_get_info["Wallet"], wallets[i]) + assert_equal(Decimal(cli_get_info['Balance']), amounts[i]) self.log.info("Test -getinfo with multiple wallets and -rpcwallet=non-existing-wallet returns no balances") - cli_get_info_keys = self.nodes[0].cli('-getinfo', '-rpcwallet=does-not-exist').send_cli().keys() - assert 'balance' not in cli_get_info_keys - assert 'balances' not in cli_get_info_keys + cli_get_info_string = self.nodes[0].cli('-getinfo', '-rpcwallet=does-not-exist').send_cli() + assert 'Balance' not in cli_get_info_string + assert 'Balances' not in cli_get_info_string self.log.info("Test -getinfo with multiple wallets returns all loaded wallet names and balances") assert_equal(set(self.nodes[0].listwallets()), set(wallets)) - cli_get_info = self.nodes[0].cli('-getinfo').send_cli() - assert 'balance' not in cli_get_info.keys() - assert_equal(cli_get_info['balances'], {k: v for k, v in zip(wallets, amounts)}) + cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() + cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) + assert 'Balance' not in cli_get_info + for k, v in zip(wallets, amounts): + assert_equal(Decimal(cli_get_info['Balances'][k]), v) # Unload the default wallet and re-verify. self.nodes[0].unloadwallet(wallets[0]) assert wallets[0] not in self.nodes[0].listwallets() - cli_get_info = self.nodes[0].cli('-getinfo').send_cli() - assert 'balance' not in cli_get_info.keys() - assert_equal(cli_get_info['balances'], {k: v for k, v in zip(wallets[1:], amounts[1:])}) + cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() + cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) + assert 'Balance' not in cli_get_info + assert 'Balances' in cli_get_info_string + for k, v in zip(wallets[1:], amounts[1:]): + assert_equal(Decimal(cli_get_info['Balances'][k]), v) + assert wallets[0] not in cli_get_info self.log.info("Test -getinfo after unloading all wallets except a non-default one returns its balance") self.nodes[0].unloadwallet(wallets[2]) assert_equal(self.nodes[0].listwallets(), [wallets[1]]) - cli_get_info = self.nodes[0].cli('-getinfo').send_cli() - assert 'balances' not in cli_get_info.keys() - assert_equal(cli_get_info['balance'], amounts[1]) + cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() + cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) + assert 'Balances' not in cli_get_info_string + assert_equal(cli_get_info['Wallet'], wallets[1]) + assert_equal(Decimal(cli_get_info['Balance']), amounts[1]) self.log.info("Test -getinfo with -rpcwallet=remaining-non-default-wallet returns only its balance") - cli_get_info = self.nodes[0].cli('-getinfo', rpcwallet2).send_cli() - assert 'balances' not in cli_get_info.keys() - assert_equal(cli_get_info['balance'], amounts[1]) + cli_get_info_string = self.nodes[0].cli('-getinfo', rpcwallet2).send_cli() + cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) + assert 'Balances' not in cli_get_info_string + assert_equal(cli_get_info['Wallet'], wallets[1]) + assert_equal(Decimal(cli_get_info['Balance']), amounts[1]) self.log.info("Test -getinfo with -rpcwallet=unloaded wallet returns no balances") - cli_get_info = self.nodes[0].cli('-getinfo', rpcwallet3).send_cli() - assert 'balance' not in cli_get_info_keys - assert 'balances' not in cli_get_info_keys + cli_get_info_string = self.nodes[0].cli('-getinfo', rpcwallet3).send_cli() + cli_get_info_keys = cli_get_info_string_to_dict(cli_get_info_string) + assert 'Balance' not in cli_get_info_keys + assert 'Balances' not in cli_get_info_string # Test bitcoin-cli -generate. n1 = 3 @@ -232,12 +301,12 @@ class TestBitcoinCli(BitcoinTestFramework): assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, self.nodes[0].cli('-generate', 1, 2, 3).echo) else: self.log.info("*** Wallet not compiled; cli getwalletinfo and -getinfo wallet tests skipped") - self.nodes[0].generate(25) # maintain block parity with the wallet_compiled conditional branch + self.generate(self.nodes[0], 25) # maintain block parity with the wallet_compiled conditional branch self.log.info("Test -version with node stopped") self.stop_node(0) cli_response = self.nodes[0].cli('-version').send_cli() - assert "{} RPC client version".format(self.config['environment']['PACKAGE_NAME']) in cli_response + assert f"{self.config['environment']['PACKAGE_NAME']} RPC client version" in cli_response self.log.info("Test -rpcwait option successfully waits for RPC connection") self.nodes[0].start() # start node without RPC connection @@ -246,6 +315,12 @@ class TestBitcoinCli(BitcoinTestFramework): self.nodes[0].wait_for_rpc_connection() assert_equal(blocks, BLOCKS + 25) + self.log.info("Test -rpcwait option waits at most -rpcwaittimeout seconds for startup") + self.stop_node(0) # stop the node so we time out + start_time = time.time() + assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcwait', '-rpcwaittimeout=5').echo) + assert_greater_than_or_equal(time.time(), start_time + 5) + if __name__ == '__main__': TestBitcoinCli().main() diff --git a/test/functional/interface_http.py b/test/functional/interface_http.py index d007490f80..6e32009e05 100755 --- a/test/functional/interface_http.py +++ b/test/functional/interface_http.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the RPC HTTP basics.""" @@ -24,8 +24,8 @@ class HTTPBasicsTest (BitcoinTestFramework): # lowlevel check for http persistent connection # ################################################# url = urllib.parse.urlparse(self.nodes[0].url) - authpair = url.username + ':' + url.password - headers = {"Authorization": "Basic " + str_to_b64str(authpair)} + authpair = f'{url.username}:{url.password}' + headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} conn = http.client.HTTPConnection(url.hostname, url.port) conn.connect() @@ -42,7 +42,7 @@ class HTTPBasicsTest (BitcoinTestFramework): conn.close() #same should be if we add keep-alive because this should be the std. behaviour - headers = {"Authorization": "Basic " + str_to_b64str(authpair), "Connection": "keep-alive"} + headers = {"Authorization": f"Basic {str_to_b64str(authpair)}", "Connection": "keep-alive"} conn = http.client.HTTPConnection(url.hostname, url.port) conn.connect() @@ -59,7 +59,7 @@ class HTTPBasicsTest (BitcoinTestFramework): conn.close() #now do the same with "Connection: close" - headers = {"Authorization": "Basic " + str_to_b64str(authpair), "Connection":"close"} + headers = {"Authorization": f"Basic {str_to_b64str(authpair)}", "Connection":"close"} conn = http.client.HTTPConnection(url.hostname, url.port) conn.connect() @@ -70,8 +70,8 @@ class HTTPBasicsTest (BitcoinTestFramework): #node1 (2nd node) is running with disabled keep-alive option urlNode1 = urllib.parse.urlparse(self.nodes[1].url) - authpair = urlNode1.username + ':' + urlNode1.password - headers = {"Authorization": "Basic " + str_to_b64str(authpair)} + authpair = f'{urlNode1.username}:{urlNode1.password}' + headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} conn = http.client.HTTPConnection(urlNode1.hostname, urlNode1.port) conn.connect() @@ -81,8 +81,8 @@ class HTTPBasicsTest (BitcoinTestFramework): #node2 (third node) is running with standard keep-alive parameters which means keep-alive is on urlNode2 = urllib.parse.urlparse(self.nodes[2].url) - authpair = urlNode2.username + ':' + urlNode2.password - headers = {"Authorization": "Basic " + str_to_b64str(authpair)} + authpair = f'{urlNode2.username}:{urlNode2.password}' + headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) conn.connect() @@ -94,13 +94,13 @@ class HTTPBasicsTest (BitcoinTestFramework): # Check excessive request size conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) conn.connect() - conn.request('GET', '/' + ('x'*1000), '', headers) + conn.request('GET', f'/{"x"*1000}', '', headers) out1 = conn.getresponse() assert_equal(out1.status, http.client.NOT_FOUND) conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) conn.connect() - conn.request('GET', '/' + ('x'*10000), '', headers) + conn.request('GET', f'/{"x"*10000}', '', headers) out1 = conn.getresponse() assert_equal(out1.status, http.client.BAD_REQUEST) diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index e73ec90819..a3d949c6a8 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -1,28 +1,37 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the REST API.""" -import binascii from decimal import Decimal from enum import Enum +import http.client from io import BytesIO import json from struct import pack, unpack - -import http.client import urllib.parse + +from test_framework.messages import ( + BLOCK_HEADER_SIZE, + COIN, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than, assert_greater_than_or_equal, - hex_str_to_bytes, +) +from test_framework.wallet import ( + MiniWallet, + getnewdestination, ) -from test_framework.messages import BLOCK_HEADER_SIZE + +INVALID_PARAM = "abc" +UNKNOWN_PARAM = "0000000000000000000000000000000000000000000000000000000000000000" + class ReqType(Enum): JSON = 1 @@ -41,14 +50,13 @@ def filter_output_indices_by_value(vouts, value): class RESTTest (BitcoinTestFramework): def set_test_params(self): - self.setup_clean_chain = True self.num_nodes = 2 - self.extra_args = [["-rest"], []] + self.extra_args = [["-rest", "-blockfilterindex=1"], []] + # whitelist peers to speed up tx relay / mempool sync + for args in self.extra_args: + args.append("-whitelist=noban@127.0.0.1") self.supports_cli = False - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def test_rest_request(self, uri, http_method='GET', req_type=ReqType.JSON, body='', status=200, ret_type=RetType.JSON): rest_uri = '/rest' + uri if req_type == ReqType.JSON: @@ -59,7 +67,7 @@ class RESTTest (BitcoinTestFramework): rest_uri += '.hex' conn = http.client.HTTPConnection(self.url.hostname, self.url.port) - self.log.debug('%s %s %s', http_method, rest_uri, body) + self.log.debug(f'{http_method} {rest_uri} {body}') if http_method == 'GET': conn.request('GET', rest_uri) elif http_method == 'POST': @@ -77,28 +85,20 @@ class RESTTest (BitcoinTestFramework): def run_test(self): self.url = urllib.parse.urlparse(self.nodes[0].url) - self.log.info("Mine blocks and send Bitcoin to node 1") + self.wallet = MiniWallet(self.nodes[0]) + self.wallet.rescan_utxos() - # Random address so node1's balance doesn't increase - not_related_address = "2MxqoHEdNQTyYeX1mHcbrrpzgojbosTpCvJ" - - self.nodes[0].generate(1) - self.sync_all() - self.nodes[1].generatetoaddress(100, not_related_address) - self.sync_all() - - assert_equal(self.nodes[0].getbalance(), 50) - - txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.1) + self.log.info("Broadcast test transaction and sync nodes") + txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=int(0.1 * COIN)) self.sync_all() self.log.info("Test the /tx URI") - json_obj = self.test_rest_request("/tx/{}".format(txid)) + json_obj = self.test_rest_request(f"/tx/{txid}") assert_equal(json_obj['txid'], txid) # Check hex format response - hex_response = self.test_rest_request("/tx/{}".format(txid), req_type=ReqType.HEX, ret_type=RetType.OBJ) + hex_response = self.test_rest_request(f"/tx/{txid}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_greater_than_or_equal(int(hex_response.getheader('content-length')), json_obj['size']*2) @@ -107,16 +107,19 @@ class RESTTest (BitcoinTestFramework): n, = filter_output_indices_by_value(json_obj['vout'], Decimal('0.1')) spending = (txid, n) + # Test /tx with an invalid and an unknown txid + resp = self.test_rest_request(uri=f"/tx/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400) + assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid hash: {INVALID_PARAM}") + resp = self.test_rest_request(uri=f"/tx/{UNKNOWN_PARAM}", ret_type=RetType.OBJ, status=404) + assert_equal(resp.read().decode('utf-8').rstrip(), f"{UNKNOWN_PARAM} not found") + self.log.info("Query an unspent TXO using the /getutxos URI") - self.nodes[1].generatetoaddress(1, not_related_address) - self.sync_all() + self.generate(self.wallet, 1) bb_hash = self.nodes[0].getbestblockhash() - assert_equal(self.nodes[1].getbalance(), Decimal("0.1")) - # Check chainTip response - json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spending)) + json_obj = self.test_rest_request(f"/getutxos/{spending[0]}-{spending[1]}") assert_equal(json_obj['chaintipHash'], bb_hash) # Make sure there is one utxo @@ -125,7 +128,7 @@ class RESTTest (BitcoinTestFramework): self.log.info("Query a spent TXO using the /getutxos URI") - json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spent)) + json_obj = self.test_rest_request(f"/getutxos/{spent[0]}-{spent[1]}") # Check chainTip response assert_equal(json_obj['chaintipHash'], bb_hash) @@ -138,7 +141,7 @@ class RESTTest (BitcoinTestFramework): self.log.info("Query two TXOs using the /getutxos URI") - json_obj = self.test_rest_request("/getutxos/{}-{}/{}-{}".format(*(spending + spent))) + json_obj = self.test_rest_request(f"/getutxos/{spending[0]}-{spending[1]}/{spent[0]}-{spent[1]}") assert_equal(len(json_obj['utxos']), 1) assert_equal(json_obj['bitmap'], "10") @@ -147,7 +150,7 @@ class RESTTest (BitcoinTestFramework): bin_request = b'\x01\x02' for txid, n in [spending, spent]: - bin_request += hex_str_to_bytes(txid) + bin_request += bytes.fromhex(txid) bin_request += pack("i", n) bin_response = self.test_rest_request("/getutxos", http_method='POST', req_type=ReqType.BIN, body=bin_request, ret_type=RetType.BYTES) @@ -156,7 +159,7 @@ class RESTTest (BitcoinTestFramework): response_hash = output.read(32)[::-1].hex() assert_equal(bb_hash, response_hash) # check if getutxo's chaintip during calculation was fine - assert_equal(chain_height, 102) # chain height must be 102 + assert_equal(chain_height, 201) # chain height must be 201 (pre-mined chain [200] + generated block [1]) self.log.info("Test the /getutxos URI with and without /checkmempool") # Create a transaction, check that it's found with /checkmempool, but @@ -164,33 +167,32 @@ class RESTTest (BitcoinTestFramework): # found with or without /checkmempool. # do a tx and don't sync - txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.1) - json_obj = self.test_rest_request("/tx/{}".format(txid)) + txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=int(0.1 * COIN)) + json_obj = self.test_rest_request(f"/tx/{txid}") # get the spent output to later check for utxo (should be spent by then) spent = (json_obj['vin'][0]['txid'], json_obj['vin'][0]['vout']) # get n of 0.1 outpoint n, = filter_output_indices_by_value(json_obj['vout'], Decimal('0.1')) spending = (txid, n) - json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spending)) + json_obj = self.test_rest_request(f"/getutxos/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 0) - json_obj = self.test_rest_request("/getutxos/checkmempool/{}-{}".format(*spending)) + json_obj = self.test_rest_request(f"/getutxos/checkmempool/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 1) - json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spent)) + json_obj = self.test_rest_request(f"/getutxos/{spent[0]}-{spent[1]}") assert_equal(len(json_obj['utxos']), 1) - json_obj = self.test_rest_request("/getutxos/checkmempool/{}-{}".format(*spent)) + json_obj = self.test_rest_request(f"/getutxos/checkmempool/{spent[0]}-{spent[1]}") assert_equal(len(json_obj['utxos']), 0) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) - json_obj = self.test_rest_request("/getutxos/{}-{}".format(*spending)) + json_obj = self.test_rest_request(f"/getutxos/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 1) - json_obj = self.test_rest_request("/getutxos/checkmempool/{}-{}".format(*spending)) + json_obj = self.test_rest_request(f"/getutxos/checkmempool/{spending[0]}-{spending[1]}") assert_equal(len(json_obj['utxos']), 1) # Do some invalid requests @@ -199,66 +201,65 @@ class RESTTest (BitcoinTestFramework): self.test_rest_request("/getutxos/checkmempool", http_method='POST', req_type=ReqType.JSON, status=400, ret_type=RetType.OBJ) # Test limits - long_uri = '/'.join(["{}-{}".format(txid, n_) for n_ in range(20)]) - self.test_rest_request("/getutxos/checkmempool/{}".format(long_uri), http_method='POST', status=400, ret_type=RetType.OBJ) + long_uri = '/'.join([f"{txid}-{n_}" for n_ in range(20)]) + self.test_rest_request(f"/getutxos/checkmempool/{long_uri}", http_method='POST', status=400, ret_type=RetType.OBJ) - long_uri = '/'.join(['{}-{}'.format(txid, n_) for n_ in range(15)]) - self.test_rest_request("/getutxos/checkmempool/{}".format(long_uri), http_method='POST', status=200) + long_uri = '/'.join([f'{txid}-{n_}' for n_ in range(15)]) + self.test_rest_request(f"/getutxos/checkmempool/{long_uri}", http_method='POST', status=200) - self.nodes[0].generate(1) # generate block to not affect upcoming tests - self.sync_all() + self.generate(self.nodes[0], 1) # generate block to not affect upcoming tests self.log.info("Test the /block, /blockhashbyheight and /headers URIs") bb_hash = self.nodes[0].getbestblockhash() # Check result if block does not exists - assert_equal(self.test_rest_request('/headers/1/0000000000000000000000000000000000000000000000000000000000000000'), []) - self.test_rest_request('/block/0000000000000000000000000000000000000000000000000000000000000000', status=404, ret_type=RetType.OBJ) + assert_equal(self.test_rest_request(f"/headers/1/{UNKNOWN_PARAM}"), []) + self.test_rest_request(f"/block/{UNKNOWN_PARAM}", status=404, ret_type=RetType.OBJ) # Check result if block is not in the active chain self.nodes[0].invalidateblock(bb_hash) - assert_equal(self.test_rest_request('/headers/1/{}'.format(bb_hash)), []) - self.test_rest_request('/block/{}'.format(bb_hash)) + assert_equal(self.test_rest_request(f'/headers/1/{bb_hash}'), []) + self.test_rest_request(f'/block/{bb_hash}') self.nodes[0].reconsiderblock(bb_hash) # Check binary format - response = self.test_rest_request("/block/{}".format(bb_hash), req_type=ReqType.BIN, ret_type=RetType.OBJ) + response = self.test_rest_request(f"/block/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ) assert_greater_than(int(response.getheader('content-length')), BLOCK_HEADER_SIZE) response_bytes = response.read() # Compare with block header - response_header = self.test_rest_request("/headers/1/{}".format(bb_hash), req_type=ReqType.BIN, ret_type=RetType.OBJ) + response_header = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ) assert_equal(int(response_header.getheader('content-length')), BLOCK_HEADER_SIZE) response_header_bytes = response_header.read() assert_equal(response_bytes[:BLOCK_HEADER_SIZE], response_header_bytes) # Check block hex format - response_hex = self.test_rest_request("/block/{}".format(bb_hash), req_type=ReqType.HEX, ret_type=RetType.OBJ) + response_hex = self.test_rest_request(f"/block/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_greater_than(int(response_hex.getheader('content-length')), BLOCK_HEADER_SIZE*2) response_hex_bytes = response_hex.read().strip(b'\n') - assert_equal(binascii.hexlify(response_bytes), response_hex_bytes) + assert_equal(response_bytes.hex().encode(), response_hex_bytes) # Compare with hex block header - response_header_hex = self.test_rest_request("/headers/1/{}".format(bb_hash), req_type=ReqType.HEX, ret_type=RetType.OBJ) + response_header_hex = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_greater_than(int(response_header_hex.getheader('content-length')), BLOCK_HEADER_SIZE*2) response_header_hex_bytes = response_header_hex.read(BLOCK_HEADER_SIZE*2) - assert_equal(binascii.hexlify(response_bytes[:BLOCK_HEADER_SIZE]), response_header_hex_bytes) + assert_equal(response_bytes[:BLOCK_HEADER_SIZE].hex().encode(), response_header_hex_bytes) # Check json format - block_json_obj = self.test_rest_request("/block/{}".format(bb_hash)) + block_json_obj = self.test_rest_request(f"/block/{bb_hash}") assert_equal(block_json_obj['hash'], bb_hash) - assert_equal(self.test_rest_request("/blockhashbyheight/{}".format(block_json_obj['height']))['blockhash'], bb_hash) + assert_equal(self.test_rest_request(f"/blockhashbyheight/{block_json_obj['height']}")['blockhash'], bb_hash) # Check hex/bin format - resp_hex = self.test_rest_request("/blockhashbyheight/{}".format(block_json_obj['height']), req_type=ReqType.HEX, ret_type=RetType.OBJ) + resp_hex = self.test_rest_request(f"/blockhashbyheight/{block_json_obj['height']}", req_type=ReqType.HEX, ret_type=RetType.OBJ) assert_equal(resp_hex.read().decode('utf-8').rstrip(), bb_hash) - resp_bytes = self.test_rest_request("/blockhashbyheight/{}".format(block_json_obj['height']), req_type=ReqType.BIN, ret_type=RetType.BYTES) + resp_bytes = self.test_rest_request(f"/blockhashbyheight/{block_json_obj['height']}", req_type=ReqType.BIN, ret_type=RetType.BYTES) blockhash = resp_bytes[::-1].hex() assert_equal(blockhash, bb_hash) # Check invalid blockhashbyheight requests - resp = self.test_rest_request("/blockhashbyheight/abc", ret_type=RetType.OBJ, status=400) - assert_equal(resp.read().decode('utf-8').rstrip(), "Invalid height: abc") + resp = self.test_rest_request(f"/blockhashbyheight/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400) + assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid height: {INVALID_PARAM}") resp = self.test_rest_request("/blockhashbyheight/1000000", ret_type=RetType.OBJ, status=404) assert_equal(resp.read().decode('utf-8').rstrip(), "Block height out of range") resp = self.test_rest_request("/blockhashbyheight/-1", ret_type=RetType.OBJ, status=400) @@ -266,7 +267,7 @@ class RESTTest (BitcoinTestFramework): self.test_rest_request("/blockhashbyheight/", ret_type=RetType.OBJ, status=400) # Compare with json block header - json_obj = self.test_rest_request("/headers/1/{}".format(bb_hash)) + json_obj = self.test_rest_request(f"/headers/1/{bb_hash}") assert_equal(len(json_obj), 1) # ensure that there is one header in the json response assert_equal(json_obj[0]['hash'], bb_hash) # request/response hash should be the same @@ -276,18 +277,35 @@ class RESTTest (BitcoinTestFramework): assert_equal(json_obj[0][key], rpc_block_json[key]) # See if we can get 5 headers in one response - self.nodes[1].generate(5) - self.sync_all() - json_obj = self.test_rest_request("/headers/5/{}".format(bb_hash)) + self.generate(self.nodes[1], 5) + json_obj = self.test_rest_request(f"/headers/5/{bb_hash}") assert_equal(len(json_obj), 5) # now we should have 5 header objects + json_obj = self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}") + first_filter_header = json_obj[0] + assert_equal(len(json_obj), 5) # now we should have 5 filter header objects + json_obj = self.test_rest_request(f"/blockfilter/basic/{bb_hash}") + + # Compare with normal RPC blockfilter response + rpc_blockfilter = self.nodes[0].getblockfilter(bb_hash) + assert_equal(first_filter_header, rpc_blockfilter['header']) + assert_equal(json_obj['filter'], rpc_blockfilter['filter']) + + # Test number parsing + for num in ['5a', '-5', '0', '2001', '99999999999999999999999999999999999']: + assert_equal( + bytes(f'Header count is invalid or out of acceptable range (1-2000): {num}\r\n', 'ascii'), + self.test_rest_request(f"/headers/{num}/{bb_hash}", ret_type=RetType.BYTES, status=400), + ) self.log.info("Test tx inclusion in the /mempool and /block URIs") - # Make 3 tx and mine them on node 1 + # Make 3 chained txs and mine them on node 1 txs = [] - txs.append(self.nodes[0].sendtoaddress(not_related_address, 11)) - txs.append(self.nodes[0].sendtoaddress(not_related_address, 11)) - txs.append(self.nodes[0].sendtoaddress(not_related_address, 11)) + input_txid = txid + for _ in range(3): + utxo_to_spend = self.wallet.get_utxo(txid=input_txid) + txs.append(self.wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_to_spend)['txid']) + input_txid = txs[-1] self.sync_all() # Check that there are exactly 3 transactions in the TX memory pool before generating the block @@ -304,17 +322,25 @@ class RESTTest (BitcoinTestFramework): assert_equal(json_obj[tx]['depends'], txs[i - 1:i]) # Now mine the transactions - newblockhash = self.nodes[1].generate(1) - self.sync_all() + newblockhash = self.generate(self.nodes[1], 1) # Check if the 3 tx show up in the new block - json_obj = self.test_rest_request("/block/{}".format(newblockhash[0])) + json_obj = self.test_rest_request(f"/block/{newblockhash[0]}") non_coinbase_txs = {tx['txid'] for tx in json_obj['tx'] if 'coinbase' not in tx['vin'][0]} assert_equal(non_coinbase_txs, set(txs)) + # Verify that the non-coinbase tx has "prevout" key set + for tx_obj in json_obj["tx"]: + for vin in tx_obj["vin"]: + if "coinbase" not in vin: + assert "prevout" in vin + assert_equal(vin["prevout"]["generated"], False) + else: + assert "prevout" not in vin + # Check the same but without tx details - json_obj = self.test_rest_request("/block/notxdetails/{}".format(newblockhash[0])) + json_obj = self.test_rest_request(f"/block/notxdetails/{newblockhash[0]}") for tx in txs: assert tx in json_obj['tx'] @@ -325,5 +351,6 @@ class RESTTest (BitcoinTestFramework): json_obj = self.test_rest_request("/chaininfo") assert_equal(json_obj['bestblockhash'], bb_hash) + if __name__ == '__main__': RESTTest().main() diff --git a/test/functional/interface_rpc.py b/test/functional/interface_rpc.py index 9c877aaeae..48082f3a17 100755 --- a/test/functional/interface_rpc.py +++ b/test/functional/interface_rpc.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Tests some generic aspects of the RPC interface.""" @@ -8,16 +8,29 @@ import os from test_framework.authproxy import JSONRPCException from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_greater_than_or_equal +from threading import Thread +import subprocess + def expect_http_status(expected_http_status, expected_rpc_code, fcn, *args): try: fcn(*args) - raise AssertionError("Expected RPC error %d, got none" % expected_rpc_code) + raise AssertionError(f"Expected RPC error {expected_rpc_code}, got none") except JSONRPCException as exc: assert_equal(exc.error["code"], expected_rpc_code) assert_equal(exc.http_status, expected_http_status) + +def test_work_queue_getblock(node, got_exceeded_error): + while not got_exceeded_error: + try: + node.cli('getrpcinfo').send_cli() + except subprocess.CalledProcessError as e: + assert_equal(e.output, 'error: Server response: Work queue depth exceeded\n') + got_exceeded_error.append(True) + + class RPCInterfaceTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -67,10 +80,23 @@ class RPCInterfaceTest(BitcoinTestFramework): expect_http_status(404, -32601, self.nodes[0].invalidmethod) expect_http_status(500, -8, self.nodes[0].getblockhash, 42) + def test_work_queue_exceeded(self): + self.log.info("Testing work queue exceeded...") + self.restart_node(0, ['-rpcworkqueue=1', '-rpcthreads=1']) + got_exceeded_error = [] + threads = [] + for _ in range(3): + t = Thread(target=test_work_queue_getblock, args=(self.nodes[0], got_exceeded_error)) + t.start() + threads.append(t) + for t in threads: + t.join() + def run_test(self): self.test_getrpcinfo() self.test_batch_request() self.test_http_status_codes() + self.test_work_queue_exceeded() if __name__ == '__main__': diff --git a/test/functional/interface_zmq.py b/test/functional/interface_zmq.py index 94e162b748..1ee12c0040 100755 --- a/test/functional/interface_zmq.py +++ b/test/functional/interface_zmq.py @@ -1,18 +1,29 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the ZMQ notification interface.""" import struct -from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE, ADDRESS_BCRT1_P2WSH_OP_TRUE -from test_framework.blocktools import create_block, create_coinbase, add_witness_commitment +from test_framework.address import ( + ADDRESS_BCRT1_P2WSH_OP_TRUE, + ADDRESS_BCRT1_UNSPENDABLE, +) +from test_framework.blocktools import ( + add_witness_commitment, + create_block, + create_coinbase, +) from test_framework.test_framework import BitcoinTestFramework -from test_framework.messages import CTransaction, hash256, FromHex +from test_framework.messages import ( + CTransaction, + hash256, +) from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) +from test_framework.netutil import test_ipv6_local from io import BytesIO from time import sleep @@ -70,9 +81,8 @@ class ZMQTestSetupBlock: the generated block's hash, it's (coinbase) transaction id, the raw block or raw transaction data. """ - - def __init__(self, node): - self.block_hash = node.generate(1)[0] + def __init__(self, test_framework, node): + self.block_hash = test_framework.generate(node, 1, sync_fun=test_framework.no_op)[0] coinbase = node.getblock(self.block_hash, 2)['tx'][0] self.tx_hash = coinbase['txid'] self.raw_tx = coinbase['hex'] @@ -108,6 +118,7 @@ class ZMQTest (BitcoinTestFramework): self.test_mempool_sync() self.test_reorg() self.test_multiple_interfaces() + self.test_ipv6() finally: # Destroy the ZMQ context. self.log.debug("Destroying ZMQ context") @@ -115,13 +126,15 @@ class ZMQTest (BitcoinTestFramework): # Restart node with the specified zmq notifications enabled, subscribe to # all of them and return the corresponding ZMQSubscriber objects. - def setup_zmq_test(self, services, *, recv_timeout=60, sync_blocks=True): + def setup_zmq_test(self, services, *, recv_timeout=60, sync_blocks=True, ipv6=False): subscribers = [] for topic, address in services: socket = self.ctx.socket(zmq.SUB) + if ipv6: + socket.setsockopt(zmq.IPV6, 1) subscribers.append(ZMQSubscriber(socket, topic.encode())) - self.restart_node(0, ["-zmqpub%s=%s" % (topic, address) for topic, address in services] + + self.restart_node(0, [f"-zmqpub{topic}={address}" for topic, address in services] + self.extra_args[0]) for i, sub in enumerate(subscribers): @@ -136,7 +149,7 @@ class ZMQTest (BitcoinTestFramework): for sub in subscribers: sub.socket.set(zmq.RCVTIMEO, 1000) while True: - test_block = ZMQTestSetupBlock(self.nodes[0]) + test_block = ZMQTestSetupBlock(self, self.nodes[0]) recv_failed = False for sub in subscribers: try: @@ -173,10 +186,8 @@ class ZMQTest (BitcoinTestFramework): rawtx = subs[3] num_blocks = 5 - self.log.info("Generate %(n)d blocks (and %(n)d coinbase txes)" % {"n": num_blocks}) - genhashes = self.nodes[0].generatetoaddress(num_blocks, ADDRESS_BCRT1_UNSPENDABLE) - - self.sync_all() + self.log.info(f"Generate {num_blocks} blocks (and {num_blocks} coinbase txes)") + genhashes = self.generatetoaddress(self.nodes[0], num_blocks, ADDRESS_BCRT1_UNSPENDABLE) for x in range(num_blocks): # Should receive the coinbase txid. @@ -215,7 +226,7 @@ class ZMQTest (BitcoinTestFramework): # Mining the block with this tx should result in second notification # after coinbase tx notification - self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) + self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) hashtx.receive() txid = hashtx.receive() assert_equal(payment_txid, txid.hex()) @@ -246,14 +257,14 @@ class ZMQTest (BitcoinTestFramework): # Generate 1 block in nodes[0] with 1 mempool tx and receive all notifications payment_txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1.0) - disconnect_block = self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE)[0] + disconnect_block = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE, sync_fun=self.no_op)[0] disconnect_cb = self.nodes[0].getblock(disconnect_block)["tx"][0] assert_equal(self.nodes[0].getbestblockhash(), hashblock.receive().hex()) assert_equal(hashtx.receive().hex(), payment_txid) assert_equal(hashtx.receive().hex(), disconnect_cb) # Generate 2 blocks in nodes[1] to a different address to ensure split - connect_blocks = self.nodes[1].generatetoaddress(2, ADDRESS_BCRT1_P2WSH_OP_TRUE) + connect_blocks = self.generatetoaddress(self.nodes[1], 2, ADDRESS_BCRT1_P2WSH_OP_TRUE, sync_fun=self.no_op) # nodes[0] will reorg chain after connecting back nodes[1] self.connect_nodes(0, 1) @@ -297,13 +308,13 @@ class ZMQTest (BitcoinTestFramework): seq_num = 1 # Generate 1 block in nodes[0] and receive all notifications - dc_block = self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE)[0] + dc_block = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE, sync_fun=self.no_op)[0] # Note: We are not notified of any block transactions, coinbase or mined assert_equal((self.nodes[0].getbestblockhash(), "C", None), seq.receive_sequence()) # Generate 2 blocks in nodes[1] to a different address to ensure a chain split - self.nodes[1].generatetoaddress(2, ADDRESS_BCRT1_P2WSH_OP_TRUE) + self.generatetoaddress(self.nodes[1], 2, ADDRESS_BCRT1_P2WSH_OP_TRUE, sync_fun=self.no_op) # nodes[0] will reorg chain after connecting back nodes[1] self.connect_nodes(0, 1) @@ -338,8 +349,7 @@ class ZMQTest (BitcoinTestFramework): # though the mempool sequence number does go up by the number of transactions # removed from the mempool by the block mining it. mempool_size = len(self.nodes[0].getrawmempool()) - c_block = self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE)[0] - self.sync_all() + c_block = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE)[0] # Make sure the number of mined transactions matches the number of txs out of mempool mempool_size_delta = mempool_size - len(self.nodes[0].getrawmempool()) assert_equal(len(self.nodes[0].getblock(c_block)["tx"])-1, mempool_size_delta) @@ -378,8 +388,7 @@ class ZMQTest (BitcoinTestFramework): # Other things may happen but aren't wallet-deterministic so we don't test for them currently self.nodes[0].reconsiderblock(best_hash) - self.nodes[1].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) - self.sync_all() + self.generatetoaddress(self.nodes[1], 1, ADDRESS_BCRT1_UNSPENDABLE) self.log.info("Evict mempool transaction by block conflict") orig_txid = self.nodes[0].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=1.0, replaceable=True) @@ -392,12 +401,8 @@ class ZMQTest (BitcoinTestFramework): raw_tx = self.nodes[0].getrawtransaction(orig_txid) bump_info = self.nodes[0].bumpfee(orig_txid) # Mine the pre-bump tx - block = create_block(int(self.nodes[0].getbestblockhash(), 16), create_coinbase(self.nodes[0].getblockcount()+1)) - tx = FromHex(CTransaction(), raw_tx) - block.vtx.append(tx) - for txid in more_tx: - tx = FromHex(CTransaction(), self.nodes[0].getrawtransaction(txid)) - block.vtx.append(tx) + txs_to_add = [raw_tx] + [self.nodes[0].getrawtransaction(txid) for txid in more_tx] + block = create_block(int(self.nodes[0].getbestblockhash(), 16), create_coinbase(self.nodes[0].getblockcount()+1), txlist=txs_to_add) add_witness_commitment(block) block.solve() assert_equal(self.nodes[0].submitblock(block.serialize().hex()), None) @@ -430,7 +435,7 @@ class ZMQTest (BitcoinTestFramework): # Last tx assert_equal((orig_txid_2, "A", mempool_seq), seq.receive_sequence()) mempool_seq += 1 - self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) + self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) self.sync_all() # want to make sure we didn't break "consensus" for other tests def test_mempool_sync(self): @@ -459,7 +464,7 @@ class ZMQTest (BitcoinTestFramework): # 1) Consume backlog until we get a mempool sequence number (hash_str, label, zmq_mem_seq) = seq.receive_sequence() while zmq_mem_seq is None: - (hash_str, label, zmq_mem_seq) = seq.receive_sequence() + (hash_str, label, zmq_mem_seq) = seq.receive_sequence() assert label == "A" or label == "R" assert hash_str is not None @@ -482,7 +487,7 @@ class ZMQTest (BitcoinTestFramework): txids.append(self.nodes[0].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=0.1, replaceable=True)) self.nodes[0].bumpfee(txids[-1]) self.sync_all() - self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) + self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) final_txid = self.nodes[0].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=0.1, replaceable=True) # 3) Consume ZMQ backlog until we get to "now" for the mempool snapshot @@ -493,7 +498,7 @@ class ZMQTest (BitcoinTestFramework): if mempool_sequence is not None: zmq_mem_seq = mempool_sequence if zmq_mem_seq > get_raw_seq: - raise Exception("We somehow jumped mempool sequence numbers! zmq_mem_seq: {} > get_raw_seq: {}".format(zmq_mem_seq, get_raw_seq)) + raise Exception(f"We somehow jumped mempool sequence numbers! zmq_mem_seq: {zmq_mem_seq} > get_raw_seq: {get_raw_seq}") # 4) Moving forward, we apply the delta to our local view # remaining txs(5) + 1 rbf(A+R) + 1 block connect + 1 final tx @@ -509,7 +514,7 @@ class ZMQTest (BitcoinTestFramework): assert mempool_sequence > expected_sequence r_gap += mempool_sequence - expected_sequence else: - raise Exception("WARNING: txhash has unexpected mempool sequence value: {} vs expected {}".format(mempool_sequence, expected_sequence)) + raise Exception(f"WARNING: txhash has unexpected mempool sequence value: {mempool_sequence} vs expected {expected_sequence}") if label == "A": assert hash_str not in mempool_view mempool_view.add(hash_str) @@ -538,7 +543,7 @@ class ZMQTest (BitcoinTestFramework): # 5) If you miss a zmq/mempool sequence number, go back to step (2) - self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) + self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) def test_multiple_interfaces(self): # Set up two subscribers with different addresses @@ -551,11 +556,28 @@ class ZMQTest (BitcoinTestFramework): ], sync_blocks=False) # Generate 1 block in nodes[0] and receive all notifications - self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) + self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE, sync_fun=self.no_op) # Should receive the same block hash on both subscribers assert_equal(self.nodes[0].getbestblockhash(), subscribers[0].receive().hex()) assert_equal(self.nodes[0].getbestblockhash(), subscribers[1].receive().hex()) + def test_ipv6(self): + if not test_ipv6_local(): + self.log.info("Skipping IPv6 test, because IPv6 is not supported.") + return + self.log.info("Testing IPv6") + # Set up subscriber using IPv6 loopback address + subscribers = self.setup_zmq_test([ + ("hashblock", "tcp://[::1]:28332") + ], ipv6=True) + + # Generate 1 block in nodes[0] + self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) + + # Should receive the same block hash + assert_equal(self.nodes[0].getbestblockhash(), subscribers[0].receive().hex()) + + if __name__ == '__main__': ZMQTest().main() diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py index c4002f524a..d3961e753d 100755 --- a/test/functional/mempool_accept.py +++ b/test/functional/mempool_accept.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test mempool acceptance of raw transactions.""" +from copy import deepcopy from decimal import Decimal -from io import BytesIO import math from test_framework.test_framework import BitcoinTestFramework @@ -14,27 +14,28 @@ from test_framework.messages import ( BIP125_SEQUENCE_NUMBER, COIN, COutPoint, - CTransaction, + CTxIn, CTxOut, - MAX_BLOCK_BASE_SIZE, + MAX_BLOCK_WEIGHT, MAX_MONEY, + SEQUENCE_FINAL, + tx_from_hex, ) from test_framework.script import ( - hash160, CScript, OP_0, - OP_2, - OP_3, - OP_CHECKMULTISIG, - OP_EQUAL, OP_HASH160, OP_RETURN, ) +from test_framework.script_util import ( + keys_to_multisig_script, + script_to_p2sh_script, +) from test_framework.util import ( assert_equal, assert_raises_rpc_error, - hex_str_to_bytes, ) +from test_framework.wallet import MiniWallet class MempoolAcceptanceTest(BitcoinTestFramework): @@ -45,9 +46,6 @@ class MempoolAcceptanceTest(BitcoinTestFramework): ]] * self.num_nodes self.supports_cli = False - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def check_mempool_result(self, result_expected, *args, **kwargs): """Wrapper to check result of testmempoolaccept on node_0's mempool""" result_test = self.nodes[0].testmempoolaccept(*args, **kwargs) @@ -58,26 +56,28 @@ class MempoolAcceptanceTest(BitcoinTestFramework): def run_test(self): node = self.nodes[0] + self.wallet = MiniWallet(node) + self.wallet.rescan_utxos() self.log.info('Start with empty mempool, and 200 blocks') self.mempool_size = 0 assert_equal(node.getblockcount(), 200) assert_equal(node.getmempoolinfo()['size'], self.mempool_size) - coins = node.listunspent() self.log.info('Should not accept garbage to testmempoolaccept') assert_raises_rpc_error(-3, 'Expected type array, got string', lambda: node.testmempoolaccept(rawtxs='ff00baar')) - assert_raises_rpc_error(-8, 'Array must contain exactly one raw transaction for now', lambda: node.testmempoolaccept(rawtxs=['ff00baar', 'ff22'])) + assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=['ff22']*26)) + assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=[])) assert_raises_rpc_error(-22, 'TX decode failed', lambda: node.testmempoolaccept(rawtxs=['ff00baar'])) self.log.info('A transaction already in the blockchain') - coin = coins.pop() # Pick a random coin(base) to spend - raw_tx_in_block = node.signrawtransactionwithwallet(node.createrawtransaction( - inputs=[{'txid': coin['txid'], 'vout': coin['vout']}], - outputs=[{node.getnewaddress(): 0.3}, {node.getnewaddress(): 49}], - ))['hex'] - txid_in_block = node.sendrawtransaction(hexstring=raw_tx_in_block, maxfeerate=0) - node.generate(1) + tx = self.wallet.create_self_transfer()['tx'] # Pick a random coin(base) to spend + tx.vout.append(deepcopy(tx.vout[0])) + tx.vout[0].nValue = int(0.3 * COIN) + tx.vout[1].nValue = int(49 * COIN) + raw_tx_in_block = tx.serialize().hex() + txid_in_block = self.wallet.sendrawtransaction(from_node=node, tx_hex=raw_tx_in_block, maxfeerate=0) + self.generate(node, 1) self.mempool_size = 0 self.check_mempool_result( result_expected=[{'txid': txid_in_block, 'allowed': False, 'reject-reason': 'txn-already-known'}], @@ -86,12 +86,10 @@ class MempoolAcceptanceTest(BitcoinTestFramework): self.log.info('A transaction not in the mempool') fee = Decimal('0.000007') - raw_tx_0 = node.signrawtransactionwithwallet(node.createrawtransaction( - inputs=[{"txid": txid_in_block, "vout": 0, "sequence": BIP125_SEQUENCE_NUMBER}], # RBF is used later - outputs=[{node.getnewaddress(): Decimal('0.3') - fee}], - ))['hex'] - tx = CTransaction() - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + utxo_to_spend = self.wallet.get_utxo(txid=txid_in_block) # use 0.3 BTC UTXO + tx = self.wallet.create_self_transfer(utxo_to_spend=utxo_to_spend, sequence=BIP125_SEQUENCE_NUMBER)['tx'] + tx.vout[0].nValue = int((Decimal('0.3') - fee) * COIN) + raw_tx_0 = tx.serialize().hex() txid_0 = tx.rehash() self.check_mempool_result( result_expected=[{'txid': txid_0, 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': fee}}], @@ -99,15 +97,15 @@ class MempoolAcceptanceTest(BitcoinTestFramework): ) self.log.info('A final transaction not in the mempool') - coin = coins.pop() # Pick a random coin(base) to spend output_amount = Decimal('0.025') - raw_tx_final = node.signrawtransactionwithwallet(node.createrawtransaction( - inputs=[{'txid': coin['txid'], 'vout': coin['vout'], "sequence": 0xffffffff}], # SEQUENCE_FINAL - outputs=[{node.getnewaddress(): output_amount}], + tx = self.wallet.create_self_transfer( + sequence=SEQUENCE_FINAL, locktime=node.getblockcount() + 2000, # Can be anything - ))['hex'] - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_final))) - fee_expected = coin['amount'] - output_amount + )['tx'] + tx.vout[0].nValue = int(output_amount * COIN) + raw_tx_final = tx.serialize().hex() + tx = tx_from_hex(raw_tx_final) + fee_expected = Decimal('50.0') - output_amount self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': fee_expected}}], rawtxs=[tx.serialize().hex()], @@ -125,11 +123,10 @@ class MempoolAcceptanceTest(BitcoinTestFramework): ) self.log.info('A transaction that replaces a mempool transaction') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + tx = tx_from_hex(raw_tx_0) tx.vout[0].nValue -= int(fee * COIN) # Double the fee tx.vin[0].nSequence = BIP125_SEQUENCE_NUMBER + 1 # Now, opt out of RBF - raw_tx_0 = node.signrawtransactionwithwallet(tx.serialize().hex())['hex'] - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + raw_tx_0 = tx.serialize().hex() txid_0 = tx.rehash() self.check_mempool_result( result_expected=[{'txid': txid_0, 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': (2 * fee)}}], @@ -140,9 +137,8 @@ class MempoolAcceptanceTest(BitcoinTestFramework): # Send the transaction that replaces the mempool transaction and opts out of replaceability node.sendrawtransaction(hexstring=tx.serialize().hex(), maxfeerate=0) # take original raw_tx_0 - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + tx = tx_from_hex(raw_tx_0) tx.vout[0].nValue -= int(4 * fee * COIN) # Set more fee - # skip re-signing the tx self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'txn-mempool-conflict'}], rawtxs=[tx.serialize().hex()], @@ -150,29 +146,28 @@ class MempoolAcceptanceTest(BitcoinTestFramework): ) self.log.info('A transaction with missing inputs, that never existed') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + tx = tx_from_hex(raw_tx_0) tx.vin[0].prevout = COutPoint(hash=int('ff' * 32, 16), n=14) - # skip re-signing the tx self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'missing-inputs'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A transaction with missing inputs, that existed once in the past') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + tx = tx_from_hex(raw_tx_0) tx.vin[0].prevout.n = 1 # Set vout to 1, to spend the other outpoint (49 coins) of the in-chain-tx we want to double spend - raw_tx_1 = node.signrawtransactionwithwallet(tx.serialize().hex())['hex'] + raw_tx_1 = tx.serialize().hex() txid_1 = node.sendrawtransaction(hexstring=raw_tx_1, maxfeerate=0) # Now spend both to "clearly hide" the outputs, ie. remove the coins from the utxo set by spending them - raw_tx_spend_both = node.signrawtransactionwithwallet(node.createrawtransaction( - inputs=[ - {'txid': txid_0, 'vout': 0}, - {'txid': txid_1, 'vout': 0}, - ], - outputs=[{node.getnewaddress(): 0.1}] - ))['hex'] - txid_spend_both = node.sendrawtransaction(hexstring=raw_tx_spend_both, maxfeerate=0) - node.generate(1) + tx = self.wallet.create_self_transfer()['tx'] + tx.vin.append(deepcopy(tx.vin[0])) + tx.wit.vtxinwit.append(deepcopy(tx.wit.vtxinwit[0])) + tx.vin[0].prevout = COutPoint(hash=int(txid_0, 16), n=0) + tx.vin[1].prevout = COutPoint(hash=int(txid_1, 16), n=0) + tx.vout[0].nValue = int(0.1 * COIN) + raw_tx_spend_both = tx.serialize().hex() + txid_spend_both = self.wallet.sendrawtransaction(from_node=node, tx_hex=raw_tx_spend_both, maxfeerate=0) + self.generate(node, 1) self.mempool_size = 0 # Now see if we can add the coins back to the utxo set by sending the exact txs again self.check_mempool_result( @@ -184,12 +179,11 @@ class MempoolAcceptanceTest(BitcoinTestFramework): rawtxs=[raw_tx_1], ) - self.log.info('Create a signed "reference" tx for later use') - raw_tx_reference = node.signrawtransactionwithwallet(node.createrawtransaction( - inputs=[{'txid': txid_spend_both, 'vout': 0}], - outputs=[{node.getnewaddress(): 0.05}], - ))['hex'] - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + self.log.info('Create a "reference" tx for later use') + utxo_to_spend = self.wallet.get_utxo(txid=txid_spend_both) + tx = self.wallet.create_self_transfer(utxo_to_spend=utxo_to_spend, sequence=SEQUENCE_FINAL)['tx'] + tx.vout[0].nValue = int(0.05 * COIN) + raw_tx_reference = tx.serialize().hex() # Reference tx should be valid on itself self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': { 'base': Decimal('0.1') - Decimal('0.05')}}], @@ -198,25 +192,23 @@ class MempoolAcceptanceTest(BitcoinTestFramework): ) self.log.info('A transaction with no outputs') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vout = [] - # Skip re-signing the transaction for context independent checks from now on - # tx.deserialize(BytesIO(hex_str_to_bytes(node.signrawtransactionwithwallet(tx.serialize().hex())['hex']))) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-vout-empty'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A really large transaction') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) - tx.vin = [tx.vin[0]] * math.ceil(MAX_BLOCK_BASE_SIZE / len(tx.vin[0].serialize())) + tx = tx_from_hex(raw_tx_reference) + tx.vin = [tx.vin[0]] * math.ceil(MAX_BLOCK_WEIGHT // 4 / len(tx.vin[0].serialize())) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-oversize'}], rawtxs=[tx.serialize().hex()], ) self.log.info('A transaction with negative output value') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vout[0].nValue *= -1 self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-vout-negative'}], @@ -225,7 +217,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework): # The following two validations prevent overflow of the output amounts (see CVE-2010-5139). self.log.info('A transaction with too large output value') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vout[0].nValue = MAX_MONEY + 1 self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-vout-toolarge'}], @@ -233,7 +225,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework): ) self.log.info('A transaction with too large sum of output values') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vout = [tx.vout[0]] * 2 tx.vout[0].nValue = MAX_MONEY self.check_mempool_result( @@ -242,72 +234,80 @@ class MempoolAcceptanceTest(BitcoinTestFramework): ) self.log.info('A transaction with duplicate inputs') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vin = [tx.vin[0]] * 2 self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-inputs-duplicate'}], rawtxs=[tx.serialize().hex()], ) + self.log.info('A non-coinbase transaction with coinbase-like outpoint') + tx = tx_from_hex(raw_tx_reference) + tx.vin.append(CTxIn(COutPoint(hash=0, n=0xffffffff))) + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-prevout-null'}], + rawtxs=[tx.serialize().hex()], + ) + self.log.info('A coinbase transaction') - # Pick the input of the first tx we signed, so it has to be a coinbase tx + # Pick the input of the first tx we created, so it has to be a coinbase tx raw_tx_coinbase_spent = node.getrawtransaction(txid=node.decoderawtransaction(hexstring=raw_tx_in_block)['vin'][0]['txid']) - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_coinbase_spent))) + tx = tx_from_hex(raw_tx_coinbase_spent) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'coinbase'}], rawtxs=[tx.serialize().hex()], ) self.log.info('Some nonstandard transactions') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.nVersion = 3 # A version currently non-standard self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'version'}], rawtxs=[tx.serialize().hex()], ) - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vout[0].scriptPubKey = CScript([OP_0]) # Some non-standard script self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'scriptpubkey'}], rawtxs=[tx.serialize().hex()], ) - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) key = ECKey() key.generate() pubkey = key.get_pubkey().get_bytes() - tx.vout[0].scriptPubKey = CScript([OP_2, pubkey, pubkey, pubkey, OP_3, OP_CHECKMULTISIG]) # Some bare multisig script (2-of-3) + tx.vout[0].scriptPubKey = keys_to_multisig_script([pubkey] * 3, k=2) # Some bare multisig script (2-of-3) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bare-multisig'}], rawtxs=[tx.serialize().hex()], ) - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vin[0].scriptSig = CScript([OP_HASH160]) # Some not-pushonly scriptSig self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'scriptsig-not-pushonly'}], rawtxs=[tx.serialize().hex()], ) - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vin[0].scriptSig = CScript([b'a' * 1648]) # Some too large scriptSig (>1650 bytes) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'scriptsig-size'}], rawtxs=[tx.serialize().hex()], ) - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) - output_p2sh_burn = CTxOut(nValue=540, scriptPubKey=CScript([OP_HASH160, hash160(b'burn'), OP_EQUAL])) + tx = tx_from_hex(raw_tx_reference) + output_p2sh_burn = CTxOut(nValue=540, scriptPubKey=script_to_p2sh_script(b'burn')) num_scripts = 100000 // len(output_p2sh_burn.serialize()) # Use enough outputs to make the tx too large for our policy tx.vout = [output_p2sh_burn] * num_scripts self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'tx-size'}], rawtxs=[tx.serialize().hex()], ) - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vout[0] = output_p2sh_burn tx.vout[0].nValue -= 1 # Make output smaller, such that it is dust for our policy self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'dust'}], rawtxs=[tx.serialize().hex()], ) - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'\xff']) tx.vout = [tx.vout[0]] * 2 self.check_mempool_result( @@ -316,7 +316,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework): ) self.log.info('A timelocked transaction') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vin[0].nSequence -= 1 # Should be non-max, so locktime is not ignored tx.nLockTime = node.getblockcount() + 1 self.check_mempool_result( @@ -325,9 +325,8 @@ class MempoolAcceptanceTest(BitcoinTestFramework): ) self.log.info('A transaction that is locked by BIP68 sequence logic') - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx = tx_from_hex(raw_tx_reference) tx.vin[0].nSequence = 2 # We could include it in the second block mined from now, but not the very next one - # Can skip re-signing the tx because of early rejection self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'non-BIP68-final'}], rawtxs=[tx.serialize().hex()], diff --git a/test/functional/mempool_accept_wtxid.py b/test/functional/mempool_accept_wtxid.py new file mode 100755 index 0000000000..4767d6db22 --- /dev/null +++ b/test/functional/mempool_accept_wtxid.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Test mempool acceptance in case of an already known transaction +with identical non-witness data but different witness. +""" + +from copy import deepcopy +from test_framework.messages import ( + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + sha256, +) +from test_framework.p2p import P2PTxInvStore +from test_framework.script import ( + CScript, + OP_0, + OP_ELSE, + OP_ENDIF, + OP_EQUAL, + OP_HASH160, + OP_IF, + OP_TRUE, + hash160, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, +) + +class MempoolWtxidTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def run_test(self): + node = self.nodes[0] + + self.log.info('Start with empty mempool and 101 blocks') + # The last 100 coinbase transactions are premature + blockhash = self.generate(node, 101)[0] + txid = node.getblock(blockhash=blockhash, verbosity=2)["tx"][0]["txid"] + assert_equal(node.getmempoolinfo()['size'], 0) + + self.log.info("Submit parent with multiple script branches to mempool") + hashlock = hash160(b'Preimage') + witness_script = CScript([OP_IF, OP_HASH160, hashlock, OP_EQUAL, OP_ELSE, OP_TRUE, OP_ENDIF]) + witness_program = sha256(witness_script) + script_pubkey = CScript([OP_0, witness_program]) + + parent = CTransaction() + parent.vin.append(CTxIn(COutPoint(int(txid, 16), 0), b"")) + parent.vout.append(CTxOut(int(9.99998 * COIN), script_pubkey)) + parent.rehash() + + privkeys = [node.get_deterministic_priv_key().key] + raw_parent = node.signrawtransactionwithkey(hexstring=parent.serialize().hex(), privkeys=privkeys)['hex'] + parent_txid = node.sendrawtransaction(hexstring=raw_parent, maxfeerate=0) + self.generate(node, 1) + + peer_wtxid_relay = node.add_p2p_connection(P2PTxInvStore()) + + # Create a new transaction with witness solving first branch + child_witness_script = CScript([OP_TRUE]) + child_witness_program = sha256(child_witness_script) + child_script_pubkey = CScript([OP_0, child_witness_program]) + + child_one = CTransaction() + child_one.vin.append(CTxIn(COutPoint(int(parent_txid, 16), 0), b"")) + child_one.vout.append(CTxOut(int(9.99996 * COIN), child_script_pubkey)) + child_one.wit.vtxinwit.append(CTxInWitness()) + child_one.wit.vtxinwit[0].scriptWitness.stack = [b'Preimage', b'\x01', witness_script] + child_one_wtxid = child_one.getwtxid() + child_one_txid = child_one.rehash() + + # Create another identical transaction with witness solving second branch + child_two = deepcopy(child_one) + child_two.wit.vtxinwit[0].scriptWitness.stack = [b'', witness_script] + child_two_wtxid = child_two.getwtxid() + child_two_txid = child_two.rehash() + + assert_equal(child_one_txid, child_two_txid) + assert child_one_wtxid != child_two_wtxid + + self.log.info("Submit child_one to the mempool") + txid_submitted = node.sendrawtransaction(child_one.serialize().hex()) + assert_equal(node.getmempoolentry(txid_submitted)['wtxid'], child_one_wtxid) + + peer_wtxid_relay.wait_for_broadcast([child_one_wtxid]) + assert_equal(node.getmempoolinfo()["unbroadcastcount"], 0) + + # testmempoolaccept reports the "already in mempool" error + assert_equal(node.testmempoolaccept([child_one.serialize().hex()]), [{ + "txid": child_one_txid, + "wtxid": child_one_wtxid, + "allowed": False, + "reject-reason": "txn-already-in-mempool" + }]) + assert_equal(node.testmempoolaccept([child_two.serialize().hex()])[0], { + "txid": child_two_txid, + "wtxid": child_two_wtxid, + "allowed": False, + "reject-reason": "txn-same-nonwitness-data-in-mempool" + }) + + # sendrawtransaction will not throw but quits early when the exact same transaction is already in mempool + node.sendrawtransaction(child_one.serialize().hex()) + + self.log.info("Connect another peer that hasn't seen child_one before") + peer_wtxid_relay_2 = node.add_p2p_connection(P2PTxInvStore()) + + self.log.info("Submit child_two to the mempool") + # sendrawtransaction will not throw but quits early when a transaction with the same non-witness data is already in mempool + node.sendrawtransaction(child_two.serialize().hex()) + + # The node should rebroadcast the transaction using the wtxid of the correct transaction + # (child_one, which is in its mempool). + peer_wtxid_relay_2.wait_for_broadcast([child_one_wtxid]) + assert_equal(node.getmempoolinfo()["unbroadcastcount"], 0) + +if __name__ == '__main__': + MempoolWtxidTest().main() diff --git a/test/functional/mempool_compatibility.py b/test/functional/mempool_compatibility.py index eb08765ebf..c545a7f68d 100755 --- a/test/functional/mempool_compatibility.py +++ b/test/functional/mempool_compatibility.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test that mempool.dat is both backward and forward compatible between versions @@ -7,16 +7,17 @@ NOTE: The test is designed to prevent cases when compatibility is broken accidentally. In case we need to break mempool compatibility we can continue to use the test by just bumping the version number. -Download node binaries: -test/get_previous_releases.py -b v0.19.1 v0.18.1 v0.17.2 v0.16.3 v0.15.2 - -Only v0.15.2 is required by this test. The rest is used in other backwards compatibility tests. +The previous release v0.19.1 is required by this test, see test/README.md. """ import os +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework -from test_framework.wallet import MiniWallet +from test_framework.wallet import ( + MiniWallet, + MiniWalletMode, +) class MempoolCompatibilityTest(BitcoinTestFramework): @@ -39,9 +40,9 @@ class MempoolCompatibilityTest(BitcoinTestFramework): self.log.info("Test that mempool.dat is compatible between versions") old_node, new_node = self.nodes - new_wallet = MiniWallet(new_node) - new_wallet.generate(1) - new_node.generate(100) + new_wallet = MiniWallet(new_node, mode=MiniWalletMode.RAW_P2PK) + self.generate(new_wallet, 1, sync_fun=self.no_op) + self.generate(new_node, COINBASE_MATURITY, sync_fun=self.no_op) # Sync the nodes to ensure old_node has the block that contains the coinbase that new_wallet will spend. # Otherwise, because coinbases are only valid in a block and not as loose txns, if the nodes aren't synced # unbroadcasted_tx won't pass old_node's `MemPoolAccept::PreChecks`. @@ -67,8 +68,7 @@ class MempoolCompatibilityTest(BitcoinTestFramework): self.log.info("Add unbroadcasted tx to mempool on new node and shutdown") unbroadcasted_tx_hash = new_wallet.send_self_transfer(from_node=new_node)['txid'] assert unbroadcasted_tx_hash in new_node.getrawmempool() - mempool = new_node.getrawmempool(True) - assert mempool[unbroadcasted_tx_hash]['unbroadcast'] + assert new_node.getmempoolentry(unbroadcasted_tx_hash)['unbroadcast'] self.stop_node(1) self.log.info("Move mempool.dat from new to old node") diff --git a/test/functional/mempool_expiry.py b/test/functional/mempool_expiry.py index 4c46075ae9..f301b29c25 100755 --- a/test/functional/mempool_expiry.py +++ b/test/functional/mempool_expiry.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Tests that a mempool transaction expires after a given timeout and that its @@ -12,6 +12,7 @@ definable expiry timeout via the '-mempoolexpiry=<n>' command line argument from datetime import timedelta +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -35,8 +36,8 @@ class MempoolExpiryTest(BitcoinTestFramework): self.wallet = MiniWallet(node) # Add enough mature utxos to the wallet so that all txs spend confirmed coins. - self.wallet.generate(4) - node.generate(100) + self.generate(self.wallet, 4) + self.generate(node, COINBASE_MATURITY) # Send a parent transaction that will expire. parent_txid = self.wallet.send_self_transfer(from_node=node)['txid'] diff --git a/test/functional/mempool_limit.py b/test/functional/mempool_limit.py index 39035f7cb1..3619e05761 100755 --- a/test/functional/mempool_limit.py +++ b/test/functional/mempool_limit.py @@ -1,13 +1,22 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test mempool limiting together/eviction with the wallet.""" from decimal import Decimal +from test_framework.blocktools import COINBASE_MATURITY +from test_framework.messages import COIN from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_greater_than, assert_raises_rpc_error, create_confirmed_utxos, create_lots_of_big_transactions, gen_return_txouts +from test_framework.util import ( + assert_equal, + assert_greater_than, + assert_raises_rpc_error, + gen_return_txouts, +) +from test_framework.wallet import MiniWallet + class MempoolLimitTest(BitcoinTestFramework): def set_test_params(self): @@ -20,55 +29,64 @@ class MempoolLimitTest(BitcoinTestFramework): ]] self.supports_cli = False - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() + def send_large_txs(self, node, miniwallet, txouts, fee, tx_batch_size): + for _ in range(tx_batch_size): + tx = miniwallet.create_self_transfer(from_node=node, fee_rate=0, mempool_valid=False)['tx'] + for txout in txouts: + tx.vout.append(txout) + tx.vout[0].nValue -= int(fee * COIN) + res = node.testmempoolaccept([tx.serialize().hex()])[0] + assert_equal(res['fees']['base'], fee) + miniwallet.sendrawtransaction(from_node=node, tx_hex=tx.serialize().hex()) def run_test(self): txouts = gen_return_txouts() - relayfee = self.nodes[0].getnetworkinfo()['relayfee'] + node = self.nodes[0] + miniwallet = MiniWallet(node) + relayfee = node.getnetworkinfo()['relayfee'] + + self.log.info('Check that mempoolminfee is minrelaytxfee') + assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) + assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) - self.log.info('Check that mempoolminfee is minrelytxfee') - assert_equal(self.nodes[0].getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) - assert_equal(self.nodes[0].getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) + tx_batch_size = 25 + num_of_batches = 3 + # Generate UTXOs to flood the mempool + # 1 to create a tx initially that will be evicted from the mempool later + # 3 batches of multiple transactions with a fee rate much higher than the previous UTXO + # And 1 more to verify that this tx does not get added to the mempool with a fee rate less than the mempoolminfee + self.generate(miniwallet, 1 + (num_of_batches * tx_batch_size) + 1) - txids = [] - utxos = create_confirmed_utxos(relayfee, self.nodes[0], 91) + # Mine 99 blocks so that the UTXOs are allowed to be spent + self.generate(node, COINBASE_MATURITY - 1) self.log.info('Create a mempool tx that will be evicted') - us0 = utxos.pop() - inputs = [{ "txid" : us0["txid"], "vout" : us0["vout"]}] - outputs = {self.nodes[0].getnewaddress() : 0.0001} - tx = self.nodes[0].createrawtransaction(inputs, outputs) - self.nodes[0].settxfee(relayfee) # specifically fund this tx with low fee - txF = self.nodes[0].fundrawtransaction(tx) - self.nodes[0].settxfee(0) # return to automatic fee selection - txFS = self.nodes[0].signrawtransactionwithwallet(txF['hex']) - txid = self.nodes[0].sendrawtransaction(txFS['hex']) - - relayfee = self.nodes[0].getnetworkinfo()['relayfee'] - base_fee = relayfee*100 - for i in range (3): - txids.append([]) - txids[i] = create_lots_of_big_transactions(self.nodes[0], txouts, utxos[30*i:30*i+30], 30, (i+1)*base_fee) + tx_to_be_evicted_id = miniwallet.send_self_transfer(from_node=node, fee_rate=relayfee)["txid"] + + # Increase the tx fee rate to give the subsequent transactions a higher priority in the mempool + # The tx has an approx. vsize of 65k, i.e. multiplying the previous fee rate (in sats/kvB) + # by 130 should result in a fee that corresponds to 2x of that fee rate + base_fee = relayfee * 130 + + self.log.info("Fill up the mempool with txs with higher fee rate") + for batch_of_txid in range(num_of_batches): + fee = (batch_of_txid + 1) * base_fee + self.send_large_txs(node, miniwallet, txouts, fee, tx_batch_size) self.log.info('The tx should be evicted by now') - assert txid not in self.nodes[0].getrawmempool() - txdata = self.nodes[0].gettransaction(txid) - assert txdata['confirmations'] == 0 #confirmation should still be 0 + # The number of transactions created should be greater than the ones present in the mempool + assert_greater_than(tx_batch_size * num_of_batches, len(node.getrawmempool())) + # Initial tx created should not be present in the mempool anymore as it had a lower fee rate + assert tx_to_be_evicted_id not in node.getrawmempool() - self.log.info('Check that mempoolminfee is larger than minrelytxfee') - assert_equal(self.nodes[0].getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) - assert_greater_than(self.nodes[0].getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) + self.log.info('Check that mempoolminfee is larger than minrelaytxfee') + assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) + assert_greater_than(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) + # Deliberately try to create a tx with a fee less than the minimum mempool fee to assert that it does not get added to the mempool self.log.info('Create a mempool tx that will not pass mempoolminfee') - us0 = utxos.pop() - inputs = [{ "txid" : us0["txid"], "vout" : us0["vout"]}] - outputs = {self.nodes[0].getnewaddress() : 0.0001} - tx = self.nodes[0].createrawtransaction(inputs, outputs) - # specifically fund this tx with a fee < mempoolminfee, >= than minrelaytxfee - txF = self.nodes[0].fundrawtransaction(tx, {'feeRate': relayfee}) - txFS = self.nodes[0].signrawtransactionwithwallet(txF['hex']) - assert_raises_rpc_error(-26, "mempool min fee not met", self.nodes[0].sendrawtransaction, txFS['hex']) + assert_raises_rpc_error(-26, "mempool min fee not met", miniwallet.send_self_transfer, from_node=node, fee_rate=relayfee, mempool_valid=False) + if __name__ == '__main__': MempoolLimitTest().main() diff --git a/test/functional/mempool_package_limits.py b/test/functional/mempool_package_limits.py new file mode 100755 index 0000000000..89a5c83826 --- /dev/null +++ b/test/functional/mempool_package_limits.py @@ -0,0 +1,545 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test logic for limiting mempool and package ancestors/descendants.""" + +from decimal import Decimal + +from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE +from test_framework.test_framework import BitcoinTestFramework +from test_framework.messages import ( + COIN, + CTransaction, + CTxInWitness, + tx_from_hex, + WITNESS_SCALE_FACTOR, +) +from test_framework.script import ( + CScript, + OP_TRUE, +) +from test_framework.util import ( + assert_equal, +) +from test_framework.wallet import ( + bulk_transaction, + create_child_with_parents, + make_chain, + DEFAULT_FEE, +) + +class MempoolPackageLimitsTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def run_test(self): + self.log.info("Generate blocks to create UTXOs") + node = self.nodes[0] + self.privkeys = [node.get_deterministic_priv_key().key] + self.address = node.get_deterministic_priv_key().address + self.coins = [] + # The last 100 coinbase transactions are premature + for b in self.generatetoaddress(node, 200, self.address)[:100]: + coinbase = node.getblock(blockhash=b, verbosity=2)["tx"][0] + self.coins.append({ + "txid": coinbase["txid"], + "amount": coinbase["vout"][0]["value"], + "scriptPubKey": coinbase["vout"][0]["scriptPubKey"], + }) + + self.test_chain_limits() + self.test_desc_count_limits() + self.test_desc_count_limits_2() + self.test_anc_count_limits() + self.test_anc_count_limits_2() + self.test_anc_count_limits_bushy() + + # The node will accept our (nonstandard) extra large OP_RETURN outputs + self.restart_node(0, extra_args=["-acceptnonstdtxn=1"]) + self.test_anc_size_limits() + self.test_desc_size_limits() + + def test_chain_limits_helper(self, mempool_count, package_count): + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + first_coin = self.coins.pop() + spk = None + txid = first_coin["txid"] + chain_hex = [] + chain_txns = [] + value = first_coin["amount"] + + for i in range(mempool_count + package_count): + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + if i < mempool_count: + node.sendrawtransaction(txhex) + assert_equal(node.getmempoolentry(txid)["ancestorcount"], i + 1) + else: + chain_hex.append(txhex) + chain_txns.append(tx) + testres_too_long = node.testmempoolaccept(rawtxs=chain_hex) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + self.generate(node, 1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=chain_hex)]) + + def test_chain_limits(self): + """Create chains from mempool and package transactions that are longer than 25, + but only if both in-mempool and in-package transactions are considered together. + This checks that both mempool and in-package transactions are taken into account when + calculating ancestors/descendant limits. + """ + self.log.info("Check that in-package ancestors count for mempool ancestor limits") + + # 24 transactions in the mempool and 2 in the package. The parent in the package has + # 24 in-mempool ancestors and 1 in-package descendant. The child has 0 direct parents + # in the mempool, but 25 in-mempool and in-package ancestors in total. + self.test_chain_limits_helper(24, 2) + # 2 transactions in the mempool and 24 in the package. + self.test_chain_limits_helper(2, 24) + # 13 transactions in the mempool and 13 in the package. + self.test_chain_limits_helper(13, 13) + + def test_desc_count_limits(self): + """Create an 'A' shaped package with 24 transactions in the mempool and 2 in the package: + M1 + ^ ^ + M2a M2b + . . + . . + . . + M12a ^ + ^ M13b + ^ ^ + Pa Pb + The top ancestor in the package exceeds descendant limits but only if the in-mempool and in-package + descendants are all considered together (24 including in-mempool descendants and 26 including both + package transactions). + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + self.log.info("Check that in-mempool and in-package descendants are calculated properly in packages") + # Top parent in mempool, M1 + first_coin = self.coins.pop() + parent_value = (first_coin["amount"] - Decimal("0.0002")) / 2 # Deduct reasonable fee and make 2 outputs + inputs = [{"txid": first_coin["txid"], "vout": 0}] + outputs = [{self.address : parent_value}, {ADDRESS_BCRT1_P2WSH_OP_TRUE : parent_value}] + rawtx = node.createrawtransaction(inputs, outputs) + + parent_signed = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=self.privkeys) + assert parent_signed["complete"] + parent_tx = tx_from_hex(parent_signed["hex"]) + parent_txid = parent_tx.rehash() + node.sendrawtransaction(parent_signed["hex"]) + + package_hex = [] + + # Chain A + spk = parent_tx.vout[0].scriptPubKey.hex() + value = parent_value + txid = parent_txid + for i in range(12): + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + if i < 11: # M2a... M12a + node.sendrawtransaction(txhex) + else: # Pa + package_hex.append(txhex) + + # Chain B + value = parent_value - Decimal("0.0001") + rawtx_b = node.createrawtransaction([{"txid": parent_txid, "vout": 1}], {self.address : value}) + tx_child_b = tx_from_hex(rawtx_b) # M2b + tx_child_b.wit.vtxinwit = [CTxInWitness()] + tx_child_b.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])] + tx_child_b_hex = tx_child_b.serialize().hex() + node.sendrawtransaction(tx_child_b_hex) + spk = tx_child_b.vout[0].scriptPubKey.hex() + txid = tx_child_b.rehash() + for i in range(12): + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + if i < 11: # M3b... M13b + node.sendrawtransaction(txhex) + else: # Pb + package_hex.append(txhex) + + assert_equal(24, node.getmempoolinfo()["size"]) + assert_equal(2, len(package_hex)) + testres_too_long = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + self.generate(node, 1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)]) + + def test_desc_count_limits_2(self): + """Create a Package with 24 transaction in mempool and 2 transaction in package: + M1 + ^ ^ + M2 ^ + . ^ + . ^ + . ^ + M24 ^ + ^ + P1 + ^ + P2 + P1 has M1 as a mempool ancestor, P2 has no in-mempool ancestors, but when + combined P2 has M1 as an ancestor and M1 exceeds descendant_limits(23 in-mempool + descendants + 2 in-package descendants, a total of 26 including itself). + """ + + node = self.nodes[0] + package_hex = [] + # M1 + first_coin_a = self.coins.pop() + parent_value = (first_coin_a["amount"] - DEFAULT_FEE) / 2 # Deduct reasonable fee and make 2 outputs + inputs = [{"txid": first_coin_a["txid"], "vout": 0}] + outputs = [{self.address : parent_value}, {ADDRESS_BCRT1_P2WSH_OP_TRUE : parent_value}] + rawtx = node.createrawtransaction(inputs, outputs) + + parent_signed = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=self.privkeys) + assert parent_signed["complete"] + parent_tx = tx_from_hex(parent_signed["hex"]) + parent_txid = parent_tx.rehash() + node.sendrawtransaction(parent_signed["hex"]) + + # Chain M2...M24 + spk = parent_tx.vout[0].scriptPubKey.hex() + value = parent_value + txid = parent_txid + for i in range(23): # M2...M24 + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + node.sendrawtransaction(txhex) + + # P1 + value_p1 = (parent_value - DEFAULT_FEE) + rawtx_p1 = node.createrawtransaction([{"txid": parent_txid, "vout": 1}], [{self.address : value_p1}]) + tx_child_p1 = tx_from_hex(rawtx_p1) + tx_child_p1.wit.vtxinwit = [CTxInWitness()] + tx_child_p1.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])] + tx_child_p1_hex = tx_child_p1.serialize().hex() + txid_child_p1 = tx_child_p1.rehash() + package_hex.append(tx_child_p1_hex) + tx_child_p1_spk = tx_child_p1.vout[0].scriptPubKey.hex() + + # P2 + (_, tx_child_p2_hex, _, _) = make_chain(node, self.address, self.privkeys, txid_child_p1, value_p1, 0, tx_child_p1_spk) + package_hex.append(tx_child_p2_hex) + + assert_equal(24, node.getmempoolinfo()["size"]) + assert_equal(2, len(package_hex)) + testres = node.testmempoolaccept(rawtxs=package_hex) + assert_equal(len(testres), len(package_hex)) + for txres in testres: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + self.generate(node, 1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)]) + + def test_anc_count_limits(self): + """Create a 'V' shaped chain with 24 transactions in the mempool and 3 in the package: + M1a M1b + ^ ^ + M2a M2b + . . + . . + . . + M12a M12b + ^ ^ + Pa Pb + ^ ^ + Pc + The lowest descendant, Pc, exceeds ancestor limits, but only if the in-mempool + and in-package ancestors are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + package_hex = [] + parents_tx = [] + values = [] + scripts = [] + + self.log.info("Check that in-mempool and in-package ancestors are calculated properly in packages") + + # Two chains of 13 transactions each + for _ in range(2): + spk = None + top_coin = self.coins.pop() + txid = top_coin["txid"] + value = top_coin["amount"] + for i in range(13): + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + if i < 12: + node.sendrawtransaction(txhex) + else: # Save the 13th transaction for the package + package_hex.append(txhex) + parents_tx.append(tx) + scripts.append(spk) + values.append(value) + + # Child Pc + child_hex = create_child_with_parents(node, self.address, self.privkeys, parents_tx, values, scripts) + package_hex.append(child_hex) + + assert_equal(24, node.getmempoolinfo()["size"]) + assert_equal(3, len(package_hex)) + testres_too_long = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + self.generate(node, 1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)]) + + def test_anc_count_limits_2(self): + """Create a 'Y' shaped chain with 24 transactions in the mempool and 2 in the package: + M1a M1b + ^ ^ + M2a M2b + . . + . . + . . + M12a M12b + ^ ^ + Pc + ^ + Pd + The lowest descendant, Pd, exceeds ancestor limits, but only if the in-mempool + and in-package ancestors are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + parents_tx = [] + values = [] + scripts = [] + + self.log.info("Check that in-mempool and in-package ancestors are calculated properly in packages") + # Two chains of 12 transactions each + for _ in range(2): + spk = None + top_coin = self.coins.pop() + txid = top_coin["txid"] + value = top_coin["amount"] + for i in range(12): + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.rehash() + value -= Decimal("0.0001") + node.sendrawtransaction(txhex) + if i == 11: + # last 2 transactions will be the parents of Pc + parents_tx.append(tx) + values.append(value) + scripts.append(spk) + + # Child Pc + pc_hex = create_child_with_parents(node, self.address, self.privkeys, parents_tx, values, scripts) + pc_tx = tx_from_hex(pc_hex) + pc_value = sum(values) - Decimal("0.0002") + pc_spk = pc_tx.vout[0].scriptPubKey.hex() + + # Child Pd + (_, pd_hex, _, _) = make_chain(node, self.address, self.privkeys, pc_tx.rehash(), pc_value, 0, pc_spk) + + assert_equal(24, node.getmempoolinfo()["size"]) + testres_too_long = node.testmempoolaccept(rawtxs=[pc_hex, pd_hex]) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + self.generate(node, 1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=[pc_hex, pd_hex])]) + + def test_anc_count_limits_bushy(self): + """Create a tree with 20 transactions in the mempool and 6 in the package: + M1...M4 M5...M8 M9...M12 M13...M16 M17...M20 + ^ ^ ^ ^ ^ (each with 4 parents) + P0 P1 P2 P3 P4 + ^ ^ ^ ^ ^ (5 parents) + PC + Where M(4i+1)...M+(4i+4) are the parents of Pi and P0, P1, P2, P3, and P4 are the parents of PC. + P0... P4 individually only have 4 parents each, and PC has no in-mempool parents. But + combined, PC has 25 in-mempool and in-package parents. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + package_hex = [] + parent_txns = [] + parent_values = [] + scripts = [] + for _ in range(5): # Make package transactions P0 ... P4 + gp_tx = [] + gp_values = [] + gp_scripts = [] + for _ in range(4): # Make mempool transactions M(4i+1)...M(4i+4) + parent_coin = self.coins.pop() + value = parent_coin["amount"] + txid = parent_coin["txid"] + (tx, txhex, value, spk) = make_chain(node, self.address, self.privkeys, txid, value) + gp_tx.append(tx) + gp_values.append(value) + gp_scripts.append(spk) + node.sendrawtransaction(txhex) + # Package transaction Pi + pi_hex = create_child_with_parents(node, self.address, self.privkeys, gp_tx, gp_values, gp_scripts) + package_hex.append(pi_hex) + pi_tx = tx_from_hex(pi_hex) + parent_txns.append(pi_tx) + parent_values.append(Decimal(pi_tx.vout[0].nValue) / COIN) + scripts.append(pi_tx.vout[0].scriptPubKey.hex()) + # Package transaction PC + package_hex.append(create_child_with_parents(node, self.address, self.privkeys, parent_txns, parent_values, scripts)) + + assert_equal(20, node.getmempoolinfo()["size"]) + assert_equal(6, len(package_hex)) + testres = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + self.generate(node, 1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)]) + + def test_anc_size_limits(self): + """Test Case with 2 independent transactions in the mempool and a parent + child in the + package, where the package parent is the child of both mempool transactions (30KvB each): + A B + ^ ^ + C + ^ + D + The lowest descendant, D, exceeds ancestor size limits, but only if the in-mempool + and in-package ancestors are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + parents_tx = [] + values = [] + scripts = [] + target_weight = WITNESS_SCALE_FACTOR * 1000 * 30 # 30KvB + high_fee = Decimal("0.003") # 10 sats/vB + self.log.info("Check that in-mempool and in-package ancestor size limits are calculated properly in packages") + # Mempool transactions A and B + for _ in range(2): + spk = None + top_coin = self.coins.pop() + txid = top_coin["txid"] + value = top_coin["amount"] + (tx, _, _, _) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk, high_fee) + bulked_tx = bulk_transaction(tx, node, target_weight, self.privkeys) + node.sendrawtransaction(bulked_tx.serialize().hex()) + parents_tx.append(bulked_tx) + values.append(Decimal(bulked_tx.vout[0].nValue) / COIN) + scripts.append(bulked_tx.vout[0].scriptPubKey.hex()) + + # Package transaction C + small_pc_hex = create_child_with_parents(node, self.address, self.privkeys, parents_tx, values, scripts, high_fee) + pc_tx = bulk_transaction(tx_from_hex(small_pc_hex), node, target_weight, self.privkeys) + pc_value = Decimal(pc_tx.vout[0].nValue) / COIN + pc_spk = pc_tx.vout[0].scriptPubKey.hex() + pc_hex = pc_tx.serialize().hex() + + # Package transaction D + (small_pd, _, val, spk) = make_chain(node, self.address, self.privkeys, pc_tx.rehash(), pc_value, 0, pc_spk, high_fee) + prevtxs = [{ + "txid": pc_tx.rehash(), + "vout": 0, + "scriptPubKey": spk, + "amount": val, + }] + pd_tx = bulk_transaction(small_pd, node, target_weight, self.privkeys, prevtxs) + pd_hex = pd_tx.serialize().hex() + + assert_equal(2, node.getmempoolinfo()["size"]) + testres_too_heavy = node.testmempoolaccept(rawtxs=[pc_hex, pd_hex]) + for txres in testres_too_heavy: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + self.generate(node, 1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=[pc_hex, pd_hex])]) + + def test_desc_size_limits(self): + """Create 3 mempool transactions and 2 package transactions (25KvB each): + Ma + ^ ^ + Mb Mc + ^ ^ + Pd Pe + The top ancestor in the package exceeds descendant size limits but only if the in-mempool + and in-package descendants are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + target_weight = 21 * 1000 * WITNESS_SCALE_FACTOR + high_fee = Decimal("0.0021") # 10 sats/vB + self.log.info("Check that in-mempool and in-package descendant sizes are calculated properly in packages") + # Top parent in mempool, Ma + first_coin = self.coins.pop() + parent_value = (first_coin["amount"] - high_fee) / 2 # Deduct fee and make 2 outputs + inputs = [{"txid": first_coin["txid"], "vout": 0}] + outputs = [{self.address : parent_value}, {ADDRESS_BCRT1_P2WSH_OP_TRUE: parent_value}] + rawtx = node.createrawtransaction(inputs, outputs) + parent_tx = bulk_transaction(tx_from_hex(rawtx), node, target_weight, self.privkeys) + node.sendrawtransaction(parent_tx.serialize().hex()) + + package_hex = [] + for j in range(2): # Two legs (left and right) + # Mempool transaction (Mb and Mc) + mempool_tx = CTransaction() + spk = parent_tx.vout[j].scriptPubKey.hex() + value = Decimal(parent_tx.vout[j].nValue) / COIN + txid = parent_tx.rehash() + prevtxs = [{ + "txid": txid, + "vout": j, + "scriptPubKey": spk, + "amount": value, + }] + if j == 0: # normal key + (tx_small, _, _, _) = make_chain(node, self.address, self.privkeys, txid, value, j, spk, high_fee) + mempool_tx = bulk_transaction(tx_small, node, target_weight, self.privkeys, prevtxs) + else: # OP_TRUE + inputs = [{"txid": txid, "vout": 1}] + outputs = {self.address: value - high_fee} + small_tx = tx_from_hex(node.createrawtransaction(inputs, outputs)) + mempool_tx = bulk_transaction(small_tx, node, target_weight, None, prevtxs) + node.sendrawtransaction(mempool_tx.serialize().hex()) + + # Package transaction (Pd and Pe) + spk = mempool_tx.vout[0].scriptPubKey.hex() + value = Decimal(mempool_tx.vout[0].nValue) / COIN + txid = mempool_tx.rehash() + (tx_small, _, _, _) = make_chain(node, self.address, self.privkeys, txid, value, 0, spk, high_fee) + prevtxs = [{ + "txid": txid, + "vout": 0, + "scriptPubKey": spk, + "amount": value, + }] + package_tx = bulk_transaction(tx_small, node, target_weight, self.privkeys, prevtxs) + package_hex.append(package_tx.serialize().hex()) + + assert_equal(3, node.getmempoolinfo()["size"]) + assert_equal(2, len(package_hex)) + testres_too_heavy = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres_too_heavy: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + self.generate(node, 1) + assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)]) + +if __name__ == "__main__": + MempoolPackageLimitsTest().main() diff --git a/test/functional/mempool_package_onemore.py b/test/functional/mempool_package_onemore.py index a9e2b000fb..a6fb1dcf35 100755 --- a/test/functional/mempool_package_onemore.py +++ b/test/functional/mempool_package_onemore.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test descendant package tracking carve-out allowing one final transaction in @@ -9,8 +9,13 @@ from decimal import Decimal +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_raises_rpc_error, satoshi_round +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + chain_transaction, +) MAX_ANCESTORS = 25 MAX_DESCENDANTS = 25 @@ -23,26 +28,9 @@ class MempoolPackagesTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() - # Build a transaction that spends parent_txid:vout - # Return amount sent - def chain_transaction(self, node, parent_txids, vouts, value, fee, num_outputs): - send_value = satoshi_round((value - fee)/num_outputs) - inputs = [] - for (txid, vout) in zip(parent_txids, vouts): - inputs.append({'txid' : txid, 'vout' : vout}) - outputs = {} - for _ in range(num_outputs): - outputs[node.getnewaddress()] = send_value - rawtx = node.createrawtransaction(inputs, outputs, 0, True) - signedtx = node.signrawtransactionwithwallet(rawtx) - txid = node.sendrawtransaction(signedtx['hex']) - fulltx = node.getrawtransaction(txid, 1) - assert len(fulltx['vout']) == num_outputs # make sure we didn't generate a change output - return (txid, send_value) - def run_test(self): # Mine some blocks and have them mature. - self.nodes[0].generate(101) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) utxo = self.nodes[0].listunspent(10) txid = utxo[0]['txid'] vout = utxo[0]['vout'] @@ -52,41 +40,41 @@ class MempoolPackagesTest(BitcoinTestFramework): # MAX_ANCESTORS transactions off a confirmed tx should be fine chain = [] for _ in range(4): - (txid, sent_value) = self.chain_transaction(self.nodes[0], [txid], [vout], value, fee, 2) + (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [vout], value, fee, 2) vout = 0 value = sent_value chain.append([txid, value]) for _ in range(MAX_ANCESTORS - 4): - (txid, sent_value) = self.chain_transaction(self.nodes[0], [txid], [0], value, fee, 1) + (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [0], value, fee, 1) value = sent_value chain.append([txid, value]) - (second_chain, second_chain_value) = self.chain_transaction(self.nodes[0], [utxo[1]['txid']], [utxo[1]['vout']], utxo[1]['amount'], fee, 1) + (second_chain, second_chain_value) = chain_transaction(self.nodes[0], [utxo[1]['txid']], [utxo[1]['vout']], utxo[1]['amount'], fee, 1) # Check mempool has MAX_ANCESTORS + 1 transactions in it - assert_equal(len(self.nodes[0].getrawmempool(True)), MAX_ANCESTORS + 1) + assert_equal(len(self.nodes[0].getrawmempool()), MAX_ANCESTORS + 1) # Adding one more transaction on to the chain should fail. - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25]", self.chain_transaction, self.nodes[0], [txid], [0], value, fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25]", chain_transaction, self.nodes[0], [txid], [0], value, fee, 1) # ...even if it chains on from some point in the middle of the chain. - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_transaction, self.nodes[0], [chain[2][0]], [1], chain[2][1], fee, 1) - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_transaction, self.nodes[0], [chain[1][0]], [1], chain[1][1], fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[2][0]], [1], chain[2][1], fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[1][0]], [1], chain[1][1], fee, 1) # ...even if it chains on to two parent transactions with one in the chain. - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_transaction, self.nodes[0], [chain[0][0], second_chain], [1, 0], chain[0][1] + second_chain_value, fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[0][0], second_chain], [1, 0], chain[0][1] + second_chain_value, fee, 1) # ...especially if its > 40k weight - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_transaction, self.nodes[0], [chain[0][0]], [1], chain[0][1], fee, 350) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[0][0]], [1], chain[0][1], fee, 350) # But not if it chains directly off the first transaction - (replacable_txid, replacable_orig_value) = self.chain_transaction(self.nodes[0], [chain[0][0]], [1], chain[0][1], fee, 1) + (replacable_txid, replacable_orig_value) = chain_transaction(self.nodes[0], [chain[0][0]], [1], chain[0][1], fee, 1) # and the second chain should work just fine - self.chain_transaction(self.nodes[0], [second_chain], [0], second_chain_value, fee, 1) + chain_transaction(self.nodes[0], [second_chain], [0], second_chain_value, fee, 1) # Make sure we can RBF the chain which used our carve-out rule - second_tx_outputs = {self.nodes[0].getrawtransaction(replacable_txid, True)["vout"][0]['scriptPubKey']['addresses'][0]: replacable_orig_value - (Decimal(1) / Decimal(100))} + second_tx_outputs = {self.nodes[0].getrawtransaction(replacable_txid, True)["vout"][0]['scriptPubKey']['address']: replacable_orig_value - (Decimal(1) / Decimal(100))} second_tx = self.nodes[0].createrawtransaction([{'txid': chain[0][0], 'vout': 1}], second_tx_outputs) signed_second_tx = self.nodes[0].signrawtransactionwithwallet(second_tx) self.nodes[0].sendrawtransaction(signed_second_tx['hex']) # Finally, check that we added two transactions - assert_equal(len(self.nodes[0].getrawmempool(True)), MAX_ANCESTORS + 3) + assert_equal(len(self.nodes[0].getrawmempool()), MAX_ANCESTORS + 3) if __name__ == '__main__': MempoolPackagesTest().main() diff --git a/test/functional/mempool_packages.py b/test/functional/mempool_packages.py index 461f9237ff..068fdc0b65 100755 --- a/test/functional/mempool_packages.py +++ b/test/functional/mempool_packages.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test descendant package tracking code.""" from decimal import Decimal +from test_framework.blocktools import COINBASE_MATURITY from test_framework.messages import COIN from test_framework.p2p import P2PTxInvStore from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, - satoshi_round, + chain_transaction, ) # default limits @@ -41,43 +42,41 @@ class MempoolPackagesTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() - # Build a transaction that spends parent_txid:vout - # Return amount sent - def chain_transaction(self, node, parent_txid, vout, value, fee, num_outputs): - send_value = satoshi_round((value - fee)/num_outputs) - inputs = [ {'txid' : parent_txid, 'vout' : vout} ] - outputs = {} - for _ in range(num_outputs): - outputs[node.getnewaddress()] = send_value - rawtx = node.createrawtransaction(inputs, outputs) - signedtx = node.signrawtransactionwithwallet(rawtx) - txid = node.sendrawtransaction(signedtx['hex']) - fulltx = node.getrawtransaction(txid, 1) - assert len(fulltx['vout']) == num_outputs # make sure we didn't generate a change output - return (txid, send_value) - def run_test(self): # Mine some blocks and have them mature. peer_inv_store = self.nodes[0].add_p2p_connection(P2PTxInvStore()) # keep track of invs - self.nodes[0].generate(101) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) utxo = self.nodes[0].listunspent(10) txid = utxo[0]['txid'] vout = utxo[0]['vout'] value = utxo[0]['amount'] + assert 'ancestorcount' not in utxo[0] + assert 'ancestorsize' not in utxo[0] + assert 'ancestorfees' not in utxo[0] fee = Decimal("0.0001") # MAX_ANCESTORS transactions off a confirmed tx should be fine chain = [] witness_chain = [] - for _ in range(MAX_ANCESTORS): - (txid, sent_value) = self.chain_transaction(self.nodes[0], txid, 0, value, fee, 1) + ancestor_vsize = 0 + ancestor_fees = Decimal(0) + for i in range(MAX_ANCESTORS): + (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [0], value, fee, 1) value = sent_value chain.append(txid) # We need the wtxids to check P2P announcements - fulltx = self.nodes[0].getrawtransaction(txid) - witnesstx = self.nodes[0].decoderawtransaction(fulltx, True) + witnesstx = self.nodes[0].gettransaction(txid=txid, verbose=True)['decoded'] witness_chain.append(witnesstx['hash']) + # Check that listunspent ancestor{count, size, fees} yield the correct results + wallet_unspent = self.nodes[0].listunspent(minconf=0) + this_unspent = next(utxo_info for utxo_info in wallet_unspent if utxo_info['txid'] == txid) + assert_equal(this_unspent['ancestorcount'], i + 1) + ancestor_vsize += self.nodes[0].getrawtransaction(txid=txid, verbose=True)['vsize'] + assert_equal(this_unspent['ancestorsize'], ancestor_vsize) + ancestor_fees -= self.nodes[0].gettransaction(txid=txid)['fee'] + assert_equal(this_unspent['ancestorfees'], ancestor_fees * COIN) + # Wait until mempool transactions have passed initial broadcast (sent inv and received getdata) # Otherwise, getrawmempool may be inconsistent with getmempoolentry if unbroadcast changes in between peer_inv_store.wait_for_broadcast(witness_chain) @@ -90,9 +89,9 @@ class MempoolPackagesTest(BitcoinTestFramework): descendant_fees = 0 descendant_vsize = 0 - ancestor_vsize = sum([mempool[tx]['vsize'] for tx in mempool]) + assert_equal(ancestor_vsize, sum([mempool[tx]['vsize'] for tx in mempool])) ancestor_count = MAX_ANCESTORS - ancestor_fees = sum([mempool[tx]['fee'] for tx in mempool]) + assert_equal(ancestor_fees, sum([mempool[tx]['fees']['base'] for tx in mempool])) descendants = [] ancestors = list(chain) @@ -102,28 +101,25 @@ class MempoolPackagesTest(BitcoinTestFramework): assert_equal(entry, mempool[x]) # Check that the descendant calculations are correct - assert_equal(mempool[x]['descendantcount'], descendant_count) - descendant_fees += mempool[x]['fee'] - assert_equal(mempool[x]['modifiedfee'], mempool[x]['fee']) - assert_equal(mempool[x]['fees']['base'], mempool[x]['fee']) - assert_equal(mempool[x]['fees']['modified'], mempool[x]['modifiedfee']) - assert_equal(mempool[x]['descendantfees'], descendant_fees * COIN) - assert_equal(mempool[x]['fees']['descendant'], descendant_fees) - descendant_vsize += mempool[x]['vsize'] - assert_equal(mempool[x]['descendantsize'], descendant_vsize) + assert_equal(entry['descendantcount'], descendant_count) + descendant_fees += entry['fees']['base'] + assert_equal(entry['fees']['modified'], entry['fees']['base']) + assert_equal(entry['fees']['descendant'], descendant_fees) + descendant_vsize += entry['vsize'] + assert_equal(entry['descendantsize'], descendant_vsize) descendant_count += 1 # Check that ancestor calculations are correct - assert_equal(mempool[x]['ancestorcount'], ancestor_count) - assert_equal(mempool[x]['ancestorfees'], ancestor_fees * COIN) - assert_equal(mempool[x]['ancestorsize'], ancestor_vsize) - ancestor_vsize -= mempool[x]['vsize'] - ancestor_fees -= mempool[x]['fee'] + assert_equal(entry['ancestorcount'], ancestor_count) + assert_equal(entry['fees']['ancestor'], ancestor_fees) + assert_equal(entry['ancestorsize'], ancestor_vsize) + ancestor_vsize -= entry['vsize'] + ancestor_fees -= entry['fees']['base'] ancestor_count -= 1 # Check that parent/child list is correct - assert_equal(mempool[x]['spentby'], descendants[-1:]) - assert_equal(mempool[x]['depends'], ancestors[-2:-1]) + assert_equal(entry['spentby'], descendants[-1:]) + assert_equal(entry['depends'], ancestors[-2:-1]) # Check that getmempooldescendants is correct assert_equal(sorted(descendants), sorted(self.nodes[0].getmempooldescendants(x))) @@ -166,12 +162,11 @@ class MempoolPackagesTest(BitcoinTestFramework): # Check that ancestor modified fees includes fee deltas from # prioritisetransaction self.nodes[0].prioritisetransaction(txid=chain[0], fee_delta=1000) - mempool = self.nodes[0].getrawmempool(True) ancestor_fees = 0 for x in chain: - ancestor_fees += mempool[x]['fee'] - assert_equal(mempool[x]['fees']['ancestor'], ancestor_fees + Decimal('0.00001')) - assert_equal(mempool[x]['ancestorfees'], ancestor_fees * COIN + 1000) + entry = self.nodes[0].getmempoolentry(x) + ancestor_fees += entry['fees']['base'] + assert_equal(entry['fees']['ancestor'], ancestor_fees + Decimal('0.00001')) # Undo the prioritisetransaction for later tests self.nodes[0].prioritisetransaction(txid=chain[0], fee_delta=-1000) @@ -179,21 +174,19 @@ class MempoolPackagesTest(BitcoinTestFramework): # Check that descendant modified fees includes fee deltas from # prioritisetransaction self.nodes[0].prioritisetransaction(txid=chain[-1], fee_delta=1000) - mempool = self.nodes[0].getrawmempool(True) descendant_fees = 0 for x in reversed(chain): - descendant_fees += mempool[x]['fee'] - assert_equal(mempool[x]['fees']['descendant'], descendant_fees + Decimal('0.00001')) - assert_equal(mempool[x]['descendantfees'], descendant_fees * COIN + 1000) + entry = self.nodes[0].getmempoolentry(x) + descendant_fees += entry['fees']['base'] + assert_equal(entry['fees']['descendant'], descendant_fees + Decimal('0.00001')) # Adding one more transaction on to the chain should fail. - assert_raises_rpc_error(-26, "too-long-mempool-chain", self.chain_transaction, self.nodes[0], txid, vout, value, fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain", chain_transaction, self.nodes[0], [txid], [vout], value, fee, 1) # Check that prioritising a tx before it's added to the mempool works # First clear the mempool by mining a block. - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) assert_equal(len(self.nodes[0].getrawmempool()), 0) # Prioritise a transaction that has been mined, then add it back to the # mempool by using invalidateblock. @@ -203,16 +196,13 @@ class MempoolPackagesTest(BitcoinTestFramework): self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash()) # Now check that the transaction is in the mempool, with the right modified fee - mempool = self.nodes[0].getrawmempool(True) - descendant_fees = 0 for x in reversed(chain): - descendant_fees += mempool[x]['fee'] + entry = self.nodes[0].getmempoolentry(x) + descendant_fees += entry['fees']['base'] if (x == chain[-1]): - assert_equal(mempool[x]['modifiedfee'], mempool[x]['fee']+satoshi_round(0.00002)) - assert_equal(mempool[x]['fees']['modified'], mempool[x]['fee']+satoshi_round(0.00002)) - assert_equal(mempool[x]['descendantfees'], descendant_fees * COIN + 2000) - assert_equal(mempool[x]['fees']['descendant'], descendant_fees+satoshi_round(0.00002)) + assert_equal(entry['fees']['modified'], entry['fees']['base'] + Decimal("0.00002")) + assert_equal(entry['fees']['descendant'], descendant_fees + Decimal("0.00002")) # Check that node1's mempool is as expected (-> custom ancestor limit) mempool0 = self.nodes[0].getrawmempool(False) @@ -237,7 +227,7 @@ class MempoolPackagesTest(BitcoinTestFramework): transaction_package = [] tx_children = [] # First create one parent tx with 10 children - (txid, sent_value) = self.chain_transaction(self.nodes[0], txid, vout, value, fee, 10) + (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [vout], value, fee, 10) parent_transaction = txid for i in range(10): transaction_package.append({'txid': txid, 'vout': i, 'amount': sent_value}) @@ -246,7 +236,7 @@ class MempoolPackagesTest(BitcoinTestFramework): chain = [] # save sent txs for the purpose of checking node1's mempool later (see below) for _ in range(MAX_DESCENDANTS - 1): utxo = transaction_package.pop(0) - (txid, sent_value) = self.chain_transaction(self.nodes[0], utxo['txid'], utxo['vout'], utxo['amount'], fee, 10) + (txid, sent_value) = chain_transaction(self.nodes[0], [utxo['txid']], [utxo['vout']], utxo['amount'], fee, 10) chain.append(txid) if utxo['txid'] is parent_transaction: tx_children.append(txid) @@ -262,13 +252,13 @@ class MempoolPackagesTest(BitcoinTestFramework): # Sending one more chained transaction will fail utxo = transaction_package.pop(0) - assert_raises_rpc_error(-26, "too-long-mempool-chain", self.chain_transaction, self.nodes[0], utxo['txid'], utxo['vout'], utxo['amount'], fee, 10) + assert_raises_rpc_error(-26, "too-long-mempool-chain", chain_transaction, self.nodes[0], [utxo['txid']], [utxo['vout']], utxo['amount'], fee, 10) # Check that node1's mempool is as expected, containing: # - txs from previous ancestor test (-> custom ancestor limit) # - parent tx for descendant test # - txs chained off parent tx (-> custom descendant limit) - self.wait_until(lambda: len(self.nodes[1].getrawmempool(False)) == + self.wait_until(lambda: len(self.nodes[1].getrawmempool()) == MAX_ANCESTORS_CUSTOM + 1 + MAX_DESCENDANTS_CUSTOM, timeout=10) mempool0 = self.nodes[0].getrawmempool(False) mempool1 = self.nodes[1].getrawmempool(False) @@ -284,8 +274,7 @@ class MempoolPackagesTest(BitcoinTestFramework): # Test reorg handling # First, the basics: - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) self.nodes[1].invalidateblock(self.nodes[0].getbestblockhash()) self.nodes[1].reconsiderblock(self.nodes[0].getbestblockhash()) @@ -308,7 +297,7 @@ class MempoolPackagesTest(BitcoinTestFramework): value = utxo[0]['amount'] vout = utxo[0]['vout'] - send_value = satoshi_round((value - fee)/2) + send_value = (value - fee) / 2 inputs = [ {'txid' : txid, 'vout' : vout} ] outputs = {} for _ in range(2): @@ -320,19 +309,18 @@ class MempoolPackagesTest(BitcoinTestFramework): value = send_value # Create tx1 - tx1_id, _ = self.chain_transaction(self.nodes[0], tx0_id, 0, value, fee, 1) + tx1_id, _ = chain_transaction(self.nodes[0], [tx0_id], [0], value, fee, 1) # Create tx2-7 vout = 1 txid = tx0_id for _ in range(6): - (txid, sent_value) = self.chain_transaction(self.nodes[0], txid, vout, value, fee, 1) + (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [vout], value, fee, 1) vout = 0 value = sent_value # Mine these in a block - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # Now generate tx8, with a big fee inputs = [ {'txid' : tx1_id, 'vout': 0}, {'txid' : txid, 'vout': 0} ] diff --git a/test/functional/mempool_persist.py b/test/functional/mempool_persist.py index 752b925b92..8c9379b90b 100755 --- a/test/functional/mempool_persist.py +++ b/test/functional/mempool_persist.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test mempool persistence. @@ -46,6 +46,7 @@ from test_framework.util import ( assert_greater_than_or_equal, assert_raises_rpc_error, ) +from test_framework.wallet import MiniWallet class MempoolPersistTest(BitcoinTestFramework): @@ -53,15 +54,26 @@ class MempoolPersistTest(BitcoinTestFramework): self.num_nodes = 3 self.extra_args = [[], ["-persistmempool=0"], []] - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def run_test(self): + self.mini_wallet = MiniWallet(self.nodes[2]) + self.mini_wallet.rescan_utxos() + if self.is_sqlite_compiled(): + self.nodes[2].createwallet( + wallet_name="watch", + descriptors=True, + disable_private_keys=True, + load_on_startup=False, + ) + wallet_watch = self.nodes[2].get_wallet_rpc("watch") + assert_equal([{'success': True}], wallet_watch.importdescriptors([{'desc': self.mini_wallet.get_descriptor(), 'timestamp': 0}])) + self.log.debug("Send 5 transactions from node2 (to its own address)") tx_creation_time_lower = int(time.time()) for _ in range(5): - last_txid = self.nodes[2].sendtoaddress(self.nodes[2].getnewaddress(), Decimal("10")) - node2_balance = self.nodes[2].getbalance() + last_txid = self.mini_wallet.send_self_transfer(from_node=self.nodes[2])["txid"] + if self.is_sqlite_compiled(): + self.nodes[2].syncwithvalidationinterfacequeue() # Flush mempool to wallet + node2_balance = wallet_watch.getbalance() self.sync_all() tx_creation_time_higher = int(time.time()) @@ -82,16 +94,16 @@ class MempoolPersistTest(BitcoinTestFramework): assert_equal(total_fee_old, self.nodes[0].getmempoolinfo()['total_fee']) assert_equal(total_fee_old, sum(v['fees']['base'] for k, v in self.nodes[0].getrawmempool(verbose=True).items())) - tx_creation_time = self.nodes[0].getmempoolentry(txid=last_txid)['time'] + last_entry = self.nodes[0].getmempoolentry(txid=last_txid) + tx_creation_time = last_entry['time'] assert_greater_than_or_equal(tx_creation_time, tx_creation_time_lower) assert_greater_than_or_equal(tx_creation_time_higher, tx_creation_time) # disconnect nodes & make a txn that remains in the unbroadcast set. self.disconnect_nodes(0, 1) - assert(len(self.nodes[0].getpeerinfo()) == 0) - assert(len(self.nodes[0].p2ps) == 0) - self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), Decimal("12")) - self.connect_nodes(0, 2) + assert_equal(len(self.nodes[0].getpeerinfo()), 0) + assert_equal(len(self.nodes[0].p2ps), 0) + self.mini_wallet.send_self_transfer(from_node=self.nodes[0]) self.log.debug("Stop-start the nodes. Verify that node0 has the transactions in its mempool and node1 does not. Verify that node2 calculates its balance correctly after loading wallet transactions.") self.stop_nodes() @@ -111,17 +123,19 @@ class MempoolPersistTest(BitcoinTestFramework): fees = self.nodes[0].getmempoolentry(txid=last_txid)['fees'] assert_equal(fees['base'] + Decimal('0.00001000'), fees['modified']) - self.log.debug('Verify time is loaded correctly') - assert_equal(tx_creation_time, self.nodes[0].getmempoolentry(txid=last_txid)['time']) + self.log.debug('Verify all fields are loaded correctly') + assert_equal(last_entry, self.nodes[0].getmempoolentry(txid=last_txid)) # Verify accounting of mempool transactions after restart is correct - self.nodes[2].syncwithvalidationinterfacequeue() # Flush mempool to wallet - assert_equal(node2_balance, self.nodes[2].getbalance()) + if self.is_sqlite_compiled(): + self.nodes[2].loadwallet("watch") + wallet_watch = self.nodes[2].get_wallet_rpc("watch") + self.nodes[2].syncwithvalidationinterfacequeue() # Flush mempool to wallet + assert_equal(node2_balance, wallet_watch.getbalance()) - # start node0 with wallet disabled so wallet transactions don't get resubmitted self.log.debug("Stop-start node0 with -persistmempool=0. Verify that it doesn't load its mempool.dat file.") self.stop_nodes() - self.start_node(0, extra_args=["-persistmempool=0", "-disablewallet"]) + self.start_node(0, extra_args=["-persistmempool=0"]) assert self.nodes[0].getmempoolinfo()["loaded"] assert_equal(len(self.nodes[0].getrawmempool()), 0) @@ -135,13 +149,14 @@ class MempoolPersistTest(BitcoinTestFramework): mempooldat1 = os.path.join(self.nodes[1].datadir, self.chain, 'mempool.dat') self.log.debug("Remove the mempool.dat file. Verify that savemempool to disk via RPC re-creates it") os.remove(mempooldat0) - self.nodes[0].savemempool() + result0 = self.nodes[0].savemempool() assert os.path.isfile(mempooldat0) + assert_equal(result0['filename'], mempooldat0) self.log.debug("Stop nodes, make node1 use mempool.dat from node0. Verify it has 6 transactions") os.rename(mempooldat0, mempooldat1) self.stop_nodes() - self.start_node(1, extra_args=[]) + self.start_node(1, extra_args=["-persistmempool"]) assert self.nodes[1].getmempoolinfo()["loaded"] assert_equal(len(self.nodes[1].getrawmempool()), 6) @@ -160,22 +175,22 @@ class MempoolPersistTest(BitcoinTestFramework): self.start_node(0) # clear out mempool - node0.generate(1) + self.generate(node0, 1, sync_fun=self.no_op) # ensure node0 doesn't have any connections # make a transaction that will remain in the unbroadcast set - assert(len(node0.getpeerinfo()) == 0) - assert(len(node0.p2ps) == 0) - node0.sendtoaddress(self.nodes[1].getnewaddress(), Decimal("12")) + assert_equal(len(node0.getpeerinfo()), 0) + assert_equal(len(node0.p2ps), 0) + self.mini_wallet.send_self_transfer(from_node=node0) # shutdown, then startup with wallet disabled - self.stop_nodes() - self.start_node(0, extra_args=["-disablewallet"]) + self.restart_node(0, extra_args=["-disablewallet"]) # check that txn gets broadcast due to unbroadcast logic conn = node0.add_p2p_connection(P2PTxInvStore()) - node0.mockscheduler(16*60) # 15 min + 1 for buffer + node0.mockscheduler(16 * 60) # 15 min + 1 for buffer self.wait_until(lambda: len(conn.get_invs()) == 1) -if __name__ == '__main__': + +if __name__ == "__main__": MempoolPersistTest().main() diff --git a/test/functional/mempool_reorg.py b/test/functional/mempool_reorg.py index 8e1f87e42c..91f2d0051a 100755 --- a/test/functional/mempool_reorg.py +++ b/test/functional/mempool_reorg.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test mempool re-org scenarios. @@ -8,10 +8,9 @@ Test re-org scenarios with a mempool that contains transactions that spend (directly or indirectly) coinbase transactions. """ -from test_framework.blocktools import create_raw_transaction from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error - +from test_framework.wallet import MiniWallet class MempoolCoinbaseTest(BitcoinTestFramework): def set_test_params(self): @@ -23,86 +22,88 @@ class MempoolCoinbaseTest(BitcoinTestFramework): [] ] - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def run_test(self): + wallet = MiniWallet(self.nodes[0]) + # Start with a 200 block chain assert_equal(self.nodes[0].getblockcount(), 200) - # Mine four blocks. After this, nodes[0] blocks - # 101, 102, and 103 are spend-able. - new_blocks = self.nodes[1].generate(4) - self.sync_all() - - node0_address = self.nodes[0].getnewaddress() - node1_address = self.nodes[1].getnewaddress() + self.log.info("Add 4 coinbase utxos to the miniwallet") + # Block 76 contains the first spendable coinbase txs. + first_block = 76 + wallet.rescan_utxos() # Three scenarios for re-orging coinbase spends in the memory pool: - # 1. Direct coinbase spend : spend_101 - # 2. Indirect (coinbase spend in chain, child in mempool) : spend_102 and spend_102_1 - # 3. Indirect (coinbase and child both in chain) : spend_103 and spend_103_1 - # Use invalidatblock to make all of the above coinbase spends invalid (immature coinbase), + # 1. Direct coinbase spend : spend_1 + # 2. Indirect (coinbase spend in chain, child in mempool) : spend_2 and spend_2_1 + # 3. Indirect (coinbase and child both in chain) : spend_3 and spend_3_1 + # Use invalidateblock to make all of the above coinbase spends invalid (immature coinbase), # and make sure the mempool code behaves correctly. - b = [self.nodes[0].getblockhash(n) for n in range(101, 105)] + b = [self.nodes[0].getblockhash(n) for n in range(first_block, first_block+4)] coinbase_txids = [self.nodes[0].getblock(h)['tx'][0] for h in b] - spend_101_raw = create_raw_transaction(self.nodes[0], coinbase_txids[1], node1_address, amount=49.99) - spend_102_raw = create_raw_transaction(self.nodes[0], coinbase_txids[2], node0_address, amount=49.99) - spend_103_raw = create_raw_transaction(self.nodes[0], coinbase_txids[3], node0_address, amount=49.99) - - # Create a transaction which is time-locked to two blocks in the future - timelock_tx = self.nodes[0].createrawtransaction( - inputs=[{ - "txid": coinbase_txids[0], - "vout": 0, - }], - outputs={node0_address: 49.99}, - locktime=self.nodes[0].getblockcount() + 2, - ) - timelock_tx = self.nodes[0].signrawtransactionwithwallet(timelock_tx)["hex"] - # This will raise an exception because the timelock transaction is too immature to spend + utxo_1 = wallet.get_utxo(txid=coinbase_txids[1]) + utxo_2 = wallet.get_utxo(txid=coinbase_txids[2]) + utxo_3 = wallet.get_utxo(txid=coinbase_txids[3]) + self.log.info("Create three transactions spending from coinbase utxos: spend_1, spend_2, spend_3") + spend_1 = wallet.create_self_transfer(utxo_to_spend=utxo_1) + spend_2 = wallet.create_self_transfer(utxo_to_spend=utxo_2) + spend_3 = wallet.create_self_transfer(utxo_to_spend=utxo_3) + + self.log.info("Create another transaction which is time-locked to two blocks in the future") + utxo = wallet.get_utxo(txid=coinbase_txids[0]) + timelock_tx = wallet.create_self_transfer( + utxo_to_spend=utxo, + mempool_valid=False, + locktime=self.nodes[0].getblockcount() + 2 + )['hex'] + + self.log.info("Check that the time-locked transaction is too immature to spend") assert_raises_rpc_error(-26, "non-final", self.nodes[0].sendrawtransaction, timelock_tx) - # Broadcast and mine spend_102 and 103: - spend_102_id = self.nodes[0].sendrawtransaction(spend_102_raw) - spend_103_id = self.nodes[0].sendrawtransaction(spend_103_raw) - self.nodes[0].generate(1) - # Time-locked transaction is still too immature to spend + self.log.info("Broadcast and mine spend_2 and spend_3") + wallet.sendrawtransaction(from_node=self.nodes[0], tx_hex=spend_2['hex']) + wallet.sendrawtransaction(from_node=self.nodes[0], tx_hex=spend_3['hex']) + self.log.info("Generate a block") + self.generate(self.nodes[0], 1) + self.log.info("Check that time-locked transaction is still too immature to spend") assert_raises_rpc_error(-26, 'non-final', self.nodes[0].sendrawtransaction, timelock_tx) - # Create 102_1 and 103_1: - spend_102_1_raw = create_raw_transaction(self.nodes[0], spend_102_id, node1_address, amount=49.98) - spend_103_1_raw = create_raw_transaction(self.nodes[0], spend_103_id, node1_address, amount=49.98) + self.log.info("Create spend_2_1 and spend_3_1") + spend_2_utxo = wallet.get_utxo(txid=spend_2['txid']) + spend_2_1 = wallet.create_self_transfer(utxo_to_spend=spend_2_utxo) + spend_3_utxo = wallet.get_utxo(txid=spend_3['txid']) + spend_3_1 = wallet.create_self_transfer(utxo_to_spend=spend_3_utxo) - # Broadcast and mine 103_1: - spend_103_1_id = self.nodes[0].sendrawtransaction(spend_103_1_raw) - last_block = self.nodes[0].generate(1) - # Sync blocks, so that peer 1 gets the block before timelock_tx - # Otherwise, peer 1 would put the timelock_tx in recentRejects - self.sync_all() + self.log.info("Broadcast and mine spend_3_1") + spend_3_1_id = self.nodes[0].sendrawtransaction(spend_3_1['hex']) + self.log.info("Generate a block") + last_block = self.generate(self.nodes[0], 1) + # generate() implicitly syncs blocks, so that peer 1 gets the block before timelock_tx + # Otherwise, peer 1 would put the timelock_tx in m_recent_rejects - # Time-locked transaction can now be spent + self.log.info("The time-locked transaction can now be spent") timelock_tx_id = self.nodes[0].sendrawtransaction(timelock_tx) - # ... now put spend_101 and spend_102_1 in memory pools: - spend_101_id = self.nodes[0].sendrawtransaction(spend_101_raw) - spend_102_1_id = self.nodes[0].sendrawtransaction(spend_102_1_raw) + self.log.info("Add spend_1 and spend_2_1 to the mempool") + spend_1_id = self.nodes[0].sendrawtransaction(spend_1['hex']) + spend_2_1_id = self.nodes[0].sendrawtransaction(spend_2_1['hex']) - assert_equal(set(self.nodes[0].getrawmempool()), {spend_101_id, spend_102_1_id, timelock_tx_id}) + assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, timelock_tx_id}) self.sync_all() + self.log.info("invalidate the last block") for node in self.nodes: node.invalidateblock(last_block[0]) - # Time-locked transaction is now too immature and has been removed from the mempool - # spend_103_1 has been re-orged out of the chain and is back in the mempool - assert_equal(set(self.nodes[0].getrawmempool()), {spend_101_id, spend_102_1_id, spend_103_1_id}) + self.log.info("The time-locked transaction is now too immature and has been removed from the mempool") + self.log.info("spend_3_1 has been re-orged out of the chain and is back in the mempool") + assert_equal(set(self.nodes[0].getrawmempool()), {spend_1_id, spend_2_1_id, spend_3_1_id}) - # Use invalidateblock to re-org back and make all those coinbase spends - # immature/invalid: + self.log.info("Use invalidateblock to re-org back and make all those coinbase spends immature/invalid") + b = self.nodes[0].getblockhash(first_block + 100) for node in self.nodes: - node.invalidateblock(new_blocks[0]) + node.invalidateblock(b) - # mempool should be empty. + self.log.info("Check that the mempool is empty") assert_equal(set(self.nodes[0].getrawmempool()), set()) self.sync_all() diff --git a/test/functional/mempool_resurrect.py b/test/functional/mempool_resurrect.py index 4aa58270b6..3e610d02ac 100755 --- a/test/functional/mempool_resurrect.py +++ b/test/functional/mempool_resurrect.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test resurrection of mined transactions when the blockchain is re-organized.""" +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal from test_framework.wallet import MiniWallet @@ -19,8 +20,8 @@ class MempoolCoinbaseTest(BitcoinTestFramework): wallet = MiniWallet(node) # Add enough mature utxos to the wallet so that all txs spend confirmed coins - wallet.generate(3) - node.generate(100) + self.generate(wallet, 3) + self.generate(node, COINBASE_MATURITY) # Spend block 1/2/3's coinbase transactions # Mine a block @@ -33,9 +34,9 @@ class MempoolCoinbaseTest(BitcoinTestFramework): # ... make sure all the transactions are confirmed again blocks = [] spends1_ids = [wallet.send_self_transfer(from_node=node)['txid'] for _ in range(3)] - blocks.extend(node.generate(1)) + blocks.extend(self.generate(node, 1)) spends2_ids = [wallet.send_self_transfer(from_node=node)['txid'] for _ in range(3)] - blocks.extend(node.generate(1)) + blocks.extend(self.generate(node, 1)) spends_ids = set(spends1_ids + spends2_ids) @@ -52,7 +53,7 @@ class MempoolCoinbaseTest(BitcoinTestFramework): assert_equal(set(node.getrawmempool()), spends_ids) # Generate another block, they should all get mined - blocks = node.generate(1) + blocks = self.generate(node, 1) # mempool should be empty, all txns confirmed assert_equal(set(node.getrawmempool()), set()) confirmed_txns = set(node.getblock(blocks[0])['tx']) diff --git a/test/functional/mempool_spend_coinbase.py b/test/functional/mempool_spend_coinbase.py index a249a73315..9c43ddaf6f 100755 --- a/test/functional/mempool_spend_coinbase.py +++ b/test/functional/mempool_spend_coinbase.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test spending coinbase transactions. @@ -20,40 +20,41 @@ from test_framework.wallet import MiniWallet class MempoolSpendCoinbaseTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.setup_clean_chain = True def run_test(self): wallet = MiniWallet(self.nodes[0]) - wallet.generate(200) - chain_height = self.nodes[0].getblockcount() - assert_equal(chain_height, 200) + # Invalidate two blocks, so that miniwallet has access to a coin that will mature in the next block + chain_height = 198 + self.nodes[0].invalidateblock(self.nodes[0].getblockhash(chain_height + 1)) + assert_equal(chain_height, self.nodes[0].getblockcount()) + wallet.rescan_utxos() # Coinbase at height chain_height-100+1 ok in mempool, should # get mined. Coinbase at height chain_height-100+2 is # too immature to spend. - b = [self.nodes[0].getblockhash(n) for n in range(101, 103)] - coinbase_txids = [self.nodes[0].getblock(h)['tx'][0] for h in b] - utxo_101 = wallet.get_utxo(txid=coinbase_txids[0]) - utxo_102 = wallet.get_utxo(txid=coinbase_txids[1]) + coinbase_txid = lambda h: self.nodes[0].getblock(self.nodes[0].getblockhash(h))['tx'][0] + utxo_mature = wallet.get_utxo(txid=coinbase_txid(chain_height - 100 + 1)) + utxo_immature = wallet.get_utxo(txid=coinbase_txid(chain_height - 100 + 2)) - spend_101_id = wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_101)["txid"] + spend_mature_id = wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_mature)["txid"] - # coinbase at height 102 should be too immature to spend + # other coinbase should be too immature to spend + immature_tx = wallet.create_self_transfer(utxo_to_spend=utxo_immature, mempool_valid=False) assert_raises_rpc_error(-26, "bad-txns-premature-spend-of-coinbase", - lambda: wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_102)) + lambda: self.nodes[0].sendrawtransaction(immature_tx['hex'])) - # mempool should have just spend_101: - assert_equal(self.nodes[0].getrawmempool(), [spend_101_id]) + # mempool should have just the mature one + assert_equal(self.nodes[0].getrawmempool(), [spend_mature_id]) - # mine a block, spend_101 should get confirmed - self.nodes[0].generate(1) + # mine a block, mature one should get confirmed + self.generate(self.nodes[0], 1) assert_equal(set(self.nodes[0].getrawmempool()), set()) - # ... and now height 102 can be spent: - spend_102_id = wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_102)["txid"] - assert_equal(self.nodes[0].getrawmempool(), [spend_102_id]) + # ... and now previously immature can be spent: + spend_new_id = self.nodes[0].sendrawtransaction(immature_tx['hex']) + assert_equal(self.nodes[0].getrawmempool(), [spend_new_id]) if __name__ == '__main__': diff --git a/test/functional/mempool_unbroadcast.py b/test/functional/mempool_unbroadcast.py index b475b65e68..adf7326dac 100755 --- a/test/functional/mempool_unbroadcast.py +++ b/test/functional/mempool_unbroadcast.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test that the mempool ensures transaction delivery by periodically sending @@ -32,7 +32,7 @@ class MempoolUnbroadcastTest(BitcoinTestFramework): node = self.nodes[0] min_relay_fee = node.getnetworkinfo()["relayfee"] - utxos = create_confirmed_utxos(min_relay_fee, node, 10) + utxos = create_confirmed_utxos(self, min_relay_fee, node, 10) self.disconnect_nodes(0, 1) @@ -92,6 +92,10 @@ class MempoolUnbroadcastTest(BitcoinTestFramework): self.disconnect_nodes(0, 1) node.disconnect_p2ps() + self.log.info("Rebroadcast transaction and ensure it is not added to unbroadcast set when already in mempool") + rpc_tx_hsh = node.sendrawtransaction(txFS["hex"]) + assert not node.getmempoolentry(rpc_tx_hsh)['unbroadcast'] + def test_txn_removal(self): self.log.info("Test that transactions removed from mempool are removed from unbroadcast set") node = self.nodes[0] @@ -105,7 +109,8 @@ class MempoolUnbroadcastTest(BitcoinTestFramework): # a block removal_reason = "Removed {} from set of unbroadcast txns before confirmation that txn was sent out".format(txhsh) with node.assert_debug_log([removal_reason]): - node.generate(1) + self.generate(node, 1, sync_fun=self.no_op) + if __name__ == "__main__": MempoolUnbroadcastTest().main() diff --git a/test/functional/mempool_updatefromblock.py b/test/functional/mempool_updatefromblock.py index 8baf974a0a..51de582ce0 100755 --- a/test/functional/mempool_updatefromblock.py +++ b/test/functional/mempool_updatefromblock.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test mempool descendants/ancestors information update. @@ -17,7 +17,7 @@ from test_framework.util import assert_equal class MempoolUpdateFromBlockTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.extra_args = [['-limitdescendantsize=1000', '-limitancestorsize=1000']] + self.extra_args = [['-limitdescendantsize=1000', '-limitancestorsize=1000', '-limitancestorcount=100']] def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -86,12 +86,12 @@ class MempoolUpdateFromBlockTest(BitcoinTestFramework): unsigned_raw_tx = self.nodes[0].createrawtransaction(inputs, outputs) signed_raw_tx = self.nodes[0].signrawtransactionwithwallet(unsigned_raw_tx) tx_id.append(self.nodes[0].sendrawtransaction(signed_raw_tx['hex'])) - tx_size.append(self.nodes[0].getrawmempool(True)[tx_id[-1]]['vsize']) + tx_size.append(self.nodes[0].getmempoolentry(tx_id[-1])['vsize']) if tx_count in n_tx_to_mine: # The created transactions are mined into blocks by batches. self.log.info('The batch of {} transactions has been accepted into the mempool.'.format(len(self.nodes[0].getrawmempool()))) - block_hash = self.nodes[0].generate(1)[0] + block_hash = self.generate(self.nodes[0], 1)[0] if not first_block_hash: first_block_hash = block_hash assert_equal(len(self.nodes[0].getrawmempool()), 0) @@ -109,10 +109,11 @@ class MempoolUpdateFromBlockTest(BitcoinTestFramework): self.log.info('Checking descendants/ancestors properties of all of the in-mempool transactions...') for k, tx in enumerate(tx_id): self.log.debug('Check transaction #{}.'.format(k)) - assert_equal(self.nodes[0].getrawmempool(True)[tx]['descendantcount'], size - k) - assert_equal(self.nodes[0].getrawmempool(True)[tx]['descendantsize'], sum(tx_size[k:size])) - assert_equal(self.nodes[0].getrawmempool(True)[tx]['ancestorcount'], k + 1) - assert_equal(self.nodes[0].getrawmempool(True)[tx]['ancestorsize'], sum(tx_size[0:(k + 1)])) + entry = self.nodes[0].getmempoolentry(tx) + assert_equal(entry['descendantcount'], size - k) + assert_equal(entry['descendantsize'], sum(tx_size[k:size])) + assert_equal(entry['ancestorcount'], k + 1) + assert_equal(entry['ancestorsize'], sum(tx_size[0:(k + 1)])) def run_test(self): # Use batch size limited by DEFAULT_ANCESTOR_LIMIT = 25 to not fire "too many unconfirmed parents" error. diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index ba467c1517..9c64bb1945 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test mining RPCs @@ -13,6 +13,7 @@ from decimal import Decimal from test_framework.blocktools import ( create_coinbase, + get_witness_script, NORMAL_GBT_REQUEST_PARAMS, TIME_GENESIS_BLOCK, ) @@ -20,6 +21,7 @@ from test_framework.messages import ( CBlock, CBlockHeader, BLOCK_HEADER_SIZE, + ser_uint256, ) from test_framework.p2p import P2PDataStore from test_framework.test_framework import BitcoinTestFramework @@ -27,6 +29,8 @@ from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) +from test_framework.wallet import MiniWallet + VERSIONBITS_TOP_BITS = 0x20000000 VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT = 28 @@ -53,25 +57,26 @@ class MiningTest(BitcoinTestFramework): self.log.info('Create some old blocks') for t in range(TIME_GENESIS_BLOCK, TIME_GENESIS_BLOCK + 200 * 600, 600): self.nodes[0].setmocktime(t) - self.nodes[0].generate(1) + self.generate(self.wallet, 1, sync_fun=self.no_op) mining_info = self.nodes[0].getmininginfo() assert_equal(mining_info['blocks'], 200) assert_equal(mining_info['currentblocktx'], 0) assert_equal(mining_info['currentblockweight'], 4000) self.log.info('test blockversion') - self.restart_node(0, extra_args=['-mocktime={}'.format(t), '-blockversion=1337']) + self.restart_node(0, extra_args=[f'-mocktime={t}', '-blockversion=1337']) self.connect_nodes(0, 1) assert_equal(1337, self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) - self.restart_node(0, extra_args=['-mocktime={}'.format(t)]) + self.restart_node(0, extra_args=[f'-mocktime={t}']) self.connect_nodes(0, 1) assert_equal(VERSIONBITS_TOP_BITS + (1 << VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT), self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) self.restart_node(0) self.connect_nodes(0, 1) def run_test(self): - self.mine_chain() node = self.nodes[0] + self.wallet = MiniWallet(node) + self.mine_chain() def assert_submitblock(block, result_str_1, result_str_2=None): block.solve() @@ -89,8 +94,22 @@ class MiningTest(BitcoinTestFramework): assert_equal(mining_info['networkhashps'], Decimal('0.003333333333333334')) assert_equal(mining_info['pooledtx'], 0) - # Mine a block to leave initial block download - node.generatetoaddress(1, node.get_deterministic_priv_key().address) + self.log.info("getblocktemplate: Test default witness commitment") + txid = int(self.wallet.send_self_transfer(from_node=node)['wtxid'], 16) + tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) + + # Check that default_witness_commitment is present. + assert 'default_witness_commitment' in tmpl + witness_commitment = tmpl['default_witness_commitment'] + + # Check that default_witness_commitment is correct. + witness_root = CBlock.get_merkle_root([ser_uint256(0), + ser_uint256(txid)]) + script = get_witness_script(witness_root, 0) + assert_equal(witness_commitment, script.hex()) + + # Mine a block to leave initial block download and clear the mempool + self.generatetoaddress(node, 1, node.get_deterministic_priv_key().address) tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) self.log.info("getblocktemplate: Test capability advertised") assert 'proposal' in tmpl['capabilities'] @@ -252,7 +271,7 @@ class MiningTest(BitcoinTestFramework): assert chain_tip(block.hash, status='active', branchlen=0) in node.getchaintips() # Building a few blocks should give the same results - node.generatetoaddress(10, node.get_deterministic_priv_key().address) + self.generatetoaddress(node, 10, node.get_deterministic_priv_key().address) assert_raises_rpc_error(-25, 'time-too-old', lambda: node.submitheader(hexdata=CBlockHeader(bad_block_time).serialize().hex())) assert_raises_rpc_error(-25, 'bad-prevblk', lambda: node.submitheader(hexdata=CBlockHeader(bad_block2).serialize().hex())) node.submitheader(hexdata=CBlockHeader(block).serialize().hex()) diff --git a/test/functional/mining_getblocktemplate_longpoll.py b/test/functional/mining_getblocktemplate_longpoll.py index cc32f78e2e..e928ee4936 100755 --- a/test/functional/mining_getblocktemplate_longpoll.py +++ b/test/functional/mining_getblocktemplate_longpoll.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test longpolling with getblocktemplate.""" @@ -8,6 +8,7 @@ from decimal import Decimal import random import threading +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import get_rpc_proxy from test_framework.wallet import MiniWallet @@ -34,7 +35,7 @@ class GetBlockTemplateLPTest(BitcoinTestFramework): def run_test(self): self.log.info("Warning: this test will take about 70 seconds in the best case. Be patient.") self.log.info("Test that longpollid doesn't change between successive getblocktemplate() invocations if nothing else happens") - self.nodes[0].generate(10) + self.generate(self.nodes[0], 10) template = self.nodes[0].getblocktemplate({'rules': ['segwit']}) longpollid = template['longpollid'] template2 = self.nodes[0].getblocktemplate({'rules': ['segwit']}) @@ -47,9 +48,9 @@ class GetBlockTemplateLPTest(BitcoinTestFramework): thr.join(5) # wait 5 seconds or until thread exits assert thr.is_alive() - miniwallets = [ MiniWallet(node) for node in self.nodes ] + miniwallets = [MiniWallet(node) for node in self.nodes] self.log.info("Test that longpoll will terminate if another node generates a block") - miniwallets[1].generate(1) # generate a block on another node + self.generate(miniwallets[1], 1) # generate a block on another node # check that thread will exit now that new transaction entered mempool thr.join(5) # wait 5 seconds or until thread exits assert not thr.is_alive() @@ -57,13 +58,12 @@ class GetBlockTemplateLPTest(BitcoinTestFramework): self.log.info("Test that longpoll will terminate if we generate a block ourselves") thr = LongpollThread(self.nodes[0]) thr.start() - miniwallets[0].generate(1) # generate a block on own node + self.generate(miniwallets[0], 1) # generate a block on own node thr.join(5) # wait 5 seconds or until thread exits assert not thr.is_alive() # Add enough mature utxos to the wallets, so that all txs spend confirmed coins - self.nodes[0].generate(100) - self.sync_blocks() + self.generate(self.nodes[0], COINBASE_MATURITY) self.log.info("Test that introducing a new transaction into the mempool will terminate the longpoll") thr = LongpollThread(self.nodes[0]) diff --git a/test/functional/mining_prioritisetransaction.py b/test/functional/mining_prioritisetransaction.py index 1426fdaacb..6f2ac805a0 100755 --- a/test/functional/mining_prioritisetransaction.py +++ b/test/functional/mining_prioritisetransaction.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2019 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the prioritisetransaction mining RPC.""" import time -from test_framework.messages import COIN, MAX_BLOCK_BASE_SIZE +from test_framework.messages import COIN, MAX_BLOCK_WEIGHT from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error, create_confirmed_utxos, create_lots_of_big_transactions, gen_return_txouts class PrioritiseTransactionTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True - self.num_nodes = 2 + self.num_nodes = 1 self.extra_args = [[ "-printpriority=1", "-acceptnonstdtxn=1", @@ -48,7 +48,7 @@ class PrioritiseTransactionTest(BitcoinTestFramework): self.relayfee = self.nodes[0].getnetworkinfo()['relayfee'] utxo_count = 90 - utxos = create_confirmed_utxos(self.relayfee, self.nodes[0], utxo_count) + utxos = create_confirmed_utxos(self, self.relayfee, self.nodes[0], utxo_count) base_fee = self.relayfee*100 # our transactions are smaller than 100kb txids = [] @@ -61,21 +61,21 @@ class PrioritiseTransactionTest(BitcoinTestFramework): txids[i] = create_lots_of_big_transactions(self.nodes[0], self.txouts, utxos[start_range:end_range], end_range - start_range, (i+1)*base_fee) # Make sure that the size of each group of transactions exceeds - # MAX_BLOCK_BASE_SIZE -- otherwise the test needs to be revised to create - # more transactions. + # MAX_BLOCK_WEIGHT // 4 -- otherwise the test needs to be revised to + # create more transactions. mempool = self.nodes[0].getrawmempool(True) sizes = [0, 0, 0] for i in range(3): for j in txids[i]: assert j in mempool sizes[i] += mempool[j]['vsize'] - assert sizes[i] > MAX_BLOCK_BASE_SIZE # Fail => raise utxo_count + assert sizes[i] > MAX_BLOCK_WEIGHT // 4 # Fail => raise utxo_count # add a fee delta to something in the cheapest bucket and make sure it gets mined # also check that a different entry in the cheapest bucket is NOT mined self.nodes[0].prioritisetransaction(txid=txids[0][0], fee_delta=int(3*base_fee*COIN)) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) mempool = self.nodes[0].getrawmempool() self.log.info("Assert that prioritised transaction was mined") @@ -105,7 +105,7 @@ class PrioritiseTransactionTest(BitcoinTestFramework): # the other high fee transactions. Keep mining until our mempool has # decreased by all the high fee size that we calculated above. while (self.nodes[0].getmempoolinfo()['bytes'] > sizes[0] + sizes[1]): - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) # High fee transaction should not have been mined, but other high fee rate # transactions should have been. diff --git a/test/functional/mocks/invalid_signer.py b/test/functional/mocks/invalid_signer.py new file mode 100755 index 0000000000..e30cc9e20b --- /dev/null +++ b/test/functional/mocks/invalid_signer.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import os +import sys +import argparse +import json + +def perform_pre_checks(): + mock_result_path = os.path.join(os.getcwd(), "mock_result") + if(os.path.isfile(mock_result_path)): + with open(mock_result_path, "r", encoding="utf8") as f: + mock_result = f.read() + if mock_result[0]: + sys.stdout.write(mock_result[2:]) + sys.exit(int(mock_result[0])) + +def enumerate(args): + sys.stdout.write(json.dumps([{"fingerprint": "b3c19bfc", "type": "trezor", "model": "trezor_t"}, {"fingerprint": "00000002"}])) + +def getdescriptors(args): + xpub_pkh = "xpub6CRhJvXV8x2AKWvqi1ZSMFU6cbxzQiYrv3dxSUXCawjMJ1JzpqVsveH4way1yCmJm29KzH1zrVZmVwes4Qo6oXVE1HFn4fdiKrYJngqFFc6" + xpub_sh = "xpub6CoNoq3Tg4tGSpom2BSwL42gy864KHo3TXkHxLxBbhvCkgmdVXADQmiHbLZhX3Me1cYhRx7s25Lpm4LnT5zu395ANHsXB2QvT9tqJDAibTN" + xpub_wpkh = "xpub6DUcLgY1DfgDy2RV6q4djwwsLitaoZDumbribqrR8mP78fEtgZa1XEsqT5MWQ7gwLwKsTQPT28XLoVE5A97rDNTwMXjmzPaNijoCApCbWvp" + + sys.stdout.write(json.dumps({ + "receive": [ + "pkh([b3c19bfc/44'/1'/" + args.account + "']" + xpub_pkh + "/0/*)#h26nxtl9", + "sh(wpkh([b3c19bfc/49'/1'/" + args.account + "']" + xpub_sh + "/0/*))#32ry02yp", + "wpkh([b3c19bfc/84'/1'/" + args.account + "']" + xpub_wpkh + "/0/*)#jftn8ppv" + ], + "internal": [ + "pkh([b3c19bfc/44'/1'/" + args.account + "']" + xpub_pkh + "/1/*)#x7ljm70a", + "sh(wpkh([b3c19bfc/49'/1'/" + args.account + "']" + xpub_sh + "/1/*))#ytdjh437", + "wpkh([b3c19bfc/84'/1'/" + args.account + "']" + xpub_wpkh + "/1/*)#rawj6535" + ] + })) + +parser = argparse.ArgumentParser(prog='./invalid_signer.py', description='External invalid signer mock') +parser.add_argument('--fingerprint') +parser.add_argument('--chain', default='main') +parser.add_argument('--stdin', action='store_true') + +subparsers = parser.add_subparsers(description='Commands', dest='command') +subparsers.required = True + +parser_enumerate = subparsers.add_parser('enumerate', help='list available signers') +parser_enumerate.set_defaults(func=enumerate) + +parser_getdescriptors = subparsers.add_parser('getdescriptors') +parser_getdescriptors.set_defaults(func=getdescriptors) +parser_getdescriptors.add_argument('--account', metavar='account') + +if not sys.stdin.isatty(): + buffer = sys.stdin.read() + if buffer and buffer.rstrip() != "": + sys.argv.extend(buffer.rstrip().split(" ")) + +args = parser.parse_args() + +perform_pre_checks() + +args.func(args) diff --git a/test/functional/mocks/signer.py b/test/functional/mocks/signer.py index 676d0a0a4d..b732b26a53 100755 --- a/test/functional/mocks/signer.py +++ b/test/functional/mocks/signer.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -18,7 +18,7 @@ def perform_pre_checks(): sys.exit(int(mock_result[0])) def enumerate(args): - sys.stdout.write(json.dumps([{"fingerprint": "00000001", "type": "trezor", "model": "trezor_t"}, {"fingerprint": "00000002"}])) + sys.stdout.write(json.dumps([{"fingerprint": "00000001", "type": "trezor", "model": "trezor_t"}, {"fingerprint": "00000002"}])) def getdescriptors(args): xpub = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B" @@ -93,7 +93,7 @@ parser_signtx.set_defaults(func=signtx) if not sys.stdin.isatty(): buffer = sys.stdin.read() if buffer and buffer.rstrip() != "": - sys.argv.extend(buffer.rstrip().split(" ")) + sys.argv.extend(buffer.rstrip().split(" ")) args = parser.parse_args() diff --git a/test/functional/p2p_add_connections.py b/test/functional/p2p_add_connections.py index a04ba5db2d..f4462673f2 100755 --- a/test/functional/p2p_add_connections.py +++ b/test/functional/p2p_add_connections.py @@ -1,19 +1,23 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test add_outbound_p2p_connection test framework functionality""" from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal - - -def check_node_connections(*, node, num_in, num_out): - info = node.getnetworkinfo() - assert_equal(info["connections_in"], num_in) - assert_equal(info["connections_out"], num_out) - +from test_framework.util import ( + assert_equal, + check_node_connections, +) + +class P2PFeelerReceiver(P2PInterface): + def on_version(self, message): + # The bitcoind node closes feeler connections as soon as a version + # message is received from the test framework. Don't send any responses + # to the node's version message since the connection will already be + # closed. + pass class P2PAddConnections(BitcoinTestFramework): def set_test_params(self): @@ -91,6 +95,16 @@ class P2PAddConnections(BitcoinTestFramework): check_node_connections(node=self.nodes[1], num_in=5, num_out=10) + self.log.info("Add 1 feeler connection to node 0") + feeler_conn = self.nodes[0].add_outbound_p2p_connection(P2PFeelerReceiver(), p2p_idx=6, connection_type="feeler") + + # Feeler connection is closed + assert not feeler_conn.is_connected + + # Verify version message received + assert_equal(feeler_conn.message_count["version"], 1) + # Feeler connections do not request tx relay + assert_equal(feeler_conn.last_message["version"].relay, 0) if __name__ == '__main__': P2PAddConnections().main() diff --git a/test/functional/p2p_addr_relay.py b/test/functional/p2p_addr_relay.py index 69821763bd..3218a9b14a 100755 --- a/test/functional/p2p_addr_relay.py +++ b/test/functional/p2p_addr_relay.py @@ -1,79 +1,155 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Test addr relay """ +import random +import time + from test_framework.messages import ( CAddress, - NODE_NETWORK, - NODE_WITNESS, msg_addr, + msg_getaddr, + msg_verack, ) -from test_framework.p2p import P2PInterface -from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import ( - assert_equal, +from test_framework.p2p import ( + P2PInterface, + p2p_lock, + P2P_SERVICES, ) -import time - -# Keep this with length <= 10. Addresses from larger messages are not relayed. -ADDRS = [] -num_ipv4_addrs = 10 - -for i in range(num_ipv4_addrs): - addr = CAddress() - addr.time = int(time.time()) + i - addr.nServices = NODE_NETWORK | NODE_WITNESS - addr.ip = "123.123.123.{}".format(i % 256) - addr.port = 8333 + i - ADDRS.append(addr) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_greater_than class AddrReceiver(P2PInterface): num_ipv4_received = 0 + test_addr_contents = False + _tokens = 1 + send_getaddr = True + + def __init__(self, test_addr_contents=False, send_getaddr=True): + super().__init__() + self.test_addr_contents = test_addr_contents + self.send_getaddr = send_getaddr def on_addr(self, message): for addr in message.addrs: - assert_equal(addr.nServices, 9) - if not 8333 <= addr.port < 8343: - raise AssertionError("Invalid addr.port of {} (8333-8342 expected)".format(addr.port)) - assert addr.ip.startswith('123.123.123.') self.num_ipv4_received += 1 + if(self.test_addr_contents): + # relay_tests checks the content of the addr messages match + # expectations based on the message creation in setup_addr_msg + assert_equal(addr.nServices, 9) + if not 8333 <= addr.port < 8343: + raise AssertionError("Invalid addr.port of {} (8333-8342 expected)".format(addr.port)) + assert addr.ip.startswith('123.123.') + + def on_getaddr(self, message): + # When the node sends us a getaddr, it increments the addr relay tokens for the connection by 1000 + self._tokens += 1000 + + @property + def tokens(self): + with p2p_lock: + return self._tokens + + def increment_tokens(self, n): + # When we move mocktime forward, the node increments the addr relay tokens for its peers + with p2p_lock: + self._tokens += n + + def addr_received(self): + return self.num_ipv4_received != 0 + + def on_version(self, message): + self.send_message(msg_verack()) + if (self.send_getaddr): + self.send_message(msg_getaddr()) + + def getaddr_received(self): + return self.message_count['getaddr'] > 0 class AddrTest(BitcoinTestFramework): + counter = 0 + mocktime = int(time.time()) + def set_test_params(self): self.num_nodes = 1 + self.extra_args = [["-whitelist=addr@127.0.0.1"]] def run_test(self): - self.log.info('Create connection that sends addr messages') - addr_source = self.nodes[0].add_p2p_connection(P2PInterface()) + self.oversized_addr_test() + self.relay_tests() + self.inbound_blackhole_tests() + + # This test populates the addrman, which can impact the node's behavior + # in subsequent tests + self.getaddr_tests() + self.blocksonly_mode_tests() + self.rate_limit_tests() + + def setup_addr_msg(self, num, sequential_ips=True): + addrs = [] + for i in range(num): + addr = CAddress() + addr.time = self.mocktime + random.randrange(-100, 100) + addr.nServices = P2P_SERVICES + if sequential_ips: + assert self.counter < 256 ** 2 # Don't allow the returned ip addresses to wrap. + addr.ip = f"123.123.{self.counter // 256}.{self.counter % 256}" + self.counter += 1 + else: + addr.ip = f"{random.randrange(128,169)}.{random.randrange(1,255)}.{random.randrange(1,255)}.{random.randrange(1,255)}" + addr.port = 8333 + i + addrs.append(addr) + msg = msg_addr() + msg.addrs = addrs + return msg - self.log.info('Send too-large addr message') - msg.addrs = ADDRS * 101 # more than 1000 addresses in one message + def send_addr_msg(self, source, msg, receivers): + source.send_and_ping(msg) + # invoke m_next_addr_send timer: + # `addr` messages are sent on an exponential distribution with mean interval of 30s. + # Setting the mocktime 600s forward gives a probability of (1 - e^-(600/30)) that + # the event will occur (i.e. this fails once in ~500 million repeats). + self.mocktime += 10 * 60 + self.nodes[0].setmocktime(self.mocktime) + for peer in receivers: + peer.sync_send_with_ping() + + def oversized_addr_test(self): + self.log.info('Send an addr message that is too large') + addr_source = self.nodes[0].add_p2p_connection(P2PInterface()) + + msg = self.setup_addr_msg(1010) with self.nodes[0].assert_debug_log(['addr message size = 1010']): addr_source.send_and_ping(msg) + self.nodes[0].disconnect_p2ps() + + def relay_tests(self): + self.log.info('Test address relay') self.log.info('Check that addr message content is relayed and added to addrman') + addr_source = self.nodes[0].add_p2p_connection(P2PInterface()) num_receivers = 7 receivers = [] for _ in range(num_receivers): - receivers.append(self.nodes[0].add_p2p_connection(AddrReceiver())) - msg.addrs = ADDRS + receivers.append(self.nodes[0].add_p2p_connection(AddrReceiver(test_addr_contents=True))) + + # Keep this with length <= 10. Addresses from larger messages are not + # relayed. + num_ipv4_addrs = 10 + msg = self.setup_addr_msg(num_ipv4_addrs) with self.nodes[0].assert_debug_log( [ - 'Added {} addresses from 127.0.0.1: 0 tried'.format(num_ipv4_addrs), - 'received: addr (301 bytes) peer=0', + 'received: addr (301 bytes) peer=1', ] ): - addr_source.send_and_ping(msg) - self.nodes[0].setmocktime(int(time.time()) + 30 * 60) - for receiver in receivers: - receiver.sync_with_ping() + self.send_addr_msg(addr_source, msg, receivers) total_ipv4_received = sum(r.num_ipv4_received for r in receivers) @@ -82,6 +158,210 @@ class AddrTest(BitcoinTestFramework): ipv4_branching_factor = 2 assert_equal(total_ipv4_received, num_ipv4_addrs * ipv4_branching_factor) + self.nodes[0].disconnect_p2ps() + + self.log.info('Check relay of addresses received from outbound peers') + inbound_peer = self.nodes[0].add_p2p_connection(AddrReceiver(test_addr_contents=True, send_getaddr=False)) + full_outbound_peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=0, connection_type="outbound-full-relay") + msg = self.setup_addr_msg(2) + self.send_addr_msg(full_outbound_peer, msg, [inbound_peer]) + self.log.info('Check that the first addr message received from an outbound peer is not relayed') + # Currently, there is a flag that prevents the first addr message received + # from a new outbound peer to be relayed to others. Originally meant to prevent + # large GETADDR responses from being relayed, it now typically affects the self-announcement + # of the outbound peer which is often sent before the GETADDR response. + assert_equal(inbound_peer.num_ipv4_received, 0) + + # Send an empty ADDR message to initialize address relay on this connection. + inbound_peer.send_and_ping(msg_addr()) + + self.log.info('Check that subsequent addr messages sent from an outbound peer are relayed') + msg2 = self.setup_addr_msg(2) + self.send_addr_msg(full_outbound_peer, msg2, [inbound_peer]) + assert_equal(inbound_peer.num_ipv4_received, 2) + + self.log.info('Check address relay to outbound peers') + block_relay_peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=1, connection_type="block-relay-only") + msg3 = self.setup_addr_msg(2) + self.send_addr_msg(inbound_peer, msg3, [full_outbound_peer, block_relay_peer]) + + self.log.info('Check that addresses are relayed to full outbound peers') + assert_equal(full_outbound_peer.num_ipv4_received, 2) + self.log.info('Check that addresses are not relayed to block-relay-only outbound peers') + assert_equal(block_relay_peer.num_ipv4_received, 0) + + self.nodes[0].disconnect_p2ps() + + def sum_addr_messages(self, msgs_dict): + return sum(bytes_received for (msg, bytes_received) in msgs_dict.items() if msg in ['addr', 'addrv2', 'getaddr']) + + def inbound_blackhole_tests(self): + self.log.info('Check that we only relay addresses to inbound peers who have previously sent us addr related messages') + + addr_source = self.nodes[0].add_p2p_connection(P2PInterface()) + receiver_peer = self.nodes[0].add_p2p_connection(AddrReceiver()) + blackhole_peer = self.nodes[0].add_p2p_connection(AddrReceiver(send_getaddr=False)) + initial_addrs_received = receiver_peer.num_ipv4_received + + peerinfo = self.nodes[0].getpeerinfo() + assert_equal(peerinfo[0]['addr_relay_enabled'], True) # addr_source + assert_equal(peerinfo[1]['addr_relay_enabled'], True) # receiver_peer + assert_equal(peerinfo[2]['addr_relay_enabled'], False) # blackhole_peer + + # addr_source sends 2 addresses to node0 + msg = self.setup_addr_msg(2) + addr_source.send_and_ping(msg) + self.mocktime += 30 * 60 + self.nodes[0].setmocktime(self.mocktime) + receiver_peer.sync_with_ping() + blackhole_peer.sync_with_ping() + + peerinfo = self.nodes[0].getpeerinfo() + + # Confirm node received addr-related messages from receiver peer + assert_greater_than(self.sum_addr_messages(peerinfo[1]['bytesrecv_per_msg']), 0) + # And that peer received addresses + assert_equal(receiver_peer.num_ipv4_received - initial_addrs_received, 2) + + # Confirm node has not received addr-related messages from blackhole peer + assert_equal(self.sum_addr_messages(peerinfo[2]['bytesrecv_per_msg']), 0) + # And that peer did not receive addresses + assert_equal(blackhole_peer.num_ipv4_received, 0) + + self.log.info("After blackhole peer sends addr message, it becomes eligible for addr gossip") + blackhole_peer.send_and_ping(msg_addr()) + + # Confirm node has now received addr-related messages from blackhole peer + assert_greater_than(self.sum_addr_messages(peerinfo[1]['bytesrecv_per_msg']), 0) + assert_equal(self.nodes[0].getpeerinfo()[2]['addr_relay_enabled'], True) + + msg = self.setup_addr_msg(2) + self.send_addr_msg(addr_source, msg, [receiver_peer, blackhole_peer]) + + # And that peer received addresses + assert_equal(blackhole_peer.num_ipv4_received, 2) + + self.nodes[0].disconnect_p2ps() + + def getaddr_tests(self): + # In the previous tests, the node answered GETADDR requests with an + # empty addrman. Due to GETADDR response caching (see + # CConnman::GetAddresses), the node would continue to provide 0 addrs + # in response until enough time has passed or the node is restarted. + self.restart_node(0) + + self.log.info('Test getaddr behavior') + self.log.info('Check that we send a getaddr message upon connecting to an outbound-full-relay peer') + full_outbound_peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=0, connection_type="outbound-full-relay") + full_outbound_peer.sync_with_ping() + assert full_outbound_peer.getaddr_received() + + self.log.info('Check that we do not send a getaddr message upon connecting to a block-relay-only peer') + block_relay_peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=1, connection_type="block-relay-only") + block_relay_peer.sync_with_ping() + assert_equal(block_relay_peer.getaddr_received(), False) + + self.log.info('Check that we answer getaddr messages only from inbound peers') + inbound_peer = self.nodes[0].add_p2p_connection(AddrReceiver(send_getaddr=False)) + inbound_peer.sync_with_ping() + + # Add some addresses to addrman + for i in range(1000): + first_octet = i >> 8 + second_octet = i % 256 + a = f"{first_octet}.{second_octet}.1.1" + self.nodes[0].addpeeraddress(a, 8333) + + full_outbound_peer.send_and_ping(msg_getaddr()) + block_relay_peer.send_and_ping(msg_getaddr()) + inbound_peer.send_and_ping(msg_getaddr()) + + # invoke m_next_addr_send timer, see under send_addr_msg() function for rationale + self.mocktime += 10 * 60 + self.nodes[0].setmocktime(self.mocktime) + inbound_peer.wait_until(lambda: inbound_peer.addr_received() is True) + + assert_equal(full_outbound_peer.num_ipv4_received, 0) + assert_equal(block_relay_peer.num_ipv4_received, 0) + assert inbound_peer.num_ipv4_received > 100 + + self.nodes[0].disconnect_p2ps() + + def blocksonly_mode_tests(self): + self.log.info('Test addr relay in -blocksonly mode') + self.restart_node(0, ["-blocksonly", "-whitelist=addr@127.0.0.1"]) + self.mocktime = int(time.time()) + + self.log.info('Check that we send getaddr messages') + full_outbound_peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=0, connection_type="outbound-full-relay") + full_outbound_peer.sync_with_ping() + assert full_outbound_peer.getaddr_received() + + self.log.info('Check that we relay address messages') + addr_source = self.nodes[0].add_p2p_connection(P2PInterface()) + msg = self.setup_addr_msg(2) + self.send_addr_msg(addr_source, msg, [full_outbound_peer]) + assert_equal(full_outbound_peer.num_ipv4_received, 2) + + self.nodes[0].disconnect_p2ps() + + def send_addrs_and_test_rate_limiting(self, peer, no_relay, *, new_addrs, total_addrs): + """Send an addr message and check that the number of addresses processed and rate-limited is as expected""" + + peer.send_and_ping(self.setup_addr_msg(new_addrs, sequential_ips=False)) + + peerinfo = self.nodes[0].getpeerinfo()[0] + addrs_processed = peerinfo['addr_processed'] + addrs_rate_limited = peerinfo['addr_rate_limited'] + self.log.debug(f"addrs_processed = {addrs_processed}, addrs_rate_limited = {addrs_rate_limited}") + + if no_relay: + assert_equal(addrs_processed, 0) + assert_equal(addrs_rate_limited, 0) + else: + assert_equal(addrs_processed, min(total_addrs, peer.tokens)) + assert_equal(addrs_rate_limited, max(0, total_addrs - peer.tokens)) + + def rate_limit_tests(self): + self.mocktime = int(time.time()) + self.restart_node(0, []) + self.nodes[0].setmocktime(self.mocktime) + + for conn_type, no_relay in [("outbound-full-relay", False), ("block-relay-only", True), ("inbound", False)]: + self.log.info(f'Test rate limiting of addr processing for {conn_type} peers') + if conn_type == "inbound": + peer = self.nodes[0].add_p2p_connection(AddrReceiver()) + else: + peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=0, connection_type=conn_type) + + # Send 600 addresses. For all but the block-relay-only peer this should result in addresses being processed. + self.send_addrs_and_test_rate_limiting(peer, no_relay, new_addrs=600, total_addrs=600) + + # Send 600 more addresses. For the outbound-full-relay peer (which we send a GETADDR, and thus will + # process up to 1001 incoming addresses), this means more addresses will be processed. + self.send_addrs_and_test_rate_limiting(peer, no_relay, new_addrs=600, total_addrs=1200) + + # Send 10 more. As we reached the processing limit for all nodes, no more addresses should be procesesd. + self.send_addrs_and_test_rate_limiting(peer, no_relay, new_addrs=10, total_addrs=1210) + + # Advance the time by 100 seconds, permitting the processing of 10 more addresses. + # Send 200 and verify that 10 are processed. + self.mocktime += 100 + self.nodes[0].setmocktime(self.mocktime) + peer.increment_tokens(10) + + self.send_addrs_and_test_rate_limiting(peer, no_relay, new_addrs=200, total_addrs=1410) + + # Advance the time by 1000 seconds, permitting the processing of 100 more addresses. + # Send 200 and verify that 100 are processed. + self.mocktime += 1000 + self.nodes[0].setmocktime(self.mocktime) + peer.increment_tokens(100) + + self.send_addrs_and_test_rate_limiting(peer, no_relay, new_addrs=200, total_addrs=1610) + + self.nodes[0].disconnect_p2ps() + if __name__ == '__main__': AddrTest().main() diff --git a/test/functional/p2p_addrfetch.py b/test/functional/p2p_addrfetch.py new file mode 100755 index 0000000000..25efd50040 --- /dev/null +++ b/test/functional/p2p_addrfetch.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Test p2p addr-fetch connections +""" + +import time + +from test_framework.messages import ( + CAddress, + msg_addr, +) +from test_framework.p2p import ( + P2PInterface, + p2p_lock, + P2P_SERVICES, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + +ADDR = CAddress() +ADDR.time = int(time.time()) +ADDR.nServices = P2P_SERVICES +ADDR.ip = "192.0.0.8" +ADDR.port = 18444 + + +class P2PAddrFetch(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + + def assert_getpeerinfo(self, *, peer_ids): + num_peers = len(peer_ids) + info = self.nodes[0].getpeerinfo() + assert_equal(len(info), num_peers) + for n in range(0, num_peers): + assert_equal(info[n]['id'], peer_ids[n]) + assert_equal(info[n]['connection_type'], 'addr-fetch') + + def run_test(self): + node = self.nodes[0] + self.log.info("Connect to an addr-fetch peer") + peer_id = 0 + peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=peer_id, connection_type="addr-fetch") + self.assert_getpeerinfo(peer_ids=[peer_id]) + + self.log.info("Check that we send getaddr but don't try to sync headers with the addr-fetch peer") + peer.sync_send_with_ping() + with p2p_lock: + assert peer.message_count['getaddr'] == 1 + assert peer.message_count['getheaders'] == 0 + + self.log.info("Check that answering the getaddr with a single address does not lead to disconnect") + # This prevents disconnecting on self-announcements + msg = msg_addr() + msg.addrs = [ADDR] + peer.send_and_ping(msg) + self.assert_getpeerinfo(peer_ids=[peer_id]) + + self.log.info("Check that answering with larger addr messages leads to disconnect") + msg.addrs = [ADDR] * 2 + peer.send_message(msg) + peer.wait_for_disconnect(timeout=5) + + self.log.info("Check timeout for addr-fetch peer that does not send addrs") + peer_id = 1 + peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=peer_id, connection_type="addr-fetch") + + time_now = int(time.time()) + self.assert_getpeerinfo(peer_ids=[peer_id]) + + # Expect addr-fetch peer connection to be maintained up to 5 minutes. + node.setmocktime(time_now + 295) + self.assert_getpeerinfo(peer_ids=[peer_id]) + + # Expect addr-fetch peer connection to be disconnected after 5 minutes. + node.setmocktime(time_now + 301) + peer.wait_for_disconnect(timeout=5) + self.assert_getpeerinfo(peer_ids=[]) + + +if __name__ == '__main__': + P2PAddrFetch().main() diff --git a/test/functional/p2p_addrv2_relay.py b/test/functional/p2p_addrv2_relay.py index 23ce3e5d04..9ab190871f 100755 --- a/test/functional/p2p_addrv2_relay.py +++ b/test/functional/p2p_addrv2_relay.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ @@ -11,19 +11,27 @@ import time from test_framework.messages import ( CAddress, msg_addrv2, - NODE_NETWORK, - NODE_WITNESS, ) -from test_framework.p2p import P2PInterface +from test_framework.p2p import ( + P2PInterface, + P2P_SERVICES, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal +I2P_ADDR = "c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p" + ADDRS = [] for i in range(10): addr = CAddress() addr.time = int(time.time()) + i - addr.nServices = NODE_NETWORK | NODE_WITNESS - addr.ip = "123.123.123.{}".format(i % 256) + addr.nServices = P2P_SERVICES + # Add one I2P address at an arbitrary position. + if i == 5: + addr.net = addr.NET_I2P + addr.ip = I2P_ADDR + else: + addr.ip = f"123.123.123.{i % 256}" addr.port = 8333 + i ADDRS.append(addr) @@ -35,11 +43,10 @@ class AddrReceiver(P2PInterface): super().__init__(support_addrv2 = True) def on_addrv2(self, message): - for addr in message.addrs: - assert_equal(addr.nServices, 9) - assert addr.ip.startswith('123.123.123.') - assert (8333 <= addr.port < 8343) - self.addrv2_received_and_checked = True + expected_set = set((addr.ip, addr.port) for addr in ADDRS) + received_set = set((addr.ip, addr.port) for addr in message.addrs) + if expected_set == received_set: + self.addrv2_received_and_checked = True def wait_for_addrv2(self): self.wait_until(lambda: "addrv2" in self.last_message) @@ -49,6 +56,7 @@ class AddrTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 + self.extra_args = [["-whitelist=addr@127.0.0.1"]] def run_test(self): self.log.info('Create connection that sends addrv2 messages') @@ -64,15 +72,15 @@ class AddrTest(BitcoinTestFramework): addr_receiver = self.nodes[0].add_p2p_connection(AddrReceiver()) msg.addrs = ADDRS with self.nodes[0].assert_debug_log([ - 'Added 10 addresses from 127.0.0.1: 0 tried', - 'received: addrv2 (131 bytes) peer=0', - 'sending addrv2 (131 bytes) peer=1', + 'received: addrv2 (159 bytes) peer=0', + 'sending addrv2 (159 bytes) peer=1', ]): addr_source.send_and_ping(msg) self.nodes[0].setmocktime(int(time.time()) + 30 * 60) addr_receiver.wait_for_addrv2() assert addr_receiver.addrv2_received_and_checked + assert_equal(len(self.nodes[0].getnodeaddresses(count=0, network="i2p")), 0) if __name__ == '__main__': diff --git a/test/functional/p2p_blockfilters.py b/test/functional/p2p_blockfilters.py index 03662babef..e73fad439f 100755 --- a/test/functional/p2p_blockfilters.py +++ b/test/functional/p2p_blockfilters.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Tests NODE_COMPACT_FILTERS (BIP 157/158). @@ -24,7 +24,7 @@ from test_framework.util import ( assert_equal, ) -class CFiltersClient(P2PInterface): +class FiltersClient(P2PInterface): def __init__(self): super().__init__() # Store the cfilters received. @@ -39,6 +39,7 @@ class CFiltersClient(P2PInterface): """Store cfilters received in a list.""" self.cfilters.append(message) + class CompactFiltersTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True @@ -51,26 +52,25 @@ class CompactFiltersTest(BitcoinTestFramework): def run_test(self): # Node 0 supports COMPACT_FILTERS, node 1 does not. - node0 = self.nodes[0].add_p2p_connection(CFiltersClient()) - node1 = self.nodes[1].add_p2p_connection(CFiltersClient()) + peer_0 = self.nodes[0].add_p2p_connection(FiltersClient()) + peer_1 = self.nodes[1].add_p2p_connection(FiltersClient()) # Nodes 0 & 1 share the same first 999 blocks in the chain. - self.nodes[0].generate(999) - self.sync_blocks(timeout=600) + self.generate(self.nodes[0], 999) # Stale blocks by disconnecting nodes 0 & 1, mining, then reconnecting self.disconnect_nodes(0, 1) - self.nodes[0].generate(1) - self.wait_until(lambda: self.nodes[0].getblockcount() == 1000) - stale_block_hash = self.nodes[0].getblockhash(1000) + stale_block_hash = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0] + self.nodes[0].syncwithvalidationinterfacequeue() + assert_equal(self.nodes[0].getblockcount(), 1000) - self.nodes[1].generate(1001) - self.wait_until(lambda: self.nodes[1].getblockcount() == 2000) + self.generate(self.nodes[1], 1001, sync_fun=self.no_op) + assert_equal(self.nodes[1].getblockcount(), 2000) # Check that nodes have signalled NODE_COMPACT_FILTERS correctly. - assert node0.nServices & NODE_COMPACT_FILTERS != 0 - assert node1.nServices & NODE_COMPACT_FILTERS == 0 + assert peer_0.nServices & NODE_COMPACT_FILTERS != 0 + assert peer_1.nServices & NODE_COMPACT_FILTERS == 0 # Check that the localservices is as expected. assert int(self.nodes[0].getnetworkinfo()['localservices'], 16) & NODE_COMPACT_FILTERS != 0 @@ -79,10 +79,10 @@ class CompactFiltersTest(BitcoinTestFramework): self.log.info("get cfcheckpt on chain to be re-orged out.") request = msg_getcfcheckpt( filter_type=FILTER_TYPE_BASIC, - stop_hash=int(stale_block_hash, 16) + stop_hash=int(stale_block_hash, 16), ) - node0.send_and_ping(message=request) - response = node0.last_message['cfcheckpt'] + peer_0.send_and_ping(message=request) + response = peer_0.last_message['cfcheckpt'] assert_equal(response.filter_type, request.filter_type) assert_equal(response.stop_hash, request.stop_hash) assert_equal(len(response.headers), 1) @@ -90,6 +90,7 @@ class CompactFiltersTest(BitcoinTestFramework): self.log.info("Reorg node 0 to a new chain.") self.connect_nodes(0, 1) self.sync_blocks(timeout=600) + self.nodes[0].syncwithvalidationinterfacequeue() main_block_hash = self.nodes[0].getblockhash(1000) assert main_block_hash != stale_block_hash, "node 0 chain did not reorganize" @@ -98,10 +99,10 @@ class CompactFiltersTest(BitcoinTestFramework): tip_hash = self.nodes[0].getbestblockhash() request = msg_getcfcheckpt( filter_type=FILTER_TYPE_BASIC, - stop_hash=int(tip_hash, 16) + stop_hash=int(tip_hash, 16), ) - node0.send_and_ping(request) - response = node0.last_message['cfcheckpt'] + peer_0.send_and_ping(request) + response = peer_0.last_message['cfcheckpt'] assert_equal(response.filter_type, request.filter_type) assert_equal(response.stop_hash, request.stop_hash) @@ -109,51 +110,51 @@ class CompactFiltersTest(BitcoinTestFramework): tip_cfcheckpt = self.nodes[0].getblockfilter(tip_hash, 'basic')['header'] assert_equal( response.headers, - [int(header, 16) for header in (main_cfcheckpt, tip_cfcheckpt)] + [int(header, 16) for header in (main_cfcheckpt, tip_cfcheckpt)], ) self.log.info("Check that peers can fetch cfcheckpt on stale chain.") request = msg_getcfcheckpt( filter_type=FILTER_TYPE_BASIC, - stop_hash=int(stale_block_hash, 16) + stop_hash=int(stale_block_hash, 16), ) - node0.send_and_ping(request) - response = node0.last_message['cfcheckpt'] + peer_0.send_and_ping(request) + response = peer_0.last_message['cfcheckpt'] stale_cfcheckpt = self.nodes[0].getblockfilter(stale_block_hash, 'basic')['header'] assert_equal( response.headers, - [int(header, 16) for header in (stale_cfcheckpt,)] + [int(header, 16) for header in (stale_cfcheckpt, )], ) self.log.info("Check that peers can fetch cfheaders on active chain.") request = msg_getcfheaders( filter_type=FILTER_TYPE_BASIC, start_height=1, - stop_hash=int(main_block_hash, 16) + stop_hash=int(main_block_hash, 16), ) - node0.send_and_ping(request) - response = node0.last_message['cfheaders'] + peer_0.send_and_ping(request) + response = peer_0.last_message['cfheaders'] main_cfhashes = response.hashes assert_equal(len(main_cfhashes), 1000) assert_equal( compute_last_header(response.prev_header, response.hashes), - int(main_cfcheckpt, 16) + int(main_cfcheckpt, 16), ) self.log.info("Check that peers can fetch cfheaders on stale chain.") request = msg_getcfheaders( filter_type=FILTER_TYPE_BASIC, start_height=1, - stop_hash=int(stale_block_hash, 16) + stop_hash=int(stale_block_hash, 16), ) - node0.send_and_ping(request) - response = node0.last_message['cfheaders'] + peer_0.send_and_ping(request) + response = peer_0.last_message['cfheaders'] stale_cfhashes = response.hashes assert_equal(len(stale_cfhashes), 1000) assert_equal( compute_last_header(response.prev_header, response.hashes), - int(stale_cfcheckpt, 16) + int(stale_cfcheckpt, 16), ) self.log.info("Check that peers can fetch cfilters.") @@ -161,11 +162,10 @@ class CompactFiltersTest(BitcoinTestFramework): request = msg_getcfilters( filter_type=FILTER_TYPE_BASIC, start_height=1, - stop_hash=int(stop_hash, 16) + stop_hash=int(stop_hash, 16), ) - node0.send_message(request) - node0.sync_with_ping() - response = node0.pop_cfilters() + peer_0.send_and_ping(request) + response = peer_0.pop_cfilters() assert_equal(len(response), 10) self.log.info("Check that cfilter responses are correct.") @@ -180,11 +180,10 @@ class CompactFiltersTest(BitcoinTestFramework): request = msg_getcfilters( filter_type=FILTER_TYPE_BASIC, start_height=1000, - stop_hash=int(stale_block_hash, 16) + stop_hash=int(stale_block_hash, 16), ) - node0.send_message(request) - node0.sync_with_ping() - response = node0.pop_cfilters() + peer_0.send_and_ping(request) + response = peer_0.pop_cfilters() assert_equal(len(response), 1) cfilter = response[0] @@ -197,23 +196,23 @@ class CompactFiltersTest(BitcoinTestFramework): requests = [ msg_getcfcheckpt( filter_type=FILTER_TYPE_BASIC, - stop_hash=int(main_block_hash, 16) + stop_hash=int(main_block_hash, 16), ), msg_getcfheaders( filter_type=FILTER_TYPE_BASIC, start_height=1000, - stop_hash=int(main_block_hash, 16) + stop_hash=int(main_block_hash, 16), ), msg_getcfilters( filter_type=FILTER_TYPE_BASIC, start_height=1000, - stop_hash=int(main_block_hash, 16) + stop_hash=int(main_block_hash, 16), ), ] for request in requests: - node1 = self.nodes[1].add_p2p_connection(P2PInterface()) - node1.send_message(request) - node1.wait_for_disconnect() + peer_1 = self.nodes[1].add_p2p_connection(P2PInterface()) + peer_1.send_message(request) + peer_1.wait_for_disconnect() self.log.info("Check that invalid requests result in disconnection.") requests = [ @@ -221,18 +220,18 @@ class CompactFiltersTest(BitcoinTestFramework): msg_getcfilters( filter_type=FILTER_TYPE_BASIC, start_height=0, - stop_hash=int(main_block_hash, 16) + stop_hash=int(main_block_hash, 16), ), # Requesting too many filter headers results in disconnection. msg_getcfheaders( filter_type=FILTER_TYPE_BASIC, start_height=0, - stop_hash=int(tip_hash, 16) + stop_hash=int(tip_hash, 16), ), # Requesting unknown filter type results in disconnection. msg_getcfcheckpt( filter_type=255, - stop_hash=int(main_block_hash, 16) + stop_hash=int(main_block_hash, 16), ), # Requesting unknown hash results in disconnection. msg_getcfcheckpt( @@ -241,9 +240,10 @@ class CompactFiltersTest(BitcoinTestFramework): ), ] for request in requests: - node0 = self.nodes[0].add_p2p_connection(P2PInterface()) - node0.send_message(request) - node0.wait_for_disconnect() + peer_0 = self.nodes[0].add_p2p_connection(P2PInterface()) + peer_0.send_message(request) + peer_0.wait_for_disconnect() + def compute_last_header(prev_header, hashes): """Compute the last filter header from a starting header and a sequence of filter hashes.""" @@ -252,5 +252,6 @@ def compute_last_header(prev_header, hashes): header = hash256(ser_uint256(filter_hash) + header) return uint256_from_str(header) + if __name__ == '__main__': CompactFiltersTest().main() diff --git a/test/functional/p2p_blocksonly.py b/test/functional/p2p_blocksonly.py index 6584efae79..6f142f23f2 100755 --- a/test/functional/p2p_blocksonly.py +++ b/test/functional/p2p_blocksonly.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test p2p blocksonly mode & block-relay-only connections.""" import time -from test_framework.blocktools import create_transaction -from test_framework.messages import msg_tx +from test_framework.messages import msg_tx, msg_inv, CInv, MSG_WTX from test_framework.p2p import P2PInterface, P2PTxInvStore from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet class P2PBlocksOnly(BitcoinTestFramework): @@ -18,10 +18,11 @@ class P2PBlocksOnly(BitcoinTestFramework): self.num_nodes = 1 self.extra_args = [["-blocksonly"]] - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def run_test(self): + self.miniwallet = MiniWallet(self.nodes[0]) + # Add enough mature utxos to the wallet, so that all txs spend confirmed coins + self.miniwallet.rescan_utxos() + self.blocksonly_mode_tests() self.blocks_relay_conn_tests() @@ -30,14 +31,21 @@ class P2PBlocksOnly(BitcoinTestFramework): assert_equal(self.nodes[0].getnetworkinfo()['localrelay'], False) self.nodes[0].add_p2p_connection(P2PInterface()) - tx, txid, tx_hex = self.check_p2p_tx_violation() + tx, txid, wtxid, tx_hex = self.check_p2p_tx_violation() + + self.log.info('Check that tx invs also violate the protocol') + self.nodes[0].add_p2p_connection(P2PInterface()) + with self.nodes[0].assert_debug_log(['transaction (0000000000000000000000000000000000000000000000000000000000001234) inv sent in violation of protocol, disconnecting peer']): + self.nodes[0].p2ps[0].send_message(msg_inv([CInv(t=MSG_WTX, h=0x1234)])) + self.nodes[0].p2ps[0].wait_for_disconnect() + del self.nodes[0].p2ps[0] self.log.info('Check that txs from rpc are not rejected and relayed to other peers') tx_relay_peer = self.nodes[0].add_p2p_connection(P2PInterface()) assert_equal(self.nodes[0].getpeerinfo()[0]['relaytxes'], True) assert_equal(self.nodes[0].testmempoolaccept([tx_hex])[0]['allowed'], True) - with self.nodes[0].assert_debug_log(['received getdata for: wtx {} peer=1'.format(txid)]): + with self.nodes[0].assert_debug_log(['received getdata for: wtx {} peer'.format(wtxid)]): self.nodes[0].sendrawtransaction(tx_hex) tx_relay_peer.wait_for_tx(txid) assert_equal(self.nodes[0].getmempoolinfo()['size'], 1) @@ -69,7 +77,7 @@ class P2PBlocksOnly(BitcoinTestFramework): self.log.info("Relay-permission peer's transaction is accepted and relayed") self.nodes[0].disconnect_p2ps() - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) def blocks_relay_conn_tests(self): self.log.info('Tests with node in normal mode with block-relay-only connections') @@ -79,7 +87,7 @@ class P2PBlocksOnly(BitcoinTestFramework): # Ensure we disconnect if a block-relay-only connection sends us a transaction self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=0, connection_type="block-relay-only") assert_equal(self.nodes[0].getpeerinfo()[0]['relaytxes'], False) - _, txid, tx_hex = self.check_p2p_tx_violation(index=2) + _, txid, _, tx_hex = self.check_p2p_tx_violation() self.log.info("Check that txs from RPC are not sent to blockrelay connection") conn = self.nodes[0].add_outbound_p2p_connection(P2PTxInvStore(), p2p_idx=1, connection_type="block-relay-only") @@ -89,29 +97,22 @@ class P2PBlocksOnly(BitcoinTestFramework): # Bump time forward to ensure nNextInvSend timer pops self.nodes[0].setmocktime(int(time.time()) + 60) - # Calling sync_with_ping twice requires that the node calls - # `ProcessMessage` twice, and thus ensures `SendMessages` must have - # been called at least once - conn.sync_with_ping() - conn.sync_with_ping() + conn.sync_send_with_ping() assert(int(txid, 16) not in conn.get_invs()) - def check_p2p_tx_violation(self, index=1): + def check_p2p_tx_violation(self): self.log.info('Check that txs from P2P are rejected and result in disconnect') - input_txid = self.nodes[0].getblock(self.nodes[0].getblockhash(index), 2)['tx'][0]['txid'] - tx = create_transaction(self.nodes[0], input_txid, self.nodes[0].getnewaddress(), amount=(50 - 0.001)) - txid = tx.rehash() - tx_hex = tx.serialize().hex() + spendtx = self.miniwallet.create_self_transfer() with self.nodes[0].assert_debug_log(['transaction sent in violation of protocol peer=0']): - self.nodes[0].p2ps[0].send_message(msg_tx(tx)) + self.nodes[0].p2ps[0].send_message(msg_tx(spendtx['tx'])) self.nodes[0].p2ps[0].wait_for_disconnect() assert_equal(self.nodes[0].getmempoolinfo()['size'], 0) # Remove the disconnected peer del self.nodes[0].p2ps[0] - return tx, txid, tx_hex + return spendtx['tx'], spendtx['txid'], spendtx['wtxid'], spendtx['hex'] if __name__ == '__main__': diff --git a/test/functional/p2p_compactblocks.py b/test/functional/p2p_compactblocks.py index 55573efc06..364e806e18 100755 --- a/test/functional/p2p_compactblocks.py +++ b/test/functional/p2p_compactblocks.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test compact blocks (BIP 152). @@ -9,12 +9,64 @@ Version 2 compact blocks are post-segwit (wtxids) """ import random -from test_framework.blocktools import create_block, NORMAL_GBT_REQUEST_PARAMS, add_witness_commitment -from test_framework.messages import BlockTransactions, BlockTransactionsRequest, calculate_shortid, CBlock, CBlockHeader, CInv, COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, FromHex, HeaderAndShortIDs, msg_no_witness_block, msg_no_witness_blocktxn, msg_cmpctblock, msg_getblocktxn, msg_getdata, msg_getheaders, msg_headers, msg_inv, msg_sendcmpct, msg_sendheaders, msg_tx, msg_block, msg_blocktxn, MSG_BLOCK, MSG_CMPCT_BLOCK, MSG_WITNESS_FLAG, NODE_NETWORK, P2PHeaderAndShortIDs, PrefilledTransaction, ser_uint256, ToHex -from test_framework.p2p import p2p_lock, P2PInterface -from test_framework.script import CScript, OP_TRUE, OP_DROP +from test_framework.blocktools import ( + COINBASE_MATURITY, + NORMAL_GBT_REQUEST_PARAMS, + add_witness_commitment, + create_block, +) +from test_framework.messages import ( + BlockTransactions, + BlockTransactionsRequest, + CBlock, + CBlockHeader, + CInv, + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + from_hex, + HeaderAndShortIDs, + MSG_BLOCK, + MSG_CMPCT_BLOCK, + MSG_WITNESS_FLAG, + NODE_NETWORK, + P2PHeaderAndShortIDs, + PrefilledTransaction, + calculate_shortid, + msg_block, + msg_blocktxn, + msg_cmpctblock, + msg_getblocktxn, + msg_getdata, + msg_getheaders, + msg_headers, + msg_inv, + msg_no_witness_block, + msg_no_witness_blocktxn, + msg_sendcmpct, + msg_sendheaders, + msg_tx, + ser_uint256, + tx_from_hex, +) +from test_framework.p2p import ( + P2PInterface, + p2p_lock, +) +from test_framework.script import ( + CScript, + OP_DROP, + OP_TRUE, +) from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, softfork_active +from test_framework.util import ( + assert_equal, + softfork_active, +) +from test_framework.wallet import MiniWallet + # TestP2PConn: A peer we use to send messages to bitcoind, and store responses. class TestP2PConn(P2PInterface): @@ -100,9 +152,6 @@ class CompactBlocksTest(BitcoinTestFramework): ]] self.utxos = [] - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def build_block_on_tip(self, node, segwit=False): block = create_block(tmpl=node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) if segwit: @@ -115,7 +164,7 @@ class CompactBlocksTest(BitcoinTestFramework): block = self.build_block_on_tip(self.nodes[0]) self.segwit_node.send_and_ping(msg_no_witness_block(block)) assert int(self.nodes[0].getbestblockhash(), 16) == block.sha256 - self.nodes[0].generatetoaddress(100, self.nodes[0].getnewaddress(address_type="bech32")) + self.generate(self.wallet, COINBASE_MATURITY) total_value = block.vtx[0].vout[0].nValue out_value = total_value // 10 @@ -162,7 +211,7 @@ class CompactBlocksTest(BitcoinTestFramework): def check_announcement_of_new_block(node, peer, predicate): peer.clear_block_announcement() - block_hash = int(node.generate(1)[0], 16) + block_hash = int(self.generate(node, 1)[0], 16) peer.wait_for_block_announcement(block_hash, timeout=30) assert peer.block_announced @@ -226,7 +275,7 @@ class CompactBlocksTest(BitcoinTestFramework): # This test actually causes bitcoind to (reasonably!) disconnect us, so do this last. def test_invalid_cmpctblock_message(self): - self.nodes[0].generate(101) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) block = self.build_block_on_tip(self.nodes[0]) cmpct_block = P2PHeaderAndShortIDs() @@ -244,15 +293,13 @@ class CompactBlocksTest(BitcoinTestFramework): version = test_node.cmpct_version node = self.nodes[0] # Generate a bunch of transactions. - node.generate(101) + self.generate(node, COINBASE_MATURITY + 1) num_transactions = 25 - address = node.getnewaddress() segwit_tx_generated = False for _ in range(num_transactions): - txid = node.sendtoaddress(address, 0.1) - hex_tx = node.gettransaction(txid)["hex"] - tx = FromHex(CTransaction(), hex_tx) + hex_tx = self.wallet.send_self_transfer(from_node=self.nodes[0])['hex'] + tx = tx_from_hex(hex_tx) if not tx.wit.is_null(): segwit_tx_generated = True @@ -268,10 +315,10 @@ class CompactBlocksTest(BitcoinTestFramework): # Now mine a block, and look at the resulting compact block. test_node.clear_block_announcement() - block_hash = int(node.generate(1)[0], 16) + block_hash = int(self.generate(node, 1)[0], 16) # Store the raw block in our internal format. - block = FromHex(CBlock(), node.getblock("%064x" % block_hash, False)) + block = from_hex(CBlock(), node.getblock("%064x" % block_hash, False)) for tx in block.vtx: tx.calc_sha256() block.rehash() @@ -564,7 +611,7 @@ class CompactBlocksTest(BitcoinTestFramework): current_height = chain_height while (current_height >= chain_height - MAX_GETBLOCKTXN_DEPTH): block_hash = node.getblockhash(current_height) - block = FromHex(CBlock(), node.getblock(block_hash, False)) + block = from_hex(CBlock(), node.getblock(block_hash, False)) msg = msg_getblocktxn() msg.block_txn_request = BlockTransactionsRequest(int(block_hash, 16), []) @@ -610,7 +657,7 @@ class CompactBlocksTest(BitcoinTestFramework): new_blocks = [] for _ in range(MAX_CMPCTBLOCK_DEPTH + 1): test_node.clear_block_announcement() - new_blocks.append(node.generate(1)[0]) + new_blocks.append(self.generate(node, 1)[0]) test_node.wait_until(test_node.received_block_announcement, timeout=30) test_node.clear_block_announcement() @@ -618,7 +665,7 @@ class CompactBlocksTest(BitcoinTestFramework): test_node.wait_until(lambda: "cmpctblock" in test_node.last_message, timeout=30) test_node.clear_block_announcement() - node.generate(1) + self.generate(node, 1) test_node.wait_until(test_node.received_block_announcement, timeout=30) test_node.clear_block_announcement() with p2p_lock: @@ -667,9 +714,9 @@ class CompactBlocksTest(BitcoinTestFramework): [l.clear_block_announcement() for l in listeners] - # ToHex() won't serialize with witness, but this block has no witnesses - # anyway. TODO: repeat this test with witness tx's to a segwit node. - node.submitblock(ToHex(block)) + # serialize without witness (this block has no witnesses anyway). + # TODO: repeat this test with witness tx's to a segwit node. + node.submitblock(block.serialize().hex()) for l in listeners: l.wait_until(lambda: "cmpctblock" in l.last_message, timeout=30) @@ -793,8 +840,7 @@ class CompactBlocksTest(BitcoinTestFramework): assert_highbandwidth_states(self.nodes[0], hb_to=True, hb_from=False) def run_test(self): - # Get the nodes out of IBD - self.nodes[0].generate(1) + self.wallet = MiniWallet(self.nodes[0]) # Setup the p2p connections self.segwit_node = self.nodes[0].add_p2p_connection(TestP2PConn(cmpct_version=2)) diff --git a/test/functional/p2p_compactblocks_blocksonly.py b/test/functional/p2p_compactblocks_blocksonly.py new file mode 100755 index 0000000000..6367eb26a3 --- /dev/null +++ b/test/functional/p2p_compactblocks_blocksonly.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test that a node in blocksonly mode does not request compact blocks.""" + +from test_framework.messages import ( + MSG_BLOCK, + MSG_CMPCT_BLOCK, + MSG_WITNESS_FLAG, + CBlock, + CBlockHeader, + CInv, + from_hex, + msg_block, + msg_getdata, + msg_headers, + msg_sendcmpct, +) +from test_framework.p2p import P2PInterface +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + + +class P2PCompactBlocksBlocksOnly(BitcoinTestFramework): + def set_test_params(self): + self.extra_args = [["-blocksonly"], [], [], []] + self.num_nodes = 4 + + def setup_network(self): + self.setup_nodes() + # Start network with everyone disconnected + self.sync_all() + + def build_block_on_tip(self): + blockhash = self.generate(self.nodes[2], 1, sync_fun=self.no_op)[0] + block_hex = self.nodes[2].getblock(blockhash=blockhash, verbosity=0) + block = from_hex(CBlock(), block_hex) + block.rehash() + return block + + def run_test(self): + # Nodes will only request hb compact blocks mode when they're out of IBD + for node in self.nodes: + assert not node.getblockchaininfo()['initialblockdownload'] + + p2p_conn_blocksonly = self.nodes[0].add_p2p_connection(P2PInterface()) + p2p_conn_high_bw = self.nodes[1].add_p2p_connection(P2PInterface()) + p2p_conn_low_bw = self.nodes[3].add_p2p_connection(P2PInterface()) + for conn in [p2p_conn_blocksonly, p2p_conn_high_bw, p2p_conn_low_bw]: + assert_equal(conn.message_count['sendcmpct'], 2) + conn.send_and_ping(msg_sendcmpct(announce=False, version=2)) + + # Nodes: + # 0 -> blocksonly + # 1 -> high bandwidth + # 2 -> miner + # 3 -> low bandwidth + # + # Topology: + # p2p_conn_blocksonly ---> node0 + # p2p_conn_high_bw ---> node1 + # p2p_conn_low_bw ---> node3 + # node2 (no connections) + # + # node2 produces blocks that are passed to the rest of the nodes + # through the respective p2p connections. + + self.log.info("Test that -blocksonly nodes do not select peers for BIP152 high bandwidth mode") + + block0 = self.build_block_on_tip() + + # A -blocksonly node should not request BIP152 high bandwidth mode upon + # receiving a new valid block at the tip. + p2p_conn_blocksonly.send_and_ping(msg_block(block0)) + assert_equal(int(self.nodes[0].getbestblockhash(), 16), block0.sha256) + assert_equal(p2p_conn_blocksonly.message_count['sendcmpct'], 2) + assert_equal(p2p_conn_blocksonly.last_message['sendcmpct'].announce, False) + + # A normal node participating in transaction relay should request BIP152 + # high bandwidth mode upon receiving a new valid block at the tip. + p2p_conn_high_bw.send_and_ping(msg_block(block0)) + assert_equal(int(self.nodes[1].getbestblockhash(), 16), block0.sha256) + p2p_conn_high_bw.wait_until(lambda: p2p_conn_high_bw.message_count['sendcmpct'] == 3) + assert_equal(p2p_conn_high_bw.last_message['sendcmpct'].announce, True) + + # Don't send a block from the p2p_conn_low_bw so the low bandwidth node + # doesn't select it for BIP152 high bandwidth relay. + self.nodes[3].submitblock(block0.serialize().hex()) + + self.log.info("Test that -blocksonly nodes send getdata(BLOCK) instead" + " of getdata(CMPCT) in BIP152 low bandwidth mode") + + block1 = self.build_block_on_tip() + + p2p_conn_blocksonly.send_message(msg_headers(headers=[CBlockHeader(block1)])) + p2p_conn_blocksonly.sync_send_with_ping() + assert_equal(p2p_conn_blocksonly.last_message['getdata'].inv, [CInv(MSG_BLOCK | MSG_WITNESS_FLAG, block1.sha256)]) + + p2p_conn_high_bw.send_message(msg_headers(headers=[CBlockHeader(block1)])) + p2p_conn_high_bw.sync_send_with_ping() + assert_equal(p2p_conn_high_bw.last_message['getdata'].inv, [CInv(MSG_CMPCT_BLOCK, block1.sha256)]) + + self.log.info("Test that getdata(CMPCT) is still sent on BIP152 low bandwidth connections" + " when no -blocksonly nodes are involved") + + p2p_conn_low_bw.send_and_ping(msg_headers(headers=[CBlockHeader(block1)])) + p2p_conn_low_bw.sync_with_ping() + assert_equal(p2p_conn_low_bw.last_message['getdata'].inv, [CInv(MSG_CMPCT_BLOCK, block1.sha256)]) + + self.log.info("Test that -blocksonly nodes still serve compact blocks") + + def test_for_cmpctblock(block): + if 'cmpctblock' not in p2p_conn_blocksonly.last_message: + return False + return p2p_conn_blocksonly.last_message['cmpctblock'].header_and_shortids.header.rehash() == block.sha256 + + p2p_conn_blocksonly.send_message(msg_getdata([CInv(MSG_CMPCT_BLOCK, block0.sha256)])) + p2p_conn_blocksonly.wait_until(lambda: test_for_cmpctblock(block0)) + + # Request BIP152 high bandwidth mode from the -blocksonly node. + p2p_conn_blocksonly.send_and_ping(msg_sendcmpct(announce=True, version=2)) + + block2 = self.build_block_on_tip() + self.nodes[0].submitblock(block1.serialize().hex()) + self.nodes[0].submitblock(block2.serialize().hex()) + p2p_conn_blocksonly.wait_until(lambda: test_for_cmpctblock(block2)) + +if __name__ == '__main__': + P2PCompactBlocksBlocksOnly().main() diff --git a/test/functional/p2p_compactblocks_hb.py b/test/functional/p2p_compactblocks_hb.py new file mode 100755 index 0000000000..c985a1f98d --- /dev/null +++ b/test/functional/p2p_compactblocks_hb.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test compact blocks HB selection logic.""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + + +class CompactBlocksConnectionTest(BitcoinTestFramework): + """Test class for verifying selection of HB peer connections.""" + + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 6 + + def peer_info(self, from_node, to_node): + """Query from_node for its getpeerinfo about to_node.""" + for peerinfo in self.nodes[from_node].getpeerinfo(): + if "(testnode%i)" % to_node in peerinfo['subver']: + return peerinfo + return None + + def setup_network(self): + self.setup_nodes() + # Start network with everyone disconnected + self.sync_all() + + def relay_block_through(self, peer): + """Relay a new block through peer peer, and return HB status between 1 and [2,3,4,5].""" + self.connect_nodes(peer, 0) + self.generate(self.nodes[0], 1) + self.disconnect_nodes(peer, 0) + status_to = [self.peer_info(1, i)['bip152_hb_to'] for i in range(2, 6)] + status_from = [self.peer_info(i, 1)['bip152_hb_from'] for i in range(2, 6)] + assert_equal(status_to, status_from) + return status_to + + def run_test(self): + self.log.info("Testing reserved high-bandwidth mode slot for outbound peer...") + + # Connect everyone to node 0, and mine some blocks to get all nodes out of IBD. + for i in range(1, 6): + self.connect_nodes(i, 0) + self.generate(self.nodes[0], 2) + for i in range(1, 6): + self.disconnect_nodes(i, 0) + + # Construct network topology: + # - Node 0 is the block producer + # - Node 1 is the "target" node being tested + # - Nodes 2-5 are intermediaries. + # - Node 1 has an outbound connection to node 2 + # - Node 1 has inbound connections from nodes 3-5 + self.connect_nodes(3, 1) + self.connect_nodes(4, 1) + self.connect_nodes(5, 1) + self.connect_nodes(1, 2) + + # Mine blocks subsequently relaying through nodes 3,4,5 (inbound to node 1) + for nodeid in range(3, 6): + status = self.relay_block_through(nodeid) + assert_equal(status, [False, nodeid >= 3, nodeid >= 4, nodeid >= 5]) + + # And again through each. This should not change HB status. + for nodeid in range(3, 6): + status = self.relay_block_through(nodeid) + assert_equal(status, [False, True, True, True]) + + # Now relay one block through peer 2 (outbound from node 1), so it should take HB status + # from one of the inbounds. + status = self.relay_block_through(2) + assert_equal(status[0], True) + assert_equal(sum(status), 3) + + # Now relay again through nodes 3,4,5. Since 2 is outbound, it should remain HB. + for nodeid in range(3, 6): + status = self.relay_block_through(nodeid) + assert status[0] + assert status[nodeid - 2] + assert_equal(sum(status), 3) + + # Reconnect peer 2, and retry. Now the three inbounds should be HB again. + self.disconnect_nodes(1, 2) + self.connect_nodes(1, 2) + for nodeid in range(3, 6): + status = self.relay_block_through(nodeid) + assert not status[0] + assert status[nodeid - 2] + assert_equal(status, [False, True, True, True]) + + +if __name__ == '__main__': + CompactBlocksConnectionTest().main() diff --git a/test/functional/p2p_disconnect_ban.py b/test/functional/p2p_disconnect_ban.py index fb8529ad2b..7284ecde83 100755 --- a/test/functional/p2p_disconnect_ban.py +++ b/test/functional/p2p_disconnect_ban.py @@ -57,11 +57,11 @@ class DisconnectBanTest(BitcoinTestFramework): assert_equal(len(self.nodes[1].listbanned()), 0) self.log.info("setban: test persistence across node restart") - self.nodes[1].setban("127.0.0.0/32", "add") - self.nodes[1].setban("127.0.0.0/24", "add") # Set the mocktime so we can control when bans expire old_time = int(time.time()) self.nodes[1].setmocktime(old_time) + self.nodes[1].setban("127.0.0.0/32", "add") + self.nodes[1].setban("127.0.0.0/24", "add") self.nodes[1].setban("192.168.0.1", "add", 1) # ban for 1 seconds self.nodes[1].setban("2001:4d48:ac57:400:cacf:e9ff:fe1d:9c63/19", "add", 1000) # ban for 1000 seconds listBeforeShutdown = self.nodes[1].listbanned() @@ -70,6 +70,15 @@ class DisconnectBanTest(BitcoinTestFramework): self.nodes[1].setmocktime(old_time + 3) assert_equal(len(self.nodes[1].listbanned()), 3) + self.log.info("Test ban_duration and time_remaining") + for ban in self.nodes[1].listbanned(): + if ban["address"] in ["127.0.0.0/32", "127.0.0.0/24"]: + assert_equal(ban["ban_duration"], 86400) + assert_equal(ban["time_remaining"], 86397) + elif ban["address"] == "2001:4d48:ac57:400:cacf:e9ff:fe1d:9c63/19": + assert_equal(ban["ban_duration"], 1000) + assert_equal(ban["time_remaining"], 997) + self.restart_node(1) listAfterShutdown = self.nodes[1].listbanned() diff --git a/test/functional/p2p_dns_seeds.py b/test/functional/p2p_dns_seeds.py new file mode 100755 index 0000000000..e58ad8e0fc --- /dev/null +++ b/test/functional/p2p_dns_seeds.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test ThreadDNSAddressSeed logic for querying DNS seeds.""" + +import itertools + +from test_framework.p2p import P2PInterface +from test_framework.test_framework import BitcoinTestFramework + + +class P2PDNSSeeds(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [["-dnsseed=1"]] + + def run_test(self): + self.init_arg_tests() + self.existing_outbound_connections_test() + self.existing_block_relay_connections_test() + self.force_dns_test() + self.wait_time_tests() + + def init_arg_tests(self): + fakeaddr = "fakenodeaddr.fakedomain.invalid." + + self.log.info("Check that setting -connect disables -dnsseed by default") + self.nodes[0].stop_node() + with self.nodes[0].assert_debug_log(expected_msgs=["DNS seeding disabled"]): + self.start_node(0, [f"-connect={fakeaddr}"]) + + self.log.info("Check that running -connect and -dnsseed means DNS logic runs.") + with self.nodes[0].assert_debug_log(expected_msgs=["Loading addresses from DNS seed"], timeout=12): + self.restart_node(0, [f"-connect={fakeaddr}", "-dnsseed=1"]) + + self.log.info("Check that running -forcednsseed and -dnsseed=0 throws an error.") + self.nodes[0].stop_node() + self.nodes[0].assert_start_raises_init_error( + expected_msg="Error: Cannot set -forcednsseed to true when setting -dnsseed to false.", + extra_args=["-forcednsseed=1", "-dnsseed=0"], + ) + + self.log.info("Check that running -forcednsseed and -connect throws an error.") + # -connect soft sets -dnsseed to false, so throws the same error + self.nodes[0].stop_node() + self.nodes[0].assert_start_raises_init_error( + expected_msg="Error: Cannot set -forcednsseed to true when setting -dnsseed to false.", + extra_args=["-forcednsseed=1", f"-connect={fakeaddr}"], + ) + + # Restore default bitcoind settings + self.restart_node(0) + + def existing_outbound_connections_test(self): + # Make sure addrman is populated to enter the conditional where we + # delay and potentially skip DNS seeding. + self.nodes[0].addpeeraddress("192.0.0.8", 8333) + + self.log.info("Check that we *do not* query DNS seeds if we have 2 outbound connections") + + self.restart_node(0) + with self.nodes[0].assert_debug_log(expected_msgs=["P2P peers available. Skipped DNS seeding."], timeout=12): + for i in range(2): + self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i, connection_type="outbound-full-relay") + + def existing_block_relay_connections_test(self): + # Make sure addrman is populated to enter the conditional where we + # delay and potentially skip DNS seeding. No-op when run after + # existing_outbound_connections_test. + self.nodes[0].addpeeraddress("192.0.0.8", 8333) + + self.log.info("Check that we *do* query DNS seeds if we only have 2 block-relay-only connections") + + self.restart_node(0) + with self.nodes[0].assert_debug_log(expected_msgs=["Loading addresses from DNS seed"], timeout=12): + # This mimics the "anchors" logic where nodes are likely to + # reconnect to block-relay-only connections on startup. + # Since we do not participate in addr relay with these connections, + # we still want to query the DNS seeds. + for i in range(2): + self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i, connection_type="block-relay-only") + + def force_dns_test(self): + self.log.info("Check that we query DNS seeds if -forcednsseed param is set") + + with self.nodes[0].assert_debug_log(expected_msgs=["Loading addresses from DNS seed"], timeout=12): + # -dnsseed defaults to 1 in bitcoind, but 0 in the test framework, + # so pass it explicitly here + self.restart_node(0, ["-forcednsseed", "-dnsseed=1"]) + + # Restore default for subsequent tests + self.restart_node(0) + + def wait_time_tests(self): + self.log.info("Check the delay before querying DNS seeds") + + # Populate addrman with < 1000 addresses + for i in range(5): + a = f"192.0.0.{i}" + self.nodes[0].addpeeraddress(a, 8333) + + # The delay should be 11 seconds + with self.nodes[0].assert_debug_log(expected_msgs=["Waiting 11 seconds before querying DNS seeds.\n"]): + self.restart_node(0) + + # Populate addrman with > 1000 addresses + for i in itertools.count(): + first_octet = i % 2 + 1 + second_octet = i % 256 + third_octet = i % 100 + a = f"{first_octet}.{second_octet}.{third_octet}.1" + self.nodes[0].addpeeraddress(a, 8333) + if (i > 1000 and i % 100 == 0): + # The addrman size is non-deterministic because new addresses + # are sorted into buckets, potentially displacing existing + # addresses. Periodically check if we have met the desired + # threshold. + if len(self.nodes[0].getnodeaddresses(0)) > 1000: + break + + # The delay should be 5 mins + with self.nodes[0].assert_debug_log(expected_msgs=["Waiting 300 seconds before querying DNS seeds.\n"]): + self.restart_node(0) + + +if __name__ == '__main__': + P2PDNSSeeds().main() diff --git a/test/functional/p2p_dos_header_tree.py b/test/functional/p2p_dos_header_tree.py index 2349afa1ee..fde1e4bfa2 100755 --- a/test/functional/p2p_dos_header_tree.py +++ b/test/functional/p2p_dos_header_tree.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test that we reject low difficulty headers to prevent our block tree from filling up with useless bloat""" from test_framework.messages import ( CBlockHeader, - FromHex, + from_hex, ) from test_framework.p2p import ( P2PInterface, @@ -42,8 +42,8 @@ class RejectLowDifficultyHeadersTest(BitcoinTestFramework): self.headers = [l for l in h_lines if not l.startswith(FORK_PREFIX)] self.headers_fork = [l[len(FORK_PREFIX):] for l in h_lines if l.startswith(FORK_PREFIX)] - self.headers = [FromHex(CBlockHeader(), h) for h in self.headers] - self.headers_fork = [FromHex(CBlockHeader(), h) for h in self.headers_fork] + self.headers = [from_hex(CBlockHeader(), h) for h in self.headers] + self.headers_fork = [from_hex(CBlockHeader(), h) for h in self.headers_fork] self.log.info("Feed all non-fork headers, including and up to the first checkpoint") peer_checkpoint = self.nodes[0].add_p2p_connection(P2PInterface()) diff --git a/test/functional/p2p_eviction.py b/test/functional/p2p_eviction.py index d60aa5b383..1f4797a89d 100755 --- a/test/functional/p2p_eviction.py +++ b/test/functional/p2p_eviction.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -15,8 +15,16 @@ Therefore, this test is limited to the remaining protection criteria. import time -from test_framework.blocktools import create_block, create_coinbase -from test_framework.messages import CTransaction, FromHex, msg_pong, msg_tx +from test_framework.blocktools import ( + COINBASE_MATURITY, + create_block, + create_coinbase, +) +from test_framework.messages import ( + msg_pong, + msg_tx, + tx_from_hex, +) from test_framework.p2p import P2PDataStore, P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal @@ -45,7 +53,7 @@ class P2PEvict(BitcoinTestFramework): protected_peers = set() # peers that we expect to be protected from eviction current_peer = -1 node = self.nodes[0] - node.generatetoaddress(101, node.get_deterministic_priv_key().address) + self.generatetoaddress(node, COINBASE_MATURITY + 1, node.get_deterministic_priv_key().address) self.log.info("Create 4 peers and protect them from eviction by sending us a block") for _ in range(4): @@ -85,7 +93,7 @@ class P2PEvict(BitcoinTestFramework): 'scriptPubKey': prevtx['vout'][0]['scriptPubKey']['hex'], }], )['hex'] - txpeer.send_message(msg_tx(FromHex(CTransaction(), sigtx))) + txpeer.send_message(msg_tx(tx_from_hex(sigtx))) protected_peers.add(current_peer) self.log.info("Create 8 peers and protect them from eviction by having faster pings") diff --git a/test/functional/p2p_feefilter.py b/test/functional/p2p_feefilter.py index a2a122b352..b65e927d5b 100755 --- a/test/functional/p2p_feefilter.py +++ b/test/functional/p2p_feefilter.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test processing of feefilter messages.""" from decimal import Decimal +from test_framework.blocktools import COINBASE_MATURITY from test_framework.messages import MSG_TX, MSG_WTX, msg_feefilter from test_framework.p2p import P2PInterface, p2p_lock from test_framework.test_framework import BitcoinTestFramework @@ -61,6 +62,7 @@ class FeeFilterTest(BitcoinTestFramework): def run_test(self): self.test_feefilter_forcerelay() self.test_feefilter() + self.test_feefilter_blocksonly() def test_feefilter_forcerelay(self): self.log.info('Check that peers without forcerelay permission (default) get a feefilter message') @@ -79,8 +81,8 @@ class FeeFilterTest(BitcoinTestFramework): node0 = self.nodes[0] miniwallet = MiniWallet(node1) # Add enough mature utxos to the wallet, so that all txs spend confirmed coins - miniwallet.generate(5) - node1.generate(100) + self.generate(miniwallet, 5) + self.generate(node1, COINBASE_MATURITY) conn = self.nodes[0].add_p2p_connection(TestP2PConn()) @@ -119,6 +121,19 @@ class FeeFilterTest(BitcoinTestFramework): conn.wait_for_invs_to_match(txids) conn.clear_invs() + def test_feefilter_blocksonly(self): + """Test that we don't send fee filters to block-relay-only peers and when we're in blocksonly mode.""" + self.log.info("Check that we don't send fee filters to block-relay-only peers.") + feefilter_peer = self.nodes[0].add_outbound_p2p_connection(FeefilterConn(), p2p_idx=0, connection_type="block-relay-only") + feefilter_peer.sync_with_ping() + feefilter_peer.assert_feefilter_received(False) + + self.log.info("Check that we don't send fee filters when in blocksonly mode.") + self.restart_node(0, ["-blocksonly"]) + feefilter_peer = self.nodes[0].add_p2p_connection(FeefilterConn()) + feefilter_peer.sync_with_ping() + feefilter_peer.assert_feefilter_received(False) + if __name__ == '__main__': FeeFilterTest().main() diff --git a/test/functional/p2p_filter.py b/test/functional/p2p_filter.py index 8f64419138..3cf92b0316 100755 --- a/test/functional/p2p_filter.py +++ b/test/functional/p2p_filter.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ @@ -8,6 +8,7 @@ Test BIP 37 from test_framework.messages import ( CInv, + COIN, MAX_BLOOM_FILTER_SIZE, MAX_BLOOM_HASH_FUNCS, MSG_BLOCK, @@ -28,11 +29,15 @@ from test_framework.p2p import ( ) from test_framework.script import MAX_SCRIPT_ELEMENT_SIZE from test_framework.test_framework import BitcoinTestFramework +from test_framework.wallet import ( + MiniWallet, + getnewdestination, +) class P2PBloomFilter(P2PInterface): # This is a P2SH watch-only wallet - watch_script_pubkey = 'a914ffffffffffffffffffffffffffffffffffffffff87' + watch_script_pubkey = bytes.fromhex('a914ffffffffffffffffffffffffffffffffffffffff87') # The initial filter (n=10, fp=0.000001) with just the above scriptPubKey added watch_filter_init = msg_filterload( data= @@ -93,8 +98,9 @@ class FilterTest(BitcoinTestFramework): '-whitelist=noban@127.0.0.1', # immediate tx relay ]] - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() + def generatetoscriptpubkey(self, scriptpubkey): + """Helper to generate a single block to the given scriptPubKey.""" + return self.generatetodescriptor(self.nodes[0], 1, f'raw({scriptpubkey.hex()})')[0] def test_size_limits(self, filter_peer): self.log.info('Check that too large filter is rejected') @@ -130,8 +136,7 @@ class FilterTest(BitcoinTestFramework): filter_peer = P2PBloomFilter() self.log.debug("Create a tx relevant to the peer before connecting") - filter_address = self.nodes[0].decodescript(filter_peer.watch_script_pubkey)['addresses'][0] - txid = self.nodes[0].sendtoaddress(filter_address, 90) + txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN) self.log.debug("Send a mempool msg after connecting and check that the tx is received") self.nodes[0].add_p2p_connection(filter_peer) @@ -142,60 +147,57 @@ class FilterTest(BitcoinTestFramework): def test_frelay_false(self, filter_peer): self.log.info("Check that a node with fRelay set to false does not receive invs until the filter is set") filter_peer.tx_received = False - filter_address = self.nodes[0].decodescript(filter_peer.watch_script_pubkey)['addresses'][0] - self.nodes[0].sendtoaddress(filter_address, 90) + self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN) # Sync to make sure the reason filter_peer doesn't receive the tx is not p2p delays filter_peer.sync_with_ping() assert not filter_peer.tx_received # Clear the mempool so that this transaction does not impact subsequent tests - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) def test_filter(self, filter_peer): # Set the bloomfilter using filterload filter_peer.send_and_ping(filter_peer.watch_filter_init) # If fRelay is not already True, sending filterload sets it to True assert self.nodes[0].getpeerinfo()[0]['relaytxes'] - filter_address = self.nodes[0].decodescript(filter_peer.watch_script_pubkey)['addresses'][0] self.log.info('Check that we receive merkleblock and tx if the filter matches a tx in a block') - block_hash = self.nodes[0].generatetoaddress(1, filter_address)[0] + block_hash = self.generatetoscriptpubkey(filter_peer.watch_script_pubkey) txid = self.nodes[0].getblock(block_hash)['tx'][0] filter_peer.wait_for_merkleblock(block_hash) filter_peer.wait_for_tx(txid) self.log.info('Check that we only receive a merkleblock if the filter does not match a tx in a block') filter_peer.tx_received = False - block_hash = self.nodes[0].generatetoaddress(1, self.nodes[0].getnewaddress())[0] + block_hash = self.generatetoscriptpubkey(getnewdestination()[1]) filter_peer.wait_for_merkleblock(block_hash) assert not filter_peer.tx_received self.log.info('Check that we not receive a tx if the filter does not match a mempool tx') filter_peer.merkleblock_received = False filter_peer.tx_received = False - self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 90) - filter_peer.sync_with_ping() - filter_peer.sync_with_ping() + self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=7 * COIN) + filter_peer.sync_send_with_ping() assert not filter_peer.merkleblock_received assert not filter_peer.tx_received self.log.info('Check that we receive a tx if the filter matches a mempool tx') filter_peer.merkleblock_received = False - txid = self.nodes[0].sendtoaddress(filter_address, 90) + txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN) filter_peer.wait_for_tx(txid) assert not filter_peer.merkleblock_received self.log.info('Check that after deleting filter all txs get relayed again') filter_peer.send_and_ping(msg_filterclear()) for _ in range(5): - txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 7) + txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=7 * COIN) filter_peer.wait_for_tx(txid) self.log.info('Check that request for filtered blocks is ignored if no filter is set') filter_peer.merkleblock_received = False filter_peer.tx_received = False with self.nodes[0].assert_debug_log(expected_msgs=['received getdata']): - block_hash = self.nodes[0].generatetoaddress(1, self.nodes[0].getnewaddress())[0] + block_hash = self.generatetoscriptpubkey(getnewdestination()[1]) filter_peer.wait_for_inv([CInv(MSG_BLOCK, int(block_hash, 16))]) filter_peer.sync_with_ping() assert not filter_peer.merkleblock_received @@ -211,6 +213,9 @@ class FilterTest(BitcoinTestFramework): self.nodes[0].disconnect_p2ps() def run_test(self): + self.wallet = MiniWallet(self.nodes[0]) + self.wallet.rescan_utxos() + filter_peer = self.nodes[0].add_p2p_connection(P2PBloomFilter()) self.log.info('Test filter size limits') self.test_size_limits(filter_peer) diff --git a/test/functional/p2p_fingerprint.py b/test/functional/p2p_fingerprint.py index 469d66a851..e8bba8555f 100755 --- a/test/functional/p2p_fingerprint.py +++ b/test/functional/p2p_fingerprint.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test various fingerprinting protections. @@ -69,7 +69,7 @@ class P2PFingerprintTest(BitcoinTestFramework): self.nodes[0].setmocktime(int(time.time()) - 60 * 24 * 60 * 60) # Generating a chain of 10 blocks - block_hashes = self.nodes[0].generatetoaddress(10, self.nodes[0].get_deterministic_priv_key().address) + block_hashes = self.generatetoaddress(self.nodes[0], 10, self.nodes[0].get_deterministic_priv_key().address) # Create longer chain starting 2 blocks before current tip height = len(block_hashes) - 2 @@ -98,7 +98,7 @@ class P2PFingerprintTest(BitcoinTestFramework): # Longest chain is extended so stale is much older than chain tip self.nodes[0].setmocktime(0) - block_hash = int(self.nodes[0].generatetoaddress(1, self.nodes[0].get_deterministic_priv_key().address)[-1], 16) + block_hash = int(self.generatetoaddress(self.nodes[0], 1, self.nodes[0].get_deterministic_priv_key().address)[-1], 16) assert_equal(self.nodes[0].getblockcount(), 14) node0.wait_for_block(block_hash, timeout=3) diff --git a/test/functional/p2p_i2p_ports.py b/test/functional/p2p_i2p_ports.py new file mode 100755 index 0000000000..13188b9305 --- /dev/null +++ b/test/functional/p2p_i2p_ports.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Test ports handling for I2P hosts +""" + +import re + +from test_framework.test_framework import BitcoinTestFramework + + +class I2PPorts(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + # The test assumes that an I2P SAM proxy is not listening here. + self.extra_args = [["-i2psam=127.0.0.1:60000"]] + + def run_test(self): + node = self.nodes[0] + + self.log.info("Ensure we don't try to connect if port!=0") + addr = "zsxwyo6qcn3chqzwxnseusqgsnuw3maqnztkiypyfxtya4snkoka.b32.i2p:8333" + raised = False + try: + with node.assert_debug_log(expected_msgs=[f"Error connecting to {addr}"]): + node.addnode(node=addr, command="onetry") + except AssertionError as e: + raised = True + if not re.search(r"Expected messages .* does not partially match log", str(e)): + raise AssertionError(f"Assertion raised as expected, but with an unexpected message: {str(e)}") + if not raised: + raise AssertionError("Assertion should have been raised") + + self.log.info("Ensure we try to connect if port=0 and get an error due to missing I2P proxy") + addr = "h3r6bkn46qxftwja53pxiykntegfyfjqtnzbm6iv6r5mungmqgmq.b32.i2p:0" + with node.assert_debug_log(expected_msgs=[f"Error connecting to {addr}"]): + node.addnode(node=addr, command="onetry") + + +if __name__ == '__main__': + I2PPorts().main() diff --git a/test/functional/p2p_ibd_txrelay.py b/test/functional/p2p_ibd_txrelay.py index c3e758b021..65a94ad31c 100755 --- a/test/functional/p2p_ibd_txrelay.py +++ b/test/functional/p2p_ibd_txrelay.py @@ -1,12 +1,31 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Test fee filters during and after IBD.""" +"""Test transaction relay behavior during IBD: +- Set fee filters to MAX_MONEY +- Don't request transactions +- Ignore all transaction messages +""" from decimal import Decimal +import time -from test_framework.messages import COIN +from test_framework.messages import ( + CInv, + COIN, + CTransaction, + from_hex, + msg_inv, + msg_tx, + MSG_WTX, +) +from test_framework.p2p import ( + NONPREF_PEER_TX_DELAY, + P2PDataStore, + P2PInterface, + p2p_lock +) from test_framework.test_framework import BitcoinTestFramework MAX_FEE_FILTER = Decimal(9170997) / COIN @@ -28,15 +47,43 @@ class P2PIBDTxRelayTest(BitcoinTestFramework): assert node.getblockchaininfo()['initialblockdownload'] self.wait_until(lambda: all(peer['minfeefilter'] == MAX_FEE_FILTER for peer in node.getpeerinfo())) + self.log.info("Check that nodes don't send getdatas for transactions while still in IBD") + peer_inver = self.nodes[0].add_p2p_connection(P2PDataStore()) + txid = 0xdeadbeef + peer_inver.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=txid)])) + # The node should not send a getdata, but if it did, it would first delay 2 seconds + self.nodes[0].setmocktime(int(time.time() + NONPREF_PEER_TX_DELAY)) + peer_inver.sync_send_with_ping() + with p2p_lock: + assert txid not in peer_inver.getdata_requests + self.nodes[0].disconnect_p2ps() + + self.log.info("Check that nodes don't process unsolicited transactions while still in IBD") + # A transaction hex pulled from tx_valid.json. There are no valid transactions since no UTXOs + # exist yet, but it should be a well-formed transaction. + rawhex = "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a01ff473" + \ + "04402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e168" + \ + "1a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696a" + \ + "d990364e555c271ad504b88ac00000000" + assert self.nodes[1].decoderawtransaction(rawhex) # returns a dict, should not throw + tx = from_hex(CTransaction(), rawhex) + peer_txer = self.nodes[0].add_p2p_connection(P2PInterface()) + with self.nodes[0].assert_debug_log(expected_msgs=["received: tx"], unexpected_msgs=["was not accepted"]): + peer_txer.send_and_ping(msg_tx(tx)) + self.nodes[0].disconnect_p2ps() + # Come out of IBD by generating a block - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) self.log.info("Check that nodes reset minfilter after coming out of IBD") for node in self.nodes: assert not node.getblockchaininfo()['initialblockdownload'] self.wait_until(lambda: all(peer['minfeefilter'] == NORMAL_FEE_FILTER for peer in node.getpeerinfo())) + self.log.info("Check that nodes process the same transaction, even when unsolicited, when no longer in IBD") + peer_txer = self.nodes[0].add_p2p_connection(P2PInterface()) + with self.nodes[0].assert_debug_log(expected_msgs=["was not accepted"]): + peer_txer.send_and_ping(msg_tx(tx)) if __name__ == '__main__': P2PIBDTxRelayTest().main() diff --git a/test/functional/p2p_invalid_block.py b/test/functional/p2p_invalid_block.py index 483f25f48c..710f3d93e1 100755 --- a/test/functional/p2p_invalid_block.py +++ b/test/functional/p2p_invalid_block.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test node responses to invalid blocks. @@ -9,15 +9,25 @@ In this test we connect to one node over p2p, and test block requests: 2) Invalid block with duplicated transaction should be re-requested. 3) Invalid block with bad coinbase value should be rejected and not re-requested. +4) Invalid block due to future timestamp is later accepted when that timestamp +becomes valid. """ import copy - -from test_framework.blocktools import create_block, create_coinbase, create_tx_with_script +import time + +from test_framework.blocktools import ( + MAX_FUTURE_BLOCK_TIME, + create_block, + create_coinbase, + create_tx_with_script, +) from test_framework.messages import COIN from test_framework.p2p import P2PDataStore +from test_framework.script import OP_TRUE from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal + class InvalidBlockRequestTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -45,7 +55,7 @@ class InvalidBlockRequestTest(BitcoinTestFramework): peer.send_blocks_and_test([block1], node, success=True) self.log.info("Mature the block.") - node.generatetoaddress(100, node.get_deterministic_priv_key().address) + self.generatetoaddress(node, 100, node.get_deterministic_priv_key().address) best_block = node.getblock(node.getbestblockhash()) tip = int(node.getbestblockhash(), 16) @@ -60,16 +70,10 @@ class InvalidBlockRequestTest(BitcoinTestFramework): # For more information on merkle-root malleability see src/consensus/merkle.cpp. self.log.info("Test merkle root malleability.") - block2 = create_block(tip, create_coinbase(height), block_time) + tx1 = create_tx_with_script(block1.vtx[0], 0, script_sig=bytes([OP_TRUE]), amount=50 * COIN) + tx2 = create_tx_with_script(tx1, 0, script_sig=bytes([OP_TRUE]), amount=50 * COIN) + block2 = create_block(tip, create_coinbase(height), block_time, txlist=[tx1, tx2]) block_time += 1 - - # b'0x51' is OP_TRUE - tx1 = create_tx_with_script(block1.vtx[0], 0, script_sig=b'\x51', amount=50 * COIN) - tx2 = create_tx_with_script(tx1, 0, script_sig=b'\x51', amount=50 * COIN) - - block2.vtx.extend([tx1, tx2]) - block2.hashMerkleRoot = block2.calc_merkle_root() - block2.rehash() block2.solve() orig_hash = block2.sha256 block2_orig = copy.deepcopy(block2) @@ -89,19 +93,13 @@ class InvalidBlockRequestTest(BitcoinTestFramework): block2_dup.vtx[2].vin.append(block2_dup.vtx[2].vin[0]) block2_dup.vtx[2].rehash() block2_dup.hashMerkleRoot = block2_dup.calc_merkle_root() - block2_dup.rehash() block2_dup.solve() peer.send_blocks_and_test([block2_dup], node, success=False, reject_reason='bad-txns-inputs-duplicate') self.log.info("Test very broken block.") - block3 = create_block(tip, create_coinbase(height), block_time) + block3 = create_block(tip, create_coinbase(height, nValue=100), block_time) block_time += 1 - block3.vtx[0].vout[0].nValue = 100 * COIN # Too high! - block3.vtx[0].sha256 = None - block3.vtx[0].calc_sha256() - block3.hashMerkleRoot = block3.calc_merkle_root() - block3.rehash() block3.solve() peer.send_blocks_and_test([block3], node, success=False, reject_reason='bad-cb-amount') @@ -120,18 +118,25 @@ class InvalidBlockRequestTest(BitcoinTestFramework): # Complete testing of CVE-2018-17144, by checking for the inflation bug. # Create a block that spends the output of a tx in a previous block. - block4 = create_block(tip, create_coinbase(height), block_time) - tx3 = create_tx_with_script(tx2, 0, script_sig=b'\x51', amount=50 * COIN) - - # Duplicates input - tx3.vin.append(tx3.vin[0]) + tx3 = create_tx_with_script(tx2, 0, script_sig=bytes([OP_TRUE]), amount=50 * COIN) + tx3.vin.append(tx3.vin[0]) # Duplicates input tx3.rehash() - block4.vtx.append(tx3) - block4.hashMerkleRoot = block4.calc_merkle_root() - block4.rehash() + block4 = create_block(tip, create_coinbase(height), block_time, txlist=[tx3]) block4.solve() self.log.info("Test inflation by duplicating input") peer.send_blocks_and_test([block4], node, success=False, reject_reason='bad-txns-inputs-duplicate') + self.log.info("Test accepting identical block after rejecting it due to a future timestamp.") + t = int(time.time()) + node.setmocktime(t) + # Set block time +1 second past max future validity + block = create_block(tip, create_coinbase(height), t + MAX_FUTURE_BLOCK_TIME + 1) + block.solve() + # Need force_send because the block will get rejected without a getdata otherwise + peer.send_blocks_and_test([block], node, force_send=True, success=False, reject_reason='time-too-new') + node.setmocktime(t + 1) + peer.send_blocks_and_test([block], node, success=True) + + if __name__ == '__main__': InvalidBlockRequestTest().main() diff --git a/test/functional/p2p_invalid_locator.py b/test/functional/p2p_invalid_locator.py index f884cf90ff..626422370a 100755 --- a/test/functional/p2p_invalid_locator.py +++ b/test/functional/p2p_invalid_locator.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test node responses to invalid locators. @@ -16,7 +16,7 @@ class InvalidLocatorTest(BitcoinTestFramework): def run_test(self): node = self.nodes[0] # convenience reference to the node - node.generatetoaddress(1, node.get_deterministic_priv_key().address) # Get node out of IBD + self.generatetoaddress(node, 1, node.get_deterministic_priv_key().address) # Get node out of IBD self.log.info('Test max locator size') block_count = node.getblockcount() diff --git a/test/functional/p2p_invalid_messages.py b/test/functional/p2p_invalid_messages.py index c0b3c2cb12..3109ad2b56 100755 --- a/test/functional/p2p_invalid_messages.py +++ b/test/functional/p2p_invalid_messages.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test node responses to invalid network messages.""" @@ -28,7 +28,6 @@ from test_framework.p2p import ( from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - hex_str_to_bytes, ) VALID_DATA_LIMIT = MAX_PROTOCOL_MESSAGE_LENGTH - 5 # Account for the 5-byte length prefix @@ -37,7 +36,7 @@ VALID_DATA_LIMIT = MAX_PROTOCOL_MESSAGE_LENGTH - 5 # Account for the 5-byte len class msg_unrecognized: """Nonsensical message. Modeled after similar types in test_framework.messages.""" - msgtype = b'badmsg' + msgtype = b'badmsg\x01' def __init__(self, *, str_data): self.str_data = str_data.encode() if not isinstance(str_data, bytes) else str_data @@ -58,6 +57,7 @@ class InvalidMessagesTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True + self.extra_args = [["-whitelist=addr@127.0.0.1"]] def run_test(self): self.test_buffer() @@ -104,7 +104,7 @@ class InvalidMessagesTest(BitcoinTestFramework): def test_magic_bytes(self): self.log.info("Test message with invalid magic bytes disconnects peer") conn = self.nodes[0].add_p2p_connection(P2PDataStore()) - with self.nodes[0].assert_debug_log(['HEADER ERROR - MESSAGESTART (badmsg, 2 bytes), received ffffffff']): + with self.nodes[0].assert_debug_log(['Header error: Wrong MessageStart ffffffff received']): msg = conn.build_message(msg_unrecognized(str_data="d")) # modify magic bytes msg = b'\xff' * 4 + msg[4:] @@ -115,7 +115,7 @@ class InvalidMessagesTest(BitcoinTestFramework): def test_checksum(self): self.log.info("Test message with invalid checksum logs an error") conn = self.nodes[0].add_p2p_connection(P2PDataStore()) - with self.nodes[0].assert_debug_log(['CHECKSUM ERROR (badmsg, 2 bytes), expected 78df0a04 was ffffffff']): + with self.nodes[0].assert_debug_log(['Header error: Wrong checksum (badmsg, 2 bytes), expected 78df0a04 was ffffffff']): msg = conn.build_message(msg_unrecognized(str_data="d")) # Checksum is after start bytes (4B), message type (12B), len (4B) cut_len = 4 + 12 + 4 @@ -130,7 +130,7 @@ class InvalidMessagesTest(BitcoinTestFramework): def test_size(self): self.log.info("Test message with oversized payload disconnects peer") conn = self.nodes[0].add_p2p_connection(P2PDataStore()) - with self.nodes[0].assert_debug_log(['HEADER ERROR - SIZE (badmsg, 4000001 bytes)']): + with self.nodes[0].assert_debug_log(['Header error: Size too large (badmsg, 4000001 bytes)']): msg = msg_unrecognized(str_data="d" * (VALID_DATA_LIMIT + 1)) msg = conn.build_message(msg) conn.send_raw_message(msg) @@ -140,7 +140,7 @@ class InvalidMessagesTest(BitcoinTestFramework): def test_msgtype(self): self.log.info("Test message with invalid message type logs an error") conn = self.nodes[0].add_p2p_connection(P2PDataStore()) - with self.nodes[0].assert_debug_log(['HEADER ERROR - COMMAND']): + with self.nodes[0].assert_debug_log(['Header error: Invalid message type']): msg = msg_unrecognized(str_data="d") msg = conn.build_message(msg) # Modify msgtype @@ -186,7 +186,7 @@ class InvalidMessagesTest(BitcoinTestFramework): [ 'received: addrv2 (1 bytes)', ], - hex_str_to_bytes('00')) + bytes.fromhex('00')) def test_addrv2_too_long_address(self): self.test_addrv2('too long address', @@ -195,7 +195,7 @@ class InvalidMessagesTest(BitcoinTestFramework): 'ProcessMessages(addrv2, 525 bytes): Exception', 'Address too long: 513 > 512', ], - hex_str_to_bytes( + bytes.fromhex( '01' + # number of entries '61bc6649' + # time, Fri Jan 9 02:54:25 UTC 2009 '00' + # service flags, COMPACTSIZE(NODE_NONE) @@ -209,10 +209,10 @@ class InvalidMessagesTest(BitcoinTestFramework): self.test_addrv2('unrecognized network', [ 'received: addrv2 (25 bytes)', - 'IP 9.9.9.9 mapped', + '9.9.9.9:8333 mapped', 'Added 1 addresses', ], - hex_str_to_bytes( + bytes.fromhex( '02' + # number of entries # this should be ignored without impeding acceptance of subsequent ones now_hex + # time diff --git a/test/functional/p2p_invalid_tx.py b/test/functional/p2p_invalid_tx.py index 8783c244c3..139f4d64e7 100755 --- a/test/functional/p2p_invalid_tx.py +++ b/test/functional/p2p_invalid_tx.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test node responses to invalid transactions. @@ -64,7 +64,7 @@ class InvalidTxRequestTest(BitcoinTestFramework): node.p2ps[0].send_blocks_and_test([block], node, success=True) self.log.info("Mature the block.") - self.nodes[0].generatetoaddress(100, self.nodes[0].get_deterministic_priv_key().address) + self.generatetoaddress(self.nodes[0], 100, self.nodes[0].get_deterministic_priv_key().address) # Iterate through a list of known invalid transaction types, ensuring each is # rejected. Some are consensus invalid and some just violate policy. @@ -141,9 +141,9 @@ class InvalidTxRequestTest(BitcoinTestFramework): tx_orphan_2_valid, # The valid transaction (with sufficient fee) ] } - # Transactions that do not end up in the mempool - # tx_orphan_no_fee, because it has too low fee (p2ps[0] is not disconnected for relaying that tx) - # tx_orphan_invalid, because it has negative fee (p2ps[1] is disconnected for relaying that tx) + # Transactions that do not end up in the mempool: + # tx_orphan_2_no_fee, because it has too low fee (p2ps[0] is not disconnected for relaying that tx) + # tx_orphan_2_invalid, because it has negative fee (p2ps[1] is disconnected for relaying that tx) self.wait_until(lambda: 1 == len(node.getpeerinfo()), timeout=12) # p2ps[1] is no longer connected assert_equal(expected_mempool, set(node.getrawmempool())) diff --git a/test/functional/p2p_leak.py b/test/functional/p2p_leak.py index 71d5ca92b3..af8e45d578 100755 --- a/test/functional/p2p_leak.py +++ b/test/functional/p2p_leak.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test message sending before handshake completion. @@ -29,6 +29,8 @@ from test_framework.util import ( assert_greater_than_or_equal, ) +PEER_TIMEOUT = 3 + class LazyPeer(P2PInterface): def __init__(self): @@ -98,7 +100,7 @@ class P2PVersionStore(P2PInterface): class P2PLeakTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.extra_args = [['-peertimeout=4']] + self.extra_args = [[f"-peertimeout={PEER_TIMEOUT}"]] def create_old_version(self, nversion): old_version_msg = msg_version() @@ -131,10 +133,10 @@ class P2PLeakTest(BitcoinTestFramework): pre_wtxidrelay_peer.wait_until(lambda: pre_wtxidrelay_peer.version_received) # Mine a block and make sure that it's not sent to the connected peers - self.nodes[0].generate(nblocks=1) + self.generate(self.nodes[0], nblocks=1) # Give the node enough time to possibly leak out a message - time.sleep(5) + time.sleep(PEER_TIMEOUT + 2) # Make sure only expected messages came in assert not no_version_idle_peer.unexpected_msg diff --git a/test/functional/p2p_leak_tx.py b/test/functional/p2p_leak_tx.py index a45f792e81..4c064b359e 100755 --- a/test/functional/p2p_leak_tx.py +++ b/test/functional/p2p_leak_tx.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test that we don't leak txs to inbound peers that we haven't yet announced to""" +from test_framework.blocktools import COINBASE_MATURITY from test_framework.messages import msg_getdata, CInv, MSG_TX from test_framework.p2p import p2p_lock, P2PDataStore from test_framework.test_framework import BitcoinTestFramework @@ -26,8 +27,8 @@ class P2PLeakTxTest(BitcoinTestFramework): gen_node = self.nodes[0] # The block and tx generating node miniwallet = MiniWallet(gen_node) # Add enough mature utxos to the wallet, so that all txs spend confirmed coins - miniwallet.generate(1) - gen_node.generate(100) + self.generate(miniwallet, 1) + self.generate(gen_node, COINBASE_MATURITY) inbound_peer = self.nodes[0].add_p2p_connection(P2PNode()) # An "attacking" inbound peer diff --git a/test/functional/p2p_message_capture.py b/test/functional/p2p_message_capture.py index 080b2d93ad..edde9a6ecf 100755 --- a/test/functional/p2p_message_capture.py +++ b/test/functional/p2p_message_capture.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test per-peer message capture capability. diff --git a/test/functional/p2p_node_network_limited.py b/test/functional/p2p_node_network_limited.py index 8d95f155c8..5a0003d3ef 100755 --- a/test/functional/p2p_node_network_limited.py +++ b/test/functional/p2p_node_network_limited.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Tests NODE_NETWORK_LIMITED. @@ -59,8 +59,7 @@ class NodeNetworkLimitedTest(BitcoinTestFramework): self.log.info("Mine enough blocks to reach the NODE_NETWORK_LIMITED range.") self.connect_nodes(0, 1) - blocks = self.nodes[1].generatetoaddress(292, self.nodes[1].get_deterministic_priv_key().address) - self.sync_blocks([self.nodes[0], self.nodes[1]]) + blocks = self.generate(self.nodes[1], 292, sync_fun=lambda: self.sync_blocks([self.nodes[0], self.nodes[1]])) self.log.info("Make sure we can max retrieve block at tip-288.") node.send_getdata_for_block(blocks[1]) # last block in valid range @@ -101,7 +100,7 @@ class NodeNetworkLimitedTest(BitcoinTestFramework): self.disconnect_all() # mine 10 blocks on node 0 (pruned node) - self.nodes[0].generatetoaddress(10, self.nodes[0].get_deterministic_priv_key().address) + self.generate(self.nodes[0], 10, sync_fun=self.no_op) # connect node1 (non pruned) with node0 (pruned) and check if the can sync self.connect_nodes(0, 1) diff --git a/test/functional/p2p_permissions.py b/test/functional/p2p_permissions.py index 62652d949d..1dc3a5b9a0 100755 --- a/test/functional/p2p_permissions.py +++ b/test/functional/p2p_permissions.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test p2p permission message. @@ -9,9 +9,8 @@ Test that permissions are correctly calculated and applied from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE from test_framework.messages import ( - CTransaction, CTxInWitness, - FromHex, + tx_from_hex, ) from test_framework.p2p import P2PDataStore from test_framework.script import ( @@ -94,8 +93,7 @@ class P2PPermissionsTests(BitcoinTestFramework): self.nodes[1].assert_start_raises_init_error(["-whitebind=noban@127.0.0.1/10"], "Cannot resolve -whitebind address", match=ErrorMatch.PARTIAL_REGEX) def check_tx_relay(self): - block_op_true = self.nodes[0].getblock(self.nodes[0].generatetoaddress(100, ADDRESS_BCRT1_P2WSH_OP_TRUE)[0]) - self.sync_all() + block_op_true = self.nodes[0].getblock(self.generatetoaddress(self.nodes[0], 100, ADDRESS_BCRT1_P2WSH_OP_TRUE)[0]) self.log.debug("Create a connection from a forcerelay peer that rebroadcasts raw txs") # A test framework p2p connection is needed to send the raw transaction directly. If a full node was used, it could only @@ -105,8 +103,7 @@ class P2PPermissionsTests(BitcoinTestFramework): p2p_rebroadcast_wallet = self.nodes[1].add_p2p_connection(P2PDataStore()) self.log.debug("Send a tx from the wallet initially") - tx = FromHex( - CTransaction(), + tx = tx_from_hex( self.nodes[0].createrawtransaction( inputs=[{ 'txid': block_op_true['tx'][0], @@ -132,7 +129,7 @@ class P2PPermissionsTests(BitcoinTestFramework): tx.vout[0].nValue += 1 txid = tx.rehash() # Send the transaction twice. The first time, it'll be rejected by ATMP because it conflicts - # with a mempool transaction. The second time, it'll be in the recentRejects filter. + # with a mempool transaction. The second time, it'll be in the m_recent_rejects filter. p2p_rebroadcast_wallet.send_txs_and_test( [tx], self.nodes[1], diff --git a/test/functional/p2p_ping.py b/test/functional/p2p_ping.py index 888e986fba..52dae90d19 100755 --- a/test/functional/p2p_ping.py +++ b/test/functional/p2p_ping.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test ping message @@ -30,11 +30,16 @@ class NodeNoPong(P2PInterface): pass +TIMEOUT_INTERVAL = 20 * 60 + + class PingPongTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - self.extra_args = [['-peertimeout=3']] + # Set the peer connection timeout low. It does not matter for this + # test, as long as it is less than TIMEOUT_INTERVAL. + self.extra_args = [['-peertimeout=1']] def check_peer_info(self, *, pingtime, minping, pingwait): stats = self.nodes[0].getpeerinfo()[0] @@ -110,8 +115,11 @@ class PingPongTest(BitcoinTestFramework): self.nodes[0].ping() no_pong_node.wait_until(lambda: 'ping' in no_pong_node.last_message) with self.nodes[0].assert_debug_log(['ping timeout: 1201.000000s']): - self.mock_forward(20 * 60 + 1) - time.sleep(4) # peertimeout + 1 + self.mock_forward(TIMEOUT_INTERVAL // 2) + # Check that sending a ping does not prevent the disconnect + no_pong_node.sync_with_ping() + self.mock_forward(TIMEOUT_INTERVAL // 2 + 1) + no_pong_node.wait_for_disconnect() if __name__ == '__main__': diff --git a/test/functional/p2p_segwit.py b/test/functional/p2p_segwit.py index 54891b07e1..f377fbaaa6 100755 --- a/test/functional/p2p_segwit.py +++ b/test/functional/p2p_segwit.py @@ -1,19 +1,22 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test segwit transactions and blocks on P2P network.""" from decimal import Decimal -import math import random import struct import time -from test_framework.blocktools import create_block, create_coinbase, add_witness_commitment, get_witness_script, WITNESS_COMMITMENT_HEADER +from test_framework.blocktools import ( + WITNESS_COMMITMENT_HEADER, + add_witness_commitment, + create_block, + create_coinbase, +) from test_framework.key import ECKey from test_framework.messages import ( BIP125_SEQUENCE_NUMBER, - CBlock, CBlockHeader, CInv, COutPoint, @@ -22,7 +25,7 @@ from test_framework.messages import ( CTxInWitness, CTxOut, CTxWitness, - MAX_BLOCK_BASE_SIZE, + MAX_BLOCK_WEIGHT, MSG_BLOCK, MSG_TX, MSG_WITNESS_FLAG, @@ -40,12 +43,12 @@ from test_framework.messages import ( ser_uint256, ser_vector, sha256, - uint256_from_str, - FromHex, + tx_from_hex, ) from test_framework.p2p import ( P2PInterface, p2p_lock, + P2P_SERVICES, ) from test_framework.script import ( CScript, @@ -60,12 +63,8 @@ from test_framework.script import ( OP_CHECKMULTISIG, OP_CHECKSIG, OP_DROP, - OP_DUP, OP_ELSE, OP_ENDIF, - OP_EQUAL, - OP_EQUALVERIFY, - OP_HASH160, OP_IF, OP_RETURN, OP_TRUE, @@ -77,18 +76,20 @@ from test_framework.script import ( LegacySignatureHash, hash160, ) +from test_framework.script_util import ( + key_to_p2pk_script, + key_to_p2wpkh_script, + keyhash_to_p2pkh_script, + script_to_p2sh_script, + script_to_p2wsh_script, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, softfork_active, - hex_str_to_bytes, assert_raises_rpc_error, ) -# The versionbit bit used to signal activation of SegWit -VB_WITNESS_BIT = 1 -VB_TOP_BITS = 0x20000000 - MAX_SIGOP_COST = 80000 SEGWIT_HEIGHT = 120 @@ -100,27 +101,30 @@ class UTXO(): self.n = n self.nValue = value -def get_p2pkh_script(pubkeyhash): - """Get the script associated with a P2PKH.""" - return CScript([CScriptOp(OP_DUP), CScriptOp(OP_HASH160), pubkeyhash, CScriptOp(OP_EQUALVERIFY), CScriptOp(OP_CHECKSIG)]) + +def subtest(func): + """Wraps the subtests for logging and state assertions.""" + def func_wrapper(self, *args, **kwargs): + self.log.info("Subtest: {} (Segwit active = {})".format(func.__name__, self.segwit_active)) + # Assert segwit status is as expected + assert_equal(softfork_active(self.nodes[0], 'segwit'), self.segwit_active) + func(self, *args, **kwargs) + # Each subtest should leave some utxos for the next subtest + assert self.utxo + self.sync_blocks() + # Assert segwit status is as expected at end of subtest + assert_equal(softfork_active(self.nodes[0], 'segwit'), self.segwit_active) + + return func_wrapper + def sign_p2pk_witness_input(script, tx_to, in_idx, hashtype, value, key): - """Add signature for a P2PK witness program.""" + """Add signature for a P2PK witness script.""" tx_hash = SegwitV0SignatureHash(script, tx_to, in_idx, hashtype, value) signature = key.sign_ecdsa(tx_hash) + chr(hashtype).encode('latin-1') tx_to.wit.vtxinwit[in_idx].scriptWitness.stack = [signature, script] tx_to.rehash() -def get_virtual_size(witness_block): - """Calculate the virtual size of a witness block. - - Virtual size is base + witness/4.""" - base_size = len(witness_block.serialize(with_witness=False)) - total_size = len(witness_block.serialize()) - # the "+3" is so we round up - vsize = int((3 * base_size + total_size + 3) / 4) - return vsize - def test_transaction_acceptance(node, p2p, tx, with_witness, accepted, reason=None): """Send a transaction to the node and check that it's accepted to the mempool @@ -164,7 +168,7 @@ class TestP2PConn(P2PInterface): def on_wtxidrelay(self, message): self.last_wtxidrelay.append(message) - def announce_tx_and_wait_for_getdata(self, tx, timeout=60, success=True, use_wtxid=False): + def announce_tx_and_wait_for_getdata(self, tx, success=True, use_wtxid=False): if success: # sanity check assert (self.wtxidrelay and use_wtxid) or (not self.wtxidrelay and not use_wtxid) @@ -178,11 +182,11 @@ class TestP2PConn(P2PInterface): if success: if use_wtxid: - self.wait_for_getdata([wtxid], timeout) + self.wait_for_getdata([wtxid]) else: - self.wait_for_getdata([tx.sha256], timeout) + self.wait_for_getdata([tx.sha256]) else: - time.sleep(timeout) + time.sleep(5) assert not self.last_message.get("getdata") def announce_block_and_wait_for_getdata(self, block, use_header, timeout=60): @@ -209,33 +213,25 @@ class TestP2PConn(P2PInterface): class SegWitTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True - self.num_nodes = 3 + self.num_nodes = 2 # This test tests SegWit both pre and post-activation, so use the normal BIP9 activation. self.extra_args = [ - ["-acceptnonstdtxn=1", "-segwitheight={}".format(SEGWIT_HEIGHT), "-whitelist=noban@127.0.0.1"], - ["-acceptnonstdtxn=0", "-segwitheight={}".format(SEGWIT_HEIGHT)], - ["-acceptnonstdtxn=1", "-segwitheight=-1"], + ["-acceptnonstdtxn=1", f"-testactivationheight=segwit@{SEGWIT_HEIGHT}", "-whitelist=noban@127.0.0.1", "-par=1"], + ["-acceptnonstdtxn=0", f"-testactivationheight=segwit@{SEGWIT_HEIGHT}"], ] self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() - def setup_network(self): - self.setup_nodes() - self.connect_nodes(0, 1) - self.connect_nodes(0, 2) - self.sync_all() - # Helper functions - def build_next_block(self, version=4): + def build_next_block(self): """Build a block on top of node0's tip.""" tip = self.nodes[0].getbestblockhash() height = self.nodes[0].getblockcount() + 1 block_time = self.nodes[0].getblockheader(tip)["mediantime"] + 1 block = create_block(int(tip, 16), create_coinbase(height), block_time) - block.nVersion = version block.rehash() return block @@ -247,14 +243,14 @@ class SegWitTest(BitcoinTestFramework): def run_test(self): # Setup the p2p connections - # self.test_node sets NODE_WITNESS|NODE_NETWORK - self.test_node = self.nodes[0].add_p2p_connection(TestP2PConn(), services=NODE_NETWORK | NODE_WITNESS) + # self.test_node sets P2P_SERVICES, i.e. NODE_WITNESS | NODE_NETWORK + self.test_node = self.nodes[0].add_p2p_connection(TestP2PConn(), services=P2P_SERVICES) # self.old_node sets only NODE_NETWORK self.old_node = self.nodes[0].add_p2p_connection(TestP2PConn(), services=NODE_NETWORK) # self.std_node is for testing node1 (fRequireStandard=true) - self.std_node = self.nodes[1].add_p2p_connection(TestP2PConn(), services=NODE_NETWORK | NODE_WITNESS) + self.std_node = self.nodes[1].add_p2p_connection(TestP2PConn(), services=P2P_SERVICES) # self.std_wtx_node is for testing node1 with wtxid relay - self.std_wtx_node = self.nodes[1].add_p2p_connection(TestP2PConn(wtxidrelay=True), services=NODE_NETWORK | NODE_WITNESS) + self.std_wtx_node = self.nodes[1].add_p2p_connection(TestP2PConn(wtxidrelay=True), services=P2P_SERVICES) assert self.test_node.nServices & NODE_WITNESS != 0 @@ -267,7 +263,6 @@ class SegWitTest(BitcoinTestFramework): self.test_non_witness_transaction() self.test_v0_outputs_arent_spendable() self.test_block_relay() - self.test_getblocktemplate_before_lockin() self.test_unnecessary_witness_before_segwit_activation() self.test_witness_tx_relay_before_segwit_activation() self.test_standardness_v0() @@ -284,7 +279,7 @@ class SegWitTest(BitcoinTestFramework): self.test_submit_block() self.test_extra_witness_data() self.test_max_witness_push_length() - self.test_max_witness_program_length() + self.test_max_witness_script_length() self.test_witness_input_length() self.test_block_relay() self.test_tx_relay_after_segwit_activation() @@ -295,40 +290,24 @@ class SegWitTest(BitcoinTestFramework): self.test_signature_version_1() self.test_non_standard_witness_blinding() self.test_non_standard_witness() - self.test_upgrade_after_activation() self.test_witness_sigops() self.test_superfluous_witness() self.test_wtxid_relay() # Individual tests - def subtest(func): # noqa: N805 - """Wraps the subtests for logging and state assertions.""" - def func_wrapper(self, *args, **kwargs): - self.log.info("Subtest: {} (Segwit active = {})".format(func.__name__, self.segwit_active)) - # Assert segwit status is as expected - assert_equal(softfork_active(self.nodes[0], 'segwit'), self.segwit_active) - func(self, *args, **kwargs) - # Each subtest should leave some utxos for the next subtest - assert self.utxo - self.sync_blocks() - # Assert segwit status is as expected at end of subtest - assert_equal(softfork_active(self.nodes[0], 'segwit'), self.segwit_active) - - return func_wrapper - - @subtest # type: ignore + @subtest def test_non_witness_transaction(self): """See if sending a regular transaction works, and create a utxo to use in later tests.""" # Mine a block with an anyone-can-spend coinbase, # let it mature, then try to spend it. - block = self.build_next_block(version=1) + block = self.build_next_block() block.solve() self.test_node.send_and_ping(msg_no_witness_block(block)) # make sure the block was processed txid = block.vtx[0].sha256 - self.nodes[0].generate(99) # let the block mature + self.generate(self.nodes[0], 99) # let the block mature # Create a transaction that spends the coinbase tx = CTransaction() @@ -344,9 +323,9 @@ class SegWitTest(BitcoinTestFramework): assert tx.hash in self.nodes[0].getrawmempool() # Save this transaction for later self.utxo.append(UTXO(tx.sha256, 0, 49 * 100000000)) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) - @subtest # type: ignore + @subtest def test_unnecessary_witness_before_segwit_activation(self): """Verify that blocks with witnesses are rejected before activation.""" @@ -361,8 +340,8 @@ class SegWitTest(BitcoinTestFramework): tx.rehash() assert tx.sha256 != tx.calc_sha256(with_witness=True) - # Construct a segwit-signaling block that includes the transaction. - block = self.build_next_block(version=(VB_TOP_BITS | (1 << VB_WITNESS_BIT))) + # Construct a block that includes the transaction. + block = self.build_next_block() self.update_witness_block_with_transactions(block, [tx]) # Sending witness data before activation is not allowed (anti-spam # rule). @@ -377,7 +356,7 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop(0) self.utxo.append(UTXO(tx.sha256, 0, tx.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_block_relay(self): """Test that block requests to NODE_WITNESS peer are with MSG_WITNESS_FLAG. @@ -389,7 +368,7 @@ class SegWitTest(BitcoinTestFramework): # test_node has set NODE_WITNESS, so all getdata requests should be for # witness blocks. # Test announcing a block via inv results in a getdata, and that - # announcing a version 4 or random VB block with a header results in a getdata + # announcing a block with a header results in a getdata block1 = self.build_next_block() block1.solve() @@ -397,19 +376,13 @@ class SegWitTest(BitcoinTestFramework): assert self.test_node.last_message["getdata"].inv[0].type == blocktype test_witness_block(self.nodes[0], self.test_node, block1, True) - block2 = self.build_next_block(version=4) + block2 = self.build_next_block() block2.solve() self.test_node.announce_block_and_wait_for_getdata(block2, use_header=True) assert self.test_node.last_message["getdata"].inv[0].type == blocktype test_witness_block(self.nodes[0], self.test_node, block2, True) - block3 = self.build_next_block(version=(VB_TOP_BITS | (1 << 15))) - block3.solve() - self.test_node.announce_block_and_wait_for_getdata(block3, use_header=True) - assert self.test_node.last_message["getdata"].inv[0].type == blocktype - test_witness_block(self.nodes[0], self.test_node, block3, True) - # Check that we can getdata for witness blocks or regular blocks, # and the right thing happens. if not self.segwit_active: @@ -428,7 +401,7 @@ class SegWitTest(BitcoinTestFramework): block = self.test_node.request_block(block_hash, 2) wit_block = self.test_node.request_block(block_hash, 2 | MSG_WITNESS_FLAG) assert_equal(block.serialize(), wit_block.serialize()) - assert_equal(block.serialize(), hex_str_to_bytes(rpc_block)) + assert_equal(block.serialize(), bytes.fromhex(rpc_block)) else: # After activation, witness blocks and non-witness blocks should # be different. Verify rpc getblock() returns witness blocks, while @@ -443,7 +416,7 @@ class SegWitTest(BitcoinTestFramework): rpc_block = self.nodes[0].getblock(block.hash, False) non_wit_block = self.test_node.request_block(block.sha256, 2) wit_block = self.test_node.request_block(block.sha256, 2 | MSG_WITNESS_FLAG) - assert_equal(wit_block.serialize(), hex_str_to_bytes(rpc_block)) + assert_equal(wit_block.serialize(), bytes.fromhex(rpc_block)) assert_equal(wit_block.serialize(False), non_wit_block.serialize()) assert_equal(wit_block.serialize(), block.serialize()) @@ -451,11 +424,10 @@ class SegWitTest(BitcoinTestFramework): rpc_details = self.nodes[0].getblock(block.hash, True) assert_equal(rpc_details["size"], len(block.serialize())) assert_equal(rpc_details["strippedsize"], len(block.serialize(False))) - weight = 3 * len(block.serialize(False)) + len(block.serialize()) - assert_equal(rpc_details["weight"], weight) + assert_equal(rpc_details["weight"], block.get_weight()) # Upgraded node should not ask for blocks from unupgraded - block4 = self.build_next_block(version=4) + block4 = self.build_next_block() block4.solve() self.old_node.getdataset = set() @@ -473,7 +445,7 @@ class SegWitTest(BitcoinTestFramework): self.old_node.announce_tx_and_wait_for_getdata(block4.vtx[0]) assert block4.sha256 not in self.old_node.getdataset - @subtest # type: ignore + @subtest def test_v0_outputs_arent_spendable(self): """Test that v0 outputs aren't spendable before segwit activation. @@ -485,18 +457,10 @@ class SegWitTest(BitcoinTestFramework): witness, and so can't be spent before segwit activation (the point at which blocks are permitted to contain witnesses).""" - # node2 doesn't need to be connected for this test. - # (If it's connected, node0 may propagate an invalid block to it over - # compact blocks and the nodes would have inconsistent tips.) - self.disconnect_nodes(0, 2) - # Create two outputs, a p2wsh and p2sh-p2wsh - witness_program = CScript([OP_TRUE]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) - - p2sh_pubkey = hash160(script_pubkey) - p2sh_script_pubkey = CScript([OP_HASH160, p2sh_pubkey, OP_EQUAL]) + witness_script = CScript([OP_TRUE]) + script_pubkey = script_to_p2wsh_script(witness_script) + p2sh_script_pubkey = script_to_p2sh_script(script_pubkey) value = self.utxo[0].nValue // 3 @@ -547,41 +511,13 @@ class SegWitTest(BitcoinTestFramework): # 'block-validation-failed' (if script check threads > 1) or # 'non-mandatory-script-verify-flag (Witness program was passed an # empty witness)' (otherwise). - # TODO: support multiple acceptable reject reasons. - test_witness_block(self.nodes[0], self.test_node, block, accepted=False, with_witness=False) - - self.connect_nodes(0, 2) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, with_witness=False, + reason='non-mandatory-script-verify-flag (Witness program was passed an empty witness)') self.utxo.pop(0) self.utxo.append(UTXO(txid, 2, value)) - @subtest # type: ignore - def test_getblocktemplate_before_lockin(self): - txid = int(self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1), 16) - - for node in [self.nodes[0], self.nodes[2]]: - gbt_results = node.getblocktemplate({"rules": ["segwit"]}) - if node == self.nodes[2]: - # If this is a non-segwit node, we should not get a witness - # commitment. - assert 'default_witness_commitment' not in gbt_results - else: - # For segwit-aware nodes, check the witness - # commitment is correct. - assert 'default_witness_commitment' in gbt_results - witness_commitment = gbt_results['default_witness_commitment'] - - # Check that default_witness_commitment is present. - witness_root = CBlock.get_merkle_root([ser_uint256(0), - ser_uint256(txid)]) - script = get_witness_script(witness_root, 0) - assert_equal(witness_commitment, script.hex()) - - # Clear out the mempool - self.nodes[0].generate(1) - self.sync_blocks() - - @subtest # type: ignore + @subtest def test_witness_tx_relay_before_segwit_activation(self): # Generate a transaction that doesn't require a witness, but send it @@ -604,7 +540,7 @@ class SegWitTest(BitcoinTestFramework): # Since we haven't delivered the tx yet, inv'ing the same tx from # a witness transaction ought not result in a getdata. - self.test_node.announce_tx_and_wait_for_getdata(tx, timeout=2, success=False) + self.test_node.announce_tx_and_wait_for_getdata(tx, success=False) # Delivering this transaction with witness should fail (no matter who # its from) @@ -617,25 +553,22 @@ class SegWitTest(BitcoinTestFramework): test_transaction_acceptance(self.nodes[0], self.test_node, tx, with_witness=False, accepted=True) # Cleanup: mine the first transaction and update utxo - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) assert_equal(len(self.nodes[0].getrawmempool()), 0) self.utxo.pop(0) self.utxo.append(UTXO(tx_hash, 0, tx_value)) - @subtest # type: ignore + @subtest def test_standardness_v0(self): """Test V0 txout standardness. V0 segwit outputs and inputs are always standard. V0 segwit inputs may only be mined after activation, but not before.""" - witness_program = CScript([OP_TRUE]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) - - p2sh_pubkey = hash160(witness_program) - p2sh_script_pubkey = CScript([OP_HASH160, p2sh_pubkey, OP_EQUAL]) + witness_script = CScript([OP_TRUE]) + script_pubkey = script_to_p2wsh_script(witness_script) + p2sh_script_pubkey = script_to_p2sh_script(witness_script) # First prepare a p2sh output (so that spending it will pass standardness) p2sh_tx = CTransaction() @@ -645,13 +578,12 @@ class SegWitTest(BitcoinTestFramework): # Mine it on test_node to create the confirmed output. test_transaction_acceptance(self.nodes[0], self.test_node, p2sh_tx, with_witness=True, accepted=True) - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) # Now test standardness of v0 P2WSH outputs. # Start by creating a transaction with two outputs. tx = CTransaction() - tx.vin = [CTxIn(COutPoint(p2sh_tx.sha256, 0), CScript([witness_program]))] + tx.vin = [CTxIn(COutPoint(p2sh_tx.sha256, 0), CScript([witness_script]))] tx.vout = [CTxOut(p2sh_tx.vout[0].nValue - 10000, script_pubkey)] tx.vout.append(CTxOut(8000, script_pubkey)) # Might burn this later tx.vin[0].nSequence = BIP125_SEQUENCE_NUMBER # Just to have the option to bump this tx from the mempool @@ -662,13 +594,14 @@ class SegWitTest(BitcoinTestFramework): test_transaction_acceptance(self.nodes[1], self.std_node, tx, with_witness=True, accepted=True) # Now create something that looks like a P2PKH output. This won't be spendable. + witness_hash = sha256(witness_script) script_pubkey = CScript([OP_0, hash160(witness_hash)]) tx2 = CTransaction() # tx was accepted, so we spend the second output. tx2.vin = [CTxIn(COutPoint(tx.sha256, 1), b"")] tx2.vout = [CTxOut(7000, script_pubkey)] tx2.wit.vtxinwit.append(CTxInWitness()) - tx2.wit.vtxinwit[0].scriptWitness.stack = [witness_program] + tx2.wit.vtxinwit[0].scriptWitness.stack = [witness_script] tx2.rehash() test_transaction_acceptance(self.nodes[1], self.std_node, tx2, with_witness=True, accepted=True) @@ -681,7 +614,7 @@ class SegWitTest(BitcoinTestFramework): tx3.vin = [CTxIn(COutPoint(tx.sha256, 0), b"")] tx3.vout = [CTxOut(tx.vout[0].nValue - 1000, CScript([OP_TRUE, OP_DROP] * 15 + [OP_TRUE]))] tx3.wit.vtxinwit.append(CTxInWitness()) - tx3.wit.vtxinwit[0].scriptWitness.stack = [witness_program] + tx3.wit.vtxinwit[0].scriptWitness.stack = [witness_script] tx3.rehash() if not self.segwit_active: # Just check mempool acceptance, but don't add the transaction to the mempool, since witness is disallowed @@ -717,33 +650,30 @@ class SegWitTest(BitcoinTestFramework): ) test_transaction_acceptance(self.nodes[0], self.test_node, tx3, with_witness=True, accepted=True) - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) self.utxo.pop(0) self.utxo.append(UTXO(tx3.sha256, 0, tx3.vout[0].nValue)) assert_equal(len(self.nodes[1].getrawmempool()), 0) - @subtest # type: ignore + @subtest def advance_to_segwit_active(self): """Mine enough blocks to activate segwit.""" assert not softfork_active(self.nodes[0], 'segwit') height = self.nodes[0].getblockcount() - self.nodes[0].generate(SEGWIT_HEIGHT - height - 2) + self.generate(self.nodes[0], SEGWIT_HEIGHT - height - 2) assert not softfork_active(self.nodes[0], 'segwit') - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) assert softfork_active(self.nodes[0], 'segwit') self.segwit_active = True - @subtest # type: ignore + @subtest def test_p2sh_witness(self): """Test P2SH wrapped witness programs.""" # Prepare the p2sh-wrapped witness output - witness_program = CScript([OP_DROP, OP_TRUE]) - witness_hash = sha256(witness_program) - p2wsh_pubkey = CScript([OP_0, witness_hash]) - p2sh_witness_hash = hash160(p2wsh_pubkey) - script_pubkey = CScript([OP_HASH160, p2sh_witness_hash, OP_EQUAL]) + witness_script = CScript([OP_DROP, OP_TRUE]) + p2wsh_pubkey = script_to_p2wsh_script(witness_script) + script_pubkey = script_to_p2sh_script(p2wsh_pubkey) script_sig = CScript([p2wsh_pubkey]) # a push of the redeem script # Fund the P2SH output @@ -786,7 +716,7 @@ class SegWitTest(BitcoinTestFramework): spend_tx.vin[0].scriptSig = script_sig spend_tx.rehash() spend_tx.wit.vtxinwit.append(CTxInWitness()) - spend_tx.wit.vtxinwit[0].scriptWitness.stack = [b'a', witness_program] + spend_tx.wit.vtxinwit[0].scriptWitness.stack = [b'a', witness_script] # Verify mempool acceptance test_transaction_acceptance(self.nodes[0], self.test_node, spend_tx, with_witness=True, accepted=True) @@ -803,7 +733,7 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop(0) self.utxo.append(UTXO(spend_tx.sha256, 0, spend_tx.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_witness_commitments(self): """Test witness commitments. @@ -835,19 +765,18 @@ class SegWitTest(BitcoinTestFramework): tx = CTransaction() tx.vin.append(CTxIn(COutPoint(self.utxo[0].sha256, self.utxo[0].n), b"")) - # Let's construct a witness program - witness_program = CScript([OP_TRUE]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) + # Let's construct a witness script + witness_script = CScript([OP_TRUE]) + script_pubkey = script_to_p2wsh_script(witness_script) tx.vout.append(CTxOut(self.utxo[0].nValue - 1000, script_pubkey)) tx.rehash() # tx2 will spend tx1, and send back to a regular anyone-can-spend address tx2 = CTransaction() tx2.vin.append(CTxIn(COutPoint(tx.sha256, 0), b"")) - tx2.vout.append(CTxOut(tx.vout[0].nValue - 1000, witness_program)) + tx2.vout.append(CTxOut(tx.vout[0].nValue - 1000, witness_script)) tx2.wit.vtxinwit.append(CTxInWitness()) - tx2.wit.vtxinwit[0].scriptWitness.stack = [witness_program] + tx2.wit.vtxinwit[0].scriptWitness.stack = [witness_script] tx2.rehash() block_3 = self.build_next_block() @@ -858,10 +787,9 @@ class SegWitTest(BitcoinTestFramework): block_3.vtx[0].vout.append(CTxOut(0, CScript([OP_RETURN, WITNESS_COMMITMENT_HEADER + ser_uint256(2), 10]))) block_3.vtx[0].rehash() block_3.hashMerkleRoot = block_3.calc_merkle_root() - block_3.rehash() block_3.solve() - test_witness_block(self.nodes[0], self.test_node, block_3, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block_3, accepted=False, reason='bad-witness-merkle-match') # Add a different commitment with different nonce, but in the # right location, and with some funds burned(!). @@ -872,7 +800,6 @@ class SegWitTest(BitcoinTestFramework): block_3.vtx[0].vout[-1].nValue += 1 block_3.vtx[0].rehash() block_3.hashMerkleRoot = block_3.calc_merkle_root() - block_3.rehash() assert len(block_3.vtx[0].vout) == 4 # 3 OP_returns block_3.solve() test_witness_block(self.nodes[0], self.test_node, block_3, accepted=True) @@ -882,7 +809,7 @@ class SegWitTest(BitcoinTestFramework): block_4 = self.build_next_block() tx3 = CTransaction() tx3.vin.append(CTxIn(COutPoint(tx2.sha256, 0), b"")) - tx3.vout.append(CTxOut(tx.vout[0].nValue - 1000, witness_program)) + tx3.vout.append(CTxOut(tx.vout[0].nValue - 1000, witness_script)) tx3.rehash() block_4.vtx.append(tx3) block_4.hashMerkleRoot = block_4.calc_merkle_root() @@ -893,7 +820,7 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop(0) self.utxo.append(UTXO(tx3.sha256, 0, tx3.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_block_malleability(self): # Make sure that a block that has too big a virtual size @@ -904,7 +831,7 @@ class SegWitTest(BitcoinTestFramework): block.solve() block.vtx[0].wit.vtxinwit[0].scriptWitness.stack.append(b'a' * 5000000) - assert get_virtual_size(block) > MAX_BLOCK_BASE_SIZE + assert block.get_weight() > MAX_BLOCK_WEIGHT # We can't send over the p2p network, because this is too big to relay # TODO: repeat this test with a block that can be relayed @@ -913,7 +840,7 @@ class SegWitTest(BitcoinTestFramework): assert self.nodes[0].getbestblockhash() != block.hash block.vtx[0].wit.vtxinwit[0].scriptWitness.stack.pop() - assert get_virtual_size(block) < MAX_BLOCK_BASE_SIZE + assert block.get_weight() < MAX_BLOCK_WEIGHT assert_equal(None, self.nodes[0].submitblock(block.serialize().hex())) assert self.nodes[0].getbestblockhash() == block.hash @@ -927,16 +854,16 @@ class SegWitTest(BitcoinTestFramework): # Change the nonce -- should not cause the block to be permanently # failed block.vtx[0].wit.vtxinwit[0].scriptWitness.stack = [ser_uint256(1)] - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, reason='bad-witness-merkle-match') # Changing the witness reserved value doesn't change the block hash block.vtx[0].wit.vtxinwit[0].scriptWitness.stack = [ser_uint256(0)] test_witness_block(self.nodes[0], self.test_node, block, accepted=True) - @subtest # type: ignore + @subtest def test_witness_block_size(self): # TODO: Test that non-witness carrying blocks can't exceed 1MB - # Skipping this test for now; this is covered in p2p-fullblocktest.py + # Skipping this test for now; this is covered in feature_block.py # Test that witness-bearing blocks are limited at ceil(base + wit/4) <= 1MB. block = self.build_next_block() @@ -944,15 +871,14 @@ class SegWitTest(BitcoinTestFramework): assert len(self.utxo) > 0 # Create a P2WSH transaction. - # The witness program will be a bunch of OP_2DROP's, followed by OP_TRUE. + # The witness script will be a bunch of OP_2DROP's, followed by OP_TRUE. # This should give us plenty of room to tweak the spending tx's # virtual size. NUM_DROPS = 200 # 201 max ops per script! NUM_OUTPUTS = 50 - witness_program = CScript([OP_2DROP] * NUM_DROPS + [OP_TRUE]) - witness_hash = uint256_from_str(sha256(witness_program)) - script_pubkey = CScript([OP_0, ser_uint256(witness_hash)]) + witness_script = CScript([OP_2DROP] * NUM_DROPS + [OP_TRUE]) + script_pubkey = script_to_p2wsh_script(witness_script) prevout = COutPoint(self.utxo[0].sha256, self.utxo[0].n) value = self.utxo[0].nValue @@ -972,15 +898,14 @@ class SegWitTest(BitcoinTestFramework): child_tx.vout = [CTxOut(value - 100000, CScript([OP_TRUE]))] for _ in range(NUM_OUTPUTS): child_tx.wit.vtxinwit.append(CTxInWitness()) - child_tx.wit.vtxinwit[-1].scriptWitness.stack = [b'a' * 195] * (2 * NUM_DROPS) + [witness_program] + child_tx.wit.vtxinwit[-1].scriptWitness.stack = [b'a' * 195] * (2 * NUM_DROPS) + [witness_script] child_tx.rehash() self.update_witness_block_with_transactions(block, [parent_tx, child_tx]) - vsize = get_virtual_size(block) - additional_bytes = (MAX_BLOCK_BASE_SIZE - vsize) * 4 + additional_bytes = MAX_BLOCK_WEIGHT - block.get_weight() i = 0 while additional_bytes > 0: - # Add some more bytes to each input until we hit MAX_BLOCK_BASE_SIZE+1 + # Add some more bytes to each input until we hit MAX_BLOCK_WEIGHT+1 extra_bytes = min(additional_bytes + 1, 55) block.vtx[-1].wit.vtxinwit[int(i / (2 * NUM_DROPS))].scriptWitness.stack[i % (2 * NUM_DROPS)] = b'a' * (195 + extra_bytes) additional_bytes -= extra_bytes @@ -989,13 +914,12 @@ class SegWitTest(BitcoinTestFramework): block.vtx[0].vout.pop() # Remove old commitment add_witness_commitment(block) block.solve() - vsize = get_virtual_size(block) - assert_equal(vsize, MAX_BLOCK_BASE_SIZE + 1) + assert_equal(block.get_weight(), MAX_BLOCK_WEIGHT + 1) # Make sure that our test case would exceed the old max-network-message # limit assert len(block.serialize()) > 2 * 1024 * 1024 - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, reason='bad-blk-weight') # Now resize the second transaction to make the block fit. cur_length = len(block.vtx[-1].wit.vtxinwit[0].scriptWitness.stack[0]) @@ -1003,7 +927,7 @@ class SegWitTest(BitcoinTestFramework): block.vtx[0].vout.pop() add_witness_commitment(block) block.solve() - assert get_virtual_size(block) == MAX_BLOCK_BASE_SIZE + assert block.get_weight() == MAX_BLOCK_WEIGHT test_witness_block(self.nodes[0], self.test_node, block, accepted=True) @@ -1011,7 +935,7 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop(0) self.utxo.append(UTXO(block.vtx[-1].sha256, 0, block.vtx[-1].vout[0].nValue)) - @subtest # type: ignore + @subtest def test_submit_block(self): """Test that submitblock adds the nonce automatically when possible.""" block = self.build_next_block() @@ -1047,15 +971,14 @@ class SegWitTest(BitcoinTestFramework): # Tip should not advance! assert self.nodes[0].getbestblockhash() != block_2.hash - @subtest # type: ignore + @subtest def test_extra_witness_data(self): """Test extra witness data in a transaction.""" block = self.build_next_block() - witness_program = CScript([OP_DROP, OP_TRUE]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) + witness_script = CScript([OP_DROP, OP_TRUE]) + script_pubkey = script_to_p2wsh_script(witness_script) # First try extra witness data on a tx that doesn't require a witness tx = CTransaction() @@ -1068,7 +991,8 @@ class SegWitTest(BitcoinTestFramework): self.update_witness_block_with_transactions(block, [tx]) # Extra witness data should not be allowed. - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Witness provided for non-witness script)') # Try extra signature data. Ok if we're not spending a witness output. block.vtx[1].wit.vtxinwit = [] @@ -1086,14 +1010,15 @@ class SegWitTest(BitcoinTestFramework): tx2.vin.append(CTxIn(COutPoint(tx.sha256, 1), b"")) # non-witness tx2.vout.append(CTxOut(tx.vout[0].nValue, CScript([OP_TRUE]))) tx2.wit.vtxinwit.extend([CTxInWitness(), CTxInWitness()]) - tx2.wit.vtxinwit[0].scriptWitness.stack = [CScript([CScriptNum(1)]), CScript([CScriptNum(1)]), witness_program] + tx2.wit.vtxinwit[0].scriptWitness.stack = [CScript([CScriptNum(1)]), CScript([CScriptNum(1)]), witness_script] tx2.wit.vtxinwit[1].scriptWitness.stack = [CScript([OP_TRUE])] block = self.build_next_block() self.update_witness_block_with_transactions(block, [tx2]) # This has extra witness data, so it should fail. - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Stack size must be exactly one after execution)') # Now get rid of the extra witness, but add extra scriptSig data tx2.vin[0].scriptSig = CScript([OP_TRUE]) @@ -1105,7 +1030,8 @@ class SegWitTest(BitcoinTestFramework): block.solve() # This has extra signature data for a witness input, so it should fail. - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Witness requires empty scriptSig)') # Now get rid of the extra scriptsig on the witness input, and verify # success (even with extra scriptsig data in the non-witness input) @@ -1120,15 +1046,14 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop(0) self.utxo.append(UTXO(tx2.sha256, 0, tx2.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_max_witness_push_length(self): """Test that witness stack can only allow up to 520 byte pushes.""" block = self.build_next_block() - witness_program = CScript([OP_DROP, OP_TRUE]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) + witness_script = CScript([OP_DROP, OP_TRUE]) + script_pubkey = script_to_p2wsh_script(witness_script) tx = CTransaction() tx.vin.append(CTxIn(COutPoint(self.utxo[0].sha256, self.utxo[0].n), b"")) @@ -1140,11 +1065,12 @@ class SegWitTest(BitcoinTestFramework): tx2.vout.append(CTxOut(tx.vout[0].nValue - 1000, CScript([OP_TRUE]))) tx2.wit.vtxinwit.append(CTxInWitness()) # First try a 521-byte stack element - tx2.wit.vtxinwit[0].scriptWitness.stack = [b'a' * (MAX_SCRIPT_ELEMENT_SIZE + 1), witness_program] + tx2.wit.vtxinwit[0].scriptWitness.stack = [b'a' * (MAX_SCRIPT_ELEMENT_SIZE + 1), witness_script] tx2.rehash() self.update_witness_block_with_transactions(block, [tx, tx2]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Push value size limit exceeded)') # Now reduce the length of the stack element tx2.wit.vtxinwit[0].scriptWitness.stack[0] = b'a' * (MAX_SCRIPT_ELEMENT_SIZE) @@ -1157,17 +1083,16 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop() self.utxo.append(UTXO(tx2.sha256, 0, tx2.vout[0].nValue)) - @subtest # type: ignore - def test_max_witness_program_length(self): + @subtest + def test_max_witness_script_length(self): """Test that witness outputs greater than 10kB can't be spent.""" - MAX_PROGRAM_LENGTH = 10000 + MAX_WITNESS_SCRIPT_LENGTH = 10000 - # This program is 19 max pushes (9937 bytes), then 64 more opcode-bytes. - long_witness_program = CScript([b'a' * MAX_SCRIPT_ELEMENT_SIZE] * 19 + [OP_DROP] * 63 + [OP_TRUE]) - assert len(long_witness_program) == MAX_PROGRAM_LENGTH + 1 - long_witness_hash = sha256(long_witness_program) - long_script_pubkey = CScript([OP_0, long_witness_hash]) + # This script is 19 max pushes (9937 bytes), then 64 more opcode-bytes. + long_witness_script = CScript([b'a' * MAX_SCRIPT_ELEMENT_SIZE] * 19 + [OP_DROP] * 63 + [OP_TRUE]) + assert len(long_witness_script) == MAX_WITNESS_SCRIPT_LENGTH + 1 + long_script_pubkey = script_to_p2wsh_script(long_witness_script) block = self.build_next_block() @@ -1180,23 +1105,23 @@ class SegWitTest(BitcoinTestFramework): tx2.vin.append(CTxIn(COutPoint(tx.sha256, 0), b"")) tx2.vout.append(CTxOut(tx.vout[0].nValue - 1000, CScript([OP_TRUE]))) tx2.wit.vtxinwit.append(CTxInWitness()) - tx2.wit.vtxinwit[0].scriptWitness.stack = [b'a'] * 44 + [long_witness_program] + tx2.wit.vtxinwit[0].scriptWitness.stack = [b'a'] * 44 + [long_witness_script] tx2.rehash() self.update_witness_block_with_transactions(block, [tx, tx2]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Script is too big)') - # Try again with one less byte in the witness program - witness_program = CScript([b'a' * MAX_SCRIPT_ELEMENT_SIZE] * 19 + [OP_DROP] * 62 + [OP_TRUE]) - assert len(witness_program) == MAX_PROGRAM_LENGTH - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) + # Try again with one less byte in the witness script + witness_script = CScript([b'a' * MAX_SCRIPT_ELEMENT_SIZE] * 19 + [OP_DROP] * 62 + [OP_TRUE]) + assert len(witness_script) == MAX_WITNESS_SCRIPT_LENGTH + script_pubkey = script_to_p2wsh_script(witness_script) tx.vout[0] = CTxOut(tx.vout[0].nValue, script_pubkey) tx.rehash() tx2.vin[0].prevout.hash = tx.sha256 - tx2.wit.vtxinwit[0].scriptWitness.stack = [b'a'] * 43 + [witness_program] + tx2.wit.vtxinwit[0].scriptWitness.stack = [b'a'] * 43 + [witness_script] tx2.rehash() block.vtx = [block.vtx[0]] self.update_witness_block_with_transactions(block, [tx, tx2]) @@ -1205,13 +1130,12 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop() self.utxo.append(UTXO(tx2.sha256, 0, tx2.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_witness_input_length(self): """Test that vin length must match vtxinwit length.""" - witness_program = CScript([OP_DROP, OP_TRUE]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) + witness_script = CScript([OP_DROP, OP_TRUE]) + script_pubkey = script_to_p2wsh_script(witness_script) # Create a transaction that splits our utxo into many outputs tx = CTransaction() @@ -1255,11 +1179,11 @@ class SegWitTest(BitcoinTestFramework): # First try using a too long vtxinwit for i in range(11): tx2.wit.vtxinwit.append(CTxInWitness()) - tx2.wit.vtxinwit[i].scriptWitness.stack = [b'a', witness_program] + tx2.wit.vtxinwit[i].scriptWitness.stack = [b'a', witness_script] block = self.build_next_block() self.update_witness_block_with_transactions(block, [tx2]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, reason='bad-txnmrklroot') # Now try using a too short vtxinwit tx2.wit.vtxinwit.pop() @@ -1267,19 +1191,22 @@ class SegWitTest(BitcoinTestFramework): block.vtx = [block.vtx[0]] self.update_witness_block_with_transactions(block, [tx2]) + # This block doesn't result in a specific reject reason, but an iostream exception: + # "Exception 'CDataStream::read(): end of data: unspecified iostream_category error' (...) caught" test_witness_block(self.nodes[0], self.test_node, block, accepted=False) # Now make one of the intermediate witnesses be incorrect tx2.wit.vtxinwit.append(CTxInWitness()) - tx2.wit.vtxinwit[-1].scriptWitness.stack = [b'a', witness_program] - tx2.wit.vtxinwit[5].scriptWitness.stack = [witness_program] + tx2.wit.vtxinwit[-1].scriptWitness.stack = [b'a', witness_script] + tx2.wit.vtxinwit[5].scriptWitness.stack = [witness_script] block.vtx = [block.vtx[0]] self.update_witness_block_with_transactions(block, [tx2]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Operation not valid with the current stack size)') # Fix the broken witness and the block should be accepted. - tx2.wit.vtxinwit[5].scriptWitness.stack = [b'a', witness_program] + tx2.wit.vtxinwit[5].scriptWitness.stack = [b'a', witness_script] block.vtx = [block.vtx[0]] self.update_witness_block_with_transactions(block, [tx2]) test_witness_block(self.nodes[0], self.test_node, block, accepted=True) @@ -1287,7 +1214,7 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop() self.utxo.append(UTXO(tx2.sha256, 0, tx2.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_tx_relay_after_segwit_activation(self): """Test transaction relay after segwit activation. @@ -1317,9 +1244,8 @@ class SegWitTest(BitcoinTestFramework): test_transaction_acceptance(self.nodes[0], self.test_node, tx, with_witness=False, accepted=True) # Now try to add extra witness data to a valid witness tx. - witness_program = CScript([OP_TRUE]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) + witness_script = CScript([OP_TRUE]) + script_pubkey = script_to_p2wsh_script(witness_script) tx2 = CTransaction() tx2.vin.append(CTxIn(COutPoint(tx_hash, 0), b"")) tx2.vout.append(CTxOut(tx.vout[0].nValue - 1000, script_pubkey)) @@ -1330,11 +1256,10 @@ class SegWitTest(BitcoinTestFramework): tx3.wit.vtxinwit.append(CTxInWitness()) # Add too-large for IsStandard witness and check that it does not enter reject filter - p2sh_program = CScript([OP_TRUE]) - p2sh_pubkey = hash160(p2sh_program) - witness_program2 = CScript([b'a' * 400000]) - tx3.vout.append(CTxOut(tx2.vout[0].nValue - 1000, CScript([OP_HASH160, p2sh_pubkey, OP_EQUAL]))) - tx3.wit.vtxinwit[0].scriptWitness.stack = [witness_program2] + p2sh_script = CScript([OP_TRUE]) + witness_script2 = CScript([b'a' * 400000]) + tx3.vout.append(CTxOut(tx2.vout[0].nValue - 1000, script_to_p2sh_script(p2sh_script))) + tx3.wit.vtxinwit[0].scriptWitness.stack = [witness_script2] tx3.rehash() # Node will not be blinded to the transaction, requesting it any number of times @@ -1348,14 +1273,14 @@ class SegWitTest(BitcoinTestFramework): # Remove witness stuffing, instead add extra witness push on stack tx3.vout[0] = CTxOut(tx2.vout[0].nValue - 1000, CScript([OP_TRUE, OP_DROP] * 15 + [OP_TRUE])) - tx3.wit.vtxinwit[0].scriptWitness.stack = [CScript([CScriptNum(1)]), witness_program] + tx3.wit.vtxinwit[0].scriptWitness.stack = [CScript([CScriptNum(1)]), witness_script] tx3.rehash() test_transaction_acceptance(self.nodes[0], self.test_node, tx2, with_witness=True, accepted=True) test_transaction_acceptance(self.nodes[0], self.test_node, tx3, with_witness=True, accepted=False) # Get rid of the extra witness, and verify acceptance. - tx3.wit.vtxinwit[0].scriptWitness.stack = [witness_program] + tx3.wit.vtxinwit[0].scriptWitness.stack = [witness_script] # Also check that old_node gets a tx announcement, even though this is # a witness transaction. self.old_node.wait_for_inv([CInv(MSG_TX, tx2.sha256)]) # wait until tx2 was inv'ed @@ -1367,22 +1292,21 @@ class SegWitTest(BitcoinTestFramework): raw_tx = self.nodes[0].getrawtransaction(tx3.hash, 1) assert_equal(int(raw_tx["hash"], 16), tx3.calc_sha256(True)) assert_equal(raw_tx["size"], len(tx3.serialize_with_witness())) - weight = len(tx3.serialize_with_witness()) + 3 * len(tx3.serialize_without_witness()) - vsize = math.ceil(weight / 4) + vsize = tx3.get_vsize() assert_equal(raw_tx["vsize"], vsize) - assert_equal(raw_tx["weight"], weight) + assert_equal(raw_tx["weight"], tx3.get_weight()) assert_equal(len(raw_tx["vin"][0]["txinwitness"]), 1) - assert_equal(raw_tx["vin"][0]["txinwitness"][0], witness_program.hex()) + assert_equal(raw_tx["vin"][0]["txinwitness"][0], witness_script.hex()) assert vsize != raw_tx["size"] # Cleanup: mine the transactions and update utxo for next test - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) assert_equal(len(self.nodes[0].getrawmempool()), 0) self.utxo.pop(0) self.utxo.append(UTXO(tx3.sha256, 0, tx3.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_segwit_versions(self): """Test validity of future segwit version transactions. @@ -1408,8 +1332,8 @@ class SegWitTest(BitcoinTestFramework): self.sync_blocks() temp_utxo = [] tx = CTransaction() - witness_program = CScript([OP_TRUE]) - witness_hash = sha256(witness_program) + witness_script = CScript([OP_TRUE]) + witness_hash = sha256(witness_script) assert_equal(len(self.nodes[1].getrawmempool()), 0) for version in list(range(OP_1, OP_16 + 1)) + [OP_0]: # First try to spend to a future version segwit script_pubkey. @@ -1426,8 +1350,7 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop(0) temp_utxo.append(UTXO(tx.sha256, 0, tx.vout[0].nValue)) - self.nodes[0].generate(1) # Mine all the transactions - self.sync_blocks() + self.generate(self.nodes[0], 1) # Mine all the transactions assert len(self.nodes[0].getrawmempool()) == 0 # Finally, verify that version 0 -> version 2 transactions @@ -1437,7 +1360,7 @@ class SegWitTest(BitcoinTestFramework): tx2.vin = [CTxIn(COutPoint(tx.sha256, 0), b"")] tx2.vout = [CTxOut(tx.vout[0].nValue - 1000, script_pubkey)] tx2.wit.vtxinwit.append(CTxInWitness()) - tx2.wit.vtxinwit[0].scriptWitness.stack = [witness_program] + tx2.wit.vtxinwit[0].scriptWitness.stack = [witness_script] tx2.rehash() # Gets accepted to both policy-enforcing nodes and others. test_transaction_acceptance(self.nodes[0], self.test_node, tx2, with_witness=True, accepted=True) @@ -1452,7 +1375,7 @@ class SegWitTest(BitcoinTestFramework): tx3.vin.append(CTxIn(COutPoint(i.sha256, i.n), b"")) tx3.wit.vtxinwit.append(CTxInWitness()) total_value += i.nValue - tx3.wit.vtxinwit[-1].scriptWitness.stack = [witness_program] + tx3.wit.vtxinwit[-1].scriptWitness.stack = [witness_script] tx3.vout.append(CTxOut(total_value - 1000, script_pubkey)) tx3.rehash() @@ -1461,7 +1384,7 @@ class SegWitTest(BitcoinTestFramework): self.std_node.announce_tx_and_wait_for_getdata(tx3) test_transaction_acceptance(self.nodes[1], self.std_node, tx3, with_witness=True, accepted=False, reason="bad-txns-nonstandard-inputs") # Now the node will no longer ask for getdata of this transaction when advertised by same txid - self.std_node.announce_tx_and_wait_for_getdata(tx3, timeout=5, success=False) + self.std_node.announce_tx_and_wait_for_getdata(tx3, success=False) # Spending a higher version witness output is not allowed by policy, # even with fRequireStandard=false. @@ -1476,14 +1399,13 @@ class SegWitTest(BitcoinTestFramework): # Add utxo to our list self.utxo.append(UTXO(tx3.sha256, 0, tx3.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_premature_coinbase_witness_spend(self): block = self.build_next_block() # Change the output of the block to be a witness output. - witness_program = CScript([OP_TRUE]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) + witness_script = CScript([OP_TRUE]) + script_pubkey = script_to_p2wsh_script(witness_script) block.vtx[0].vout[0].scriptPubKey = script_pubkey # This next line will rehash the coinbase and update the merkle # root, and solve. @@ -1492,26 +1414,25 @@ class SegWitTest(BitcoinTestFramework): spend_tx = CTransaction() spend_tx.vin = [CTxIn(COutPoint(block.vtx[0].sha256, 0), b"")] - spend_tx.vout = [CTxOut(block.vtx[0].vout[0].nValue, witness_program)] + spend_tx.vout = [CTxOut(block.vtx[0].vout[0].nValue, witness_script)] spend_tx.wit.vtxinwit.append(CTxInWitness()) - spend_tx.wit.vtxinwit[0].scriptWitness.stack = [witness_program] + spend_tx.wit.vtxinwit[0].scriptWitness.stack = [witness_script] spend_tx.rehash() # Now test a premature spend. - self.nodes[0].generate(98) - self.sync_blocks() + self.generate(self.nodes[0], 98) block2 = self.build_next_block() self.update_witness_block_with_transactions(block2, [spend_tx]) - test_witness_block(self.nodes[0], self.test_node, block2, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block2, accepted=False, reason='bad-txns-premature-spend-of-coinbase') # Advancing one more block should allow the spend. - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) block2 = self.build_next_block() self.update_witness_block_with_transactions(block2, [spend_tx]) test_witness_block(self.nodes[0], self.test_node, block2, accepted=True) self.sync_blocks() - @subtest # type: ignore + @subtest def test_uncompressed_pubkey(self): """Test uncompressed pubkey validity in segwit transactions. @@ -1530,7 +1451,7 @@ class SegWitTest(BitcoinTestFramework): # Test 1: P2WPKH # First create a P2WPKH output that uses an uncompressed pubkey pubkeyhash = hash160(pubkey) - script_pkh = CScript([OP_0, pubkeyhash]) + script_pkh = key_to_p2wpkh_script(pubkey) tx = CTransaction() tx.vin.append(CTxIn(COutPoint(utxo.sha256, utxo.n), b"")) tx.vout.append(CTxOut(utxo.nValue - 1000, script_pkh)) @@ -1543,14 +1464,13 @@ class SegWitTest(BitcoinTestFramework): # Now try to spend it. Send it to a P2WSH output, which we'll # use in the next test. - witness_program = CScript([pubkey, CScriptOp(OP_CHECKSIG)]) - witness_hash = sha256(witness_program) - script_wsh = CScript([OP_0, witness_hash]) + witness_script = key_to_p2pk_script(pubkey) + script_wsh = script_to_p2wsh_script(witness_script) tx2 = CTransaction() tx2.vin.append(CTxIn(COutPoint(tx.sha256, 0), b"")) tx2.vout.append(CTxOut(tx.vout[0].nValue - 1000, script_wsh)) - script = get_p2pkh_script(pubkeyhash) + script = keyhash_to_p2pkh_script(pubkeyhash) sig_hash = SegwitV0SignatureHash(script, tx2, 0, SIGHASH_ALL, tx.vout[0].nValue) signature = key.sign_ecdsa(sig_hash) + b'\x01' # 0x1 is SIGHASH_ALL tx2.wit.vtxinwit.append(CTxInWitness()) @@ -1567,15 +1487,14 @@ class SegWitTest(BitcoinTestFramework): # Test 2: P2WSH # Try to spend the P2WSH output created in last test. # Send it to a P2SH(P2WSH) output, which we'll use in the next test. - p2sh_witness_hash = hash160(script_wsh) - script_p2sh = CScript([OP_HASH160, p2sh_witness_hash, OP_EQUAL]) + script_p2sh = script_to_p2sh_script(script_wsh) script_sig = CScript([script_wsh]) tx3 = CTransaction() tx3.vin.append(CTxIn(COutPoint(tx2.sha256, 0), b"")) tx3.vout.append(CTxOut(tx2.vout[0].nValue - 1000, script_p2sh)) tx3.wit.vtxinwit.append(CTxInWitness()) - sign_p2pk_witness_input(witness_program, tx3, 0, SIGHASH_ALL, tx2.vout[0].nValue, key) + sign_p2pk_witness_input(witness_script, tx3, 0, SIGHASH_ALL, tx2.vout[0].nValue, key) # Should fail policy test. test_transaction_acceptance(self.nodes[0], self.test_node, tx3, True, False, 'non-mandatory-script-verify-flag (Using non-compressed keys in segwit)') @@ -1587,12 +1506,12 @@ class SegWitTest(BitcoinTestFramework): # Test 3: P2SH(P2WSH) # Try to spend the P2SH output created in the last test. # Send it to a P2PKH output, which we'll use in the next test. - script_pubkey = get_p2pkh_script(pubkeyhash) + script_pubkey = keyhash_to_p2pkh_script(pubkeyhash) tx4 = CTransaction() tx4.vin.append(CTxIn(COutPoint(tx3.sha256, 0), script_sig)) tx4.vout.append(CTxOut(tx3.vout[0].nValue - 1000, script_pubkey)) tx4.wit.vtxinwit.append(CTxInWitness()) - sign_p2pk_witness_input(witness_program, tx4, 0, SIGHASH_ALL, tx3.vout[0].nValue, key) + sign_p2pk_witness_input(witness_script, tx4, 0, SIGHASH_ALL, tx3.vout[0].nValue, key) # Should fail policy test. test_transaction_acceptance(self.nodes[0], self.test_node, tx4, True, False, 'non-mandatory-script-verify-flag (Using non-compressed keys in segwit)') @@ -1616,16 +1535,15 @@ class SegWitTest(BitcoinTestFramework): test_witness_block(self.nodes[0], self.test_node, block, accepted=True) self.utxo.append(UTXO(tx5.sha256, 0, tx5.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_signature_version_1(self): key = ECKey() key.generate() pubkey = key.get_pubkey().get_bytes() - witness_program = CScript([pubkey, CScriptOp(OP_CHECKSIG)]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) + witness_script = key_to_p2pk_script(pubkey) + script_pubkey = script_to_p2wsh_script(witness_script) # First create a witness output for use in the tests. tx = CTransaction() @@ -1652,18 +1570,22 @@ class SegWitTest(BitcoinTestFramework): tx.vout.append(CTxOut(prev_utxo.nValue - 1000, script_pubkey)) tx.wit.vtxinwit.append(CTxInWitness()) # Too-large input value - sign_p2pk_witness_input(witness_program, tx, 0, hashtype, prev_utxo.nValue + 1, key) + sign_p2pk_witness_input(witness_script, tx, 0, hashtype, prev_utxo.nValue + 1, key) self.update_witness_block_with_transactions(block, [tx]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Script evaluated without error ' + 'but finished with a false/empty top stack element') # Too-small input value - sign_p2pk_witness_input(witness_program, tx, 0, hashtype, prev_utxo.nValue - 1, key) + sign_p2pk_witness_input(witness_script, tx, 0, hashtype, prev_utxo.nValue - 1, key) block.vtx.pop() # remove last tx self.update_witness_block_with_transactions(block, [tx]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Script evaluated without error ' + 'but finished with a false/empty top stack element') # Now try correct value - sign_p2pk_witness_input(witness_program, tx, 0, hashtype, prev_utxo.nValue, key) + sign_p2pk_witness_input(witness_script, tx, 0, hashtype, prev_utxo.nValue, key) block.vtx.pop() self.update_witness_block_with_transactions(block, [tx]) test_witness_block(self.nodes[0], self.test_node, block, accepted=True) @@ -1684,7 +1606,7 @@ class SegWitTest(BitcoinTestFramework): for _ in range(NUM_SIGHASH_TESTS): tx.vout.append(CTxOut(split_value, script_pubkey)) tx.wit.vtxinwit.append(CTxInWitness()) - sign_p2pk_witness_input(witness_program, tx, 0, SIGHASH_ALL, prev_utxo.nValue, key) + sign_p2pk_witness_input(witness_script, tx, 0, SIGHASH_ALL, prev_utxo.nValue, key) for i in range(NUM_SIGHASH_TESTS): temp_utxos.append(UTXO(tx.sha256, i, split_value)) @@ -1719,7 +1641,7 @@ class SegWitTest(BitcoinTestFramework): if random.randint(0, 1): anyonecanpay = SIGHASH_ANYONECANPAY hashtype = random.randint(1, 3) | anyonecanpay - sign_p2pk_witness_input(witness_program, tx, i, hashtype, temp_utxos[i].nValue, key) + sign_p2pk_witness_input(witness_script, tx, i, hashtype, temp_utxos[i].nValue, key) if (hashtype == SIGHASH_SINGLE and i >= num_outputs): used_sighash_single_out_of_bounds = True tx.rehash() @@ -1730,7 +1652,7 @@ class SegWitTest(BitcoinTestFramework): block.vtx.append(tx) # Test the block periodically, if we're close to maxblocksize - if (get_virtual_size(block) > MAX_BLOCK_BASE_SIZE - 1000): + if block.get_weight() > MAX_BLOCK_WEIGHT - 4000: self.update_witness_block_with_transactions(block, []) test_witness_block(self.nodes[0], self.test_node, block, accepted=True) block = self.build_next_block() @@ -1744,17 +1666,17 @@ class SegWitTest(BitcoinTestFramework): # Now test witness version 0 P2PKH transactions pubkeyhash = hash160(pubkey) - script_pkh = CScript([OP_0, pubkeyhash]) + script_pkh = key_to_p2wpkh_script(pubkey) tx = CTransaction() tx.vin.append(CTxIn(COutPoint(temp_utxos[0].sha256, temp_utxos[0].n), b"")) tx.vout.append(CTxOut(temp_utxos[0].nValue, script_pkh)) tx.wit.vtxinwit.append(CTxInWitness()) - sign_p2pk_witness_input(witness_program, tx, 0, SIGHASH_ALL, temp_utxos[0].nValue, key) + sign_p2pk_witness_input(witness_script, tx, 0, SIGHASH_ALL, temp_utxos[0].nValue, key) tx2 = CTransaction() tx2.vin.append(CTxIn(COutPoint(tx.sha256, 0), b"")) tx2.vout.append(CTxOut(tx.vout[0].nValue, CScript([OP_TRUE]))) - script = get_p2pkh_script(pubkeyhash) + script = keyhash_to_p2pkh_script(pubkeyhash) sig_hash = SegwitV0SignatureHash(script, tx2, 0, SIGHASH_ALL, tx.vout[0].nValue) signature = key.sign_ecdsa(sig_hash) + b'\x01' # 0x1 is SIGHASH_ALL @@ -1762,7 +1684,8 @@ class SegWitTest(BitcoinTestFramework): tx2.vin[0].scriptSig = CScript([signature, pubkey]) block = self.build_next_block() self.update_witness_block_with_transactions(block, [tx, tx2]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Witness requires empty scriptSig)') # Move the signature to the witness. block.vtx.pop() @@ -1789,7 +1712,7 @@ class SegWitTest(BitcoinTestFramework): # the signatures as we go. tx.vin.append(CTxIn(COutPoint(i.sha256, i.n), b"")) tx.wit.vtxinwit.append(CTxInWitness()) - sign_p2pk_witness_input(witness_program, tx, index, SIGHASH_ALL | SIGHASH_ANYONECANPAY, i.nValue, key) + sign_p2pk_witness_input(witness_script, tx, index, SIGHASH_ALL | SIGHASH_ANYONECANPAY, i.nValue, key) index += 1 block = self.build_next_block() self.update_witness_block_with_transactions(block, [tx]) @@ -1798,7 +1721,7 @@ class SegWitTest(BitcoinTestFramework): for i in range(len(tx.vout)): self.utxo.append(UTXO(tx.sha256, i, tx.vout[i].nValue)) - @subtest # type: ignore + @subtest def test_non_standard_witness_blinding(self): """Test behavior of unnecessary witnesses in transactions does not blind the node for the transaction""" @@ -1806,8 +1729,7 @@ class SegWitTest(BitcoinTestFramework): # rules (an anyone-can-spend OP_TRUE would be rejected, if not wrapped # in P2SH). p2sh_program = CScript([OP_TRUE]) - p2sh_pubkey = hash160(p2sh_program) - script_pubkey = CScript([OP_HASH160, p2sh_pubkey, OP_EQUAL]) + script_pubkey = script_to_p2sh_script(p2sh_program) # Now check that unnecessary witnesses can't be used to blind a node # to a transaction, eg by violating standardness checks. @@ -1816,8 +1738,7 @@ class SegWitTest(BitcoinTestFramework): tx.vout.append(CTxOut(self.utxo[0].nValue - 1000, script_pubkey)) tx.rehash() test_transaction_acceptance(self.nodes[0], self.test_node, tx, False, True) - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) # We'll add an unnecessary witness to this transaction that would cause # it to be non-standard, to test that violating policy with a witness @@ -1845,14 +1766,13 @@ class SegWitTest(BitcoinTestFramework): test_transaction_acceptance(self.nodes[0], self.test_node, tx2, False, True) test_transaction_acceptance(self.nodes[0], self.test_node, tx3, False, True) - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) # Update our utxo list; we spent the first entry. self.utxo.pop(0) self.utxo.append(UTXO(tx3.sha256, 0, tx3.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_non_standard_witness(self): """Test detection of non-standard P2WSH witness""" pad = chr(1).encode('latin-1') @@ -1872,17 +1792,15 @@ class SegWitTest(BitcoinTestFramework): # For each script, generate a pair of P2WSH and P2SH-P2WSH output. outputvalue = (self.utxo[0].nValue - 1000) // (len(scripts) * 2) for i in scripts: - p2wsh = CScript([OP_0, sha256(i)]) - p2sh = hash160(p2wsh) + p2wsh = script_to_p2wsh_script(i) p2wsh_scripts.append(p2wsh) tx.vout.append(CTxOut(outputvalue, p2wsh)) - tx.vout.append(CTxOut(outputvalue, CScript([OP_HASH160, p2sh, OP_EQUAL]))) + tx.vout.append(CTxOut(outputvalue, script_to_p2sh_script(p2wsh))) tx.rehash() txid = tx.sha256 test_transaction_acceptance(self.nodes[0], self.test_node, tx, with_witness=False, accepted=True) - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) # Creating transactions for tests p2wsh_txs = [] @@ -1890,13 +1808,13 @@ class SegWitTest(BitcoinTestFramework): for i in range(len(scripts)): p2wsh_tx = CTransaction() p2wsh_tx.vin.append(CTxIn(COutPoint(txid, i * 2))) - p2wsh_tx.vout.append(CTxOut(outputvalue - 5000, CScript([OP_0, hash160(hex_str_to_bytes(""))]))) + p2wsh_tx.vout.append(CTxOut(outputvalue - 5000, CScript([OP_0, hash160(b"")]))) p2wsh_tx.wit.vtxinwit.append(CTxInWitness()) p2wsh_tx.rehash() p2wsh_txs.append(p2wsh_tx) p2sh_tx = CTransaction() p2sh_tx.vin.append(CTxIn(COutPoint(txid, i * 2 + 1), CScript([p2wsh_scripts[i]]))) - p2sh_tx.vout.append(CTxOut(outputvalue - 5000, CScript([OP_0, hash160(hex_str_to_bytes(""))]))) + p2sh_tx.vout.append(CTxOut(outputvalue - 5000, CScript([OP_0, hash160(b"")]))) p2sh_tx.wit.vtxinwit.append(CTxInWitness()) p2sh_tx.rehash() p2sh_txs.append(p2sh_tx) @@ -1944,7 +1862,7 @@ class SegWitTest(BitcoinTestFramework): test_transaction_acceptance(self.nodes[1], self.std_node, p2sh_txs[3], True, False, 'bad-witness-nonstandard') test_transaction_acceptance(self.nodes[0], self.test_node, p2sh_txs[3], True, True) - self.nodes[0].generate(1) # Mine and clean up the mempool of non-standard node + self.generate(self.nodes[0], 1) # Mine and clean up the mempool of non-standard node # Valid but non-standard transactions in a block should be accepted by standard node self.sync_blocks() assert_equal(len(self.nodes[0].getrawmempool()), 0) @@ -1952,35 +1870,13 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop(0) - @subtest # type: ignore - def test_upgrade_after_activation(self): - """Test the behavior of starting up a segwit-aware node after the softfork has activated.""" - - self.restart_node(2, extra_args=["-segwitheight={}".format(SEGWIT_HEIGHT)]) - self.connect_nodes(0, 2) - - # We reconnect more than 100 blocks, give it plenty of time - self.sync_blocks(timeout=240) - - # Make sure that this peer thinks segwit has activated. - assert softfork_active(self.nodes[2], 'segwit') - - # Make sure this peer's blocks match those of node0. - height = self.nodes[2].getblockcount() - while height >= 0: - block_hash = self.nodes[2].getblockhash(height) - assert_equal(block_hash, self.nodes[0].getblockhash(height)) - assert_equal(self.nodes[0].getblock(block_hash), self.nodes[2].getblock(block_hash)) - height -= 1 - - @subtest # type: ignore + @subtest def test_witness_sigops(self): """Test sigop counting is correct inside witnesses.""" # Keep this under MAX_OPS_PER_SCRIPT (201) - witness_program = CScript([OP_TRUE, OP_IF, OP_TRUE, OP_ELSE] + [OP_CHECKMULTISIG] * 5 + [OP_CHECKSIG] * 193 + [OP_ENDIF]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) + witness_script = CScript([OP_TRUE, OP_IF, OP_TRUE, OP_ELSE] + [OP_CHECKMULTISIG] * 5 + [OP_CHECKSIG] * 193 + [OP_ENDIF]) + script_pubkey = script_to_p2wsh_script(witness_script) sigops_per_script = 20 * 5 + 193 * 1 # We'll produce 2 extra outputs, one with a program that would take us @@ -1995,15 +1891,13 @@ class SegWitTest(BitcoinTestFramework): # This script, when spent with the first # N(=MAX_SIGOP_COST//sigops_per_script) outputs of our transaction, # would push us just over the block sigop limit. - witness_program_toomany = CScript([OP_TRUE, OP_IF, OP_TRUE, OP_ELSE] + [OP_CHECKSIG] * (extra_sigops_available + 1) + [OP_ENDIF]) - witness_hash_toomany = sha256(witness_program_toomany) - script_pubkey_toomany = CScript([OP_0, witness_hash_toomany]) + witness_script_toomany = CScript([OP_TRUE, OP_IF, OP_TRUE, OP_ELSE] + [OP_CHECKSIG] * (extra_sigops_available + 1) + [OP_ENDIF]) + script_pubkey_toomany = script_to_p2wsh_script(witness_script_toomany) # If we spend this script instead, we would exactly reach our sigop # limit (for witness sigops). - witness_program_justright = CScript([OP_TRUE, OP_IF, OP_TRUE, OP_ELSE] + [OP_CHECKSIG] * (extra_sigops_available) + [OP_ENDIF]) - witness_hash_justright = sha256(witness_program_justright) - script_pubkey_justright = CScript([OP_0, witness_hash_justright]) + witness_script_justright = CScript([OP_TRUE, OP_IF, OP_TRUE, OP_ELSE] + [OP_CHECKSIG] * (extra_sigops_available) + [OP_ENDIF]) + script_pubkey_justright = script_to_p2wsh_script(witness_script_justright) # First split our available utxo into a bunch of outputs split_value = self.utxo[0].nValue // outputs @@ -2026,15 +1920,15 @@ class SegWitTest(BitcoinTestFramework): for i in range(outputs - 1): tx2.vin.append(CTxIn(COutPoint(tx.sha256, i), b"")) tx2.wit.vtxinwit.append(CTxInWitness()) - tx2.wit.vtxinwit[-1].scriptWitness.stack = [witness_program] + tx2.wit.vtxinwit[-1].scriptWitness.stack = [witness_script] total_value += tx.vout[i].nValue - tx2.wit.vtxinwit[-1].scriptWitness.stack = [witness_program_toomany] + tx2.wit.vtxinwit[-1].scriptWitness.stack = [witness_script_toomany] tx2.vout.append(CTxOut(total_value, CScript([OP_TRUE]))) tx2.rehash() block_2 = self.build_next_block() self.update_witness_block_with_transactions(block_2, [tx2]) - test_witness_block(self.nodes[0], self.test_node, block_2, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block_2, accepted=False, reason='bad-blk-sigops') # Try dropping the last input in tx2, and add an output that has # too many sigops (contributing to legacy sigop count). @@ -2047,7 +1941,7 @@ class SegWitTest(BitcoinTestFramework): tx2.rehash() block_3 = self.build_next_block() self.update_witness_block_with_transactions(block_3, [tx2]) - test_witness_block(self.nodes[0], self.test_node, block_3, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block_3, accepted=False, reason='bad-blk-sigops') # If we drop the last checksig in this output, the tx should succeed. block_4 = self.build_next_block() @@ -2067,7 +1961,7 @@ class SegWitTest(BitcoinTestFramework): tx2.vout.pop() tx2.vin.append(CTxIn(COutPoint(tx.sha256, outputs - 1), b"")) tx2.wit.vtxinwit.append(CTxInWitness()) - tx2.wit.vtxinwit[-1].scriptWitness.stack = [witness_program_justright] + tx2.wit.vtxinwit[-1].scriptWitness.stack = [witness_script_justright] tx2.rehash() self.update_witness_block_with_transactions(block_5, [tx2]) test_witness_block(self.nodes[0], self.test_node, block_5, accepted=True) @@ -2078,7 +1972,7 @@ class SegWitTest(BitcoinTestFramework): self.utxo.pop(0) self.utxo.append(UTXO(tx2.sha256, 0, tx2.vout[0].nValue)) - @subtest # type: ignore + @subtest def test_superfluous_witness(self): # Serialization of tx that puts witness flag to 3 always def serialize_with_bogus_witness(tx): @@ -2106,27 +2000,27 @@ class SegWitTest(BitcoinTestFramework): return serialize_with_bogus_witness(self.tx) self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(address_type='bech32'), 5) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) unspent = next(u for u in self.nodes[0].listunspent() if u['spendable'] and u['address'].startswith('bcrt')) raw = self.nodes[0].createrawtransaction([{"txid": unspent['txid'], "vout": unspent['vout']}], {self.nodes[0].getnewaddress(): 1}) - tx = FromHex(CTransaction(), raw) + tx = tx_from_hex(raw) assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decoderawtransaction, hexstring=serialize_with_bogus_witness(tx).hex(), iswitness=True) with self.nodes[0].assert_debug_log(['Superfluous witness record']): self.test_node.send_and_ping(msg_bogus_tx(tx)) raw = self.nodes[0].signrawtransactionwithwallet(raw) assert raw['complete'] raw = raw['hex'] - tx = FromHex(CTransaction(), raw) + tx = tx_from_hex(raw) assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decoderawtransaction, hexstring=serialize_with_bogus_witness(tx).hex(), iswitness=True) with self.nodes[0].assert_debug_log(['Unknown transaction optional data']): self.test_node.send_and_ping(msg_bogus_tx(tx)) - @subtest # type: ignore + @subtest def test_wtxid_relay(self): # Use brand new nodes to avoid contamination from earlier tests - self.wtx_node = self.nodes[0].add_p2p_connection(TestP2PConn(wtxidrelay=True), services=NODE_NETWORK | NODE_WITNESS) - self.tx_node = self.nodes[0].add_p2p_connection(TestP2PConn(wtxidrelay=False), services=NODE_NETWORK | NODE_WITNESS) + self.wtx_node = self.nodes[0].add_p2p_connection(TestP2PConn(wtxidrelay=True), services=P2P_SERVICES) + self.tx_node = self.nodes[0].add_p2p_connection(TestP2PConn(wtxidrelay=False), services=P2P_SERVICES) # Check wtxidrelay feature negotiation message through connecting a new peer def received_wtxidrelay(): @@ -2135,9 +2029,8 @@ class SegWitTest(BitcoinTestFramework): # Create a Segwit output from the latest UTXO # and announce it to the network - witness_program = CScript([OP_TRUE]) - witness_hash = sha256(witness_program) - script_pubkey = CScript([OP_0, witness_hash]) + witness_script = CScript([OP_TRUE]) + script_pubkey = script_to_p2wsh_script(witness_script) tx = CTransaction() tx.vin.append(CTxIn(COutPoint(self.utxo[0].sha256, self.utxo[0].n), b"")) @@ -2149,7 +2042,7 @@ class SegWitTest(BitcoinTestFramework): tx2.vin.append(CTxIn(COutPoint(tx.sha256, 0), b"")) tx2.vout.append(CTxOut(tx.vout[0].nValue - 1000, script_pubkey)) tx2.wit.vtxinwit.append(CTxInWitness()) - tx2.wit.vtxinwit[0].scriptWitness.stack = [witness_program] + tx2.wit.vtxinwit[0].scriptWitness.stack = [witness_script] tx2.rehash() # Announce Segwit transaction with wtxid diff --git a/test/functional/p2p_sendheaders.py b/test/functional/p2p_sendheaders.py index 04e6ec4172..1ccc447b89 100755 --- a/test/functional/p2p_sendheaders.py +++ b/test/functional/p2p_sendheaders.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test behavior of headers messages to announce blocks. @@ -205,7 +205,7 @@ class SendHeadersTest(BitcoinTestFramework): # Clear out block announcements from each p2p listener [x.clear_block_announcements() for x in self.nodes[0].p2ps] - self.nodes[0].generatetoaddress(count, self.nodes[0].get_deterministic_priv_key().address) + self.generatetoaddress(self.nodes[0], count, self.nodes[0].get_deterministic_priv_key().address) return int(self.nodes[0].getbestblockhash(), 16) def mine_reorg(self, length): @@ -216,8 +216,7 @@ class SendHeadersTest(BitcoinTestFramework): return the list of block hashes newly mined.""" # make sure all invalidated blocks are node0's - self.nodes[0].generatetoaddress(length, self.nodes[0].get_deterministic_priv_key().address) - self.sync_blocks(self.nodes, wait=0.1) + self.generatetoaddress(self.nodes[0], length, self.nodes[0].get_deterministic_priv_key().address) for x in self.nodes[0].p2ps: x.wait_for_block_announcement(int(self.nodes[0].getbestblockhash(), 16)) x.clear_block_announcements() @@ -225,8 +224,7 @@ class SendHeadersTest(BitcoinTestFramework): tip_height = self.nodes[1].getblockcount() hash_to_invalidate = self.nodes[1].getblockhash(tip_height - (length - 1)) self.nodes[1].invalidateblock(hash_to_invalidate) - all_hashes = self.nodes[1].generatetoaddress(length + 1, self.nodes[1].get_deterministic_priv_key().address) # Must be longer than the orig chain - self.sync_blocks(self.nodes, wait=0.1) + all_hashes = self.generatetoaddress(self.nodes[1], length + 1, self.nodes[1].get_deterministic_priv_key().address) # Must be longer than the orig chain return [int(x, 16) for x in all_hashes] def run_test(self): @@ -240,7 +238,7 @@ class SendHeadersTest(BitcoinTestFramework): self.test_nonnull_locators(test_node, inv_node) def test_null_locators(self, test_node, inv_node): - tip = self.nodes[0].getblockheader(self.nodes[0].generatetoaddress(1, self.nodes[0].get_deterministic_priv_key().address)[0]) + tip = self.nodes[0].getblockheader(self.generatetoaddress(self.nodes[0], 1, self.nodes[0].get_deterministic_priv_key().address)[0]) tip_hash = int(tip["hash"], 16) inv_node.check_last_inv_announcement(inv=[tip_hash]) diff --git a/test/functional/p2p_timeouts.py b/test/functional/p2p_timeouts.py index a7e240dcfa..f0abbc7d8b 100755 --- a/test/functional/p2p_timeouts.py +++ b/test/functional/p2p_timeouts.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test various net timeouts. -- Create three bitcoind nodes: +- Create three peers: no_verack_node - we never send a verack in response to their version no_version_node - we never send a version (only a ping) no_send_node - we never send any P2P message. -- Start all three nodes - Wait 1 second - Assert that we're connected - Send a ping to no_verack_node and no_version_node @@ -21,12 +20,12 @@ - Assert that we're no longer connected (timeout to receive version/verack is 3 seconds) """ -from time import sleep - from test_framework.messages import msg_ping from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework +import time + class TestP2PConn(P2PInterface): def on_version(self, message): @@ -41,17 +40,27 @@ class TimeoutsTest(BitcoinTestFramework): # set timeout to receive version/verack to 3 seconds self.extra_args = [["-peertimeout=3"]] + def mock_forward(self, delta): + self.mock_time += delta + self.nodes[0].setmocktime(self.mock_time) + def run_test(self): - # Setup the p2p connections - no_verack_node = self.nodes[0].add_p2p_connection(TestP2PConn(), wait_for_verack=False) - no_version_node = self.nodes[0].add_p2p_connection(TestP2PConn(), send_version=False, wait_for_verack=False) - no_send_node = self.nodes[0].add_p2p_connection(TestP2PConn(), send_version=False, wait_for_verack=False) + self.mock_time = int(time.time()) + self.mock_forward(0) + + # Setup the p2p connections, making sure the connections are established before the mocktime is bumped + with self.nodes[0].assert_debug_log(['Added connection peer=0']): + no_verack_node = self.nodes[0].add_p2p_connection(TestP2PConn(), wait_for_verack=False) + with self.nodes[0].assert_debug_log(['Added connection peer=1']): + no_version_node = self.nodes[0].add_p2p_connection(TestP2PConn(), send_version=False, wait_for_verack=False) + with self.nodes[0].assert_debug_log(['Added connection peer=2']): + no_send_node = self.nodes[0].add_p2p_connection(TestP2PConn(), send_version=False, wait_for_verack=False) # Wait until we got the verack in response to the version. Though, don't wait for the other node to receive the # verack, since we never sent one no_verack_node.wait_for_verack() - sleep(1) + self.mock_forward(1) assert no_verack_node.is_connected assert no_version_node.is_connected @@ -62,7 +71,7 @@ class TimeoutsTest(BitcoinTestFramework): with self.nodes[0].assert_debug_log(['non-version message before version handshake. Message "ping" from peer=1']): no_version_node.send_message(msg_ping()) - sleep(1) + self.mock_forward(1) assert "version" in no_verack_node.last_message @@ -80,14 +89,10 @@ class TimeoutsTest(BitcoinTestFramework): ] with self.nodes[0].assert_debug_log(expected_msgs=expected_timeout_logs): - sleep(3) - # By now, we waited a total of 5 seconds. Off-by-two for two - # reasons: - # * The internal precision is one second - # * Account for network delay - assert not no_verack_node.is_connected - assert not no_version_node.is_connected - assert not no_send_node.is_connected + self.mock_forward(2) + no_verack_node.wait_for_disconnect(timeout=1) + no_version_node.wait_for_disconnect(timeout=1) + no_send_node.wait_for_disconnect(timeout=1) if __name__ == '__main__': diff --git a/test/functional/p2p_tx_download.py b/test/functional/p2p_tx_download.py index 4bf96cb0e6..7356b8bbb3 100755 --- a/test/functional/p2p_tx_download.py +++ b/test/functional/p2p_tx_download.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ @@ -8,13 +8,12 @@ Test transaction download behavior from test_framework.messages import ( CInv, - CTransaction, - FromHex, MSG_TX, MSG_TYPE_MASK, MSG_WTX, msg_inv, msg_notfound, + tx_from_hex, ) from test_framework.p2p import ( P2PInterface, @@ -100,7 +99,7 @@ class TxDownloadTest(BitcoinTestFramework): hexstring=tx, privkeys=[self.nodes[0].get_deterministic_priv_key().key], )['hex'] - ctx = FromHex(CTransaction(), tx) + ctx = tx_from_hex(tx) txid = int(ctx.rehash(), 16) self.log.info( diff --git a/test/functional/p2p_unrequested_blocks.py b/test/functional/p2p_unrequested_blocks.py index e7a05d8547..9c4e1dd1b1 100755 --- a/test/functional/p2p_unrequested_blocks.py +++ b/test/functional/p2p_unrequested_blocks.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test processing of unrequested blocks. @@ -77,7 +77,7 @@ class AcceptBlockTest(BitcoinTestFramework): min_work_node = self.nodes[1].add_p2p_connection(P2PInterface()) # 1. Have nodes mine a block (leave IBD) - [n.generatetoaddress(1, n.get_deterministic_priv_key().address) for n in self.nodes] + [self.generate(n, 1, sync_fun=self.no_op) for n in self.nodes] tips = [int("0x" + n.getbestblockhash(), 0) for n in self.nodes] # 2. Send one block that builds on each tip. @@ -225,10 +225,9 @@ class AcceptBlockTest(BitcoinTestFramework): block_289f.solve() block_290f = create_block(block_289f.sha256, create_coinbase(290), block_289f.nTime+1) block_290f.solve() - block_291 = create_block(block_290f.sha256, create_coinbase(291), block_290f.nTime+1) # block_291 spends a coinbase below maturity! - block_291.vtx.append(create_tx_with_script(block_290f.vtx[0], 0, script_sig=b"42", amount=1)) - block_291.hashMerkleRoot = block_291.calc_merkle_root() + tx_to_add = create_tx_with_script(block_290f.vtx[0], 0, script_sig=b"42", amount=1) + block_291 = create_block(block_290f.sha256, create_coinbase(291), block_290f.nTime+1, txlist=[tx_to_add]) block_291.solve() block_292 = create_block(block_291.sha256, create_coinbase(292), block_291.nTime+1) block_292.solve() diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index e090030205..2d96ba74b5 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -1,18 +1,21 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test RPCs related to blockchainstate. Test the following RPCs: - getblockchaininfo + - getdeploymentinfo + - getchaintxstats - gettxoutsetinfo - - getdifficulty - - getbestblockhash - - getblockhash - getblockheader - - getchaintxstats + - getdifficulty - getnetworkhashps + - waitforblockheight + - getblock + - getblockhash + - getbestblockhash - verifychain Tests correspond to code in rpc/blockchain.cpp. @@ -23,15 +26,15 @@ import http.client import os import subprocess -from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE from test_framework.blocktools import ( + MAX_FUTURE_BLOCK_TIME, + TIME_GENESIS_BLOCK, create_block, create_coinbase, - TIME_GENESIS_BLOCK, ) from test_framework.messages import ( CBlockHeader, - FromHex, + from_hex, msg_block, ) from test_framework.p2p import P2PInterface @@ -49,6 +52,13 @@ from test_framework.util import ( from test_framework.wallet import MiniWallet +HEIGHT = 200 # blocks mined +TIME_RANGE_STEP = 600 # ten-minute steps +TIME_RANGE_MTP = TIME_GENESIS_BLOCK + (HEIGHT - 6) * TIME_RANGE_STEP +TIME_RANGE_TIP = TIME_GENESIS_BLOCK + (HEIGHT - 1) * TIME_RANGE_STEP +TIME_RANGE_END = TIME_GENESIS_BLOCK + HEIGHT * TIME_RANGE_STEP + + class BlockchainTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True @@ -56,7 +66,9 @@ class BlockchainTest(BitcoinTestFramework): self.supports_cli = False def run_test(self): + self.wallet = MiniWallet(self.nodes[0]) self.mine_chain() + self._test_max_future_block_time() self.restart_node(0, extra_args=['-stopatheight=207', '-prune=1']) # Set extra args with pruning after rescan is complete self._test_getblockchaininfo() @@ -68,15 +80,28 @@ class BlockchainTest(BitcoinTestFramework): self._test_stopatheight() self._test_waitforblockheight() self._test_getblock() + self._test_getdeploymentinfo() assert self.nodes[0].verifychain(4, 0) def mine_chain(self): - self.log.info('Create some old blocks') - for t in range(TIME_GENESIS_BLOCK, TIME_GENESIS_BLOCK + 200 * 600, 600): - # ten-minute steps from genesis block time + self.log.info(f"Generate {HEIGHT} blocks after the genesis block in ten-minute steps") + for t in range(TIME_GENESIS_BLOCK, TIME_RANGE_END, TIME_RANGE_STEP): self.nodes[0].setmocktime(t) - self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_P2WSH_OP_TRUE) - assert_equal(self.nodes[0].getblockchaininfo()['blocks'], 200) + self.generate(self.wallet, 1) + assert_equal(self.nodes[0].getblockchaininfo()['blocks'], HEIGHT) + + def _test_max_future_block_time(self): + self.stop_node(0) + self.log.info("A block tip of more than MAX_FUTURE_BLOCK_TIME in the future raises an error") + self.nodes[0].assert_start_raises_init_error( + extra_args=[f"-mocktime={TIME_RANGE_TIP - MAX_FUTURE_BLOCK_TIME - 1}"], + expected_msg=": The block database contains a block which appears to be from the future." + " This may be due to your computer's date and time being set incorrectly." + f" Only rebuild the block database if you are sure that your computer's date and time are correct.{os.linesep}" + "Please restart with -reindex or -reindex-chainstate to recover.", + ) + self.log.info("A block tip of MAX_FUTURE_BLOCK_TIME in the future is fine") + self.start_node(0, extra_args=[f"-mocktime={TIME_RANGE_TIP - MAX_FUTURE_BLOCK_TIME}"]) def _test_getblockchaininfo(self): self.log.info("Test getblockchaininfo") @@ -92,12 +117,15 @@ class BlockchainTest(BitcoinTestFramework): 'mediantime', 'pruned', 'size_on_disk', - 'softforks', + 'time', 'verificationprogress', 'warnings', ] res = self.nodes[0].getblockchaininfo() + assert_equal(res['time'], TIME_RANGE_END - TIME_RANGE_STEP) + assert_equal(res['mediantime'], TIME_RANGE_MTP) + # result should have these additional pruning keys if manual pruning is enabled assert_equal(sorted(res.keys()), sorted(['pruneheight', 'automatic_pruning'] + keys)) @@ -116,7 +144,24 @@ class BlockchainTest(BitcoinTestFramework): # should have exact keys assert_equal(sorted(res.keys()), keys) - self.restart_node(0, ['-stopatheight=207', '-prune=550']) + self.stop_node(0) + self.nodes[0].assert_start_raises_init_error( + extra_args=['-testactivationheight=name@2'], + expected_msg='Error: Invalid name (name@2) for -testactivationheight=name@height.', + ) + self.nodes[0].assert_start_raises_init_error( + extra_args=['-testactivationheight=bip34@-2'], + expected_msg='Error: Invalid height value (bip34@-2) for -testactivationheight=name@height.', + ) + self.nodes[0].assert_start_raises_init_error( + extra_args=['-testactivationheight='], + expected_msg='Error: Invalid format () for -testactivationheight=name@height.', + ) + self.start_node(0, extra_args=[ + '-stopatheight=207', + '-prune=550', + ]) + res = self.nodes[0].getblockchaininfo() # result should have these additional pruning keys if prune=550 assert_equal(sorted(res.keys()), sorted(['pruneheight', 'automatic_pruning', 'prune_target_size'] + keys)) @@ -128,43 +173,80 @@ class BlockchainTest(BitcoinTestFramework): assert_equal(res['prune_target_size'], 576716800) assert_greater_than(res['size_on_disk'], 0) - assert_equal(res['softforks'], { - 'bip34': {'type': 'buried', 'active': False, 'height': 500}, - 'bip66': {'type': 'buried', 'active': False, 'height': 1251}, - 'bip65': {'type': 'buried', 'active': False, 'height': 1351}, - 'csv': {'type': 'buried', 'active': False, 'height': 432}, - 'segwit': {'type': 'buried', 'active': True, 'height': 0}, + def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash, status_next): + assert height >= 144 and height <= 287 + + assert_equal(gdi_result, { + "hash": blockhash, + "height": height, + "deployments": { + 'bip34': {'type': 'buried', 'active': True, 'height': 2}, + 'bip66': {'type': 'buried', 'active': True, 'height': 3}, + 'bip65': {'type': 'buried', 'active': True, 'height': 4}, + 'csv': {'type': 'buried', 'active': True, 'height': 5}, + 'segwit': {'type': 'buried', 'active': True, 'height': 6}, 'testdummy': { 'type': 'bip9', 'bip9': { - 'status': 'started', 'bit': 28, 'start_time': 0, 'timeout': 0x7fffffffffffffff, # testdummy does not have a timeout so is set to the max int64 value + 'min_activation_height': 0, + 'status': 'started', + 'status-next': status_next, 'since': 144, 'statistics': { 'period': 144, 'threshold': 108, - 'elapsed': 57, - 'count': 57, + 'elapsed': height - 143, + 'count': height - 143, 'possible': True, }, + 'signalling': '#'*(height-143), }, 'active': False }, 'taproot': { 'type': 'bip9', 'bip9': { - 'status': 'active', 'start_time': -1, 'timeout': 9223372036854775807, - 'since': 0 + 'min_activation_height': 0, + 'status': 'active', + 'status-next': 'active', + 'since': 0, }, 'height': 0, 'active': True } + } }) + def _test_getdeploymentinfo(self): + # Note: continues past -stopatheight height, so must be invoked + # after _test_stopatheight + + self.log.info("Test getdeploymentinfo") + self.stop_node(0) + self.start_node(0, extra_args=[ + '-testactivationheight=bip34@2', + '-testactivationheight=dersig@3', + '-testactivationheight=cltv@4', + '-testactivationheight=csv@5', + '-testactivationheight=segwit@6', + ]) + + gbci207 = self.nodes[0].getblockchaininfo() + self.check_signalling_deploymentinfo_result(self.nodes[0].getdeploymentinfo(), gbci207["blocks"], gbci207["bestblockhash"], "started") + + # block just prior to lock in + self.generate(self.wallet, 287 - gbci207["blocks"]) + gbci287 = self.nodes[0].getblockchaininfo() + self.check_signalling_deploymentinfo_result(self.nodes[0].getdeploymentinfo(), gbci287["blocks"], gbci287["bestblockhash"], "locked_in") + + # calling with an explicit hash works + self.check_signalling_deploymentinfo_result(self.nodes[0].getdeploymentinfo(gbci207["bestblockhash"]), gbci207["blocks"], gbci207["bestblockhash"], "started") + def _test_getchaintxstats(self): self.log.info("Test getchaintxstats") @@ -181,33 +263,33 @@ class BlockchainTest(BitcoinTestFramework): assert_raises_rpc_error(-8, "blockhash must be of length 64 (not 1, for '0')", self.nodes[0].getchaintxstats, blockhash='0') assert_raises_rpc_error(-8, "blockhash must be hexadecimal string (not 'ZZZ0000000000000000000000000000000000000000000000000000000000000')", self.nodes[0].getchaintxstats, blockhash='ZZZ0000000000000000000000000000000000000000000000000000000000000') assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getchaintxstats, blockhash='0000000000000000000000000000000000000000000000000000000000000000') - blockhash = self.nodes[0].getblockhash(200) + blockhash = self.nodes[0].getblockhash(HEIGHT) self.nodes[0].invalidateblock(blockhash) assert_raises_rpc_error(-8, "Block is not in main chain", self.nodes[0].getchaintxstats, blockhash=blockhash) self.nodes[0].reconsiderblock(blockhash) chaintxstats = self.nodes[0].getchaintxstats(nblocks=1) # 200 txs plus genesis tx - assert_equal(chaintxstats['txcount'], 201) + assert_equal(chaintxstats['txcount'], HEIGHT + 1) # tx rate should be 1 per 10 minutes, or 1/600 # we have to round because of binary math - assert_equal(round(chaintxstats['txrate'] * 600, 10), Decimal(1)) + assert_equal(round(chaintxstats['txrate'] * TIME_RANGE_STEP, 10), Decimal(1)) b1_hash = self.nodes[0].getblockhash(1) b1 = self.nodes[0].getblock(b1_hash) - b200_hash = self.nodes[0].getblockhash(200) + b200_hash = self.nodes[0].getblockhash(HEIGHT) b200 = self.nodes[0].getblock(b200_hash) time_diff = b200['mediantime'] - b1['mediantime'] chaintxstats = self.nodes[0].getchaintxstats() assert_equal(chaintxstats['time'], b200['time']) - assert_equal(chaintxstats['txcount'], 201) + assert_equal(chaintxstats['txcount'], HEIGHT + 1) assert_equal(chaintxstats['window_final_block_hash'], b200_hash) - assert_equal(chaintxstats['window_final_block_height'], 200) - assert_equal(chaintxstats['window_block_count'], 199) - assert_equal(chaintxstats['window_tx_count'], 199) + assert_equal(chaintxstats['window_final_block_height'], HEIGHT ) + assert_equal(chaintxstats['window_block_count'], HEIGHT - 1) + assert_equal(chaintxstats['window_tx_count'], HEIGHT - 1) assert_equal(chaintxstats['window_interval'], time_diff) - assert_equal(round(chaintxstats['txrate'] * time_diff, 10), Decimal(199)) + assert_equal(round(chaintxstats['txrate'] * time_diff, 10), Decimal(HEIGHT - 1)) chaintxstats = self.nodes[0].getchaintxstats(blockhash=b1_hash) assert_equal(chaintxstats['time'], b1['time']) @@ -224,18 +306,18 @@ class BlockchainTest(BitcoinTestFramework): res = node.gettxoutsetinfo() assert_equal(res['total_amount'], Decimal('8725.00000000')) - assert_equal(res['transactions'], 200) - assert_equal(res['height'], 200) - assert_equal(res['txouts'], 200) + assert_equal(res['transactions'], HEIGHT) + assert_equal(res['height'], HEIGHT) + assert_equal(res['txouts'], HEIGHT) assert_equal(res['bogosize'], 16800), - assert_equal(res['bestblock'], node.getblockhash(200)) + assert_equal(res['bestblock'], node.getblockhash(HEIGHT)) size = res['disk_size'] assert size > 6400 assert size < 64000 assert_equal(len(res['bestblock']), 64) assert_equal(len(res['hash_serialized_2']), 64) - self.log.info("Test that gettxoutsetinfo() works for blockchain with just the genesis block") + self.log.info("Test gettxoutsetinfo works for blockchain with just the genesis block") b1hash = node.getblockhash(1) node.invalidateblock(b1hash) @@ -248,7 +330,7 @@ class BlockchainTest(BitcoinTestFramework): assert_equal(res2['bestblock'], node.getblockhash(0)) assert_equal(len(res2['hash_serialized_2']), 64) - self.log.info("Test that gettxoutsetinfo() returns the same result after invalidate/reconsider block") + self.log.info("Test gettxoutsetinfo returns the same result after invalidate/reconsider block") node.reconsiderblock(b1hash) res3 = node.gettxoutsetinfo() @@ -257,7 +339,7 @@ class BlockchainTest(BitcoinTestFramework): del res['disk_size'], res3['disk_size'] assert_equal(res, res3) - self.log.info("Test hash_type option for gettxoutsetinfo()") + self.log.info("Test gettxoutsetinfo hash_type option") # Adding hash_type 'hash_serialized_2', which is the default, should # not change the result. res4 = node.gettxoutsetinfo(hash_type='hash_serialized_2') @@ -273,14 +355,15 @@ class BlockchainTest(BitcoinTestFramework): assert 'muhash' in res6 assert(res['hash_serialized_2'] != res6['muhash']) - # muhash should not be included in gettxoutset unless requested. + # muhash should not be returned unless requested. for r in [res, res2, res3, res4, res5]: assert 'muhash' not in r # Unknown hash_type raises an error - assert_raises_rpc_error(-8, "foohash is not a valid hash_type", node.gettxoutsetinfo, "foohash") + assert_raises_rpc_error(-8, "'foo hash' is not a valid hash_type", node.gettxoutsetinfo, "foo hash") def _test_getblockheader(self): + self.log.info("Test getblockheader") node = self.nodes[0] assert_raises_rpc_error(-8, "hash must be of length 64 (not 8, for 'nonsense')", node.getblockheader, "nonsense") @@ -288,11 +371,11 @@ class BlockchainTest(BitcoinTestFramework): assert_raises_rpc_error(-5, "Block not found", node.getblockheader, "0cf7bb8b1697ea987f3b223ba7819250cae33efacb068d23dc24859824a77844") besthash = node.getbestblockhash() - secondbesthash = node.getblockhash(199) + secondbesthash = node.getblockhash(HEIGHT - 1) header = node.getblockheader(blockhash=besthash) assert_equal(header['hash'], besthash) - assert_equal(header['height'], 200) + assert_equal(header['height'], HEIGHT) assert_equal(header['confirmations'], 1) assert_equal(header['previousblockhash'], secondbesthash) assert_is_hex_string(header['chainwork']) @@ -302,7 +385,7 @@ class BlockchainTest(BitcoinTestFramework): assert_is_hash_string(header['merkleroot']) assert_is_hash_string(header['bits'], length=None) assert isinstance(header['time'], int) - assert isinstance(header['mediantime'], int) + assert_equal(header['mediantime'], TIME_RANGE_MTP) assert isinstance(header['nonce'], int) assert isinstance(header['version'], int) assert isinstance(int(header['versionHex'], 16), int) @@ -312,7 +395,7 @@ class BlockchainTest(BitcoinTestFramework): header_hex = node.getblockheader(blockhash=besthash, verbose=False) assert_is_hex_string(header_hex) - header = FromHex(CBlockHeader(), header_hex) + header = from_hex(CBlockHeader(), header_hex) header.calc_sha256() assert_equal(header.hash, besthash) @@ -320,30 +403,33 @@ class BlockchainTest(BitcoinTestFramework): assert 'nextblockhash' not in node.getblockheader(node.getbestblockhash()) def _test_getdifficulty(self): + self.log.info("Test getdifficulty") difficulty = self.nodes[0].getdifficulty() # 1 hash in 2 should be valid, so difficulty should be 1/2**31 # binary => decimal => binary math is why we do this check assert abs(difficulty * 2**31 - 1) < 0.0001 def _test_getnetworkhashps(self): + self.log.info("Test getnetworkhashps") hashes_per_second = self.nodes[0].getnetworkhashps() # This should be 2 hashes every 10 minutes or 1/300 assert abs(hashes_per_second * 300 - 1) < 0.0001 def _test_stopatheight(self): - assert_equal(self.nodes[0].getblockcount(), 200) - self.nodes[0].generatetoaddress(6, ADDRESS_BCRT1_P2WSH_OP_TRUE) - assert_equal(self.nodes[0].getblockcount(), 206) + self.log.info("Test stopping at height") + assert_equal(self.nodes[0].getblockcount(), HEIGHT) + self.generate(self.wallet, 6) + assert_equal(self.nodes[0].getblockcount(), HEIGHT + 6) self.log.debug('Node should not stop at this height') assert_raises(subprocess.TimeoutExpired, lambda: self.nodes[0].process.wait(timeout=3)) try: - self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_P2WSH_OP_TRUE) + self.generatetoaddress(self.nodes[0], 1, self.wallet.get_address(), sync_fun=self.no_op) except (ConnectionError, http.client.BadStatusLine): pass # The node already shut down before response self.log.debug('Node should stop at this height...') self.nodes[0].wait_until_stopped() self.start_node(0) - assert_equal(self.nodes[0].getblockcount(), 207) + assert_equal(self.nodes[0].getblockcount(), HEIGHT + 7) def _test_waitforblockheight(self): self.log.info("Test waitforblockheight") @@ -385,29 +471,66 @@ class BlockchainTest(BitcoinTestFramework): def _test_getblock(self): node = self.nodes[0] - - miniwallet = MiniWallet(node) - miniwallet.scan_blocks(num=5) - fee_per_byte = Decimal('0.00000010') fee_per_kb = 1000 * fee_per_byte - miniwallet.send_self_transfer(fee_rate=fee_per_kb, from_node=node) - blockhash = node.generate(1)[0] + self.wallet.send_self_transfer(fee_rate=fee_per_kb, from_node=node) + blockhash = self.generate(node, 1)[0] + + def assert_fee_not_in_block(verbosity): + block = node.getblock(blockhash, verbosity) + assert 'fee' not in block['tx'][1] + + def assert_fee_in_block(verbosity): + block = node.getblock(blockhash, verbosity) + tx = block['tx'][1] + assert 'fee' in tx + assert_equal(tx['fee'], tx['vsize'] * fee_per_byte) + + def assert_vin_contains_prevout(verbosity): + block = node.getblock(blockhash, verbosity) + tx = block["tx"][1] + total_vin = Decimal("0.00000000") + total_vout = Decimal("0.00000000") + for vin in tx["vin"]: + assert "prevout" in vin + assert_equal(set(vin["prevout"].keys()), set(("value", "height", "generated", "scriptPubKey"))) + assert_equal(vin["prevout"]["generated"], True) + total_vin += vin["prevout"]["value"] + for vout in tx["vout"]: + total_vout += vout["value"] + assert_equal(total_vin, total_vout + tx["fee"]) + + def assert_vin_does_not_contain_prevout(verbosity): + block = node.getblock(blockhash, verbosity) + tx = block["tx"][1] + if isinstance(tx, str): + # In verbosity level 1, only the transaction hashes are written + pass + else: + for vin in tx["vin"]: + assert "prevout" not in vin self.log.info("Test that getblock with verbosity 1 doesn't include fee") - block = node.getblock(blockhash, 1) - assert 'fee' not in block['tx'][1] + assert_fee_not_in_block(1) - self.log.info('Test that getblock with verbosity 2 includes expected fee') - block = node.getblock(blockhash, 2) - tx = block['tx'][1] - assert 'fee' in tx - assert_equal(tx['fee'], tx['vsize'] * fee_per_byte) + self.log.info('Test that getblock with verbosity 2 and 3 includes expected fee') + assert_fee_in_block(2) + assert_fee_in_block(3) - self.log.info("Test that getblock with verbosity 2 still works with pruned Undo data") + self.log.info("Test that getblock with verbosity 1 and 2 does not include prevout") + assert_vin_does_not_contain_prevout(1) + assert_vin_does_not_contain_prevout(2) + + self.log.info("Test that getblock with verbosity 3 includes prevout") + assert_vin_contains_prevout(3) + + self.log.info("Test that getblock with verbosity 2 and 3 still works with pruned Undo data") datadir = get_datadir_path(self.options.tmpdir, 0) + self.log.info("Test getblock with invalid verbosity type returns proper error message") + assert_raises_rpc_error(-1, "JSON value is not an integer as expected", node.getblock, blockhash, "2") + def move_block_file(old, new): old_path = os.path.join(datadir, self.chain, 'blocks', old) new_path = os.path.join(datadir, self.chain, 'blocks', new) @@ -416,8 +539,10 @@ class BlockchainTest(BitcoinTestFramework): # Move instead of deleting so we can restore chain state afterwards move_block_file('rev00000.dat', 'rev_wrong') - block = node.getblock(blockhash, 2) - assert 'fee' not in block['tx'][1] + assert_fee_not_in_block(2) + assert_fee_not_in_block(3) + assert_vin_does_not_contain_prevout(2) + assert_vin_does_not_contain_prevout(3) # Restore chain state move_block_file('rev_wrong', 'rev00000.dat') diff --git a/test/functional/rpc_createmultisig.py b/test/functional/rpc_createmultisig.py index 31baeba582..1a3d14100f 100755 --- a/test/functional/rpc_createmultisig.py +++ b/test/functional/rpc_createmultisig.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test multisig RPCs""" -import binascii import decimal import itertools import json import os +from test_framework.blocktools import COINBASE_MATURITY from test_framework.authproxy import JSONRPCException from test_framework.descriptors import descsum_create, drop_origins from test_framework.key import ECPubKey, ECKey @@ -45,8 +45,7 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): self.check_addmultisigaddress_errors() self.log.info('Generating blocks ...') - node0.generate(149) - self.sync_all() + self.generate(node0, 149) self.moved = 0 for self.nkeys in [3, 5]: @@ -65,9 +64,9 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): # decompress pk2 pk_obj = ECPubKey() - pk_obj.set(binascii.unhexlify(pk2)) + pk_obj.set(bytes.fromhex(pk2)) pk_obj.compressed = False - pk2 = binascii.hexlify(pk_obj.get_bytes()).decode() + pk2 = pk_obj.get_bytes().hex() node0.createwallet(wallet_name='wmulti0', disable_private_keys=True) wmulti0 = node0.get_wallet_rpc('wmulti0') @@ -76,13 +75,19 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): for keys in itertools.permutations([pk0, pk1, pk2]): # Results should be the same as this legacy one legacy_addr = node0.createmultisig(2, keys, 'legacy')['address'] - assert_equal(legacy_addr, wmulti0.addmultisigaddress(2, keys, '', 'legacy')['address']) + result = wmulti0.addmultisigaddress(2, keys, '', 'legacy') + assert_equal(legacy_addr, result['address']) + assert 'warnings' not in result # Generate addresses with the segwit types. These should all make legacy addresses - assert_equal(legacy_addr, wmulti0.createmultisig(2, keys, 'bech32')['address']) - assert_equal(legacy_addr, wmulti0.createmultisig(2, keys, 'p2sh-segwit')['address']) - assert_equal(legacy_addr, wmulti0.addmultisigaddress(2, keys, '', 'bech32')['address']) - assert_equal(legacy_addr, wmulti0.addmultisigaddress(2, keys, '', 'p2sh-segwit')['address']) + for addr_type in ['bech32', 'p2sh-segwit']: + result = wmulti0.createmultisig(2, keys, addr_type) + assert_equal(legacy_addr, result['address']) + assert_equal(result['warnings'], ["Unable to make chosen address type, please ensure no uncompressed public keys are present."]) + + result = wmulti0.addmultisigaddress(2, keys, '', addr_type) + assert_equal(legacy_addr, result['address']) + assert_equal(result['warnings'], ["Unable to make chosen address type, please ensure no uncompressed public keys are present."]) self.log.info('Testing sortedmulti descriptors with BIP 67 test vectors') with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/rpc_bip67.json'), encoding='utf-8') as f: @@ -96,6 +101,9 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): sorted_key_desc = descsum_create('sh(multi(2,{}))'.format(sorted_key_str)) assert_equal(self.nodes[0].deriveaddresses(sorted_key_desc)[0], t['address']) + # Check that bech32m is currently not allowed + assert_raises_rpc_error(-5, "createmultisig cannot create bech32m multisig addresses", self.nodes[0].createmultisig, 2, self.pub, "bech32m") + def check_addmultisigaddress_errors(self): if self.options.descriptors: return @@ -107,10 +115,13 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): self.nodes[0].importaddress(a) assert_raises_rpc_error(-5, 'no full public key for address', lambda: self.nodes[0].addmultisigaddress(nrequired=1, keys=addresses)) + # Bech32m address type is disallowed for legacy wallets + pubs = [self.nodes[1].getaddressinfo(addr)["pubkey"] for addr in addresses] + assert_raises_rpc_error(-5, "Bech32m multisig addresses cannot be created with legacy wallets", self.nodes[0].addmultisigaddress, 2, pubs, "", "bech32m") + def checkbalances(self): node0, node1, node2 = self.nodes - node0.generate(100) - self.sync_all() + self.generate(node0, COINBASE_MATURITY) bal0 = node0.getbalance() bal1 = node1.getbalance() @@ -165,14 +176,14 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): txid = node0.sendtoaddress(madd, 40) tx = node0.getrawtransaction(txid, True) - vout = [v["n"] for v in tx["vout"] if madd in v["scriptPubKey"].get("addresses", [])] + vout = [v["n"] for v in tx["vout"] if madd == v["scriptPubKey"]["address"]] assert len(vout) == 1 vout = vout[0] scriptPubKey = tx["vout"][vout]["scriptPubKey"]["hex"] value = tx["vout"][vout]["value"] prevtxs = [{"txid": txid, "vout": vout, "scriptPubKey": scriptPubKey, "redeemScript": mredeem, "amount": value}] - node0.generate(1) + self.generate(node0, 1) outval = value - decimal.Decimal("0.00001000") rawtx = node2.createrawtransaction([{"txid": txid, "vout": vout}], [{self.final: outval}]) @@ -208,7 +219,7 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): self.moved += outval tx = node0.sendrawtransaction(rawtx3["hex"], 0) - blk = node0.generate(1)[0] + blk = self.generate(node0, 1)[0] assert tx in node0.getblock(blk)["tx"] txinfo = node0.getrawtransaction(tx, True, blk) diff --git a/test/functional/rpc_decodescript.py b/test/functional/rpc_decodescript.py index 01b8cb1854..56f596d419 100755 --- a/test/functional/rpc_decodescript.py +++ b/test/functional/rpc_decodescript.py @@ -1,14 +1,21 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2019 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test decoding scripts via decodescript RPC command.""" -from test_framework.messages import CTransaction, sha256 +import json +import os + +from test_framework.messages import ( + sha256, + tx_from_hex, +) from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, hex_str_to_bytes +from test_framework.util import ( + assert_equal, +) -from io import BytesIO class DecodeScriptTest(BitcoinTestFramework): def set_test_params(self): @@ -23,29 +30,29 @@ class DecodeScriptTest(BitcoinTestFramework): # below are test cases for all of the standard transaction types - # 1) P2PK scriptSig + self.log.info("- P2PK") # the scriptSig of a public key scriptPubKey simply pushes a signature onto the stack rpc_result = self.nodes[0].decodescript(push_signature) assert_equal(signature, rpc_result['asm']) - # 2) P2PKH scriptSig + self.log.info("- P2PKH") rpc_result = self.nodes[0].decodescript(push_signature + push_public_key) assert_equal(signature + ' ' + public_key, rpc_result['asm']) - # 3) multisig scriptSig + self.log.info("- multisig") # this also tests the leading portion of a P2SH multisig scriptSig # OP_0 <A sig> <B sig> rpc_result = self.nodes[0].decodescript('00' + push_signature + push_signature) assert_equal('0 ' + signature + ' ' + signature, rpc_result['asm']) - # 4) P2SH scriptSig + self.log.info("- P2SH") # an empty P2SH redeemScript is valid and makes for a very simple test case. # thus, such a spending scriptSig would just need to pass the outer redeemScript # hash test and leave true on the top of the stack. rpc_result = self.nodes[0].decodescript('5100') assert_equal('1 0', rpc_result['asm']) - # 5) null data scriptSig - no such thing because null data scripts can not be spent. + # null data scriptSig - no such thing because null data scripts can not be spent. # thus, no test case for that standard transaction type is here. def decodescript_script_pub_key(self): @@ -59,50 +66,58 @@ class DecodeScriptTest(BitcoinTestFramework): # below are test cases for all of the standard transaction types - # 1) P2PK scriptPubKey + self.log.info("- P2PK") # <pubkey> OP_CHECKSIG rpc_result = self.nodes[0].decodescript(push_public_key + 'ac') assert_equal(public_key + ' OP_CHECKSIG', rpc_result['asm']) + assert_equal('pubkey', rpc_result['type']) # P2PK is translated to P2WPKH assert_equal('0 ' + public_key_hash, rpc_result['segwit']['asm']) - # 2) P2PKH scriptPubKey + self.log.info("- P2PKH") # OP_DUP OP_HASH160 <PubKeyHash> OP_EQUALVERIFY OP_CHECKSIG rpc_result = self.nodes[0].decodescript('76a9' + push_public_key_hash + '88ac') + assert_equal('pubkeyhash', rpc_result['type']) assert_equal('OP_DUP OP_HASH160 ' + public_key_hash + ' OP_EQUALVERIFY OP_CHECKSIG', rpc_result['asm']) # P2PKH is translated to P2WPKH + assert_equal('witness_v0_keyhash', rpc_result['segwit']['type']) assert_equal('0 ' + public_key_hash, rpc_result['segwit']['asm']) - # 3) multisig scriptPubKey + self.log.info("- multisig") # <m> <A pubkey> <B pubkey> <C pubkey> <n> OP_CHECKMULTISIG # just imagine that the pub keys used below are different. # for our purposes here it does not matter that they are the same even though it is unrealistic. multisig_script = '52' + push_public_key + push_public_key + push_public_key + '53ae' rpc_result = self.nodes[0].decodescript(multisig_script) + assert_equal('multisig', rpc_result['type']) assert_equal('2 ' + public_key + ' ' + public_key + ' ' + public_key + ' 3 OP_CHECKMULTISIG', rpc_result['asm']) # multisig in P2WSH - multisig_script_hash = sha256(hex_str_to_bytes(multisig_script)).hex() + multisig_script_hash = sha256(bytes.fromhex(multisig_script)).hex() + assert_equal('witness_v0_scripthash', rpc_result['segwit']['type']) assert_equal('0 ' + multisig_script_hash, rpc_result['segwit']['asm']) - # 4) P2SH scriptPubKey + self.log.info ("- P2SH") # OP_HASH160 <Hash160(redeemScript)> OP_EQUAL. # push_public_key_hash here should actually be the hash of a redeem script. # but this works the same for purposes of this test. rpc_result = self.nodes[0].decodescript('a9' + push_public_key_hash + '87') + assert_equal('scripthash', rpc_result['type']) assert_equal('OP_HASH160 ' + public_key_hash + ' OP_EQUAL', rpc_result['asm']) # P2SH does not work in segwit secripts. decodescript should not return a result for it. assert 'segwit' not in rpc_result - # 5) null data scriptPubKey + self.log.info("- null data") # use a signature look-alike here to make sure that we do not decode random data as a signature. # this matters if/when signature sighash decoding comes along. # would want to make sure that no such decoding takes place in this case. signature_imposter = '48304502207fa7a6d1e0ee81132a269ad84e68d695483745cde8b541e3bf630749894e342a022100c1f7ab20e13e22fb95281a870f3dcf38d782e53023ee313d741ad0cfbc0c509001' # OP_RETURN <data> rpc_result = self.nodes[0].decodescript('6a' + signature_imposter) + assert_equal('nulldata', rpc_result['type']) assert_equal('OP_RETURN ' + signature_imposter[2:], rpc_result['asm']) - # 6) a CLTV redeem script. redeem scripts are in-effect scriptPubKey scripts, so adding a test here. + self.log.info("- CLTV redeem script") + # redeem scripts are in-effect scriptPubKey scripts, so adding a test here. # OP_NOP2 is also known as OP_CHECKLOCKTIMEVERIFY. # just imagine that the pub keys used below are different. # for our purposes here it does not matter that they are the same even though it is unrealistic. @@ -117,55 +132,69 @@ class DecodeScriptTest(BitcoinTestFramework): # lock until block 500,000 cltv_script = '63' + push_public_key + 'ad670320a107b17568' + push_public_key + 'ac' rpc_result = self.nodes[0].decodescript(cltv_script) + assert_equal('nonstandard', rpc_result['type']) assert_equal('OP_IF ' + public_key + ' OP_CHECKSIGVERIFY OP_ELSE 500000 OP_CHECKLOCKTIMEVERIFY OP_DROP OP_ENDIF ' + public_key + ' OP_CHECKSIG', rpc_result['asm']) # CLTV script in P2WSH - cltv_script_hash = sha256(hex_str_to_bytes(cltv_script)).hex() + cltv_script_hash = sha256(bytes.fromhex(cltv_script)).hex() assert_equal('0 ' + cltv_script_hash, rpc_result['segwit']['asm']) - # 7) P2PK scriptPubKey + self.log.info("- P2PK with uncompressed pubkey") # <pubkey> OP_CHECKSIG rpc_result = self.nodes[0].decodescript(push_uncompressed_public_key + 'ac') + assert_equal('pubkey', rpc_result['type']) assert_equal(uncompressed_public_key + ' OP_CHECKSIG', rpc_result['asm']) # uncompressed pubkeys are invalid for checksigs in segwit scripts. # decodescript should not return a P2WPKH equivalent. assert 'segwit' not in rpc_result - # 8) multisig scriptPubKey with an uncompressed pubkey + self.log.info("- multisig with uncompressed pubkey") # <m> <A pubkey> <B pubkey> <n> OP_CHECKMULTISIG # just imagine that the pub keys used below are different. # the purpose of this test is to check that a segwit script is not returned for bare multisig scripts # with an uncompressed pubkey in them. rpc_result = self.nodes[0].decodescript('52' + push_public_key + push_uncompressed_public_key +'52ae') + assert_equal('multisig', rpc_result['type']) assert_equal('2 ' + public_key + ' ' + uncompressed_public_key + ' 2 OP_CHECKMULTISIG', rpc_result['asm']) # uncompressed pubkeys are invalid for checksigs in segwit scripts. # decodescript should not return a P2WPKH equivalent. assert 'segwit' not in rpc_result - # 9) P2WPKH scriptpubkey + self.log.info("- P2WPKH") # 0 <PubKeyHash> rpc_result = self.nodes[0].decodescript('00' + push_public_key_hash) + assert_equal('witness_v0_keyhash', rpc_result['type']) assert_equal('0 ' + public_key_hash, rpc_result['asm']) # segwit scripts do not work nested into each other. # a nested segwit script should not be returned in the results. assert 'segwit' not in rpc_result - # 10) P2WSH scriptpubkey + self.log.info("- P2WSH") # 0 <ScriptHash> # even though this hash is of a P2PK script which is better used as bare P2WPKH, it should not matter # for the purpose of this test. rpc_result = self.nodes[0].decodescript('0020' + p2wsh_p2pk_script_hash) + assert_equal('witness_v0_scripthash', rpc_result['type']) assert_equal('0 ' + p2wsh_p2pk_script_hash, rpc_result['asm']) # segwit scripts do not work nested into each other. # a nested segwit script should not be returned in the results. assert 'segwit' not in rpc_result + self.log.info("- P2TR") + # 1 <x-only pubkey> + xonly_public_key = '01'*32 # first ever P2TR output on mainnet + rpc_result = self.nodes[0].decodescript('5120' + xonly_public_key) + assert_equal('witness_v1_taproot', rpc_result['type']) + assert_equal('1 ' + xonly_public_key, rpc_result['asm']) + assert 'segwit' not in rpc_result + def decoderawtransaction_asm_sighashtype(self): """Test decoding scripts via RPC command "decoderawtransaction". This test is in with the "decodescript" tests because they are testing the same "asm" script decodes. """ - # this test case uses a random plain vanilla mainnet transaction with a single P2PKH input and output + self.log.info("- various mainnet txs") + # this test case uses a mainnet transaction that has a P2SH input and both P2PKH and P2SH outputs. tx = '0100000001696a20784a2c70143f634e95227dbdfdf0ecd51647052e70854512235f5986ca010000008a47304402207174775824bec6c2700023309a168231ec80b82c6069282f5133e6f11cbb04460220570edc55c7c5da2ca687ebd0372d3546ebc3f810516a002350cac72dfe192dfb014104d3f898e6487787910a690410b7a917ef198905c27fb9d3b0a42da12aceae0544fc7088d239d9a48f2828a15a09e84043001f27cc80d162cb95404e1210161536ffffffff0100e1f505000000001976a914eb6c6e0cdb2d256a32d97b8df1fc75d1920d9bca88ac00000000' rpc_result = self.nodes[0].decoderawtransaction(tx) assert_equal('304402207174775824bec6c2700023309a168231ec80b82c6069282f5133e6f11cbb04460220570edc55c7c5da2ca687ebd0372d3546ebc3f810516a002350cac72dfe192dfb[ALL] 04d3f898e6487787910a690410b7a917ef198905c27fb9d3b0a42da12aceae0544fc7088d239d9a48f2828a15a09e84043001f27cc80d162cb95404e1210161536', rpc_result['vin'][0]['scriptSig']['asm']) @@ -179,14 +208,15 @@ class DecodeScriptTest(BitcoinTestFramework): assert_equal('0 3045022100ae3b4e589dfc9d48cb82d41008dc5fa6a86f94d5c54f9935531924602730ab8002202f88cf464414c4ed9fa11b773c5ee944f66e9b05cc1e51d97abc22ce098937ea[ALL] 3045022100b44883be035600e9328a01b66c7d8439b74db64187e76b99a68f7893b701d5380220225bf286493e4c4adcf928c40f785422572eb232f84a0b83b0dea823c3a19c75[ALL] 5221020743d44be989540d27b1b4bbbcfd17721c337cb6bc9af20eb8a32520b393532f2102c0120a1dda9e51a938d39ddd9fe0ebc45ea97e1d27a7cbd671d5431416d3dd87210213820eb3d5f509d7438c9eeecb4157b2f595105e7cd564b3cdbb9ead3da41eed53ae', rpc_result['vin'][0]['scriptSig']['asm']) assert_equal('OP_DUP OP_HASH160 dc863734a218bfe83ef770ee9d41a27f824a6e56 OP_EQUALVERIFY OP_CHECKSIG', rpc_result['vout'][0]['scriptPubKey']['asm']) assert_equal('OP_HASH160 2a5edea39971049a540474c6a99edf0aa4074c58 OP_EQUAL', rpc_result['vout'][1]['scriptPubKey']['asm']) - txSave = CTransaction() - txSave.deserialize(BytesIO(hex_str_to_bytes(tx))) + txSave = tx_from_hex(tx) + self.log.info("- tx not passing DER signature checks") # make sure that a specifically crafted op_return value will not pass all the IsDERSignature checks and then get decoded as a sighash type tx = '01000000015ded05872fdbda629c7d3d02b194763ce3b9b1535ea884e3c8e765d42e316724020000006b48304502204c10d4064885c42638cbff3585915b322de33762598321145ba033fc796971e2022100bb153ad3baa8b757e30a2175bd32852d2e1cb9080f84d7e32fcdfd667934ef1b012103163c0ff73511ea1743fb5b98384a2ff09dd06949488028fd819f4d83f56264efffffffff0200000000000000000b6a0930060201000201000180380100000000001976a9141cabd296e753837c086da7a45a6c2fe0d49d7b7b88ac00000000' rpc_result = self.nodes[0].decoderawtransaction(tx) assert_equal('OP_RETURN 300602010002010001', rpc_result['vout'][0]['scriptPubKey']['asm']) + self.log.info("- tx passing DER signature checks") # verify that we have not altered scriptPubKey processing even of a specially crafted P2PKH pubkeyhash and P2SH redeem script hash that is made to pass the der signature checks tx = '01000000018d1f5635abd06e2c7e2ddf58dc85b3de111e4ad6e0ab51bb0dcf5e84126d927300000000fdfe0000483045022100ae3b4e589dfc9d48cb82d41008dc5fa6a86f94d5c54f9935531924602730ab8002202f88cf464414c4ed9fa11b773c5ee944f66e9b05cc1e51d97abc22ce098937ea01483045022100b44883be035600e9328a01b66c7d8439b74db64187e76b99a68f7893b701d5380220225bf286493e4c4adcf928c40f785422572eb232f84a0b83b0dea823c3a19c75014c695221020743d44be989540d27b1b4bbbcfd17721c337cb6bc9af20eb8a32520b393532f2102c0120a1dda9e51a938d39ddd9fe0ebc45ea97e1d27a7cbd671d5431416d3dd87210213820eb3d5f509d7438c9eeecb4157b2f595105e7cd564b3cdbb9ead3da41eed53aeffffffff02611e0000000000001976a914301102070101010101010102060101010101010188acee2a02000000000017a91430110207010101010101010206010101010101018700000000' rpc_result = self.nodes[0].decoderawtransaction(tx) @@ -204,31 +234,44 @@ class DecodeScriptTest(BitcoinTestFramework): push_signature_2 = '48' + signature_2 signature_2_sighash_decoded = der_signature + '[NONE|ANYONECANPAY]' - # 1) P2PK scriptSig - txSave.vin[0].scriptSig = hex_str_to_bytes(push_signature) + self.log.info("- P2PK scriptSig") + txSave.vin[0].scriptSig = bytes.fromhex(push_signature) rpc_result = self.nodes[0].decoderawtransaction(txSave.serialize().hex()) assert_equal(signature_sighash_decoded, rpc_result['vin'][0]['scriptSig']['asm']) # make sure that the sighash decodes come out correctly for a more complex / lesser used case. - txSave.vin[0].scriptSig = hex_str_to_bytes(push_signature_2) + txSave.vin[0].scriptSig = bytes.fromhex(push_signature_2) rpc_result = self.nodes[0].decoderawtransaction(txSave.serialize().hex()) assert_equal(signature_2_sighash_decoded, rpc_result['vin'][0]['scriptSig']['asm']) - # 2) multisig scriptSig - txSave.vin[0].scriptSig = hex_str_to_bytes('00' + push_signature + push_signature_2) + self.log.info("- multisig scriptSig") + txSave.vin[0].scriptSig = bytes.fromhex('00' + push_signature + push_signature_2) rpc_result = self.nodes[0].decoderawtransaction(txSave.serialize().hex()) assert_equal('0 ' + signature_sighash_decoded + ' ' + signature_2_sighash_decoded, rpc_result['vin'][0]['scriptSig']['asm']) - # 3) test a scriptSig that contains more than push operations. + self.log.info("- scriptSig that contains more than push operations") # in fact, it contains an OP_RETURN with data specially crafted to cause improper decode if the code does not catch it. - txSave.vin[0].scriptSig = hex_str_to_bytes('6a143011020701010101010101020601010101010101') + txSave.vin[0].scriptSig = bytes.fromhex('6a143011020701010101010101020601010101010101') rpc_result = self.nodes[0].decoderawtransaction(txSave.serialize().hex()) assert_equal('OP_RETURN 3011020701010101010101020601010101010101', rpc_result['vin'][0]['scriptSig']['asm']) + def decodescript_datadriven_tests(self): + with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/rpc_decodescript.json'), encoding='utf-8') as f: + dd_tests = json.load(f) + + for script, result in dd_tests: + rpc_result = self.nodes[0].decodescript(script) + assert_equal(result, rpc_result) + def run_test(self): + self.log.info("Test decoding of standard input scripts [scriptSig]") self.decodescript_script_sig() + self.log.info("Test decoding of standard output scripts [scriptPubKey]") self.decodescript_script_pub_key() + self.log.info("Test 'asm' script decoding of transactions") self.decoderawtransaction_asm_sighashtype() + self.log.info("Data-driven tests") + self.decodescript_datadriven_tests() if __name__ == '__main__': DecodeScriptTest().main() diff --git a/test/functional/rpc_deprecated.py b/test/functional/rpc_deprecated.py index 1af79b9f7c..15c77ed856 100755 --- a/test/functional/rpc_deprecated.py +++ b/test/functional/rpc_deprecated.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test deprecation of RPC calls.""" @@ -21,7 +21,7 @@ class DeprecatedRpcTest(BitcoinTestFramework): # In run_test: # self.log.info("Test generate RPC") # assert_raises_rpc_error(-32, 'The wallet generate rpc method is deprecated', self.nodes[0].rpc.generate, 1) - # self.nodes[1].generate(1) + # self.generate(self.nodes[1], 1) self.log.info("No tested deprecated RPC methods") diff --git a/test/functional/rpc_dumptxoutset.py b/test/functional/rpc_dumptxoutset.py index e65787ce08..1721b6ffe8 100755 --- a/test/functional/rpc_dumptxoutset.py +++ b/test/functional/rpc_dumptxoutset.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the generation of UTXO snapshots using `dumptxoutset`. """ + +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error @@ -21,7 +23,7 @@ class DumptxoutsetTest(BitcoinTestFramework): node = self.nodes[0] mocktime = node.getblockheader(node.getblockhash(0))['time'] + 1 node.setmocktime(mocktime) - node.generate(100) + self.generate(node, COINBASE_MATURITY) FILENAME = 'txoutset.dat' out = node.dumptxoutset(FILENAME) @@ -41,7 +43,11 @@ class DumptxoutsetTest(BitcoinTestFramework): digest = hashlib.sha256(f.read()).hexdigest() # UTXO snapshot hash should be deterministic based on mocked time. assert_equal( - digest, 'be032e5f248264ba08e11099ac09dbd001f6f87ffc68bf0f87043d8146d50664') + digest, '7ae82c986fa5445678d2a21453bb1c86d39e47af13da137640c2b1cf8093691c') + + assert_equal( + out['txoutset_hash'], 'd4b614f476b99a6e569973bf1c0120d88b1a168076f8ce25691fb41dd1cef149') + assert_equal(out['nchaintx'], 101) # Specifying a path to an existing file will fail. assert_raises_rpc_error( diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 569471dc87..759e43194b 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -1,11 +1,16 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the fundrawtransaction RPC.""" + from decimal import Decimal +from itertools import product +from math import ceil + from test_framework.descriptors import descsum_create +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_approx, @@ -17,6 +22,7 @@ from test_framework.util import ( count_bytes, find_vout_for_address, ) +from test_framework.wallet_util import bytes_to_wif def get_unspent(listunspent, amount): @@ -32,6 +38,7 @@ class RawTransactionsTest(BitcoinTestFramework): # This test isn't testing tx relay. Set whitelist on the peers for # instant tx relay. self.extra_args = [['-whitelist=noban@127.0.0.1']] * self.num_nodes + self.rpc_timeout = 90 # to prevent timeouts in `test_transaction_too_large` def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -44,7 +51,40 @@ class RawTransactionsTest(BitcoinTestFramework): self.connect_nodes(0, 2) self.connect_nodes(0, 3) + def lock_outputs_type(self, wallet, outputtype): + """ + Only allow UTXOs of the given type + """ + if outputtype in ["legacy", "p2pkh", "pkh"]: + prefixes = ["pkh(", "sh(multi("] + elif outputtype in ["p2sh-segwit", "sh_wpkh"]: + prefixes = ["sh(wpkh(", "sh(wsh("] + elif outputtype in ["bech32", "wpkh"]: + prefixes = ["wpkh(", "wsh("] + else: + assert False, f"Unknown output type {outputtype}" + + to_lock = [] + for utxo in wallet.listunspent(): + if "desc" in utxo: + for prefix in prefixes: + if utxo["desc"].startswith(prefix): + to_lock.append({"txid": utxo["txid"], "vout": utxo["vout"]}) + wallet.lockunspent(False, to_lock) + + def unlock_utxos(self, wallet): + """ + Unlock all UTXOs except the watchonly one + """ + to_keep = [] + if self.watchonly_txid is not None and self.watchonly_vout is not None: + to_keep.append({"txid": self.watchonly_txid, "vout": self.watchonly_vout}) + wallet.lockunspent(True) + wallet.lockunspent(False, to_keep) + def run_test(self): + self.watchonly_txid = None + self.watchonly_vout = None self.log.info("Connect nodes, set fees, generate blocks, and sync") self.min_relay_tx_fee = self.nodes[0].getnetworkinfo()['relayfee'] # This test is not meant to test fee estimation and we'd like @@ -60,10 +100,8 @@ class RawTransactionsTest(BitcoinTestFramework): # = 2 bytes * minRelayTxFeePerByte self.fee_tolerance = 2 * self.min_relay_tx_fee / 1000 - self.nodes[2].generate(1) - self.sync_all() - self.nodes[0].generate(121) - self.sync_all() + self.generate(self.nodes[2], 1) + self.generate(self.nodes[0], 121) self.test_change_position() self.test_simple() @@ -94,6 +132,11 @@ class RawTransactionsTest(BitcoinTestFramework): self.test_address_reuse() self.test_option_subtract_fee_from_outputs() self.test_subtract_fee_with_presets() + self.test_transaction_too_large() + self.test_include_unsafe() + self.test_external_inputs() + self.test_22670() + self.test_feerate_rounding() def test_change_position(self): """Ensure setting changePosition in fundraw with an exact match is handled properly.""" @@ -120,8 +163,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1.0) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 5.0) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) wwatch.unloadwallet() @@ -246,7 +288,7 @@ class RawTransactionsTest(BitcoinTestFramework): rawtxfund = self.nodes[2].fundrawtransaction(rawtx, {'changeAddress': change, 'changePosition': 0}) dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex']) out = dec_tx['vout'][0] - assert_equal(change, out['scriptPubKey']['addresses'][0]) + assert_equal(change, out['scriptPubKey']['address']) def test_change_type(self): self.log.info("Test fundrawtxn with a provided change type") @@ -286,7 +328,7 @@ class RawTransactionsTest(BitcoinTestFramework): matchingOuts = 0 for i, out in enumerate(dec_tx['vout']): totalOut += out['value'] - if out['scriptPubKey']['addresses'][0] in outputs: + if out['scriptPubKey']['address'] in outputs: matchingOuts+=1 else: assert_equal(i, rawtxfund['changepos']) @@ -317,7 +359,7 @@ class RawTransactionsTest(BitcoinTestFramework): matchingOuts = 0 for out in dec_tx['vout']: totalOut += out['value'] - if out['scriptPubKey']['addresses'][0] in outputs: + if out['scriptPubKey']['address'] in outputs: matchingOuts+=1 assert_equal(matchingOuts, 1) @@ -351,7 +393,7 @@ class RawTransactionsTest(BitcoinTestFramework): matchingOuts = 0 for out in dec_tx['vout']: totalOut += out['value'] - if out['scriptPubKey']['addresses'][0] in outputs: + if out['scriptPubKey']['address'] in outputs: matchingOuts+=1 assert_equal(matchingOuts, 2) @@ -367,6 +409,7 @@ class RawTransactionsTest(BitcoinTestFramework): def test_fee_p2pkh(self): """Compare fee of a standard pubkeyhash transaction.""" self.log.info("Test fundrawtxn p2pkh fee") + self.lock_outputs_type(self.nodes[0], "p2pkh") inputs = [] outputs = {self.nodes[1].getnewaddress():1.1} rawtx = self.nodes[0].createrawtransaction(inputs, outputs) @@ -374,15 +417,18 @@ class RawTransactionsTest(BitcoinTestFramework): # Create same transaction over sendtoaddress. txId = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1.1) - signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] + signedFee = self.nodes[0].getmempoolentry(txId)['fees']['base'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance + self.unlock_utxos(self.nodes[0]) + def test_fee_p2pkh_multi_out(self): """Compare fee of a standard pubkeyhash transaction with multiple outputs.""" self.log.info("Test fundrawtxn p2pkh fee with multiple outputs") + self.lock_outputs_type(self.nodes[0], "p2pkh") inputs = [] outputs = { self.nodes[1].getnewaddress():1.1, @@ -397,14 +443,17 @@ class RawTransactionsTest(BitcoinTestFramework): # Create same transaction over sendtoaddress. txId = self.nodes[0].sendmany("", outputs) - signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] + signedFee = self.nodes[0].getmempoolentry(txId)['fees']['base'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance + self.unlock_utxos(self.nodes[0]) + def test_fee_p2sh(self): """Compare fee of a 2-of-2 multisig p2sh transaction.""" + self.lock_outputs_type(self.nodes[0], "p2pkh") # Create 2-of-2 addr. addr1 = self.nodes[1].getnewaddress() addr2 = self.nodes[1].getnewaddress() @@ -421,15 +470,18 @@ class RawTransactionsTest(BitcoinTestFramework): # Create same transaction over sendtoaddress. txId = self.nodes[0].sendtoaddress(mSigObj, 1.1) - signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] + signedFee = self.nodes[0].getmempoolentry(txId)['fees']['base'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance + self.unlock_utxos(self.nodes[0]) + def test_fee_4of5(self): """Compare fee of a standard pubkeyhash transaction.""" self.log.info("Test fundrawtxn fee with 4-of-5 addresses") + self.lock_outputs_type(self.nodes[0], "p2pkh") # Create 4-of-5 addr. addr1 = self.nodes[1].getnewaddress() @@ -462,12 +514,14 @@ class RawTransactionsTest(BitcoinTestFramework): # Create same transaction over sendtoaddress. txId = self.nodes[0].sendtoaddress(mSigObj, 1.1) - signedFee = self.nodes[0].getrawmempool(True)[txId]['fee'] + signedFee = self.nodes[0].getmempoolentry(txId)['fees']['base'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance + self.unlock_utxos(self.nodes[0]) + def test_spend_2of2(self): """Spend a 2-of-2 multisig transaction over fundraw.""" self.log.info("Test fundpsbt spending 2-of-2 multisig") @@ -494,8 +548,7 @@ class RawTransactionsTest(BitcoinTestFramework): # Send 1.2 BTC to msig addr. self.nodes[0].sendtoaddress(mSigObj, 1.2) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) oldBalance = self.nodes[1].getbalance() inputs = [] @@ -505,8 +558,7 @@ class RawTransactionsTest(BitcoinTestFramework): signed_psbt = w2.walletprocesspsbt(funded_psbt) final_psbt = w2.finalizepsbt(signed_psbt['psbt']) self.nodes[2].sendrawtransaction(final_psbt['hex']) - self.nodes[2].generate(1) - self.sync_all() + self.generate(self.nodes[2], 1) # Make sure funds are received at node1. assert_equal(oldBalance+Decimal('1.10000000'), self.nodes[1].getbalance()) @@ -536,17 +588,20 @@ class RawTransactionsTest(BitcoinTestFramework): # Drain the keypool. self.nodes[1].getnewaddress() self.nodes[1].getrawchangeaddress() - inputs = [] - outputs = {self.nodes[0].getnewaddress():1.09999500} + + # Choose 2 inputs + inputs = self.nodes[1].listunspent()[0:2] + value = sum(inp["amount"] for inp in inputs) - Decimal("0.00000500") # Pay a 500 sat fee + outputs = {self.nodes[0].getnewaddress():value} rawtx = self.nodes[1].createrawtransaction(inputs, outputs) # fund a transaction that does not require a new key for the change output self.nodes[1].fundrawtransaction(rawtx) # fund a transaction that requires a new key for the change output # creating the key must be impossible because the wallet is locked - outputs = {self.nodes[0].getnewaddress():1.1} + outputs = {self.nodes[0].getnewaddress():value - Decimal("0.1")} rawtx = self.nodes[1].createrawtransaction(inputs, outputs) - assert_raises_rpc_error(-4, "Transaction needs a change address, but we can't generate it. Please call keypoolrefill first.", self.nodes[1].fundrawtransaction, rawtx) + assert_raises_rpc_error(-4, "Transaction needs a change address, but we can't generate it.", self.nodes[1].fundrawtransaction, rawtx) # Refill the keypool. self.nodes[1].walletpassphrase("test", 100) @@ -566,8 +621,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[1].walletpassphrase("test", 600) signedTx = self.nodes[1].signrawtransactionwithwallet(fundedTx['hex']) self.nodes[1].sendrawtransaction(signedTx['hex']) - self.nodes[1].generate(1) - self.sync_all() + self.generate(self.nodes[1], 1) # Make sure funds are received at node1. assert_equal(oldBalance+Decimal('51.10000000'), self.nodes[0].getbalance()) @@ -578,13 +632,11 @@ class RawTransactionsTest(BitcoinTestFramework): # Empty node1, send some small coins from node0 to node1. self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), self.nodes[1].getbalance(), "", "", True) - self.nodes[1].generate(1) - self.sync_all() + self.generate(self.nodes[1], 1) for _ in range(20): self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.01) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # Fund a tx with ~20 small inputs. inputs = [] @@ -594,7 +646,7 @@ class RawTransactionsTest(BitcoinTestFramework): # Create same transaction over sendtoaddress. txId = self.nodes[1].sendmany("", outputs) - signedFee = self.nodes[1].getrawmempool(True)[txId]['fee'] + signedFee = self.nodes[1].getmempoolentry(txId)['fees']['base'] # Compare fee. feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) @@ -606,13 +658,11 @@ class RawTransactionsTest(BitcoinTestFramework): # Again, empty node1, send some small coins from node0 to node1. self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), self.nodes[1].getbalance(), "", "", True) - self.nodes[1].generate(1) - self.sync_all() + self.generate(self.nodes[1], 1) for _ in range(20): self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.01) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # Fund a tx with ~20 small inputs. oldBalance = self.nodes[0].getbalance() @@ -623,8 +673,7 @@ class RawTransactionsTest(BitcoinTestFramework): fundedTx = self.nodes[1].fundrawtransaction(rawtx) fundedAndSignedTx = self.nodes[1].signrawtransactionwithwallet(fundedTx['hex']) self.nodes[1].sendrawtransaction(fundedAndSignedTx['hex']) - self.nodes[1].generate(1) - self.sync_all() + self.generate(self.nodes[1], 1) assert_equal(oldBalance+Decimal('50.19000000'), self.nodes[0].getbalance()) #0.19+block reward def test_op_return(self): @@ -701,8 +750,7 @@ class RawTransactionsTest(BitcoinTestFramework): signedtx = self.nodes[0].signrawtransactionwithwallet(signedtx["hex"]) assert signedtx["complete"] self.nodes[0].sendrawtransaction(signedtx["hex"]) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) wwatch.unloadwallet() @@ -721,17 +769,16 @@ class RawTransactionsTest(BitcoinTestFramework): result2 = node.fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee}) result3 = node.fundrawtransaction(rawtx, {"fee_rate": 10 * btc_kvb_to_sat_vb * self.min_relay_tx_fee}) result4 = node.fundrawtransaction(rawtx, {"feeRate": str(10 * self.min_relay_tx_fee)}) - # Test that funding non-standard "zero-fee" transactions is valid. - result5 = self.nodes[3].fundrawtransaction(rawtx, {"fee_rate": 0}) - result6 = self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 0}) result_fee_rate = result['fee'] * 1000 / count_bytes(result['hex']) - assert_fee_amount(result1['fee'], count_bytes(result2['hex']), 2 * result_fee_rate) + assert_fee_amount(result1['fee'], count_bytes(result1['hex']), 2 * result_fee_rate) assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate) assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate) - assert_fee_amount(result4['fee'], count_bytes(result3['hex']), 10 * result_fee_rate) - assert_fee_amount(result5['fee'], count_bytes(result5['hex']), 0) - assert_fee_amount(result6['fee'], count_bytes(result6['hex']), 0) + assert_fee_amount(result4['fee'], count_bytes(result4['hex']), 10 * result_fee_rate) + + # Test that funding non-standard "zero-fee" transactions is valid. + for param, zero_value in product(["fee_rate", "feeRate"], [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]): + assert_equal(self.nodes[3].fundrawtransaction(rawtx, {param: zero_value})["fee"], 0) # With no arguments passed, expect fee of 141 satoshis. assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000141, vspan=0.00000001) @@ -765,11 +812,16 @@ class RawTransactionsTest(BitcoinTestFramework): node.fundrawtransaction, rawtx, {param: -1, "add_inputs": True}) assert_raises_rpc_error(-3, "Amount is not a number or string", node.fundrawtransaction, rawtx, {param: {"foo": "bar"}, "add_inputs": True}) + # Test fee rate values that don't pass fixed-point parsing checks. + for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]: + assert_raises_rpc_error(-3, "Invalid amount", node.fundrawtransaction, rawtx, {param: invalid_value, "add_inputs": True}) + # Test fee_rate values that cannot be represented in sat/vB. + for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]: assert_raises_rpc_error(-3, "Invalid amount", - node.fundrawtransaction, rawtx, {param: "", "add_inputs": True}) + node.fundrawtransaction, rawtx, {"fee_rate": invalid_value, "add_inputs": True}) self.log.info("Test min fee rate checks are bypassed with fundrawtxn, e.g. a fee_rate under 1 sat/vB is allowed") - node.fundrawtransaction(rawtx, {"fee_rate": 0.99999999, "add_inputs": True}) + node.fundrawtransaction(rawtx, {"fee_rate": 0.999, "add_inputs": True}) node.fundrawtransaction(rawtx, {"feeRate": 0.00000999, "add_inputs": True}) self.log.info("- raises RPC error if both feeRate and fee_rate are passed") @@ -800,7 +852,7 @@ class RawTransactionsTest(BitcoinTestFramework): changeaddress = "" for out in res_dec['vout']: if out['value'] > 1.0: - changeaddress += out['scriptPubKey']['addresses'][0] + changeaddress += out['scriptPubKey']['address'] assert changeaddress != "" nextaddr = self.nodes[3].getnewaddress() # Now the change address key should be removed from the keypool. @@ -907,5 +959,233 @@ class RawTransactionsTest(BitcoinTestFramework): signedtx = self.nodes[0].signrawtransactionwithwallet(fundedtx['hex']) self.nodes[0].sendrawtransaction(signedtx['hex']) + def test_transaction_too_large(self): + self.log.info("Test fundrawtx where BnB solution would result in a too large transaction, but Knapsack would not") + self.nodes[0].createwallet("large") + wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + recipient = self.nodes[0].get_wallet_rpc("large") + outputs = {} + rawtx = recipient.createrawtransaction([], {wallet.getnewaddress(): 147.99899260}) + + # Make 1500 0.1 BTC outputs. The amount that we target for funding is in + # the BnB range when these outputs are used. However if these outputs + # are selected, the transaction will end up being too large, so it + # shouldn't use BnB and instead fall back to Knapsack but that behavior + # is not implemented yet. For now we just check that we get an error. + for _ in range(1500): + outputs[recipient.getnewaddress()] = 0.1 + wallet.sendmany("", outputs) + self.generate(self.nodes[0], 10) + assert_raises_rpc_error(-4, "Transaction too large", recipient.fundrawtransaction, rawtx) + self.nodes[0].unloadwallet("large") + + def test_external_inputs(self): + self.log.info("Test funding with external inputs") + + eckey = ECKey() + eckey.generate() + privkey = bytes_to_wif(eckey.get_bytes()) + + self.nodes[2].createwallet("extfund") + wallet = self.nodes[2].get_wallet_rpc("extfund") + + # Make a weird but signable script. sh(pkh()) descriptor accomplishes this + desc = descsum_create("sh(pkh({}))".format(privkey)) + if self.options.descriptors: + res = self.nodes[0].importdescriptors([{"desc": desc, "timestamp": "now"}]) + else: + res = self.nodes[0].importmulti([{"desc": desc, "timestamp": "now"}]) + assert res[0]["success"] + addr = self.nodes[0].deriveaddresses(desc)[0] + addr_info = self.nodes[0].getaddressinfo(addr) + + self.nodes[0].sendtoaddress(addr, 10) + self.nodes[0].sendtoaddress(wallet.getnewaddress(), 10) + self.generate(self.nodes[0], 6) + ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0] + + # An external input without solving data should result in an error + raw_tx = wallet.createrawtransaction([ext_utxo], {self.nodes[0].getnewaddress(): ext_utxo["amount"] / 2}) + assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, raw_tx) + + # Error conditions + assert_raises_rpc_error(-5, "'not a pubkey' is not hex", wallet.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["not a pubkey"]}}) + assert_raises_rpc_error(-5, "'01234567890a0b0c0d0e0f' is not a valid public key", wallet.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["01234567890a0b0c0d0e0f"]}}) + assert_raises_rpc_error(-5, "'not a script' is not hex", wallet.fundrawtransaction, raw_tx, {"solving_data": {"scripts":["not a script"]}}) + assert_raises_rpc_error(-8, "Unable to parse descriptor 'not a descriptor'", wallet.fundrawtransaction, raw_tx, {"solving_data": {"descriptors":["not a descriptor"]}}) + assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"]}]}) + assert_raises_rpc_error(-8, "Invalid parameter, vout cannot be negative", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": -1}]}) + assert_raises_rpc_error(-8, "Invalid parameter, missing weight key", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"]}]}) + assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be less than 165", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 164}]}) + assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be less than 165", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": -1}]}) + assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be greater than", wallet.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 400001}]}) + + # But funding should work when the solving data is provided + funded_tx = wallet.fundrawtransaction(raw_tx, {"solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}}) + signed_tx = wallet.signrawtransactionwithwallet(funded_tx['hex']) + assert not signed_tx['complete'] + signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx['hex']) + assert signed_tx['complete'] + + funded_tx = wallet.fundrawtransaction(raw_tx, {"solving_data": {"descriptors": [desc]}}) + signed_tx1 = wallet.signrawtransactionwithwallet(funded_tx['hex']) + assert not signed_tx1['complete'] + signed_tx2 = self.nodes[0].signrawtransactionwithwallet(signed_tx1['hex']) + assert signed_tx2['complete'] + + unsigned_weight = self.nodes[0].decoderawtransaction(signed_tx1["hex"])["weight"] + signed_weight = self.nodes[0].decoderawtransaction(signed_tx2["hex"])["weight"] + # Input's weight is difference between weight of signed and unsigned, + # and the weight of stuff that didn't change (prevout, sequence, 1 byte of scriptSig) + input_weight = signed_weight - unsigned_weight + (41 * 4) + low_input_weight = input_weight // 2 + high_input_weight = input_weight * 2 + + # Funding should also work if the input weight is provided + funded_tx = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}]}) + signed_tx = wallet.signrawtransactionwithwallet(funded_tx["hex"]) + signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx["hex"]) + assert_equal(self.nodes[0].testmempoolaccept([signed_tx["hex"]])[0]["allowed"], True) + assert_equal(signed_tx["complete"], True) + # Reducing the weight should have a lower fee + funded_tx2 = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": low_input_weight}]}) + assert_greater_than(funded_tx["fee"], funded_tx2["fee"]) + # Increasing the weight should have a higher fee + funded_tx2 = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}]}) + assert_greater_than(funded_tx2["fee"], funded_tx["fee"]) + # The provided weight should override the calculated weight when solving data is provided + funded_tx3 = wallet.fundrawtransaction(raw_tx, {"solving_data": {"descriptors": [desc]}, "input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}]}) + assert_equal(funded_tx2["fee"], funded_tx3["fee"]) + # The feerate should be met + funded_tx4 = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}], "fee_rate": 10}) + input_add_weight = high_input_weight - (41 * 4) + tx4_weight = wallet.decoderawtransaction(funded_tx4["hex"])["weight"] + input_add_weight + tx4_vsize = int(ceil(tx4_weight / 4)) + assert_fee_amount(funded_tx4["fee"], tx4_vsize, Decimal(0.0001)) + + # Funding with weight at csuint boundaries should not cause problems + funded_tx = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 255}]}) + funded_tx = wallet.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 65539}]}) + + self.nodes[2].unloadwallet("extfund") + + def test_include_unsafe(self): + self.log.info("Test fundrawtxn with unsafe inputs") + + self.nodes[0].createwallet("unsafe") + wallet = self.nodes[0].get_wallet_rpc("unsafe") + + # We receive unconfirmed funds from external keys (unsafe outputs). + addr = wallet.getnewaddress() + inputs = [] + for i in range(0, 2): + txid = self.nodes[2].sendtoaddress(addr, 5) + self.sync_mempools() + vout = find_vout_for_address(wallet, txid, addr) + inputs.append((txid, vout)) + + # Unsafe inputs are ignored by default. + rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 7.5}]) + assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, rawtx) + + # But we can opt-in to use them for funding. + fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": True}) + tx_dec = wallet.decoderawtransaction(fundedtx['hex']) + assert all((txin["txid"], txin["vout"]) in inputs for txin in tx_dec["vin"]) + signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex']) + assert wallet.testmempoolaccept([signedtx['hex']])[0]["allowed"] + + # And we can also use them once they're confirmed. + self.generate(self.nodes[0], 1) + fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": False}) + tx_dec = wallet.decoderawtransaction(fundedtx['hex']) + assert all((txin["txid"], txin["vout"]) in inputs for txin in tx_dec["vin"]) + signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex']) + assert wallet.testmempoolaccept([signedtx['hex']])[0]["allowed"] + self.nodes[0].unloadwallet("unsafe") + + def test_22670(self): + # In issue #22670, it was observed that ApproximateBestSubset may + # choose enough value to cover the target amount but not enough to cover the transaction fees. + # This leads to a transaction whose actual transaction feerate is lower than expected. + # However at normal feerates, the difference between the effective value and the real value + # that this bug is not detected because the transaction fee must be at least 0.01 BTC (the minimum change value). + # Otherwise the targeted minimum change value will be enough to cover the transaction fees that were not + # being accounted for. So the minimum relay fee is set to 0.1 BTC/kvB in this test. + self.log.info("Test issue 22670 ApproximateBestSubset bug") + # Make sure the default wallet will not be loaded when restarted with a high minrelaytxfee + self.nodes[0].unloadwallet(self.default_wallet_name, False) + feerate = Decimal("0.1") + self.restart_node(0, [f"-minrelaytxfee={feerate}", "-discardfee=0"]) # Set high minrelayfee, set discardfee to 0 for easier calculation + + self.nodes[0].loadwallet(self.default_wallet_name, True) + funds = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + self.nodes[0].createwallet(wallet_name="tester") + tester = self.nodes[0].get_wallet_rpc("tester") + + # Because this test is specifically for ApproximateBestSubset, the target value must be greater + # than any single input available, and require more than 1 input. So we make 3 outputs + for i in range(0, 3): + funds.sendtoaddress(tester.getnewaddress(address_type="bech32"), 1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) + + # Create transactions in order to calculate fees for the target bounds that can trigger this bug + change_tx = tester.fundrawtransaction(tester.createrawtransaction([], [{funds.getnewaddress(): 1.5}])) + tx = tester.createrawtransaction([], [{funds.getnewaddress(): 2}]) + no_change_tx = tester.fundrawtransaction(tx, {"subtractFeeFromOutputs": [0]}) + + overhead_fees = feerate * len(tx) / 2 / 1000 + cost_of_change = change_tx["fee"] - no_change_tx["fee"] + fees = no_change_tx["fee"] + assert_greater_than(fees, 0.01) + + def do_fund_send(target): + create_tx = tester.createrawtransaction([], [{funds.getnewaddress(): target}]) + funded_tx = tester.fundrawtransaction(create_tx) + signed_tx = tester.signrawtransactionwithwallet(funded_tx["hex"]) + assert signed_tx["complete"] + decoded_tx = tester.decoderawtransaction(signed_tx["hex"]) + assert_equal(len(decoded_tx["vin"]), 3) + assert tester.testmempoolaccept([signed_tx["hex"]])[0]["allowed"] + + # We want to choose more value than is available in 2 inputs when considering the fee, + # but not enough to need 3 inputs when not considering the fee. + # So the target value must be at least 2.00000001 - fee. + lower_bound = Decimal("2.00000001") - fees + # The target value must be at most 2 - cost_of_change - not_input_fees - min_change (these are all + # included in the target before ApproximateBestSubset). + upper_bound = Decimal("2.0") - cost_of_change - overhead_fees - Decimal("0.01") + assert_greater_than_or_equal(upper_bound, lower_bound) + do_fund_send(lower_bound) + do_fund_send(upper_bound) + + self.restart_node(0) + self.connect_nodes(0, 1) + self.connect_nodes(0, 2) + self.connect_nodes(0, 3) + + def test_feerate_rounding(self): + self.log.info("Test that rounding of GetFee does not result in an assertion") + + self.nodes[1].createwallet("roundtest") + w = self.nodes[1].get_wallet_rpc("roundtest") + + addr = w.getnewaddress(address_type="bech32") + self.nodes[0].sendtoaddress(addr, 1) + self.generate(self.nodes[0], 1) + + # A P2WPKH input costs 68 vbytes; With a single P2WPKH output, the rest of the tx is 42 vbytes for a total of 110 vbytes. + # At a feerate of 1.85 sat/vb, the input will need a fee of 125.8 sats and the rest 77.7 sats + # The entire tx fee should be 203.5 sats. + # Coin selection rounds the fee individually instead of at the end (due to how CFeeRate::GetFee works). + # If rounding down (which is the incorrect behavior), then the calculated fee will be 125 + 77 = 202. + # If rounding up, then the calculated fee will be 126 + 78 = 204. + # In the former case, the calculated needed fee is higher than the actual fee being paid, so an assertion is reached + # To test this does not happen, we subtract 202 sats from the input value. If working correctly, this should + # fail with insufficient funds rather than bitcoind asserting. + rawtx = w.createrawtransaction(inputs=[], outputs=[{self.nodes[0].getnewaddress(address_type="bech32"): 1 - 0.00000202}]) + assert_raises_rpc_error(-4, "Insufficient funds", w.fundrawtransaction, rawtx, {"fee_rate": 1.85}) + + if __name__ == '__main__': RawTransactionsTest().main() diff --git a/test/functional/rpc_generate.py b/test/functional/rpc_generate.py index e55f2e6d12..47d7814da3 100755 --- a/test/functional/rpc_generate.py +++ b/test/functional/rpc_generate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test generate RPC.""" @@ -17,9 +17,9 @@ class RPCGenerateTest(BitcoinTestFramework): def run_test(self): message = ( - "generate\n" + "generate\n\n" "has been replaced by the -generate " - "cli option. Refer to -help for more information." + "cli option. Refer to -help for more information.\n" ) self.log.info("Test rpc generate raises with message to use cli option") diff --git a/test/functional/rpc_generateblock.py b/test/functional/rpc_generateblock.py index 08ff0fba50..7eeb745817 100755 --- a/test/functional/rpc_generateblock.py +++ b/test/functional/rpc_generateblock.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. '''Test generateblock rpc. ''' from test_framework.test_framework import BitcoinTestFramework +from test_framework.wallet import MiniWallet from test_framework.util import ( assert_equal, assert_raises_rpc_error, @@ -16,91 +17,84 @@ class GenerateBlockTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def run_test(self): node = self.nodes[0] + miniwallet = MiniWallet(node) + miniwallet.rescan_utxos() self.log.info('Generate an empty block to address') - address = node.getnewaddress() - hash = node.generateblock(output=address, transactions=[])['hash'] + address = miniwallet.get_address() + hash = self.generateblock(node, output=address, transactions=[])['hash'] block = node.getblock(blockhash=hash, verbose=2) assert_equal(len(block['tx']), 1) - assert_equal(block['tx'][0]['vout'][0]['scriptPubKey']['addresses'][0], address) + assert_equal(block['tx'][0]['vout'][0]['scriptPubKey']['address'], address) self.log.info('Generate an empty block to a descriptor') - hash = node.generateblock('addr(' + address + ')', [])['hash'] + hash = self.generateblock(node, 'addr(' + address + ')', [])['hash'] block = node.getblock(blockhash=hash, verbosity=2) assert_equal(len(block['tx']), 1) - assert_equal(block['tx'][0]['vout'][0]['scriptPubKey']['addresses'][0], address) + assert_equal(block['tx'][0]['vout'][0]['scriptPubKey']['address'], address) self.log.info('Generate an empty block to a combo descriptor with compressed pubkey') combo_key = '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798' combo_address = 'bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080' - hash = node.generateblock('combo(' + combo_key + ')', [])['hash'] + hash = self.generateblock(node, 'combo(' + combo_key + ')', [])['hash'] block = node.getblock(hash, 2) assert_equal(len(block['tx']), 1) - assert_equal(block['tx'][0]['vout'][0]['scriptPubKey']['addresses'][0], combo_address) + assert_equal(block['tx'][0]['vout'][0]['scriptPubKey']['address'], combo_address) self.log.info('Generate an empty block to a combo descriptor with uncompressed pubkey') combo_key = '0408ef68c46d20596cc3f6ddf7c8794f71913add807f1dc55949fa805d764d191c0b7ce6894c126fce0babc6663042f3dde9b0cf76467ea315514e5a6731149c67' combo_address = 'mkc9STceoCcjoXEXe6cm66iJbmjM6zR9B2' - hash = node.generateblock('combo(' + combo_key + ')', [])['hash'] + hash = self.generateblock(node, 'combo(' + combo_key + ')', [])['hash'] block = node.getblock(hash, 2) assert_equal(len(block['tx']), 1) - assert_equal(block['tx'][0]['vout'][0]['scriptPubKey']['addresses'][0], combo_address) - - # Generate 110 blocks to spend - node.generatetoaddress(110, address) + assert_equal(block['tx'][0]['vout'][0]['scriptPubKey']['address'], combo_address) # Generate some extra mempool transactions to verify they don't get mined for _ in range(10): - node.sendtoaddress(address, 0.001) + miniwallet.send_self_transfer(from_node=node) self.log.info('Generate block with txid') - txid = node.sendtoaddress(address, 1) - hash = node.generateblock(address, [txid])['hash'] + txid = miniwallet.send_self_transfer(from_node=node)['txid'] + hash = self.generateblock(node, address, [txid])['hash'] block = node.getblock(hash, 1) assert_equal(len(block['tx']), 2) assert_equal(block['tx'][1], txid) self.log.info('Generate block with raw tx') - utxos = node.listunspent(addresses=[address]) - raw = node.createrawtransaction([{'txid':utxos[0]['txid'], 'vout':utxos[0]['vout']}],[{address:1}]) - signed_raw = node.signrawtransactionwithwallet(raw)['hex'] - hash = node.generateblock(address, [signed_raw])['hash'] + rawtx = miniwallet.create_self_transfer()['hex'] + hash = self.generateblock(node, address, [rawtx])['hash'] + block = node.getblock(hash, 1) assert_equal(len(block['tx']), 2) txid = block['tx'][1] - assert_equal(node.gettransaction(txid)['hex'], signed_raw) + assert_equal(node.getrawtransaction(txid=txid, verbose=False, blockhash=hash), rawtx) self.log.info('Fail to generate block with out of order txs') - raw1 = node.createrawtransaction([{'txid':txid, 'vout':0}],[{address:0.9999}]) - signed_raw1 = node.signrawtransactionwithwallet(raw1)['hex'] - txid1 = node.sendrawtransaction(signed_raw1) - raw2 = node.createrawtransaction([{'txid':txid1, 'vout':0}],[{address:0.999}]) - signed_raw2 = node.signrawtransactionwithwallet(raw2)['hex'] - assert_raises_rpc_error(-25, 'TestBlockValidity failed: bad-txns-inputs-missingorspent', node.generateblock, address, [signed_raw2, txid1]) + txid1 = miniwallet.send_self_transfer(from_node=node)['txid'] + utxo1 = miniwallet.get_utxo(txid=txid1) + rawtx2 = miniwallet.create_self_transfer(utxo_to_spend=utxo1)['hex'] + assert_raises_rpc_error(-25, 'TestBlockValidity failed: bad-txns-inputs-missingorspent', self.generateblock, node, address, [rawtx2, txid1]) self.log.info('Fail to generate block with txid not in mempool') missing_txid = '0000000000000000000000000000000000000000000000000000000000000000' - assert_raises_rpc_error(-5, 'Transaction ' + missing_txid + ' not in mempool.', node.generateblock, address, [missing_txid]) + assert_raises_rpc_error(-5, 'Transaction ' + missing_txid + ' not in mempool.', self.generateblock, node, address, [missing_txid]) self.log.info('Fail to generate block with invalid raw tx') invalid_raw_tx = '0000' - assert_raises_rpc_error(-22, 'Transaction decode failed for ' + invalid_raw_tx, node.generateblock, address, [invalid_raw_tx]) + assert_raises_rpc_error(-22, 'Transaction decode failed for ' + invalid_raw_tx, self.generateblock, node, address, [invalid_raw_tx]) self.log.info('Fail to generate block with invalid address/descriptor') - assert_raises_rpc_error(-5, 'Invalid address or descriptor', node.generateblock, '1234', []) + assert_raises_rpc_error(-5, 'Invalid address or descriptor', self.generateblock, node, '1234', []) self.log.info('Fail to generate block with a ranged descriptor') ranged_descriptor = 'pkh(tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp/0/*)' - assert_raises_rpc_error(-8, 'Ranged descriptor not accepted. Maybe pass through deriveaddresses first?', node.generateblock, ranged_descriptor, []) + assert_raises_rpc_error(-8, 'Ranged descriptor not accepted. Maybe pass through deriveaddresses first?', self.generateblock, node, ranged_descriptor, []) self.log.info('Fail to generate block with a descriptor missing a private key') child_descriptor = 'pkh(tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp/0\'/0)' - assert_raises_rpc_error(-5, 'Cannot derive script without private keys', node.generateblock, child_descriptor, []) + assert_raises_rpc_error(-5, 'Cannot derive script without private keys', self.generateblock, node, child_descriptor, []) if __name__ == '__main__': GenerateBlockTest().main() diff --git a/test/functional/rpc_getblockfilter.py b/test/functional/rpc_getblockfilter.py index a99e50f29f..b09af9e078 100755 --- a/test/functional/rpc_getblockfilter.py +++ b/test/functional/rpc_getblockfilter.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the getblockfilter RPC.""" @@ -21,8 +21,8 @@ class GetBlockFilterTest(BitcoinTestFramework): # Create two chains by disconnecting nodes 0 & 1, mining, then reconnecting self.disconnect_nodes(0, 1) - self.nodes[0].generate(3) - self.nodes[1].generate(4) + self.generate(self.nodes[0], 3, sync_fun=self.no_op) + self.generate(self.nodes[1], 4, sync_fun=self.no_op) assert_equal(self.nodes[0].getblockcount(), 3) chain0_hashes = [self.nodes[0].getblockhash(block_height) for block_height in range(4)] diff --git a/test/functional/rpc_getblockfrompeer.py b/test/functional/rpc_getblockfrompeer.py new file mode 100755 index 0000000000..b65322d920 --- /dev/null +++ b/test/functional/rpc_getblockfrompeer.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the getblockfrompeer RPC.""" + +from test_framework.authproxy import JSONRPCException +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) + +class GetBlockFromPeerTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + + def setup_network(self): + self.setup_nodes() + + def check_for_block(self, hash): + try: + self.nodes[0].getblock(hash) + return True + except JSONRPCException: + return False + + def run_test(self): + self.log.info("Mine 4 blocks on Node 0") + self.generate(self.nodes[0], 4, sync_fun=self.no_op) + assert_equal(self.nodes[0].getblockcount(), 204) + + self.log.info("Mine competing 3 blocks on Node 1") + self.generate(self.nodes[1], 3, sync_fun=self.no_op) + assert_equal(self.nodes[1].getblockcount(), 203) + short_tip = self.nodes[1].getbestblockhash() + + self.log.info("Connect nodes to sync headers") + self.connect_nodes(0, 1) + self.sync_blocks() + + self.log.info("Node 0 should only have the header for node 1's block 3") + x = next(filter(lambda x: x['hash'] == short_tip, self.nodes[0].getchaintips())) + assert_equal(x['status'], "headers-only") + assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, short_tip) + + self.log.info("Fetch block from node 1") + peers = self.nodes[0].getpeerinfo() + assert_equal(len(peers), 1) + peer_0_peer_1_id = peers[0]["id"] + + self.log.info("Arguments must be sensible") + assert_raises_rpc_error(-8, "hash must be of length 64 (not 4, for '1234')", self.nodes[0].getblockfrompeer, "1234", 0) + + self.log.info("We must already have the header") + assert_raises_rpc_error(-1, "Block header missing", self.nodes[0].getblockfrompeer, "00" * 32, 0) + + self.log.info("Non-existent peer generates error") + assert_raises_rpc_error(-1, "Peer does not exist", self.nodes[0].getblockfrompeer, short_tip, peer_0_peer_1_id + 1) + + self.log.info("Successful fetch") + result = self.nodes[0].getblockfrompeer(short_tip, peer_0_peer_1_id) + self.wait_until(lambda: self.check_for_block(short_tip), timeout=1) + assert_equal(result, {}) + + self.log.info("Don't fetch blocks we already have") + assert_raises_rpc_error(-1, "Block already downloaded", self.nodes[0].getblockfrompeer, short_tip, peer_0_peer_1_id) + +if __name__ == '__main__': + GetBlockFromPeerTest().main() diff --git a/test/functional/rpc_getblockstats.py b/test/functional/rpc_getblockstats.py index 57794ae973..1ea1ee5659 100755 --- a/test/functional/rpc_getblockstats.py +++ b/test/functional/rpc_getblockstats.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2019 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # # Test getblockstats rpc call # + +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -41,19 +43,18 @@ class GetblockstatsTest(BitcoinTestFramework): def generate_test_data(self, filename): mocktime = 1525107225 self.nodes[0].setmocktime(mocktime) - self.nodes[0].generate(101) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) address = self.nodes[0].get_deterministic_priv_key().address self.nodes[0].sendtoaddress(address=address, amount=10, subtractfeefromamount=True) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) self.nodes[0].sendtoaddress(address=address, amount=10, subtractfeefromamount=True) self.nodes[0].sendtoaddress(address=address, amount=10, subtractfeefromamount=False) self.nodes[0].settxfee(amount=0.003) self.nodes[0].sendtoaddress(address=address, amount=1, subtractfeefromamount=True) self.sync_all() - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) self.expected_stats = self.get_stats() @@ -141,17 +142,17 @@ class GetblockstatsTest(BitcoinTestFramework): inv_sel_stat = 'asdfghjkl' inv_stats = [ [inv_sel_stat], - ['minfee' , inv_sel_stat], + ['minfee', inv_sel_stat], [inv_sel_stat, 'minfee'], ['minfee', inv_sel_stat, 'maxfee'], ] for inv_stat in inv_stats: - assert_raises_rpc_error(-8, 'Invalid selected statistic %s' % inv_sel_stat, + assert_raises_rpc_error(-8, f"Invalid selected statistic '{inv_sel_stat}'", self.nodes[0].getblockstats, hash_or_height=1, stats=inv_stat) # Make sure we aren't always returning inv_sel_stat as the culprit stat - assert_raises_rpc_error(-8, 'Invalid selected statistic aaa%s' % inv_sel_stat, - self.nodes[0].getblockstats, hash_or_height=1, stats=['minfee' , 'aaa%s' % inv_sel_stat]) + assert_raises_rpc_error(-8, f"Invalid selected statistic 'aaa{inv_sel_stat}'", + self.nodes[0].getblockstats, hash_or_height=1, stats=['minfee', f'aaa{inv_sel_stat}']) # Mainchain's genesis block shouldn't be found on regtest assert_raises_rpc_error(-5, 'Block not found', self.nodes[0].getblockstats, hash_or_height='000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f') diff --git a/test/functional/rpc_getchaintips.py b/test/functional/rpc_getchaintips.py index 8dc8474374..7efa306c8c 100755 --- a/test/functional/rpc_getchaintips.py +++ b/test/functional/rpc_getchaintips.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the getchaintips RPC. @@ -26,10 +26,8 @@ class GetChainTipsTest (BitcoinTestFramework): # Split the network and build two chains of different lengths. self.split_network() - self.nodes[0].generatetoaddress(10, self.nodes[0].get_deterministic_priv_key().address) - self.nodes[2].generatetoaddress(20, self.nodes[2].get_deterministic_priv_key().address) - self.sync_all(self.nodes[:2]) - self.sync_all(self.nodes[2:]) + self.generate(self.nodes[0], 10, sync_fun=lambda: self.sync_all(self.nodes[:2])) + self.generate(self.nodes[2], 20, sync_fun=lambda: self.sync_all(self.nodes[2:])) tips = self.nodes[1].getchaintips () assert_equal (len (tips), 1) diff --git a/test/functional/rpc_getdescriptorinfo.py b/test/functional/rpc_getdescriptorinfo.py index 563f54c5e7..5e6fd66aab 100755 --- a/test/functional/rpc_getdescriptorinfo.py +++ b/test/functional/rpc_getdescriptorinfo.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2022 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test getdescriptorinfo RPC. @@ -30,7 +30,7 @@ class DescriptorTest(BitcoinTestFramework): def run_test(self): assert_raises_rpc_error(-1, 'getdescriptorinfo', self.nodes[0].getdescriptorinfo) assert_raises_rpc_error(-3, 'Expected type string', self.nodes[0].getdescriptorinfo, 1) - assert_raises_rpc_error(-5, 'is not a valid descriptor function', self.nodes[0].getdescriptorinfo, '') + assert_raises_rpc_error(-5, "'' is not a valid descriptor function", self.nodes[0].getdescriptorinfo, "") # P2PK output with the specified public key. self.test_desc('pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)', isrange=False, issolvable=True, hasprivatekeys=False) diff --git a/test/functional/rpc_help.py b/test/functional/rpc_help.py index de21f43747..ccb380e25b 100755 --- a/test/functional/rpc_help.py +++ b/test/functional/rpc_help.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test RPC help output.""" diff --git a/test/functional/rpc_invalid_address_message.py b/test/functional/rpc_invalid_address_message.py index 469d6bdb05..fcc49d0a75 100755 --- a/test/functional/rpc_invalid_address_message.py +++ b/test/functional/rpc_invalid_address_message.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test error messages for 'getaddressinfo' and 'validateaddress' RPC commands.""" @@ -12,66 +12,103 @@ from test_framework.util import ( ) BECH32_VALID = 'bcrt1qtmp74ayg7p24uslctssvjm06q5phz4yrxucgnv' -BECH32_INVALID_SIZE = 'bcrt1sqqpl9r5c' -BECH32_INVALID_PREFIX = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4' +BECH32_VALID_CAPITALS = 'BCRT1QPLMTZKC2XHARPPZDLNPAQL78RSHJ68U33RAH7R' +BECH32_VALID_MULTISIG = 'bcrt1qdg3myrgvzw7ml9q0ejxhlkyxm7vl9r56yzkfgvzclrf4hkpx9yfqhpsuks' + +BECH32_INVALID_BECH32 = 'bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqdmchcc' +BECH32_INVALID_BECH32M = 'bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7k35mrzd' +BECH32_INVALID_VERSION = 'bcrt130xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqynjegk' +BECH32_INVALID_SIZE = 'bcrt1s0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav25430mtr' +BECH32_INVALID_V0_SIZE = 'bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kqqq5k3my' +BECH32_INVALID_PREFIX = 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx' +BECH32_TOO_LONG = 'bcrt1q049edschfnwystcqnsvyfpj23mpsg3jcedq9xv049edschfnwystcqnsvyfpj23mpsg3jcedq9xv049edschfnwystcqnsvyfpj23m' +BECH32_ONE_ERROR = 'bcrt1q049edschfnwystcqnsvyfpj23mpsg3jcedq9xv' +BECH32_ONE_ERROR_CAPITALS = 'BCRT1QPLMTZKC2XHARPPZDLNPAQL78RSHJ68U32RAH7R' +BECH32_TWO_ERRORS = 'bcrt1qax9suht3qv95sw33xavx8crpxduefdrsvgsklu' # should be bcrt1qax9suht3qv95sw33wavx8crpxduefdrsvgsklx +BECH32_NO_SEPARATOR = 'bcrtq049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv' +BECH32_INVALID_CHAR = 'bcrt1q04oldschfnwystcqnsvyfpj23mpsg3jcedq9xv' +BECH32_MULTISIG_TWO_ERRORS = 'bcrt1qdg3myrgvzw7ml8q0ejxhlkyxn7vl9r56yzkfgvzclrf4hkpx9yfqhpsuks' +BECH32_WRONG_VERSION = 'bcrt1ptmp74ayg7p24uslctssvjm06q5phz4yrxucgnv' BASE58_VALID = 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn' BASE58_INVALID_PREFIX = '17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem' +BASE58_INVALID_CHECKSUM = 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJJfn' +BASE58_INVALID_LENGTH = '2VKf7XKMrp4bVNVmuRbyCewkP8FhGLP2E54LHDPakr9Sq5mtU2' INVALID_ADDRESS = 'asfah14i8fajz0123f' +INVALID_ADDRESS_2 = '1q049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv' class InvalidAddressErrorMessageTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - - def test_validateaddress(self): - node = self.nodes[0] - - # Bech32 - info = node.validateaddress(BECH32_INVALID_SIZE) - assert not info['isvalid'] - assert_equal(info['error'], 'Invalid Bech32 address data size') - - info = node.validateaddress(BECH32_INVALID_PREFIX) - assert not info['isvalid'] - assert_equal(info['error'], 'Invalid prefix for Bech32 address') - - info = node.validateaddress(BECH32_VALID) + def check_valid(self, addr): + info = self.nodes[0].validateaddress(addr) assert info['isvalid'] assert 'error' not in info + assert 'error_locations' not in info - # Base58 - info = node.validateaddress(BASE58_INVALID_PREFIX) - assert not info['isvalid'] - assert_equal(info['error'], 'Invalid prefix for Base58-encoded address') + def check_invalid(self, addr, error_str, error_locations=None): + res = self.nodes[0].validateaddress(addr) + assert not res['isvalid'] + assert_equal(res['error'], error_str) + if error_locations: + assert_equal(res['error_locations'], error_locations) + else: + assert_equal(res['error_locations'], []) - info = node.validateaddress(BASE58_VALID) - assert info['isvalid'] - assert 'error' not in info + def test_validateaddress(self): + # Invalid Bech32 + self.check_invalid(BECH32_INVALID_SIZE, 'Invalid Bech32 address data size') + self.check_invalid(BECH32_INVALID_PREFIX, 'Not a valid Bech32 or Base58 encoding') + self.check_invalid(BECH32_INVALID_BECH32, 'Version 1+ witness address must use Bech32m checksum') + self.check_invalid(BECH32_INVALID_BECH32M, 'Version 0 witness address must use Bech32 checksum') + self.check_invalid(BECH32_INVALID_VERSION, 'Invalid Bech32 address witness version') + self.check_invalid(BECH32_INVALID_V0_SIZE, 'Invalid Bech32 v0 address data size') + self.check_invalid(BECH32_TOO_LONG, 'Bech32 string too long', list(range(90, 108))) + self.check_invalid(BECH32_ONE_ERROR, 'Invalid Bech32 checksum', [9]) + self.check_invalid(BECH32_TWO_ERRORS, 'Invalid Bech32 checksum', [22, 43]) + self.check_invalid(BECH32_ONE_ERROR_CAPITALS, 'Invalid Bech32 checksum', [38]) + self.check_invalid(BECH32_NO_SEPARATOR, 'Missing separator') + self.check_invalid(BECH32_INVALID_CHAR, 'Invalid Base 32 character', [8]) + self.check_invalid(BECH32_MULTISIG_TWO_ERRORS, 'Invalid Bech32 checksum', [19, 30]) + self.check_invalid(BECH32_WRONG_VERSION, 'Invalid Bech32 checksum', [5]) + + # Valid Bech32 + self.check_valid(BECH32_VALID) + self.check_valid(BECH32_VALID_CAPITALS) + self.check_valid(BECH32_VALID_MULTISIG) + + # Invalid Base58 + self.check_invalid(BASE58_INVALID_PREFIX, 'Invalid prefix for Base58-encoded address') + self.check_invalid(BASE58_INVALID_CHECKSUM, 'Invalid checksum or length of Base58 address') + self.check_invalid(BASE58_INVALID_LENGTH, 'Invalid checksum or length of Base58 address') + + # Valid Base58 + self.check_valid(BASE58_VALID) # Invalid address format - info = node.validateaddress(INVALID_ADDRESS) - assert not info['isvalid'] - assert_equal(info['error'], 'Invalid address format') + self.check_invalid(INVALID_ADDRESS, 'Not a valid Bech32 or Base58 encoding') + self.check_invalid(INVALID_ADDRESS_2, 'Not a valid Bech32 or Base58 encoding') def test_getaddressinfo(self): node = self.nodes[0] assert_raises_rpc_error(-5, "Invalid Bech32 address data size", node.getaddressinfo, BECH32_INVALID_SIZE) - assert_raises_rpc_error(-5, "Invalid prefix for Bech32 address", node.getaddressinfo, BECH32_INVALID_PREFIX) + assert_raises_rpc_error(-5, "Not a valid Bech32 or Base58 encoding", node.getaddressinfo, BECH32_INVALID_PREFIX) assert_raises_rpc_error(-5, "Invalid prefix for Base58-encoded address", node.getaddressinfo, BASE58_INVALID_PREFIX) - assert_raises_rpc_error(-5, "Invalid address format", node.getaddressinfo, INVALID_ADDRESS) + assert_raises_rpc_error(-5, "Not a valid Bech32 or Base58 encoding", node.getaddressinfo, INVALID_ADDRESS) def run_test(self): self.test_validateaddress() - self.test_getaddressinfo() + + if self.is_wallet_compiled(): + self.init_wallet(node=0) + self.test_getaddressinfo() if __name__ == '__main__': diff --git a/test/functional/rpc_invalidateblock.py b/test/functional/rpc_invalidateblock.py index 114bd5e7e9..f1c2537ef9 100755 --- a/test/functional/rpc_invalidateblock.py +++ b/test/functional/rpc_invalidateblock.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the invalidateblock RPC.""" @@ -22,12 +22,12 @@ class InvalidateTest(BitcoinTestFramework): def run_test(self): self.log.info("Make sure we repopulate setBlockIndexCandidates after InvalidateBlock:") self.log.info("Mine 4 blocks on Node 0") - self.nodes[0].generatetoaddress(4, self.nodes[0].get_deterministic_priv_key().address) + self.generate(self.nodes[0], 4, sync_fun=self.no_op) assert_equal(self.nodes[0].getblockcount(), 4) besthash_n0 = self.nodes[0].getbestblockhash() self.log.info("Mine competing 6 blocks on Node 1") - self.nodes[1].generatetoaddress(6, self.nodes[1].get_deterministic_priv_key().address) + self.generate(self.nodes[1], 6, sync_fun=self.no_op) assert_equal(self.nodes[1].getblockcount(), 6) self.log.info("Connect nodes to force a reorg") @@ -53,14 +53,14 @@ class InvalidateTest(BitcoinTestFramework): self.nodes[2].invalidateblock(self.nodes[2].getblockhash(3)) assert_equal(self.nodes[2].getblockcount(), 2) self.log.info("..and then mine a block") - self.nodes[2].generatetoaddress(1, self.nodes[2].get_deterministic_priv_key().address) + self.generate(self.nodes[2], 1, sync_fun=self.no_op) self.log.info("Verify all nodes are at the right height") self.wait_until(lambda: self.nodes[2].getblockcount() == 3, timeout=5) self.wait_until(lambda: self.nodes[0].getblockcount() == 4, timeout=5) self.wait_until(lambda: self.nodes[1].getblockcount() == 4, timeout=5) self.log.info("Verify that we reconsider all ancestors as well") - blocks = self.nodes[1].generatetodescriptor(10, ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR) + blocks = self.generatetodescriptor(self.nodes[1], 10, ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR, sync_fun=self.no_op) assert_equal(self.nodes[1].getbestblockhash(), blocks[-1]) # Invalidate the two blocks at the tip self.nodes[1].invalidateblock(blocks[-1]) @@ -72,7 +72,7 @@ class InvalidateTest(BitcoinTestFramework): assert_equal(self.nodes[1].getbestblockhash(), blocks[-1]) self.log.info("Verify that we reconsider all descendants") - blocks = self.nodes[1].generatetodescriptor(10, ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR) + blocks = self.generatetodescriptor(self.nodes[1], 10, ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR, sync_fun=self.no_op) assert_equal(self.nodes[1].getbestblockhash(), blocks[-1]) # Invalidate the two blocks at the tip self.nodes[1].invalidateblock(blocks[-2]) diff --git a/test/functional/rpc_mempool_entry_fee_fields_deprecation.py b/test/functional/rpc_mempool_entry_fee_fields_deprecation.py new file mode 100755 index 0000000000..82761ff7c8 --- /dev/null +++ b/test/functional/rpc_mempool_entry_fee_fields_deprecation.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test deprecation of fee fields from top level mempool entry object""" + +from test_framework.blocktools import COIN +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet + + +def assertions_helper(new_object, deprecated_object, deprecated_fields): + for field in deprecated_fields: + assert field in deprecated_object + assert field not in new_object + + +class MempoolFeeFieldsDeprecationTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.extra_args = [[], ["-deprecatedrpc=fees"]] + + def run_test(self): + # we get spendable outputs from the premined chain starting + # at block 76. see BitcoinTestFramework._initialize_chain() for details + self.wallet = MiniWallet(self.nodes[0]) + self.wallet.rescan_utxos() + + # we create the tx on the first node and wait until it syncs to node_deprecated + # thus, any differences must be coming from getmempoolentry or getrawmempool + tx = self.wallet.send_self_transfer(from_node=self.nodes[0]) + self.nodes[1].sendrawtransaction(tx["hex"]) + + deprecated_fields = ["ancestorfees", "descendantfees", "modifiedfee", "fee"] + self.test_getmempoolentry(tx["txid"], deprecated_fields) + self.test_getrawmempool(tx["txid"], deprecated_fields) + self.test_deprecated_fields_match(tx["txid"]) + + def test_getmempoolentry(self, txid, deprecated_fields): + + self.log.info("Test getmempoolentry rpc") + entry = self.nodes[0].getmempoolentry(txid) + deprecated_entry = self.nodes[1].getmempoolentry(txid) + assertions_helper(entry, deprecated_entry, deprecated_fields) + + def test_getrawmempool(self, txid, deprecated_fields): + + self.log.info("Test getrawmempool rpc") + entry = self.nodes[0].getrawmempool(verbose=True)[txid] + deprecated_entry = self.nodes[1].getrawmempool(verbose=True)[txid] + assertions_helper(entry, deprecated_entry, deprecated_fields) + + def test_deprecated_fields_match(self, txid): + + self.log.info("Test deprecated fee fields match new fees object") + entry = self.nodes[0].getmempoolentry(txid) + deprecated_entry = self.nodes[1].getmempoolentry(txid) + + assert_equal(deprecated_entry["fee"], entry["fees"]["base"]) + assert_equal(deprecated_entry["modifiedfee"], entry["fees"]["modified"]) + assert_equal(deprecated_entry["descendantfees"], entry["fees"]["descendant"] * COIN) + assert_equal(deprecated_entry["ancestorfees"], entry["fees"]["ancestor"] * COIN) + + +if __name__ == "__main__": + MempoolFeeFieldsDeprecationTest().main() diff --git a/test/functional/rpc_misc.py b/test/functional/rpc_misc.py index 1398d1237f..2f1796d7cc 100755 --- a/test/functional/rpc_misc.py +++ b/test/functional/rpc_misc.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test RPC misc output.""" @@ -50,41 +50,55 @@ class RpcMiscTest(BitcoinTestFramework): assert_equal(tree.tag, 'malloc') except JSONRPCException: self.log.info('getmemoryinfo(mode="mallocinfo") not available') - assert_raises_rpc_error(-8, 'mallocinfo is only available when compiled with glibc 2.10+', node.getmemoryinfo, mode="mallocinfo") + assert_raises_rpc_error(-8, 'mallocinfo mode not available', node.getmemoryinfo, mode="mallocinfo") assert_raises_rpc_error(-8, "unknown mode foobar", node.getmemoryinfo, mode="foobar") - self.log.info("test logging") + self.log.info("test logging rpc and help") + + # Test logging RPC returns the expected number of logging categories. + assert_equal(len(node.logging()), 27) + + # Test toggling a logging category on/off/on with the logging RPC. assert_equal(node.logging()['qt'], True) node.logging(exclude=['qt']) assert_equal(node.logging()['qt'], False) node.logging(include=['qt']) assert_equal(node.logging()['qt'], True) + # Test logging RPC returns the logging categories in alphabetical order. + sorted_logging_categories = sorted(node.logging()) + assert_equal(list(node.logging()), sorted_logging_categories) + + # Test logging help returns the logging categories string in alphabetical order. + categories = ', '.join(sorted_logging_categories) + logging_help = self.nodes[0].help('logging') + assert f"valid logging categories are: {categories}" in logging_help + + self.log.info("test echoipc (testing spawned process in multiprocess build)") + assert_equal(node.echoipc("hello"), "hello") + self.log.info("test getindexinfo") # Without any indices running the RPC returns an empty object assert_equal(node.getindexinfo(), {}) # Restart the node with indices and wait for them to sync - self.restart_node(0, ["-txindex", "-blockfilterindex"]) + self.restart_node(0, ["-txindex", "-blockfilterindex", "-coinstatsindex"]) self.wait_until(lambda: all(i["synced"] for i in node.getindexinfo().values())) # Returns a list of all running indices by default + values = {"synced": True, "best_block_height": 200} assert_equal( node.getindexinfo(), { - "txindex": {"synced": True, "best_block_height": 200}, - "basic block filter index": {"synced": True, "best_block_height": 200} + "txindex": values, + "basic block filter index": values, + "coinstatsindex": values, } ) - # Specifying an index by name returns only the status of that index - assert_equal( - node.getindexinfo("txindex"), - { - "txindex": {"synced": True, "best_block_height": 200}, - } - ) + for i in {"txindex", "basic block filter index", "coinstatsindex"}: + assert_equal(node.getindexinfo(i), {i: values}) # Specifying an unknown index name returns an empty result assert_equal(node.getindexinfo("foo"), {}) diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py index 9adb32c3c5..81a3cfee97 100755 --- a/test/functional/rpc_net.py +++ b/test/functional/rpc_net.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test RPC calls related to net. @@ -11,11 +11,11 @@ from decimal import Decimal from itertools import product import time -from test_framework.p2p import P2PInterface +from test_framework.blocktools import COINBASE_MATURITY import test_framework.messages -from test_framework.messages import ( - NODE_NETWORK, - NODE_WITNESS, +from test_framework.p2p import ( + P2PInterface, + P2P_SERVICES, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -51,9 +51,9 @@ class NetTest(BitcoinTestFramework): def run_test(self): # We need miniwallet to make a transaction self.wallet = MiniWallet(self.nodes[0]) - self.wallet.generate(1) + self.generate(self.wallet, 1) # Get out of IBD for the minfeefilter and getpeerinfo tests. - self.nodes[0].generate(101) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) # By default, the test framework sets up an addnode connection from # node 1 --> node0. By connecting node0 --> node 1, we're left with @@ -69,6 +69,7 @@ class NetTest(BitcoinTestFramework): self.test_getaddednodeinfo() self.test_service_flags() self.test_getnodeaddresses() + self.test_addpeeraddress() def test_connection_count(self): self.log.info("Test getconnectioncount") @@ -79,8 +80,7 @@ class NetTest(BitcoinTestFramework): self.log.info("Test getpeerinfo") # Create a few getpeerinfo last_block/last_transaction values. self.wallet.send_self_transfer(from_node=self.nodes[0]) # Make a transaction so we can see it in the getpeerinfo results - self.nodes[1].generate(1) - self.sync_all() + self.generate(self.nodes[1], 1) time_now = int(time.time()) peer_info = [x.getpeerinfo() for x in self.nodes] # Verify last_block and last_transaction keys/values. @@ -105,7 +105,7 @@ class NetTest(BitcoinTestFramework): assert_equal(peer_info[1][1]['connection_type'], 'inbound') # Check dynamically generated networks list in getpeerinfo help output. - assert "(ipv4, ipv6, onion, i2p, not_publicly_routable)" in self.nodes[0].help("getpeerinfo") + assert "(ipv4, ipv6, onion, i2p, cjdns, not_publicly_routable)" in self.nodes[0].help("getpeerinfo") def test_getnettotals(self): self.log.info("Test getnettotals") @@ -156,7 +156,7 @@ class NetTest(BitcoinTestFramework): assert_net_servicesnames(int(info["localservices"], 0x10), info["localservicesnames"]) # Check dynamically generated networks list in getnetworkinfo help output. - assert "(ipv4, ipv6, onion, i2p)" in self.nodes[0].help("getnetworkinfo") + assert "(ipv4, ipv6, onion, i2p, cjdns)" in self.nodes[0].help("getnetworkinfo") def test_getaddednodeinfo(self): self.log.info("Test getaddednodeinfo") @@ -188,41 +188,93 @@ class NetTest(BitcoinTestFramework): self.log.info("Test getnodeaddresses") self.nodes[0].add_p2p_connection(P2PInterface()) - # Add some addresses to the Address Manager over RPC. Due to the way - # bucket and bucket position are calculated, some of these addresses - # will collide. + # Add an IPv6 address to the address manager. + ipv6_addr = "1233:3432:2434:2343:3234:2345:6546:4534" + self.nodes[0].addpeeraddress(address=ipv6_addr, port=8333) + + # Add 10,000 IPv4 addresses to the address manager. Due to the way bucket + # and bucket positions are calculated, some of these addresses will collide. imported_addrs = [] for i in range(10000): first_octet = i >> 8 second_octet = i % 256 - a = "{}.{}.1.1".format(first_octet, second_octet) + a = f"{first_octet}.{second_octet}.1.1" imported_addrs.append(a) self.nodes[0].addpeeraddress(a, 8333) - # Obtain addresses via rpc call and check they were ones sent in before. - # - # Maximum possible addresses in addrman is 10000, although actual - # number will usually be less due to bucket and bucket position - # collisions. - node_addresses = self.nodes[0].getnodeaddresses(0) + # Fetch the addresses via the RPC and test the results. + assert_equal(len(self.nodes[0].getnodeaddresses()), 1) # default count is 1 + assert_equal(len(self.nodes[0].getnodeaddresses(count=2)), 2) + assert_equal(len(self.nodes[0].getnodeaddresses(network="ipv4", count=8)), 8) + + # Maximum possible addresses in AddrMan is 10000. The actual number will + # usually be less due to bucket and bucket position collisions. + node_addresses = self.nodes[0].getnodeaddresses(0, "ipv4") assert_greater_than(len(node_addresses), 5000) assert_greater_than(10000, len(node_addresses)) for a in node_addresses: assert_greater_than(a["time"], 1527811200) # 1st June 2018 - assert_equal(a["services"], NODE_NETWORK | NODE_WITNESS) + assert_equal(a["services"], P2P_SERVICES) assert a["address"] in imported_addrs assert_equal(a["port"], 8333) + assert_equal(a["network"], "ipv4") - node_addresses = self.nodes[0].getnodeaddresses(1) - assert_equal(len(node_addresses), 1) + # Test the IPv6 address. + res = self.nodes[0].getnodeaddresses(0, "ipv6") + assert_equal(len(res), 1) + assert_equal(res[0]["address"], ipv6_addr) + assert_equal(res[0]["network"], "ipv6") + assert_equal(res[0]["port"], 8333) + assert_equal(res[0]["services"], P2P_SERVICES) - assert_raises_rpc_error(-8, "Address count out of range", self.nodes[0].getnodeaddresses, -1) + # Test for the absence of onion, I2P and CJDNS addresses. + for network in ["onion", "i2p", "cjdns"]: + assert_equal(self.nodes[0].getnodeaddresses(0, network), []) - # addrman's size cannot be known reliably after insertion, as hash collisions may occur - # so only test that requesting a large number of addresses returns less than that - LARGE_REQUEST_COUNT = 10000 - node_addresses = self.nodes[0].getnodeaddresses(LARGE_REQUEST_COUNT) - assert_greater_than(LARGE_REQUEST_COUNT, len(node_addresses)) + # Test invalid arguments. + assert_raises_rpc_error(-8, "Address count out of range", self.nodes[0].getnodeaddresses, -1) + assert_raises_rpc_error(-8, "Network not recognized: Foo", self.nodes[0].getnodeaddresses, 1, "Foo") + + def test_addpeeraddress(self): + """RPC addpeeraddress sets the source address equal to the destination address. + If an address with the same /16 as an existing new entry is passed, it will be + placed in the same new bucket and have a 1/64 chance of the bucket positions + colliding (depending on the value of nKey in the addrman), in which case the + new address won't be added. The probability of collision can be reduced to + 1/2^16 = 1/65536 by using an address from a different /16. We avoid this here + by first testing adding a tried table entry before testing adding a new table one. + """ + self.log.info("Test addpeeraddress") + self.restart_node(1, ["-checkaddrman=1"]) + node = self.nodes[1] + + self.log.debug("Test that addpeerinfo is a hidden RPC") + # It is hidden from general help, but its detailed help may be called directly. + assert "addpeerinfo" not in node.help() + assert "addpeerinfo" in node.help("addpeerinfo") + + self.log.debug("Test that adding an empty address fails") + assert_equal(node.addpeeraddress(address="", port=8333), {"success": False}) + assert_equal(node.getnodeaddresses(count=0), []) + + self.log.debug("Test that adding a valid address to the tried table succeeds") + assert_equal(node.addpeeraddress(address="1.2.3.4", tried=True, port=8333), {"success": True}) + with node.assert_debug_log(expected_msgs=["CheckAddrman: new 0, tried 1, total 1 started"]): + addrs = node.getnodeaddresses(count=0) # getnodeaddresses re-runs the addrman checks + assert_equal(len(addrs), 1) + assert_equal(addrs[0]["address"], "1.2.3.4") + assert_equal(addrs[0]["port"], 8333) + + self.log.debug("Test that adding an already-present tried address to the new and tried tables fails") + for value in [True, False]: + assert_equal(node.addpeeraddress(address="1.2.3.4", tried=value, port=8333), {"success": False}) + assert_equal(len(node.getnodeaddresses(count=0)), 1) + + self.log.debug("Test that adding a second address, this time to the new table, succeeds") + assert_equal(node.addpeeraddress(address="2.0.0.0", port=8333), {"success": True}) + with node.assert_debug_log(expected_msgs=["CheckAddrman: new 1, tried 1, total 2 started"]): + addrs = node.getnodeaddresses(count=0) # getnodeaddresses re-runs the addrman checks + assert_equal(len(addrs), 2) if __name__ == '__main__': diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py new file mode 100755 index 0000000000..63533affd0 --- /dev/null +++ b/test/functional/rpc_packages.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""RPCs that handle raw transaction packages.""" + +from decimal import Decimal +import random + +from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE +from test_framework.test_framework import BitcoinTestFramework +from test_framework.messages import ( + BIP125_SEQUENCE_NUMBER, + COIN, + CTxInWitness, + tx_from_hex, +) +from test_framework.script import ( + CScript, + OP_TRUE, +) +from test_framework.util import ( + assert_equal, +) +from test_framework.wallet import ( + create_child_with_parents, + create_raw_chain, + make_chain, +) + +class RPCPackagesTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def assert_testres_equal(self, package_hex, testres_expected): + """Shuffle package_hex and assert that the testmempoolaccept result matches testres_expected. This should only + be used to test packages where the order does not matter. The ordering of transactions in package_hex and + testres_expected must match. + """ + shuffled_indeces = list(range(len(package_hex))) + random.shuffle(shuffled_indeces) + shuffled_package = [package_hex[i] for i in shuffled_indeces] + shuffled_testres = [testres_expected[i] for i in shuffled_indeces] + assert_equal(shuffled_testres, self.nodes[0].testmempoolaccept(shuffled_package)) + + def run_test(self): + self.log.info("Generate blocks to create UTXOs") + node = self.nodes[0] + self.privkeys = [node.get_deterministic_priv_key().key] + self.address = node.get_deterministic_priv_key().address + self.coins = [] + # The last 100 coinbase transactions are premature + for b in self.generatetoaddress(node, 200, self.address)[:100]: + coinbase = node.getblock(blockhash=b, verbosity=2)["tx"][0] + self.coins.append({ + "txid": coinbase["txid"], + "amount": coinbase["vout"][0]["value"], + "scriptPubKey": coinbase["vout"][0]["scriptPubKey"], + }) + + # Create some transactions that can be reused throughout the test. Never submit these to mempool. + self.independent_txns_hex = [] + self.independent_txns_testres = [] + for _ in range(3): + coin = self.coins.pop() + rawtx = node.createrawtransaction([{"txid": coin["txid"], "vout": 0}], + {self.address : coin["amount"] - Decimal("0.0001")}) + signedtx = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=self.privkeys) + assert signedtx["complete"] + testres = node.testmempoolaccept([signedtx["hex"]]) + assert testres[0]["allowed"] + self.independent_txns_hex.append(signedtx["hex"]) + # testmempoolaccept returns a list of length one, avoid creating a 2D list + self.independent_txns_testres.append(testres[0]) + self.independent_txns_testres_blank = [{ + "txid": res["txid"], "wtxid": res["wtxid"]} for res in self.independent_txns_testres] + + self.test_independent() + self.test_chain() + self.test_multiple_children() + self.test_multiple_parents() + self.test_conflicting() + self.test_rbf() + + + def test_independent(self): + self.log.info("Test multiple independent transactions in a package") + node = self.nodes[0] + # For independent transactions, order doesn't matter. + self.assert_testres_equal(self.independent_txns_hex, self.independent_txns_testres) + + self.log.info("Test an otherwise valid package with an extra garbage tx appended") + garbage_tx = node.createrawtransaction([{"txid": "00" * 32, "vout": 5}], {self.address: 1}) + tx = tx_from_hex(garbage_tx) + # Only the txid and wtxids are returned because validation is incomplete for the independent txns. + # Package validation is atomic: if the node cannot find a UTXO for any single tx in the package, + # it terminates immediately to avoid unnecessary, expensive signature verification. + package_bad = self.independent_txns_hex + [garbage_tx] + testres_bad = self.independent_txns_testres_blank + [{"txid": tx.rehash(), "wtxid": tx.getwtxid(), "allowed": False, "reject-reason": "missing-inputs"}] + self.assert_testres_equal(package_bad, testres_bad) + + self.log.info("Check testmempoolaccept tells us when some transactions completed validation successfully") + coin = self.coins.pop() + tx_bad_sig_hex = node.createrawtransaction([{"txid": coin["txid"], "vout": 0}], + {self.address : coin["amount"] - Decimal("0.0001")}) + tx_bad_sig = tx_from_hex(tx_bad_sig_hex) + testres_bad_sig = node.testmempoolaccept(self.independent_txns_hex + [tx_bad_sig_hex]) + # By the time the signature for the last transaction is checked, all the other transactions + # have been fully validated, which is why the node returns full validation results for all + # transactions here but empty results in other cases. + assert_equal(testres_bad_sig, self.independent_txns_testres + [{ + "txid": tx_bad_sig.rehash(), + "wtxid": tx_bad_sig.getwtxid(), "allowed": False, + "reject-reason": "mandatory-script-verify-flag-failed (Operation not valid with the current stack size)" + }]) + + self.log.info("Check testmempoolaccept reports txns in packages that exceed max feerate") + coin = self.coins.pop() + tx_high_fee_raw = node.createrawtransaction([{"txid": coin["txid"], "vout": 0}], + {self.address : coin["amount"] - Decimal("0.999")}) + tx_high_fee_signed = node.signrawtransactionwithkey(hexstring=tx_high_fee_raw, privkeys=self.privkeys) + assert tx_high_fee_signed["complete"] + tx_high_fee = tx_from_hex(tx_high_fee_signed["hex"]) + testres_high_fee = node.testmempoolaccept([tx_high_fee_signed["hex"]]) + assert_equal(testres_high_fee, [ + {"txid": tx_high_fee.rehash(), "wtxid": tx_high_fee.getwtxid(), "allowed": False, "reject-reason": "max-fee-exceeded"} + ]) + package_high_fee = [tx_high_fee_signed["hex"]] + self.independent_txns_hex + testres_package_high_fee = node.testmempoolaccept(package_high_fee) + assert_equal(testres_package_high_fee, testres_high_fee + self.independent_txns_testres_blank) + + def test_chain(self): + node = self.nodes[0] + first_coin = self.coins.pop() + (chain_hex, chain_txns) = create_raw_chain(node, first_coin, self.address, self.privkeys) + self.log.info("Check that testmempoolaccept requires packages to be sorted by dependency") + assert_equal(node.testmempoolaccept(rawtxs=chain_hex[::-1]), + [{"txid": tx.rehash(), "wtxid": tx.getwtxid(), "package-error": "package-not-sorted"} for tx in chain_txns[::-1]]) + + self.log.info("Testmempoolaccept a chain of 25 transactions") + testres_multiple = node.testmempoolaccept(rawtxs=chain_hex) + + testres_single = [] + # Test accept and then submit each one individually, which should be identical to package test accept + for rawtx in chain_hex: + testres = node.testmempoolaccept([rawtx]) + testres_single.append(testres[0]) + # Submit the transaction now so its child should have no problem validating + node.sendrawtransaction(rawtx) + assert_equal(testres_single, testres_multiple) + + # Clean up by clearing the mempool + self.generate(node, 1) + + def test_multiple_children(self): + node = self.nodes[0] + + self.log.info("Testmempoolaccept a package in which a transaction has two children within the package") + first_coin = self.coins.pop() + value = (first_coin["amount"] - Decimal("0.0002")) / 2 # Deduct reasonable fee and make 2 outputs + inputs = [{"txid": first_coin["txid"], "vout": 0}] + outputs = [{self.address : value}, {ADDRESS_BCRT1_P2WSH_OP_TRUE : value}] + rawtx = node.createrawtransaction(inputs, outputs) + + parent_signed = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=self.privkeys) + assert parent_signed["complete"] + parent_tx = tx_from_hex(parent_signed["hex"]) + parent_txid = parent_tx.rehash() + assert node.testmempoolaccept([parent_signed["hex"]])[0]["allowed"] + + parent_locking_script_a = parent_tx.vout[0].scriptPubKey.hex() + child_value = value - Decimal("0.0001") + + # Child A + (_, tx_child_a_hex, _, _) = make_chain(node, self.address, self.privkeys, parent_txid, child_value, 0, parent_locking_script_a) + assert not node.testmempoolaccept([tx_child_a_hex])[0]["allowed"] + + # Child B + rawtx_b = node.createrawtransaction([{"txid": parent_txid, "vout": 1}], {self.address : child_value}) + tx_child_b = tx_from_hex(rawtx_b) + tx_child_b.wit.vtxinwit = [CTxInWitness()] + tx_child_b.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])] + tx_child_b_hex = tx_child_b.serialize().hex() + assert not node.testmempoolaccept([tx_child_b_hex])[0]["allowed"] + + self.log.info("Testmempoolaccept with entire package, should work with children in either order") + testres_multiple_ab = node.testmempoolaccept(rawtxs=[parent_signed["hex"], tx_child_a_hex, tx_child_b_hex]) + testres_multiple_ba = node.testmempoolaccept(rawtxs=[parent_signed["hex"], tx_child_b_hex, tx_child_a_hex]) + assert all([testres["allowed"] for testres in testres_multiple_ab + testres_multiple_ba]) + + testres_single = [] + # Test accept and then submit each one individually, which should be identical to package testaccept + for rawtx in [parent_signed["hex"], tx_child_a_hex, tx_child_b_hex]: + testres = node.testmempoolaccept([rawtx]) + testres_single.append(testres[0]) + # Submit the transaction now so its child should have no problem validating + node.sendrawtransaction(rawtx) + assert_equal(testres_single, testres_multiple_ab) + + + def test_multiple_parents(self): + node = self.nodes[0] + + self.log.info("Testmempoolaccept a package in which a transaction has multiple parents within the package") + for num_parents in [2, 10, 24]: + # Test a package with num_parents parents and 1 child transaction. + package_hex = [] + parents_tx = [] + values = [] + parent_locking_scripts = [] + for _ in range(num_parents): + parent_coin = self.coins.pop() + value = parent_coin["amount"] + (tx, txhex, value, parent_locking_script) = make_chain(node, self.address, self.privkeys, parent_coin["txid"], value) + package_hex.append(txhex) + parents_tx.append(tx) + values.append(value) + parent_locking_scripts.append(parent_locking_script) + child_hex = create_child_with_parents(node, self.address, self.privkeys, parents_tx, values, parent_locking_scripts) + # Package accept should work with the parents in any order (as long as parents come before child) + for _ in range(10): + random.shuffle(package_hex) + testres_multiple = node.testmempoolaccept(rawtxs=package_hex + [child_hex]) + assert all([testres["allowed"] for testres in testres_multiple]) + + testres_single = [] + # Test accept and then submit each one individually, which should be identical to package testaccept + for rawtx in package_hex + [child_hex]: + testres_single.append(node.testmempoolaccept([rawtx])[0]) + # Submit the transaction now so its child should have no problem validating + node.sendrawtransaction(rawtx) + assert_equal(testres_single, testres_multiple) + + def test_conflicting(self): + node = self.nodes[0] + prevtx = self.coins.pop() + inputs = [{"txid": prevtx["txid"], "vout": 0}] + output1 = {node.get_deterministic_priv_key().address: 50 - 0.00125} + output2 = {ADDRESS_BCRT1_P2WSH_OP_TRUE: 50 - 0.00125} + + # tx1 and tx2 share the same inputs + rawtx1 = node.createrawtransaction(inputs, output1) + rawtx2 = node.createrawtransaction(inputs, output2) + signedtx1 = node.signrawtransactionwithkey(hexstring=rawtx1, privkeys=self.privkeys) + signedtx2 = node.signrawtransactionwithkey(hexstring=rawtx2, privkeys=self.privkeys) + tx1 = tx_from_hex(signedtx1["hex"]) + tx2 = tx_from_hex(signedtx2["hex"]) + assert signedtx1["complete"] + assert signedtx2["complete"] + + # Ensure tx1 and tx2 are valid by themselves + assert node.testmempoolaccept([signedtx1["hex"]])[0]["allowed"] + assert node.testmempoolaccept([signedtx2["hex"]])[0]["allowed"] + + self.log.info("Test duplicate transactions in the same package") + testres = node.testmempoolaccept([signedtx1["hex"], signedtx1["hex"]]) + assert_equal(testres, [ + {"txid": tx1.rehash(), "wtxid": tx1.getwtxid(), "package-error": "conflict-in-package"}, + {"txid": tx1.rehash(), "wtxid": tx1.getwtxid(), "package-error": "conflict-in-package"} + ]) + + self.log.info("Test conflicting transactions in the same package") + testres = node.testmempoolaccept([signedtx1["hex"], signedtx2["hex"]]) + assert_equal(testres, [ + {"txid": tx1.rehash(), "wtxid": tx1.getwtxid(), "package-error": "conflict-in-package"}, + {"txid": tx2.rehash(), "wtxid": tx2.getwtxid(), "package-error": "conflict-in-package"} + ]) + + def test_rbf(self): + node = self.nodes[0] + coin = self.coins.pop() + inputs = [{"txid": coin["txid"], "vout": 0, "sequence": BIP125_SEQUENCE_NUMBER}] + fee = Decimal('0.00125000') + output = {node.get_deterministic_priv_key().address: 50 - fee} + raw_replaceable_tx = node.createrawtransaction(inputs, output) + signed_replaceable_tx = node.signrawtransactionwithkey(hexstring=raw_replaceable_tx, privkeys=self.privkeys) + testres_replaceable = node.testmempoolaccept([signed_replaceable_tx["hex"]]) + replaceable_tx = tx_from_hex(signed_replaceable_tx["hex"]) + assert_equal(testres_replaceable, [ + {"txid": replaceable_tx.rehash(), "wtxid": replaceable_tx.getwtxid(), + "allowed": True, "vsize": replaceable_tx.get_vsize(), "fees": { "base": fee }} + ]) + + # Replacement transaction is identical except has double the fee + replacement_tx = tx_from_hex(signed_replaceable_tx["hex"]) + replacement_tx.vout[0].nValue -= int(fee * COIN) # Doubled fee + signed_replacement_tx = node.signrawtransactionwithkey(replacement_tx.serialize().hex(), self.privkeys) + replacement_tx = tx_from_hex(signed_replacement_tx["hex"]) + + self.log.info("Test that transactions within a package cannot replace each other") + testres_rbf_conflicting = node.testmempoolaccept([signed_replaceable_tx["hex"], signed_replacement_tx["hex"]]) + assert_equal(testres_rbf_conflicting, [ + {"txid": replaceable_tx.rehash(), "wtxid": replaceable_tx.getwtxid(), "package-error": "conflict-in-package"}, + {"txid": replacement_tx.rehash(), "wtxid": replacement_tx.getwtxid(), "package-error": "conflict-in-package"} + ]) + + self.log.info("Test that packages cannot conflict with mempool transactions, even if a valid BIP125 RBF") + node.sendrawtransaction(signed_replaceable_tx["hex"]) + testres_rbf_single = node.testmempoolaccept([signed_replacement_tx["hex"]]) + # This transaction is a valid BIP125 replace-by-fee + assert testres_rbf_single[0]["allowed"] + testres_rbf_package = self.independent_txns_testres_blank + [{ + "txid": replacement_tx.rehash(), "wtxid": replacement_tx.getwtxid(), "allowed": False, + "reject-reason": "bip125-replacement-disallowed" + }] + self.assert_testres_equal(self.independent_txns_hex + [signed_replacement_tx["hex"]], testres_rbf_package) + +if __name__ == "__main__": + RPCPackagesTest().main() diff --git a/test/functional/rpc_preciousblock.py b/test/functional/rpc_preciousblock.py index 7d3a5cc9ce..91298937fd 100755 --- a/test/functional/rpc_preciousblock.py +++ b/test/functional/rpc_preciousblock.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the preciousblock RPC.""" @@ -42,19 +42,18 @@ class PreciousTest(BitcoinTestFramework): def run_test(self): self.log.info("Ensure submitblock can in principle reorg to a competing chain") - gen_address = lambda i: self.nodes[i].get_deterministic_priv_key().address # A non-wallet address to mine to - self.nodes[0].generatetoaddress(1, gen_address(0)) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) assert_equal(self.nodes[0].getblockcount(), 1) - hashZ = self.nodes[1].generatetoaddress(2, gen_address(1))[-1] + hashZ = self.generate(self.nodes[1], 2, sync_fun=self.no_op)[-1] assert_equal(self.nodes[1].getblockcount(), 2) node_sync_via_rpc(self.nodes[0:3]) assert_equal(self.nodes[0].getbestblockhash(), hashZ) self.log.info("Mine blocks A-B-C on Node 0") - hashC = self.nodes[0].generatetoaddress(3, gen_address(0))[-1] + hashC = self.generate(self.nodes[0], 3, sync_fun=self.no_op)[-1] assert_equal(self.nodes[0].getblockcount(), 5) self.log.info("Mine competing blocks E-F-G on Node 1") - hashG = self.nodes[1].generatetoaddress(3, gen_address(1))[-1] + hashG = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[-1] assert_equal(self.nodes[1].getblockcount(), 5) assert hashC != hashG self.log.info("Connect nodes and check no reorg occurs") @@ -83,7 +82,7 @@ class PreciousTest(BitcoinTestFramework): self.nodes[1].preciousblock(hashC) assert_equal(self.nodes[1].getbestblockhash(), hashC) self.log.info("Mine another block (E-F-G-)H on Node 0 and reorg Node 1") - self.nodes[0].generatetoaddress(1, gen_address(0)) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) assert_equal(self.nodes[0].getblockcount(), 6) self.sync_blocks(self.nodes[0:2]) hashH = self.nodes[0].getbestblockhash() @@ -92,7 +91,7 @@ class PreciousTest(BitcoinTestFramework): self.nodes[1].preciousblock(hashC) assert_equal(self.nodes[1].getbestblockhash(), hashH) self.log.info("Mine competing blocks I-J-K-L on Node 2") - self.nodes[2].generatetoaddress(4, gen_address(2)) + self.generate(self.nodes[2], 4, sync_fun=self.no_op) assert_equal(self.nodes[2].getblockcount(), 6) hashL = self.nodes[2].getbestblockhash() self.log.info("Connect nodes and check no reorg occurs") diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index ed6abaed78..b037807b53 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -1,11 +1,15 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the Partially Signed Transaction RPCs. """ from decimal import Decimal +from itertools import product + +from test_framework.descriptors import descsum_create +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_approx, @@ -14,6 +18,7 @@ from test_framework.util import ( assert_raises_rpc_error, find_output, ) +from test_framework.wallet_util import bytes_to_wif import json import os @@ -26,7 +31,7 @@ class PSBTTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 self.extra_args = [ - ["-walletrbf=1"], + ["-walletrbf=1", "-addresstype=bech32", "-changetype=bech32"], #TODO: Remove address type restrictions once taproot has psbt extensions ["-walletrbf=0", "-changetype=legacy"], [] ] @@ -55,8 +60,7 @@ class PSBTTest(BitcoinTestFramework): online_addr = w2.getnewaddress(address_type="p2sh-segwit") wonline.importaddress(offline_addr, "", False) mining_node.sendtoaddress(address=offline_addr, amount=1.0) - mining_node.generate(nblocks=1) - self.sync_blocks([mining_node, online_node]) + self.generate(mining_node, nblocks=1) # Construct an unsigned PSBT on the online node (who doesn't know the output is Segwit, so will include a non-witness UTXO) utxos = wonline.listunspent(addresses=[offline_addr]) @@ -70,8 +74,7 @@ class PSBTTest(BitcoinTestFramework): # Make sure we can mine the resulting transaction txid = mining_node.sendrawtransaction(mining_node.finalizepsbt(signed_psbt)["hex"]) - mining_node.generate(1) - self.sync_blocks([mining_node, online_node]) + self.generate(mining_node, 1) assert_equal(online_node.gettxout(txid,0)["confirmations"], 1) wonline.unloadwallet() @@ -106,8 +109,20 @@ class PSBTTest(BitcoinTestFramework): psbtx = self.nodes[1].walletprocesspsbt(psbtx1)['psbt'] assert_equal(psbtx1, psbtx) + # Node 0 should not be able to sign the transaction with the wallet is locked + self.nodes[0].encryptwallet("password") + assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first", self.nodes[0].walletprocesspsbt, psbtx) + + # Node 0 should be able to process without signing though + unsigned_tx = self.nodes[0].walletprocesspsbt(psbtx, False) + assert_equal(unsigned_tx['complete'], False) + + self.nodes[0].walletpassphrase(passphrase="password", timeout=1000000) + # Sign the transaction and send - signed_tx = self.nodes[0].walletprocesspsbt(psbtx)['psbt'] + signed_tx = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=False)['psbt'] + finalized_tx = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=True)['psbt'] + assert signed_tx != finalized_tx final_tx = self.nodes[0].finalizepsbt(signed_tx)['hex'] self.nodes[0].sendrawtransaction(final_tx) @@ -146,8 +161,7 @@ class PSBTTest(BitcoinTestFramework): rawtx = self.nodes[0].fundrawtransaction(rawtx, {"changePosition":3}) signed_tx = self.nodes[0].signrawtransactionwithwallet(rawtx['hex'])['hex'] txid = self.nodes[0].sendrawtransaction(signed_tx) - self.nodes[0].generate(6) - self.sync_all() + self.generate(self.nodes[0], 6) # Find the output pos p2sh_pos = -1 @@ -158,17 +172,17 @@ class PSBTTest(BitcoinTestFramework): p2sh_p2wpkh_pos = -1 decoded = self.nodes[0].decoderawtransaction(signed_tx) for out in decoded['vout']: - if out['scriptPubKey']['addresses'][0] == p2sh: + if out['scriptPubKey']['address'] == p2sh: p2sh_pos = out['n'] - elif out['scriptPubKey']['addresses'][0] == p2wsh: + elif out['scriptPubKey']['address'] == p2wsh: p2wsh_pos = out['n'] - elif out['scriptPubKey']['addresses'][0] == p2wpkh: + elif out['scriptPubKey']['address'] == p2wpkh: p2wpkh_pos = out['n'] - elif out['scriptPubKey']['addresses'][0] == p2sh_p2wsh: + elif out['scriptPubKey']['address'] == p2sh_p2wsh: p2sh_p2wsh_pos = out['n'] - elif out['scriptPubKey']['addresses'][0] == p2sh_p2wpkh: + elif out['scriptPubKey']['address'] == p2sh_p2wpkh: p2sh_p2wpkh_pos = out['n'] - elif out['scriptPubKey']['addresses'][0] == p2pkh: + elif out['scriptPubKey']['address'] == p2pkh: p2pkh_pos = out['n'] inputs = [{"txid": txid, "vout": p2wpkh_pos}, {"txid": txid, "vout": p2sh_p2wpkh_pos}, {"txid": txid, "vout": p2pkh_pos}] @@ -193,14 +207,14 @@ class PSBTTest(BitcoinTestFramework): assert_approx(res2["fee"], 0.055, 0.005) self.log.info("Test min fee rate checks with walletcreatefundedpsbt are bypassed, e.g. a fee_rate under 1 sat/vB is allowed") - res3 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": "0.99999999", "add_inputs": True}) + res3 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": "0.999", "add_inputs": True}) assert_approx(res3["fee"], 0.00000381, 0.0000001) res4 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.00000999, "add_inputs": True}) assert_approx(res4["fee"], 0.00000381, 0.0000001) self.log.info("Test min fee rate checks with walletcreatefundedpsbt are bypassed and that funding non-standard 'zero-fee' transactions is valid") - for param in ["fee_rate", "feeRate"]: - assert_equal(self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {param: 0, "add_inputs": True})["fee"], 0) + for param, zero_value in product(["fee_rate", "feeRate"], [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]): + assert_equal(0, self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {param: zero_value, "add_inputs": True})["fee"]) self.log.info("Test invalid fee rate settings") for param, value in {("fee_rate", 100000), ("feeRate", 1)}: @@ -210,8 +224,14 @@ class PSBTTest(BitcoinTestFramework): self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: -1, "add_inputs": True}) assert_raises_rpc_error(-3, "Amount is not a number or string", self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: {"foo": "bar"}, "add_inputs": True}) + # Test fee rate values that don't pass fixed-point parsing checks. + for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]: + assert_raises_rpc_error(-3, "Invalid amount", + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: invalid_value, "add_inputs": True}) + # Test fee_rate values that cannot be represented in sat/vB. + for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]: assert_raises_rpc_error(-3, "Invalid amount", - self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: "", "add_inputs": True}) + self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": invalid_value, "add_inputs": True}) self.log.info("- raises RPC error if both feeRate and fee_rate are passed") assert_raises_rpc_error(-8, "Cannot specify both fee_rate (sat/vB) and feeRate (BTC/kvB)", @@ -299,8 +319,7 @@ class PSBTTest(BitcoinTestFramework): node2_addr = self.nodes[2].getnewaddress() txid1 = self.nodes[0].sendtoaddress(node1_addr, 13) txid2 = self.nodes[0].sendtoaddress(node2_addr, 13) - blockhash = self.nodes[0].generate(6)[0] - self.sync_all() + blockhash = self.generate(self.nodes[0], 6)[0] vout1 = find_output(self.nodes[1], txid1, 13, blockhash=blockhash) vout2 = find_output(self.nodes[2], txid2, 13, blockhash=blockhash) @@ -327,8 +346,7 @@ class PSBTTest(BitcoinTestFramework): combined = self.nodes[0].combinepsbt([psbt1, psbt2]) finalized = self.nodes[0].finalizepsbt(combined)['hex'] self.nodes[0].sendrawtransaction(finalized) - self.nodes[0].generate(6) - self.sync_all() + self.generate(self.nodes[0], 6) # Test additional args in walletcreatepsbt # Make sure both pre-included and funded inputs @@ -394,6 +412,14 @@ class PSBTTest(BitcoinTestFramework): # We don't care about the decode result, but decoding must succeed. self.nodes[0].decodepsbt(double_processed_psbt["psbt"]) + # Make sure unsafe inputs are included if specified + self.nodes[2].createwallet(wallet_name="unsafe") + wunsafe = self.nodes[2].get_wallet_rpc("unsafe") + self.nodes[0].sendtoaddress(wunsafe.getnewaddress(), 2) + self.sync_mempools() + assert_raises_rpc_error(-4, "Insufficient funds", wunsafe.walletcreatefundedpsbt, [], [{self.nodes[0].getnewaddress(): 1}]) + wunsafe.walletcreatefundedpsbt([], [{self.nodes[0].getnewaddress(): 1}], 0, {"include_unsafe": True}) + # BIP 174 Test Vectors # Check that unknown values are just passed through @@ -431,7 +457,7 @@ class PSBTTest(BitcoinTestFramework): wrpc = self.nodes[2].get_wallet_rpc("wallet{}".format(i)) for key in signer['privkeys']: wrpc.importprivkey(key) - signed_tx = wrpc.walletprocesspsbt(signer['psbt'])['psbt'] + signed_tx = wrpc.walletprocesspsbt(signer['psbt'], True, "ALL")['psbt'] assert_equal(signed_tx, signer['result']) # Combiner test @@ -514,8 +540,7 @@ class PSBTTest(BitcoinTestFramework): addr4 = self.nodes[1].getnewaddress("", "p2sh-segwit") txid4 = self.nodes[0].sendtoaddress(addr4, 5) vout4 = find_output(self.nodes[0], txid4, 5) - self.nodes[0].generate(6) - self.sync_all() + self.generate(self.nodes[0], 6) psbt2 = self.nodes[1].createpsbt([{"txid":txid4, "vout":vout4}], {self.nodes[0].getnewaddress():Decimal('4.999')}) psbt2 = self.nodes[1].walletprocesspsbt(psbt2)['psbt'] psbt2_decoded = self.nodes[0].decodepsbt(psbt2) @@ -538,8 +563,7 @@ class PSBTTest(BitcoinTestFramework): addr = self.nodes[1].getnewaddress("", "p2sh-segwit") txid = self.nodes[0].sendtoaddress(addr, 7) addrinfo = self.nodes[1].getaddressinfo(addr) - blockhash = self.nodes[0].generate(6)[0] - self.sync_all() + blockhash = self.generate(self.nodes[0], 6)[0] vout = find_output(self.nodes[0], txid, 7, blockhash=blockhash) psbt = self.nodes[1].createpsbt([{"txid":txid, "vout":vout}], {self.nodes[0].getnewaddress("", "p2sh-segwit"):Decimal('6.999')}) analyzed = self.nodes[0].analyzepsbt(psbt) @@ -582,5 +606,117 @@ class PSBTTest(BitcoinTestFramework): assert_raises_rpc_error(-25, 'Inputs missing or spent', self.nodes[0].walletprocesspsbt, 'cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==') + self.log.info("Test that we can fund psbts with external inputs specified") + + eckey = ECKey() + eckey.generate() + privkey = bytes_to_wif(eckey.get_bytes()) + + self.nodes[1].createwallet("extfund") + wallet = self.nodes[1].get_wallet_rpc("extfund") + + # Make a weird but signable script. sh(pkh()) descriptor accomplishes this + desc = descsum_create("sh(pkh({}))".format(privkey)) + if self.options.descriptors: + res = self.nodes[0].importdescriptors([{"desc": desc, "timestamp": "now"}]) + else: + res = self.nodes[0].importmulti([{"desc": desc, "timestamp": "now"}]) + assert res[0]["success"] + addr = self.nodes[0].deriveaddresses(desc)[0] + addr_info = self.nodes[0].getaddressinfo(addr) + + self.nodes[0].sendtoaddress(addr, 10) + self.nodes[0].sendtoaddress(wallet.getnewaddress(), 10) + self.generate(self.nodes[0], 6) + ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0] + + # An external input without solving data should result in an error + assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, [ext_utxo], {self.nodes[0].getnewaddress(): 15}) + + # But funding should work when the solving data is provided + psbt = wallet.walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {"add_inputs": True, "solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}}) + signed = wallet.walletprocesspsbt(psbt['psbt']) + assert not signed['complete'] + signed = self.nodes[0].walletprocesspsbt(signed['psbt']) + assert signed['complete'] + self.nodes[0].finalizepsbt(signed['psbt']) + + psbt = wallet.walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {"add_inputs": True, "solving_data":{"descriptors": [desc]}}) + signed = wallet.walletprocesspsbt(psbt['psbt']) + assert not signed['complete'] + signed = self.nodes[0].walletprocesspsbt(signed['psbt']) + assert signed['complete'] + final = self.nodes[0].finalizepsbt(signed['psbt'], False) + + dec = self.nodes[0].decodepsbt(signed["psbt"]) + for i, txin in enumerate(dec["tx"]["vin"]): + if txin["txid"] == ext_utxo["txid"] and txin["vout"] == ext_utxo["vout"]: + input_idx = i + break + psbt_in = dec["inputs"][input_idx] + # Calculate the input weight + # (prevout + sequence + length of scriptSig + 2 bytes buffer) * 4 + len of scriptwitness + len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0 + len_scriptwitness = len(psbt_in["final_scriptwitness"]["hex"]) // 2 if "final_scriptwitness" in psbt_in else 0 + input_weight = ((41 + len_scriptsig + 2) * 4) + len_scriptwitness + low_input_weight = input_weight // 2 + high_input_weight = input_weight * 2 + + # Input weight error conditions + assert_raises_rpc_error( + -8, + "Input weights should be specified in inputs rather than in options.", + wallet.walletcreatefundedpsbt, + inputs=[ext_utxo], + outputs={self.nodes[0].getnewaddress(): 15}, + options={"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 1000}]} + ) + + # Funding should also work if the input weight is provided + psbt = wallet.walletcreatefundedpsbt( + inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}], + outputs={self.nodes[0].getnewaddress(): 15}, + options={"add_inputs": True} + ) + signed = wallet.walletprocesspsbt(psbt["psbt"]) + signed = self.nodes[0].walletprocesspsbt(signed["psbt"]) + final = self.nodes[0].finalizepsbt(signed["psbt"]) + assert self.nodes[0].testmempoolaccept([final["hex"]])[0]["allowed"] + # Reducing the weight should have a lower fee + psbt2 = wallet.walletcreatefundedpsbt( + inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": low_input_weight}], + outputs={self.nodes[0].getnewaddress(): 15}, + options={"add_inputs": True} + ) + assert_greater_than(psbt["fee"], psbt2["fee"]) + # Increasing the weight should have a higher fee + psbt2 = wallet.walletcreatefundedpsbt( + inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}], + outputs={self.nodes[0].getnewaddress(): 15}, + options={"add_inputs": True} + ) + assert_greater_than(psbt2["fee"], psbt["fee"]) + # The provided weight should override the calculated weight when solving data is provided + psbt3 = wallet.walletcreatefundedpsbt( + inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}], + outputs={self.nodes[0].getnewaddress(): 15}, + options={'add_inputs': True, "solving_data":{"descriptors": [desc]}} + ) + assert_equal(psbt2["fee"], psbt3["fee"]) + + # Import the external utxo descriptor so that we can sign for it from the test wallet + if self.options.descriptors: + res = wallet.importdescriptors([{"desc": desc, "timestamp": "now"}]) + else: + res = wallet.importmulti([{"desc": desc, "timestamp": "now"}]) + assert res[0]["success"] + # The provided weight should override the calculated weight for a wallet input + psbt3 = wallet.walletcreatefundedpsbt( + inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}], + outputs={self.nodes[0].getnewaddress(): 15}, + options={"add_inputs": True} + ) + assert_equal(psbt2["fee"], psbt3["fee"]) + if __name__ == '__main__': PSBTTest().main() diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index 86c7b3fbcc..a839af0288 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -1,30 +1,36 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the rawtransaction RPCs. Test the following RPCs: + - getrawtransaction - createrawtransaction - signrawtransactionwithwallet - sendrawtransaction - decoderawtransaction - - getrawtransaction """ from collections import OrderedDict from decimal import Decimal -from io import BytesIO -from test_framework.messages import CTransaction, ToHex + +from test_framework.blocktools import COINBASE_MATURITY +from test_framework.messages import ( + CTransaction, + tx_from_hex, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, find_vout_for_address, - hex_str_to_bytes, ) +TXID = "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000" + + class multidict(dict): """Dictionary that allows duplicate keys. @@ -43,16 +49,20 @@ class multidict(dict): return self.x -# Create one-input, one-output, no-fee transaction: class RawTransactionsTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True - self.num_nodes = 3 + self.num_nodes = 4 self.extra_args = [ ["-txindex"], ["-txindex"], ["-txindex"], + [], ] + # whitelist all peers to speed up tx relay / mempool sync + for args in self.extra_args: + args.append("-whitelist=noban@127.0.0.1") + self.supports_cli = False def skip_test_if_missing_module(self): @@ -63,23 +73,113 @@ class RawTransactionsTest(BitcoinTestFramework): self.connect_nodes(0, 2) def run_test(self): - self.log.info('prepare some coins for multiple *rawtransaction commands') - self.nodes[2].generate(1) - self.sync_all() - self.nodes[0].generate(101) - self.sync_all() - self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(),1.5) - self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(),1.0) - self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(),5.0) - self.sync_all() - self.nodes[0].generate(5) + self.log.info("Prepare some coins for multiple *rawtransaction commands") + self.generate(self.nodes[2], 1) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) + for amount in [1.5, 1.0, 5.0]: + self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), amount) self.sync_all() + self.generate(self.nodes[0], 5) + + self.getrawtransaction_tests() + self.createrawtransaction_tests() + self.signrawtransactionwithwallet_tests() + self.sendrawtransaction_tests() + self.sendrawtransaction_testmempoolaccept_tests() + self.decoderawtransaction_tests() + self.transaction_version_number_tests() + if not self.options.descriptors: + self.raw_multisig_transaction_legacy_tests() - self.log.info('Test getrawtransaction on genesis block coinbase returns an error') + def getrawtransaction_tests(self): + addr = self.nodes[1].getnewaddress() + txid = self.nodes[0].sendtoaddress(addr, 10) + self.generate(self.nodes[0], 1) + vout = find_vout_for_address(self.nodes[1], txid, addr) + rawTx = self.nodes[1].createrawtransaction([{'txid': txid, 'vout': vout}], {self.nodes[1].getnewaddress(): 9.999}) + rawTxSigned = self.nodes[1].signrawtransactionwithwallet(rawTx) + txId = self.nodes[1].sendrawtransaction(rawTxSigned['hex']) + self.generateblock(self.nodes[0], output=self.nodes[0].getnewaddress(), transactions=[rawTxSigned['hex']]) + err_msg = ( + "No such mempool transaction. Use -txindex or provide a block hash to enable" + " blockchain transaction queries. Use gettransaction for wallet transactions." + ) + + for n in [0, 3]: + self.log.info(f"Test getrawtransaction {'with' if n == 0 else 'without'} -txindex") + + if n == 0: + # With -txindex. + # 1. valid parameters - only supply txid + assert_equal(self.nodes[n].getrawtransaction(txId), rawTxSigned['hex']) + + # 2. valid parameters - supply txid and 0 for non-verbose + assert_equal(self.nodes[n].getrawtransaction(txId, 0), rawTxSigned['hex']) + + # 3. valid parameters - supply txid and False for non-verbose + assert_equal(self.nodes[n].getrawtransaction(txId, False), rawTxSigned['hex']) + + # 4. valid parameters - supply txid and 1 for verbose. + # We only check the "hex" field of the output so we don't need to update this test every time the output format changes. + assert_equal(self.nodes[n].getrawtransaction(txId, 1)["hex"], rawTxSigned['hex']) + + # 5. valid parameters - supply txid and True for non-verbose + assert_equal(self.nodes[n].getrawtransaction(txId, True)["hex"], rawTxSigned['hex']) + else: + # Without -txindex, expect to raise. + for verbose in [None, 0, False, 1, True]: + assert_raises_rpc_error(-5, err_msg, self.nodes[n].getrawtransaction, txId, verbose) + + # 6. invalid parameters - supply txid and invalid boolean values (strings) for verbose + for value in ["True", "False"]: + assert_raises_rpc_error(-1, "not a boolean", self.nodes[n].getrawtransaction, txid=txId, verbose=value) + + # 7. invalid parameters - supply txid and empty array + assert_raises_rpc_error(-1, "not a boolean", self.nodes[n].getrawtransaction, txId, []) + + # 8. invalid parameters - supply txid and empty dict + assert_raises_rpc_error(-1, "not a boolean", self.nodes[n].getrawtransaction, txId, {}) + + # Make a tx by sending, then generate 2 blocks; block1 has the tx in it + tx = self.nodes[2].sendtoaddress(self.nodes[1].getnewaddress(), 1) + block1, block2 = self.generate(self.nodes[2], 2) + for n in [0, 3]: + self.log.info(f"Test getrawtransaction {'with' if n == 0 else 'without'} -txindex, with blockhash") + # We should be able to get the raw transaction by providing the correct block + gottx = self.nodes[n].getrawtransaction(txid=tx, verbose=True, blockhash=block1) + assert_equal(gottx['txid'], tx) + assert_equal(gottx['in_active_chain'], True) + if n == 0: + self.log.info("Test getrawtransaction with -txindex, without blockhash: 'in_active_chain' should be absent") + gottx = self.nodes[n].getrawtransaction(txid=tx, verbose=True) + assert_equal(gottx['txid'], tx) + assert 'in_active_chain' not in gottx + else: + self.log.info("Test getrawtransaction without -txindex, without blockhash: expect the call to raise") + assert_raises_rpc_error(-5, err_msg, self.nodes[n].getrawtransaction, txid=tx, verbose=True) + # We should not get the tx if we provide an unrelated block + assert_raises_rpc_error(-5, "No such transaction found", self.nodes[n].getrawtransaction, txid=tx, blockhash=block2) + # An invalid block hash should raise the correct errors + assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[n].getrawtransaction, txid=tx, blockhash=True) + assert_raises_rpc_error(-8, "parameter 3 must be of length 64 (not 6, for 'foobar')", self.nodes[n].getrawtransaction, txid=tx, blockhash="foobar") + assert_raises_rpc_error(-8, "parameter 3 must be of length 64 (not 8, for 'abcd1234')", self.nodes[n].getrawtransaction, txid=tx, blockhash="abcd1234") + foo = "ZZZ0000000000000000000000000000000000000000000000000000000000000" + assert_raises_rpc_error(-8, f"parameter 3 must be hexadecimal string (not '{foo}')", self.nodes[n].getrawtransaction, txid=tx, blockhash=foo) + bar = "0000000000000000000000000000000000000000000000000000000000000000" + assert_raises_rpc_error(-5, "Block hash not found", self.nodes[n].getrawtransaction, txid=tx, blockhash=bar) + # Undo the blocks and verify that "in_active_chain" is false. + self.nodes[n].invalidateblock(block1) + gottx = self.nodes[n].getrawtransaction(txid=tx, verbose=True, blockhash=block1) + assert_equal(gottx['in_active_chain'], False) + self.nodes[n].reconsiderblock(block1) + assert_equal(self.nodes[n].getbestblockhash(), block2) + + self.log.info("Test getrawtransaction on genesis block coinbase returns an error") block = self.nodes[0].getblock(self.nodes[0].getblockhash(0)) assert_raises_rpc_error(-5, "The genesis block coinbase is not considered an ordinary transaction", self.nodes[0].getrawtransaction, block['merkleroot']) - self.log.info('Check parameter types and required parameters of createrawtransaction') + def createrawtransaction_tests(self): + self.log.info("Test createrawtransaction") # Test `createrawtransaction` required parameters assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction) assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, []) @@ -88,16 +188,28 @@ class RawTransactionsTest(BitcoinTestFramework): assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [], {}, 0, False, 'foo') # Test `createrawtransaction` invalid `inputs` - txid = '1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000' assert_raises_rpc_error(-3, "Expected type array", self.nodes[0].createrawtransaction, 'foo', {}) assert_raises_rpc_error(-1, "JSON value is not an object as expected", self.nodes[0].createrawtransaction, ['foo'], {}) assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[0].createrawtransaction, [{}], {}) assert_raises_rpc_error(-8, "txid must be of length 64 (not 3, for 'foo')", self.nodes[0].createrawtransaction, [{'txid': 'foo'}], {}) - assert_raises_rpc_error(-8, "txid must be hexadecimal string (not 'ZZZ7bb8b1697ea987f3b223ba7819250cae33efacb068d23dc24859824a77844')", self.nodes[0].createrawtransaction, [{'txid': 'ZZZ7bb8b1697ea987f3b223ba7819250cae33efacb068d23dc24859824a77844'}], {}) - assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", self.nodes[0].createrawtransaction, [{'txid': txid}], {}) - assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': 'foo'}], {}) - assert_raises_rpc_error(-8, "Invalid parameter, vout cannot be negative", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': -1}], {}) - assert_raises_rpc_error(-8, "Invalid parameter, sequence number is out of range", self.nodes[0].createrawtransaction, [{'txid': txid, 'vout': 0, 'sequence': -1}], {}) + txid = "ZZZ7bb8b1697ea987f3b223ba7819250cae33efacb068d23dc24859824a77844" + assert_raises_rpc_error(-8, f"txid must be hexadecimal string (not '{txid}')", self.nodes[0].createrawtransaction, [{'txid': txid}], {}) + assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", self.nodes[0].createrawtransaction, [{'txid': TXID}], {}) + assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", self.nodes[0].createrawtransaction, [{'txid': TXID, 'vout': 'foo'}], {}) + assert_raises_rpc_error(-8, "Invalid parameter, vout cannot be negative", self.nodes[0].createrawtransaction, [{'txid': TXID, 'vout': -1}], {}) + # sequence number out of range + for invalid_seq in [-1, 4294967296]: + inputs = [{'txid': TXID, 'vout': 1, 'sequence': invalid_seq}] + outputs = {self.nodes[0].getnewaddress(): 1} + assert_raises_rpc_error(-8, 'Invalid parameter, sequence number is out of range', + self.nodes[0].createrawtransaction, inputs, outputs) + # with valid sequence number + for valid_seq in [1000, 4294967294]: + inputs = [{'txid': TXID, 'vout': 1, 'sequence': valid_seq}] + outputs = {self.nodes[0].getnewaddress(): 1} + rawtx = self.nodes[0].createrawtransaction(inputs, outputs) + decrawtx = self.nodes[0].decoderawtransaction(rawtx) + assert_equal(decrawtx['vin'][0]['sequence'], valid_seq) # Test `createrawtransaction` invalid `outputs` address = self.nodes[0].getnewaddress() @@ -124,54 +236,51 @@ class RawTransactionsTest(BitcoinTestFramework): # Test `createrawtransaction` invalid `replaceable` assert_raises_rpc_error(-3, "Expected type bool", self.nodes[0].createrawtransaction, [], {}, 0, 'foo') - self.log.info('Check that createrawtransaction accepts an array and object as outputs') - tx = CTransaction() + # Test that createrawtransaction accepts an array and object as outputs # One output - tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs={address: 99})))) + tx = tx_from_hex(self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs={address: 99})) assert_equal(len(tx.vout), 1) assert_equal( tx.serialize().hex(), - self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}]), + self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs=[{address: 99}]), ) # Two outputs - tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=OrderedDict([(address, 99), (address2, 99)]))))) + tx = tx_from_hex(self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs=OrderedDict([(address, 99), (address2, 99)]))) assert_equal(len(tx.vout), 2) assert_equal( tx.serialize().hex(), - self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}, {address2: 99}]), + self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs=[{address: 99}, {address2: 99}]), ) # Multiple mixed outputs - tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=multidict([(address, 99), (address2, 99), ('data', '99')]))))) + tx = tx_from_hex(self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs=multidict([(address, 99), (address2, 99), ('data', '99')]))) assert_equal(len(tx.vout), 3) assert_equal( tx.serialize().hex(), - self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}, {address2: 99}, {'data': '99'}]), + self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs=[{address: 99}, {address2: 99}, {'data': '99'}]), ) + def signrawtransactionwithwallet_tests(self): for type in ["bech32", "p2sh-segwit", "legacy"]: + self.log.info(f"Test signrawtransactionwithwallet with missing prevtx info ({type})") addr = self.nodes[0].getnewaddress("", type) addrinfo = self.nodes[0].getaddressinfo(addr) pubkey = addrinfo["scriptPubKey"] + inputs = [{'txid': TXID, 'vout': 3, 'sequence': 1000}] + outputs = {self.nodes[0].getnewaddress(): 1} + rawtx = self.nodes[0].createrawtransaction(inputs, outputs) - self.log.info('sendrawtransaction with missing prevtx info (%s)' %(type)) - - # Test `signrawtransactionwithwallet` invalid `prevtxs` - inputs = [ {'txid' : txid, 'vout' : 3, 'sequence' : 1000}] - outputs = { self.nodes[0].getnewaddress() : 1 } - rawtx = self.nodes[0].createrawtransaction(inputs, outputs) - - prevtx = dict(txid=txid, scriptPubKey=pubkey, vout=3, amount=1) + prevtx = dict(txid=TXID, scriptPubKey=pubkey, vout=3, amount=1) succ = self.nodes[0].signrawtransactionwithwallet(rawtx, [prevtx]) assert succ["complete"] + if type == "legacy": del prevtx["amount"] succ = self.nodes[0].signrawtransactionwithwallet(rawtx, [prevtx]) assert succ["complete"] - - if type != "legacy": + else: assert_raises_rpc_error(-3, "Missing amount", self.nodes[0].signrawtransactionwithwallet, rawtx, [ { - "txid": txid, + "txid": TXID, "scriptPubKey": pubkey, "vout": 3, } @@ -179,7 +288,7 @@ class RawTransactionsTest(BitcoinTestFramework): assert_raises_rpc_error(-3, "Missing vout", self.nodes[0].signrawtransactionwithwallet, rawtx, [ { - "txid": txid, + "txid": TXID, "scriptPubKey": pubkey, "amount": 1, } @@ -193,273 +302,23 @@ class RawTransactionsTest(BitcoinTestFramework): ]) assert_raises_rpc_error(-3, "Missing scriptPubKey", self.nodes[0].signrawtransactionwithwallet, rawtx, [ { - "txid": txid, + "txid": TXID, "vout": 3, "amount": 1 } ]) - ######################################### - # sendrawtransaction with missing input # - ######################################### - - self.log.info('sendrawtransaction with missing input') - inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1}] #won't exists - outputs = { self.nodes[0].getnewaddress() : 4.998 } - rawtx = self.nodes[2].createrawtransaction(inputs, outputs) - rawtx = self.nodes[2].signrawtransactionwithwallet(rawtx) - - # This will raise an exception since there are missing inputs + def sendrawtransaction_tests(self): + self.log.info("Test sendrawtransaction with missing input") + inputs = [{'txid': TXID, 'vout': 1}] # won't exist + outputs = {self.nodes[0].getnewaddress(): 4.998} + rawtx = self.nodes[2].createrawtransaction(inputs, outputs) + rawtx = self.nodes[2].signrawtransactionwithwallet(rawtx) assert_raises_rpc_error(-25, "bad-txns-inputs-missingorspent", self.nodes[2].sendrawtransaction, rawtx['hex']) - ##################################### - # getrawtransaction with block hash # - ##################################### - - # make a tx by sending then generate 2 blocks; block1 has the tx in it - tx = self.nodes[2].sendtoaddress(self.nodes[1].getnewaddress(), 1) - block1, block2 = self.nodes[2].generate(2) - self.sync_all() - # We should be able to get the raw transaction by providing the correct block - gottx = self.nodes[0].getrawtransaction(tx, True, block1) - assert_equal(gottx['txid'], tx) - assert_equal(gottx['in_active_chain'], True) - # We should not have the 'in_active_chain' flag when we don't provide a block - gottx = self.nodes[0].getrawtransaction(tx, True) - assert_equal(gottx['txid'], tx) - assert 'in_active_chain' not in gottx - # We should not get the tx if we provide an unrelated block - assert_raises_rpc_error(-5, "No such transaction found", self.nodes[0].getrawtransaction, tx, True, block2) - # An invalid block hash should raise the correct errors - assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[0].getrawtransaction, tx, True, True) - assert_raises_rpc_error(-8, "parameter 3 must be of length 64 (not 6, for 'foobar')", self.nodes[0].getrawtransaction, tx, True, "foobar") - assert_raises_rpc_error(-8, "parameter 3 must be of length 64 (not 8, for 'abcd1234')", self.nodes[0].getrawtransaction, tx, True, "abcd1234") - assert_raises_rpc_error(-8, "parameter 3 must be hexadecimal string (not 'ZZZ0000000000000000000000000000000000000000000000000000000000000')", self.nodes[0].getrawtransaction, tx, True, "ZZZ0000000000000000000000000000000000000000000000000000000000000") - assert_raises_rpc_error(-5, "Block hash not found", self.nodes[0].getrawtransaction, tx, True, "0000000000000000000000000000000000000000000000000000000000000000") - # Undo the blocks and check in_active_chain - self.nodes[0].invalidateblock(block1) - gottx = self.nodes[0].getrawtransaction(txid=tx, verbose=True, blockhash=block1) - assert_equal(gottx['in_active_chain'], False) - self.nodes[0].reconsiderblock(block1) - assert_equal(self.nodes[0].getbestblockhash(), block2) - - if not self.options.descriptors: - # The traditional multisig workflow does not work with descriptor wallets so these are legacy only. - # The multisig workflow with descriptor wallets uses PSBTs and is tested elsewhere, no need to do them here. - ######################### - # RAW TX MULTISIG TESTS # - ######################### - # 2of2 test - addr1 = self.nodes[2].getnewaddress() - addr2 = self.nodes[2].getnewaddress() - - addr1Obj = self.nodes[2].getaddressinfo(addr1) - addr2Obj = self.nodes[2].getaddressinfo(addr2) - - # Tests for createmultisig and addmultisigaddress - assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 1, ["01020304"]) - self.nodes[0].createmultisig(2, [addr1Obj['pubkey'], addr2Obj['pubkey']]) # createmultisig can only take public keys - assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 2, [addr1Obj['pubkey'], addr1]) # addmultisigaddress can take both pubkeys and addresses so long as they are in the wallet, which is tested here. - - mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr1])['address'] - - #use balance deltas instead of absolute values - bal = self.nodes[2].getbalance() - - # send 1.2 BTC to msig adr - txId = self.nodes[0].sendtoaddress(mSigObj, 1.2) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - assert_equal(self.nodes[2].getbalance(), bal+Decimal('1.20000000')) #node2 has both keys of the 2of2 ms addr., tx should affect the balance - - - # 2of3 test from different nodes - bal = self.nodes[2].getbalance() - addr1 = self.nodes[1].getnewaddress() - addr2 = self.nodes[2].getnewaddress() - addr3 = self.nodes[2].getnewaddress() - - addr1Obj = self.nodes[1].getaddressinfo(addr1) - addr2Obj = self.nodes[2].getaddressinfo(addr2) - addr3Obj = self.nodes[2].getaddressinfo(addr3) - - mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey'], addr3Obj['pubkey']])['address'] - - txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) - decTx = self.nodes[0].gettransaction(txId) - rawTx = self.nodes[0].decoderawtransaction(decTx['hex']) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - - #THIS IS AN INCOMPLETE FEATURE - #NODE2 HAS TWO OF THREE KEY AND THE FUNDS SHOULD BE SPENDABLE AND COUNT AT BALANCE CALCULATION - assert_equal(self.nodes[2].getbalance(), bal) #for now, assume the funds of a 2of3 multisig tx are not marked as spendable - - txDetails = self.nodes[0].gettransaction(txId, True) - rawTx = self.nodes[0].decoderawtransaction(txDetails['hex']) - vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('2.20000000')) - - bal = self.nodes[0].getbalance() - inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "amount" : vout['value']}] - outputs = { self.nodes[0].getnewaddress() : 2.19 } - rawTx = self.nodes[2].createrawtransaction(inputs, outputs) - rawTxPartialSigned = self.nodes[1].signrawtransactionwithwallet(rawTx, inputs) - assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx - - rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx, inputs) - assert_equal(rawTxSigned['complete'], True) #node2 can sign the tx compl., own two of three keys - self.nodes[2].sendrawtransaction(rawTxSigned['hex']) - rawTx = self.nodes[0].decoderawtransaction(rawTxSigned['hex']) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx - - # 2of2 test for combining transactions - bal = self.nodes[2].getbalance() - addr1 = self.nodes[1].getnewaddress() - addr2 = self.nodes[2].getnewaddress() - - addr1Obj = self.nodes[1].getaddressinfo(addr1) - addr2Obj = self.nodes[2].getaddressinfo(addr2) - - self.nodes[1].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] - mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] - mSigObjValid = self.nodes[2].getaddressinfo(mSigObj) - - txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) - decTx = self.nodes[0].gettransaction(txId) - rawTx2 = self.nodes[0].decoderawtransaction(decTx['hex']) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - - assert_equal(self.nodes[2].getbalance(), bal) # the funds of a 2of2 multisig tx should not be marked as spendable - - txDetails = self.nodes[0].gettransaction(txId, True) - rawTx2 = self.nodes[0].decoderawtransaction(txDetails['hex']) - vout = next(o for o in rawTx2['vout'] if o['value'] == Decimal('2.20000000')) - - bal = self.nodes[0].getbalance() - inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "redeemScript" : mSigObjValid['hex'], "amount" : vout['value']}] - outputs = { self.nodes[0].getnewaddress() : 2.19 } - rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs) - rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(rawTx2, inputs) - self.log.debug(rawTxPartialSigned1) - assert_equal(rawTxPartialSigned1['complete'], False) #node1 only has one key, can't comp. sign the tx - - rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs) - self.log.debug(rawTxPartialSigned2) - assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx - rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']]) - self.log.debug(rawTxComb) - self.nodes[2].sendrawtransaction(rawTxComb) - rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx - - # decoderawtransaction tests - # witness transaction - encrawtx = "010000000001010000000000000072c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000000ffffffff0100e1f50500000000000102616100000000" - decrawtx = self.nodes[0].decoderawtransaction(encrawtx, True) # decode as witness transaction - assert_equal(decrawtx['vout'][0]['value'], Decimal('1.00000000')) - assert_raises_rpc_error(-22, 'TX decode failed', self.nodes[0].decoderawtransaction, encrawtx, False) # force decode as non-witness transaction - # non-witness transaction - encrawtx = "01000000010000000000000072c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000000ffffffff0100e1f505000000000000000000" - decrawtx = self.nodes[0].decoderawtransaction(encrawtx, False) # decode as non-witness transaction - assert_equal(decrawtx['vout'][0]['value'], Decimal('1.00000000')) - # known ambiguous transaction in the chain (see https://github.com/bitcoin/bitcoin/issues/20579) - encrawtx = "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff4b03c68708046ff8415c622f4254432e434f4d2ffabe6d6de1965d02c68f928e5b244ab1965115a36f56eb997633c7f690124bbf43644e23080000000ca3d3af6d005a65ff0200fd00000000ffffffff03f4c1fb4b0000000016001497cfc76442fe717f2a3f0cc9c175f7561b6619970000000000000000266a24aa21a9ed957d1036a80343e0d1b659497e1b48a38ebe876a056d45965fac4a85cda84e1900000000000000002952534b424c4f434b3a8e092581ab01986cbadc84f4b43f4fa4bb9e7a2e2a0caf9b7cf64d939028e22c0120000000000000000000000000000000000000000000000000000000000000000000000000" - decrawtx = self.nodes[0].decoderawtransaction(encrawtx) - decrawtx_wit = self.nodes[0].decoderawtransaction(encrawtx, True) - assert_raises_rpc_error(-22, 'TX decode failed', self.nodes[0].decoderawtransaction, encrawtx, False) # fails to decode as non-witness transaction - assert_equal(decrawtx, decrawtx_wit) # the witness interpretation should be chosen - assert_equal(decrawtx['vin'][0]['coinbase'], "03c68708046ff8415c622f4254432e434f4d2ffabe6d6de1965d02c68f928e5b244ab1965115a36f56eb997633c7f690124bbf43644e23080000000ca3d3af6d005a65ff0200fd00000000") - - # Basic signrawtransaction test - addr = self.nodes[1].getnewaddress() - txid = self.nodes[0].sendtoaddress(addr, 10) - self.nodes[0].generate(1) - self.sync_all() - vout = find_vout_for_address(self.nodes[1], txid, addr) - rawTx = self.nodes[1].createrawtransaction([{'txid': txid, 'vout': vout}], {self.nodes[1].getnewaddress(): 9.999}) - rawTxSigned = self.nodes[1].signrawtransactionwithwallet(rawTx) - txId = self.nodes[1].sendrawtransaction(rawTxSigned['hex']) - self.nodes[0].generate(1) - self.sync_all() - - # getrawtransaction tests - # 1. valid parameters - only supply txid - assert_equal(self.nodes[0].getrawtransaction(txId), rawTxSigned['hex']) - - # 2. valid parameters - supply txid and 0 for non-verbose - assert_equal(self.nodes[0].getrawtransaction(txId, 0), rawTxSigned['hex']) - - # 3. valid parameters - supply txid and False for non-verbose - assert_equal(self.nodes[0].getrawtransaction(txId, False), rawTxSigned['hex']) - - # 4. valid parameters - supply txid and 1 for verbose. - # We only check the "hex" field of the output so we don't need to update this test every time the output format changes. - assert_equal(self.nodes[0].getrawtransaction(txId, 1)["hex"], rawTxSigned['hex']) - - # 5. valid parameters - supply txid and True for non-verbose - assert_equal(self.nodes[0].getrawtransaction(txId, True)["hex"], rawTxSigned['hex']) - - # 6. invalid parameters - supply txid and string "Flase" - assert_raises_rpc_error(-1, "not a boolean", self.nodes[0].getrawtransaction, txId, "Flase") - - # 7. invalid parameters - supply txid and empty array - assert_raises_rpc_error(-1, "not a boolean", self.nodes[0].getrawtransaction, txId, []) - - # 8. invalid parameters - supply txid and empty dict - assert_raises_rpc_error(-1, "not a boolean", self.nodes[0].getrawtransaction, txId, {}) - - inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1, 'sequence' : 1000}] - outputs = { self.nodes[0].getnewaddress() : 1 } - rawtx = self.nodes[0].createrawtransaction(inputs, outputs) - decrawtx= self.nodes[0].decoderawtransaction(rawtx) - assert_equal(decrawtx['vin'][0]['sequence'], 1000) - - # 9. invalid parameters - sequence number out of range - inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1, 'sequence' : -1}] - outputs = { self.nodes[0].getnewaddress() : 1 } - assert_raises_rpc_error(-8, 'Invalid parameter, sequence number is out of range', self.nodes[0].createrawtransaction, inputs, outputs) - - # 10. invalid parameters - sequence number out of range - inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1, 'sequence' : 4294967296}] - outputs = { self.nodes[0].getnewaddress() : 1 } - assert_raises_rpc_error(-8, 'Invalid parameter, sequence number is out of range', self.nodes[0].createrawtransaction, inputs, outputs) - - inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1, 'sequence' : 4294967294}] - outputs = { self.nodes[0].getnewaddress() : 1 } - rawtx = self.nodes[0].createrawtransaction(inputs, outputs) - decrawtx= self.nodes[0].decoderawtransaction(rawtx) - assert_equal(decrawtx['vin'][0]['sequence'], 4294967294) - - #################################### - # TRANSACTION VERSION NUMBER TESTS # - #################################### - - # Test the minimum transaction version number that fits in a signed 32-bit integer. - # As transaction version is unsigned, this should convert to its unsigned equivalent. - tx = CTransaction() - tx.nVersion = -0x80000000 - rawtx = ToHex(tx) - decrawtx = self.nodes[0].decoderawtransaction(rawtx) - assert_equal(decrawtx['version'], 0x80000000) - - # Test the maximum transaction version number that fits in a signed 32-bit integer. - tx = CTransaction() - tx.nVersion = 0x7fffffff - rawtx = ToHex(tx) - decrawtx = self.nodes[0].decoderawtransaction(rawtx) - assert_equal(decrawtx['version'], 0x7fffffff) - - self.log.info('sendrawtransaction/testmempoolaccept with maxfeerate') + def sendrawtransaction_testmempoolaccept_tests(self): + self.log.info("Test sendrawtransaction/testmempoolaccept with maxfeerate") + fee_exceeds_max = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)" # Test a transaction with a small fee. txId = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1.0) @@ -467,9 +326,9 @@ class RawTransactionsTest(BitcoinTestFramework): vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('1.00000000')) self.sync_all() - inputs = [{ "txid" : txId, "vout" : vout['n'] }] + inputs = [{"txid": txId, "vout": vout['n']}] # Fee 10,000 satoshis, (1 - (10000 sat * 0.00000001 BTC/sat)) = 0.9999 - outputs = { self.nodes[0].getnewaddress() : Decimal("0.99990000") } + outputs = {self.nodes[0].getnewaddress(): Decimal("0.99990000")} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx) assert_equal(rawTxSigned['complete'], True) @@ -479,7 +338,7 @@ class RawTransactionsTest(BitcoinTestFramework): assert_equal(testres['allowed'], False) assert_equal(testres['reject-reason'], 'max-fee-exceeded') # and sendrawtransaction should throw - assert_raises_rpc_error(-25, 'Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)', self.nodes[2].sendrawtransaction, rawTxSigned['hex'], 0.00001000) + assert_raises_rpc_error(-25, fee_exceeds_max, self.nodes[2].sendrawtransaction, rawTxSigned['hex'], 0.00001000) # and the following calls should both succeed testres = self.nodes[2].testmempoolaccept(rawtxs=[rawTxSigned['hex']])[0] assert_equal(testres['allowed'], True) @@ -491,9 +350,9 @@ class RawTransactionsTest(BitcoinTestFramework): vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('1.00000000')) self.sync_all() - inputs = [{ "txid" : txId, "vout" : vout['n'] }] + inputs = [{"txid": txId, "vout": vout['n']}] # Fee 2,000,000 satoshis, (1 - (2000000 sat * 0.00000001 BTC/sat)) = 0.98 - outputs = { self.nodes[0].getnewaddress() : Decimal("0.98000000") } + outputs = {self.nodes[0].getnewaddress() : Decimal("0.98000000")} rawTx = self.nodes[2].createrawtransaction(inputs, outputs) rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx) assert_equal(rawTxSigned['complete'], True) @@ -503,12 +362,175 @@ class RawTransactionsTest(BitcoinTestFramework): assert_equal(testres['allowed'], False) assert_equal(testres['reject-reason'], 'max-fee-exceeded') # and sendrawtransaction should throw - assert_raises_rpc_error(-25, 'Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)', self.nodes[2].sendrawtransaction, rawTxSigned['hex']) + assert_raises_rpc_error(-25, fee_exceeds_max, self.nodes[2].sendrawtransaction, rawTxSigned['hex']) # and the following calls should both succeed testres = self.nodes[2].testmempoolaccept(rawtxs=[rawTxSigned['hex']], maxfeerate='0.20000000')[0] assert_equal(testres['allowed'], True) self.nodes[2].sendrawtransaction(hexstring=rawTxSigned['hex'], maxfeerate='0.20000000') + self.log.info("Test sendrawtransaction/testmempoolaccept with tx already in the chain") + self.generate(self.nodes[2], 1) + for node in self.nodes: + testres = node.testmempoolaccept([rawTxSigned['hex']])[0] + assert_equal(testres['allowed'], False) + assert_equal(testres['reject-reason'], 'txn-already-known') + assert_raises_rpc_error(-27, 'Transaction already in block chain', node.sendrawtransaction, rawTxSigned['hex']) + + def decoderawtransaction_tests(self): + self.log.info("Test decoderawtransaction") + # witness transaction + encrawtx = "010000000001010000000000000072c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000000ffffffff0100e1f50500000000000102616100000000" + decrawtx = self.nodes[0].decoderawtransaction(encrawtx, True) # decode as witness transaction + assert_equal(decrawtx['vout'][0]['value'], Decimal('1.00000000')) + assert_raises_rpc_error(-22, 'TX decode failed', self.nodes[0].decoderawtransaction, encrawtx, False) # force decode as non-witness transaction + # non-witness transaction + encrawtx = "01000000010000000000000072c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000000ffffffff0100e1f505000000000000000000" + decrawtx = self.nodes[0].decoderawtransaction(encrawtx, False) # decode as non-witness transaction + assert_equal(decrawtx['vout'][0]['value'], Decimal('1.00000000')) + # known ambiguous transaction in the chain (see https://github.com/bitcoin/bitcoin/issues/20579) + coinbase = "03c68708046ff8415c622f4254432e434f4d2ffabe6d6de1965d02c68f928e5b244ab1965115a36f56eb997633c7f690124bbf43644e23080000000ca3d3af6d005a65ff0200fd00000000" + encrawtx = f"020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff4b{coinbase}" \ + "ffffffff03f4c1fb4b0000000016001497cfc76442fe717f2a3f0cc9c175f7561b6619970000000000000000266a24aa21a9ed957d1036a80343e0d1b659497e1b48a38ebe876a056d45965fac4a85cda84e1900000000000000002952534b424c4f434b3a8e092581ab01986cbadc84f4b43f4fa4bb9e7a2e2a0caf9b7cf64d939028e22c0120000000000000000000000000000000000000000000000000000000000000000000000000" + decrawtx = self.nodes[0].decoderawtransaction(encrawtx) + decrawtx_wit = self.nodes[0].decoderawtransaction(encrawtx, True) + assert_raises_rpc_error(-22, 'TX decode failed', self.nodes[0].decoderawtransaction, encrawtx, False) # fails to decode as non-witness transaction + assert_equal(decrawtx, decrawtx_wit) # the witness interpretation should be chosen + assert_equal(decrawtx['vin'][0]['coinbase'], coinbase) + + def transaction_version_number_tests(self): + self.log.info("Test transaction version numbers") + + # Test the minimum transaction version number that fits in a signed 32-bit integer. + # As transaction version is unsigned, this should convert to its unsigned equivalent. + tx = CTransaction() + tx.nVersion = -0x80000000 + rawtx = tx.serialize().hex() + decrawtx = self.nodes[0].decoderawtransaction(rawtx) + assert_equal(decrawtx['version'], 0x80000000) + + # Test the maximum transaction version number that fits in a signed 32-bit integer. + tx = CTransaction() + tx.nVersion = 0x7fffffff + rawtx = tx.serialize().hex() + decrawtx = self.nodes[0].decoderawtransaction(rawtx) + assert_equal(decrawtx['version'], 0x7fffffff) + + def raw_multisig_transaction_legacy_tests(self): + self.log.info("Test raw multisig transactions (legacy)") + # The traditional multisig workflow does not work with descriptor wallets so these are legacy only. + # The multisig workflow with descriptor wallets uses PSBTs and is tested elsewhere, no need to do them here. + + # 2of2 test + addr1 = self.nodes[2].getnewaddress() + addr2 = self.nodes[2].getnewaddress() + + addr1Obj = self.nodes[2].getaddressinfo(addr1) + addr2Obj = self.nodes[2].getaddressinfo(addr2) + + # Tests for createmultisig and addmultisigaddress + assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 1, ["01020304"]) + # createmultisig can only take public keys + self.nodes[0].createmultisig(2, [addr1Obj['pubkey'], addr2Obj['pubkey']]) + # addmultisigaddress can take both pubkeys and addresses so long as they are in the wallet, which is tested here + assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 2, [addr1Obj['pubkey'], addr1]) + + mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr1])['address'] + + # use balance deltas instead of absolute values + bal = self.nodes[2].getbalance() + + # send 1.2 BTC to msig adr + txId = self.nodes[0].sendtoaddress(mSigObj, 1.2) + self.sync_all() + self.generate(self.nodes[0], 1) + # node2 has both keys of the 2of2 ms addr, tx should affect the balance + assert_equal(self.nodes[2].getbalance(), bal + Decimal('1.20000000')) + + + # 2of3 test from different nodes + bal = self.nodes[2].getbalance() + addr1 = self.nodes[1].getnewaddress() + addr2 = self.nodes[2].getnewaddress() + addr3 = self.nodes[2].getnewaddress() + + addr1Obj = self.nodes[1].getaddressinfo(addr1) + addr2Obj = self.nodes[2].getaddressinfo(addr2) + addr3Obj = self.nodes[2].getaddressinfo(addr3) + + mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey'], addr3Obj['pubkey']])['address'] + + txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) + decTx = self.nodes[0].gettransaction(txId) + rawTx = self.nodes[0].decoderawtransaction(decTx['hex']) + self.sync_all() + self.generate(self.nodes[0], 1) + + # THIS IS AN INCOMPLETE FEATURE + # NODE2 HAS TWO OF THREE KEYS AND THE FUNDS SHOULD BE SPENDABLE AND COUNT AT BALANCE CALCULATION + assert_equal(self.nodes[2].getbalance(), bal) # for now, assume the funds of a 2of3 multisig tx are not marked as spendable + + txDetails = self.nodes[0].gettransaction(txId, True) + rawTx = self.nodes[0].decoderawtransaction(txDetails['hex']) + vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('2.20000000')) + + bal = self.nodes[0].getbalance() + inputs = [{"txid": txId, "vout": vout['n'], "scriptPubKey": vout['scriptPubKey']['hex'], "amount": vout['value']}] + outputs = {self.nodes[0].getnewaddress(): 2.19} + rawTx = self.nodes[2].createrawtransaction(inputs, outputs) + rawTxPartialSigned = self.nodes[1].signrawtransactionwithwallet(rawTx, inputs) + assert_equal(rawTxPartialSigned['complete'], False) # node1 only has one key, can't comp. sign the tx + + rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx, inputs) + assert_equal(rawTxSigned['complete'], True) # node2 can sign the tx compl., own two of three keys + self.nodes[2].sendrawtransaction(rawTxSigned['hex']) + rawTx = self.nodes[0].decoderawtransaction(rawTxSigned['hex']) + self.sync_all() + self.generate(self.nodes[0], 1) + assert_equal(self.nodes[0].getbalance(), bal + Decimal('50.00000000') + Decimal('2.19000000')) # block reward + tx + + # 2of2 test for combining transactions + bal = self.nodes[2].getbalance() + addr1 = self.nodes[1].getnewaddress() + addr2 = self.nodes[2].getnewaddress() + + addr1Obj = self.nodes[1].getaddressinfo(addr1) + addr2Obj = self.nodes[2].getaddressinfo(addr2) + + self.nodes[1].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] + mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] + mSigObjValid = self.nodes[2].getaddressinfo(mSigObj) + + txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) + decTx = self.nodes[0].gettransaction(txId) + rawTx2 = self.nodes[0].decoderawtransaction(decTx['hex']) + self.sync_all() + self.generate(self.nodes[0], 1) + + assert_equal(self.nodes[2].getbalance(), bal) # the funds of a 2of2 multisig tx should not be marked as spendable + + txDetails = self.nodes[0].gettransaction(txId, True) + rawTx2 = self.nodes[0].decoderawtransaction(txDetails['hex']) + vout = next(o for o in rawTx2['vout'] if o['value'] == Decimal('2.20000000')) + + bal = self.nodes[0].getbalance() + inputs = [{"txid": txId, "vout": vout['n'], "scriptPubKey": vout['scriptPubKey']['hex'], "redeemScript": mSigObjValid['hex'], "amount": vout['value']}] + outputs = {self.nodes[0].getnewaddress(): 2.19} + rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs) + rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(rawTx2, inputs) + self.log.debug(rawTxPartialSigned1) + assert_equal(rawTxPartialSigned1['complete'], False) # node1 only has one key, can't comp. sign the tx + + rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs) + self.log.debug(rawTxPartialSigned2) + assert_equal(rawTxPartialSigned2['complete'], False) # node2 only has one key, can't comp. sign the tx + rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']]) + self.log.debug(rawTxComb) + self.nodes[2].sendrawtransaction(rawTxComb) + rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb) + self.sync_all() + self.generate(self.nodes[0], 1) + assert_equal(self.nodes[0].getbalance(), bal + Decimal('50.00000000') + Decimal('2.19000000')) # block reward + tx + if __name__ == '__main__': RawTransactionsTest().main() diff --git a/test/functional/rpc_scantxoutset.py b/test/functional/rpc_scantxoutset.py index 070f59d314..acb6d3ea4a 100755 --- a/test/functional/rpc_scantxoutset.py +++ b/test/functional/rpc_scantxoutset.py @@ -1,63 +1,61 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the scantxoutset rpc call.""" +from test_framework.messages import COIN from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.wallet import ( + MiniWallet, + address_to_scriptpubkey, + getnewdestination, +) from decimal import Decimal -import shutil -import os + def descriptors(out): return sorted(u['desc'] for u in out['unspents']) + class ScantxoutsetTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.setup_clean_chain = True - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() + def sendtodestination(self, destination, amount): + # interpret strings as addresses, assume scriptPubKey otherwise + if isinstance(destination, str): + destination = address_to_scriptpubkey(destination) + self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=destination, amount=int(COIN * amount)) def run_test(self): - self.log.info("Mining blocks...") - self.nodes[0].generate(110) - - addr_P2SH_SEGWIT = self.nodes[0].getnewaddress("", "p2sh-segwit") - pubk1 = self.nodes[0].getaddressinfo(addr_P2SH_SEGWIT)['pubkey'] - addr_LEGACY = self.nodes[0].getnewaddress("", "legacy") - pubk2 = self.nodes[0].getaddressinfo(addr_LEGACY)['pubkey'] - addr_BECH32 = self.nodes[0].getnewaddress("", "bech32") - pubk3 = self.nodes[0].getaddressinfo(addr_BECH32)['pubkey'] - self.nodes[0].sendtoaddress(addr_P2SH_SEGWIT, 0.001) - self.nodes[0].sendtoaddress(addr_LEGACY, 0.002) - self.nodes[0].sendtoaddress(addr_BECH32, 0.004) + self.wallet = MiniWallet(self.nodes[0]) + self.wallet.rescan_utxos() + + self.log.info("Create UTXOs...") + pubk1, spk_P2SH_SEGWIT, addr_P2SH_SEGWIT = getnewdestination("p2sh-segwit") + pubk2, spk_LEGACY, addr_LEGACY = getnewdestination("legacy") + pubk3, spk_BECH32, addr_BECH32 = getnewdestination("bech32") + self.sendtodestination(spk_P2SH_SEGWIT, 0.001) + self.sendtodestination(spk_LEGACY, 0.002) + self.sendtodestination(spk_BECH32, 0.004) #send to child keys of tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK - self.nodes[0].sendtoaddress("mkHV1C6JLheLoUSSZYk7x3FH5tnx9bu7yc", 0.008) # (m/0'/0'/0') - self.nodes[0].sendtoaddress("mipUSRmJAj2KrjSvsPQtnP8ynUon7FhpCR", 0.016) # (m/0'/0'/1') - self.nodes[0].sendtoaddress("n37dAGe6Mq1HGM9t4b6rFEEsDGq7Fcgfqg", 0.032) # (m/0'/0'/1500') - self.nodes[0].sendtoaddress("mqS9Rpg8nNLAzxFExsgFLCnzHBsoQ3PRM6", 0.064) # (m/0'/0'/0) - self.nodes[0].sendtoaddress("mnTg5gVWr3rbhHaKjJv7EEEc76ZqHgSj4S", 0.128) # (m/0'/0'/1) - self.nodes[0].sendtoaddress("mketCd6B9U9Uee1iCsppDJJBHfvi6U6ukC", 0.256) # (m/0'/0'/1500) - self.nodes[0].sendtoaddress("mj8zFzrbBcdaWXowCQ1oPZ4qioBVzLzAp7", 0.512) # (m/1/1/0') - self.nodes[0].sendtoaddress("mfnKpKQEftniaoE1iXuMMePQU3PUpcNisA", 1.024) # (m/1/1/1') - self.nodes[0].sendtoaddress("mou6cB1kaP1nNJM1sryW6YRwnd4shTbXYQ", 2.048) # (m/1/1/1500') - self.nodes[0].sendtoaddress("mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", 4.096) # (m/1/1/0) - self.nodes[0].sendtoaddress("mxp7w7j8S1Aq6L8StS2PqVvtt4HGxXEvdy", 8.192) # (m/1/1/1) - self.nodes[0].sendtoaddress("mpQ8rokAhp1TAtJQR6F6TaUmjAWkAWYYBq", 16.384) # (m/1/1/1500) - - - self.nodes[0].generate(1) - - self.log.info("Stop node, remove wallet, mine again some blocks...") - self.stop_node(0) - shutil.rmtree(os.path.join(self.nodes[0].datadir, self.chain, 'wallets')) - self.start_node(0, ['-nowallet']) - self.import_deterministic_coinbase_privkeys() - self.nodes[0].generate(110) + self.sendtodestination("mkHV1C6JLheLoUSSZYk7x3FH5tnx9bu7yc", 0.008) # (m/0'/0'/0') + self.sendtodestination("mipUSRmJAj2KrjSvsPQtnP8ynUon7FhpCR", 0.016) # (m/0'/0'/1') + self.sendtodestination("n37dAGe6Mq1HGM9t4b6rFEEsDGq7Fcgfqg", 0.032) # (m/0'/0'/1500') + self.sendtodestination("mqS9Rpg8nNLAzxFExsgFLCnzHBsoQ3PRM6", 0.064) # (m/0'/0'/0) + self.sendtodestination("mnTg5gVWr3rbhHaKjJv7EEEc76ZqHgSj4S", 0.128) # (m/0'/0'/1) + self.sendtodestination("mketCd6B9U9Uee1iCsppDJJBHfvi6U6ukC", 0.256) # (m/0'/0'/1500) + self.sendtodestination("mj8zFzrbBcdaWXowCQ1oPZ4qioBVzLzAp7", 0.512) # (m/1/1/0') + self.sendtodestination("mfnKpKQEftniaoE1iXuMMePQU3PUpcNisA", 1.024) # (m/1/1/1') + self.sendtodestination("mou6cB1kaP1nNJM1sryW6YRwnd4shTbXYQ", 2.048) # (m/1/1/1500') + self.sendtodestination("mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", 4.096) # (m/1/1/0) + self.sendtodestination("mxp7w7j8S1Aq6L8StS2PqVvtt4HGxXEvdy", 8.192) # (m/1/1/1) + self.sendtodestination("mpQ8rokAhp1TAtJQR6F6TaUmjAWkAWYYBq", 16.384) # (m/1/1/1500) + + self.generate(self.nodes[0], 1) scan = self.nodes[0].scantxoutset("start", []) info = self.nodes[0].gettxoutsetinfo() @@ -66,56 +64,55 @@ class ScantxoutsetTest(BitcoinTestFramework): assert_equal(scan['txouts'], info['txouts']) assert_equal(scan['bestblock'], info['bestblock']) - self.restart_node(0, ['-nowallet']) self.log.info("Test if we have found the non HD unspent outputs.") - assert_equal(self.nodes[0].scantxoutset("start", [ "pkh(" + pubk1 + ")", "pkh(" + pubk2 + ")", "pkh(" + pubk3 + ")"])['total_amount'], Decimal("0.002")) - assert_equal(self.nodes[0].scantxoutset("start", [ "wpkh(" + pubk1 + ")", "wpkh(" + pubk2 + ")", "wpkh(" + pubk3 + ")"])['total_amount'], Decimal("0.004")) - assert_equal(self.nodes[0].scantxoutset("start", [ "sh(wpkh(" + pubk1 + "))", "sh(wpkh(" + pubk2 + "))", "sh(wpkh(" + pubk3 + "))"])['total_amount'], Decimal("0.001")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(" + pubk1 + ")", "combo(" + pubk2 + ")", "combo(" + pubk3 + ")"])['total_amount'], Decimal("0.007")) - assert_equal(self.nodes[0].scantxoutset("start", [ "addr(" + addr_P2SH_SEGWIT + ")", "addr(" + addr_LEGACY + ")", "addr(" + addr_BECH32 + ")"])['total_amount'], Decimal("0.007")) - assert_equal(self.nodes[0].scantxoutset("start", [ "addr(" + addr_P2SH_SEGWIT + ")", "addr(" + addr_LEGACY + ")", "combo(" + pubk3 + ")"])['total_amount'], Decimal("0.007")) + assert_equal(self.nodes[0].scantxoutset("start", ["pkh(" + pubk1.hex() + ")", "pkh(" + pubk2.hex() + ")", "pkh(" + pubk3.hex() + ")"])['total_amount'], Decimal("0.002")) + assert_equal(self.nodes[0].scantxoutset("start", ["wpkh(" + pubk1.hex() + ")", "wpkh(" + pubk2.hex() + ")", "wpkh(" + pubk3.hex() + ")"])['total_amount'], Decimal("0.004")) + assert_equal(self.nodes[0].scantxoutset("start", ["sh(wpkh(" + pubk1.hex() + "))", "sh(wpkh(" + pubk2.hex() + "))", "sh(wpkh(" + pubk3.hex() + "))"])['total_amount'], Decimal("0.001")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(" + pubk1.hex() + ")", "combo(" + pubk2.hex() + ")", "combo(" + pubk3.hex() + ")"])['total_amount'], Decimal("0.007")) + assert_equal(self.nodes[0].scantxoutset("start", ["addr(" + addr_P2SH_SEGWIT + ")", "addr(" + addr_LEGACY + ")", "addr(" + addr_BECH32 + ")"])['total_amount'], Decimal("0.007")) + assert_equal(self.nodes[0].scantxoutset("start", ["addr(" + addr_P2SH_SEGWIT + ")", "addr(" + addr_LEGACY + ")", "combo(" + pubk3.hex() + ")"])['total_amount'], Decimal("0.007")) self.log.info("Test range validation.") - assert_raises_rpc_error(-8, "End of range is too high", self.nodes[0].scantxoutset, "start", [ {"desc": "desc", "range": -1}]) - assert_raises_rpc_error(-8, "Range should be greater or equal than 0", self.nodes[0].scantxoutset, "start", [ {"desc": "desc", "range": [-1, 10]}]) - assert_raises_rpc_error(-8, "End of range is too high", self.nodes[0].scantxoutset, "start", [ {"desc": "desc", "range": [(2 << 31 + 1) - 1000000, (2 << 31 + 1)]}]) - assert_raises_rpc_error(-8, "Range specified as [begin,end] must not have begin after end", self.nodes[0].scantxoutset, "start", [ {"desc": "desc", "range": [2, 1]}]) - assert_raises_rpc_error(-8, "Range is too large", self.nodes[0].scantxoutset, "start", [ {"desc": "desc", "range": [0, 1000001]}]) + assert_raises_rpc_error(-8, "End of range is too high", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": -1}]) + assert_raises_rpc_error(-8, "Range should be greater or equal than 0", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [-1, 10]}]) + assert_raises_rpc_error(-8, "End of range is too high", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [(2 << 31 + 1) - 1000000, (2 << 31 + 1)]}]) + assert_raises_rpc_error(-8, "Range specified as [begin,end] must not have begin after end", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [2, 1]}]) + assert_raises_rpc_error(-8, "Range is too large", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [0, 1000001]}]) self.log.info("Test extended key derivation.") # Run various scans, and verify that the sum of the amounts of the matches corresponds to the expected subset. # Note that all amounts in the UTXO set are powers of 2 multiplied by 0.001 BTC, so each amounts uniquely identifies a subset. - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/0h)"])['total_amount'], Decimal("0.008")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0'/1h)"])['total_amount'], Decimal("0.016")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/1500')"])['total_amount'], Decimal("0.032")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0h/0)"])['total_amount'], Decimal("0.064")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/1)"])['total_amount'], Decimal("0.128")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/1500)"])['total_amount'], Decimal("0.256")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/*h)", "range": 1499}])['total_amount'], Decimal("0.024")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0'/*h)", "range": 1500}])['total_amount'], Decimal("0.056")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/*)", "range": 1499}])['total_amount'], Decimal("0.192")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/*)", "range": 1500}])['total_amount'], Decimal("0.448")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0')"])['total_amount'], Decimal("0.512")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1')"])['total_amount'], Decimal("1.024")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1500h)"])['total_amount'], Decimal("2.048")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"])['total_amount'], Decimal("4.096")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1)"])['total_amount'], Decimal("8.192")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1500)"])['total_amount'], Decimal("16.384")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/0)"])['total_amount'], Decimal("4.096")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo([abcdef88/1/2'/3/4h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/1)"])['total_amount'], Decimal("8.192")) - assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/1500)"])['total_amount'], Decimal("16.384")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*')", "range": 1499}])['total_amount'], Decimal("1.536")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*')", "range": 1500}])['total_amount'], Decimal("3.584")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", "range": 1499}])['total_amount'], Decimal("12.288")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", "range": 1500}])['total_amount'], Decimal("28.672")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1499}])['total_amount'], Decimal("12.288")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])['total_amount'], Decimal("28.672")) - assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": [1500,1500]}])['total_amount'], Decimal("16.384")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/0h)"])['total_amount'], Decimal("0.008")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0'/1h)"])['total_amount'], Decimal("0.016")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/1500')"])['total_amount'], Decimal("0.032")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0h/0)"])['total_amount'], Decimal("0.064")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/1)"])['total_amount'], Decimal("0.128")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/1500)"])['total_amount'], Decimal("0.256")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/*h)", "range": 1499}])['total_amount'], Decimal("0.024")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0'/*h)", "range": 1500}])['total_amount'], Decimal("0.056")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/*)", "range": 1499}])['total_amount'], Decimal("0.192")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/*)", "range": 1500}])['total_amount'], Decimal("0.448")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0')"])['total_amount'], Decimal("0.512")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1')"])['total_amount'], Decimal("1.024")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1500h)"])['total_amount'], Decimal("2.048")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"])['total_amount'], Decimal("4.096")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1)"])['total_amount'], Decimal("8.192")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1500)"])['total_amount'], Decimal("16.384")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/0)"])['total_amount'], Decimal("4.096")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo([abcdef88/1/2'/3/4h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/1)"])['total_amount'], Decimal("8.192")) + assert_equal(self.nodes[0].scantxoutset("start", ["combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/1500)"])['total_amount'], Decimal("16.384")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*')", "range": 1499}])['total_amount'], Decimal("1.536")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*')", "range": 1500}])['total_amount'], Decimal("3.584")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", "range": 1499}])['total_amount'], Decimal("12.288")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", "range": 1500}])['total_amount'], Decimal("28.672")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1499}])['total_amount'], Decimal("12.288")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])['total_amount'], Decimal("28.672")) + assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": [1500, 1500]}])['total_amount'], Decimal("16.384")) # Test the reported descriptors for a few matches - assert_equal(descriptors(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/*)", "range": 1499}])), ["pkh([0c5f9a1e/0'/0'/0]026dbd8b2315f296d36e6b6920b1579ca75569464875c7ebe869b536a7d9503c8c)#dzxw429x", "pkh([0c5f9a1e/0'/0'/1]033e6f25d76c00bedb3a8993c7d5739ee806397f0529b1b31dda31ef890f19a60c)#43rvceed"]) - assert_equal(descriptors(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"])), ["pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)#cxmct4w8"]) - assert_equal(descriptors(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])), ['pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)#cxmct4w8', 'pkh([0c5f9a1e/1/1/1500]03832901c250025da2aebae2bfb38d5c703a57ab66ad477f9c578bfbcd78abca6f)#vchwd07g', 'pkh([0c5f9a1e/1/1/1]030d820fc9e8211c4169be8530efbc632775d8286167afd178caaf1089b77daba7)#z2t3ypsa']) + assert_equal(descriptors(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/*)", "range": 1499}])), ["pkh([0c5f9a1e/0'/0'/0]026dbd8b2315f296d36e6b6920b1579ca75569464875c7ebe869b536a7d9503c8c)#dzxw429x", "pkh([0c5f9a1e/0'/0'/1]033e6f25d76c00bedb3a8993c7d5739ee806397f0529b1b31dda31ef890f19a60c)#43rvceed"]) + assert_equal(descriptors(self.nodes[0].scantxoutset("start", ["combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"])), ["pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)#cxmct4w8"]) + assert_equal(descriptors(self.nodes[0].scantxoutset("start", [{"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])), ['pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)#cxmct4w8', 'pkh([0c5f9a1e/1/1/1500]03832901c250025da2aebae2bfb38d5c703a57ab66ad477f9c578bfbcd78abca6f)#vchwd07g', 'pkh([0c5f9a1e/1/1/1]030d820fc9e8211c4169be8530efbc632775d8286167afd178caaf1089b77daba7)#z2t3ypsa']) # Check that status and abort don't need second arg assert_equal(self.nodes[0].scantxoutset("status"), None) @@ -124,5 +121,6 @@ class ScantxoutsetTest(BitcoinTestFramework): # Check that second arg is needed for start assert_raises_rpc_error(-1, "scanobjects argument is required for the start action", self.nodes[0].scantxoutset, "start") -if __name__ == '__main__': + +if __name__ == "__main__": ScantxoutsetTest().main() diff --git a/test/functional/rpc_setban.py b/test/functional/rpc_setban.py index fd5f8aa098..97354f480c 100755 --- a/test/functional/rpc_setban.py +++ b/test/functional/rpc_setban.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the setban rpc call.""" @@ -22,7 +22,7 @@ class SetBanTests(BitcoinTestFramework): # Node 0 connects to Node 1, check that the noban permission is not granted self.connect_nodes(0, 1) peerinfo = self.nodes[1].getpeerinfo()[0] - assert(not 'noban' in peerinfo['permissions']) + assert not "noban" in peerinfo["permissions"] # Node 0 get banned by Node 1 self.nodes[1].setban("127.0.0.1", "add") @@ -36,27 +36,40 @@ class SetBanTests(BitcoinTestFramework): self.restart_node(1, ['-whitelist=127.0.0.1']) self.connect_nodes(0, 1) peerinfo = self.nodes[1].getpeerinfo()[0] - assert('noban' in peerinfo['permissions']) + assert "noban" in peerinfo["permissions"] # If we remove the ban, Node 0 should be able to reconnect even without noban permission self.nodes[1].setban("127.0.0.1", "remove") self.restart_node(1, []) self.connect_nodes(0, 1) peerinfo = self.nodes[1].getpeerinfo()[0] - assert(not 'noban' in peerinfo['permissions']) + assert not "noban" in peerinfo["permissions"] self.log.info("Test that a non-IP address can be banned/unbanned") node = self.nodes[1] tor_addr = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion" ip_addr = "1.2.3.4" - assert(not self.is_banned(node, tor_addr)) - assert(not self.is_banned(node, ip_addr)) + assert not self.is_banned(node, tor_addr) + assert not self.is_banned(node, ip_addr) + node.setban(tor_addr, "add") - assert(self.is_banned(node, tor_addr)) - assert(not self.is_banned(node, ip_addr)) + assert self.is_banned(node, tor_addr) + assert not self.is_banned(node, ip_addr) + + self.log.info("Test the ban list is preserved through restart") + + self.restart_node(1) + assert self.is_banned(node, tor_addr) + assert not self.is_banned(node, ip_addr) + node.setban(tor_addr, "remove") - assert(not self.is_banned(self.nodes[1], tor_addr)) - assert(not self.is_banned(node, ip_addr)) + assert not self.is_banned(self.nodes[1], tor_addr) + assert not self.is_banned(node, ip_addr) + + self.restart_node(1) + assert not self.is_banned(node, tor_addr) + assert not self.is_banned(node, ip_addr) + if __name__ == '__main__': SetBanTests().main() diff --git a/test/functional/rpc_signer.py b/test/functional/rpc_signer.py new file mode 100755 index 0000000000..f1107197c5 --- /dev/null +++ b/test/functional/rpc_signer.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test external signer. + +Verify that a bitcoind node can use an external signer command. +See also wallet_signer.py for tests that require wallet context. +""" +import os +import platform + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) + + +class RPCSignerTest(BitcoinTestFramework): + def mock_signer_path(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'signer.py') + if platform.system() == "Windows": + return "py " + path + else: + return path + + def set_test_params(self): + self.num_nodes = 4 + # The experimental syscall sandbox feature (-sandbox) is not compatible with -signer (which + # invokes execve). + self.disable_syscall_sandbox = True + + self.extra_args = [ + [], + [f"-signer={self.mock_signer_path()}", '-keypool=10'], + [f"-signer={self.mock_signer_path()}", '-keypool=10'], + ["-signer=fake.py"], + ] + + def skip_test_if_missing_module(self): + self.skip_if_no_external_signer() + + def set_mock_result(self, node, res): + with open(os.path.join(node.cwd, "mock_result"), "w", encoding="utf8") as f: + f.write(res) + + def clear_mock_result(self, node): + os.remove(os.path.join(node.cwd, "mock_result")) + + def run_test(self): + self.log.debug(f"-signer={self.mock_signer_path()}") + + assert_raises_rpc_error(-1, 'Error: restart bitcoind with -signer=<cmd>', + self.nodes[0].enumeratesigners + ) + + # Handle script missing: + assert_raises_rpc_error( + -1, + "CreateProcess failed: The system cannot find the file specified." + if platform.system() == "Windows" + else "execve failed: No such file or directory", + self.nodes[3].enumeratesigners, + ) + + # Handle error thrown by script + self.set_mock_result(self.nodes[1], "2") + assert_raises_rpc_error(-1, 'RunCommandParseJSON error', + self.nodes[1].enumeratesigners + ) + self.clear_mock_result(self.nodes[1]) + + self.set_mock_result(self.nodes[1], '0 [{"type": "trezor", "model": "trezor_t", "error": "fingerprint not found"}]') + assert_raises_rpc_error(-1, 'fingerprint not found', + self.nodes[1].enumeratesigners + ) + self.clear_mock_result(self.nodes[1]) + + result = self.nodes[1].enumeratesigners() + assert_equal(len(result['signers']), 2) + assert_equal(result['signers'][0]["fingerprint"], "00000001") + assert_equal(result['signers'][0]["name"], "trezor_t") + +if __name__ == '__main__': + RPCSignerTest().main() diff --git a/test/functional/rpc_signmessage.py b/test/functional/rpc_signmessagewithprivkey.py index 1c71732a61..80555eab75 100755 --- a/test/functional/rpc_signmessage.py +++ b/test/functional/rpc_signmessagewithprivkey.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2019 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Test RPC commands for signing and verifying messages.""" +"""Test RPC commands for signing messages with private key.""" from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -10,14 +10,10 @@ from test_framework.util import ( assert_raises_rpc_error, ) -class SignMessagesTest(BitcoinTestFramework): +class SignMessagesWithPrivTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - self.extra_args = [["-addresstype=legacy"]] - - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() def run_test(self): message = 'This is just a test message' @@ -30,33 +26,20 @@ class SignMessagesTest(BitcoinTestFramework): assert_equal(expected_signature, signature) assert self.nodes[0].verifymessage(address, signature, message) - self.log.info('test signing with an address with wallet') - address = self.nodes[0].getnewaddress() - signature = self.nodes[0].signmessage(address, message) - assert self.nodes[0].verifymessage(address, signature, message) - - self.log.info('test verifying with another address should not work') - other_address = self.nodes[0].getnewaddress() - other_signature = self.nodes[0].signmessage(other_address, message) - assert not self.nodes[0].verifymessage(other_address, signature, message) - assert not self.nodes[0].verifymessage(address, other_signature, message) - self.log.info('test parameter validity and error codes') - # signmessage(withprivkey) have two required parameters + # signmessagewithprivkey has two required parameters for num_params in [0, 1, 3, 4, 5]: param_list = ["dummy"]*num_params assert_raises_rpc_error(-1, "signmessagewithprivkey", self.nodes[0].signmessagewithprivkey, *param_list) - assert_raises_rpc_error(-1, "signmessage", self.nodes[0].signmessage, *param_list) # verifymessage has three required parameters for num_params in [0, 1, 2, 4, 5]: param_list = ["dummy"]*num_params assert_raises_rpc_error(-1, "verifymessage", self.nodes[0].verifymessage, *param_list) # invalid key or address provided assert_raises_rpc_error(-5, "Invalid private key", self.nodes[0].signmessagewithprivkey, "invalid_key", message) - assert_raises_rpc_error(-5, "Invalid address", self.nodes[0].signmessage, "invalid_addr", message) assert_raises_rpc_error(-5, "Invalid address", self.nodes[0].verifymessage, "invalid_addr", signature, message) # malformed signature provided - assert_raises_rpc_error(-3, "Malformed base64 encoding", self.nodes[0].verifymessage, self.nodes[0].getnewaddress(), "invalid_sig", message) + assert_raises_rpc_error(-3, "Malformed base64 encoding", self.nodes[0].verifymessage, 'mpLQjfK79b7CCV4VMJWEWAj5Mpx8Up5zxB', "invalid_sig", message) if __name__ == '__main__': - SignMessagesTest().main() + SignMessagesWithPrivTest().main() diff --git a/test/functional/rpc_signrawtransaction.py b/test/functional/rpc_signrawtransaction.py index 2fbbdbbdf0..a2091b4ece 100755 --- a/test/functional/rpc_signrawtransaction.py +++ b/test/functional/rpc_signrawtransaction.py @@ -1,19 +1,46 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test transaction signing using the signrawtransaction* RPCs.""" -from test_framework.address import check_script, script_to_p2sh +from test_framework.blocktools import ( + COINBASE_MATURITY, +) +from test_framework.address import ( + script_to_p2sh, + script_to_p2wsh, +) from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_raises_rpc_error, find_vout_for_address, hex_str_to_bytes -from test_framework.messages import sha256 -from test_framework.script import CScript, OP_0, OP_CHECKSIG -from test_framework.script_util import key_to_p2pkh_script, script_to_p2sh_p2wsh_script, script_to_p2wsh_script +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + find_vout_for_address, +) +from test_framework.messages import ( + CTxInWitness, + tx_from_hex, +) +from test_framework.script import ( + CScript, + OP_CHECKLOCKTIMEVERIFY, + OP_CHECKSEQUENCEVERIFY, + OP_DROP, + OP_TRUE, +) +from test_framework.script_util import ( + key_to_p2pk_script, + key_to_p2pkh_script, + script_to_p2sh_p2wsh_script, + script_to_p2wsh_script, +) from test_framework.wallet_util import bytes_to_wif -from decimal import Decimal +from decimal import ( + Decimal, + getcontext, +) class SignRawTransactionsTest(BitcoinTestFramework): def set_test_params(self): @@ -154,7 +181,7 @@ class SignRawTransactionsTest(BitcoinTestFramework): def test_fully_signed_tx(self): self.log.info("Test signing a fully signed transaction does nothing") self.nodes[0].walletpassphrase("password", 9999) - self.nodes[0].generate(101) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) rawtx = self.nodes[0].createrawtransaction([], [{self.nodes[0].getnewaddress(): 10}]) fundedtx = self.nodes[0].fundrawtransaction(rawtx) signedtx = self.nodes[0].signrawtransactionwithwallet(fundedtx["hex"]) @@ -173,10 +200,9 @@ class SignRawTransactionsTest(BitcoinTestFramework): embedded_pubkey = eckey.get_pubkey().get_bytes().hex() p2sh_p2wsh_address = self.nodes[1].createmultisig(1, [embedded_pubkey], "p2sh-segwit") # send transaction to P2SH-P2WSH 1-of-1 multisig address - self.nodes[0].generate(101) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) self.nodes[0].sendtoaddress(p2sh_p2wsh_address["address"], 49.999) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # Get the UTXO info from scantxoutset unspent_output = self.nodes[1].scantxoutset('start', [p2sh_p2wsh_address['descriptor']])['unspents'][0] spk = script_to_p2sh_p2wsh_script(p2sh_p2wsh_address['redeemScript']).hex() @@ -202,15 +228,15 @@ class SignRawTransactionsTest(BitcoinTestFramework): embedded_pubkey = eckey.get_pubkey().get_bytes().hex() witness_script = { 'P2PKH': key_to_p2pkh_script(embedded_pubkey).hex(), - 'P2PK': CScript([hex_str_to_bytes(embedded_pubkey), OP_CHECKSIG]).hex() + 'P2PK': key_to_p2pk_script(embedded_pubkey).hex() }.get(tx_type, "Invalid tx_type") - redeem_script = CScript([OP_0, sha256(check_script(witness_script))]).hex() + redeem_script = script_to_p2wsh_script(witness_script).hex() addr = script_to_p2sh(redeem_script) script_pub_key = self.nodes[1].validateaddress(addr)['scriptPubKey'] # Fund that address txid = self.nodes[0].sendtoaddress(addr, 10) vout = find_vout_for_address(self.nodes[0], txid, addr) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) # Now create and sign a transaction spending that output on node[0], which doesn't know the scripts or keys spending_tx = self.nodes[0].createrawtransaction([{'txid': txid, 'vout': vout}], {self.nodes[1].getnewaddress(): Decimal("9.999")}) spending_tx_signed = self.nodes[0].signrawtransactionwithkey(spending_tx, [embedded_privkey], [{'txid': txid, 'vout': vout, 'scriptPubKey': script_pub_key, 'redeemScript': redeem_script, 'witnessScript': witness_script, 'amount': 10}]) @@ -238,6 +264,76 @@ class SignRawTransactionsTest(BitcoinTestFramework): txn = self.nodes[0].signrawtransactionwithwallet(hex_str, prev_txs) assert txn["complete"] + def test_signing_with_csv(self): + self.log.info("Test signing a transaction containing a fully signed CSV input") + self.nodes[0].walletpassphrase("password", 9999) + getcontext().prec = 8 + + # Make sure CSV is active + assert self.nodes[0].getdeploymentinfo()['deployments']['csv']['active'] + + # Create a P2WSH script with CSV + script = CScript([1, OP_CHECKSEQUENCEVERIFY, OP_DROP]) + address = script_to_p2wsh(script) + + # Fund that address and make the spend + txid = self.nodes[0].sendtoaddress(address, 1) + vout = find_vout_for_address(self.nodes[0], txid, address) + self.generate(self.nodes[0], 1) + utxo = self.nodes[0].listunspent()[0] + amt = Decimal(1) + utxo["amount"] - Decimal(0.00001) + tx = self.nodes[0].createrawtransaction( + [{"txid": txid, "vout": vout, "sequence": 1},{"txid": utxo["txid"], "vout": utxo["vout"]}], + [{self.nodes[0].getnewaddress(): amt}], + self.nodes[0].getblockcount() + ) + + # Set the witness script + ctx = tx_from_hex(tx) + ctx.wit.vtxinwit.append(CTxInWitness()) + ctx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE]), script] + tx = ctx.serialize_with_witness().hex() + + # Sign and send the transaction + signed = self.nodes[0].signrawtransactionwithwallet(tx) + assert_equal(signed["complete"], True) + self.nodes[0].sendrawtransaction(signed["hex"]) + + def test_signing_with_cltv(self): + self.log.info("Test signing a transaction containing a fully signed CLTV input") + self.nodes[0].walletpassphrase("password", 9999) + getcontext().prec = 8 + + # Make sure CLTV is active + assert self.nodes[0].getdeploymentinfo()['deployments']['bip65']['active'] + + # Create a P2WSH script with CLTV + script = CScript([100, OP_CHECKLOCKTIMEVERIFY, OP_DROP]) + address = script_to_p2wsh(script) + + # Fund that address and make the spend + txid = self.nodes[0].sendtoaddress(address, 1) + vout = find_vout_for_address(self.nodes[0], txid, address) + self.generate(self.nodes[0], 1) + utxo = self.nodes[0].listunspent()[0] + amt = Decimal(1) + utxo["amount"] - Decimal(0.00001) + tx = self.nodes[0].createrawtransaction( + [{"txid": txid, "vout": vout},{"txid": utxo["txid"], "vout": utxo["vout"]}], + [{self.nodes[0].getnewaddress(): amt}], + self.nodes[0].getblockcount() + ) + + # Set the witness script + ctx = tx_from_hex(tx) + ctx.wit.vtxinwit.append(CTxInWitness()) + ctx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE]), script] + tx = ctx.serialize_with_witness().hex() + + # Sign and send the transaction + signed = self.nodes[0].signrawtransactionwithwallet(tx) + assert_equal(signed["complete"], True) + self.nodes[0].sendrawtransaction(signed["hex"]) + def run_test(self): self.successful_signing_test() self.script_verification_error_test() @@ -245,6 +341,8 @@ class SignRawTransactionsTest(BitcoinTestFramework): self.OP_1NEGATE_test() self.test_with_lock_outputs() self.test_fully_signed_tx() + self.test_signing_with_csv() + self.test_signing_with_cltv() if __name__ == '__main__': diff --git a/test/functional/rpc_txoutproof.py b/test/functional/rpc_txoutproof.py index 528da0cbfc..d04d05962f 100755 --- a/test/functional/rpc_txoutproof.py +++ b/test/functional/rpc_txoutproof.py @@ -1,12 +1,19 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test gettxoutproof and verifytxoutproof RPCs.""" -from test_framework.messages import CMerkleBlock, FromHex, ToHex +from test_framework.blocktools import COINBASE_MATURITY +from test_framework.messages import ( + CMerkleBlock, + from_hex, +) from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) from test_framework.wallet import MiniWallet @@ -22,9 +29,8 @@ class MerkleBlockTest(BitcoinTestFramework): def run_test(self): miniwallet = MiniWallet(self.nodes[0]) # Add enough mature utxos to the wallet, so that all txs spend confirmed coins - miniwallet.generate(5) - self.nodes[0].generate(100) - self.sync_all() + self.generate(miniwallet, 5) + self.generate(self.nodes[0], COINBASE_MATURITY) chain_height = self.nodes[1].getblockcount() assert_equal(chain_height, 105) @@ -34,9 +40,8 @@ class MerkleBlockTest(BitcoinTestFramework): # This will raise an exception because the transaction is not yet in a block assert_raises_rpc_error(-5, "Transaction not yet in block", self.nodes[0].gettxoutproof, [txid1]) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) blockhash = self.nodes[0].getblockhash(chain_height + 1) - self.sync_all() txlist = [] blocktxn = self.nodes[0].getblock(blockhash, True)["tx"] @@ -47,11 +52,10 @@ class MerkleBlockTest(BitcoinTestFramework): assert_equal(self.nodes[0].verifytxoutproof(self.nodes[0].gettxoutproof([txid1, txid2])), txlist) assert_equal(self.nodes[0].verifytxoutproof(self.nodes[0].gettxoutproof([txid1, txid2], blockhash)), txlist) - txin_spent = miniwallet.get_utxo() # Get the change from txid2 + txin_spent = miniwallet.get_utxo(txid=txid2) # Get the change from txid2 tx3 = miniwallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=txin_spent) txid3 = tx3['txid'] - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) txid_spent = txin_spent["txid"] txid_unspent = txid1 # Input was change from txid2, so txid1 should be unspent @@ -87,10 +91,10 @@ class MerkleBlockTest(BitcoinTestFramework): assert txid1 in self.nodes[0].verifytxoutproof(proof) assert txid2 in self.nodes[1].verifytxoutproof(proof) - tweaked_proof = FromHex(CMerkleBlock(), proof) + tweaked_proof = from_hex(CMerkleBlock(), proof) # Make sure that our serialization/deserialization is working - assert txid1 in self.nodes[0].verifytxoutproof(ToHex(tweaked_proof)) + assert txid1 in self.nodes[0].verifytxoutproof(tweaked_proof.serialize().hex()) # Check to see if we can go up the merkle tree and pass this off as a # single-transaction block @@ -99,7 +103,7 @@ class MerkleBlockTest(BitcoinTestFramework): tweaked_proof.txn.vBits = [True] + [False]*7 for n in self.nodes: - assert not n.verifytxoutproof(ToHex(tweaked_proof)) + assert not n.verifytxoutproof(tweaked_proof.serialize().hex()) # TODO: try more variants, eg transactions at different depths, and # verify that the proofs are invalid diff --git a/test/functional/rpc_uptime.py b/test/functional/rpc_uptime.py index 6177970872..1a82d1fa41 100755 --- a/test/functional/rpc_uptime.py +++ b/test/functional/rpc_uptime.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2019 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the RPC call related to the uptime command. diff --git a/test/functional/test-shell.md b/test/functional/test-shell.md index b8e899d675..78737509cb 100644 --- a/test/functional/test-shell.md +++ b/test/functional/test-shell.md @@ -94,7 +94,7 @@ rewards to a wallet address owned by the mining node. ``` >>> address = test.nodes[0].getnewaddress() ->>> test.nodes[0].generatetoaddress(101, address) +>>> test.self.generatetoaddress(nodes[0], 101, address) ['2b98dd0044aae6f1cca7f88a0acf366a4bfe053c7f7b00da3c0d115f03d67efb', ... ``` Since the two nodes are both initialized by default to establish an outbound diff --git a/test/functional/test_framework/address.py b/test/functional/test_framework/address.py index 360962b8da..c7fbf679b6 100644 --- a/test/functional/test_framework/address.py +++ b/test/functional/test_framework/address.py @@ -1,18 +1,27 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Encode and decode Bitcoin addresses. - base58 P2PKH and P2SH addresses. -- bech32 segwit v0 P2WPKH and P2WSH addresses.""" +- bech32 segwit v0 P2WPKH and P2WSH addresses. +- bech32m segwit v1 P2TR addresses.""" import enum import unittest -from .script import hash256, hash160, sha256, CScript, OP_0 +from .script import ( + CScript, + OP_0, + OP_TRUE, + hash160, + hash256, + sha256, + taproot_construct, +) from .segwit_addr import encode_segwit_address -from .util import assert_equal, hex_str_to_bytes +from .util import assert_equal ADDRESS_BCRT1_UNSPENDABLE = 'bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj' ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR = 'addr(bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj)#juyq9d97' @@ -29,19 +38,32 @@ class AddressType(enum.Enum): chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +def create_deterministic_address_bcrt1_p2tr_op_true(): + """ + Generates a deterministic bech32m address (segwit v1 output) that + can be spent with a witness stack of OP_TRUE and the control block + with internal public key (script-path spending). + + Returns a tuple with the generated address and the internal key. + """ + internal_key = (1).to_bytes(32, 'big') + scriptPubKey = taproot_construct(internal_key, [(None, CScript([OP_TRUE]))]).scriptPubKey + address = encode_segwit_address("bcrt", 1, scriptPubKey[2:]) + assert_equal(address, 'bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka') + return (address, internal_key) + + def byte_to_base58(b, version): result = '' - str = b.hex() - str = chr(version).encode('latin-1').hex() + str - checksum = hash256(hex_str_to_bytes(str)).hex() - str += checksum[:8] - value = int('0x' + str, 0) + b = bytes([version]) + b # prepend version + b += hash256(b)[:4] # append checksum + value = int.from_bytes(b, 'big') while value > 0: result = chars[value % 58] + result value //= 58 - while (str[:2] == '00'): + while b[0] == 0: result = chars[0] + result - str = str[2:] + b = b[1:] return result @@ -100,7 +122,7 @@ def key_to_p2sh_p2wpkh(key, main=False): def program_to_witness(version, program, main=False): if (type(program) is str): - program = hex_str_to_bytes(program) + program = bytes.fromhex(program) assert 0 <= version <= 16 assert 2 <= len(program) <= 40 assert version > 0 or len(program) in [20, 32] @@ -121,14 +143,14 @@ def script_to_p2sh_p2wsh(script, main=False): def check_key(key): if (type(key) is str): - key = hex_str_to_bytes(key) # Assuming this is hex string + key = bytes.fromhex(key) # Assuming this is hex string if (type(key) is bytes and (len(key) == 33 or len(key) == 65)): return key assert False def check_script(script): if (type(script) is str): - script = hex_str_to_bytes(script) # Assuming this is hex string + script = bytes.fromhex(script) # Assuming this is hex string if (type(script) is bytes or type(script) is CScript): return script assert False diff --git a/test/functional/test_framework/authproxy.py b/test/functional/test_framework/authproxy.py index 81eb881234..c4ffd1fbf6 100644 --- a/test/functional/test_framework/authproxy.py +++ b/test/functional/test_framework/authproxy.py @@ -113,10 +113,8 @@ class AuthServiceProxy(): self.__conn.request(method, path, postdata, headers) return self._get_response() except OSError as e: - retry = ( - '[WinError 10053] An established connection was aborted by the software in your host machine' in str(e)) # Workaround for a bug on macOS. See https://bugs.python.org/issue33450 - retry = retry or ('[Errno 41] Protocol wrong type for socket' in str(e)) + retry = '[Errno 41] Protocol wrong type for socket' in str(e) if retry: self.__conn.close() self.__conn.request(method, path, postdata, headers) diff --git a/test/functional/test_framework/bdb.py b/test/functional/test_framework/bdb.py index 97b9c1d6d0..41886c09fd 100644 --- a/test/functional/test_framework/bdb.py +++ b/test/functional/test_framework/bdb.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ @@ -24,7 +24,6 @@ transactions. `db_dump -da wallet.dat` is useful to see the data in a wallet.dat BDB file """ -import binascii import struct # Important constants @@ -96,7 +95,7 @@ def dump_meta_page(page): metadata['key_count'] = key_count metadata['record_count'] = record_count metadata['flags'] = flags - metadata['uid'] = binascii.hexlify(uid) + metadata['uid'] = uid.hex().encode() assert magic == BTREE_MAGIC, 'bdb magic does not match bdb btree magic' assert pg_type == BTREE_META, 'Metadata page is not a btree metadata page' @@ -110,8 +109,9 @@ def dump_meta_page(page): metadata['re_pad'] = re_pad metadata['root'] = root metadata['crypto_magic'] = crypto_magic - metadata['iv'] = binascii.hexlify(iv) - metadata['chksum'] = binascii.hexlify(chksum) + metadata['iv'] = iv.hex().encode() + metadata['chksum'] = chksum.hex().encode() + return metadata # Given the dict from dump_leaf_page, get the key-value pairs and put them into a dict diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index e691b63df6..40fcbf7761 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Utilities for manipulating blocks and transactions.""" -from binascii import a2b_hex import struct import time import unittest @@ -23,25 +22,25 @@ from .messages import ( CTxIn, CTxInWitness, CTxOut, - FromHex, - ToHex, + SEQUENCE_FINAL, hash256, - hex_str_to_bytes, ser_uint256, - sha256, + tx_from_hex, uint256_from_str, ) from .script import ( CScript, CScriptNum, CScriptOp, - OP_0, OP_1, - OP_CHECKMULTISIG, - OP_CHECKSIG, OP_RETURN, OP_TRUE, - hash160, +) +from .script_util import ( + key_to_p2pk_script, + key_to_p2wpkh_script, + keys_to_multisig_script, + script_to_p2wsh_script, ) from .util import assert_equal @@ -52,10 +51,16 @@ MAX_BLOCK_SIGOPS_WEIGHT = MAX_BLOCK_SIGOPS * WITNESS_SCALE_FACTOR # Genesis block time (regtest) TIME_GENESIS_BLOCK = 1296688602 +MAX_FUTURE_BLOCK_TIME = 2 * 60 * 60 + +# Coinbase transaction outputs can only be spent after this number of new blocks (network rule) +COINBASE_MATURITY = 100 + # From BIP141 WITNESS_COMMITMENT_HEADER = b"\xaa\x21\xa9\xed" NORMAL_GBT_REQUEST_PARAMS = {"rules": ["segwit"]} +VERSIONBITS_LAST_OLD_BLOCK_VERSION = 4 def create_block(hashprev=None, coinbase=None, ntime=None, *, version=None, tmpl=None, txlist=None): @@ -63,11 +68,11 @@ def create_block(hashprev=None, coinbase=None, ntime=None, *, version=None, tmpl block = CBlock() if tmpl is None: tmpl = {} - block.nVersion = version or tmpl.get('version') or 1 + block.nVersion = version or tmpl.get('version') or VERSIONBITS_LAST_OLD_BLOCK_VERSION block.nTime = ntime or tmpl.get('curtime') or int(time.time() + 600) block.hashPrevBlock = hashprev or int(tmpl['previousblockhash'], 0x10) if tmpl and not tmpl.get('bits') is None: - block.nBits = struct.unpack('>I', a2b_hex(tmpl['bits']))[0] + block.nBits = struct.unpack('>I', bytes.fromhex(tmpl['bits']))[0] else: block.nBits = 0x207fffff # difficulty retargeting is disabled in REGTEST chainparams if coinbase is None: @@ -76,7 +81,7 @@ def create_block(hashprev=None, coinbase=None, ntime=None, *, version=None, tmpl if txlist: for tx in txlist: if not hasattr(tx, 'calc_sha256'): - tx = FromHex(CTransaction(), tx) + tx = tx_from_hex(tx) block.vtx.append(tx) block.hashMerkleRoot = block.calc_merkle_root() block.calc_sha256() @@ -115,7 +120,7 @@ def script_BIP34_coinbase_height(height): return CScript([CScriptNum(height)]) -def create_coinbase(height, pubkey=None, extra_output_script=None, fees=0): +def create_coinbase(height, pubkey=None, extra_output_script=None, fees=0, nValue=50): """Create a coinbase transaction. If pubkey is passed in, the coinbase output will be a P2PK output; @@ -124,14 +129,15 @@ def create_coinbase(height, pubkey=None, extra_output_script=None, fees=0): If extra_output_script is given, make a 0-value output to that script. This is useful to pad block weight/sigops as needed. """ coinbase = CTransaction() - coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), 0xffffffff)) + coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), SEQUENCE_FINAL)) coinbaseoutput = CTxOut() - coinbaseoutput.nValue = 50 * COIN - halvings = int(height / 150) # regtest - coinbaseoutput.nValue >>= halvings - coinbaseoutput.nValue += fees + coinbaseoutput.nValue = nValue * COIN + if nValue == 50: + halvings = int(height / 150) # regtest + coinbaseoutput.nValue >>= halvings + coinbaseoutput.nValue += fees if pubkey is not None: - coinbaseoutput.scriptPubKey = CScript([pubkey, OP_CHECKSIG]) + coinbaseoutput.scriptPubKey = key_to_p2pk_script(pubkey) else: coinbaseoutput.scriptPubKey = CScript([OP_TRUE]) coinbase.vout = [coinbaseoutput] @@ -151,7 +157,7 @@ def create_tx_with_script(prevtx, n, script_sig=b"", *, amount, script_pub_key=C """ tx = CTransaction() assert n < len(prevtx.vout) - tx.vin.append(CTxIn(COutPoint(prevtx.sha256, n), script_sig, 0xffffffff)) + tx.vin.append(CTxIn(COutPoint(prevtx.sha256, n), script_sig, SEQUENCE_FINAL)) tx.vout.append(CTxOut(amount, script_pub_key)) tx.calc_sha256() return tx @@ -162,7 +168,7 @@ def create_transaction(node, txid, to_address, *, amount): sign for the output that is being spent. """ raw_tx = create_raw_transaction(node, txid, to_address, amount=amount) - tx = FromHex(CTransaction(), raw_tx) + tx = tx_from_hex(raw_tx) return tx def create_raw_transaction(node, txid, to_address, *, amount): @@ -203,13 +209,11 @@ def witness_script(use_p2wsh, pubkey): scriptPubKey.""" if not use_p2wsh: # P2WPKH instead - pubkeyhash = hash160(hex_str_to_bytes(pubkey)) - pkscript = CScript([OP_0, pubkeyhash]) + pkscript = key_to_p2wpkh_script(pubkey) else: # 1-of-1 multisig - witness_program = CScript([OP_1, hex_str_to_bytes(pubkey), OP_1, OP_CHECKMULTISIG]) - scripthash = sha256(witness_program) - pkscript = CScript([OP_0, scripthash]) + witness_script = keys_to_multisig_script([pubkey]) + pkscript = script_to_p2wsh_script(witness_script) return pkscript.hex() def create_witness_tx(node, use_p2wsh, utxo, pubkey, encode_p2sh, amount): @@ -217,7 +221,7 @@ def create_witness_tx(node, use_p2wsh, utxo, pubkey, encode_p2sh, amount): Optionally wrap the segwit output using P2SH.""" if use_p2wsh: - program = CScript([OP_1, hex_str_to_bytes(pubkey), OP_1, OP_CHECKMULTISIG]) + program = keys_to_multisig_script([pubkey]) addr = script_to_p2sh_p2wsh(program) if encode_p2sh else script_to_p2wsh(program) else: addr = key_to_p2sh_p2wpkh(pubkey) if encode_p2sh else key_to_p2wpkh(pubkey) @@ -239,9 +243,9 @@ def send_to_witness(use_p2wsh, node, utxo, pubkey, encode_p2sh, amount, sign=Tru return node.sendrawtransaction(signed["hex"]) else: if (insert_redeem_script): - tx = FromHex(CTransaction(), tx_to_witness) - tx.vin[0].scriptSig += CScript([hex_str_to_bytes(insert_redeem_script)]) - tx_to_witness = ToHex(tx) + tx = tx_from_hex(tx_to_witness) + tx.vin[0].scriptSig += CScript([bytes.fromhex(insert_redeem_script)]) + tx_to_witness = tx.serialize().hex() return node.sendrawtransaction(tx_to_witness) diff --git a/test/functional/test_framework/coverage.py b/test/functional/test_framework/coverage.py index 7705dd3e4d..4fb4f8bb82 100644 --- a/test/functional/test_framework/coverage.py +++ b/test/functional/test_framework/coverage.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2018 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Utilities for doing coverage analysis on the RPC interface. @@ -10,6 +10,7 @@ testing. import os +from .authproxy import AuthServiceProxy REFERENCE_FILENAME = 'rpc_interface.txt' @@ -19,16 +20,17 @@ class AuthServiceProxyWrapper(): An object that wraps AuthServiceProxy to record specific RPC calls. """ - def __init__(self, auth_service_proxy_instance, coverage_logfile=None): + def __init__(self, auth_service_proxy_instance: AuthServiceProxy, rpc_url: str, coverage_logfile: str=None): """ Kwargs: - auth_service_proxy_instance (AuthServiceProxy): the instance - being wrapped. - coverage_logfile (str): if specified, write each service_name + auth_service_proxy_instance: the instance being wrapped. + rpc_url: url of the RPC instance being wrapped + coverage_logfile: if specified, write each service_name out to a file when called. """ self.auth_service_proxy_instance = auth_service_proxy_instance + self.rpc_url = rpc_url self.coverage_logfile = coverage_logfile def __getattr__(self, name): @@ -36,7 +38,7 @@ class AuthServiceProxyWrapper(): if not isinstance(return_val, type(self.auth_service_proxy_instance)): # If proxy getattr returned an unwrapped value, do the same here. return return_val - return AuthServiceProxyWrapper(return_val, self.coverage_logfile) + return AuthServiceProxyWrapper(return_val, self.rpc_url, self.coverage_logfile) def __call__(self, *args, **kwargs): """ @@ -57,6 +59,7 @@ class AuthServiceProxyWrapper(): def __truediv__(self, relative_uri): return AuthServiceProxyWrapper(self.auth_service_proxy_instance / relative_uri, + self.rpc_url, self.coverage_logfile) def get_request(self, *args, **kwargs): @@ -74,18 +77,18 @@ def get_filename(dirname, n_node): dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node))) -def write_all_rpc_commands(dirname, node): +def write_all_rpc_commands(dirname: str, node: AuthServiceProxy) -> bool: """ Write out a list of all RPC functions available in `bitcoin-cli` for coverage comparison. This will only happen once per coverage directory. Args: - dirname (str): temporary test dir - node (AuthServiceProxy): client + dirname: temporary test dir + node: client Returns: - bool. if the RPC interface file was written. + if the RPC interface file was written. """ filename = os.path.join(dirname, REFERENCE_FILENAME) diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index 26526e35fa..e5dea66963 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -8,6 +8,7 @@ keys, and is trivially vulnerable to side channel attacks. Do not use for anything but tests.""" import csv import hashlib +import hmac import os import random import unittest @@ -326,6 +327,16 @@ def generate_privkey(): """Generate a valid random 32-byte private key.""" return random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big') +def rfc6979_nonce(key): + """Compute signing nonce using RFC6979.""" + v = bytes([1] * 32) + k = bytes([0] * 32) + k = hmac.new(k, v + b"\x00" + key, 'sha256').digest() + v = hmac.new(k, v, 'sha256').digest() + k = hmac.new(k, v + b"\x01" + key, 'sha256').digest() + v = hmac.new(k, v, 'sha256').digest() + return hmac.new(k, v, 'sha256').digest() + class ECKey(): """A secp256k1 private key""" @@ -368,15 +379,18 @@ class ECKey(): ret.compressed = self.compressed return ret - def sign_ecdsa(self, msg, low_s=True): + def sign_ecdsa(self, msg, low_s=True, rfc6979=False): """Construct a DER-encoded ECDSA signature with this key. See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the ECDSA signer algorithm.""" assert(self.valid) z = int.from_bytes(msg, 'big') - # Note: no RFC6979, but a simple random nonce (some tests rely on distinct transactions for the same operation) - k = random.randrange(1, SECP256K1_ORDER) + # Note: no RFC6979 by default, but a simple random nonce (some tests rely on distinct transactions for the same operation) + if rfc6979: + k = int.from_bytes(rfc6979_nonce(self.secret.to_bytes(32, 'big') + msg), 'big') + else: + k = random.randrange(1, SECP256K1_ORDER) R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)])) r = R[0] % SECP256K1_ORDER s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index a18a9ec109..f57b6e7494 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Copyright (c) 2010 ArtForz -- public domain half-a-node # Copyright (c) 2012 Jeff Garzik -# Copyright (c) 2010-2020 The Bitcoin Core developers +# Copyright (c) 2010-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Bitcoin test framework primitive and message structures @@ -18,7 +18,7 @@ ser_*, deser_*: functions that handle serialization/deserialization. Classes use __slots__ to ensure extraneous attributes aren't accidentally added by tests, compromising their intended effect. """ -from codecs import encode +from base64 import b32decode, b32encode import copy import hashlib from io import BytesIO @@ -29,17 +29,18 @@ import struct import time from test_framework.siphash import siphash256 -from test_framework.util import hex_str_to_bytes, assert_equal +from test_framework.util import assert_equal MAX_LOCATOR_SZ = 101 -MAX_BLOCK_BASE_SIZE = 1000000 +MAX_BLOCK_WEIGHT = 4000000 MAX_BLOOM_FILTER_SIZE = 36000 MAX_BLOOM_HASH_FUNCS = 50 COIN = 100000000 # 1 btc in satoshis MAX_MONEY = 21000000 * COIN -BIP125_SEQUENCE_NUMBER = 0xfffffffd # Sequence number that is BIP 125 opt-in and BIP 68-opt-out +BIP125_SEQUENCE_NUMBER = 0xfffffffd # Sequence number that is rbf-opt-in (BIP 125) and csv-opt-out (BIP 68) +SEQUENCE_FINAL = 0xffffffff # Sequence number that disables nLockTime if set for every input of a tx MAX_PROTOCOL_MESSAGE_LENGTH = 4000000 # Maximum length of incoming protocol messages MAX_HEADERS_RESULTS = 2000 # Number of headers sent in one getheaders result @@ -64,13 +65,15 @@ FILTER_TYPE_BASIC = 0 WITNESS_SCALE_FACTOR = 4 -# Serialization/deserialization tools + def sha256(s): - return hashlib.new('sha256', s).digest() + return hashlib.sha256(s).digest() + def hash256(s): return sha256(sha256(s)) + def ser_compact_size(l): r = b"" if l < 253: @@ -190,14 +193,20 @@ def ser_string_vector(l): return r -# Deserialize from a hex string representation (eg from RPC) -def FromHex(obj, hex_string): - obj.deserialize(BytesIO(hex_str_to_bytes(hex_string))) +def from_hex(obj, hex_string): + """Deserialize from a hex string representation (e.g. from RPC) + + Note that there is no complementary helper like e.g. `to_hex` for the + inverse operation. To serialize a message object to a hex string, simply + use obj.serialize().hex()""" + obj.deserialize(BytesIO(bytes.fromhex(hex_string))) return obj -# Convert a binary-serializable object to hex (eg for submission via RPC) -def ToHex(obj): - return obj.serialize().hex() + +def tx_from_hex(hex_string): + """Deserialize from hex string to a transaction object""" + return from_hex(CTransaction(), hex_string) + # Objects that map to bitcoind objects, which can be serialized/deserialized @@ -207,15 +216,20 @@ class CAddress: # see https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki NET_IPV4 = 1 + NET_I2P = 5 ADDRV2_NET_NAME = { - NET_IPV4: "IPv4" + NET_IPV4: "IPv4", + NET_I2P: "I2P" } ADDRV2_ADDRESS_LENGTH = { - NET_IPV4: 4 + NET_IPV4: 4, + NET_I2P: 32 } + I2P_PAD = "====" + def __init__(self): self.time = 0 self.nServices = 1 @@ -223,6 +237,9 @@ class CAddress: self.ip = "0.0.0.0" self.port = 0 + def __eq__(self, other): + return self.net == other.net and self.ip == other.ip and self.nServices == other.nServices and self.port == other.port and self.time == other.time + def deserialize(self, f, *, with_time=True): """Deserialize from addrv1 format (pre-BIP155)""" if with_time: @@ -255,24 +272,33 @@ class CAddress: self.nServices = deser_compact_size(f) self.net = struct.unpack("B", f.read(1))[0] - assert self.net == self.NET_IPV4 + assert self.net in (self.NET_IPV4, self.NET_I2P) address_length = deser_compact_size(f) assert address_length == self.ADDRV2_ADDRESS_LENGTH[self.net] - self.ip = socket.inet_ntoa(f.read(4)) + addr_bytes = f.read(address_length) + if self.net == self.NET_IPV4: + self.ip = socket.inet_ntoa(addr_bytes) + else: + self.ip = b32encode(addr_bytes)[0:-len(self.I2P_PAD)].decode("ascii").lower() + ".b32.i2p" self.port = struct.unpack(">H", f.read(2))[0] def serialize_v2(self): """Serialize in addrv2 format (BIP155)""" - assert self.net == self.NET_IPV4 + assert self.net in (self.NET_IPV4, self.NET_I2P) r = b"" r += struct.pack("<I", self.time) r += ser_compact_size(self.nServices) r += struct.pack("B", self.net) r += ser_compact_size(self.ADDRV2_ADDRESS_LENGTH[self.net]) - r += socket.inet_aton(self.ip) + if self.net == self.NET_IPV4: + r += socket.inet_aton(self.ip) + else: + sfx = ".b32.i2p" + assert self.ip.endswith(sfx) + r += b32decode(self.ip[0:-len(sfx)] + self.I2P_PAD, True) r += struct.pack(">H", self.port) return r @@ -483,7 +509,7 @@ class CTransaction: def __init__(self, tx=None): if tx is None: - self.nVersion = 1 + self.nVersion = 2 self.vin = [] self.vout = [] self.wit = CTxWitness() @@ -584,12 +610,15 @@ class CTransaction: return False return True - # Calculate the virtual transaction size using witness and non-witness + # Calculate the transaction weight using witness and non-witness # serialization size (does NOT use sigops). - def get_vsize(self): + def get_weight(self): with_witness_size = len(self.serialize_with_witness()) without_witness_size = len(self.serialize_without_witness()) - return math.ceil(((WITNESS_SCALE_FACTOR - 1) * without_witness_size + with_witness_size) / WITNESS_SCALE_FACTOR) + return (WITNESS_SCALE_FACTOR - 1) * without_witness_size + with_witness_size + + def get_vsize(self): + return math.ceil(self.get_weight() / WITNESS_SCALE_FACTOR) def __repr__(self): return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ @@ -615,7 +644,7 @@ class CBlockHeader: self.calc_sha256() def set_null(self): - self.nVersion = 1 + self.nVersion = 4 self.hashPrevBlock = 0 self.hashMerkleRoot = 0 self.nTime = 0 @@ -654,7 +683,7 @@ class CBlockHeader: r += struct.pack("<I", self.nBits) r += struct.pack("<I", self.nNonce) self.sha256 = uint256_from_str(hash256(r)) - self.hash = encode(hash256(r)[::-1], 'hex_codec').decode('ascii') + self.hash = hash256(r)[::-1].hex() def rehash(self): self.sha256 = None @@ -737,6 +766,13 @@ class CBlock(CBlockHeader): self.nNonce += 1 self.rehash() + # Calculate the block weight using witness and non-witness + # serialization size (does NOT use sigops). + def get_weight(self): + with_witness_size = len(self.serialize(with_witness=True)) + without_witness_size = len(self.serialize(with_witness=False)) + return (WITNESS_SCALE_FACTOR - 1) * without_witness_size + with_witness_size + def __repr__(self): return "CBlock(nVersion=%i hashPrevBlock=%064x hashMerkleRoot=%064x nTime=%s nBits=%08x nNonce=%08x vtx=%s)" \ % (self.nVersion, self.hashPrevBlock, self.hashMerkleRoot, @@ -1045,13 +1081,11 @@ class msg_version: self.nStartingHeight = struct.unpack("<i", f.read(4))[0] - if self.nVersion >= 70001: - # Relay field is optional for version 70001 onwards - try: - self.relay = struct.unpack("<b", f.read(1))[0] - except: - self.relay = 0 - else: + # Relay field is optional for version 70001 onwards + # But, unconditionally check it to match behaviour in bitcoind + try: + self.relay = struct.unpack("<b", f.read(1))[0] + except struct.error: self.relay = 0 def serialize(self): diff --git a/test/functional/test_framework/netutil.py b/test/functional/test_framework/netutil.py index e047e7fa14..b64f66e69b 100644 --- a/test/functional/test_framework/netutil.py +++ b/test/functional/test_framework/netutil.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Linux network utilities. @@ -12,7 +12,6 @@ import socket import struct import array import os -from binascii import unhexlify # STATE_ESTABLISHED = '01' # STATE_SYN_SENT = '02' @@ -44,7 +43,7 @@ def _remove_empty(array): def _convert_ip_port(array): host,port = array.split(':') # convert host from mangled-per-four-bytes form as used by kernel - host = unhexlify(host) + host = bytes.fromhex(host) host_out = '' for x in range(0, len(host) // 4): (val,) = struct.unpack('=I', host[x*4:(x+1)*4]) @@ -145,13 +144,12 @@ def test_ipv6_local(): ''' Check for (local) IPv6 support. ''' - import socket # By using SOCK_DGRAM this will not actually make a connection, but it will # fail if there is no route to IPv6 localhost. have_ipv6 = True try: s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) - s.connect(('::1', 0)) + s.connect(('::1', 1)) except socket.error: have_ipv6 = False return have_ipv6 diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py index 05099f3339..251d3d5eae 100755 --- a/test/functional/test_framework/p2p.py +++ b/test/functional/test_framework/p2p.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Copyright (c) 2010 ArtForz -- public domain half-a-node # Copyright (c) 2012 Jeff Garzik -# Copyright (c) 2010-2020 The Bitcoin Core developers +# Copyright (c) 2010-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test objects for interacting with a bitcoind node over the p2p protocol. @@ -89,6 +89,8 @@ P2P_SERVICES = NODE_NETWORK | NODE_WITNESS P2P_SUBVERSION = "/python-p2p-tester:0.0.3/" # Value for relay that this test framework sends in its `version` message P2P_VERSION_RELAY = 1 +# Delay after receiving a tx inv before requesting transactions from non-preferred peers, in seconds +NONPREF_PEER_TX_DELAY = 2 MESSAGEMAP = { b"addr": msg_addr, @@ -356,7 +358,7 @@ class P2PInterface(P2PConnection): return create_conn - def peer_accept_connection(self, *args, services=NODE_NETWORK | NODE_WITNESS, **kwargs): + def peer_accept_connection(self, *args, services=P2P_SERVICES, **kwargs): create_conn = super().peer_accept_connection(*args, **kwargs) self.peer_connect_send_version(services) @@ -438,6 +440,7 @@ class P2PInterface(P2PConnection): self.send_message(msg_sendaddrv2()) self.send_message(msg_verack()) self.nServices = message.nServices + self.send_message(msg_getaddr()) # Connection helper methods @@ -539,8 +542,16 @@ class P2PInterface(P2PConnection): self.send_message(message) self.sync_with_ping(timeout=timeout) - # Sync up with the node + def sync_send_with_ping(self, timeout=60): + """Ensure SendMessages is called on this connection""" + # Calling sync_with_ping twice requires that the node calls + # `ProcessMessage` twice, and thus ensures `SendMessages` must have + # been called at least once + self.sync_with_ping() + self.sync_with_ping() + def sync_with_ping(self, timeout=60): + """Ensure ProcessMessages is called on this connection""" self.send_message(msg_ping(nonce=self.ping_counter)) def test_function(): @@ -568,6 +579,8 @@ class NetworkThread(threading.Thread): NetworkThread.listeners = {} NetworkThread.protos = {} + if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) NetworkThread.network_event_loop = asyncio.new_event_loop() def run(self): diff --git a/test/functional/test_framework/ripemd160.py b/test/functional/test_framework/ripemd160.py new file mode 100644 index 0000000000..12801364b4 --- /dev/null +++ b/test/functional/test_framework/ripemd160.py @@ -0,0 +1,130 @@ +# Copyright (c) 2021 Pieter Wuille +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test-only pure Python RIPEMD160 implementation.""" + +import unittest + +# Message schedule indexes for the left path. +ML = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, + 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, + 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, + 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 +] + +# Message schedule indexes for the right path. +MR = [ + 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, + 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, + 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, + 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, + 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 +] + +# Rotation counts for the left path. +RL = [ + 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, + 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, + 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, + 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, + 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 +] + +# Rotation counts for the right path. +RR = [ + 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, + 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, + 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, + 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, + 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 +] + +# K constants for the left path. +KL = [0, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e] + +# K constants for the right path. +KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0] + + +def fi(x, y, z, i): + """The f1, f2, f3, f4, and f5 functions from the specification.""" + if i == 0: + return x ^ y ^ z + elif i == 1: + return (x & y) | (~x & z) + elif i == 2: + return (x | ~y) ^ z + elif i == 3: + return (x & z) | (y & ~z) + elif i == 4: + return x ^ (y | ~z) + else: + assert False + + +def rol(x, i): + """Rotate the bottom 32 bits of x left by i bits.""" + return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff + + +def compress(h0, h1, h2, h3, h4, block): + """Compress state (h0, h1, h2, h3, h4) with block.""" + # Left path variables. + al, bl, cl, dl, el = h0, h1, h2, h3, h4 + # Right path variables. + ar, br, cr, dr, er = h0, h1, h2, h3, h4 + # Message variables. + x = [int.from_bytes(block[4*i:4*(i+1)], 'little') for i in range(16)] + + # Iterate over the 80 rounds of the compression. + for j in range(80): + rnd = j >> 4 + # Perform left side of the transformation. + al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el + al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl + # Perform right side of the transformation. + ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er + ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr + + # Compose old state, left transform, and right transform into new state. + return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr + + +def ripemd160(data): + """Compute the RIPEMD-160 hash of data.""" + # Initialize state. + state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0) + # Process full 64-byte blocks in the input. + for b in range(len(data) >> 6): + state = compress(*state, data[64*b:64*(b+1)]) + # Construct final blocks (with padding and size). + pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63) + fin = data[len(data) & ~63:] + pad + (8 * len(data)).to_bytes(8, 'little') + # Process final blocks. + for b in range(len(fin) >> 6): + state = compress(*state, fin[64*b:64*(b+1)]) + # Produce output. + return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state) + + +class TestFrameworkKey(unittest.TestCase): + def test_ripemd160(self): + """RIPEMD-160 test vectors.""" + # See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html + for msg, hexout in [ + (b"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"), + (b"a", "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"), + (b"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"), + (b"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"), + (b"abcdefghijklmnopqrstuvwxyz", + "f71c27109c692c1b56bbdceb5b9d2865b3708dbc"), + (b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "12a053384a9c0c88e405a06c27dcf49ada62eb2b"), + (b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "b0e20b6e3116640286ed3a87a5713079b21f5189"), + (b"1234567890" * 8, "9b752e45573d4b39f4dbd3323cab82bf63326bfb"), + (b"a" * 1000000, "52783243c1697bdbe16d37f97f68f08325dc1528") + ]: + self.assertEqual(ripemd160(msg).hex(), hexout) diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index 3c9b8a6e69..7791ae5392 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Functionality to build scripts, as well as signature hash functions. @@ -8,7 +8,6 @@ This file is modified from python-bitcoinlib. """ from collections import namedtuple -import hashlib import struct import unittest from typing import List, Dict @@ -25,6 +24,8 @@ from .messages import ( uint256_from_str, ) +from .ripemd160 import ripemd160 + MAX_SCRIPT_ELEMENT_SIZE = 520 LOCKTIME_THRESHOLD = 500000000 ANNEX_TAG = 0x50 @@ -32,7 +33,7 @@ ANNEX_TAG = 0x50 LEAF_VERSION_TAPSCRIPT = 0xc0 def hash160(s): - return hashlib.new('ripemd160', sha256(s)).digest() + return ripemd160(sha256(s)) def bn2vch(v): """Convert number to bitcoin-specific little endian format.""" @@ -619,16 +620,15 @@ def FindAndDelete(script, sig): r += script[last_sop_idx:] return CScript(r) -def LegacySignatureHash(script, txTo, inIdx, hashtype): - """Consensus-correct SignatureHash +def LegacySignatureMsg(script, txTo, inIdx, hashtype): + """Preimage of the signature hash, if it exists. - Returns (hash, err) to precisely match the consensus-critical behavior of - the SIGHASH_SINGLE bug. (inIdx is *not* checked for validity) + Returns either (None, err) to indicate error (which translates to sighash 1), + or (msg, None). """ - 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' if inIdx >= len(txTo.vin): - return (HASH_ONE, "inIdx %d out of range (%d)" % (inIdx, len(txTo.vin))) + return (None, "inIdx %d out of range (%d)" % (inIdx, len(txTo.vin))) txtmp = CTransaction(txTo) for txin in txtmp.vin: @@ -645,7 +645,7 @@ def LegacySignatureHash(script, txTo, inIdx, hashtype): elif (hashtype & 0x1f) == SIGHASH_SINGLE: outIdx = inIdx if outIdx >= len(txtmp.vout): - return (HASH_ONE, "outIdx %d out of range (%d)" % (outIdx, len(txtmp.vout))) + return (None, "outIdx %d out of range (%d)" % (outIdx, len(txtmp.vout))) tmp = txtmp.vout[outIdx] txtmp.vout = [] @@ -665,15 +665,27 @@ def LegacySignatureHash(script, txTo, inIdx, hashtype): s = txtmp.serialize_without_witness() s += struct.pack(b"<I", hashtype) - hash = hash256(s) + return (s, None) - return (hash, None) +def LegacySignatureHash(*args, **kwargs): + """Consensus-correct SignatureHash + + Returns (hash, err) to precisely match the consensus-critical behavior of + the SIGHASH_SINGLE bug. (inIdx is *not* checked for validity) + """ + + 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' + msg, err = LegacySignatureMsg(*args, **kwargs) + if msg is None: + return (HASH_ONE, err) + else: + return (hash256(msg), err) # TODO: Allow cached hashPrevouts/hashSequence/hashOutputs to be provided. # Performance optimization probably not necessary for python tests, however. # Note that this corresponds to sigversion == 1 in EvalScript, which is used # for version 0 witnesses. -def SegwitV0SignatureHash(script, txTo, inIdx, hashtype, amount): +def SegwitV0SignatureMsg(script, txTo, inIdx, hashtype, amount): hashPrevouts = 0 hashSequence = 0 @@ -711,8 +723,10 @@ def SegwitV0SignatureHash(script, txTo, inIdx, hashtype, amount): ss += ser_uint256(hashOutputs) ss += struct.pack("<i", txTo.nLockTime) ss += struct.pack("<I", hashtype) + return ss - return hash256(ss) +def SegwitV0SignatureHash(*args, **kwargs): + return hash256(SegwitV0SignatureMsg(*args, **kwargs)) class TestFrameworkScript(unittest.TestCase): def test_bn2vch(self): @@ -742,7 +756,22 @@ class TestFrameworkScript(unittest.TestCase): for value in values: self.assertEqual(CScriptNum.decode(CScriptNum.encode(CScriptNum(value))), value) -def TaprootSignatureHash(txTo, spent_utxos, hash_type, input_index = 0, scriptpath = False, script = CScript(), codeseparator_pos = -1, annex = None, leaf_ver = LEAF_VERSION_TAPSCRIPT): +def BIP341_sha_prevouts(txTo): + return sha256(b"".join(i.prevout.serialize() for i in txTo.vin)) + +def BIP341_sha_amounts(spent_utxos): + return sha256(b"".join(struct.pack("<q", u.nValue) for u in spent_utxos)) + +def BIP341_sha_scriptpubkeys(spent_utxos): + return sha256(b"".join(ser_string(u.scriptPubKey) for u in spent_utxos)) + +def BIP341_sha_sequences(txTo): + return sha256(b"".join(struct.pack("<I", i.nSequence) for i in txTo.vin)) + +def BIP341_sha_outputs(txTo): + return sha256(b"".join(o.serialize() for o in txTo.vout)) + +def TaprootSignatureMsg(txTo, spent_utxos, hash_type, input_index = 0, scriptpath = False, script = CScript(), codeseparator_pos = -1, annex = None, leaf_ver = LEAF_VERSION_TAPSCRIPT): assert (len(txTo.vin) == len(spent_utxos)) assert (input_index < len(txTo.vin)) out_type = SIGHASH_ALL if hash_type == 0 else hash_type & 3 @@ -752,12 +781,12 @@ def TaprootSignatureHash(txTo, spent_utxos, hash_type, input_index = 0, scriptpa ss += struct.pack("<i", txTo.nVersion) ss += struct.pack("<I", txTo.nLockTime) if in_type != SIGHASH_ANYONECANPAY: - ss += sha256(b"".join(i.prevout.serialize() for i in txTo.vin)) - ss += sha256(b"".join(struct.pack("<q", u.nValue) for u in spent_utxos)) - ss += sha256(b"".join(ser_string(u.scriptPubKey) for u in spent_utxos)) - ss += sha256(b"".join(struct.pack("<I", i.nSequence) for i in txTo.vin)) + ss += BIP341_sha_prevouts(txTo) + ss += BIP341_sha_amounts(spent_utxos) + ss += BIP341_sha_scriptpubkeys(spent_utxos) + ss += BIP341_sha_sequences(txTo) if out_type == SIGHASH_ALL: - ss += sha256(b"".join(o.serialize() for o in txTo.vout)) + ss += BIP341_sha_outputs(txTo) spend_type = 0 if annex is not None: spend_type |= 1 @@ -783,7 +812,10 @@ def TaprootSignatureHash(txTo, spent_utxos, hash_type, input_index = 0, scriptpa ss += bytes([0]) ss += struct.pack("<i", codeseparator_pos) assert len(ss) == 175 - (in_type == SIGHASH_ANYONECANPAY) * 49 - (out_type != SIGHASH_ALL and out_type != SIGHASH_SINGLE) * 32 + (annex is not None) * 32 + scriptpath * 37 - return TaggedHash("TapSighash", ss) + return ss + +def TaprootSignatureHash(*args, **kwargs): + return TaggedHash("TapSighash", TaprootSignatureMsg(*args, **kwargs)) def taproot_tree_helper(scripts): if len(scripts) == 0: @@ -805,20 +837,20 @@ def taproot_tree_helper(scripts): h = TaggedHash("TapLeaf", bytes([version]) + ser_string(code)) if name is None: return ([], h) - return ([(name, version, code, bytes())], h) + return ([(name, version, code, bytes(), h)], h) elif len(scripts) == 2 and callable(scripts[1]): # Two entries, and the right one is a function left, left_h = taproot_tree_helper(scripts[0:1]) right_h = scripts[1](left_h) - left = [(name, version, script, control + right_h) for name, version, script, control in left] + left = [(name, version, script, control + right_h, leaf) for name, version, script, control, leaf in left] right = [] else: # Two or more entries: descend into each side split_pos = len(scripts) // 2 left, left_h = taproot_tree_helper(scripts[0:split_pos]) right, right_h = taproot_tree_helper(scripts[split_pos:]) - left = [(name, version, script, control + right_h) for name, version, script, control in left] - right = [(name, version, script, control + left_h) for name, version, script, control in right] + left = [(name, version, script, control + right_h, leaf) for name, version, script, control, leaf in left] + right = [(name, version, script, control + left_h, leaf) for name, version, script, control, leaf in right] if right_h < left_h: right_h, left_h = left_h, right_h h = TaggedHash("TapBranch", left_h + right_h) @@ -830,13 +862,14 @@ def taproot_tree_helper(scripts): # - negflag: whether the pubkey in the scriptPubKey was negated from internal_pubkey+tweak*G (bool). # - tweak: the tweak (32 bytes) # - leaves: a dict of name -> TaprootLeafInfo objects for all known leaves -TaprootInfo = namedtuple("TaprootInfo", "scriptPubKey,internal_pubkey,negflag,tweak,leaves") +# - merkle_root: the script tree's Merkle root, or bytes() if no leaves are present +TaprootInfo = namedtuple("TaprootInfo", "scriptPubKey,internal_pubkey,negflag,tweak,leaves,merkle_root,output_pubkey") # A TaprootLeafInfo object has the following fields: # - script: the leaf script (CScript or bytes) # - version: the leaf version (0xc0 for BIP342 tapscript) # - merklebranch: the merkle branch to use for this leaf (32*N bytes) -TaprootLeafInfo = namedtuple("TaprootLeafInfo", "script,version,merklebranch") +TaprootLeafInfo = namedtuple("TaprootLeafInfo", "script,version,merklebranch,leaf_hash") def taproot_construct(pubkey, scripts=None): """Construct a tree of Taproot spending conditions @@ -858,8 +891,8 @@ def taproot_construct(pubkey, scripts=None): ret, h = taproot_tree_helper(scripts) tweak = TaggedHash("TapTweak", pubkey + h) tweaked, negated = tweak_add_pubkey(pubkey, tweak) - leaves = dict((name, TaprootLeafInfo(script, version, merklebranch)) for name, version, script, merklebranch in ret) - return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves) + leaves = dict((name, TaprootLeafInfo(script, version, merklebranch, leaf)) for name, version, script, merklebranch, leaf in ret) + return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked) def is_op_success(o): return o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or (o >= 0x95 and o <= 0x99) or (o >= 0xbb and o <= 0xfe) diff --git a/test/functional/test_framework/script_util.py b/test/functional/test_framework/script_util.py index 457be6b0e6..f7d8422eee 100755 --- a/test/functional/test_framework/script_util.py +++ b/test/functional/test_framework/script_util.py @@ -1,10 +1,21 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Useful Script constants and utils.""" -from test_framework.script import CScript, hash160, sha256, OP_0, OP_DUP, OP_HASH160, OP_CHECKSIG, OP_EQUAL, OP_EQUALVERIFY -from test_framework.util import hex_str_to_bytes +from test_framework.script import ( + CScript, + CScriptOp, + OP_0, + OP_CHECKMULTISIG, + OP_CHECKSIG, + OP_DUP, + OP_EQUAL, + OP_EQUALVERIFY, + OP_HASH160, + hash160, + sha256, +) # To prevent a "tx-size-small" policy rule error, a transaction has to have a # non-witness size of at least 82 bytes (MIN_STANDARD_TX_NONWITNESS_SIZE in @@ -26,58 +37,85 @@ from test_framework.util import hex_str_to_bytes DUMMY_P2WPKH_SCRIPT = CScript([b'a' * 21]) DUMMY_2_P2WPKH_SCRIPT = CScript([b'b' * 21]) -def keyhash_to_p2pkh_script(hash, main = False): + +def key_to_p2pk_script(key): + key = check_key(key) + return CScript([key, OP_CHECKSIG]) + + +def keys_to_multisig_script(keys, *, k=None): + n = len(keys) + if k is None: # n-of-n multisig by default + k = n + assert k <= n + op_k = CScriptOp.encode_op_n(k) + op_n = CScriptOp.encode_op_n(n) + checked_keys = [check_key(key) for key in keys] + return CScript([op_k] + checked_keys + [op_n, OP_CHECKMULTISIG]) + + +def keyhash_to_p2pkh_script(hash): assert len(hash) == 20 return CScript([OP_DUP, OP_HASH160, hash, OP_EQUALVERIFY, OP_CHECKSIG]) -def scripthash_to_p2sh_script(hash, main = False): + +def scripthash_to_p2sh_script(hash): assert len(hash) == 20 return CScript([OP_HASH160, hash, OP_EQUAL]) -def key_to_p2pkh_script(key, main = False): + +def key_to_p2pkh_script(key): key = check_key(key) - return keyhash_to_p2pkh_script(hash160(key), main) + return keyhash_to_p2pkh_script(hash160(key)) -def script_to_p2sh_script(script, main = False): + +def script_to_p2sh_script(script): script = check_script(script) - return scripthash_to_p2sh_script(hash160(script), main) + return scripthash_to_p2sh_script(hash160(script)) + -def key_to_p2sh_p2wpkh_script(key, main = False): +def key_to_p2sh_p2wpkh_script(key): key = check_key(key) p2shscript = CScript([OP_0, hash160(key)]) - return script_to_p2sh_script(p2shscript, main) + return script_to_p2sh_script(p2shscript) -def program_to_witness_script(version, program, main = False): + +def program_to_witness_script(version, program): if isinstance(program, str): - program = hex_str_to_bytes(program) + program = bytes.fromhex(program) assert 0 <= version <= 16 assert 2 <= len(program) <= 40 assert version > 0 or len(program) in [20, 32] return CScript([version, program]) -def script_to_p2wsh_script(script, main = False): + +def script_to_p2wsh_script(script): script = check_script(script) - return program_to_witness_script(0, sha256(script), main) + return program_to_witness_script(0, sha256(script)) -def key_to_p2wpkh_script(key, main = False): + +def key_to_p2wpkh_script(key): key = check_key(key) - return program_to_witness_script(0, hash160(key), main) + return program_to_witness_script(0, hash160(key)) + -def script_to_p2sh_p2wsh_script(script, main = False): +def script_to_p2sh_p2wsh_script(script): script = check_script(script) p2shscript = CScript([OP_0, sha256(script)]) - return script_to_p2sh_script(p2shscript, main) + return script_to_p2sh_script(p2shscript) + def check_key(key): if isinstance(key, str): - key = hex_str_to_bytes(key) # Assuming this is hex string + key = bytes.fromhex(key) # Assuming this is hex string if isinstance(key, bytes) and (len(key) == 33 or len(key) == 65): return key assert False + def check_script(script): if isinstance(script, str): - script = hex_str_to_bytes(script) # Assuming this is hex string + script = bytes.fromhex(script) # Assuming this is hex string if isinstance(script, bytes) or isinstance(script, CScript): return script assert False diff --git a/test/functional/test_framework/segwit_addr.py b/test/functional/test_framework/segwit_addr.py index 00c0d8a919..861ca2b949 100644 --- a/test/functional/test_framework/segwit_addr.py +++ b/test/functional/test_framework/segwit_addr.py @@ -2,10 +2,18 @@ # Copyright (c) 2017 Pieter Wuille # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Reference implementation for Bech32 and segwit addresses.""" +"""Reference implementation for Bech32/Bech32m and segwit addresses.""" import unittest +from enum import Enum CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32_CONST = 1 +BECH32M_CONST = 0x2bc830a3 + +class Encoding(Enum): + """Enumeration type to list the various supported encodings.""" + BECH32 = 1 + BECH32M = 2 def bech32_polymod(values): @@ -27,38 +35,45 @@ def bech32_hrp_expand(hrp): def bech32_verify_checksum(hrp, data): """Verify a checksum given HRP and converted data characters.""" - return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 - + check = bech32_polymod(bech32_hrp_expand(hrp) + data) + if check == BECH32_CONST: + return Encoding.BECH32 + elif check == BECH32M_CONST: + return Encoding.BECH32M + else: + return None -def bech32_create_checksum(hrp, data): +def bech32_create_checksum(encoding, hrp, data): """Compute the checksum values given HRP and data.""" values = bech32_hrp_expand(hrp) + data - polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 + const = BECH32M_CONST if encoding == Encoding.BECH32M else BECH32_CONST + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] -def bech32_encode(hrp, data): - """Compute a Bech32 string given HRP and data values.""" - combined = data + bech32_create_checksum(hrp, data) +def bech32_encode(encoding, hrp, data): + """Compute a Bech32 or Bech32m string given HRP and data values.""" + combined = data + bech32_create_checksum(encoding, hrp, data) return hrp + '1' + ''.join([CHARSET[d] for d in combined]) def bech32_decode(bech): - """Validate a Bech32 string, and determine HRP and data.""" + """Validate a Bech32/Bech32m string, and determine HRP and data.""" if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (bech.lower() != bech and bech.upper() != bech)): - return (None, None) + return (None, None, None) bech = bech.lower() pos = bech.rfind('1') if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: - return (None, None) + return (None, None, None) if not all(x in CHARSET for x in bech[pos+1:]): - return (None, None) + return (None, None, None) hrp = bech[:pos] data = [CHARSET.find(x) for x in bech[pos+1:]] - if not bech32_verify_checksum(hrp, data): - return (None, None) - return (hrp, data[:-6]) + encoding = bech32_verify_checksum(hrp, data) + if encoding is None: + return (None, None, None) + return (encoding, hrp, data[:-6]) def convertbits(data, frombits, tobits, pad=True): @@ -86,7 +101,7 @@ def convertbits(data, frombits, tobits, pad=True): def decode_segwit_address(hrp, addr): """Decode a segwit address.""" - hrpgot, data = bech32_decode(addr) + encoding, hrpgot, data = bech32_decode(addr) if hrpgot != hrp: return (None, None) decoded = convertbits(data[1:], 5, 8, False) @@ -96,12 +111,15 @@ def decode_segwit_address(hrp, addr): return (None, None) if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: return (None, None) + if (data[0] == 0 and encoding != Encoding.BECH32) or (data[0] != 0 and encoding != Encoding.BECH32M): + return (None, None) return (data[0], decoded) def encode_segwit_address(hrp, witver, witprog): """Encode a segwit address.""" - ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) + encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M + ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5)) if decode_segwit_address(hrp, ret) == (None, None): return None return ret @@ -119,3 +137,5 @@ class TestFrameworkScript(unittest.TestCase): # P2WSH test_python_bech32('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj') test_python_bech32('bcrt1qft5p2uhsdcdc3l2ua4ap5qqfg4pjaqlp250x7us7a8qqhrxrxfsqseac85') + # P2TR + test_python_bech32('bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6') diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 02eb10b5a4..8f75255caf 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Base class for RPC testing.""" @@ -19,7 +19,7 @@ import tempfile import time from typing import List -from .address import ADDRESS_BCRT1_P2WSH_OP_TRUE +from .address import create_deterministic_address_bcrt1_p2tr_op_true from .authproxy import JSONRPCException from . import coverage from .p2p import NetworkThread @@ -101,6 +101,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.supports_cli = True self.bind_to_localhost_only = True self.parse_args() + self.disable_syscall_sandbox = self.options.nosandbox or self.options.valgrind self.default_wallet_name = "default_wallet" if self.options.descriptors else "" self.wallet_data_filename = "wallet.dat" # Optional list of wallet names that can be set in set_test_params to @@ -112,6 +113,9 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): # By default the wallet is not required. Set to true by skip_if_no_wallet(). # When False, we ignore wallet_names regardless of what it is. self.requires_wallet = False + # Disable ThreadOpenConnections by default, so that adding entries to + # addrman will not result in automatic connections to them. + self.disable_autoconnect = True self.set_test_params() assert self.wallet_names is None or len(self.wallet_names) <= self.num_nodes if self.options.timeout_factor == 0 : @@ -156,6 +160,8 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): parser = argparse.ArgumentParser(usage="%(prog)s [options]") parser.add_argument("--nocleanup", dest="nocleanup", default=False, action="store_true", help="Leave bitcoinds and test.* datadir on exit or error") + parser.add_argument("--nosandbox", dest="nosandbox", default=False, action="store_true", + help="Don't use the syscall sandbox") parser.add_argument("--noshutdown", dest="noshutdown", default=False, action="store_true", help="Don't stop bitcoinds after the test execution") parser.add_argument("--cachedir", dest="cachedir", default=os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../../cache"), @@ -182,7 +188,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): parser.add_argument("--perf", dest="perf", default=False, action="store_true", help="profile running nodes with perf for the duration of the test") parser.add_argument("--valgrind", dest="valgrind", default=False, action="store_true", - help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown, valgrind 3.14 or later required") + help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown. valgrind 3.14 or later required. Forces --nosandbox.") parser.add_argument("--randomseed", type=int, help="set a random seed for deterministically reproducing a previous test run") parser.add_argument('--timeout-factor', dest="timeout_factor", type=float, default=1.0, help='adjust test timeouts by a factor. Setting it to 0 disables all timeouts') @@ -194,6 +200,10 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): help="Run test using legacy wallets", dest='descriptors') self.add_options(parser) + # Running TestShell in a Jupyter notebook causes an additional -f argument + # To keep TestShell from failing with an "unrecognized argument" error, we add a dummy "-f" argument + # source: https://stackoverflow.com/questions/48796169/how-to-fix-ipykernel-launcher-py-error-unrecognized-arguments-in-jupyter/56349168#56349168 + parser.add_argument("-f", "--fff", help="a dummy argument to fool ipython", default="1") self.options = parser.parse_args() self.options.previous_releases_path = previous_releases_path @@ -403,7 +413,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): # To ensure that all nodes are out of IBD, the most recent block # must have a timestamp not too old (see IsInitialBlockDownload()). self.log.debug('Generate a block with current time') - block_hash = self.nodes[0].generate(1)[0] + block_hash = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0] block = self.nodes[0].getblock(blockhash=block_hash, verbosity=0) for n in self.nodes: n.submitblock(block) @@ -413,15 +423,15 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): def import_deterministic_coinbase_privkeys(self): for i in range(self.num_nodes): - self.init_wallet(i) + self.init_wallet(node=i) - def init_wallet(self, i): - wallet_name = self.default_wallet_name if self.wallet_names is None else self.wallet_names[i] if i < len(self.wallet_names) else False + def init_wallet(self, *, node): + wallet_name = self.default_wallet_name if self.wallet_names is None else self.wallet_names[node] if node < len(self.wallet_names) else False if wallet_name is not False: - n = self.nodes[i] + n = self.nodes[node] if wallet_name is not None: n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors, load_on_startup=True) - n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase') + n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase', rescan=True) def run_test(self): """Tests must override this method to define test logic""" @@ -461,6 +471,10 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): extra_args = [[]] * num_nodes if versions is None: versions = [None] * num_nodes + if self.is_syscall_sandbox_compiled() and not self.disable_syscall_sandbox: + for i in range(len(extra_args)): + if versions[i] is None or versions[i] >= 219900: + extra_args[i] = extra_args[i] + ["-sandbox=log-and-abort"] if binary is None: binary = [get_bin_from_version(v, 'bitcoind', self.options.bitcoind) for v in versions] if binary_cli is None: @@ -553,18 +567,19 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.nodes[i].process.wait(timeout) def connect_nodes(self, a, b): - def connect_nodes_helper(from_connection, node_num): - ip_port = "127.0.0.1:" + str(p2p_port(node_num)) - from_connection.addnode(ip_port, "onetry") - # poll until version handshake complete to avoid race conditions - # with transaction relaying - # See comments in net_processing: - # * Must have a version message before anything else - # * Must have a verack message before anything else - wait_until_helper(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo())) - wait_until_helper(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in from_connection.getpeerinfo())) - - connect_nodes_helper(self.nodes[a], b) + from_connection = self.nodes[a] + to_connection = self.nodes[b] + ip_port = "127.0.0.1:" + str(p2p_port(b)) + from_connection.addnode(ip_port, "onetry") + # poll until version handshake complete to avoid race conditions + # with transaction relaying + # See comments in net_processing: + # * Must have a version message before anything else + # * Must have a verack message before anything else + wait_until_helper(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo())) + wait_until_helper(lambda: all(peer['version'] != 0 for peer in to_connection.getpeerinfo())) + wait_until_helper(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in from_connection.getpeerinfo())) + wait_until_helper(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in to_connection.getpeerinfo())) def disconnect_nodes(self, a, b): def disconnect_nodes_helper(from_connection, node_num): @@ -612,6 +627,29 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.connect_nodes(1, 2) self.sync_all() + def no_op(self): + pass + + def generate(self, generator, *args, sync_fun=None, **kwargs): + blocks = generator.generate(*args, invalid_call=False, **kwargs) + sync_fun() if sync_fun else self.sync_all() + return blocks + + def generateblock(self, generator, *args, sync_fun=None, **kwargs): + blocks = generator.generateblock(*args, invalid_call=False, **kwargs) + sync_fun() if sync_fun else self.sync_all() + return blocks + + def generatetoaddress(self, generator, *args, sync_fun=None, **kwargs): + blocks = generator.generatetoaddress(*args, invalid_call=False, **kwargs) + sync_fun() if sync_fun else self.sync_all() + return blocks + + def generatetodescriptor(self, generator, *args, sync_fun=None, **kwargs): + blocks = generator.generatetodescriptor(*args, invalid_call=False, **kwargs) + sync_fun() if sync_fun else self.sync_all() + return blocks + def sync_blocks(self, nodes=None, wait=1, timeout=60): """ Wait until everybody has the same tip. @@ -707,7 +745,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): if not os.path.isdir(cache_node_dir): self.log.debug("Creating cache directory {}".format(cache_node_dir)) - initialize_datadir(self.options.cachedir, CACHE_NODE_ID, self.chain) + initialize_datadir(self.options.cachedir, CACHE_NODE_ID, self.chain, self.disable_autoconnect) self.nodes.append( TestNode( CACHE_NODE_ID, @@ -739,11 +777,13 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): # block in the cache does not age too much (have an old tip age). # This is needed so that we are out of IBD when the test starts, # see the tip age check in IsInitialBlockDownload(). - gen_addresses = [k.address for k in TestNode.PRIV_KEYS] + [ADDRESS_BCRT1_P2WSH_OP_TRUE] + gen_addresses = [k.address for k in TestNode.PRIV_KEYS][:3] + [create_deterministic_address_bcrt1_p2tr_op_true()[0]] + assert_equal(len(gen_addresses), 4) for i in range(8): - cache_node.generatetoaddress( + self.generatetoaddress( + cache_node, nblocks=25 if i != 7 else 24, - address=gen_addresses[i % 4], + address=gen_addresses[i % len(gen_addresses)], ) assert_equal(cache_node.getblockchaininfo()["blocks"], 199) @@ -757,14 +797,14 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): os.rmdir(cache_path('wallets')) # Remove empty wallets dir for entry in os.listdir(cache_path()): - if entry not in ['chainstate', 'blocks']: # Only keep chainstate and blocks folder + if entry not in ['chainstate', 'blocks', 'indexes']: # Only indexes, chainstate and blocks folders os.remove(cache_path(entry)) for i in range(self.num_nodes): self.log.debug("Copy cache directory {} to node {}".format(cache_node_dir, i)) to_dir = get_datadir_path(self.options.tmpdir, i) shutil.copytree(cache_node_dir, to_dir) - initialize_datadir(self.options.tmpdir, i, self.chain) # Overwrite port/rpcport in bitcoin.conf + initialize_datadir(self.options.tmpdir, i, self.chain, self.disable_autoconnect) # Overwrite port/rpcport in bitcoin.conf def _initialize_chain_clean(self): """Initialize empty blockchain for use by the test. @@ -772,7 +812,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): Create an empty blockchain and num_nodes wallets. Useful if a test case wants complete control over initialization.""" for i in range(self.num_nodes): - initialize_datadir(self.options.tmpdir, i, self.chain) + initialize_datadir(self.options.tmpdir, i, self.chain, self.disable_autoconnect) def skip_if_no_py3_zmq(self): """Attempt to import the zmq package and skip the test if the import fails.""" @@ -846,6 +886,14 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): """Checks whether the wallet module was compiled.""" return self.config["components"].getboolean("ENABLE_WALLET") + def is_specified_wallet_compiled(self): + """Checks whether wallet support for the specified type + (legacy or descriptor wallet) was compiled.""" + if self.options.descriptors: + return self.is_sqlite_compiled() + else: + return self.is_bdb_compiled() + def is_wallet_tool_compiled(self): """Checks whether bitcoin-wallet was compiled.""" return self.config["components"].getboolean("ENABLE_WALLET_TOOL") @@ -861,3 +909,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): def is_bdb_compiled(self): """Checks whether the wallet module was compiled with BDB support.""" return self.config["components"].getboolean("USE_BDB") + + def is_syscall_sandbox_compiled(self): + """Checks whether the syscall sandbox was compiled.""" + return self.config["components"].getboolean("ENABLE_SYSCALL_SANDBOX") diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index ce9c1bc024..289e83579b 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Class for bitcoind node under test""" @@ -20,6 +20,7 @@ import urllib.parse import collections import shlex import sys +from pathlib import Path from .authproxy import JSONRPCException from .descriptors import descsum_create @@ -258,7 +259,7 @@ class TestNode(): return self.rpc = rpc self.rpc_connected = True - self.url = self.rpc.url + self.url = self.rpc.rpc_url return except JSONRPCException as e: # Initialization phase # -28 RPC in warmup @@ -297,9 +298,21 @@ class TestNode(): time.sleep(1.0 / poll_per_s) self._raise_assertion_error("Unable to retrieve cookie credentials after {}s".format(self.rpc_timeout)) - def generate(self, nblocks, maxtries=1000000): + def generate(self, nblocks, maxtries=1000000, **kwargs): self.log.debug("TestNode.generate() dispatches `generate` call to `generatetoaddress`") - return self.generatetoaddress(nblocks=nblocks, address=self.get_deterministic_priv_key().address, maxtries=maxtries) + return self.generatetoaddress(nblocks=nblocks, address=self.get_deterministic_priv_key().address, maxtries=maxtries, **kwargs) + + def generateblock(self, *args, invalid_call, **kwargs): + assert not invalid_call + return self.__getattr__('generateblock')(*args, **kwargs) + + def generatetoaddress(self, *args, invalid_call, **kwargs): + assert not invalid_call + return self.__getattr__('generatetoaddress')(*args, **kwargs) + + def generatetodescriptor(self, *args, invalid_call, **kwargs): + assert not invalid_call + return self.__getattr__('generatetodescriptor')(*args, **kwargs) def get_wallet_rpc(self, wallet_name): if self.use_cli: @@ -368,21 +381,31 @@ class TestNode(): def wait_until_stopped(self, timeout=BITCOIND_PROC_WAIT_TIMEOUT): wait_until_helper(self.is_node_stopped, timeout=timeout, timeout_factor=self.timeout_factor) + @property + def chain_path(self) -> Path: + return Path(self.datadir) / self.chain + + @property + def debug_log_path(self) -> Path: + return self.chain_path / 'debug.log' + + def debug_log_bytes(self) -> int: + with open(self.debug_log_path, encoding='utf-8') as dl: + dl.seek(0, 2) + return dl.tell() + @contextlib.contextmanager def assert_debug_log(self, expected_msgs, unexpected_msgs=None, timeout=2): if unexpected_msgs is None: unexpected_msgs = [] time_end = time.time() + timeout * self.timeout_factor - debug_log = os.path.join(self.datadir, self.chain, 'debug.log') - with open(debug_log, encoding='utf-8') as dl: - dl.seek(0, 2) - prev_size = dl.tell() + prev_size = self.debug_log_bytes() yield while True: found = True - with open(debug_log, encoding='utf-8') as dl: + with open(self.debug_log_path, encoding='utf-8') as dl: dl.seek(prev_size) log = dl.read() print_log = " - " + "\n - ".join(log.splitlines()) @@ -400,14 +423,51 @@ class TestNode(): self._raise_assertion_error('Expected messages "{}" does not partially match log:\n\n{}\n\n'.format(str(expected_msgs), print_log)) @contextlib.contextmanager - def profile_with_perf(self, profile_name): + def wait_for_debug_log(self, expected_msgs, timeout=60, ignore_case=False): + """ + Block until we see a particular debug log message fragment or until we exceed the timeout. + Return: + the number of log lines we encountered when matching + """ + time_end = time.time() + timeout * self.timeout_factor + prev_size = self.debug_log_bytes() + re_flags = re.MULTILINE | (re.IGNORECASE if ignore_case else 0) + + yield + + while True: + found = True + with open(self.debug_log_path, encoding='utf-8') as dl: + dl.seek(prev_size) + log = dl.read() + + for expected_msg in expected_msgs: + if re.search(re.escape(expected_msg), log, flags=re_flags) is None: + found = False + + if found: + return + + if time.time() >= time_end: + print_log = " - " + "\n - ".join(log.splitlines()) + break + + # No sleep here because we want to detect the message fragment as fast as + # possible. + + self._raise_assertion_error( + 'Expected messages "{}" does not partially match log:\n\n{}\n\n'.format( + str(expected_msgs), print_log)) + + @contextlib.contextmanager + def profile_with_perf(self, profile_name: str): """ Context manager that allows easy profiling of node activity using `perf`. See `test/functional/README.md` for details on perf usage. Args: - profile_name (str): This string will be appended to the + profile_name: This string will be appended to the profile data filename generated by perf. """ subp = self._start_perf(profile_name) @@ -491,6 +551,7 @@ class TestNode(): self.start(extra_args, stdout=log_stdout, stderr=log_stderr, *args, **kwargs) ret = self.process.wait(timeout=self.rpc_timeout) self.log.debug(self._node_msg(f'bitcoind exited with status {ret} during initialization')) + assert ret != 0 # Exit code must indicate failure self.running = False self.process = None # Check stderr for expected message @@ -556,9 +617,8 @@ class TestNode(): return p2p_conn def add_outbound_p2p_connection(self, p2p_conn, *, p2p_idx, connection_type="outbound-full-relay", **kwargs): - """Add an outbound p2p connection from node. Either - full-relay("outbound-full-relay") or - block-relay-only("block-relay-only") connection. + """Add an outbound p2p connection from node. Must be an + "outbound-full-relay", "block-relay-only", "addr-fetch" or "feeler" connection. This method adds the p2p connection to the self.p2ps list and returns the connection to the caller. @@ -570,11 +630,16 @@ class TestNode(): p2p_conn.peer_accept_connection(connect_cb=addconnection_callback, connect_id=p2p_idx + 1, net=self.chain, timeout_factor=self.timeout_factor, **kwargs)() - p2p_conn.wait_for_connect() - self.p2ps.append(p2p_conn) + if connection_type == "feeler": + # feeler connections are closed as soon as the node receives a `version` message + p2p_conn.wait_until(lambda: p2p_conn.message_count["version"] == 1, check_connected=False) + p2p_conn.wait_until(lambda: not p2p_conn.is_connected, check_connected=False) + else: + p2p_conn.wait_for_connect() + self.p2ps.append(p2p_conn) - p2p_conn.wait_for_verack() - p2p_conn.sync_with_ping() + p2p_conn.wait_for_verack() + p2p_conn.sync_with_ping() return p2p_conn diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index d335d4ea79..210025104e 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Helpful routines for regression testing.""" from base64 import b64encode -from binascii import unhexlify from decimal import Decimal, ROUND_DOWN from subprocess import CalledProcessError import hashlib @@ -19,7 +18,7 @@ import unittest from . import coverage from .authproxy import AuthServiceProxy, JSONRPCException -from io import BytesIO +from typing import Callable, Optional logger = logging.getLogger("TestFramework.utils") @@ -35,13 +34,15 @@ def assert_approx(v, vexp, vspan=0.00001): raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan))) -def assert_fee_amount(fee, tx_size, fee_per_kB): - """Assert the fee was in range""" - target_fee = round(tx_size * fee_per_kB / 1000, 8) +def assert_fee_amount(fee, tx_size, feerate_BTC_kvB): + """Assert the fee is in range.""" + assert isinstance(tx_size, int) + target_fee = get_fee(tx_size, feerate_BTC_kvB) if fee < target_fee: raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee))) # allow the wallet's estimation to be at most 2 bytes off - if fee > (tx_size + 2) * fee_per_kB / 1000: + high_fee = get_fee(tx_size + 2, feerate_BTC_kvB) + if fee > high_fee: raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee))) @@ -80,7 +81,7 @@ def assert_raises_message(exc, message, fun, *args, **kwds): raise AssertionError("No exception raised") -def assert_raises_process_error(returncode, output, fun, *args, **kwds): +def assert_raises_process_error(returncode: int, output: str, fun: Callable, *args, **kwds): """Execute a process and asserts the process return code and output. Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError @@ -88,9 +89,9 @@ def assert_raises_process_error(returncode, output, fun, *args, **kwds): no CalledProcessError was raised or if the return code and output are not as expected. Args: - returncode (int): the process return code. - output (string): [a substring of] the process output. - fun (function): the function to call. This should execute a process. + returncode: the process return code. + output: [a substring of] the process output. + fun: the function to call. This should execute a process. args*: positional arguments for the function. kwds**: named arguments for the function. """ @@ -105,7 +106,7 @@ def assert_raises_process_error(returncode, output, fun, *args, **kwds): raise AssertionError("No exception raised") -def assert_raises_rpc_error(code, message, fun, *args, **kwds): +def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Callable, *args, **kwds): """Run an RPC and verify that a specific JSONRPC exception code and message is raised. Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException @@ -113,11 +114,11 @@ def assert_raises_rpc_error(code, message, fun, *args, **kwds): no JSONRPCException was raised or if the error code/message are not as expected. Args: - code (int), optional: the error code returned by the RPC call (defined - in src/rpc/protocol.h). Set to None if checking the error code is not required. - message (string), optional: [a substring of] the error string returned by the - RPC call. Set to None if checking the error string is not required. - fun (function): the function to call. This should be the name of an RPC. + code: the error code returned by the RPC call (defined in src/rpc/protocol.h). + Set to None if checking the error code is not required. + message: [a substring of] the error string returned by the RPC call. + Set to None if checking the error string is not required. + fun: the function to call. This should be the name of an RPC. args*: positional arguments for the function. kwds**: named arguments for the function. """ @@ -214,14 +215,28 @@ def count_bytes(hex_string): return len(bytearray.fromhex(hex_string)) -def hex_str_to_bytes(hex_str): - return unhexlify(hex_str.encode('ascii')) - - def str_to_b64str(string): return b64encode(string.encode('utf-8')).decode('ascii') +def ceildiv(a, b): + """ + Divide 2 ints and round up to next int rather than round down + Implementation requires python integers, which have a // operator that does floor division. + Other types like decimal.Decimal whose // operator truncates towards 0 will not work. + """ + assert isinstance(a, int) + assert isinstance(b, int) + return -(-a // b) + + +def get_fee(tx_size, feerate_btc_kvb): + """Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee""" + feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors + target_fee_sat = ceildiv(feerate_sat_kvb * tx_size, 1000) # Round calculated fee up to nearest sat + return target_fee_sat / Decimal(1e8) # Return result in BTC + + def satoshi_round(amount): return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) @@ -261,6 +276,7 @@ def wait_until_helper(predicate, *, attempts=float('inf'), timeout=float('inf'), raise AssertionError("Predicate {} not true after {} seconds".format(predicate_source, timeout)) raise RuntimeError('Unreachable') + def sha256sum_file(filename): h = hashlib.sha256() with open(filename, 'rb') as f: @@ -286,15 +302,15 @@ class PortSeed: n = None -def get_rpc_proxy(url, node_number, *, timeout=None, coveragedir=None): +def get_rpc_proxy(url: str, node_number: int, *, timeout: int=None, coveragedir: str=None) -> coverage.AuthServiceProxyWrapper: """ Args: - url (str): URL of the RPC server to call - node_number (int): the node number (or id) that this calls to + url: URL of the RPC server to call + node_number: the node number (or id) that this calls to Kwargs: - timeout (int): HTTP timeout in seconds - coveragedir (str): Directory + timeout: HTTP timeout in seconds + coveragedir: Directory Returns: AuthServiceProxy. convenience object for making RPC calls. @@ -305,11 +321,10 @@ def get_rpc_proxy(url, node_number, *, timeout=None, coveragedir=None): proxy_kwargs['timeout'] = int(timeout) proxy = AuthServiceProxy(url, **proxy_kwargs) - proxy.url = url # store URL on proxy for info coverage_logfile = coverage.get_filename(coveragedir, node_number) if coveragedir else None - return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile) + return coverage.AuthServiceProxyWrapper(proxy, url, coverage_logfile) def p2p_port(n): @@ -338,17 +353,17 @@ def rpc_url(datadir, i, chain, rpchost): ################ -def initialize_datadir(dirname, n, chain): +def initialize_datadir(dirname, n, chain, disable_autoconnect=True): datadir = get_datadir_path(dirname, n) if not os.path.isdir(datadir): os.makedirs(datadir) - write_config(os.path.join(datadir, "bitcoin.conf"), n=n, chain=chain) + write_config(os.path.join(datadir, "bitcoin.conf"), n=n, chain=chain, disable_autoconnect=disable_autoconnect) os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True) os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True) return datadir -def write_config(config_path, *, n, chain, extra_config=""): +def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect=True): # Translate chain subdirectory name to config name if chain == 'testnet3': chain_name_conf_arg = 'testnet' @@ -370,10 +385,19 @@ def write_config(config_path, *, n, chain, extra_config=""): f.write("dnsseed=0\n") f.write("fixedseeds=0\n") f.write("listenonion=0\n") + # Increase peertimeout to avoid disconnects while using mocktime. + # peertimeout is measured in mock time, so setting it large enough to + # cover any duration in mock time is sufficient. It can be overridden + # in tests. + f.write("peertimeout=999999999\n") f.write("printtoconsole=0\n") f.write("upnp=0\n") f.write("natpmp=0\n") f.write("shrinkdebugfile=0\n") + # To improve SQLite wallet performance so that the tests don't timeout, use -unsafesqlitesync + f.write("unsafesqlitesync=1\n") + if disable_autoconnect: + f.write("connect=0\n") f.write(extra_config) @@ -421,7 +445,7 @@ def delete_cookie_file(datadir, chain): def softfork_active(node, key): """Return whether a softfork is active.""" - return node.getblockchaininfo()['softforks'][key]['active'] + return node.getdeploymentinfo()['deployments'][key]['active'] def set_node_times(nodes, t): @@ -429,6 +453,12 @@ def set_node_times(nodes, t): node.setmocktime(t) +def check_node_connections(*, node, num_in, num_out): + info = node.getnetworkinfo() + assert_equal(info["connections_in"], num_in) + assert_equal(info["connections_out"], num_out) + + # Transaction/Block functions ############################# @@ -447,10 +477,10 @@ def find_output(node, txid, amount, *, blockhash=None): # Helper to create at least "count" utxos # Pass in a fee that is sufficient for relay and mining new transactions. -def create_confirmed_utxos(fee, node, count): +def create_confirmed_utxos(test_framework, fee, node, count, **kwargs): to_generate = int(0.5 * count) + 101 while to_generate > 0: - node.generate(min(25, to_generate)) + test_framework.generate(node, min(25, to_generate), **kwargs) to_generate -= 25 utxos = node.listunspent() iterations = count - len(utxos) @@ -471,13 +501,35 @@ def create_confirmed_utxos(fee, node, count): node.sendrawtransaction(signed_tx) while (node.getmempoolinfo()['size'] > 0): - node.generate(1) + test_framework.generate(node, 1, **kwargs) utxos = node.listunspent() assert len(utxos) >= count return utxos +def chain_transaction(node, parent_txids, vouts, value, fee, num_outputs): + """Build and send a transaction that spends the given inputs (specified + by lists of parent_txid:vout each), with the desired total value and fee, + equally divided up to the desired number of outputs. + + Returns a tuple with the txid and the amount sent per output. + """ + send_value = satoshi_round((value - fee)/num_outputs) + inputs = [] + for (txid, vout) in zip(parent_txids, vouts): + inputs.append({'txid' : txid, 'vout' : vout}) + outputs = {} + for _ in range(num_outputs): + outputs[node.getnewaddress()] = send_value + rawtx = node.createrawtransaction(inputs, outputs, 0, True) + signedtx = node.signrawtransactionwithwallet(rawtx) + txid = node.sendrawtransaction(signedtx['hex']) + fulltx = node.getrawtransaction(txid, 1) + assert len(fulltx['vout']) == num_outputs # make sure we didn't generate a change output + return (txid, send_value) + + # Create large OP_RETURN txouts that can be appended to a transaction # to make it large (helper for constructing large transactions). def gen_return_txouts(): @@ -492,7 +544,7 @@ def gen_return_txouts(): from .messages import CTxOut txout = CTxOut() txout.nValue = 0 - txout.scriptPubKey = hex_str_to_bytes(script_pubkey) + txout.scriptPubKey = bytes.fromhex(script_pubkey) for _ in range(128): txouts.append(txout) return txouts @@ -503,7 +555,7 @@ def gen_return_txouts(): def create_lots_of_big_transactions(node, txouts, utxos, num, fee): addr = node.getnewaddress() txids = [] - from .messages import CTransaction + from .messages import tx_from_hex for _ in range(num): t = utxos.pop() inputs = [{"txid": t["txid"], "vout": t["vout"]}] @@ -511,8 +563,7 @@ def create_lots_of_big_transactions(node, txouts, utxos, num, fee): change = t['amount'] - fee outputs[addr] = satoshi_round(change) rawtx = node.createrawtransaction(inputs, outputs) - tx = CTransaction() - tx.deserialize(BytesIO(hex_str_to_bytes(rawtx))) + tx = tx_from_hex(rawtx) for txout in txouts: tx.vout.append(txout) newtx = tx.serialize().hex() @@ -522,7 +573,7 @@ def create_lots_of_big_transactions(node, txouts, utxos, num, fee): return txids -def mine_large_block(node, utxos=None): +def mine_large_block(test_framework, node, utxos=None): # generate a 66k transaction, # and 14 of them is close to the 1MB block limit num = 14 @@ -533,7 +584,7 @@ def mine_large_block(node, utxos=None): utxos.extend(node.listunspent()) fee = 100 * node.getnetworkinfo()["relayfee"] create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee) - node.generate(1) + test_framework.generate(node, 1) def find_vout_for_address(node, txid, addr): @@ -543,7 +594,7 @@ def find_vout_for_address(node, txid, addr): """ tx = node.getrawtransaction(txid, True) for i in range(len(tx["vout"])): - if any([addr == a for a in tx["vout"][i]["scriptPubKey"]["addresses"]]): + if addr == tx["vout"][i]["scriptPubKey"]["address"]: return i raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr)) diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 38fbf3c1a6..dd41a740ae 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -1,11 +1,23 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """A limited-functionality wallet, which may replace a real wallet in tests""" +from copy import deepcopy from decimal import Decimal -from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE +from enum import Enum +from random import choice +from typing import Optional +from test_framework.address import ( + base58_to_byte, + create_deterministic_address_bcrt1_p2tr_op_true, + key_to_p2pkh, + key_to_p2sh_p2wpkh, + key_to_p2wpkh, +) +from test_framework.descriptors import descsum_create +from test_framework.key import ECKey from test_framework.messages import ( COIN, COutPoint, @@ -13,76 +25,305 @@ from test_framework.messages import ( CTxIn, CTxInWitness, CTxOut, + tx_from_hex, ) from test_framework.script import ( CScript, + LegacySignatureHash, + LEAF_VERSION_TAPSCRIPT, + OP_NOP, OP_TRUE, + SIGHASH_ALL, +) +from test_framework.script_util import ( + key_to_p2pk_script, + key_to_p2pkh_script, + key_to_p2sh_p2wpkh_script, + key_to_p2wpkh_script, + keyhash_to_p2pkh_script, + scripthash_to_p2sh_script, ) from test_framework.util import ( assert_equal, - hex_str_to_bytes, - satoshi_round, + assert_greater_than_or_equal, ) +DEFAULT_FEE = Decimal("0.0001") + +class MiniWalletMode(Enum): + """Determines the transaction type the MiniWallet is creating and spending. + + For most purposes, the default mode ADDRESS_OP_TRUE should be sufficient; + it simply uses a fixed bech32m P2TR address whose coins are spent with a + witness stack of OP_TRUE, i.e. following an anyone-can-spend policy. + However, if the transactions need to be modified by the user (e.g. prepending + scriptSig for testing opcodes that are activated by a soft-fork), or the txs + should contain an actual signature, the raw modes RAW_OP_TRUE and RAW_P2PK + can be useful. Summary of modes: + + | output | | tx is | can modify | needs + mode | description | address | standard | scriptSig | signing + ----------------+-------------------+-----------+----------+------------+---------- + ADDRESS_OP_TRUE | anyone-can-spend | bech32m | yes | no | no + RAW_OP_TRUE | anyone-can-spend | - (raw) | no | yes | no + RAW_P2PK | pay-to-public-key | - (raw) | yes | yes | yes + """ + ADDRESS_OP_TRUE = 1 + RAW_OP_TRUE = 2 + RAW_P2PK = 3 + class MiniWallet: - def __init__(self, test_node): + def __init__(self, test_node, *, mode=MiniWalletMode.ADDRESS_OP_TRUE): self._test_node = test_node self._utxos = [] - self._address = ADDRESS_BCRT1_P2WSH_OP_TRUE - self._scriptPubKey = hex_str_to_bytes(self._test_node.validateaddress(self._address)['scriptPubKey']) - - def scan_blocks(self, *, start=1, num): - """Scan the blocks for self._address outputs and add them to self._utxos""" - for i in range(start, start + num): - block = self._test_node.getblock(blockhash=self._test_node.getblockhash(i), verbosity=2) - for tx in block['tx']: - for out in tx['vout']: - if out['scriptPubKey']['hex'] == self._scriptPubKey.hex(): - self._utxos.append({'txid': tx['txid'], 'vout': out['n'], 'value': out['value']}) - - def generate(self, num_blocks): + self._priv_key = None + self._address = None + + assert isinstance(mode, MiniWalletMode) + if mode == MiniWalletMode.RAW_OP_TRUE: + self._scriptPubKey = bytes(CScript([OP_TRUE])) + elif mode == MiniWalletMode.RAW_P2PK: + # use simple deterministic private key (k=1) + self._priv_key = ECKey() + self._priv_key.set((1).to_bytes(32, 'big'), True) + pub_key = self._priv_key.get_pubkey() + self._scriptPubKey = key_to_p2pk_script(pub_key.get_bytes()) + elif mode == MiniWalletMode.ADDRESS_OP_TRUE: + self._address, self._internal_key = create_deterministic_address_bcrt1_p2tr_op_true() + self._scriptPubKey = bytes.fromhex(self._test_node.validateaddress(self._address)['scriptPubKey']) + + def rescan_utxos(self): + """Drop all utxos and rescan the utxo set""" + self._utxos = [] + res = self._test_node.scantxoutset(action="start", scanobjects=[self.get_descriptor()]) + assert_equal(True, res['success']) + for utxo in res['unspents']: + self._utxos.append({'txid': utxo['txid'], 'vout': utxo['vout'], 'value': utxo['amount'], 'height': utxo['height']}) + + def scan_tx(self, tx): + """Scan the tx for self._scriptPubKey outputs and add them to self._utxos""" + for out in tx['vout']: + if out['scriptPubKey']['hex'] == self._scriptPubKey.hex(): + self._utxos.append({'txid': tx['txid'], 'vout': out['n'], 'value': out['value'], 'height': 0}) + + def sign_tx(self, tx, fixed_length=True): + """Sign tx that has been created by MiniWallet in P2PK mode""" + assert self._priv_key is not None + (sighash, err) = LegacySignatureHash(CScript(self._scriptPubKey), tx, 0, SIGHASH_ALL) + assert err is None + # for exact fee calculation, create only signatures with fixed size by default (>49.89% probability): + # 65 bytes: high-R val (33 bytes) + low-S val (32 bytes) + # with the DER header/skeleton data of 6 bytes added, this leads to a target size of 71 bytes + der_sig = b'' + while not len(der_sig) == 71: + der_sig = self._priv_key.sign_ecdsa(sighash) + if not fixed_length: + break + tx.vin[0].scriptSig = CScript([der_sig + bytes(bytearray([SIGHASH_ALL]))]) + + def generate(self, num_blocks, **kwargs): """Generate blocks with coinbase outputs to the internal address, and append the outputs to the internal list""" - blocks = self._test_node.generatetoaddress(num_blocks, self._address) + blocks = self._test_node.generatetodescriptor(num_blocks, self.get_descriptor(), **kwargs) for b in blocks: - cb_tx = self._test_node.getblock(blockhash=b, verbosity=2)['tx'][0] - self._utxos.append({'txid': cb_tx['txid'], 'vout': 0, 'value': cb_tx['vout'][0]['value']}) + block_info = self._test_node.getblock(blockhash=b, verbosity=2) + cb_tx = block_info['tx'][0] + self._utxos.append({'txid': cb_tx['txid'], 'vout': 0, 'value': cb_tx['vout'][0]['value'], 'height': block_info['height']}) return blocks - def get_utxo(self, *, txid=''): + def get_descriptor(self): + return descsum_create(f'raw({self._scriptPubKey.hex()})') + + def get_address(self): + return self._address + + def get_utxo(self, *, txid: Optional[str]='', mark_as_spent=True): """ Returns a utxo and marks it as spent (pops it from the internal list) Args: - txid (string), optional: get the first utxo we find from a specific transaction - - Note: Can be used to get the change output immediately after a send_self_transfer + txid: get the first utxo we find from a specific transaction """ index = -1 # by default the last utxo + self._utxos = sorted(self._utxos, key=lambda k: (k['value'], -k['height'])) # Put the largest utxo last if txid: utxo = next(filter(lambda utxo: txid == utxo['txid'], self._utxos)) index = self._utxos.index(utxo) - return self._utxos.pop(index) + if mark_as_spent: + return self._utxos.pop(index) + else: + return self._utxos[index] - def send_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node, utxo_to_spend=None): + def send_self_transfer(self, **kwargs): """Create and send a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" - self._utxos = sorted(self._utxos, key=lambda k: k['value']) - utxo_to_spend = utxo_to_spend or self._utxos.pop() # Pick the largest utxo (if none provided) and hope it covers the fee - vsize = Decimal(96) - send_value = satoshi_round(utxo_to_spend['value'] - fee_rate * (vsize / 1000)) - fee = utxo_to_spend['value'] - send_value + tx = self.create_self_transfer(**kwargs) + self.sendrawtransaction(from_node=kwargs['from_node'], tx_hex=tx['hex']) + return tx + + def send_to(self, *, from_node, scriptPubKey, amount, fee=1000): + """ + Create and send a tx with an output to a given scriptPubKey/amount, + plus a change output to our internal address. To keep things simple, a + fixed fee given in Satoshi is used. + + Note that this method fails if there is no single internal utxo + available that can cover the cost for the amount and the fixed fee + (the utxo with the largest value is taken). + + Returns a tuple (txid, n) referring to the created external utxo outpoint. + """ + tx = self.create_self_transfer(from_node=from_node, fee_rate=0, mempool_valid=False)['tx'] + assert_greater_than_or_equal(tx.vout[0].nValue, amount + fee) + tx.vout[0].nValue -= (amount + fee) # change output -> MiniWallet + tx.vout.append(CTxOut(amount, scriptPubKey)) # arbitrary output -> to be returned + txid = self.sendrawtransaction(from_node=from_node, tx_hex=tx.serialize().hex()) + return txid, 1 + + def create_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node=None, utxo_to_spend=None, mempool_valid=True, locktime=0, sequence=0): + """Create and return a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" + from_node = from_node or self._test_node + utxo_to_spend = utxo_to_spend or self.get_utxo() + if self._priv_key is None: + vsize = Decimal(104) # anyone-can-spend + else: + vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other) + send_value = int(COIN * (utxo_to_spend['value'] - fee_rate * (vsize / 1000))) assert send_value > 0 tx = CTransaction() - tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']))] - tx.vout = [CTxOut(int(send_value * COIN), self._scriptPubKey)] - tx.wit.vtxinwit = [CTxInWitness()] - tx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])] + tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=sequence)] + tx.vout = [CTxOut(send_value, self._scriptPubKey)] + tx.nLockTime = locktime + if not self._address: + # raw script + if self._priv_key is not None: + # P2PK, need to sign + self.sign_tx(tx) + else: + # anyone-can-spend + tx.vin[0].scriptSig = CScript([OP_NOP] * 43) # pad to identical size + else: + tx.wit.vtxinwit = [CTxInWitness()] + tx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE]), bytes([LEAF_VERSION_TAPSCRIPT]) + self._internal_key] tx_hex = tx.serialize().hex() tx_info = from_node.testmempoolaccept([tx_hex])[0] - self._utxos.append({'txid': tx_info['txid'], 'vout': 0, 'value': send_value}) - from_node.sendrawtransaction(tx_hex) - assert_equal(tx_info['vsize'], vsize) - assert_equal(tx_info['fees']['base'], fee) - return {'txid': tx_info['txid'], 'wtxid': tx_info['wtxid'], 'hex': tx_hex} + assert_equal(mempool_valid, tx_info['allowed']) + if mempool_valid: + assert_equal(tx_info['vsize'], vsize) + assert_equal(tx_info['fees']['base'], utxo_to_spend['value'] - Decimal(send_value) / COIN) + return {'txid': tx_info['txid'], 'wtxid': tx_info['wtxid'], 'hex': tx_hex, 'tx': tx} + + def sendrawtransaction(self, *, from_node, tx_hex, **kwargs): + txid = from_node.sendrawtransaction(hexstring=tx_hex, **kwargs) + self.scan_tx(from_node.decoderawtransaction(tx_hex)) + return txid + + +def getnewdestination(address_type='bech32'): + """Generate a random destination of the specified type and return the + corresponding public key, scriptPubKey and address. Supported types are + 'legacy', 'p2sh-segwit' and 'bech32'. Can be used when a random + destination is needed, but no compiled wallet is available (e.g. as + replacement to the getnewaddress/getaddressinfo RPCs).""" + key = ECKey() + key.generate() + pubkey = key.get_pubkey().get_bytes() + if address_type == 'legacy': + scriptpubkey = key_to_p2pkh_script(pubkey) + address = key_to_p2pkh(pubkey) + elif address_type == 'p2sh-segwit': + scriptpubkey = key_to_p2sh_p2wpkh_script(pubkey) + address = key_to_p2sh_p2wpkh(pubkey) + elif address_type == 'bech32': + scriptpubkey = key_to_p2wpkh_script(pubkey) + address = key_to_p2wpkh(pubkey) + # TODO: also support bech32m (need to generate x-only-pubkey) + else: + assert False + return pubkey, scriptpubkey, address + + +def address_to_scriptpubkey(address): + """Converts a given address to the corresponding output script (scriptPubKey).""" + payload, version = base58_to_byte(address) + if version == 111: # testnet pubkey hash + return keyhash_to_p2pkh_script(payload) + elif version == 196: # testnet script hash + return scripthash_to_p2sh_script(payload) + # TODO: also support other address formats + else: + assert False + + +def make_chain(node, address, privkeys, parent_txid, parent_value, n=0, parent_locking_script=None, fee=DEFAULT_FEE): + """Build a transaction that spends parent_txid.vout[n] and produces one output with + amount = parent_value with a fee deducted. + Return tuple (CTransaction object, raw hex, nValue, scriptPubKey of the output created). + """ + inputs = [{"txid": parent_txid, "vout": n}] + my_value = parent_value - fee + outputs = {address : my_value} + rawtx = node.createrawtransaction(inputs, outputs) + prevtxs = [{ + "txid": parent_txid, + "vout": n, + "scriptPubKey": parent_locking_script, + "amount": parent_value, + }] if parent_locking_script else None + signedtx = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=privkeys, prevtxs=prevtxs) + assert signedtx["complete"] + tx = tx_from_hex(signedtx["hex"]) + return (tx, signedtx["hex"], my_value, tx.vout[0].scriptPubKey.hex()) + +def create_child_with_parents(node, address, privkeys, parents_tx, values, locking_scripts, fee=DEFAULT_FEE): + """Creates a transaction that spends the first output of each parent in parents_tx.""" + num_parents = len(parents_tx) + total_value = sum(values) + inputs = [{"txid": tx.rehash(), "vout": 0} for tx in parents_tx] + outputs = {address : total_value - fee} + rawtx_child = node.createrawtransaction(inputs, outputs) + prevtxs = [] + for i in range(num_parents): + prevtxs.append({"txid": parents_tx[i].rehash(), "vout": 0, "scriptPubKey": locking_scripts[i], "amount": values[i]}) + signedtx_child = node.signrawtransactionwithkey(hexstring=rawtx_child, privkeys=privkeys, prevtxs=prevtxs) + assert signedtx_child["complete"] + return signedtx_child["hex"] + +def create_raw_chain(node, first_coin, address, privkeys, chain_length=25): + """Helper function: create a "chain" of chain_length transactions. The nth transaction in the + chain is a child of the n-1th transaction and parent of the n+1th transaction. + """ + parent_locking_script = None + txid = first_coin["txid"] + chain_hex = [] + chain_txns = [] + value = first_coin["amount"] + + for _ in range(chain_length): + (tx, txhex, value, parent_locking_script) = make_chain(node, address, privkeys, txid, value, 0, parent_locking_script) + txid = tx.rehash() + chain_hex.append(txhex) + chain_txns.append(tx) + + return (chain_hex, chain_txns) + +def bulk_transaction(tx, node, target_weight, privkeys, prevtxs=None): + """Pad a transaction with extra outputs until it reaches a target weight (or higher). + returns CTransaction object + """ + tx_heavy = deepcopy(tx) + assert_greater_than_or_equal(target_weight, tx_heavy.get_weight()) + while tx_heavy.get_weight() < target_weight: + random_spk = "6a4d0200" # OP_RETURN OP_PUSH2 512 bytes + for _ in range(512*2): + random_spk += choice("0123456789ABCDEF") + tx_heavy.vout.append(CTxOut(0, bytes.fromhex(random_spk))) + # Re-sign the transaction + if privkeys: + signed = node.signrawtransactionwithkey(tx_heavy.serialize().hex(), privkeys, prevtxs) + return tx_from_hex(signed["hex"]) + # OP_TRUE + tx_heavy.wit.vtxinwit = [CTxInWitness()] + tx_heavy.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])] + return tx_heavy diff --git a/test/functional/test_framework/wallet_util.py b/test/functional/test_framework/wallet_util.py index b9c0fb6691..410d85cd8c 100755 --- a/test/functional/test_framework/wallet_util.py +++ b/test/functional/test_framework/wallet_util.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Useful util functions for testing the wallet""" @@ -15,21 +15,13 @@ from test_framework.address import ( script_to_p2wsh, ) from test_framework.key import ECKey -from test_framework.script import ( - CScript, - OP_0, - OP_2, - OP_3, - OP_CHECKMULTISIG, - OP_CHECKSIG, - OP_DUP, - OP_EQUAL, - OP_EQUALVERIFY, - OP_HASH160, - hash160, - sha256, +from test_framework.script_util import ( + key_to_p2pkh_script, + key_to_p2wpkh_script, + keys_to_multisig_script, + script_to_p2sh_script, + script_to_p2wsh_script, ) -from test_framework.util import hex_str_to_bytes Key = namedtuple('Key', ['privkey', 'pubkey', @@ -57,15 +49,14 @@ def get_key(node): Returns a named tuple of privkey, pubkey and all address and scripts.""" addr = node.getnewaddress() pubkey = node.getaddressinfo(addr)['pubkey'] - pkh = hash160(hex_str_to_bytes(pubkey)) return Key(privkey=node.dumpprivkey(addr), pubkey=pubkey, - p2pkh_script=CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]).hex(), + p2pkh_script=key_to_p2pkh_script(pubkey).hex(), p2pkh_addr=key_to_p2pkh(pubkey), - p2wpkh_script=CScript([OP_0, pkh]).hex(), + p2wpkh_script=key_to_p2wpkh_script(pubkey).hex(), p2wpkh_addr=key_to_p2wpkh(pubkey), - p2sh_p2wpkh_script=CScript([OP_HASH160, hash160(CScript([OP_0, pkh])), OP_EQUAL]).hex(), - p2sh_p2wpkh_redeem_script=CScript([OP_0, pkh]).hex(), + p2sh_p2wpkh_script=script_to_p2sh_script(key_to_p2wpkh_script(pubkey)).hex(), + p2sh_p2wpkh_redeem_script=key_to_p2wpkh_script(pubkey).hex(), p2sh_p2wpkh_addr=key_to_p2sh_p2wpkh(pubkey)) def get_generate_key(): @@ -76,15 +67,14 @@ def get_generate_key(): eckey.generate() privkey = bytes_to_wif(eckey.get_bytes()) pubkey = eckey.get_pubkey().get_bytes().hex() - pkh = hash160(hex_str_to_bytes(pubkey)) return Key(privkey=privkey, pubkey=pubkey, - p2pkh_script=CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]).hex(), + p2pkh_script=key_to_p2pkh_script(pubkey).hex(), p2pkh_addr=key_to_p2pkh(pubkey), - p2wpkh_script=CScript([OP_0, pkh]).hex(), + p2wpkh_script=key_to_p2wpkh_script(pubkey).hex(), p2wpkh_addr=key_to_p2wpkh(pubkey), - p2sh_p2wpkh_script=CScript([OP_HASH160, hash160(CScript([OP_0, pkh])), OP_EQUAL]).hex(), - p2sh_p2wpkh_redeem_script=CScript([OP_0, pkh]).hex(), + p2sh_p2wpkh_script=script_to_p2sh_script(key_to_p2wpkh_script(pubkey)).hex(), + p2sh_p2wpkh_redeem_script=key_to_p2wpkh_script(pubkey).hex(), p2sh_p2wpkh_addr=key_to_p2sh_p2wpkh(pubkey)) def get_multisig(node): @@ -97,16 +87,16 @@ def get_multisig(node): addr = node.getaddressinfo(node.getnewaddress()) addrs.append(addr['address']) pubkeys.append(addr['pubkey']) - script_code = CScript([OP_2] + [hex_str_to_bytes(pubkey) for pubkey in pubkeys] + [OP_3, OP_CHECKMULTISIG]) - witness_script = CScript([OP_0, sha256(script_code)]) + script_code = keys_to_multisig_script(pubkeys, k=2) + witness_script = script_to_p2wsh_script(script_code) return Multisig(privkeys=[node.dumpprivkey(addr) for addr in addrs], pubkeys=pubkeys, - p2sh_script=CScript([OP_HASH160, hash160(script_code), OP_EQUAL]).hex(), + p2sh_script=script_to_p2sh_script(script_code).hex(), p2sh_addr=script_to_p2sh(script_code), redeem_script=script_code.hex(), p2wsh_script=witness_script.hex(), p2wsh_addr=script_to_p2wsh(script_code), - p2sh_p2wsh_script=CScript([OP_HASH160, witness_script, OP_EQUAL]).hex(), + p2sh_p2wsh_script=script_to_p2sh_script(witness_script).hex(), p2sh_p2wsh_addr=script_to_p2sh_p2wsh(script_code)) def test_address(node, address, **kwargs): diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 79ad2cf161..516e8be638 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Run regression test suite. @@ -19,6 +19,7 @@ import datetime import os import time import shutil +import signal import subprocess import sys import tempfile @@ -39,7 +40,7 @@ except UnicodeDecodeError: CROSS = "x " CIRCLE = "o " -if os.name != 'nt' or sys.getwindowsversion() >= (10, 0, 14393): +if os.name != 'nt' or sys.getwindowsversion() >= (10, 0, 14393): #type:ignore if os.name == 'nt': import ctypes kernel32 = ctypes.windll.kernel32 # type: ignore @@ -97,7 +98,9 @@ BASE_SCRIPTS = [ 'rpc_fundrawtransaction.py --legacy-wallet', 'rpc_fundrawtransaction.py --descriptors', 'p2p_compactblocks.py', + 'p2p_compactblocks_blocksonly.py', 'feature_segwit.py --legacy-wallet', + 'feature_segwit.py --descriptors', # vv Tests less than 2m vv 'wallet_basic.py --legacy-wallet', 'wallet_basic.py --descriptors', @@ -108,9 +111,9 @@ BASE_SCRIPTS = [ 'p2p_tx_download.py', 'mempool_updatefromblock.py', 'wallet_dump.py --legacy-wallet', - 'wallet_listtransactions.py --legacy-wallet', - 'wallet_listtransactions.py --descriptors', + 'feature_taproot.py --previous_release', 'feature_taproot.py', + 'rpc_signer.py', 'wallet_signer.py --descriptors', # vv Tests less than 60s vv 'p2p_sendheaders.py', @@ -120,10 +123,9 @@ BASE_SCRIPTS = [ 'wallet_listreceivedby.py --legacy-wallet', 'wallet_listreceivedby.py --descriptors', 'wallet_abandonconflict.py --legacy-wallet', + 'p2p_dns_seeds.py', 'wallet_abandonconflict.py --descriptors', 'feature_csv_activation.py', - 'rpc_rawtransaction.py --legacy-wallet', - 'rpc_rawtransaction.py --descriptors', 'wallet_address_types.py --legacy-wallet', 'wallet_address_types.py --descriptors', 'feature_bip68_sequence.py', @@ -136,7 +138,9 @@ BASE_SCRIPTS = [ 'feature_fee_estimation.py', 'interface_zmq.py', 'rpc_invalid_address_message.py', - 'interface_bitcoin_cli.py', + 'interface_bitcoin_cli.py --legacy-wallet', + 'interface_bitcoin_cli.py --descriptors', + 'feature_bind_extra.py', 'mempool_resurrect.py', 'wallet_txn_doublespend.py --mineblock', 'tool_wallet.py --legacy-wallet', @@ -157,6 +161,8 @@ BASE_SCRIPTS = [ 'wallet_createwallet.py --legacy-wallet', 'wallet_createwallet.py --usecli', 'wallet_createwallet.py --descriptors', + 'wallet_listtransactions.py --legacy-wallet', + 'wallet_listtransactions.py --descriptors', 'wallet_watchonly.py --legacy-wallet', 'wallet_watchonly.py --usecli --legacy-wallet', 'wallet_reorgsrestore.py', @@ -167,11 +173,17 @@ BASE_SCRIPTS = [ 'rpc_users.py', 'rpc_whitelist.py', 'feature_proxy.py', + 'feature_syscall_sandbox.py', 'rpc_signrawtransaction.py --legacy-wallet', 'rpc_signrawtransaction.py --descriptors', + 'rpc_rawtransaction.py --legacy-wallet', + 'rpc_rawtransaction.py --descriptors', 'wallet_groups.py --legacy-wallet', + 'wallet_transactiontime_rescan.py --descriptors', + 'wallet_transactiontime_rescan.py --legacy-wallet', 'p2p_addrv2_relay.py', 'wallet_groups.py --descriptors', + 'p2p_compactblocks_hb.py', 'p2p_disconnect_ban.py', 'rpc_decodescript.py', 'rpc_blockchain.py', @@ -181,10 +193,12 @@ BASE_SCRIPTS = [ 'p2p_addr_relay.py', 'p2p_getaddr_caching.py', 'p2p_getdata.py', + 'p2p_addrfetch.py', 'rpc_net.py', 'wallet_keypool.py --legacy-wallet', 'wallet_keypool.py --descriptors', 'wallet_descriptor.py --descriptors', + 'feature_maxtipage.py', 'p2p_nobloomfilter_messages.py', 'p2p_filter.py', 'rpc_setban.py', @@ -197,26 +211,32 @@ BASE_SCRIPTS = [ 'feature_assumevalid.py', 'example_test.py', 'wallet_txn_doublespend.py --legacy-wallet', + 'wallet_multisig_descriptor_psbt.py', 'wallet_txn_doublespend.py --descriptors', 'feature_backwards_compatibility.py --legacy-wallet', 'feature_backwards_compatibility.py --descriptors', 'wallet_txn_clone.py --mineblock', 'feature_notifications.py', 'rpc_getblockfilter.py', + 'rpc_getblockfrompeer.py', 'rpc_invalidateblock.py', 'feature_utxo_set_hash.py', - 'feature_rbf.py', + 'feature_rbf.py --legacy-wallet', + 'feature_rbf.py --descriptors', 'mempool_packages.py', 'mempool_package_onemore.py', 'rpc_createmultisig.py --legacy-wallet', 'rpc_createmultisig.py --descriptors', + 'rpc_packages.py', + 'mempool_package_limits.py', 'feature_versionbits_warning.py', 'rpc_preciousblock.py', 'wallet_importprunedfunds.py --legacy-wallet', 'wallet_importprunedfunds.py --descriptors', 'p2p_leak_tx.py', 'p2p_eviction.py', - 'rpc_signmessage.py', + 'wallet_signmessagewithaddress.py', + 'rpc_signmessagewithprivkey.py', 'rpc_generateblock.py', 'rpc_generate.py', 'wallet_balance.py --legacy-wallet', @@ -238,6 +258,7 @@ BASE_SCRIPTS = [ 'wallet_bumpfee.py --descriptors', 'wallet_implicitsegwit.py --legacy-wallet', 'rpc_named_arguments.py', + 'feature_startupnotify.py', 'wallet_listsinceblock.py --legacy-wallet', 'wallet_listsinceblock.py --descriptors', 'wallet_listdescriptors.py --descriptors', @@ -259,8 +280,10 @@ BASE_SCRIPTS = [ 'wallet_send.py --legacy-wallet', 'wallet_send.py --descriptors', 'wallet_create_tx.py --descriptors', + 'wallet_taproot.py', 'p2p_fingerprint.py', 'feature_uacomment.py', + 'feature_init.py', 'wallet_coinbase_category.py --legacy-wallet', 'wallet_coinbase_category.py --descriptors', 'feature_filelock.py', @@ -271,22 +294,34 @@ BASE_SCRIPTS = [ 'p2p_blockfilters.py', 'p2p_message_capture.py', 'feature_includeconf.py', + 'feature_addrman.py', 'feature_asmap.py', 'mempool_unbroadcast.py', 'mempool_compatibility.py', + 'mempool_accept_wtxid.py', 'rpc_deriveaddresses.py', 'rpc_deriveaddresses.py --usecli', 'p2p_ping.py', 'rpc_scantxoutset.py', + 'feature_txindex_compatibility.py', 'feature_logging.py', + 'feature_anchors.py', + 'feature_coinstatsindex.py --legacy-wallet', + 'feature_coinstatsindex.py --descriptors', + 'wallet_orphanedreward.py', + 'wallet_timelock.py', 'p2p_node_network_limited.py', 'p2p_permissions.py', 'feature_blocksdir.py', 'wallet_startup.py', + 'p2p_i2p_ports.py', 'feature_config_args.py', + 'feature_presegwit_node_upgrade.py', 'feature_settings.py', 'rpc_getdescriptorinfo.py', + 'rpc_mempool_entry_fee_fields_deprecation.py', 'rpc_help.py', + 'feature_dirsymlinks.py', 'feature_help.py', 'feature_shutdown.py', 'p2p_ibd_txrelay.py', @@ -324,7 +359,7 @@ def main(): parser.add_argument('--keepcache', '-k', action='store_true', help='the default behavior is to flush the cache directory on startup. --keepcache retains the cache from the previous testrun.') parser.add_argument('--quiet', '-q', action='store_true', help='only print dots, results summary and failure logs') parser.add_argument('--tmpdirprefix', '-t', default=tempfile.gettempdir(), help="Root directory for datadirs") - parser.add_argument('--failfast', action='store_true', help='stop execution after the first test failure') + parser.add_argument('--failfast', '-F', action='store_true', help='stop execution after the first test failure') parser.add_argument('--filter', help='filter scripts to run by regular expression') args, unknown_args = parser.parse_known_args() @@ -382,8 +417,9 @@ def main(): for test in tests: script = test.split("/")[-1] script = script + ".py" if ".py" not in script else script - if script in ALL_SCRIPTS: - test_list.append(script) + matching_scripts = [s for s in ALL_SCRIPTS if s.startswith(script)] + if matching_scripts: + test_list.extend(matching_scripts) else: print("{}WARNING!{} Test '{}' not found in full test list.".format(BOLD[1], BOLD[0], test)) elif args.extended: @@ -497,33 +533,39 @@ def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, enable_coverage= max_len_name = len(max(test_list, key=len)) test_count = len(test_list) - for i in range(test_count): - test_result, testdir, stdout, stderr = job_queue.get_next() - test_results.append(test_result) - done_str = "{}/{} - {}{}{}".format(i + 1, test_count, BOLD[1], test_result.name, BOLD[0]) - if test_result.status == "Passed": - logging.debug("%s passed, Duration: %s s" % (done_str, test_result.time)) - elif test_result.status == "Skipped": - logging.debug("%s skipped" % (done_str)) - else: - print("%s failed, Duration: %s s\n" % (done_str, test_result.time)) - print(BOLD[1] + 'stdout:\n' + BOLD[0] + stdout + '\n') - print(BOLD[1] + 'stderr:\n' + BOLD[0] + stderr + '\n') - if combined_logs_len and os.path.isdir(testdir): - # Print the final `combinedlogslen` lines of the combined logs - print('{}Combine the logs and print the last {} lines ...{}'.format(BOLD[1], combined_logs_len, BOLD[0])) - print('\n============') - print('{}Combined log for {}:{}'.format(BOLD[1], testdir, BOLD[0])) - print('============\n') - combined_logs_args = [sys.executable, os.path.join(tests_dir, 'combine_logs.py'), testdir] - if BOLD[0]: - combined_logs_args += ['--color'] - combined_logs, _ = subprocess.Popen(combined_logs_args, universal_newlines=True, stdout=subprocess.PIPE).communicate() - print("\n".join(deque(combined_logs.splitlines(), combined_logs_len))) - - if failfast: - logging.debug("Early exiting after test failure") - break + all_passed = True + i = 0 + while i < test_count: + if failfast and not all_passed: + break + for test_result, testdir, stdout, stderr in job_queue.get_next(): + test_results.append(test_result) + i += 1 + done_str = "{}/{} - {}{}{}".format(i, test_count, BOLD[1], test_result.name, BOLD[0]) + if test_result.status == "Passed": + logging.debug("%s passed, Duration: %s s" % (done_str, test_result.time)) + elif test_result.status == "Skipped": + logging.debug("%s skipped" % (done_str)) + else: + all_passed = False + print("%s failed, Duration: %s s\n" % (done_str, test_result.time)) + print(BOLD[1] + 'stdout:\n' + BOLD[0] + stdout + '\n') + print(BOLD[1] + 'stderr:\n' + BOLD[0] + stderr + '\n') + if combined_logs_len and os.path.isdir(testdir): + # Print the final `combinedlogslen` lines of the combined logs + print('{}Combine the logs and print the last {} lines ...{}'.format(BOLD[1], combined_logs_len, BOLD[0])) + print('\n============') + print('{}Combined log for {}:{}'.format(BOLD[1], testdir, BOLD[0])) + print('============\n') + combined_logs_args = [sys.executable, os.path.join(tests_dir, 'combine_logs.py'), testdir] + if BOLD[0]: + combined_logs_args += ['--color'] + combined_logs, _ = subprocess.Popen(combined_logs_args, universal_newlines=True, stdout=subprocess.PIPE).communicate() + print("\n".join(deque(combined_logs.splitlines(), combined_logs_len))) + + if failfast: + logging.debug("Early exiting after test failure") + break print_results(test_results, max_len_name, (int(time.time() - start_time))) @@ -539,11 +581,13 @@ def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, enable_coverage= if not os.listdir(tmpdir): os.rmdir(tmpdir) - all_passed = all(map(lambda test_result: test_result.was_successful, test_results)) and coverage_passed + all_passed = all_passed and coverage_passed - # This will be a no-op unless failfast is True in which case there may be dangling - # processes which need to be killed. - job_queue.kill_and_join() + # Clean up dangling processes if any. This may only happen with --failfast option. + # Killing the process group will also terminate the current process but that is + # not an issue + if len(job_queue.jobs): + os.killpg(os.getpgid(0), signal.SIGKILL) sys.exit(not all_passed) @@ -615,8 +659,9 @@ class TestHandler: dot_count = 0 while True: - # Return first proc that finishes + # Return all procs that have finished, if any. Otherwise sleep until there is one. time.sleep(.5) + ret = [] for job in self.jobs: (name, start_time, proc, testdir, log_out, log_err) = job if proc.poll() is not None: @@ -635,21 +680,13 @@ class TestHandler: clearline = '\r' + (' ' * dot_count) + '\r' print(clearline, end='', flush=True) dot_count = 0 - return TestResult(name, status, int(time.time() - start_time)), testdir, stdout, stderr + ret.append((TestResult(name, status, int(time.time() - start_time)), testdir, stdout, stderr)) + if ret: + return ret if self.use_term_control: print('.', end='', flush=True) dot_count += 1 - def kill_and_join(self): - """Send SIGKILL to all jobs and block until all have ended.""" - procs = [i[2] for i in self.jobs] - - for proc in procs: - proc.kill() - - for proc in procs: - proc.wait() - class TestResult(): def __init__(self, name, status, time): diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index 28103793df..2cb9dc4523 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test bitcoin-wallet.""" @@ -31,8 +31,8 @@ class ToolWalletTest(BitcoinTestFramework): def bitcoin_wallet_process(self, *args): binary = self.config["environment"]["BUILDDIR"] + '/src/bitcoin-wallet' + self.config["environment"]["EXEEXT"] default_args = ['-datadir={}'.format(self.nodes[0].datadir), '-chain=%s' % self.chain] - if self.options.descriptors and 'create' in args: - default_args.append('-descriptors') + if not self.options.descriptors and 'create' in args: + default_args.append('-legacy') return subprocess.Popen([binary] + default_args + list(args), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) @@ -70,8 +70,8 @@ class ToolWalletTest(BitcoinTestFramework): def get_expected_info_output(self, name="", transactions=0, keypool=2, address=0): wallet_name = self.default_wallet_name if name == "" else name - output_types = 3 # p2pkh, p2sh, segwit if self.options.descriptors: + output_types = 4 # p2pkh, p2sh, segwit, bech32m return textwrap.dedent('''\ Wallet info =========== @@ -85,6 +85,7 @@ class ToolWalletTest(BitcoinTestFramework): Address Book: %d ''' % (wallet_name, keypool * output_types, transactions, address)) else: + output_types = 3 # p2pkh, p2sh, segwit. Legacy wallets do not support bech32m. return textwrap.dedent('''\ Wallet info =========== @@ -193,7 +194,7 @@ class ToolWalletTest(BitcoinTestFramework): locked_dir = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets") error = 'Error initializing wallet database environment "{}"!'.format(locked_dir) if self.options.descriptors: - error = "SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?" + error = f"SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of {self.config['environment']['PACKAGE_NAME']}?" self.assert_raises_tool_error( error, '-wallet=' + self.default_wallet_name, @@ -242,7 +243,7 @@ class ToolWalletTest(BitcoinTestFramework): """ self.start_node(0) self.log.info('Generating transaction to mutate wallet') - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) self.stop_node(0) self.log.info('Calling wallet tool info after generating a transaction, testing output') @@ -298,8 +299,8 @@ class ToolWalletTest(BitcoinTestFramework): assert_equal(1000, out['keypoolsize_hd_internal']) assert_equal(True, 'hdseedid' in out) else: - assert_equal(3000, out['keypoolsize']) - assert_equal(3000, out['keypoolsize_hd_internal']) + assert_equal(4000, out['keypoolsize']) + assert_equal(4000, out['keypoolsize_hd_internal']) self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after) assert_equal(timestamp_before, timestamp_after) @@ -344,7 +345,7 @@ class ToolWalletTest(BitcoinTestFramework): non_exist_dump = os.path.join(self.nodes[0].datadir, "wallet.nodump") self.assert_raises_tool_error('Unknown wallet file format "notaformat" provided. Please provide one of "bdb" or "sqlite".', '-wallet=todump', '-format=notaformat', '-dumpfile={}'.format(wallet_dump), 'createfromdump') self.assert_raises_tool_error('Dump file {} does not exist.'.format(non_exist_dump), '-wallet=todump', '-dumpfile={}'.format(non_exist_dump), 'createfromdump') - wallet_path = os.path.join(self.nodes[0].datadir, 'regtest/wallets/todump2') + wallet_path = os.path.join(self.nodes[0].datadir, 'regtest', 'wallets', 'todump2') self.assert_raises_tool_error('Failed to create database path \'{}\'. Database already exists.'.format(wallet_path), '-wallet=todump2', '-dumpfile={}'.format(wallet_dump), 'createfromdump') self.assert_raises_tool_error("The -descriptors option can only be used with the 'create' command.", '-descriptors', '-wallet=todump2', '-dumpfile={}'.format(wallet_dump), 'createfromdump') @@ -389,7 +390,11 @@ class ToolWalletTest(BitcoinTestFramework): bad_sum_wallet_dump = os.path.join(self.nodes[0].datadir, "wallet-bad_sum3.dump") dump_data["checksum"] = "2" * 10 self.write_dump(dump_data, bad_sum_wallet_dump) - self.assert_raises_tool_error('Error: Dumpfile checksum does not match. Computed {}, expected {}{}'.format(checksum, "2" * 10, "0" * 54), '-wallet=badload', '-dumpfile={}'.format(bad_sum_wallet_dump), 'createfromdump') + self.assert_raises_tool_error('Error: Checksum is not the correct size', '-wallet=badload', '-dumpfile={}'.format(bad_sum_wallet_dump), 'createfromdump') + assert not os.path.isdir(os.path.join(self.nodes[0].datadir, "regtest/wallets", "badload")) + dump_data["checksum"] = "3" * 66 + self.write_dump(dump_data, bad_sum_wallet_dump) + self.assert_raises_tool_error('Error: Checksum is not the correct size', '-wallet=badload', '-dumpfile={}'.format(bad_sum_wallet_dump), 'createfromdump') assert not os.path.isdir(os.path.join(self.nodes[0].datadir, "regtest/wallets", "badload")) diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py index 2e0edcfa38..36fcdb36d6 100755 --- a/test/functional/wallet_abandonconflict.py +++ b/test/functional/wallet_abandonconflict.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the abandontransaction RPC. @@ -12,6 +12,7 @@ """ from decimal import Decimal +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -28,22 +29,25 @@ class AbandonConflictTest(BitcoinTestFramework): self.skip_if_no_wallet() def run_test(self): - self.nodes[1].generate(100) - self.sync_blocks() - balance = self.nodes[0].getbalance() - txA = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) - txB = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) - txC = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) + # create two wallets to tests conflicts from both sender's and receiver's sides + alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + self.nodes[0].createwallet(wallet_name="bob") + bob = self.nodes[0].get_wallet_rpc("bob") + + self.generate(self.nodes[1], COINBASE_MATURITY) + balance = alice.getbalance() + txA = alice.sendtoaddress(alice.getnewaddress(), Decimal("10")) + txB = alice.sendtoaddress(alice.getnewaddress(), Decimal("10")) + txC = alice.sendtoaddress(alice.getnewaddress(), Decimal("10")) self.sync_mempools() - self.nodes[1].generate(1) + self.generate(self.nodes[1], 1) # Can not abandon non-wallet transaction - assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', lambda: self.nodes[0].abandontransaction(txid='ff' * 32)) + assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', lambda: alice.abandontransaction(txid='ff' * 32)) # Can not abandon confirmed transaction - assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: self.nodes[0].abandontransaction(txid=txA)) + assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: alice.abandontransaction(txid=txA)) - self.sync_blocks() - newbalance = self.nodes[0].getbalance() + newbalance = alice.getbalance() assert balance - newbalance < Decimal("0.001") #no more than fees lost balance = newbalance @@ -51,9 +55,9 @@ class AbandonConflictTest(BitcoinTestFramework): self.disconnect_nodes(0, 1) # Identify the 10btc outputs - nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txA)["details"] if tx_out["amount"] == Decimal("10")) - nB = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txB)["details"] if tx_out["amount"] == Decimal("10")) - nC = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txC)["details"] if tx_out["amount"] == Decimal("10")) + nA = next(tx_out["vout"] for tx_out in alice.gettransaction(txA)["details"] if tx_out["amount"] == Decimal("10")) + nB = next(tx_out["vout"] for tx_out in alice.gettransaction(txB)["details"] if tx_out["amount"] == Decimal("10")) + nC = next(tx_out["vout"] for tx_out in alice.gettransaction(txC)["details"] if tx_out["amount"] == Decimal("10")) inputs = [] # spend 10btc outputs from txA and txB @@ -61,39 +65,40 @@ class AbandonConflictTest(BitcoinTestFramework): inputs.append({"txid": txB, "vout": nB}) outputs = {} - outputs[self.nodes[0].getnewaddress()] = Decimal("14.99998") - outputs[self.nodes[1].getnewaddress()] = Decimal("5") - signed = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs)) + outputs[alice.getnewaddress()] = Decimal("14.99998") + outputs[bob.getnewaddress()] = Decimal("5") + signed = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs)) txAB1 = self.nodes[0].sendrawtransaction(signed["hex"]) # Identify the 14.99998btc output - nAB = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txAB1)["details"] if tx_out["amount"] == Decimal("14.99998")) + nAB = next(tx_out["vout"] for tx_out in alice.gettransaction(txAB1)["details"] if tx_out["amount"] == Decimal("14.99998")) #Create a child tx spending AB1 and C inputs = [] inputs.append({"txid": txAB1, "vout": nAB}) inputs.append({"txid": txC, "vout": nC}) outputs = {} - outputs[self.nodes[0].getnewaddress()] = Decimal("24.9996") - signed2 = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs)) + outputs[alice.getnewaddress()] = Decimal("24.9996") + signed2 = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs)) txABC2 = self.nodes[0].sendrawtransaction(signed2["hex"]) # Create a child tx spending ABC2 signed3_change = Decimal("24.999") inputs = [{"txid": txABC2, "vout": 0}] - outputs = {self.nodes[0].getnewaddress(): signed3_change} - signed3 = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs)) + outputs = {alice.getnewaddress(): signed3_change} + signed3 = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs)) # note tx is never directly referenced, only abandoned as a child of the above self.nodes[0].sendrawtransaction(signed3["hex"]) # In mempool txs from self should increase balance from change - newbalance = self.nodes[0].getbalance() + newbalance = alice.getbalance() assert_equal(newbalance, balance - Decimal("30") + signed3_change) balance = newbalance # Restart the node with a higher min relay fee so the parent tx is no longer in mempool # TODO: redo with eviction self.restart_node(0, extra_args=["-minrelaytxfee=0.0001"]) + alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name) assert self.nodes[0].getmempoolinfo()['loaded'] # Verify txs no longer in either node's mempool @@ -102,68 +107,120 @@ class AbandonConflictTest(BitcoinTestFramework): # Not in mempool txs from self should only reduce balance # inputs are still spent, but change not received - newbalance = self.nodes[0].getbalance() + newbalance = alice.getbalance() assert_equal(newbalance, balance - signed3_change) # Unconfirmed received funds that are not in mempool, also shouldn't show # up in unconfirmed balance - balances = self.nodes[0].getbalances()['mine'] + balances = alice.getbalances()['mine'] assert_equal(balances['untrusted_pending'] + balances['trusted'], newbalance) # Also shouldn't show up in listunspent - assert not txABC2 in [utxo["txid"] for utxo in self.nodes[0].listunspent(0)] + assert not txABC2 in [utxo["txid"] for utxo in alice.listunspent(0)] balance = newbalance # Abandon original transaction and verify inputs are available again # including that the child tx was also abandoned - self.nodes[0].abandontransaction(txAB1) - newbalance = self.nodes[0].getbalance() + alice.abandontransaction(txAB1) + newbalance = alice.getbalance() assert_equal(newbalance, balance + Decimal("30")) balance = newbalance + self.log.info("Check abandoned transactions in listsinceblock") + listsinceblock = alice.listsinceblock() + txAB1_listsinceblock = [d for d in listsinceblock['transactions'] if d['txid'] == txAB1 and d['category'] == 'send'] + for tx in txAB1_listsinceblock: + assert_equal(tx['abandoned'], True) + assert_equal(tx['confirmations'], 0) + assert_equal(tx['trusted'], False) + # Verify that even with a low min relay fee, the tx is not reaccepted from wallet on startup once abandoned self.restart_node(0, extra_args=["-minrelaytxfee=0.00001"]) + alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name) assert self.nodes[0].getmempoolinfo()['loaded'] assert_equal(len(self.nodes[0].getrawmempool()), 0) - assert_equal(self.nodes[0].getbalance(), balance) + assert_equal(alice.getbalance(), balance) # But if it is received again then it is unabandoned # And since now in mempool, the change is available # But its child tx remains abandoned self.nodes[0].sendrawtransaction(signed["hex"]) - newbalance = self.nodes[0].getbalance() + newbalance = alice.getbalance() assert_equal(newbalance, balance - Decimal("20") + Decimal("14.99998")) balance = newbalance # Send child tx again so it is unabandoned self.nodes[0].sendrawtransaction(signed2["hex"]) - newbalance = self.nodes[0].getbalance() + newbalance = alice.getbalance() assert_equal(newbalance, balance - Decimal("10") - Decimal("14.99998") + Decimal("24.9996")) balance = newbalance # Remove using high relay fee again self.restart_node(0, extra_args=["-minrelaytxfee=0.0001"]) + alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name) assert self.nodes[0].getmempoolinfo()['loaded'] assert_equal(len(self.nodes[0].getrawmempool()), 0) - newbalance = self.nodes[0].getbalance() + newbalance = alice.getbalance() assert_equal(newbalance, balance - Decimal("24.9996")) balance = newbalance + self.log.info("Test transactions conflicted by a double spend") + self.nodes[0].loadwallet("bob") + bob = self.nodes[0].get_wallet_rpc("bob") + # Create a double spend of AB1 by spending again from only A's 10 output # Mine double spend from node 1 inputs = [] inputs.append({"txid": txA, "vout": nA}) outputs = {} - outputs[self.nodes[1].getnewaddress()] = Decimal("9.9999") - tx = self.nodes[0].createrawtransaction(inputs, outputs) - signed = self.nodes[0].signrawtransactionwithwallet(tx) - self.nodes[1].sendrawtransaction(signed["hex"]) - self.nodes[1].generate(1) - + outputs[self.nodes[1].getnewaddress()] = Decimal("3.9999") + outputs[bob.getnewaddress()] = Decimal("5.9999") + tx = alice.createrawtransaction(inputs, outputs) + signed = alice.signrawtransactionwithwallet(tx) + double_spend_txid = self.nodes[1].sendrawtransaction(signed["hex"]) self.connect_nodes(0, 1) - self.sync_blocks() + self.generate(self.nodes[1], 1) + + tx_list = alice.listtransactions() + + conflicted = [tx for tx in tx_list if tx["confirmations"] < 0] + assert_equal(4, len(conflicted)) + + wallet_conflicts = [tx for tx in conflicted if tx["walletconflicts"]] + assert_equal(2, len(wallet_conflicts)) + + double_spends = [tx for tx in tx_list if tx["walletconflicts"] and tx["confirmations"] > 0] + assert_equal(2, len(double_spends)) # one for each output + double_spend = double_spends[0] + + # Test the properties of the conflicted transactions, i.e. with confirmations < 0. + for tx in conflicted: + assert_equal(tx["abandoned"], False) + assert_equal(tx["confirmations"], -1) + assert_equal(tx["trusted"], False) + + # Test the properties of the double-spend transaction, i.e. having wallet conflicts and confirmations > 0. + assert_equal(double_spend["abandoned"], False) + assert_equal(double_spend["confirmations"], 1) + assert "trusted" not in double_spend.keys() # "trusted" only returned if tx has 0 or negative confirmations. + + # Test the walletconflicts field of each. + for tx in wallet_conflicts: + assert_equal(double_spend["walletconflicts"], [tx["txid"]]) + assert_equal(tx["walletconflicts"], [double_spend["txid"]]) + + # Test walletconflicts on the receiver's side + txinfo = bob.gettransaction(txAB1) + assert_equal(txinfo['confirmations'], -1) + assert_equal(txinfo['walletconflicts'], [double_spend['txid']]) + + double_spends = [tx for tx in bob.listtransactions() if tx["walletconflicts"] and tx["confirmations"] > 0] + assert_equal(1, len(double_spends)) + double_spend = double_spends[0] + assert_equal(double_spend_txid, double_spend['txid']) + assert_equal(double_spend["walletconflicts"], [txAB1]) # Verify that B and C's 10 BTC outputs are available for spending again because AB1 is now conflicted - newbalance = self.nodes[0].getbalance() + newbalance = alice.getbalance() assert_equal(newbalance, balance + Decimal("20")) balance = newbalance @@ -171,11 +228,11 @@ class AbandonConflictTest(BitcoinTestFramework): # Invalidate the block with the double spend and B's 10 BTC output should no longer be available # Don't think C's should either self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) - newbalance = self.nodes[0].getbalance() + newbalance = alice.getbalance() #assert_equal(newbalance, balance - Decimal("10")) self.log.info("If balance has not declined after invalidateblock then out of mempool wallet tx which is no longer") self.log.info("conflicted has not resumed causing its inputs to be seen as spent. See Issue #7315") - self.log.info(str(balance) + " -> " + str(newbalance) + " ?") + assert_equal(balance, newbalance) if __name__ == '__main__': diff --git a/test/functional/wallet_address_types.py b/test/functional/wallet_address_types.py index 2db5eae33b..f7c80f805c 100755 --- a/test/functional/wallet_address_types.py +++ b/test/functional/wallet_address_types.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test that the wallet can send and receive using all combinations of address types. @@ -53,6 +53,7 @@ Test that the nodes generate the correct change address type: from decimal import Decimal import itertools +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.descriptors import ( descsum_create, @@ -120,6 +121,12 @@ class AddressTypeTest(BitcoinTestFramework): assert_equal(info['witness_version'], 0) assert_equal(len(info['witness_program']), 40) assert 'pubkey' in info + elif not multisig and typ == "bech32m": + # P2TR single sig + assert info["isscript"] + assert info["iswitness"] + assert_equal(info["witness_version"], 1) + assert_equal(len(info["witness_program"]), 64) elif typ == 'legacy': # P2SH-multisig assert info['isscript'] @@ -203,14 +210,13 @@ class AddressTypeTest(BitcoinTestFramework): def test_change_output_type(self, node_sender, destinations, expected_type): txid = self.nodes[node_sender].sendmany(dummy="", amounts=dict.fromkeys(destinations, 0.001)) - raw_tx = self.nodes[node_sender].getrawtransaction(txid) - tx = self.nodes[node_sender].decoderawtransaction(raw_tx) + tx = self.nodes[node_sender].gettransaction(txid=txid, verbose=True)['decoded'] # Make sure the transaction has change: assert_equal(len(tx["vout"]), len(destinations) + 1) # Make sure the destinations are included, and remove them: - output_addresses = [vout['scriptPubKey']['addresses'][0] for vout in tx["vout"]] + output_addresses = [vout['scriptPubKey']['address'] for vout in tx["vout"]] change_addresses = [d for d in output_addresses if d not in destinations] assert_equal(len(change_addresses), 1) @@ -220,8 +226,7 @@ class AddressTypeTest(BitcoinTestFramework): def run_test(self): # Mine 101 blocks on node5 to bring nodes out of IBD and make sure that # no coinbases are maturing for the nodes-under-test during the test - self.nodes[5].generate(101) - self.sync_blocks() + self.generate(self.nodes[5], COINBASE_MATURITY + 1) uncompressed_1 = "0496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858ee" uncompressed_2 = "047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77" @@ -258,7 +263,7 @@ class AddressTypeTest(BitcoinTestFramework): self.log.info("Sending from node {} ({}) with{} multisig using {}".format(from_node, self.extra_args[from_node], "" if multisig else "out", "default" if address_type is None else address_type)) old_balances = self.get_balances() self.log.debug("Old balances are {}".format(old_balances)) - to_send = (old_balances[from_node] / 101).quantize(Decimal("0.00000001")) + to_send = (old_balances[from_node] / (COINBASE_MATURITY + 1)).quantize(Decimal("0.00000001")) sends = {} addresses = {} @@ -305,8 +310,7 @@ class AddressTypeTest(BitcoinTestFramework): assert_equal(unconf_balances[to_node], to_send * 10 * (2 + n)) # node5 collects fee and block subsidy to keep accounting simple - self.nodes[5].generate(1) - self.sync_blocks() + self.generate(self.nodes[5], 1) # Verify that the receiving wallet contains a UTXO with the expected address, and expected descriptor for n, to_node in enumerate(range(from_node, from_node + 4)): @@ -335,26 +339,37 @@ class AddressTypeTest(BitcoinTestFramework): # Fund node 4: self.nodes[5].sendtoaddress(self.nodes[4].getnewaddress(), Decimal("1")) - self.nodes[5].generate(1) - self.sync_blocks() + self.generate(self.nodes[5], 1) assert_equal(self.nodes[4].getbalance(), 1) self.log.info("Nodes with addresstype=legacy never use a P2WPKH change output (unless changetype is set otherwise):") self.test_change_output_type(0, [to_address_bech32_1], 'legacy') - self.log.info("Nodes with addresstype=p2sh-segwit only use a P2WPKH change output if any destination address is bech32:") - self.test_change_output_type(1, [to_address_p2sh], 'p2sh-segwit') - self.test_change_output_type(1, [to_address_bech32_1], 'bech32') - self.test_change_output_type(1, [to_address_p2sh, to_address_bech32_1], 'bech32') - self.test_change_output_type(1, [to_address_bech32_1, to_address_bech32_2], 'bech32') + if self.options.descriptors: + self.log.info("Nodes with addresstype=p2sh-segwit match the change output") + self.test_change_output_type(1, [to_address_p2sh], 'p2sh-segwit') + self.test_change_output_type(1, [to_address_bech32_1], 'bech32') + self.test_change_output_type(1, [to_address_p2sh, to_address_bech32_1], 'bech32') + self.test_change_output_type(1, [to_address_bech32_1, to_address_bech32_2], 'bech32') + else: + self.log.info("Nodes with addresstype=p2sh-segwit match the change output") + self.test_change_output_type(1, [to_address_p2sh], 'p2sh-segwit') + self.test_change_output_type(1, [to_address_bech32_1], 'bech32') + self.test_change_output_type(1, [to_address_p2sh, to_address_bech32_1], 'bech32') + self.test_change_output_type(1, [to_address_bech32_1, to_address_bech32_2], 'bech32') self.log.info("Nodes with change_type=bech32 always use a P2WPKH change output:") self.test_change_output_type(2, [to_address_bech32_1], 'bech32') self.test_change_output_type(2, [to_address_p2sh], 'bech32') - self.log.info("Nodes with addresstype=bech32 always use a P2WPKH change output (unless changetype is set otherwise):") - self.test_change_output_type(3, [to_address_bech32_1], 'bech32') - self.test_change_output_type(3, [to_address_p2sh], 'bech32') + if self.options.descriptors: + self.log.info("Nodes with addresstype=bech32 match the change output (unless changetype is set otherwise):") + self.test_change_output_type(3, [to_address_bech32_1], 'bech32') + self.test_change_output_type(3, [to_address_p2sh], 'p2sh-segwit') + else: + self.log.info("Nodes with addresstype=bech32 match the change output (unless changetype is set otherwise):") + self.test_change_output_type(3, [to_address_bech32_1], 'bech32') + self.test_change_output_type(3, [to_address_p2sh], 'p2sh-segwit') self.log.info('getrawchangeaddress defaults to addresstype if -changetype is not set and argument is absent') self.test_address(3, self.nodes[3].getrawchangeaddress(), multisig=False, typ='bech32') @@ -372,5 +387,14 @@ class AddressTypeTest(BitcoinTestFramework): self.test_address(4, self.nodes[4].getrawchangeaddress(), multisig=False, typ='p2sh-segwit') self.test_address(4, self.nodes[4].getrawchangeaddress('bech32'), multisig=False, typ='bech32') + if self.options.descriptors: + self.log.info("Descriptor wallets have bech32m addresses") + self.test_address(4, self.nodes[4].getnewaddress("", "bech32m"), multisig=False, typ="bech32m") + self.test_address(4, self.nodes[4].getrawchangeaddress("bech32m"), multisig=False, typ="bech32m") + else: + self.log.info("Legacy wallets cannot make bech32m addresses") + assert_raises_rpc_error(-8, "Legacy wallets cannot provide bech32m addresses", self.nodes[0].getnewaddress, "", "bech32m") + assert_raises_rpc_error(-8, "Legacy wallets cannot provide bech32m addresses", self.nodes[0].getrawchangeaddress, "bech32m") + if __name__ == '__main__': AddressTypeTest().main() diff --git a/test/functional/wallet_avoidreuse.py b/test/functional/wallet_avoidreuse.py index bc4fa90e83..dc823c2c60 100755 --- a/test/functional/wallet_avoidreuse.py +++ b/test/functional/wallet_avoidreuse.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the avoid_reuse and setwalletflag features.""" @@ -42,25 +42,25 @@ def count_unspent(node): r["reused"]["supported"] = supports_reused return r -def assert_unspent(node, total_count=None, total_sum=None, reused_supported=None, reused_count=None, reused_sum=None): +def assert_unspent(node, total_count=None, total_sum=None, reused_supported=None, reused_count=None, reused_sum=None, margin=0.001): '''Make assertions about a node's unspent output statistics''' stats = count_unspent(node) if total_count is not None: assert_equal(stats["total"]["count"], total_count) if total_sum is not None: - assert_approx(stats["total"]["sum"], total_sum, 0.001) + assert_approx(stats["total"]["sum"], total_sum, margin) if reused_supported is not None: assert_equal(stats["reused"]["supported"], reused_supported) if reused_count is not None: assert_equal(stats["reused"]["count"], reused_count) if reused_sum is not None: - assert_approx(stats["reused"]["sum"], reused_sum, 0.001) + assert_approx(stats["reused"]["sum"], reused_sum, margin) -def assert_balances(node, mine): +def assert_balances(node, mine, margin=0.001): '''Make assertions about a node's getbalances output''' got = node.getbalances()["mine"] for k,v in mine.items(): - assert_approx(got[k], v, 0.001) + assert_approx(got[k], v, margin) class AvoidReuseTest(BitcoinTestFramework): @@ -79,8 +79,7 @@ class AvoidReuseTest(BitcoinTestFramework): self.test_persistence() self.test_immutable() - self.nodes[0].generate(110) - self.sync_all() + self.generate(self.nodes[0], 110) self.test_change_remains_change(self.nodes[1]) reset_balance(self.nodes[1], self.nodes[0].getnewaddress()) self.test_sending_from_reused_address_without_avoid_reuse() @@ -174,8 +173,7 @@ class AvoidReuseTest(BitcoinTestFramework): retaddr = self.nodes[0].getnewaddress() self.nodes[0].sendtoaddress(fundaddr, 10) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # listunspent should show 1 single, unused 10 btc output assert_unspent(self.nodes[1], total_count=1, total_sum=10, reused_supported=True, reused_count=0) @@ -185,8 +183,7 @@ class AvoidReuseTest(BitcoinTestFramework): assert("used" not in self.nodes[0].getbalances()["mine"]) self.nodes[1].sendtoaddress(retaddr, 5) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # listunspent should show 1 single, unused 5 btc output assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_supported=True, reused_count=0) @@ -194,8 +191,7 @@ class AvoidReuseTest(BitcoinTestFramework): assert_balances(self.nodes[1], mine={"used": 0, "trusted": 5}) self.nodes[0].sendtoaddress(fundaddr, 10) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # listunspent should show 2 total outputs (5, 10 btc), one unused (5), one reused (10) assert_unspent(self.nodes[1], total_count=2, total_sum=15, reused_count=1, reused_sum=10) @@ -228,8 +224,7 @@ class AvoidReuseTest(BitcoinTestFramework): retaddr = self.nodes[0].getnewaddress() self.nodes[0].sendtoaddress(fundaddr, 10) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # listunspent should show 1 single, unused 10 btc output assert_unspent(self.nodes[1], total_count=1, total_sum=10, reused_supported=True, reused_count=0) @@ -237,8 +232,7 @@ class AvoidReuseTest(BitcoinTestFramework): assert_balances(self.nodes[1], mine={"used": 0, "trusted": 10}) self.nodes[1].sendtoaddress(retaddr, 5) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # listunspent should show 1 single, unused 5 btc output assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_supported=True, reused_count=0) @@ -253,14 +247,13 @@ class AvoidReuseTest(BitcoinTestFramework): if second_addr_type == "p2sh-segwit": new_fundaddr = fund_decoded["segwit"]["p2sh-segwit"] elif second_addr_type == "bech32": - new_fundaddr = fund_decoded["segwit"]["addresses"][0] + new_fundaddr = fund_decoded["segwit"]["address"] else: new_fundaddr = fundaddr assert_equal(second_addr_type, "legacy") self.nodes[0].sendtoaddress(new_fundaddr, 10) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # listunspent should show 2 total outputs (5, 10 btc), one unused (5), one reused (10) assert_unspent(self.nodes[1], total_count=2, total_sum=15, reused_count=1, reused_sum=10) @@ -299,11 +292,10 @@ class AvoidReuseTest(BitcoinTestFramework): ret_addr = self.nodes[0].getnewaddress() # send multiple transactions, reusing one address - for _ in range(11): + for _ in range(101): self.nodes[0].sendtoaddress(new_addr, 1) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # send transaction that should not use all the available outputs # per the current coin selection algorithm @@ -311,14 +303,14 @@ class AvoidReuseTest(BitcoinTestFramework): # getbalances and listunspent should show the remaining outputs # in the reused address as used/reused - assert_unspent(self.nodes[1], total_count=2, total_sum=6, reused_count=1, reused_sum=1) - assert_balances(self.nodes[1], mine={"used": 1, "trusted": 5}) + assert_unspent(self.nodes[1], total_count=2, total_sum=96, reused_count=1, reused_sum=1, margin=0.01) + assert_balances(self.nodes[1], mine={"used": 1, "trusted": 95}, margin=0.01) def test_full_destination_group_is_preferred(self): ''' - Test the case where [1] only has 11 outputs of 1 BTC in the same reused + Test the case where [1] only has 101 outputs of 1 BTC in the same reused address and tries to send a small payment of 0.5 BTC. The wallet - should use 10 outputs from the reused address as inputs and not a + should use 100 outputs from the reused address as inputs and not a single 1 BTC input, in order to join several outputs from the reused address. ''' @@ -330,26 +322,25 @@ class AvoidReuseTest(BitcoinTestFramework): new_addr = self.nodes[1].getnewaddress() ret_addr = self.nodes[0].getnewaddress() - # Send 11 outputs of 1 BTC to the same, reused address in the wallet - for _ in range(11): + # Send 101 outputs of 1 BTC to the same, reused address in the wallet + for _ in range(101): self.nodes[0].sendtoaddress(new_addr, 1) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # Sending a transaction that is smaller than each one of the # available outputs txid = self.nodes[1].sendtoaddress(address=ret_addr, amount=0.5) inputs = self.nodes[1].getrawtransaction(txid, 1)["vin"] - # The transaction should use 10 inputs exactly - assert_equal(len(inputs), 10) + # The transaction should use 100 inputs exactly + assert_equal(len(inputs), 100) def test_all_destination_groups_are_used(self): ''' - Test the case where [1] only has 22 outputs of 1 BTC in the same reused - address and tries to send a payment of 20.5 BTC. The wallet - should use all 22 outputs from the reused address as inputs. + Test the case where [1] only has 202 outputs of 1 BTC in the same reused + address and tries to send a payment of 200.5 BTC. The wallet + should use all 202 outputs from the reused address as inputs. ''' self.log.info("Test that all destination groups are used") @@ -359,20 +350,19 @@ class AvoidReuseTest(BitcoinTestFramework): new_addr = self.nodes[1].getnewaddress() ret_addr = self.nodes[0].getnewaddress() - # Send 22 outputs of 1 BTC to the same, reused address in the wallet - for _ in range(22): + # Send 202 outputs of 1 BTC to the same, reused address in the wallet + for _ in range(202): self.nodes[0].sendtoaddress(new_addr, 1) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # Sending a transaction that needs to use the full groups - # of 10 inputs but also the incomplete group of 2 inputs. - txid = self.nodes[1].sendtoaddress(address=ret_addr, amount=20.5) + # of 100 inputs but also the incomplete group of 2 inputs. + txid = self.nodes[1].sendtoaddress(address=ret_addr, amount=200.5) inputs = self.nodes[1].getrawtransaction(txid, 1)["vin"] - # The transaction should use 22 inputs exactly - assert_equal(len(inputs), 22) + # The transaction should use 202 inputs exactly + assert_equal(len(inputs), 202) if __name__ == '__main__': diff --git a/test/functional/wallet_backup.py b/test/functional/wallet_backup.py index f34c1345e0..292fe3a310 100755 --- a/test/functional/wallet_backup.py +++ b/test/functional/wallet_backup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet backup features. @@ -35,6 +35,7 @@ import os from random import randint import shutil +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -87,8 +88,7 @@ class WalletBackupTest(BitcoinTestFramework): # Have the miner (node3) mine a block. # Must sync mempools before mining. self.sync_mempools() - self.nodes[3].generate(1) - self.sync_blocks() + self.generate(self.nodes[3], 1) # As above, this mirrors the original bash test. def start_three(self, args=()): @@ -110,21 +110,44 @@ class WalletBackupTest(BitcoinTestFramework): os.remove(os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) os.remove(os.path.join(self.nodes[2].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) + def restore_invalid_wallet(self): + node = self.nodes[3] + invalid_wallet_file = os.path.join(self.nodes[0].datadir, 'invalid_wallet_file.bak') + open(invalid_wallet_file, 'a', encoding="utf8").write('invald wallet') + wallet_name = "res0" + not_created_wallet_file = os.path.join(node.datadir, self.chain, 'wallets', wallet_name) + error_message = "Wallet file verification failed. Failed to load database path '{}'. Data is not in recognized format.".format(not_created_wallet_file) + assert_raises_rpc_error(-18, error_message, node.restorewallet, wallet_name, invalid_wallet_file) + assert not os.path.exists(not_created_wallet_file) + + def restore_nonexistent_wallet(self): + node = self.nodes[3] + nonexistent_wallet_file = os.path.join(self.nodes[0].datadir, 'nonexistent_wallet.bak') + wallet_name = "res0" + assert_raises_rpc_error(-8, "Backup file does not exist", node.restorewallet, wallet_name, nonexistent_wallet_file) + not_created_wallet_file = os.path.join(node.datadir, self.chain, 'wallets', wallet_name) + assert not os.path.exists(not_created_wallet_file) + + def restore_wallet_existent_name(self): + node = self.nodes[3] + backup_file = os.path.join(self.nodes[0].datadir, 'wallet.bak') + wallet_name = "res0" + wallet_file = os.path.join(node.datadir, self.chain, 'wallets', wallet_name) + error_message = "Failed to create database path '{}'. Database already exists.".format(wallet_file) + assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file) + assert os.path.exists(wallet_file) + def init_three(self): - self.init_wallet(0) - self.init_wallet(1) - self.init_wallet(2) + self.init_wallet(node=0) + self.init_wallet(node=1) + self.init_wallet(node=2) def run_test(self): self.log.info("Generating initial blockchain") - self.nodes[0].generate(1) - self.sync_blocks() - self.nodes[1].generate(1) - self.sync_blocks() - self.nodes[2].generate(1) - self.sync_blocks() - self.nodes[3].generate(100) - self.sync_blocks() + self.generate(self.nodes[0], 1) + self.generate(self.nodes[1], 1) + self.generate(self.nodes[2], 1) + self.generate(self.nodes[3], COINBASE_MATURITY) assert_equal(self.nodes[0].getbalance(), 50) assert_equal(self.nodes[1].getbalance(), 50) @@ -152,8 +175,7 @@ class WalletBackupTest(BitcoinTestFramework): self.do_one_round() # Generate 101 more blocks, so any fees paid mature - self.nodes[3].generate(101) - self.sync_all() + self.generate(self.nodes[3], COINBASE_MATURITY + 1) balance0 = self.nodes[0].getbalance() balance1 = self.nodes[1].getbalance() @@ -168,26 +190,32 @@ class WalletBackupTest(BitcoinTestFramework): ## # Test restoring spender wallets from backups ## - self.log.info("Restoring using wallet.dat") - self.stop_three() - self.erase_three() - - # Start node2 with no chain - shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'blocks')) - shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'chainstate')) - - # Restore wallets from backup - shutil.copyfile(os.path.join(self.nodes[0].datadir, 'wallet.bak'), os.path.join(self.nodes[0].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) - shutil.copyfile(os.path.join(self.nodes[1].datadir, 'wallet.bak'), os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) - shutil.copyfile(os.path.join(self.nodes[2].datadir, 'wallet.bak'), os.path.join(self.nodes[2].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) - - self.log.info("Re-starting nodes") - self.start_three() - self.sync_blocks() - - assert_equal(self.nodes[0].getbalance(), balance0) - assert_equal(self.nodes[1].getbalance(), balance1) - assert_equal(self.nodes[2].getbalance(), balance2) + self.log.info("Restoring wallets on node 3 using backup files") + + self.restore_invalid_wallet() + self.restore_nonexistent_wallet() + + backup_file_0 = os.path.join(self.nodes[0].datadir, 'wallet.bak') + backup_file_1 = os.path.join(self.nodes[1].datadir, 'wallet.bak') + backup_file_2 = os.path.join(self.nodes[2].datadir, 'wallet.bak') + + self.nodes[3].restorewallet("res0", backup_file_0) + self.nodes[3].restorewallet("res1", backup_file_1) + self.nodes[3].restorewallet("res2", backup_file_2) + + assert os.path.exists(os.path.join(self.nodes[3].datadir, self.chain, 'wallets', "res0")) + assert os.path.exists(os.path.join(self.nodes[3].datadir, self.chain, 'wallets', "res1")) + assert os.path.exists(os.path.join(self.nodes[3].datadir, self.chain, 'wallets', "res2")) + + res0_rpc = self.nodes[3].get_wallet_rpc("res0") + res1_rpc = self.nodes[3].get_wallet_rpc("res1") + res2_rpc = self.nodes[3].get_wallet_rpc("res2") + + assert_equal(res0_rpc.getbalance(), balance0) + assert_equal(res1_rpc.getbalance(), balance1) + assert_equal(res2_rpc.getbalance(), balance2) + + self.restore_wallet_existent_name() if not self.options.descriptors: self.log.info("Restoring using dumped wallet") diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index 433b40faee..0cfbefb719 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet balance RPC methods.""" @@ -7,6 +7,7 @@ from decimal import Decimal import struct from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE as ADDRESS_WATCHONLY +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -69,11 +70,9 @@ class WalletTest(BitcoinTestFramework): assert 'watchonly' not in self.nodes[1].getbalances() self.log.info("Mining blocks ...") - self.nodes[0].generate(1) - self.sync_all() - self.nodes[1].generate(1) - self.nodes[1].generatetoaddress(101, ADDRESS_WATCHONLY) - self.sync_all() + self.generate(self.nodes[0], 1) + self.generate(self.nodes[1], 1) + self.generatetoaddress(self.nodes[1], COINBASE_MATURITY + 1, ADDRESS_WATCHONLY) if not self.options.descriptors: # Tests legacy watchonly behavior which is not present (and does not need to be tested) in descriptor wallets @@ -195,8 +194,7 @@ class WalletTest(BitcoinTestFramework): self.log.info("Test getbalance and getbalances.mine.untrusted_pending with conflicted unconfirmed inputs") test_balances(fee_node_1=Decimal('0.02')) - self.nodes[1].generatetoaddress(1, ADDRESS_WATCHONLY) - self.sync_all() + self.generatetoaddress(self.nodes[1], 1, ADDRESS_WATCHONLY) # balances are correct after the transactions are confirmed balance_node0 = Decimal('69.99') # node 1's send plus change from node 0's send @@ -209,8 +207,7 @@ class WalletTest(BitcoinTestFramework): # Send total balance away from node 1 txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), Decimal('29.97'), [Decimal('0.01')]) self.nodes[1].sendrawtransaction(txs[0]['hex']) - self.nodes[1].generatetoaddress(2, ADDRESS_WATCHONLY) - self.sync_all() + self.generatetoaddress(self.nodes[1], 2, ADDRESS_WATCHONLY) # getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago # TODO: fix getbalance tracking of coin spentness depth @@ -256,15 +253,14 @@ class WalletTest(BitcoinTestFramework): self.nodes[1].sendrawtransaction(hexstring=tx_replace, maxfeerate=0) # Now confirm tx_replace - block_reorg = self.nodes[1].generatetoaddress(1, ADDRESS_WATCHONLY)[0] - self.sync_all() + block_reorg = self.generatetoaddress(self.nodes[1], 1, ADDRESS_WATCHONLY)[0] assert_equal(self.nodes[0].getbalance(minconf=0), total_amount) self.log.info('Put txs back into mempool of node 1 (not node 0)') self.nodes[0].invalidateblock(block_reorg) self.nodes[1].invalidateblock(block_reorg) assert_equal(self.nodes[0].getbalance(minconf=0), 0) # wallet txs not in the mempool are untrusted - self.nodes[0].generatetoaddress(1, ADDRESS_WATCHONLY) + self.generatetoaddress(self.nodes[0], 1, ADDRESS_WATCHONLY, sync_fun=self.no_op) assert_equal(self.nodes[0].getbalance(minconf=0), 0) # wallet txs not in the mempool are untrusted # Now confirm tx_orig @@ -272,8 +268,7 @@ class WalletTest(BitcoinTestFramework): self.connect_nodes(0, 1) self.sync_blocks() self.nodes[1].sendrawtransaction(tx_orig) - self.nodes[1].generatetoaddress(1, ADDRESS_WATCHONLY) - self.sync_all() + self.generatetoaddress(self.nodes[1], 1, ADDRESS_WATCHONLY) assert_equal(self.nodes[0].getbalance(minconf=0), total_amount + 1) # The reorg recovered our fee of 1 coin diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 4a589f0393..69f9df57d8 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -1,20 +1,23 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet.""" from decimal import Decimal from itertools import product +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_array_result, assert_equal, assert_fee_amount, assert_raises_rpc_error, + find_vout_for_address, ) from test_framework.wallet_util import test_address +NOT_A_NUMBER_OR_STRING = "Amount is not a number or string" OUT_OF_RANGE = "Amount out of range" @@ -57,15 +60,14 @@ class WalletTest(BitcoinTestFramework): self.log.info("Mining blocks...") - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) walletinfo = self.nodes[0].getwalletinfo() assert_equal(walletinfo['immature_balance'], 50) assert_equal(walletinfo['balance'], 0) self.sync_all(self.nodes[0:3]) - self.nodes[1].generate(101) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[1], COINBASE_MATURITY + 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) assert_equal(self.nodes[0].getbalance(), 50) assert_equal(self.nodes[1].getbalance(), 50) @@ -95,6 +97,8 @@ class WalletTest(BitcoinTestFramework): # but invisible if you include mempool txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, False) assert_equal(txout['value'], 50) + txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index) # by default include_mempool=True + assert txout is None txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, True) assert txout is None # new utxo from mempool should be invisible if you exclude mempool @@ -111,19 +115,54 @@ class WalletTest(BitcoinTestFramework): assert_equal(walletinfo['immature_balance'], 0) # Have node0 mine a block, thus it will collect its own fee. - self.nodes[0].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) # Exercise locking of unspent outputs unspent_0 = self.nodes[2].listunspent()[0] unspent_0 = {"txid": unspent_0["txid"], "vout": unspent_0["vout"]} + # Trying to unlock an output which isn't locked should error assert_raises_rpc_error(-8, "Invalid parameter, expected locked output", self.nodes[2].lockunspent, True, [unspent_0]) + + # Locking an already-locked output should error self.nodes[2].lockunspent(False, [unspent_0]) assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0]) + + # Restarting the node should clear the lock + self.restart_node(2) + self.nodes[2].lockunspent(False, [unspent_0]) + + # Unloading and reloating the wallet should clear the lock + assert_equal(self.nodes[0].listwallets(), [self.default_wallet_name]) + self.nodes[2].unloadwallet(self.default_wallet_name) + self.nodes[2].loadwallet(self.default_wallet_name) + assert_equal(len(self.nodes[2].listlockunspent()), 0) + + # Locking non-persistently, then re-locking persistently, is allowed + self.nodes[2].lockunspent(False, [unspent_0]) + self.nodes[2].lockunspent(False, [unspent_0], True) + + # Restarting the node with the lock written to the wallet should keep the lock + self.restart_node(2) + assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0]) + + # Unloading and reloading the wallet with a persistent lock should keep the lock + self.nodes[2].unloadwallet(self.default_wallet_name) + self.nodes[2].loadwallet(self.default_wallet_name) + assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0]) + + # Locked outputs should not be used, even if they are the only available funds assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[2].sendtoaddress, self.nodes[2].getnewaddress(), 20) assert_equal([unspent_0], self.nodes[2].listlockunspent()) + + # Unlocking should remove the persistent lock self.nodes[2].lockunspent(True, [unspent_0]) + self.restart_node(2) assert_equal(len(self.nodes[2].listlockunspent()), 0) + + # Reconnect node 2 after restarts + self.connect_nodes(1, 2) + self.connect_nodes(0, 2) + assert_raises_rpc_error(-8, "txid must be of length 64 (not 34, for '0000000000000000000000000000000000')", self.nodes[2].lockunspent, False, [{"txid": "0000000000000000000000000000000000", "vout": 0}]) @@ -155,8 +194,7 @@ class WalletTest(BitcoinTestFramework): assert_equal(len(self.nodes[1].listlockunspent()), 0) # Have node1 generate 100 blocks (so node0 can recover the fee) - self.nodes[1].generate(100) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[1], COINBASE_MATURITY, sync_fun=lambda: self.sync_all(self.nodes[0:3])) # node0 should end up with 100 btc in block rewards plus fees, but # minus the 21 plus fees sent to node2 @@ -184,8 +222,7 @@ class WalletTest(BitcoinTestFramework): self.nodes[1].sendrawtransaction(hexstring=txns_to_send[1]["hex"], maxfeerate=0) # Have node1 mine a block to confirm transactions: - self.nodes[1].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[1], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) assert_equal(self.nodes[0].getbalance(), 0) assert_equal(self.nodes[2].getbalance(), 94) @@ -199,15 +236,13 @@ class WalletTest(BitcoinTestFramework): fee_per_byte = Decimal('0.001') / 1000 self.nodes[2].settxfee(fee_per_byte * 1000) txid = self.nodes[2].sendtoaddress(address, 10, "", "", False) - self.nodes[2].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[2], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) node_2_bal = self.check_fee_amount(self.nodes[2].getbalance(), Decimal('84'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) assert_equal(self.nodes[0].getbalance(), Decimal('10')) # Send 10 BTC with subtract fee from amount txid = self.nodes[2].sendtoaddress(address, 10, "", "", True) - self.nodes[2].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[2], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) node_2_bal -= Decimal('10') assert_equal(self.nodes[2].getbalance(), node_2_bal) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), Decimal('20'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) @@ -216,16 +251,14 @@ class WalletTest(BitcoinTestFramework): # Sendmany 10 BTC txid = self.nodes[2].sendmany('', {address: 10}, 0, "", []) - self.nodes[2].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[2], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) node_0_bal += Decimal('10') node_2_bal = self.check_fee_amount(self.nodes[2].getbalance(), node_2_bal - Decimal('10'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) assert_equal(self.nodes[0].getbalance(), node_0_bal) # Sendmany 10 BTC with subtract fee from amount txid = self.nodes[2].sendmany('', {address: 10}, 0, "", [address]) - self.nodes[2].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[2], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) node_2_bal -= Decimal('10') assert_equal(self.nodes[2].getbalance(), node_2_bal) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), node_0_bal + Decimal('10'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) @@ -237,8 +270,7 @@ class WalletTest(BitcoinTestFramework): # Test passing fee_rate as a string txid = self.nodes[2].sendmany(amounts={address: 10}, fee_rate=str(fee_rate_sat_vb)) - self.nodes[2].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[2], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) balance = self.nodes[2].getbalance() node_2_bal = self.check_fee_amount(balance, node_2_bal - Decimal('10'), explicit_fee_rate_btc_kvb, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) assert_equal(balance, node_2_bal) @@ -248,8 +280,7 @@ class WalletTest(BitcoinTestFramework): # Test passing fee_rate as an integer amount = Decimal("0.0001") txid = self.nodes[2].sendmany(amounts={address: amount}, fee_rate=fee_rate_sat_vb) - self.nodes[2].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[2], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) balance = self.nodes[2].getbalance() node_2_bal = self.check_fee_amount(balance, node_2_bal - amount, explicit_fee_rate_btc_kvb, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) assert_equal(balance, node_2_bal) @@ -262,12 +293,25 @@ class WalletTest(BitcoinTestFramework): # Test setting explicit fee rate just below the minimum. self.log.info("Test sendmany raises 'fee rate too low' if fee_rate of 0.99999999 is passed") assert_raises_rpc_error(-6, "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)", - self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0.99999999) - - self.log.info("Test sendmany raises if fee_rate of 0 or -1 is passed") - assert_raises_rpc_error(-6, "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)", - self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0) + self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0.999) + + self.log.info("Test sendmany raises if an invalid fee_rate is passed") + # Test fee_rate with zero values. + msg = "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)" + for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]: + assert_raises_rpc_error(-6, msg, self.nodes[2].sendmany, amounts={address: 1}, fee_rate=zero_value) + msg = "Invalid amount" + # Test fee_rate values that don't pass fixed-point parsing checks. + for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]: + assert_raises_rpc_error(-3, msg, self.nodes[2].sendmany, amounts={address: 1.0}, fee_rate=invalid_value) + # Test fee_rate values that cannot be represented in sat/vB. + for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]: + assert_raises_rpc_error(-3, msg, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=invalid_value) + # Test fee_rate out of range (negative number). assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=-1) + # Test type error. + for invalid_value in [True, {"foo": "bar"}]: + assert_raises_rpc_error(-3, NOT_A_NUMBER_OR_STRING, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=invalid_value) self.log.info("Test sendmany raises if an invalid conf_target or estimate_mode is passed") for target, mode in product([-1, 0, 1009], ["economical", "conservative"]): @@ -297,8 +341,7 @@ class WalletTest(BitcoinTestFramework): self.nodes[1].sendrawtransaction(signed_raw_tx['hex']) self.sync_all() - self.nodes[1].generate(1) # mine a block - self.sync_all() + self.generate(self.nodes[1], 1) # mine a block unspent_txs = self.nodes[0].listunspent() # zero value tx must be in listunspents output found = False @@ -320,14 +363,12 @@ class WalletTest(BitcoinTestFramework): txid_not_broadcast = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 2) tx_obj_not_broadcast = self.nodes[0].gettransaction(txid_not_broadcast) - self.nodes[1].generate(1) # mine a block, tx should not be in there - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[1], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) # mine a block, tx should not be in there assert_equal(self.nodes[2].getbalance(), node_2_bal) # should not be changed because tx was not broadcasted # now broadcast from another node, mine a block, sync, and check the balance self.nodes[1].sendrawtransaction(tx_obj_not_broadcast['hex']) - self.nodes[1].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[1], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) node_2_bal += 2 tx_obj_not_broadcast = self.nodes[0].gettransaction(txid_not_broadcast) assert_equal(self.nodes[2].getbalance(), node_2_bal) @@ -345,8 +386,7 @@ class WalletTest(BitcoinTestFramework): self.connect_nodes(0, 2) self.sync_blocks(self.nodes[0:3]) - self.nodes[0].generate(1) - self.sync_blocks(self.nodes[0:3]) + self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:3])) node_2_bal += 2 # tx should be added to balance because after restarting the nodes tx should be broadcast @@ -374,7 +414,7 @@ class WalletTest(BitcoinTestFramework): assert_raises_rpc_error(-3, "Invalid amount", self.nodes[0].sendtoaddress, self.nodes[2].getnewaddress(), "1f-4") # This will raise an exception since generate does not accept a string - assert_raises_rpc_error(-1, "not an integer", self.nodes[0].generate, "2") + assert_raises_rpc_error(-1, "not an integer", self.generate, self.nodes[0], "2") if not self.options.descriptors: @@ -403,12 +443,17 @@ class WalletTest(BitcoinTestFramework): # This will raise an exception for importing an invalid pubkey assert_raises_rpc_error(-5, "Pubkey is not a valid public key", self.nodes[0].importpubkey, "5361746f736869204e616b616d6f746f") + # Bech32m addresses cannot be imported into a legacy wallet + assert_raises_rpc_error(-5, "Bech32m addresses cannot be imported into legacy wallets", self.nodes[0].importaddress, "bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6") + # Import address and private key to check correct behavior of spendable unspents # 1. Send some coins to generate new UTXO address_to_import = self.nodes[2].getnewaddress() txid = self.nodes[0].sendtoaddress(address_to_import, 1) - self.nodes[0].generate(1) - self.sync_all(self.nodes[0:3]) + self.sync_mempools(self.nodes[0:3]) + vout = find_vout_for_address(self.nodes[2], txid, address_to_import) + self.nodes[2].lockunspent(False, [{"txid": txid, "vout": vout}]) + self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) self.log.info("Test sendtoaddress with fee_rate param (explicit fee rate in sat/vB)") prebalance = self.nodes[2].getbalance() @@ -420,8 +465,7 @@ class WalletTest(BitcoinTestFramework): # Test passing fee_rate as an integer txid = self.nodes[2].sendtoaddress(address=address, amount=amount, fee_rate=fee_rate_sat_vb) tx_size = self.get_vsize(self.nodes[2].gettransaction(txid)['hex']) - self.nodes[0].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) postbalance = self.nodes[2].getbalance() fee = prebalance - postbalance - Decimal(amount) assert_fee_amount(fee, tx_size, Decimal(fee_rate_btc_kvb)) @@ -433,8 +477,7 @@ class WalletTest(BitcoinTestFramework): # Test passing fee_rate as a string txid = self.nodes[2].sendtoaddress(address=address, amount=amount, fee_rate=str(fee_rate_sat_vb)) tx_size = self.get_vsize(self.nodes[2].gettransaction(txid)['hex']) - self.nodes[0].generate(1) - self.sync_all(self.nodes[0:3]) + self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_all(self.nodes[0:3])) postbalance = self.nodes[2].getbalance() fee = prebalance - postbalance - amount assert_fee_amount(fee, tx_size, Decimal(fee_rate_btc_kvb)) @@ -445,12 +488,25 @@ class WalletTest(BitcoinTestFramework): # Test setting explicit fee rate just below the minimum. self.log.info("Test sendtoaddress raises 'fee rate too low' if fee_rate of 0.99999999 is passed") assert_raises_rpc_error(-6, "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)", - self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=0.99999999) - - self.log.info("Test sendtoaddress raises if fee_rate of 0 or -1 is passed") - assert_raises_rpc_error(-6, "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)", - self.nodes[2].sendtoaddress, address=address, amount=10, fee_rate=0) + self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=0.999) + + self.log.info("Test sendtoaddress raises if an invalid fee_rate is passed") + # Test fee_rate with zero values. + msg = "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)" + for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]: + assert_raises_rpc_error(-6, msg, self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=zero_value) + msg = "Invalid amount" + # Test fee_rate values that don't pass fixed-point parsing checks. + for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]: + assert_raises_rpc_error(-3, msg, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=invalid_value) + # Test fee_rate values that cannot be represented in sat/vB. + for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]: + assert_raises_rpc_error(-3, msg, self.nodes[2].sendtoaddress, address=address, amount=10, fee_rate=invalid_value) + # Test fee_rate out of range (negative number). assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=-1) + # Test type error. + for invalid_value in [True, {"foo": "bar"}]: + assert_raises_rpc_error(-3, NOT_A_NUMBER_OR_STRING, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=invalid_value) self.log.info("Test sendtoaddress raises if an invalid conf_target or estimate_mode is passed") for target, mode in product([-1, 0, 1009], ["economical", "conservative"]): @@ -482,17 +538,15 @@ class WalletTest(BitcoinTestFramework): # Mine a block from node0 to an address from node1 coinbase_addr = self.nodes[1].getnewaddress() - block_hash = self.nodes[0].generatetoaddress(1, coinbase_addr)[0] + block_hash = self.generatetoaddress(self.nodes[0], 1, coinbase_addr, sync_fun=lambda: self.sync_all(self.nodes[0:3]))[0] coinbase_txid = self.nodes[0].getblock(block_hash)['tx'][0] - self.sync_all(self.nodes[0:3]) # Check that the txid and balance is found by node1 self.nodes[1].gettransaction(coinbase_txid) # check if wallet or blockchain maintenance changes the balance self.sync_all(self.nodes[0:3]) - blocks = self.nodes[0].generate(2) - self.sync_all(self.nodes[0:3]) + blocks = self.generate(self.nodes[0], 2, sync_fun=lambda: self.sync_all(self.nodes[0:3])) balance_nodes = [self.nodes[i].getbalance() for i in range(3)] block_count = self.nodes[0].getblockcount() @@ -509,23 +563,17 @@ class WalletTest(BitcoinTestFramework): assert label in self.nodes[0].listlabels() self.nodes[0].rpc.ensure_ascii = True # restore to default - # maintenance tests - maintenance = [ - '-rescan', - '-reindex', - ] + # -reindex tests chainlimit = 6 - for m in maintenance: - self.log.info("Test " + m) - self.stop_nodes() - # set lower ancestor limit for later - self.start_node(0, [m, "-limitancestorcount=" + str(chainlimit)]) - self.start_node(1, [m, "-limitancestorcount=" + str(chainlimit)]) - self.start_node(2, [m, "-limitancestorcount=" + str(chainlimit)]) - if m == '-reindex': - # reindex will leave rpc warm up "early"; Wait for it to finish - self.wait_until(lambda: [block_count] * 3 == [self.nodes[i].getblockcount() for i in range(3)]) - assert_equal(balance_nodes, [self.nodes[i].getbalance() for i in range(3)]) + self.log.info("Test -reindex") + self.stop_nodes() + # set lower ancestor limit for later + self.start_node(0, ['-reindex', "-limitancestorcount=" + str(chainlimit)]) + self.start_node(1, ['-reindex', "-limitancestorcount=" + str(chainlimit)]) + self.start_node(2, ['-reindex', "-limitancestorcount=" + str(chainlimit)]) + # reindex will leave rpc warm up "early"; Wait for it to finish + self.wait_until(lambda: [block_count] * 3 == [self.nodes[i].getblockcount() for i in range(3)]) + assert_equal(balance_nodes, [self.nodes[i].getbalance() for i in range(3)]) # Exercise listsinceblock with the last two blocks coinbase_tx_1 = self.nodes[0].listsinceblock(blocks[0]) @@ -539,13 +587,13 @@ class WalletTest(BitcoinTestFramework): # Get all non-zero utxos together chain_addrs = [self.nodes[0].getnewaddress(), self.nodes[0].getnewaddress()] singletxid = self.nodes[0].sendtoaddress(chain_addrs[0], self.nodes[0].getbalance(), "", "", True) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) node0_balance = self.nodes[0].getbalance() # Split into two chains rawtx = self.nodes[0].createrawtransaction([{"txid": singletxid, "vout": 0}], {chain_addrs[0]: node0_balance / 2 - Decimal('0.01'), chain_addrs[1]: node0_balance / 2 - Decimal('0.01')}) signedtx = self.nodes[0].signrawtransactionwithwallet(rawtx) singletxid = self.nodes[0].sendrawtransaction(hexstring=signedtx["hex"], maxfeerate=0) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) # Make a long chain of unconfirmed payments without hitting mempool limit # Each tx we make leaves only one output of change on a chain 1 longer @@ -567,7 +615,7 @@ class WalletTest(BitcoinTestFramework): total_txs = len(self.nodes[0].listtransactions("*", 99999)) # Try with walletrejectlongchains - # Double chain limit but require combining inputs, so we pass SelectCoinsMinConf + # Double chain limit but require combining inputs, so we pass AttemptSelection self.stop_node(0) extra_args = ["-walletrejectlongchains", "-limitancestorcount=" + str(2 * chainlimit)] self.start_node(0, extra_args=extra_args) @@ -596,11 +644,11 @@ class WalletTest(BitcoinTestFramework): assert not address_info["ischange"] # Test getaddressinfo 'ischange' field on change address. - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) destination = self.nodes[1].getnewaddress() txid = self.nodes[0].sendtoaddress(destination, 0.123) - tx = self.nodes[0].decoderawtransaction(self.nodes[0].gettransaction(txid)['hex']) - output_addresses = [vout['scriptPubKey']['addresses'][0] for vout in tx["vout"]] + tx = self.nodes[0].gettransaction(txid=txid, verbose=True)['decoded'] + output_addresses = [vout['scriptPubKey']['address'] for vout in tx["vout"]] assert len(output_addresses) > 1 for address in output_addresses: ischange = self.nodes[0].getaddressinfo(address)['ischange'] diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index 5fc8438e8f..f6843d597d 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the bumpfee RPC. @@ -14,17 +14,25 @@ added in the future, they should try to follow the same convention and not make assumptions about execution order. """ from decimal import Decimal -import io -from test_framework.blocktools import add_witness_commitment, create_block, create_coinbase, send_to_witness -from test_framework.messages import BIP125_SEQUENCE_NUMBER, CTransaction +from test_framework.blocktools import ( + COINBASE_MATURITY, + add_witness_commitment, + create_block, + create_coinbase, + send_to_witness, +) +from test_framework.messages import ( + BIP125_SEQUENCE_NUMBER, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than, assert_raises_rpc_error, - hex_str_to_bytes, ) +from test_framework.wallet import MiniWallet + WALLET_PASSPHRASE = "test" WALLET_PASSPHRASE_TIMEOUT = 3600 @@ -52,8 +60,7 @@ class BumpFeeTest(BitcoinTestFramework): def clear_mempool(self): # Clear mempool between subtests. The subtests may only depend on chainstate (utxos) - self.nodes[1].generate(1) - self.sync_all() + self.generate(self.nodes[1], 1) def run_test(self): # Encrypt wallet for test_locked_wallet_fails test @@ -65,13 +72,11 @@ class BumpFeeTest(BitcoinTestFramework): # fund rbf node with 10 coins of 0.001 btc (100,000 satoshis) self.log.info("Mining blocks...") - peer_node.generate(110) - self.sync_all() + self.generate(peer_node, 110) for _ in range(25): peer_node.sendtoaddress(rbf_node_address, 0.001) self.sync_all() - peer_node.generate(1) - self.sync_all() + self.generate(peer_node, 1) assert_equal(rbf_node.getbalance(), Decimal("0.025")) self.log.info("Running tests") @@ -110,13 +115,24 @@ class BumpFeeTest(BitcoinTestFramework): assert_raises_rpc_error(-8, "Insufficient total fee 0.00000141", rbf_node.bumpfee, rbfid, {"fee_rate": INSUFFICIENT}) self.log.info("Test invalid fee rate settings") - assert_raises_rpc_error(-8, "Insufficient total fee 0.00", rbf_node.bumpfee, rbfid, {"fee_rate": 0}) assert_raises_rpc_error(-4, "Specified or calculated fee 0.141 is too high (cannot be higher than -maxtxfee 0.10", rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH}) + # Test fee_rate with zero values. + msg = "Insufficient total fee 0.00" + for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]: + assert_raises_rpc_error(-8, msg, rbf_node.bumpfee, rbfid, {"fee_rate": zero_value}) + msg = "Invalid amount" + # Test fee_rate values that don't pass fixed-point parsing checks. + for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]: + assert_raises_rpc_error(-3, msg, rbf_node.bumpfee, rbfid, {"fee_rate": invalid_value}) + # Test fee_rate values that cannot be represented in sat/vB. + for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]: + assert_raises_rpc_error(-3, msg, rbf_node.bumpfee, rbfid, {"fee_rate": invalid_value}) + # Test fee_rate out of range (negative number). assert_raises_rpc_error(-3, "Amount out of range", rbf_node.bumpfee, rbfid, {"fee_rate": -1}) + # Test type error. for value in [{"foo": "bar"}, True]: assert_raises_rpc_error(-3, "Amount is not a number or string", rbf_node.bumpfee, rbfid, {"fee_rate": value}) - assert_raises_rpc_error(-3, "Invalid amount", rbf_node.bumpfee, rbfid, {"fee_rate": ""}) self.log.info("Test explicit fee rate raises RPC error if both fee_rate and conf_target are passed") assert_raises_rpc_error(-8, "Cannot specify both conf_target and fee_rate. Please provide either a confirmation " @@ -247,6 +263,14 @@ def test_bumpfee_with_descendant_fails(self, rbf_node, rbf_node_address, dest_ad tx = rbf_node.signrawtransactionwithwallet(tx) rbf_node.sendrawtransaction(tx["hex"]) assert_raises_rpc_error(-8, "Transaction has descendants in the wallet", rbf_node.bumpfee, parent_id) + + # create tx with descendant in the mempool by using MiniWallet + miniwallet = MiniWallet(rbf_node) + parent_id = spend_one_input(rbf_node, miniwallet.get_address()) + tx = rbf_node.gettransaction(txid=parent_id, verbose=True)['decoded'] + miniwallet.scan_tx(tx) + miniwallet.send_self_transfer(from_node=rbf_node) + assert_raises_rpc_error(-8, "Transaction has descendants in the mempool", rbf_node.bumpfee, parent_id) self.clear_mempool() @@ -254,7 +278,7 @@ def test_small_output_with_feerate_succeeds(self, rbf_node, dest_address): self.log.info('Testing small output with feerate bump succeeds') # Make sure additional inputs exist - rbf_node.generatetoaddress(101, rbf_node.getnewaddress()) + self.generatetoaddress(rbf_node, COINBASE_MATURITY + 1, rbf_node.getnewaddress()) rbfid = spend_one_input(rbf_node, dest_address) input_list = rbf_node.getrawtransaction(rbfid, 1)["vin"] assert_equal(len(input_list), 1) @@ -287,7 +311,7 @@ def test_small_output_with_feerate_succeeds(self, rbf_node, dest_address): if txin["txid"] == original_txin["txid"] and txin["vout"] == original_txin["vout"]] - rbf_node.generatetoaddress(1, rbf_node.getnewaddress()) + self.generatetoaddress(rbf_node, 1, rbf_node.getnewaddress()) assert_equal(rbf_node.gettransaction(rbfid)["confirmations"], 1) self.clear_mempool() @@ -415,8 +439,7 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): funding_address1 = watcher.getnewaddress(address_type='bech32') funding_address2 = watcher.getnewaddress(address_type='bech32') peer_node.sendmany("", {funding_address1: 0.001, funding_address2: 0.001}) - peer_node.generate(1) - self.sync_all() + self.generate(peer_node, 1) # Create single-input PSBT for transaction to be bumped psbt = watcher.walletcreatefundedpsbt([], {dest_address: 0.0005}, 0, {"fee_rate": 1}, True)['psbt'] @@ -501,7 +524,7 @@ def test_unconfirmed_not_spendable(self, rbf_node, rbf_node_address): assert_equal([t for t in rbf_node.listunspent(minconf=0, include_unsafe=False) if t["txid"] == rbfid], []) # check that the main output from the rbf tx is spendable after confirmed - rbf_node.generate(1) + self.generate(rbf_node, 1, sync_fun=self.no_op) assert_equal( sum(1 for t in rbf_node.listunspent(minconf=0, include_unsafe=False) if t["txid"] == rbfid and t["address"] == rbf_node_address and t["spendable"]), 1) @@ -511,7 +534,7 @@ def test_unconfirmed_not_spendable(self, rbf_node, rbf_node_address): def test_bumpfee_metadata(self, rbf_node, dest_address): self.log.info('Test that bumped txn metadata persists to new txn record') assert(rbf_node.getbalance() < 49) - rbf_node.generatetoaddress(101, rbf_node.getnewaddress()) + self.generatetoaddress(rbf_node, 101, rbf_node.getnewaddress()) rbfid = rbf_node.sendtoaddress(dest_address, 49, "comment value", "to value") bumped_tx = rbf_node.bumpfee(rbfid) bumped_wtx = rbf_node.gettransaction(bumped_tx["txid"]) @@ -535,7 +558,7 @@ def test_change_script_match(self, rbf_node, dest_address): def get_change_address(tx): tx_details = rbf_node.getrawtransaction(tx, 1) - txout_addresses = [txout['scriptPubKey']['addresses'][0] for txout in tx_details["vout"]] + txout_addresses = [txout['scriptPubKey']['address'] for txout in tx_details["vout"]] return [address for address in txout_addresses if rbf_node.getaddressinfo(address)["ischange"]] # Check that there is only one change output @@ -564,16 +587,10 @@ def spend_one_input(node, dest_address, change_size=Decimal("0.00049000")): def submit_block_with_tx(node, tx): - ctx = CTransaction() - ctx.deserialize(io.BytesIO(hex_str_to_bytes(tx))) - tip = node.getbestblockhash() height = node.getblockcount() + 1 block_time = node.getblockheader(tip)["mediantime"] + 1 - block = create_block(int(tip, 16), create_coinbase(height), block_time) - block.vtx.append(ctx) - block.rehash() - block.hashMerkleRoot = block.calc_merkle_root() + block = create_block(int(tip, 16), create_coinbase(height), block_time, txlist=[tx]) add_witness_commitment(block) block.solve() node.submitblock(block.serialize().hex()) @@ -583,7 +600,7 @@ def submit_block_with_tx(node, tx): def test_no_more_inputs_fails(self, rbf_node, dest_address): self.log.info('Test that bumpfee fails when there are no available confirmed outputs') # feerate rbf requires confirmed outputs when change output doesn't exist or is insufficient - rbf_node.generatetoaddress(1, dest_address) + self.generatetoaddress(rbf_node, 1, dest_address) # spend all funds, no change output rbfid = rbf_node.sendtoaddress(rbf_node.getnewaddress(), rbf_node.getbalance(), "", "", True) assert_raises_rpc_error(-4, "Unable to create transaction. Insufficient funds", rbf_node.bumpfee, rbfid) diff --git a/test/functional/wallet_coinbase_category.py b/test/functional/wallet_coinbase_category.py index 7aa8b44ebd..5a6b6cee59 100755 --- a/test/functional/wallet_coinbase_category.py +++ b/test/functional/wallet_coinbase_category.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2018 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test coinbase transactions return the correct categories. @@ -33,7 +33,7 @@ class CoinbaseCategoryTest(BitcoinTestFramework): def run_test(self): # Generate one block to an address address = self.nodes[0].getnewaddress() - self.nodes[0].generatetoaddress(1, address) + self.generatetoaddress(self.nodes[0], 1, address) hash = self.nodes[0].getbestblockhash() txid = self.nodes[0].getblock(hash)["tx"][0] @@ -41,12 +41,12 @@ class CoinbaseCategoryTest(BitcoinTestFramework): self.assert_category("immature", address, txid, 0) # Mine another 99 blocks on top - self.nodes[0].generate(99) + self.generate(self.nodes[0], 99) # Coinbase transaction is still immature after 100 confirmations self.assert_category("immature", address, txid, 99) # Mine one more block - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) # Coinbase transaction is now matured, so category is "generate" self.assert_category("generate", address, txid, 100) diff --git a/test/functional/wallet_create_tx.py b/test/functional/wallet_create_tx.py index a39a3c8d9b..a213a261ef 100755 --- a/test/functional/wallet_create_tx.py +++ b/test/functional/wallet_create_tx.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -24,7 +24,7 @@ class CreateTxWalletTest(BitcoinTestFramework): def run_test(self): self.log.info('Create some old blocks') self.nodes[0].setmocktime(TIME_GENESIS_BLOCK) - self.nodes[0].generate(200) + self.generate(self.nodes[0], 200) self.nodes[0].setmocktime(0) self.test_anti_fee_sniping() @@ -34,13 +34,13 @@ class CreateTxWalletTest(BitcoinTestFramework): self.log.info('Check that we have some (old) blocks and that anti-fee-sniping is disabled') assert_equal(self.nodes[0].getblockchaininfo()['blocks'], 200) txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) - tx = self.nodes[0].decoderawtransaction(self.nodes[0].gettransaction(txid)['hex']) + tx = self.nodes[0].gettransaction(txid=txid, verbose=True)['decoded'] assert_equal(tx['locktime'], 0) self.log.info('Check that anti-fee-sniping is enabled when we mine a recent block') - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) - tx = self.nodes[0].decoderawtransaction(self.nodes[0].gettransaction(txid)['hex']) + tx = self.nodes[0].gettransaction(txid=txid, verbose=True)['decoded'] assert 0 < tx['locktime'] <= 201 def test_tx_size_too_large(self): diff --git a/test/functional/wallet_createwallet.py b/test/functional/wallet_createwallet.py index 16a0a50b07..4416a9655f 100755 --- a/test/functional/wallet_createwallet.py +++ b/test/functional/wallet_createwallet.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test createwallet arguments. @@ -24,7 +24,7 @@ class CreateWalletTest(BitcoinTestFramework): def run_test(self): node = self.nodes[0] - node.generate(1) # Leave IBD for sethdseed + self.generate(node, 1) # Leave IBD for sethdseed self.nodes[0].createwallet(wallet_name='w0') w0 = node.get_wallet_rpc('w0') @@ -146,7 +146,7 @@ class CreateWalletTest(BitcoinTestFramework): w6.keypoolrefill(1) # There should only be 1 key for legacy, 3 for descriptors walletinfo = w6.getwalletinfo() - keys = 3 if self.options.descriptors else 1 + keys = 4 if self.options.descriptors else 1 assert_equal(walletinfo['keypoolsize'], keys) assert_equal(walletinfo['keypoolsize_hd_internal'], keys) # Allow empty passphrase, but there should be a warning diff --git a/test/functional/wallet_descriptor.py b/test/functional/wallet_descriptor.py index 1e032bdd6c..e47d021210 100755 --- a/test/functional/wallet_descriptor.py +++ b/test/functional/wallet_descriptor.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test descriptor wallet function.""" +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -36,12 +37,12 @@ class WalletDescriptorTest(BitcoinTestFramework): self.log.info("Making a descriptor wallet") self.nodes[0].createwallet(wallet_name="desc1", descriptors=True) - # A descriptor wallet should have 100 addresses * 3 types = 300 keys + # A descriptor wallet should have 100 addresses * 4 types = 400 keys self.log.info("Checking wallet info") wallet_info = self.nodes[0].getwalletinfo() assert_equal(wallet_info['format'], 'sqlite') - assert_equal(wallet_info['keypoolsize'], 300) - assert_equal(wallet_info['keypoolsize_hd_internal'], 300) + assert_equal(wallet_info['keypoolsize'], 400) + assert_equal(wallet_info['keypoolsize_hd_internal'], 400) assert 'keypoololdest' not in wallet_info # Check that getnewaddress works @@ -83,7 +84,7 @@ class WalletDescriptorTest(BitcoinTestFramework): send_wrpc = self.nodes[0].get_wallet_rpc("desc1") # Generate some coins - send_wrpc.generatetoaddress(101, send_wrpc.getnewaddress()) + self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, send_wrpc.getnewaddress()) # Make transactions self.log.info("Test sending and receiving") diff --git a/test/functional/wallet_disable.py b/test/functional/wallet_disable.py index 78cf378642..2c7996ca6b 100755 --- a/test/functional/wallet_disable.py +++ b/test/functional/wallet_disable.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2015-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test a node with the -disablewallet option. @@ -28,8 +28,8 @@ class DisableWalletTest (BitcoinTestFramework): # Checking mining to an address without a wallet. Generating to a valid address should succeed # but generating to an invalid address will fail. - self.nodes[0].generatetoaddress(1, 'mneYUmWYsuk7kySiURxCi3AGxrAqZxLgPZ') - assert_raises_rpc_error(-5, "Invalid address", self.nodes[0].generatetoaddress, 1, '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy') + self.generatetoaddress(self.nodes[0], 1, 'mneYUmWYsuk7kySiURxCi3AGxrAqZxLgPZ') + assert_raises_rpc_error(-5, "Invalid address", self.generatetoaddress, self.nodes[0], 1, '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy') if __name__ == '__main__': DisableWalletTest ().main () diff --git a/test/functional/wallet_dump.py b/test/functional/wallet_dump.py index eb54da99f5..9f0d666270 100755 --- a/test/functional/wallet_dump.py +++ b/test/functional/wallet_dump.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the dumpwallet RPC.""" @@ -134,7 +134,7 @@ class WalletDumpTest(BitcoinTestFramework): self.log.info('Mine a block one second before the wallet is dumped') dump_time = int(time.time()) self.nodes[0].setmocktime(dump_time - 1) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) self.nodes[0].setmocktime(dump_time) dump_time_str = '# * Created on {}Z'.format( datetime.datetime.fromtimestamp( @@ -209,6 +209,15 @@ class WalletDumpTest(BitcoinTestFramework): with self.nodes[0].assert_debug_log(['Flushing wallet.dat'], timeout=20): self.nodes[0].getnewaddress() + # Make sure that dumpwallet doesn't have a lock order issue when there is an unconfirmed tx and it is reloaded + # See https://github.com/bitcoin/bitcoin/issues/22489 + self.nodes[0].createwallet("w3") + w3 = self.nodes[0].get_wallet_rpc("w3") + w3.importprivkey(privkey=self.nodes[0].get_deterministic_priv_key().key, label="coinbase_import") + w3.sendtoaddress(w3.getnewaddress(), 10) + w3.unloadwallet() + self.nodes[0].loadwallet("w3") + w3.dumpwallet(os.path.join(self.nodes[0].datadir, "w3.dump")) if __name__ == '__main__': WalletDumpTest().main() diff --git a/test/functional/wallet_fallbackfee.py b/test/functional/wallet_fallbackfee.py index 78eef4b790..acd92097ff 100755 --- a/test/functional/wallet_fallbackfee.py +++ b/test/functional/wallet_fallbackfee.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test wallet replace-by-fee capabilities in conjunction with the fallbackfee.""" + +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_raises_rpc_error @@ -15,7 +17,7 @@ class WalletRBFTest(BitcoinTestFramework): self.skip_if_no_wallet() def run_test(self): - self.nodes[0].generate(101) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) # sending a transaction without fee estimations must be possible by default on regtest self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) diff --git a/test/functional/wallet_groups.py b/test/functional/wallet_groups.py index e5c4f12f20..eb305c5fa2 100755 --- a/test/functional/wallet_groups.py +++ b/test/functional/wallet_groups.py @@ -1,11 +1,14 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test wallet group functionality.""" +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework -from test_framework.messages import CTransaction, FromHex, ToHex +from test_framework.messages import ( + tx_from_hex, +) from test_framework.util import ( assert_approx, assert_equal, @@ -29,8 +32,9 @@ class WalletGroupTest(BitcoinTestFramework): self.skip_if_no_wallet() def run_test(self): + self.log.info("Setting up") # Mine some coins - self.nodes[0].generate(110) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) # Get some addresses from the two nodes addr1 = [self.nodes[1].getnewaddress() for _ in range(3)] @@ -41,13 +45,13 @@ class WalletGroupTest(BitcoinTestFramework): [self.nodes[0].sendtoaddress(addr, 1.0) for addr in addrs] [self.nodes[0].sendtoaddress(addr, 0.5) for addr in addrs] - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # For each node, send 0.2 coins back to 0; # - node[1] should pick one 0.5 UTXO and leave the rest # - node[2] should pick one (1.0 + 0.5) UTXO group corresponding to a # given address, and leave the rest + self.log.info("Test sending transactions picks one UTXO group and leaves the rest") txid1 = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 0.2) tx1 = self.nodes[1].getrawtransaction(txid1, True) # txid1 should have 1 input and 2 outputs @@ -70,9 +74,9 @@ class WalletGroupTest(BitcoinTestFramework): assert_approx(v[0], vexp=0.2, vspan=0.0001) assert_approx(v[1], vexp=1.3, vspan=0.0001) - # Test 'avoid partial if warranted, even if disabled' + self.log.info("Test avoiding partial spends if warranted, even if avoidpartialspends is disabled") self.sync_all() - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) # Nodes 1-2 now have confirmed UTXOs (letters denote destinations): # Node #1: Node #2: # - A 1.0 - D0 1.0 @@ -104,13 +108,17 @@ class WalletGroupTest(BitcoinTestFramework): assert_equal(input_addrs[0], input_addrs[1]) # Node 2 enforces avoidpartialspends so needs no checking here - # Test wallet option maxapsfee with Node 3 + tx4_ungrouped_fee = 2820 + tx4_grouped_fee = 4160 + tx5_6_ungrouped_fee = 5520 + tx5_6_grouped_fee = 8240 + + self.log.info("Test wallet option maxapsfee") addr_aps = self.nodes[3].getnewaddress() self.nodes[0].sendtoaddress(addr_aps, 1.0) self.nodes[0].sendtoaddress(addr_aps, 1.0) - self.nodes[0].generate(1) - self.sync_all() - with self.nodes[3].assert_debug_log(['Fee non-grouped = 2820, grouped = 4160, using grouped']): + self.generate(self.nodes[0], 1) + with self.nodes[3].assert_debug_log([f'Fee non-grouped = {tx4_ungrouped_fee}, grouped = {tx4_grouped_fee}, using grouped']): txid4 = self.nodes[3].sendtoaddress(self.nodes[0].getnewaddress(), 0.1) tx4 = self.nodes[3].getrawtransaction(txid4, True) # tx4 should have 2 inputs and 2 outputs although one output would @@ -120,9 +128,8 @@ class WalletGroupTest(BitcoinTestFramework): addr_aps2 = self.nodes[3].getnewaddress() [self.nodes[0].sendtoaddress(addr_aps2, 1.0) for _ in range(5)] - self.nodes[0].generate(1) - self.sync_all() - with self.nodes[3].assert_debug_log(['Fee non-grouped = 5520, grouped = 8240, using non-grouped']): + self.generate(self.nodes[0], 1) + with self.nodes[3].assert_debug_log([f'Fee non-grouped = {tx5_6_ungrouped_fee}, grouped = {tx5_6_grouped_fee}, using non-grouped']): txid5 = self.nodes[3].sendtoaddress(self.nodes[0].getnewaddress(), 2.95) tx5 = self.nodes[3].getrawtransaction(txid5, True) # tx5 should have 3 inputs (1.0, 1.0, 1.0) and 2 outputs @@ -131,11 +138,11 @@ class WalletGroupTest(BitcoinTestFramework): # Test wallet option maxapsfee with node 4, which sets maxapsfee # 1 sat higher, crossing the threshold from non-grouped to grouped. + self.log.info("Test wallet option maxapsfee threshold from non-grouped to grouped") addr_aps3 = self.nodes[4].getnewaddress() [self.nodes[0].sendtoaddress(addr_aps3, 1.0) for _ in range(5)] - self.nodes[0].generate(1) - self.sync_all() - with self.nodes[4].assert_debug_log(['Fee non-grouped = 5520, grouped = 8240, using grouped']): + self.generate(self.nodes[0], 1) + with self.nodes[4].assert_debug_log([f'Fee non-grouped = {tx5_6_ungrouped_fee}, grouped = {tx5_6_grouped_fee}, using grouped']): txid6 = self.nodes[4].sendtoaddress(self.nodes[0].getnewaddress(), 2.95) tx6 = self.nodes[4].getrawtransaction(txid6, True) # tx6 should have 5 inputs and 2 outputs @@ -145,25 +152,23 @@ class WalletGroupTest(BitcoinTestFramework): # Empty out node2's wallet self.nodes[2].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=self.nodes[2].getbalance(), subtractfeefromamount=True) self.sync_all() - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) - # Fill node2's wallet with 10000 outputs corresponding to the same - # scriptPubKey + self.log.info("Fill a wallet with 10,000 outputs corresponding to the same scriptPubKey") for _ in range(5): raw_tx = self.nodes[0].createrawtransaction([{"txid":"0"*64, "vout":0}], [{addr2[0]: 0.05}]) - tx = FromHex(CTransaction(), raw_tx) + tx = tx_from_hex(raw_tx) tx.vin = [] tx.vout = [tx.vout[0]] * 2000 - funded_tx = self.nodes[0].fundrawtransaction(ToHex(tx)) + funded_tx = self.nodes[0].fundrawtransaction(tx.serialize().hex()) signed_tx = self.nodes[0].signrawtransactionwithwallet(funded_tx['hex']) self.nodes[0].sendrawtransaction(signed_tx['hex']) - self.nodes[0].generate(1) - - self.sync_all() + self.generate(self.nodes[0], 1) # Check that we can create a transaction that only requires ~100 of our # utxos, without pulling in all outputs and creating a transaction that # is way too big. + self.log.info("Test creating txn that only requires ~100 of our UTXOs without pulling in all outputs") assert self.nodes[2].sendtoaddress(address=addr2[0], amount=5) diff --git a/test/functional/wallet_hd.py b/test/functional/wallet_hd.py index d45cf05689..ac878ea0aa 100755 --- a/test/functional/wallet_hd.py +++ b/test/functional/wallet_hd.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test Hierarchical Deterministic wallet function.""" @@ -7,6 +7,7 @@ import os import shutil +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -48,7 +49,7 @@ class WalletHDTest(BitcoinTestFramework): # Derive some HD addresses and remember the last # Also send funds to each add - self.nodes[0].generate(101) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) hd_add = None NUM_HD_ADDS = 10 for i in range(1, NUM_HD_ADDS + 1): @@ -60,9 +61,9 @@ class WalletHDTest(BitcoinTestFramework): assert_equal(hd_info["hdkeypath"], "m/0'/0'/" + str(i) + "'") assert_equal(hd_info["hdmasterfingerprint"], hd_fingerprint) self.nodes[0].sendtoaddress(hd_add, 1) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) self.nodes[0].sendtoaddress(non_hd_add, 1) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) # create an internal key (again) change_addr = self.nodes[1].getrawchangeaddress() @@ -102,7 +103,7 @@ class WalletHDTest(BitcoinTestFramework): self.sync_all() # Needs rescan - self.restart_node(1, extra_args=self.extra_args[1] + ['-rescan']) + self.nodes[1].rescanblockchain() assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1) # Try a RPC based rescan @@ -128,11 +129,11 @@ class WalletHDTest(BitcoinTestFramework): # send a tx and make sure its using the internal chain for the changeoutput txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1) - outs = self.nodes[1].decoderawtransaction(self.nodes[1].gettransaction(txid)['hex'])['vout'] + outs = self.nodes[1].gettransaction(txid=txid, verbose=True)['decoded']['vout'] keypath = "" for out in outs: if out['value'] != 1: - keypath = self.nodes[1].getaddressinfo(out['scriptPubKey']['addresses'][0])['hdkeypath'] + keypath = self.nodes[1].getaddressinfo(out['scriptPubKey']['address'])['hdkeypath'] if self.options.descriptors: assert_equal(keypath[0:14], "m/84'/1'/0'/1/") @@ -178,7 +179,7 @@ class WalletHDTest(BitcoinTestFramework): assert_raises_rpc_error(-5, "Already have this key", self.nodes[1].sethdseed, False, self.nodes[1].dumpprivkey(self.nodes[1].getnewaddress())) self.log.info('Test sethdseed restoring with keys outside of the initial keypool') - self.nodes[0].generate(10) + self.generate(self.nodes[0], 10) # Restart node 1 with keypool of 3 and a different wallet self.nodes[1].createwallet(wallet_name='origin', blank=True) self.restart_node(1, extra_args=['-keypool=3', '-wallet=origin']) @@ -227,8 +228,7 @@ class WalletHDTest(BitcoinTestFramework): # The wallet that has set a new seed (restore_rpc) should not detect this transaction. txid = self.nodes[0].sendtoaddress(addr, 1) origin_rpc.sendrawtransaction(self.nodes[0].gettransaction(txid)['hex']) - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) origin_rpc.gettransaction(txid) assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, txid) out_of_kp_txid = txid @@ -238,8 +238,7 @@ class WalletHDTest(BitcoinTestFramework): # The previous transaction (out_of_kp_txid) should still not be detected as a rescan is required. txid = self.nodes[0].sendtoaddress(last_addr, 1) origin_rpc.sendrawtransaction(self.nodes[0].gettransaction(txid)['hex']) - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) origin_rpc.gettransaction(txid) restore_rpc.gettransaction(txid) assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, out_of_kp_txid) diff --git a/test/functional/wallet_import_rescan.py b/test/functional/wallet_import_rescan.py index 59089456e9..d9acc8cea5 100755 --- a/test/functional/wallet_import_rescan.py +++ b/test/functional/wallet_import_rescan.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test wallet import RPCs. @@ -99,7 +99,7 @@ class Variant(collections.namedtuple("Variant", "call data address_type rescan p assert_equal(tx["label"], self.label) assert_equal(tx["txid"], txid) assert_equal(tx["confirmations"], 1 + current_height - confirmation_height) - assert_equal("trusted" not in tx, True) + assert "trusted" not in tx address, = [ad for ad in addresses if txid in ad["txids"]] assert_equal(address["address"], self.address["address"]) @@ -178,10 +178,9 @@ class ImportRescanTest(BitcoinTestFramework): variant.key = self.nodes[1].dumpprivkey(variant.address["address"]) variant.initial_amount = get_rand_amount() variant.initial_txid = self.nodes[0].sendtoaddress(variant.address["address"], variant.initial_amount) - self.nodes[0].generate(1) # Generate one block for each send + self.generate(self.nodes[0], 1) # Generate one block for each send variant.confirmation_height = self.nodes[0].getblockcount() variant.timestamp = self.nodes[0].getblockheader(self.nodes[0].getbestblockhash())["time"] - self.sync_all() # Conclude sync before calling setmocktime to avoid timeouts # Generate a block further in the future (past the rescan window). assert_equal(self.nodes[0].getrawmempool(), []) @@ -189,8 +188,7 @@ class ImportRescanTest(BitcoinTestFramework): self.nodes, self.nodes[0].getblockheader(self.nodes[0].getbestblockhash())["time"] + TIMESTAMP_WINDOW + 1, ) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # For each variation of wallet key import, invoke the import RPC and # check the results from getbalance and listtransactions. @@ -212,7 +210,7 @@ class ImportRescanTest(BitcoinTestFramework): for i, variant in enumerate(IMPORT_VARIANTS): variant.sent_amount = get_rand_amount() variant.sent_txid = self.nodes[0].sendtoaddress(variant.address["address"], variant.sent_amount) - self.nodes[0].generate(1) # Generate one block for each send + self.generate(self.nodes[0], 1) # Generate one block for each send variant.confirmation_height = self.nodes[0].getblockcount() assert_equal(self.nodes[0].getrawmempool(), []) diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index ed62ef0e9d..ac74ff2484 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the importdescriptors RPC. @@ -16,6 +16,7 @@ variants. and test the values returned.""" from test_framework.address import key_to_p2pkh +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.descriptors import descsum_create from test_framework.util import ( @@ -52,7 +53,7 @@ class ImportDescriptorsTest(BitcoinTestFramework): result = wrpc.importdescriptors([req]) observed_warnings = [] if 'warnings' in result[0]: - observed_warnings = result[0]['warnings'] + observed_warnings = result[0]['warnings'] assert_equal("\n".join(sorted(warnings)), "\n".join(sorted(observed_warnings))) assert_equal(result[0]['success'], success) if error_code is not None: @@ -73,12 +74,11 @@ class ImportDescriptorsTest(BitcoinTestFramework): assert_equal(wpriv.getwalletinfo()['keypoolsize'], 0) self.log.info('Mining coins') - w0.generatetoaddress(101, w0.getnewaddress()) + self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, w0.getnewaddress()) # RPC importdescriptors ----------------------------------------------- # # Test import fails if no descriptor present - key = get_generate_key() self.log.info("Import should fail if a descriptor is not provided") self.test_importdesc({"timestamp": "now"}, success=False, @@ -88,10 +88,10 @@ class ImportDescriptorsTest(BitcoinTestFramework): # # Test importing of a P2PKH descriptor key = get_generate_key() self.log.info("Should import a p2pkh descriptor") - self.test_importdesc({"desc": descsum_create("pkh(" + key.pubkey + ")"), - "timestamp": "now", - "label": "Descriptor import test"}, - success=True) + import_request = {"desc": descsum_create("pkh(" + key.pubkey + ")"), + "timestamp": "now", + "label": "Descriptor import test"} + self.test_importdesc(import_request, success=True) test_address(w1, key.p2pkh_addr, solvable=True, @@ -99,11 +99,15 @@ class ImportDescriptorsTest(BitcoinTestFramework): labels=["Descriptor import test"]) assert_equal(w1.getwalletinfo()['keypoolsize'], 0) + self.log.info("Test can import same descriptor with public key twice") + self.test_importdesc(import_request, success=True) + + self.log.info("Test can update descriptor label") + self.test_importdesc({**import_request, "label": "Updated label"}, success=True) + test_address(w1, key.p2pkh_addr, solvable=True, ismine=True, labels=["Updated label"]) + self.log.info("Internal addresses cannot have labels") - self.test_importdesc({"desc": descsum_create("pkh(" + key.pubkey + ")"), - "timestamp": "now", - "internal": True, - "label": "Descriptor import test"}, + self.test_importdesc({**import_request, "internal": True}, success=False, error_code=-8, error_message="Internal addresses should not have a label") @@ -251,6 +255,39 @@ class ImportDescriptorsTest(BitcoinTestFramework): self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [0, 1000001]}, success=False, error_code=-8, error_message='Range is too large') + self.log.info("Verify we can only extend descriptor's range") + range_request = {"desc": descsum_create(desc), "timestamp": "now", "range": [5, 10], 'active': True} + self.test_importdesc(range_request, wallet=wpriv, success=True) + assert_equal(wpriv.getwalletinfo()['keypoolsize'], 6) + self.test_importdesc({**range_request, "range": [0, 10]}, wallet=wpriv, success=True) + assert_equal(wpriv.getwalletinfo()['keypoolsize'], 11) + self.test_importdesc({**range_request, "range": [0, 20]}, wallet=wpriv, success=True) + assert_equal(wpriv.getwalletinfo()['keypoolsize'], 21) + # Can keep range the same + self.test_importdesc({**range_request, "range": [0, 20]}, wallet=wpriv, success=True) + assert_equal(wpriv.getwalletinfo()['keypoolsize'], 21) + + self.test_importdesc({**range_request, "range": [5, 10]}, wallet=wpriv, success=False, + error_code=-8, error_message='new range must include current range = [0,20]') + self.test_importdesc({**range_request, "range": [0, 10]}, wallet=wpriv, success=False, + error_code=-8, error_message='new range must include current range = [0,20]') + self.test_importdesc({**range_request, "range": [5, 20]}, wallet=wpriv, success=False, + error_code=-8, error_message='new range must include current range = [0,20]') + assert_equal(wpriv.getwalletinfo()['keypoolsize'], 21) + + self.log.info("Check we can change descriptor internal flag") + self.test_importdesc({**range_request, "range": [0, 20], "internal": True}, wallet=wpriv, success=True) + assert_equal(wpriv.getwalletinfo()['keypoolsize'], 0) + assert_raises_rpc_error(-4, 'This wallet has no available keys', wpriv.getnewaddress, '', 'p2sh-segwit') + assert_equal(wpriv.getwalletinfo()['keypoolsize_hd_internal'], 21) + wpriv.getrawchangeaddress('p2sh-segwit') + + self.test_importdesc({**range_request, "range": [0, 20], "internal": False}, wallet=wpriv, success=True) + assert_equal(wpriv.getwalletinfo()['keypoolsize'], 21) + wpriv.getnewaddress('', 'p2sh-segwit') + assert_equal(wpriv.getwalletinfo()['keypoolsize_hd_internal'], 0) + assert_raises_rpc_error(-4, 'This wallet has no available keys', wpriv.getrawchangeaddress, 'p2sh-segwit') + # Make sure ranged imports import keys in order w1 = self.nodes[1].get_wallet_rpc('w1') self.log.info('Key ranges should be imported in order') @@ -302,9 +339,21 @@ class ImportDescriptorsTest(BitcoinTestFramework): w1.keypoolrefill() assert_equal(w1.getwalletinfo()['keypoolsize'], 5 * 3) + self.log.info("Check we can change next_index") + # go back and forth with next_index + for i in [4, 0, 2, 1, 3]: + self.test_importdesc({'desc': descsum_create('wpkh([80002067/0h/0h]' + xpub + '/*)'), + 'active': True, + 'range': [0, 9], + 'next_index': i, + 'timestamp': 'now' + }, + success=True) + assert_equal(w1.getnewaddress('', 'bech32'), addresses[i]) + # Check active=False default self.log.info('Check imported descriptors are not active by default') - self.test_importdesc({'desc': descsum_create('pkh([12345678/0h/0h]' + xpub + '/*)'), + self.test_importdesc({'desc': descsum_create('pkh([12345678/1h]' + xpub + '/*)'), 'range' : [0, 2], 'timestamp': 'now', 'internal': True @@ -312,6 +361,32 @@ class ImportDescriptorsTest(BitcoinTestFramework): success=True) assert_raises_rpc_error(-4, 'This wallet has no available keys', w1.getrawchangeaddress, 'legacy') + self.log.info('Check can activate inactive descriptor') + self.test_importdesc({'desc': descsum_create('pkh([12345678]' + xpub + '/*)'), + 'range': [0, 5], + 'active': True, + 'timestamp': 'now', + 'internal': True + }, + success=True) + address = w1.getrawchangeaddress('legacy') + assert_equal(address, "mpA2Wh9dvZT7yfELq1UnrUmAoc5qCkMetg") + + self.log.info('Check can deactivate active descriptor') + self.test_importdesc({'desc': descsum_create('pkh([12345678]' + xpub + '/*)'), + 'range': [0, 5], + 'active': False, + 'timestamp': 'now', + 'internal': True + }, + success=True) + assert_raises_rpc_error(-4, 'This wallet has no available keys', w1.getrawchangeaddress, 'legacy') + + self.log.info('Verify activation state is persistent') + w1.unloadwallet() + self.nodes[1].loadwallet('w1') + assert_raises_rpc_error(-4, 'This wallet has no available keys', w1.getrawchangeaddress, 'legacy') + # # Test importing a descriptor containing a WIF private key wif_priv = "cTe1f5rdT8A8DFgVWTjyPwACsDPJM9ff4QngFxUixCSvvbg1x6sh" address = "2MuhcG52uHPknxDgmGPsV18jSHFBnnRgjPg" @@ -321,13 +396,16 @@ class ImportDescriptorsTest(BitcoinTestFramework): "timestamp": "now"}, success=True, wallet=wpriv) + + self.log.info('Test can import same descriptor with private key twice') + self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now"}, success=True, wallet=wpriv) + test_address(wpriv, address, solvable=True, ismine=True) txid = w0.sendtoaddress(address, 49.99995540) - w0.generatetoaddress(6, w0.getnewaddress()) - self.sync_blocks() + self.generatetoaddress(self.nodes[0], 6, w0.getnewaddress()) tx = wpriv.createrawtransaction([{"txid": txid, "vout": 0}], {w0.getnewaddress(): 49.999}) signed_tx = wpriv.signrawtransactionwithwallet(tx) w1.sendrawtransaction(signed_tx['hex']) @@ -338,14 +416,25 @@ class ImportDescriptorsTest(BitcoinTestFramework): wmulti_priv = self.nodes[1].get_wallet_rpc("wmulti_priv") assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 0) - self.test_importdesc({"desc":"wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/0h/0h/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/0h/0h/*,tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1/84h/0h/0h/*))#m2sr93jn", + xprv1 = 'tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52' + acc_xpub1 = 'tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8' # /84'/0'/0' + chg_xpub1 = 'tpubDCXqdwWZcszwqYJSnZp8eARkxGJfHAk23KDxbztV4BbschfaTfYLTcSkSJ3TN64dRqwa1rnFUScsYormKkGqNbbPwkorQimVevXjxzUV9Gf' # /84'/1'/0' + xprv2 = 'tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq' + acc_xprv2 = 'tprv8gVCsmRAxVSxyUpsL13Y7ZEWBFPWbgS5E2MmFVNGuANrknvmmn2vWnmHvU8AwEFYzR2ji6EeZLSCLVacsYkvor3Pcb5JY5FGcevqTwYvdYx' + acc_xpub2 = 'tpubDDBF2BTR6s8drwrfDei8WxtckGuSm1cyoKxYY1QaKSBFbHBYQArWhHPA6eJrzZej6nfHGLSURYSLHr7GuYch8aY5n61tGqgn8b4cXrMuoPH' + chg_xpub2 = 'tpubDCYfZY2ceyHzYzMMVPt9MNeiqtQ2T7Uyp9QSFwYXh8Vi9iJFYXcuphJaGXfF3jUQJi5Y3GMNXvM11gaL4txzZgNGK22BFAwMXynnzv4z2Jh' + xprv3 = 'tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1' + acc_xpub3 = 'tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E' + chg_xpub3 = 'tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb' + + self.test_importdesc({"desc":"wsh(multi(2," + xprv1 + "/84h/0h/0h/*," + xprv2 + "/84h/0h/0h/*," + xprv3 + "/84h/0h/0h/*))#m2sr93jn", "active": True, "range": 1000, "next_index": 0, "timestamp": "now"}, success=True, wallet=wmulti_priv) - self.test_importdesc({"desc":"wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/1h/0h/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/1h/0h/*,tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1/84h/1h/0h/*))#q3sztvx5", + self.test_importdesc({"desc":"wsh(multi(2," + xprv1 + "/84h/1h/0h/*," + xprv2 + "/84h/1h/0h/*," + xprv3 + "/84h/1h/0h/*))#q3sztvx5", "active": True, "internal" : True, "range": 1000, @@ -361,26 +450,24 @@ class ImportDescriptorsTest(BitcoinTestFramework): assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e') assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1000) txid = w0.sendtoaddress(addr, 10) - self.nodes[0].generate(6) - self.sync_all() + self.generate(self.nodes[0], 6) send_txid = wmulti_priv.sendtoaddress(w0.getnewaddress(), 8) - decoded = wmulti_priv.decoderawtransaction(wmulti_priv.gettransaction(send_txid)['hex']) + decoded = wmulti_priv.gettransaction(txid=send_txid, verbose=True)['decoded'] assert_equal(len(decoded['vin'][0]['txinwitness']), 4) - self.nodes[0].generate(6) - self.sync_all() + self.generate(self.nodes[0], 6) self.nodes[1].createwallet(wallet_name="wmulti_pub", disable_private_keys=True, blank=True, descriptors=True) wmulti_pub = self.nodes[1].get_wallet_rpc("wmulti_pub") assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 0) - self.test_importdesc({"desc":"wsh(multi(2,[7b2d0242/84h/0h/0h]tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*,[59b09cd6/84h/0h/0h]tpubDDBF2BTR6s8drwrfDei8WxtckGuSm1cyoKxYY1QaKSBFbHBYQArWhHPA6eJrzZej6nfHGLSURYSLHr7GuYch8aY5n61tGqgn8b4cXrMuoPH/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))#tsry0s5e", + self.test_importdesc({"desc":"wsh(multi(2,[7b2d0242/84h/0h/0h]" + acc_xpub1 + "/*,[59b09cd6/84h/0h/0h]" + acc_xpub2 + "/*,[e81a0532/84h/0h/0h]" + acc_xpub3 +"/*))#tsry0s5e", "active": True, "range": 1000, "next_index": 0, "timestamp": "now"}, success=True, wallet=wmulti_pub) - self.test_importdesc({"desc":"wsh(multi(2,[7b2d0242/84h/1h/0h]tpubDCXqdwWZcszwqYJSnZp8eARkxGJfHAk23KDxbztV4BbschfaTfYLTcSkSJ3TN64dRqwa1rnFUScsYormKkGqNbbPwkorQimVevXjxzUV9Gf/*,[59b09cd6/84h/1h/0h]tpubDCYfZY2ceyHzYzMMVPt9MNeiqtQ2T7Uyp9QSFwYXh8Vi9iJFYXcuphJaGXfF3jUQJi5Y3GMNXvM11gaL4txzZgNGK22BFAwMXynnzv4z2Jh/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))#c08a2rzv", + self.test_importdesc({"desc":"wsh(multi(2,[7b2d0242/84h/1h/0h]" + chg_xpub1 + "/*,[59b09cd6/84h/1h/0h]" + chg_xpub2 + "/*,[e81a0532/84h/1h/0h]" + chg_xpub3 + "/*))#c08a2rzv", "active": True, "internal" : True, "range": 1000, @@ -395,10 +482,16 @@ class ImportDescriptorsTest(BitcoinTestFramework): change_addr = wmulti_pub.getrawchangeaddress('bech32') assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e') assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 999) + + # generate some utxos for next tests txid = w0.sendtoaddress(addr, 10) vout = find_vout_for_address(self.nodes[0], txid, addr) - self.nodes[0].generate(6) - self.sync_all() + + addr2 = wmulti_pub.getnewaddress('', 'bech32') + txid2 = w0.sendtoaddress(addr2, 10) + vout2 = find_vout_for_address(self.nodes[0], txid2, addr2) + + self.generate(self.nodes[0], 6) assert_equal(wmulti_pub.getbalance(), wmulti_priv.getbalance()) # Make sure that descriptor wallets containing multiple xpubs in a single descriptor load correctly @@ -410,14 +503,14 @@ class ImportDescriptorsTest(BitcoinTestFramework): wmulti_priv1 = self.nodes[1].get_wallet_rpc("wmulti_priv1") res = wmulti_priv1.importdescriptors([ { - "desc": descsum_create("wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/0h/0h/*,[59b09cd6/84h/0h/0h]tpubDDBF2BTR6s8drwrfDei8WxtckGuSm1cyoKxYY1QaKSBFbHBYQArWhHPA6eJrzZej6nfHGLSURYSLHr7GuYch8aY5n61tGqgn8b4cXrMuoPH/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))"), + "desc": descsum_create("wsh(multi(2," + xprv1 + "/84h/0h/0h/*,[59b09cd6/84h/0h/0h]" + acc_xpub2 + "/*,[e81a0532/84h/0h/0h]" + acc_xpub3 + "/*))"), "active": True, "range": 1000, "next_index": 0, "timestamp": "now" }, { - "desc": descsum_create("wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/1h/0h/*,[59b09cd6/84h/1h/0h]tpubDCYfZY2ceyHzYzMMVPt9MNeiqtQ2T7Uyp9QSFwYXh8Vi9iJFYXcuphJaGXfF3jUQJi5Y3GMNXvM11gaL4txzZgNGK22BFAwMXynnzv4z2Jh/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))"), + "desc": descsum_create("wsh(multi(2," + xprv1 + "/84h/1h/0h/*,[59b09cd6/84h/1h/0h]" + chg_xpub2 + "/*,[e81a0532/84h/1h/0h]" + chg_xpub3 + "/*))"), "active": True, "internal" : True, "range": 1000, @@ -433,14 +526,14 @@ class ImportDescriptorsTest(BitcoinTestFramework): wmulti_priv2 = self.nodes[1].get_wallet_rpc('wmulti_priv2') res = wmulti_priv2.importdescriptors([ { - "desc": descsum_create("wsh(multi(2,[7b2d0242/84h/0h/0h]tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/0h/0h/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))"), + "desc": descsum_create("wsh(multi(2,[7b2d0242/84h/0h/0h]" + acc_xpub1 + "/*," + xprv2 + "/84h/0h/0h/*,[e81a0532/84h/0h/0h]" + acc_xpub3 + "/*))"), "active": True, "range": 1000, "next_index": 0, "timestamp": "now" }, { - "desc": descsum_create("wsh(multi(2,[7b2d0242/84h/1h/0h]tpubDCXqdwWZcszwqYJSnZp8eARkxGJfHAk23KDxbztV4BbschfaTfYLTcSkSJ3TN64dRqwa1rnFUScsYormKkGqNbbPwkorQimVevXjxzUV9Gf/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/1h/0h/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))"), + "desc": descsum_create("wsh(multi(2,[7b2d0242/84h/1h/0h]" + chg_xpub1 + "/*," + xprv2 + "/84h/1h/0h/*,[e81a0532/84h/1h/0h]" + chg_xpub3 + "/*))"), "active": True, "internal" : True, "range": 1000, @@ -459,6 +552,98 @@ class ImportDescriptorsTest(BitcoinTestFramework): assert_equal(tx_signed_2['complete'], True) self.nodes[1].sendrawtransaction(tx_signed_2['hex']) + self.log.info("We can create and use a huge multisig under P2WSH") + self.nodes[1].createwallet(wallet_name='wmulti_priv_big', blank=True, descriptors=True) + wmulti_priv_big = self.nodes[1].get_wallet_rpc('wmulti_priv_big') + xkey = "tprv8ZgxMBicQKsPeZSeYx7VXDDTs3XrTcmZQpRLbAeSQFCQGgKwR4gKpcxHaKdoTNHniv4EPDJNdzA3KxRrrBHcAgth8fU5X4oCndkkxk39iAt/*" + xkey_int = "tprv8ZgxMBicQKsPeZSeYx7VXDDTs3XrTcmZQpRLbAeSQFCQGgKwR4gKpcxHaKdoTNHniv4EPDJNdzA3KxRrrBHcAgth8fU5X4oCndkkxk39iAt/1/*" + res = wmulti_priv_big.importdescriptors([ + { + "desc": descsum_create(f"wsh(sortedmulti(20,{(xkey + ',') * 19}{xkey}))"), + "active": True, + "range": 1000, + "next_index": 0, + "timestamp": "now" + }, + { + "desc": descsum_create(f"wsh(sortedmulti(20,{(xkey_int + ',') * 19}{xkey_int}))"), + "active": True, + "internal": True, + "range": 1000, + "next_index": 0, + "timestamp": "now" + }]) + assert_equal(res[0]['success'], True) + assert_equal(res[1]['success'], True) + + addr = wmulti_priv_big.getnewaddress() + w0.sendtoaddress(addr, 10) + self.generate(self.nodes[0], 1) + # It is standard and would relay. + txid = wmulti_priv_big.sendtoaddress(w0.getnewaddress(), 9.999) + decoded = wmulti_priv_big.gettransaction(txid=txid, verbose=True)['decoded'] + # 20 sigs + dummy + witness script + assert_equal(len(decoded['vin'][0]['txinwitness']), 22) + + + self.log.info("Under P2SH, multisig are standard with up to 15 " + "compressed keys") + self.nodes[1].createwallet(wallet_name='multi_priv_big_legacy', + blank=True, descriptors=True) + multi_priv_big = self.nodes[1].get_wallet_rpc('multi_priv_big_legacy') + res = multi_priv_big.importdescriptors([ + { + "desc": descsum_create(f"sh(multi(15,{(xkey + ',') * 14}{xkey}))"), + "active": True, + "range": 1000, + "next_index": 0, + "timestamp": "now" + }, + { + "desc": descsum_create(f"sh(multi(15,{(xkey_int + ',') * 14}{xkey_int}))"), + "active": True, + "internal": True, + "range": 1000, + "next_index": 0, + "timestamp": "now" + }]) + assert_equal(res[0]['success'], True) + assert_equal(res[1]['success'], True) + + addr = multi_priv_big.getnewaddress("", "legacy") + w0.sendtoaddress(addr, 10) + self.generate(self.nodes[0], 6) + # It is standard and would relay. + txid = multi_priv_big.sendtoaddress(w0.getnewaddress(), 10, "", "", True) + decoded = multi_priv_big.gettransaction(txid=txid, verbose=True)['decoded'] + + self.log.info("Amending multisig with new private keys") + self.nodes[1].createwallet(wallet_name="wmulti_priv3", descriptors=True) + wmulti_priv3 = self.nodes[1].get_wallet_rpc("wmulti_priv3") + res = wmulti_priv3.importdescriptors([ + { + "desc": descsum_create("wsh(multi(2," + xprv1 + "/84h/0h/0h/*,[59b09cd6/84h/0h/0h]" + acc_xpub2 + "/*,[e81a0532/84h/0h/0h]" + acc_xpub3 + "/*))"), + "active": True, + "range": 1000, + "next_index": 0, + "timestamp": "now" + }]) + assert_equal(res[0]['success'], True) + res = wmulti_priv3.importdescriptors([ + { + "desc": descsum_create("wsh(multi(2," + xprv1 + "/84h/0h/0h/*,[59b09cd6/84h/0h/0h]" + acc_xprv2 + "/*,[e81a0532/84h/0h/0h]" + acc_xpub3 + "/*))"), + "active": True, + "range": 1000, + "next_index": 0, + "timestamp": "now" + }]) + assert_equal(res[0]['success'], True) + + rawtx = self.nodes[1].createrawtransaction([{'txid': txid2, 'vout': vout2}], {w0.getnewaddress(): 9.999}) + tx = wmulti_priv3.signrawtransactionwithwallet(rawtx) + assert_equal(tx['complete'], True) + self.nodes[1].sendrawtransaction(tx['hex']) + self.log.info("Combo descriptors cannot be active") self.test_importdesc({"desc": descsum_create("combo(tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*)"), "active": True, diff --git a/test/functional/wallet_importmulti.py b/test/functional/wallet_importmulti.py index 13186b9e1d..3953851491 100755 --- a/test/functional/wallet_importmulti.py +++ b/test/functional/wallet_importmulti.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the importmulti RPC. @@ -15,6 +15,7 @@ variants. - `test_address()` is called to call getaddressinfo for an address on node1 and test the values returned.""" +from test_framework.blocktools import COINBASE_MATURITY from test_framework.script import ( CScript, OP_NOP, @@ -61,10 +62,9 @@ class ImportMultiTest(BitcoinTestFramework): def run_test(self): self.log.info("Mining blocks...") - self.nodes[0].generate(1) - self.nodes[1].generate(1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) + self.generate(self.nodes[1], 1, sync_fun=self.no_op) timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime'] - self.nodes[1].syncwithvalidationinterfacequeue() # Sync the timestamp to the wallet, so that importmulti works node0_address1 = self.nodes[0].getaddressinfo(self.nodes[0].getnewaddress()) @@ -255,11 +255,10 @@ class ImportMultiTest(BitcoinTestFramework): # P2SH address multisig = get_multisig(self.nodes[0]) - self.nodes[1].generate(100) + self.generate(self.nodes[1], COINBASE_MATURITY, sync_fun=self.no_op) self.nodes[1].sendtoaddress(multisig.p2sh_addr, 10.00) - self.nodes[1].generate(1) + self.generate(self.nodes[1], 1, sync_fun=self.no_op) timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime'] - self.nodes[1].syncwithvalidationinterfacequeue() self.log.info("Should import a p2sh") self.test_importmulti({"scriptPubKey": {"address": multisig.p2sh_addr}, @@ -276,11 +275,10 @@ class ImportMultiTest(BitcoinTestFramework): # P2SH + Redeem script multisig = get_multisig(self.nodes[0]) - self.nodes[1].generate(100) + self.generate(self.nodes[1], COINBASE_MATURITY, sync_fun=self.no_op) self.nodes[1].sendtoaddress(multisig.p2sh_addr, 10.00) - self.nodes[1].generate(1) + self.generate(self.nodes[1], 1, sync_fun=self.no_op) timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime'] - self.nodes[1].syncwithvalidationinterfacequeue() self.log.info("Should import a p2sh with respective redeem script") self.test_importmulti({"scriptPubKey": {"address": multisig.p2sh_addr}, @@ -297,11 +295,10 @@ class ImportMultiTest(BitcoinTestFramework): # P2SH + Redeem script + Private Keys + !Watchonly multisig = get_multisig(self.nodes[0]) - self.nodes[1].generate(100) + self.generate(self.nodes[1], COINBASE_MATURITY, sync_fun=self.no_op) self.nodes[1].sendtoaddress(multisig.p2sh_addr, 10.00) - self.nodes[1].generate(1) + self.generate(self.nodes[1], 1, sync_fun=self.no_op) timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime'] - self.nodes[1].syncwithvalidationinterfacequeue() self.log.info("Should import a p2sh with respective redeem script and private keys") self.test_importmulti({"scriptPubKey": {"address": multisig.p2sh_addr}, @@ -323,11 +320,10 @@ class ImportMultiTest(BitcoinTestFramework): # P2SH + Redeem script + Private Keys + Watchonly multisig = get_multisig(self.nodes[0]) - self.nodes[1].generate(100) + self.generate(self.nodes[1], COINBASE_MATURITY, sync_fun=self.no_op) self.nodes[1].sendtoaddress(multisig.p2sh_addr, 10.00) - self.nodes[1].generate(1) + self.generate(self.nodes[1], 1, sync_fun=self.no_op) timestamp = self.nodes[1].getblock(self.nodes[1].getbestblockhash())['mediantime'] - self.nodes[1].syncwithvalidationinterfacequeue() self.log.info("Should import a p2sh with respective redeem script and private keys") self.test_importmulti({"scriptPubKey": {"address": multisig.p2sh_addr}, @@ -745,6 +741,27 @@ class ImportMultiTest(BitcoinTestFramework): assert 'hdmasterfingerprint' not in pub_import_info assert 'hdkeypath' not in pub_import_info + # Bech32m addresses and descriptors cannot be imported + self.log.info("Bech32m addresses and descriptors cannot be imported") + self.test_importmulti( + { + "scriptPubKey": {"address": "bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6"}, + "timestamp": "now", + }, + success=False, + error_code=-5, + error_message="Bech32m addresses cannot be imported into legacy wallets", + ) + self.test_importmulti( + { + "desc": descsum_create("tr({})".format(pub)), + "timestamp": "now", + }, + success=False, + error_code=-5, + error_message="Bech32m descriptors cannot be imported into legacy wallets", + ) + # Import some public keys to the keypool of a no privkey wallet self.log.info("Adding pubkey to keypool of disableprivkey wallet") self.nodes[1].createwallet(wallet_name="noprivkeys", disable_private_keys=True) diff --git a/test/functional/wallet_importprunedfunds.py b/test/functional/wallet_importprunedfunds.py index 7635ce2139..cdb5823109 100755 --- a/test/functional/wallet_importprunedfunds.py +++ b/test/functional/wallet_importprunedfunds.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the importprunedfunds and removeprunedfunds RPCs.""" from decimal import Decimal +from test_framework.blocktools import COINBASE_MATURITY from test_framework.address import key_to_p2wpkh from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework @@ -24,9 +25,7 @@ class ImportPrunedFundsTest(BitcoinTestFramework): def run_test(self): self.log.info("Mining blocks...") - self.nodes[0].generate(101) - - self.sync_all() + self.generate(self.nodes[0], COINBASE_MATURITY + 1) # address address1 = self.nodes[0].getnewaddress() @@ -46,7 +45,7 @@ class ImportPrunedFundsTest(BitcoinTestFramework): self.sync_all() # Node 1 sync test - assert_equal(self.nodes[1].getblockcount(), 101) + assert_equal(self.nodes[1].getblockcount(), COINBASE_MATURITY + 1) # Address Test - before import address_info = self.nodes[1].getaddressinfo(address1) @@ -63,17 +62,17 @@ class ImportPrunedFundsTest(BitcoinTestFramework): # Send funds to self txnid1 = self.nodes[0].sendtoaddress(address1, 0.1) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) rawtxn1 = self.nodes[0].gettransaction(txnid1)['hex'] proof1 = self.nodes[0].gettxoutproof([txnid1]) txnid2 = self.nodes[0].sendtoaddress(address2, 0.05) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) rawtxn2 = self.nodes[0].gettransaction(txnid2)['hex'] proof2 = self.nodes[0].gettxoutproof([txnid2]) txnid3 = self.nodes[0].sendtoaddress(address3, 0.025) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) rawtxn3 = self.nodes[0].gettransaction(txnid3)['hex'] proof3 = self.nodes[0].gettxoutproof([txnid3]) diff --git a/test/functional/wallet_keypool.py b/test/functional/wallet_keypool.py index 3fe6adeebc..54c47511a9 100755 --- a/test/functional/wallet_keypool.py +++ b/test/functional/wallet_keypool.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet keypool and interaction with wallet encryption/locking.""" @@ -87,8 +87,8 @@ class KeyPoolTest(BitcoinTestFramework): nodes[0].walletlock() wi = nodes[0].getwalletinfo() if self.options.descriptors: - assert_equal(wi['keypoolsize_hd_internal'], 18) - assert_equal(wi['keypoolsize'], 18) + assert_equal(wi['keypoolsize_hd_internal'], 24) + assert_equal(wi['keypoolsize'], 24) else: assert_equal(wi['keypoolsize_hd_internal'], 6) assert_equal(wi['keypoolsize'], 6) @@ -132,12 +132,26 @@ class KeyPoolTest(BitcoinTestFramework): nodes[0].keypoolrefill(100) wi = nodes[0].getwalletinfo() if self.options.descriptors: - assert_equal(wi['keypoolsize_hd_internal'], 300) - assert_equal(wi['keypoolsize'], 300) + assert_equal(wi['keypoolsize_hd_internal'], 400) + assert_equal(wi['keypoolsize'], 400) else: assert_equal(wi['keypoolsize_hd_internal'], 100) assert_equal(wi['keypoolsize'], 100) + if not self.options.descriptors: + # Check that newkeypool entirely flushes the keypool + start_keypath = nodes[0].getaddressinfo(nodes[0].getnewaddress())['hdkeypath'] + start_change_keypath = nodes[0].getaddressinfo(nodes[0].getrawchangeaddress())['hdkeypath'] + # flush keypool and get new addresses + nodes[0].newkeypool() + end_keypath = nodes[0].getaddressinfo(nodes[0].getnewaddress())['hdkeypath'] + end_change_keypath = nodes[0].getaddressinfo(nodes[0].getrawchangeaddress())['hdkeypath'] + # The new keypath index should be 100 more than the old one + new_index = int(start_keypath.rsplit('/', 1)[1][:-1]) + 100 + new_change_index = int(start_change_keypath.rsplit('/', 1)[1][:-1]) + 100 + assert_equal(end_keypath, "m/0'/0'/" + str(new_index) + "'") + assert_equal(end_change_keypath, "m/0'/1'/" + str(new_change_index) + "'") + # create a blank wallet nodes[0].createwallet(wallet_name='w2', blank=True, disable_private_keys=True) w2 = nodes[0].get_wallet_rpc('w2') @@ -156,12 +170,12 @@ class KeyPoolTest(BitcoinTestFramework): w1.walletpassphrase('test', 100) res = w1.sendtoaddress(address=address, amount=0.00010000) - nodes[0].generate(1) + self.generate(nodes[0], 1) destination = addr.pop() # Using a fee rate (10 sat / byte) well above the minimum relay rate # creating a 5,000 sat transaction with change should not be possible - assert_raises_rpc_error(-4, "Transaction needs a change address, but we can't generate it. Please call keypoolrefill first.", w2.walletcreatefundedpsbt, inputs=[], outputs=[{addr.pop(): 0.00005000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010}) + assert_raises_rpc_error(-4, "Transaction needs a change address, but we can't generate it.", w2.walletcreatefundedpsbt, inputs=[], outputs=[{addr.pop(): 0.00005000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010}) # creating a 10,000 sat transaction without change, with a manual input, should still be possible res = w2.walletcreatefundedpsbt(inputs=w2.listunspent(), outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010}) @@ -179,7 +193,7 @@ class KeyPoolTest(BitcoinTestFramework): assert_equal("psbt" in res, True) # create a transaction without change at the maximum fee rate, such that the output is still spendable: - res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.0008824}) + res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.0008823}) assert_equal("psbt" in res, True) assert_equal(res["fee"], Decimal("0.00009706")) diff --git a/test/functional/wallet_keypool_topup.py b/test/functional/wallet_keypool_topup.py index 5619d57947..4c965b7160 100755 --- a/test/functional/wallet_keypool_topup.py +++ b/test/functional/wallet_keypool_topup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test HD Wallet keypool restore function. @@ -13,6 +13,7 @@ Two nodes. Node1 is under test. Node0 is providing transactions and generating b import os import shutil +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -31,7 +32,7 @@ class KeypoolRestoreTest(BitcoinTestFramework): def run_test(self): wallet_path = os.path.join(self.nodes[1].datadir, self.chain, "wallets", self.default_wallet_name, self.wallet_data_filename) wallet_backup_path = os.path.join(self.nodes[1].datadir, "wallet.bak") - self.nodes[0].generate(101) + self.generate(self.nodes[0], COINBASE_MATURITY + 1) self.log.info("Make backup of wallet") self.stop_node(1) @@ -62,10 +63,9 @@ class KeypoolRestoreTest(BitcoinTestFramework): self.log.info("Send funds to wallet") self.nodes[0].sendtoaddress(addr_oldpool, 10) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) self.nodes[0].sendtoaddress(addr_extpool, 5) - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) self.log.info("Restart node with wallet backup") self.stop_node(idx) diff --git a/test/functional/wallet_labels.py b/test/functional/wallet_labels.py index 883b97561e..c29b02e661 100755 --- a/test/functional/wallet_labels.py +++ b/test/functional/wallet_labels.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2016-2020 The Bitcoin Core developers +# Copyright (c) 2016-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test label RPCs. @@ -11,6 +11,7 @@ RPCs tested are: """ from collections import defaultdict +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error from test_framework.wallet_util import test_address @@ -31,8 +32,8 @@ class WalletLabelsTest(BitcoinTestFramework): # Note each time we call generate, all generated coins go into # the same address, so we call twice to get two addresses w/50 each - node.generatetoaddress(nblocks=1, address=node.getnewaddress(label='coinbase')) - node.generatetoaddress(nblocks=101, address=node.getnewaddress(label='coinbase')) + self.generatetoaddress(node, nblocks=1, address=node.getnewaddress(label='coinbase')) + self.generatetoaddress(node, nblocks=COINBASE_MATURITY + 1, address=node.getnewaddress(label='coinbase')) assert_equal(node.getbalance(), 100) # there should be 2 address groups @@ -64,7 +65,7 @@ class WalletLabelsTest(BitcoinTestFramework): assert_equal(set([a[0] for a in address_groups[0]]), linked_addresses) assert_equal([a[1] for a in address_groups[0]], [0, 0]) - node.generate(1) + self.generate(node, 1) # we want to reset so that the "" label has what's expected. # otherwise we're off by exactly the fee amount as that's mined @@ -88,7 +89,7 @@ class WalletLabelsTest(BitcoinTestFramework): label.verify(node) # Check the amounts received. - node.generate(1) + self.generate(node, 1) for label in labels: assert_equal( node.getreceivedbyaddress(label.addresses[0]), amount_to_send) @@ -97,14 +98,14 @@ class WalletLabelsTest(BitcoinTestFramework): for i, label in enumerate(labels): to_label = labels[(i + 1) % len(labels)] node.sendtoaddress(to_label.addresses[0], amount_to_send) - node.generate(1) + self.generate(node, 1) for label in labels: address = node.getnewaddress(label.name) label.add_receive_address(address) label.verify(node) assert_equal(node.getreceivedbylabel(label.name), 2) label.verify(node) - node.generate(101) + self.generate(node, COINBASE_MATURITY + 1) # Check that setlabel can assign a label to a new unused address. for label in labels: @@ -124,7 +125,7 @@ class WalletLabelsTest(BitcoinTestFramework): label.add_address(multisig_address) label.purpose[multisig_address] = "send" label.verify(node) - node.generate(101) + self.generate(node, COINBASE_MATURITY + 1) # Check that setlabel can change the label of an address from a # different label. @@ -134,31 +135,33 @@ class WalletLabelsTest(BitcoinTestFramework): # in the label. This is a no-op. change_label(node, labels[2].addresses[0], labels[2], labels[2]) - self.log.info('Check watchonly labels') - node.createwallet(wallet_name='watch_only', disable_private_keys=True) - wallet_watch_only = node.get_wallet_rpc('watch_only') - BECH32_VALID = { - '✔️_VER15_PROG40': 'bcrt10qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqn2cjv3', - '✔️_VER16_PROG03': 'bcrt1sqqqqqjq8pdp', - '✔️_VER16_PROB02': 'bcrt1sqqqqqjq8pv', - } - BECH32_INVALID = { - '❌_VER15_PROG41': 'bcrt10qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzc7xyq', - '❌_VER16_PROB01': 'bcrt1sqqpl9r5c', - } - for l in BECH32_VALID: - ad = BECH32_VALID[l] - wallet_watch_only.importaddress(label=l, rescan=False, address=ad) - node.generatetoaddress(1, ad) - assert_equal(wallet_watch_only.getaddressesbylabel(label=l), {ad: {'purpose': 'receive'}}) - assert_equal(wallet_watch_only.getreceivedbylabel(label=l), 0) - for l in BECH32_INVALID: - ad = BECH32_INVALID[l] - assert_raises_rpc_error( - -5, - "Address is not valid" if self.options.descriptors else "Invalid Bitcoin address or script", - lambda: wallet_watch_only.importaddress(label=l, rescan=False, address=ad), - ) + if self.options.descriptors: + # This is a descriptor wallet test because of segwit v1+ addresses + self.log.info('Check watchonly labels') + node.createwallet(wallet_name='watch_only', disable_private_keys=True) + wallet_watch_only = node.get_wallet_rpc('watch_only') + BECH32_VALID = { + '✔️_VER15_PROG40': 'bcrt10qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqxkg7fn', + '✔️_VER16_PROG03': 'bcrt1sqqqqq8uhdgr', + '✔️_VER16_PROB02': 'bcrt1sqqqq4wstyw', + } + BECH32_INVALID = { + '❌_VER15_PROG41': 'bcrt1sqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqajlxj8', + '❌_VER16_PROB01': 'bcrt1sqq5r4036', + } + for l in BECH32_VALID: + ad = BECH32_VALID[l] + wallet_watch_only.importaddress(label=l, rescan=False, address=ad) + self.generatetoaddress(node, 1, ad) + assert_equal(wallet_watch_only.getaddressesbylabel(label=l), {ad: {'purpose': 'receive'}}) + assert_equal(wallet_watch_only.getreceivedbylabel(label=l), 0) + for l in BECH32_INVALID: + ad = BECH32_INVALID[l] + assert_raises_rpc_error( + -5, + "Address is not valid" if self.options.descriptors else "Invalid Bitcoin address or script", + lambda: wallet_watch_only.importaddress(label=l, rescan=False, address=ad), + ) class Label: diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py index 8d02949ff4..202ef92887 100755 --- a/test/functional/wallet_listdescriptors.py +++ b/test/functional/wallet_listdescriptors.py @@ -23,28 +23,30 @@ class ListDescriptorsTest(BitcoinTestFramework): self.skip_if_no_sqlite() # do not create any wallet by default - def init_wallet(self, i): + def init_wallet(self, *, node): return def run_test(self): node = self.nodes[0] assert_raises_rpc_error(-18, 'No wallet is loaded.', node.listdescriptors) - self.log.info('Test that the command is not available for legacy wallets.') - node.createwallet(wallet_name='w1', descriptors=False) - assert_raises_rpc_error(-4, 'listdescriptors is not available for non-descriptor wallets', node.listdescriptors) + if self.is_bdb_compiled(): + self.log.info('Test that the command is not available for legacy wallets.') + node.createwallet(wallet_name='w1', descriptors=False) + assert_raises_rpc_error(-4, 'listdescriptors is not available for non-descriptor wallets', node.listdescriptors) self.log.info('Test the command for empty descriptors wallet.') node.createwallet(wallet_name='w2', blank=True, descriptors=True) - assert_equal(0, len(node.get_wallet_rpc('w2').listdescriptors())) + assert_equal(0, len(node.get_wallet_rpc('w2').listdescriptors()['descriptors'])) self.log.info('Test the command for a default descriptors wallet.') node.createwallet(wallet_name='w3', descriptors=True) result = node.get_wallet_rpc('w3').listdescriptors() - assert_equal(6, len(result)) - assert_equal(6, len([d for d in result if d['active']])) - assert_equal(3, len([d for d in result if d['internal']])) - for item in result: + assert_equal("w3", result['wallet_name']) + assert_equal(8, len(result['descriptors'])) + assert_equal(8, len([d for d in result['descriptors'] if d['active']])) + assert_equal(4, len([d for d in result['descriptors'] if d['internal']])) + for item in result['descriptors']: assert item['desc'] != '' assert item['next'] == 0 assert item['range'] == [0, 0] @@ -59,12 +61,49 @@ class ListDescriptorsTest(BitcoinTestFramework): 'desc': descsum_create('wpkh(' + xprv + hardened_path + '/0/*)'), 'timestamp': 1296688602, }]) - expected = {'desc': descsum_create('wpkh([80002067' + hardened_path + ']' + xpub_acc + '/0/*)'), - 'timestamp': 1296688602, - 'active': False, - 'range': [0, 0], - 'next': 0} - assert_equal([expected], wallet.listdescriptors()) + expected = { + 'wallet_name': 'w2', + 'descriptors': [ + {'desc': descsum_create('wpkh([80002067' + hardened_path + ']' + xpub_acc + '/0/*)'), + 'timestamp': 1296688602, + 'active': False, + 'range': [0, 0], + 'next': 0}, + ], + } + assert_equal(expected, wallet.listdescriptors()) + assert_equal(expected, wallet.listdescriptors(False)) + + self.log.info('Test list private descriptors') + expected_private = { + 'wallet_name': 'w2', + 'descriptors': [ + {'desc': descsum_create('wpkh(' + xprv + hardened_path + '/0/*)'), + 'timestamp': 1296688602, + 'active': False, + 'range': [0, 0], + 'next': 0}, + ], + } + assert_equal(expected_private, wallet.listdescriptors(True)) + + self.log.info("Test listdescriptors with encrypted wallet") + wallet.encryptwallet("pass") + assert_equal(expected, wallet.listdescriptors()) + + self.log.info('Test list private descriptors with encrypted wallet') + assert_raises_rpc_error(-13, 'Please enter the wallet passphrase with walletpassphrase first.', wallet.listdescriptors, True) + wallet.walletpassphrase(passphrase="pass", timeout=1000000) + assert_equal(expected_private, wallet.listdescriptors(True)) + + self.log.info('Test list private descriptors with watch-only wallet') + node.createwallet(wallet_name='watch-only', descriptors=True, disable_private_keys=True) + watch_only_wallet = node.get_wallet_rpc('watch-only') + watch_only_wallet.importdescriptors([{ + 'desc': descsum_create('wpkh(' + xpub_acc + ')'), + 'timestamp': 1296688602, + }]) + assert_raises_rpc_error(-4, 'Can\'t get descriptor string', watch_only_wallet.listdescriptors, True) self.log.info('Test non-active non-range combo descriptor') node.createwallet(wallet_name='w4', blank=True, descriptors=True) @@ -73,9 +112,14 @@ class ListDescriptorsTest(BitcoinTestFramework): 'desc': descsum_create('combo(' + node.get_deterministic_priv_key().key + ')'), 'timestamp': 1296688602, }]) - expected = [{'active': False, - 'desc': 'combo(0227d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f)#np574htj', - 'timestamp': 1296688602}] + expected = { + 'wallet_name': 'w4', + 'descriptors': [ + {'active': False, + 'desc': 'combo(0227d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f)#np574htj', + 'timestamp': 1296688602}, + ] + } assert_equal(expected, wallet.listdescriptors()) diff --git a/test/functional/wallet_listreceivedby.py b/test/functional/wallet_listreceivedby.py index b0590b149a..48b92796fc 100755 --- a/test/functional/wallet_listreceivedby.py +++ b/test/functional/wallet_listreceivedby.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Test the listreceivedbyaddress RPC.""" +"""Test the listreceivedbyaddress, listreceivedbylabel, getreceivedybaddress, and getreceivedbylabel RPCs.""" from decimal import Decimal +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_array_result, @@ -17,6 +18,8 @@ from test_framework.wallet_util import test_address class ReceivedByTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 + # Test deprecated exclude coinbase on second node + self.extra_args = [[], ["-deprecatedrpc=exclude_coinbase"]] def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -24,8 +27,7 @@ class ReceivedByTest(BitcoinTestFramework): def run_test(self): # Generate block to get out of IBD - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) # save the number of coinbase reward addresses so far num_cb_reward_addresses = len(self.nodes[1].listreceivedbyaddress(minconf=0, include_empty=True, include_watchonly=True)) @@ -43,8 +45,7 @@ class ReceivedByTest(BitcoinTestFramework): {}, True) # Bury Tx under 10 block so it will be returned by listreceivedbyaddress - self.nodes[1].generate(10) - self.sync_all() + self.generate(self.nodes[1], 10) assert_array_result(self.nodes[1].listreceivedbyaddress(), {"address": addr}, {"address": addr, "label": "", "amount": Decimal("0.1"), "confirmations": 10, "txids": [txid, ]}) @@ -78,8 +79,7 @@ class ReceivedByTest(BitcoinTestFramework): assert_equal(len(res), 2 + num_cb_reward_addresses) # Right now 2 entries other_addr = self.nodes[1].getnewaddress() txid2 = self.nodes[0].sendtoaddress(other_addr, 0.1) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # Same test as above should still pass expected = {"address": addr, "label": "", "amount": Decimal("0.1"), "confirmations": 11, "txids": [txid, ]} res = self.nodes[1].listreceivedbyaddress(0, True, True, addr) @@ -115,8 +115,7 @@ class ReceivedByTest(BitcoinTestFramework): assert_equal(balance, Decimal("0.1")) # Bury Tx under 10 block so it will be returned by the default getreceivedbyaddress - self.nodes[1].generate(10) - self.sync_all() + self.generate(self.nodes[1], 10) balance = self.nodes[1].getreceivedbyaddress(addr) assert_equal(balance, Decimal("0.1")) @@ -144,8 +143,7 @@ class ReceivedByTest(BitcoinTestFramework): balance = self.nodes[1].getreceivedbylabel(label) assert_equal(balance, balance_by_label) - self.nodes[1].generate(10) - self.sync_all() + self.generate(self.nodes[1], 10) # listreceivedbylabel should return updated received list assert_array_result(self.nodes[1].listreceivedbylabel(), {"label": label}, @@ -167,5 +165,123 @@ class ReceivedByTest(BitcoinTestFramework): balance = self.nodes[1].getreceivedbylabel("mynewlabel") assert_equal(balance, Decimal("0.0")) + self.log.info("Tests for including coinbase outputs") + + # Generate block reward to address with label + label = "label" + address = self.nodes[0].getnewaddress(label) + + reward = Decimal("25") + self.generatetoaddress(self.nodes[0], 1, address, sync_fun=self.no_op) + hash = self.nodes[0].getbestblockhash() + + self.log.info("getreceivedbyaddress returns nothing with defaults") + balance = self.nodes[0].getreceivedbyaddress(address) + assert_equal(balance, 0) + + self.log.info("getreceivedbyaddress returns block reward when including immature coinbase") + balance = self.nodes[0].getreceivedbyaddress(address=address, include_immature_coinbase=True) + assert_equal(balance, reward) + + self.log.info("getreceivedbylabel returns nothing with defaults") + balance = self.nodes[0].getreceivedbylabel("label") + assert_equal(balance, 0) + + self.log.info("getreceivedbylabel returns block reward when including immature coinbase") + balance = self.nodes[0].getreceivedbylabel(label="label", include_immature_coinbase=True) + assert_equal(balance, reward) + + self.log.info("listreceivedbyaddress does not include address with defaults") + assert_array_result(self.nodes[0].listreceivedbyaddress(), + {"address": address}, + {}, True) + + self.log.info("listreceivedbyaddress includes address when including immature coinbase") + assert_array_result(self.nodes[0].listreceivedbyaddress(minconf=1, include_immature_coinbase=True), + {"address": address}, + {"address": address, "amount": reward}) + + self.log.info("listreceivedbylabel does not include label with defaults") + assert_array_result(self.nodes[0].listreceivedbylabel(), + {"label": label}, + {}, True) + + self.log.info("listreceivedbylabel includes label when including immature coinbase") + assert_array_result(self.nodes[0].listreceivedbylabel(minconf=1, include_immature_coinbase=True), + {"label": label}, + {"label": label, "amount": reward}) + + self.log.info("Generate 100 more blocks") + self.generate(self.nodes[0], COINBASE_MATURITY, sync_fun=self.no_op) + + self.log.info("getreceivedbyaddress returns reward with defaults") + balance = self.nodes[0].getreceivedbyaddress(address) + assert_equal(balance, reward) + + self.log.info("getreceivedbylabel returns reward with defaults") + balance = self.nodes[0].getreceivedbylabel("label") + assert_equal(balance, reward) + + self.log.info("listreceivedbyaddress includes address with defaults") + assert_array_result(self.nodes[0].listreceivedbyaddress(), + {"address": address}, + {"address": address, "amount": reward}) + + self.log.info("listreceivedbylabel includes label with defaults") + assert_array_result(self.nodes[0].listreceivedbylabel(), + {"label": label}, + {"label": label, "amount": reward}) + + self.log.info("Invalidate block that paid to address") + self.nodes[0].invalidateblock(hash) + + self.log.info("getreceivedbyaddress does not include invalidated block when minconf is 0 when including immature coinbase") + balance = self.nodes[0].getreceivedbyaddress(address=address, minconf=0, include_immature_coinbase=True) + assert_equal(balance, 0) + + self.log.info("getreceivedbylabel does not include invalidated block when minconf is 0 when including immature coinbase") + balance = self.nodes[0].getreceivedbylabel(label="label", minconf=0, include_immature_coinbase=True) + assert_equal(balance, 0) + + self.log.info("listreceivedbyaddress does not include invalidated block when minconf is 0 when including immature coinbase") + assert_array_result(self.nodes[0].listreceivedbyaddress(minconf=0, include_immature_coinbase=True), + {"address": address}, + {}, True) + + self.log.info("listreceivedbylabel does not include invalidated block when minconf is 0 when including immature coinbase") + assert_array_result(self.nodes[0].listreceivedbylabel(minconf=0, include_immature_coinbase=True), + {"label": label}, + {}, True) + + # Test exclude_coinbase + address2 = self.nodes[1].getnewaddress(label) + self.generatetoaddress(self.nodes[1], COINBASE_MATURITY + 1, address2, sync_fun=self.no_op) + + self.log.info("getreceivedbyaddress returns nothing when excluding coinbase") + balance = self.nodes[1].getreceivedbyaddress(address2) + assert_equal(balance, 0) + + self.log.info("getreceivedbylabel returns nothing when excluding coinbase") + balance = self.nodes[1].getreceivedbylabel("label") + assert_equal(balance, 0) + + self.log.info("listreceivedbyaddress does not include address when excluding coinbase") + assert_array_result(self.nodes[1].listreceivedbyaddress(), + {"address": address2}, + {}, True) + + self.log.info("listreceivedbylabel does not include label when excluding coinbase") + assert_array_result(self.nodes[1].listreceivedbylabel(), + {"label": label}, + {}, True) + + self.log.info("getreceivedbyaddress throws when setting include_immature_coinbase with deprecated exclude_coinbase") + assert_raises_rpc_error(-8, 'include_immature_coinbase is incompatible with deprecated exclude_coinbase', self.nodes[1].getreceivedbyaddress, address2, 1, True) + + + self.log.info("listreceivedbyaddress throws when setting include_immature_coinbase with deprecated exclude_coinbase") + assert_raises_rpc_error(-8, 'include_immature_coinbase is incompatible with deprecated exclude_coinbase', self.nodes[1].listreceivedbyaddress, 1, False, False, "", True) + + if __name__ == '__main__': ReceivedByTest().main() diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py index 448720530c..fc06565983 100755 --- a/test/functional/wallet_listsinceblock.py +++ b/test/functional/wallet_listsinceblock.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the listsinceblock RPC.""" from test_framework.address import key_to_p2wpkh +from test_framework.blocktools import COINBASE_MATURITY from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.messages import BIP125_SEQUENCE_NUMBER @@ -29,8 +30,7 @@ class ListSinceBlockTest(BitcoinTestFramework): # All nodes are in IBD from genesis, so they'll need the miner (node2) to be an outbound connection, or have # only one connection. (See fPreferredDownload in net_processing) self.connect_nodes(1, 2) - self.nodes[2].generate(101) - self.sync_all() + self.generate(self.nodes[2], COINBASE_MATURITY + 1) self.test_no_blockhash() self.test_invalid_blockhash() @@ -43,9 +43,16 @@ class ListSinceBlockTest(BitcoinTestFramework): def test_no_blockhash(self): self.log.info("Test no blockhash") txid = self.nodes[2].sendtoaddress(self.nodes[0].getnewaddress(), 1) - blockhash, = self.nodes[2].generate(1) - blockheight = self.nodes[2].getblockheader(blockhash)['height'] self.sync_all() + assert_array_result(self.nodes[0].listtransactions(), {"txid": txid}, { + "category": "receive", + "amount": 1, + "confirmations": 0, + "trusted": False, + }) + + blockhash, = self.generate(self.nodes[2], 1) + blockheight = self.nodes[2].getblockheader(blockhash)['height'] txs = self.nodes[0].listtransactions() assert_array_result(txs, {"txid": txid}, { @@ -55,6 +62,9 @@ class ListSinceBlockTest(BitcoinTestFramework): "blockheight": blockheight, "confirmations": 1, }) + assert_equal(len(txs), 1) + assert "trusted" not in txs[0] + assert_equal( self.nodes[0].listsinceblock(), {"lastblock": blockhash, @@ -85,9 +95,8 @@ class ListSinceBlockTest(BitcoinTestFramework): a -8 invalid parameter error is thrown. ''' self.log.info("Test target_confirmations") - blockhash, = self.nodes[2].generate(1) + blockhash, = self.generate(self.nodes[2], 1) blockheight = self.nodes[2].getblockheader(blockhash)['height'] - self.sync_all() assert_equal( self.nodes[0].getblockhash(0), @@ -135,14 +144,11 @@ class ListSinceBlockTest(BitcoinTestFramework): senttx = self.nodes[2].sendtoaddress(self.nodes[0].getnewaddress(), 1) # generate on both sides - nodes1_last_blockhash = self.nodes[1].generate(6)[-1] - nodes2_first_blockhash = self.nodes[2].generate(7)[0] + nodes1_last_blockhash = self.generate(self.nodes[1], 6, sync_fun=lambda: self.sync_all(self.nodes[:2]))[-1] + nodes2_first_blockhash = self.generate(self.nodes[2], 7, sync_fun=lambda: self.sync_all(self.nodes[2:]))[0] self.log.debug("nodes[1] last blockhash = {}".format(nodes1_last_blockhash)) self.log.debug("nodes[2] first blockhash = {}".format(nodes2_first_blockhash)) - self.sync_all(self.nodes[:2]) - self.sync_all(self.nodes[2:]) - self.join_network() # listsinceblock(nodes1_last_blockhash) should now include tx as seen from nodes[0] @@ -190,8 +196,7 @@ class ListSinceBlockTest(BitcoinTestFramework): privkey = bytes_to_wif(eckey.get_bytes()) address = key_to_p2wpkh(eckey.get_pubkey().get_bytes()) self.nodes[2].sendtoaddress(address, 10) - self.nodes[2].generate(6) - self.sync_all() + self.generate(self.nodes[2], 6) self.nodes[2].importprivkey(privkey) utxos = self.nodes[2].listunspent() utxo = [u for u in utxos if u["address"] == address][0] @@ -224,8 +229,8 @@ class ListSinceBlockTest(BitcoinTestFramework): self.nodes[2].createrawtransaction(utxo_dicts, recipient_dict2))['hex']) # generate on both sides - lastblockhash = self.nodes[1].generate(3)[2] - self.nodes[2].generate(4) + lastblockhash = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[2] + self.generate(self.nodes[2], 4, sync_fun=self.no_op) self.join_network() @@ -296,7 +301,7 @@ class ListSinceBlockTest(BitcoinTestFramework): txid1 = self.nodes[1].sendrawtransaction(signedtx) # generate bb1-bb2 on right side - self.nodes[2].generate(2) + self.generate(self.nodes[2], 2, sync_fun=self.no_op) # send from nodes[2]; this will end up in bb3 txid2 = self.nodes[2].sendrawtransaction(signedtx) @@ -304,8 +309,8 @@ class ListSinceBlockTest(BitcoinTestFramework): assert_equal(txid1, txid2) # generate on both sides - lastblockhash = self.nodes[1].generate(3)[2] - self.nodes[2].generate(2) + lastblockhash = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[2] + self.generate(self.nodes[2], 2, sync_fun=self.no_op) self.join_network() @@ -364,7 +369,7 @@ class ListSinceBlockTest(BitcoinTestFramework): assert_equal(original_found, True) assert_equal(double_found, True) - lastblockhash = spending_node.generate(1)[0] + lastblockhash = self.generate(spending_node, 1)[0] # check that neither transaction exists block_hash = spending_node.listsinceblock(lastblockhash) diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py index 71573964de..f75877f256 100755 --- a/test/functional/wallet_listtransactions.py +++ b/test/functional/wallet_listtransactions.py @@ -1,48 +1,48 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the listtransactions API.""" + from decimal import Decimal -from io import BytesIO +import os +import shutil -from test_framework.messages import COIN, CTransaction +from test_framework.messages import ( + COIN, + tx_from_hex, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_array_result, assert_equal, - hex_str_to_bytes, + assert_raises_rpc_error, ) -def tx_from_hex(hexstring): - tx = CTransaction() - f = BytesIO(hex_str_to_bytes(hexstring)) - tx.deserialize(f) - return tx class ListTransactionsTest(BitcoinTestFramework): def set_test_params(self): - self.num_nodes = 2 + self.num_nodes = 3 + # This test isn't testing txn relay/timing, so set whitelist on the + # peers for instant txn relay. This speeds up the test run time 2-3x. + self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): - self.nodes[0].generate(1) # Get out of IBD - self.sync_all() - # Simple send, 0 to 1: + self.log.info("Test simple send from node0 to node1") txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.1) self.sync_all() assert_array_result(self.nodes[0].listtransactions(), {"txid": txid}, - {"category": "send", "amount": Decimal("-0.1"), "confirmations": 0}) + {"category": "send", "amount": Decimal("-0.1"), "confirmations": 0, "trusted": True}) assert_array_result(self.nodes[1].listtransactions(), {"txid": txid}, - {"category": "receive", "amount": Decimal("0.1"), "confirmations": 0}) - # mine a block, confirmations should change: - blockhash = self.nodes[0].generate(1)[0] + {"category": "receive", "amount": Decimal("0.1"), "confirmations": 0, "trusted": False}) + self.log.info("Test confirmations change after mining a block") + blockhash = self.generate(self.nodes[0], 1)[0] blockheight = self.nodes[0].getblockheader(blockhash)['height'] - self.sync_all() assert_array_result(self.nodes[0].listtransactions(), {"txid": txid}, {"category": "send", "amount": Decimal("-0.1"), "confirmations": 1, "blockhash": blockhash, "blockheight": blockheight}) @@ -50,7 +50,7 @@ class ListTransactionsTest(BitcoinTestFramework): {"txid": txid}, {"category": "receive", "amount": Decimal("0.1"), "confirmations": 1, "blockhash": blockhash, "blockheight": blockheight}) - # send-to-self: + self.log.info("Test send-to-self on node0") txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 0.2) assert_array_result(self.nodes[0].listtransactions(), {"txid": txid, "category": "send"}, @@ -59,7 +59,7 @@ class ListTransactionsTest(BitcoinTestFramework): {"txid": txid, "category": "receive"}, {"amount": Decimal("0.2")}) - # sendmany from node1: twice to self, twice to node2: + self.log.info("Test sendmany from node1: twice to self, twice to node0") send_to = {self.nodes[0].getnewaddress(): 0.11, self.nodes[1].getnewaddress(): 0.22, self.nodes[0].getnewaddress(): 0.33, @@ -93,12 +93,12 @@ class ListTransactionsTest(BitcoinTestFramework): if not self.options.descriptors: # include_watchonly is a legacy wallet feature, so don't test it for descriptor wallets + self.log.info("Test 'include_watchonly' feature (legacy wallet)") pubkey = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['pubkey'] multisig = self.nodes[1].createmultisig(1, [pubkey]) self.nodes[0].importaddress(multisig["redeemScript"], "watchonly", False, True) txid = self.nodes[1].sendtoaddress(multisig["address"], 0.1) - self.nodes[1].generate(1) - self.sync_all() + self.generate(self.nodes[1], 1) assert_equal(len(self.nodes[0].listtransactions(label="watchonly", include_watchonly=True)), 1) assert_equal(len(self.nodes[0].listtransactions(dummy="watchonly", include_watchonly=True)), 1) assert len(self.nodes[0].listtransactions(label="watchonly", count=100, include_watchonly=False)) == 0 @@ -107,38 +107,40 @@ class ListTransactionsTest(BitcoinTestFramework): {"txid": txid, "label": "watchonly"}) self.run_rbf_opt_in_test() + self.run_externally_generated_address_test() + self.run_invalid_parameters_test() - # Check that the opt-in-rbf flag works properly, for sent and received - # transactions. def run_rbf_opt_in_test(self): - # Check whether a transaction signals opt-in RBF itself + """Test the opt-in-rbf flag for sent and received transactions.""" + def is_opt_in(node, txid): + """Check whether a transaction signals opt-in RBF itself.""" rawtx = node.getrawtransaction(txid, 1) for x in rawtx["vin"]: if x["sequence"] < 0xfffffffe: return True return False - # Find an unconfirmed output matching a certain txid def get_unconfirmed_utxo_entry(node, txid_to_match): + """Find an unconfirmed output matching a certain txid.""" utxo = node.listunspent(0, 0) for i in utxo: if i["txid"] == txid_to_match: return i return None - # 1. Chain a few transactions that don't opt-in. + self.log.info("Test txs w/o opt-in RBF (bip125-replaceable=no)") + # Chain a few transactions that don't opt in. txid_1 = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1) assert not is_opt_in(self.nodes[0], txid_1) assert_array_result(self.nodes[0].listtransactions(), {"txid": txid_1}, {"bip125-replaceable": "no"}) self.sync_mempools() assert_array_result(self.nodes[1].listtransactions(), {"txid": txid_1}, {"bip125-replaceable": "no"}) - # Tx2 will build off txid_1, still not opting in to RBF. + # Tx2 will build off tx1, still not opting in to RBF. utxo_to_use = get_unconfirmed_utxo_entry(self.nodes[0], txid_1) assert_equal(utxo_to_use["safe"], True) utxo_to_use = get_unconfirmed_utxo_entry(self.nodes[1], txid_1) - utxo_to_use = get_unconfirmed_utxo_entry(self.nodes[1], txid_1) assert_equal(utxo_to_use["safe"], False) # Create tx2 using createrawtransaction @@ -154,6 +156,7 @@ class ListTransactionsTest(BitcoinTestFramework): self.sync_mempools() assert_array_result(self.nodes[0].listtransactions(), {"txid": txid_2}, {"bip125-replaceable": "no"}) + self.log.info("Test txs with opt-in RBF (bip125-replaceable=yes)") # Tx3 will opt-in to RBF utxo_to_use = get_unconfirmed_utxo_entry(self.nodes[0], txid_2) inputs = [{"txid": txid_2, "vout": utxo_to_use["vout"]}] @@ -184,6 +187,7 @@ class ListTransactionsTest(BitcoinTestFramework): self.sync_mempools() assert_array_result(self.nodes[0].listtransactions(), {"txid": txid_4}, {"bip125-replaceable": "yes"}) + self.log.info("Test tx with unknown RBF state (bip125-replaceable=unknown)") # Replace tx3, and check that tx4 becomes unknown tx3_b = tx3_modified tx3_b.vout[0].nValue -= int(Decimal("0.004") * COIN) # bump the fee @@ -196,7 +200,7 @@ class ListTransactionsTest(BitcoinTestFramework): self.sync_mempools() assert_array_result(self.nodes[1].listtransactions(), {"txid": txid_4}, {"bip125-replaceable": "unknown"}) - # Check gettransaction as well: + self.log.info("Test bip125-replaceable status with gettransaction RPC") for n in self.nodes[0:2]: assert_equal(n.gettransaction(txid_1)["bip125-replaceable"], "no") assert_equal(n.gettransaction(txid_2)["bip125-replaceable"], "no") @@ -204,11 +208,82 @@ class ListTransactionsTest(BitcoinTestFramework): assert_equal(n.gettransaction(txid_3b)["bip125-replaceable"], "yes") assert_equal(n.gettransaction(txid_4)["bip125-replaceable"], "unknown") - # After mining a transaction, it's no longer BIP125-replaceable - self.nodes[0].generate(1) + self.log.info("Test bip125-replaceable status with listsinceblock") + for n in self.nodes[0:2]: + txs = {tx['txid']: tx['bip125-replaceable'] for tx in n.listsinceblock()['transactions']} + assert_equal(txs[txid_1], "no") + assert_equal(txs[txid_2], "no") + assert_equal(txs[txid_3], "yes") + assert_equal(txs[txid_3b], "yes") + assert_equal(txs[txid_4], "unknown") + + self.log.info("Test mined transactions are no longer bip125-replaceable") + self.generate(self.nodes[0], 1) assert txid_3b not in self.nodes[0].getrawmempool() assert_equal(self.nodes[0].gettransaction(txid_3b)["bip125-replaceable"], "no") assert_equal(self.nodes[0].gettransaction(txid_4)["bip125-replaceable"], "unknown") + def run_externally_generated_address_test(self): + """Test behavior when receiving address is not in the address book.""" + + self.log.info("Setup the same wallet on two nodes") + # refill keypool otherwise the second node wouldn't recognize addresses generated on the first nodes + self.nodes[0].keypoolrefill(1000) + self.stop_nodes() + wallet0 = os.path.join(self.nodes[0].datadir, self.chain, self.default_wallet_name, "wallet.dat") + wallet2 = os.path.join(self.nodes[2].datadir, self.chain, self.default_wallet_name, "wallet.dat") + shutil.copyfile(wallet0, wallet2) + self.start_nodes() + # reconnect nodes + self.connect_nodes(0, 1) + self.connect_nodes(1, 2) + self.connect_nodes(2, 0) + + addr1 = self.nodes[0].getnewaddress("pizza1", 'legacy') + addr2 = self.nodes[0].getnewaddress("pizza2", 'p2sh-segwit') + addr3 = self.nodes[0].getnewaddress("pizza3", 'bech32') + + self.log.info("Send to externally generated addresses") + # send to an address beyond the next to be generated to test the keypool gap + self.nodes[1].sendtoaddress(addr3, "0.001") + self.generate(self.nodes[1], 1) + + # send to an address that is already marked as used due to the keypool gap mechanics + self.nodes[1].sendtoaddress(addr2, "0.001") + self.generate(self.nodes[1], 1) + + # send to self transaction + self.nodes[0].sendtoaddress(addr1, "0.001") + self.generate(self.nodes[0], 1) + + self.log.info("Verify listtransactions is the same regardless of where the address was generated") + transactions0 = self.nodes[0].listtransactions() + transactions2 = self.nodes[2].listtransactions() + + # normalize results: remove fields that normally could differ and sort + def normalize_list(txs): + for tx in txs: + tx.pop('label', None) + tx.pop('time', None) + tx.pop('timereceived', None) + txs.sort(key=lambda x: x['txid']) + + normalize_list(transactions0) + normalize_list(transactions2) + assert_equal(transactions0, transactions2) + + self.log.info("Verify labels are persistent on the node that generated the addresses") + assert_equal(['pizza1'], self.nodes[0].getaddressinfo(addr1)['labels']) + assert_equal(['pizza2'], self.nodes[0].getaddressinfo(addr2)['labels']) + assert_equal(['pizza3'], self.nodes[0].getaddressinfo(addr3)['labels']) + + def run_invalid_parameters_test(self): + self.log.info("Test listtransactions RPC parameter validity") + assert_raises_rpc_error(-8, 'Label argument must be a valid label name or "*".', self.nodes[0].listtransactions, label="") + self.nodes[0].listtransactions(label="*") + assert_raises_rpc_error(-8, "Negative count", self.nodes[0].listtransactions, count=-1) + assert_raises_rpc_error(-8, "Negative from", self.nodes[0].listtransactions, skip=-1) + + if __name__ == '__main__': ListTransactionsTest().main() diff --git a/test/functional/wallet_multisig_descriptor_psbt.py b/test/functional/wallet_multisig_descriptor_psbt.py new file mode 100755 index 0000000000..2b565db137 --- /dev/null +++ b/test/functional/wallet_multisig_descriptor_psbt.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test a basic M-of-N multisig setup between multiple people using descriptor wallets and PSBTs, as well as a signing flow. + +This is meant to be documentation as much as functional tests, so it is kept as simple and readable as possible. +""" + +from test_framework.address import base58_to_byte +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_approx, + assert_equal, +) + + +class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 3 + self.setup_clean_chain = True + self.wallet_names = [] + self.extra_args = [["-keypool=100"]] * self.num_nodes + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + self.skip_if_no_sqlite() + + @staticmethod + def _get_xpub(wallet): + """Extract the wallet's xpubs using `listdescriptors` and pick the one from the `pkh` descriptor since it's least likely to be accidentally reused (legacy addresses).""" + descriptor = next(filter(lambda d: d["desc"].startswith("pkh"), wallet.listdescriptors()["descriptors"])) + return descriptor["desc"].split("]")[-1].split("/")[0] + + @staticmethod + def _check_psbt(psbt, to, value, multisig): + """Helper function for any of the N participants to check the psbt with decodepsbt and verify it is OK before signing.""" + tx = multisig.decodepsbt(psbt)["tx"] + amount = 0 + for vout in tx["vout"]: + address = vout["scriptPubKey"]["address"] + assert_equal(multisig.getaddressinfo(address)["ischange"], address != to) + if address == to: + amount += vout["value"] + assert_approx(amount, float(value), vspan=0.001) + + def participants_create_multisigs(self, xpubs): + """The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every participant can do this.""" + # some simple validation + assert_equal(len(xpubs), self.N) + # a sanity-check/assertion, this will throw if the base58 checksum of any of the provided xpubs are invalid + for xpub in xpubs: + base58_to_byte(xpub) + + for i, node in enumerate(self.nodes): + node.createwallet(wallet_name=f"{self.name}_{i}", blank=True, descriptors=True, disable_private_keys=True) + multisig = node.get_wallet_rpc(f"{self.name}_{i}") + external = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/0/*,'.join(xpubs)}/0/*))") + internal = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/1/*,'.join(xpubs)}/1/*))") + result = multisig.importdescriptors([ + { # receiving addresses (internal: False) + "desc": external["descriptor"], + "active": True, + "internal": False, + "timestamp": "now", + }, + { # change addresses (internal: True) + "desc": internal["descriptor"], + "active": True, + "internal": True, + "timestamp": "now", + }, + ]) + assert all(r["success"] for r in result) + yield multisig + + def run_test(self): + self.M = 2 + self.N = self.num_nodes + self.name = f"{self.M}_of_{self.N}_multisig" + self.log.info(f"Testing {self.name}...") + + participants = { + # Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet. + # This wallet will be the participant's `signer` for the resulting multisig. Avoid reusing this wallet for any other purpose (for privacy reasons). + "signers": [node.get_wallet_rpc(node.createwallet(wallet_name=f"participant_{self.nodes.index(node)}", descriptors=True)["name"]) for node in self.nodes], + # After participants generate and exchange their xpubs they will each create their own watch-only multisig. + # Note: these multisigs are all the same, this just highlights that each participant can independently verify everything on their own node. + "multisigs": [] + } + + self.log.info("Generate and exchange xpubs...") + xpubs = [self._get_xpub(signer) for signer in participants["signers"]] + + self.log.info("Every participant imports the following descriptors to create the watch-only multisig...") + participants["multisigs"] = list(self.participants_create_multisigs(xpubs)) + + self.log.info("Check that every participant's multisig generates the same addresses...") + for _ in range(10): # we check that the first 10 generated addresses are the same for all participant's multisigs + receive_addresses = [multisig.getnewaddress() for multisig in participants["multisigs"]] + all(address == receive_addresses[0] for address in receive_addresses) + change_addresses = [multisig.getrawchangeaddress() for multisig in participants["multisigs"]] + all(address == change_addresses[0] for address in change_addresses) + + self.log.info("Get a mature utxo to send to the multisig...") + coordinator_wallet = participants["signers"][0] + self.generatetoaddress(self.nodes[0], 101, coordinator_wallet.getnewaddress()) + + deposit_amount = 6.15 + multisig_receiving_address = participants["multisigs"][0].getnewaddress() + self.log.info("Send funds to the resulting multisig receiving address...") + coordinator_wallet.sendtoaddress(multisig_receiving_address, deposit_amount) + self.generate(self.nodes[0], 1) + for participant in participants["multisigs"]: + assert_approx(participant.getbalance(), deposit_amount, vspan=0.001) + + self.log.info("Send a transaction from the multisig!") + to = participants["signers"][self.N - 1].getnewaddress() + value = 1 + self.log.info("First, make a sending transaction, created using `walletcreatefundedpsbt` (anyone can initiate this)...") + psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, options={"feeRate": 0.00010}) + + psbts = [] + self.log.info("Now at least M users check the psbt with decodepsbt and (if OK) signs it with walletprocesspsbt...") + for m in range(self.M): + signers_multisig = participants["multisigs"][m] + self._check_psbt(psbt["psbt"], to, value, signers_multisig) + signing_wallet = participants["signers"][m] + partially_signed_psbt = signing_wallet.walletprocesspsbt(psbt["psbt"]) + psbts.append(partially_signed_psbt["psbt"]) + + self.log.info("Finally, collect the signed PSBTs with combinepsbt, finalizepsbt, then broadcast the resulting transaction...") + combined = coordinator_wallet.combinepsbt(psbts) + finalized = coordinator_wallet.finalizepsbt(combined) + coordinator_wallet.sendrawtransaction(finalized["hex"]) + + self.log.info("Check that balances are correct after the transaction has been included in a block.") + self.generate(self.nodes[0], 1) + assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - value, vspan=0.001) + assert_equal(participants["signers"][self.N - 1].getbalance(), value) + + self.log.info("Send another transaction from the multisig, this time with a daisy chained signing flow (one after another in series)!") + psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, options={"feeRate": 0.00010}) + for m in range(self.M): + signers_multisig = participants["multisigs"][m] + self._check_psbt(psbt["psbt"], to, value, signers_multisig) + signing_wallet = participants["signers"][m] + psbt = signing_wallet.walletprocesspsbt(psbt["psbt"]) + assert_equal(psbt["complete"], m == self.M - 1) + finalized = coordinator_wallet.finalizepsbt(psbt["psbt"]) + coordinator_wallet.sendrawtransaction(finalized["hex"]) + + self.log.info("Check that balances are correct after the transaction has been included in a block.") + self.generate(self.nodes[0], 1) + assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - (value * 2), vspan=0.001) + assert_equal(participants["signers"][self.N - 1].getbalance(), value * 2) + + +if __name__ == "__main__": + WalletMultisigDescriptorPSBTTest().main() diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index bf24b9c7b3..dcb82bbbe9 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test multiwallet. @@ -11,9 +11,11 @@ from threading import Thread import os import shutil import stat +import sys import time from test_framework.authproxy import JSONRPCException +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.test_node import ErrorMatch from test_framework.util import ( @@ -34,7 +36,7 @@ def test_load_unload(node, name): node.loadwallet(name) node.unloadwallet(name) except JSONRPCException as e: - if e.error['code'] == -4 and 'Wallet already being loading' in e.error['message']: + if e.error['code'] == -4 and 'Wallet already loading' in e.error['message']: got_loading_error = True return @@ -140,7 +142,7 @@ class MultiWalletTest(BitcoinTestFramework): # should raise rpc error if wallet path can't be created err_code = -4 if self.options.descriptors else -1 - assert_raises_rpc_error(err_code, "boost::filesystem::create_directory:", self.nodes[0].createwallet, "w8/bad") + assert_raises_rpc_error(err_code, "filesystem error:" if sys.platform != 'win32' else "create_directories:", self.nodes[0].createwallet, "w8/bad") # check that all requested wallets were created self.stop_node(0) @@ -184,7 +186,7 @@ class MultiWalletTest(BitcoinTestFramework): self.nodes[0].createwallet("w5") assert_equal(set(node.listwallets()), {"w4", "w5"}) w5 = wallet("w5") - node.generatetoaddress(nblocks=1, address=w5.getnewaddress()) + self.generatetoaddress(node, nblocks=1, address=w5.getnewaddress(), sync_fun=self.no_op) # now if wallets/ exists again, but the rootdir is specified as the walletdir, w4 and w5 should still be loaded os.rename(wallet_dir2, wallet_dir()) @@ -201,7 +203,7 @@ class MultiWalletTest(BitcoinTestFramework): self.restart_node(0, ['-nowallet', '-walletdir=' + competing_wallet_dir]) self.nodes[0].createwallet(self.default_wallet_name) if self.options.descriptors: - exp_stderr = r"Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?" + exp_stderr = f"Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of {self.config['environment']['PACKAGE_NAME']}?" else: exp_stderr = r"Error: Error initializing wallet database environment \"\S+competing_walletdir\S*\"!" self.nodes[1].assert_start_raises_init_error(['-walletdir=' + competing_wallet_dir], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) @@ -216,7 +218,7 @@ class MultiWalletTest(BitcoinTestFramework): wallet_bad = wallet("bad") # check wallet names and balances - node.generatetoaddress(nblocks=1, address=wallets[0].getnewaddress()) + self.generatetoaddress(node, nblocks=1, address=wallets[0].getnewaddress(), sync_fun=self.no_op) for wallet_name, wallet in zip(wallet_names, wallets): info = wallet.getwalletinfo() assert_equal(info['immature_balance'], 50 if wallet is wallets[0] else 0) @@ -229,7 +231,7 @@ class MultiWalletTest(BitcoinTestFramework): assert_raises_rpc_error(-19, "Wallet file not specified", node.getwalletinfo) w1, w2, w3, w4, *_ = wallets - node.generatetoaddress(nblocks=101, address=w1.getnewaddress()) + self.generatetoaddress(node, nblocks=COINBASE_MATURITY + 1, address=w1.getnewaddress(), sync_fun=self.no_op) assert_equal(w1.getbalance(), 100) assert_equal(w2.getbalance(), 0) assert_equal(w3.getbalance(), 0) @@ -238,7 +240,7 @@ class MultiWalletTest(BitcoinTestFramework): w1.sendtoaddress(w2.getnewaddress(), 1) w1.sendtoaddress(w3.getnewaddress(), 2) w1.sendtoaddress(w4.getnewaddress(), 3) - node.generatetoaddress(nblocks=1, address=w1.getnewaddress()) + self.generatetoaddress(node, nblocks=1, address=w1.getnewaddress(), sync_fun=self.no_op) assert_equal(w2.getbalance(), 1) assert_equal(w3.getbalance(), 2) assert_equal(w4.getbalance(), 3) @@ -302,7 +304,7 @@ class MultiWalletTest(BitcoinTestFramework): # Fail to load duplicate wallets path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "w1", "wallet.dat") if self.options.descriptors: - assert_raises_rpc_error(-4, "Wallet file verification failed. SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?", self.nodes[0].loadwallet, wallet_names[0]) + assert_raises_rpc_error(-4, f"Wallet file verification failed. SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of {self.config['environment']['PACKAGE_NAME']}?", self.nodes[0].loadwallet, wallet_names[0]) else: assert_raises_rpc_error(-35, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format(path), self.nodes[0].loadwallet, wallet_names[0]) diff --git a/test/functional/wallet_orphanedreward.py b/test/functional/wallet_orphanedreward.py new file mode 100755 index 0000000000..7295db4653 --- /dev/null +++ b/test/functional/wallet_orphanedreward.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test orphaned block rewards in the wallet.""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + +class OrphanedBlockRewardTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 2 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + # Generate some blocks and obtain some coins on node 0. We send + # some balance to node 1, which will hold it as a single coin. + self.generate(self.nodes[0], 150) + self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 10) + self.generate(self.nodes[0], 1) + + # Get a block reward with node 1 and remember the block so we can orphan + # it later. + self.sync_blocks() + blk = self.generate(self.nodes[1], 1)[0] + + # Let the block reward mature and send coins including both + # the existing balance and the block reward. + self.generate(self.nodes[0], 150) + assert_equal(self.nodes[1].getbalance(), 10 + 25) + txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 30) + + # Orphan the block reward and make sure that the original coins + # from the wallet can still be spent. + self.nodes[0].invalidateblock(blk) + self.generate(self.nodes[0], 152) + # Without the following abandontransaction call, the coins are + # not considered available yet. + assert_equal(self.nodes[1].getbalances()["mine"], { + "trusted": 0, + "untrusted_pending": 0, + "immature": 0, + }) + # The following abandontransaction is necessary to make the later + # lines succeed, and probably should not be needed; see + # https://github.com/bitcoin/bitcoin/issues/14148. + self.nodes[1].abandontransaction(txid) + assert_equal(self.nodes[1].getbalances()["mine"], { + "trusted": 10, + "untrusted_pending": 0, + "immature": 0, + }) + self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 9) + +if __name__ == '__main__': + OrphanedBlockRewardTest().main() diff --git a/test/functional/wallet_reorgsrestore.py b/test/functional/wallet_reorgsrestore.py index 9a5866a361..f2bdb114b7 100755 --- a/test/functional/wallet_reorgsrestore.py +++ b/test/functional/wallet_reorgsrestore.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020 The Bitcoin Core developers +# Copyright (c) 2019-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -32,8 +32,7 @@ class ReorgsRestoreTest(BitcoinTestFramework): def run_test(self): # Send a tx from which to conflict outputs later txid_conflict_from = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) # Disconnect node1 from others to reorg its chain later self.disconnect_nodes(0, 1) @@ -43,7 +42,7 @@ class ReorgsRestoreTest(BitcoinTestFramework): # Send a tx to be unconfirmed later txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) tx = self.nodes[0].gettransaction(txid) - self.nodes[0].generate(4) + self.generate(self.nodes[0], 4, sync_fun=self.no_op) tx_before_reorg = self.nodes[0].gettransaction(txid) assert_equal(tx_before_reorg["confirmations"], 4) @@ -62,9 +61,9 @@ class ReorgsRestoreTest(BitcoinTestFramework): conflicting = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_2)) conflicted_txid = self.nodes[0].sendrawtransaction(conflicted["hex"]) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) conflicting_txid = self.nodes[2].sendrawtransaction(conflicting["hex"]) - self.nodes[2].generate(9) + self.generate(self.nodes[2], 9, sync_fun=self.no_op) # Reconnect node0 and node2 and check that conflicted_txid is effectively conflicted self.connect_nodes(0, 2) @@ -78,11 +77,11 @@ class ReorgsRestoreTest(BitcoinTestFramework): self.restart_node(0) # The block chain re-orgs and the tx is included in a different block - self.nodes[1].generate(9) + self.generate(self.nodes[1], 9, sync_fun=self.no_op) self.nodes[1].sendrawtransaction(tx["hex"]) - self.nodes[1].generate(1) + self.generate(self.nodes[1], 1, sync_fun=self.no_op) self.nodes[1].sendrawtransaction(conflicted["hex"]) - self.nodes[1].generate(1) + self.generate(self.nodes[1], 1, sync_fun=self.no_op) # Node0 wallet file is loaded on longest sync'ed node1 self.stop_node(1) diff --git a/test/functional/wallet_resendwallettransactions.py b/test/functional/wallet_resendwallettransactions.py index 78d88f8aa5..5aae2c813a 100755 --- a/test/functional/wallet_resendwallettransactions.py +++ b/test/functional/wallet_resendwallettransactions.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2020 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test that the wallet resends transactions periodically.""" import time -from test_framework.blocktools import create_block, create_coinbase -from test_framework.messages import ToHex +from test_framework.blocktools import ( + create_block, + create_coinbase, +) from test_framework.p2p import P2PTxInvStore from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal @@ -46,9 +48,8 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): block_time = int(time.time()) + 6 * 60 node.setmocktime(block_time) block = create_block(int(node.getbestblockhash(), 16), create_coinbase(node.getblockcount() + 1), block_time) - block.rehash() block.solve() - node.submitblock(ToHex(block)) + node.submitblock(block.serialize().hex()) # Set correct m_best_block_time, which is used in ResendWalletTransactions node.syncwithvalidationinterfacequeue() diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index 880341fdd9..86e36be8f7 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2020-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the send RPC command.""" @@ -9,13 +9,16 @@ from itertools import product from test_framework.authproxy import JSONRPCException from test_framework.descriptors import descsum_create +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_fee_amount, assert_greater_than, assert_raises_rpc_error, + count_bytes, ) +from test_framework.wallet_util import bytes_to_wif class WalletSendTest(BitcoinTestFramework): def set_test_params(self): @@ -33,12 +36,15 @@ class WalletSendTest(BitcoinTestFramework): def test_send(self, from_wallet, to_wallet=None, amount=None, data=None, arg_conf_target=None, arg_estimate_mode=None, arg_fee_rate=None, conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None, - inputs=None, add_inputs=None, change_address=None, change_position=None, change_type=None, + inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None, change_type=None, include_watching=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None, - expect_error=None): + expect_error=None, solving_data=None): assert (amount is None) != (data is None) - from_balance_before = from_wallet.getbalance() + from_balance_before = from_wallet.getbalances()["mine"]["trusted"] + if include_unsafe: + from_balance_before += from_wallet.getbalances()["mine"]["untrusted_pending"] + if to_wallet is None: assert amount is None else: @@ -71,6 +77,8 @@ class WalletSendTest(BitcoinTestFramework): options["inputs"] = inputs if add_inputs is not None: options["add_inputs"] = add_inputs + if include_unsafe is not None: + options["include_unsafe"] = include_unsafe if change_address is not None: options["change_address"] = change_address if change_position is not None: @@ -89,6 +97,8 @@ class WalletSendTest(BitcoinTestFramework): options["replaceable"] = replaceable if subtract_fee_from_outputs is not None: options["subtract_fee_from_outputs"] = subtract_fee_from_outputs + if solving_data is not None: + options["solving_data"] = solving_data if len(options.keys()) == 0: options = None @@ -133,6 +143,10 @@ class WalletSendTest(BitcoinTestFramework): assert not "txid" in res assert "psbt" in res + from_balance = from_wallet.getbalances()["mine"]["trusted"] + if include_unsafe: + from_balance += from_wallet.getbalances()["mine"]["untrusted_pending"] + if add_to_wallet and not include_watching: # Ensure transaction exists in the wallet: tx = from_wallet.gettransaction(res["txid"]) @@ -143,13 +157,13 @@ class WalletSendTest(BitcoinTestFramework): assert tx if amount: if subtract_fee_from_outputs: - assert_equal(from_balance_before - from_wallet.getbalance(), amount) + assert_equal(from_balance_before - from_balance, amount) else: - assert_greater_than(from_balance_before - from_wallet.getbalance(), amount) + assert_greater_than(from_balance_before - from_balance, amount) else: assert next((out for out in tx["vout"] if out["scriptPubKey"]["asm"] == "OP_RETURN 35"), None) else: - assert_equal(from_balance_before, from_wallet.getbalance()) + assert_equal(from_balance_before, from_balance) if to_wallet: self.sync_mempools() @@ -232,8 +246,7 @@ class WalletSendTest(BitcoinTestFramework): assert_equal(res, [{"success": True}, {"success": True}]) w0.sendtoaddress(a2_receive, 10) # fund w3 - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) if not self.options.descriptors: # w4 has private keys enabled, but only contains watch-only keys (from w2) @@ -251,8 +264,7 @@ class WalletSendTest(BitcoinTestFramework): assert_equal(res, [{"success": True}]) w0.sendtoaddress(a2_receive, 10) # fund w4 - self.nodes[0].generate(1) - self.sync_blocks() + self.generate(self.nodes[0], 1) self.log.info("Send to address...") self.test_send(from_wallet=w0, to_wallet=w1, amount=1) @@ -309,20 +321,20 @@ class WalletSendTest(BitcoinTestFramework): res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=7, add_to_wallet=False) fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] - assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00007")) + assert_fee_amount(fee, count_bytes(res["hex"]), Decimal("0.00007")) # "unset" and None are treated the same for estimate_mode res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=2, estimate_mode="unset", add_to_wallet=False) fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] - assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00002")) + assert_fee_amount(fee, count_bytes(res["hex"]), Decimal("0.00002")) res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=4.531, add_to_wallet=False) fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] - assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00004531")) + assert_fee_amount(fee, count_bytes(res["hex"]), Decimal("0.00004531")) res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=3, add_to_wallet=False) fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] - assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00003")) + assert_fee_amount(fee, count_bytes(res["hex"]), Decimal("0.00003")) # Test that passing fee_rate as both an argument and an option raises. self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=1, fee_rate=1, add_to_wallet=False, @@ -343,22 +355,41 @@ class WalletSendTest(BitcoinTestFramework): self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=0.1, arg_estimate_mode=mode, expect_error=(-8, msg)) assert_raises_rpc_error(-8, msg, w0.send, {w1.getnewaddress(): 1}, 0.1, mode) - for mode in ["economical", "conservative", "btc/kb", "sat/b"]: - self.log.debug("{}".format(mode)) - for k, v in {"string": "true", "object": {"foo": "bar"}}.items(): + for mode in ["economical", "conservative"]: + for k, v in {"string": "true", "bool": True, "object": {"foo": "bar"}}.items(): self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=v, estimate_mode=mode, - expect_error=(-3, "Expected type number for conf_target, got {}".format(k))) + expect_error=(-3, f"Expected type number for conf_target, got {k}")) - # Test setting explicit fee rate just below the minimum and at zero. + # Test setting explicit fee rate just below the minimum of 1 sat/vB. self.log.info("Explicit fee rate raises RPC error 'fee rate too low' if fee_rate of 0.99999999 is passed") - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=0.99999999, - expect_error=(-4, "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)")) - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=0.99999999, - expect_error=(-4, "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)")) - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=0, - expect_error=(-4, "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)")) - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=0, - expect_error=(-4, "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)")) + msg = "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)" + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=0.999, expect_error=(-4, msg)) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=0.999, expect_error=(-4, msg)) + + self.log.info("Explicit fee rate raises if invalid fee_rate is passed") + # Test fee_rate with zero values. + msg = "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)" + for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=zero_value, expect_error=(-4, msg)) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=zero_value, expect_error=(-4, msg)) + msg = "Invalid amount" + # Test fee_rate values that don't pass fixed-point parsing checks. + for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=invalid_value, expect_error=(-3, msg)) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=invalid_value, expect_error=(-3, msg)) + # Test fee_rate values that cannot be represented in sat/vB. + for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=invalid_value, expect_error=(-3, msg)) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=invalid_value, expect_error=(-3, msg)) + # Test fee_rate out of range (negative number). + msg = "Amount out of range" + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=-1, expect_error=(-3, msg)) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=-1, expect_error=(-3, msg)) + # Test type error. + msg = "Amount is not a number or string" + for invalid_value in [True, {"foo": "bar"}]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=invalid_value, expect_error=(-3, msg)) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=invalid_value, expect_error=(-3, msg)) # TODO: Return hex if fee rate is below -maxmempool # res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b", add_to_wallet=False) @@ -389,10 +420,10 @@ class WalletSendTest(BitcoinTestFramework): assert res["complete"] res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, change_address=change_address, change_position=0) assert res["complete"] - assert_equal(self.nodes[0].decodepsbt(res["psbt"])["tx"]["vout"][0]["scriptPubKey"]["addresses"], [change_address]) + assert_equal(self.nodes[0].decodepsbt(res["psbt"])["tx"]["vout"][0]["scriptPubKey"]["address"], change_address) res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, add_to_wallet=False, change_type="legacy", change_position=0) assert res["complete"] - change_address = self.nodes[0].decodepsbt(res["psbt"])["tx"]["vout"][0]["scriptPubKey"]["addresses"][0] + change_address = self.nodes[0].decodepsbt(res["psbt"])["tx"]["vout"][0]["scriptPubKey"]["address"] assert change_address[0] == "m" or change_address[0] == "n" self.log.info("Set lock time...") @@ -407,16 +438,15 @@ class WalletSendTest(BitcoinTestFramework): assert not res[0]["allowed"] assert_equal(res[0]["reject-reason"], "non-final") # It shouldn't be confirmed in the next block - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) assert_equal(self.nodes[0].gettransaction(txid)["confirmations"], 0) # The mempool should allow it now: res = self.nodes[0].testmempoolaccept([hex]) assert res[0]["allowed"] # Don't wait for wallet to add it to the mempool: res = self.nodes[0].sendrawtransaction(hex) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) assert_equal(self.nodes[0].gettransaction(txid)["confirmations"], 1) - self.sync_all() self.log.info("Lock unspents...") utxo1 = w0.listunspent()[0] @@ -440,6 +470,94 @@ class WalletSendTest(BitcoinTestFramework): self.log.info("Subtract fee from output") self.test_send(from_wallet=w0, to_wallet=w1, amount=1, subtract_fee_from_outputs=[0]) + self.log.info("Include unsafe inputs") + self.nodes[1].createwallet(wallet_name="w5") + w5 = self.nodes[1].get_wallet_rpc("w5") + self.test_send(from_wallet=w0, to_wallet=w5, amount=2) + self.test_send(from_wallet=w5, to_wallet=w0, amount=1, expect_error=(-4, "Insufficient funds")) + res = self.test_send(from_wallet=w5, to_wallet=w0, amount=1, include_unsafe=True) + assert res["complete"] + + self.log.info("External outputs") + eckey = ECKey() + eckey.generate() + privkey = bytes_to_wif(eckey.get_bytes()) + + self.nodes[1].createwallet("extsend") + ext_wallet = self.nodes[1].get_wallet_rpc("extsend") + self.nodes[1].createwallet("extfund") + ext_fund = self.nodes[1].get_wallet_rpc("extfund") + + # Make a weird but signable script. sh(pkh()) descriptor accomplishes this + desc = descsum_create("sh(pkh({}))".format(privkey)) + if self.options.descriptors: + res = ext_fund.importdescriptors([{"desc": desc, "timestamp": "now"}]) + else: + res = ext_fund.importmulti([{"desc": desc, "timestamp": "now"}]) + assert res[0]["success"] + addr = self.nodes[0].deriveaddresses(desc)[0] + addr_info = ext_fund.getaddressinfo(addr) + + self.nodes[0].sendtoaddress(addr, 10) + self.nodes[0].sendtoaddress(ext_wallet.getnewaddress(), 10) + self.generate(self.nodes[0], 6) + ext_utxo = ext_fund.listunspent(addresses=[addr])[0] + + # An external input without solving data should result in an error + self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, expect_error=(-4, "Insufficient funds")) + + # But funding should work when the solving data is provided + res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}) + signed = ext_wallet.walletprocesspsbt(res["psbt"]) + signed = ext_fund.walletprocesspsbt(res["psbt"]) + assert signed["complete"] + self.nodes[0].finalizepsbt(signed["psbt"]) + + res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"descriptors": [desc]}) + signed = ext_wallet.walletprocesspsbt(res["psbt"]) + signed = ext_fund.walletprocesspsbt(res["psbt"]) + assert signed["complete"] + self.nodes[0].finalizepsbt(signed["psbt"]) + + dec = self.nodes[0].decodepsbt(signed["psbt"]) + for i, txin in enumerate(dec["tx"]["vin"]): + if txin["txid"] == ext_utxo["txid"] and txin["vout"] == ext_utxo["vout"]: + input_idx = i + break + psbt_in = dec["inputs"][input_idx] + # Calculate the input weight + # (prevout + sequence + length of scriptSig + 2 bytes buffer) * 4 + len of scriptwitness + len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0 + len_scriptwitness = len(psbt_in["final_scriptwitness"]["hex"]) // 2 if "final_scriptwitness" in psbt_in else 0 + input_weight = ((41 + len_scriptsig + 2) * 4) + len_scriptwitness + + # Input weight error conditions + assert_raises_rpc_error( + -8, + "Input weights should be specified in inputs rather than in options.", + ext_wallet.send, + outputs={self.nodes[0].getnewaddress(): 15}, + options={"inputs": [ext_utxo], "input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 1000}]} + ) + + # Funding should also work when input weights are provided + res = self.test_send( + from_wallet=ext_wallet, + to_wallet=self.nodes[0], + amount=15, + inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}], + add_inputs=True, + psbt=True, + include_watching=True, + fee_rate=10 + ) + signed = ext_wallet.walletprocesspsbt(res["psbt"]) + signed = ext_fund.walletprocesspsbt(res["psbt"]) + assert signed["complete"] + tx = self.nodes[0].finalizepsbt(signed["psbt"]) + testres = self.nodes[0].testmempoolaccept([tx["hex"]])[0] + assert_equal(testres["allowed"], True) + assert_fee_amount(testres["fees"]["base"], testres["vsize"], Decimal(0.0001)) if __name__ == '__main__': WalletSendTest().main() diff --git a/test/functional/wallet_signer.py b/test/functional/wallet_signer.py index 9dd080dca9..423cfecdc0 100755 --- a/test/functional/wallet_signer.py +++ b/test/functional/wallet_signer.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2018 The Bitcoin Core developers +# Copyright (c) 2017-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test external signer. Verify that a bitcoind node can use an external signer command +See also rpc_signer.py for tests without wallet context. """ import os import platform @@ -16,7 +17,7 @@ from test_framework.util import ( ) -class SignerTest(BitcoinTestFramework): +class WalletSignerTest(BitcoinTestFramework): def mock_signer_path(self): path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'signer.py') if platform.system() == "Windows": @@ -24,19 +25,27 @@ class SignerTest(BitcoinTestFramework): else: return path + def mock_invalid_signer_path(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'invalid_signer.py') + if platform.system() == "Windows": + return "py " + path + else: + return path + def set_test_params(self): - self.num_nodes = 4 + self.num_nodes = 2 + # The experimental syscall sandbox feature (-sandbox) is not compatible with -signer (which + # invokes execve). + self.disable_syscall_sandbox = True self.extra_args = [ [], [f"-signer={self.mock_signer_path()}", '-keypool=10'], - [f"-signer={self.mock_signer_path()}", '-keypool=10'], - ["-signer=fake.py"], ] def skip_test_if_missing_module(self): - self.skip_if_no_wallet() self.skip_if_no_external_signer() + self.skip_if_no_wallet() def set_mock_result(self, node, res): with open(os.path.join(node.cwd, "mock_result"), "w", encoding="utf8") as f: @@ -46,29 +55,12 @@ class SignerTest(BitcoinTestFramework): os.remove(os.path.join(node.cwd, "mock_result")) def run_test(self): - self.log.debug(f"-signer={self.mock_signer_path()}") - - assert_raises_rpc_error(-4, 'Error: restart bitcoind with -signer=<cmd>', - self.nodes[0].enumeratesigners - ) - - # Handle script missing: - assert_raises_rpc_error(-1, 'execve failed: No such file or directory', - self.nodes[3].enumeratesigners - ) - - # Handle error thrown by script - self.set_mock_result(self.nodes[1], "2") - assert_raises_rpc_error(-1, 'RunCommandParseJSON error', - self.nodes[1].enumeratesigners - ) - self.clear_mock_result(self.nodes[1]) + self.test_valid_signer() + self.restart_node(1, [f"-signer={self.mock_invalid_signer_path()}", "-keypool=10"]) + self.test_invalid_signer() - self.set_mock_result(self.nodes[1], '0 [{"type": "trezor", "model": "trezor_t", "error": "fingerprint not found"}]') - assert_raises_rpc_error(-4, 'fingerprint not found', - self.nodes[1].enumeratesigners - ) - self.clear_mock_result(self.nodes[1]) + def test_valid_signer(self): + self.log.debug(f"-signer={self.mock_signer_path()}") # Create new wallets for an external signer. # disable_private_keys and descriptors must be true: @@ -80,15 +72,12 @@ class SignerTest(BitcoinTestFramework): self.nodes[1].createwallet(wallet_name='hww', disable_private_keys=True, descriptors=True, external_signer=True) hww = self.nodes[1].get_wallet_rpc('hww') - - result = hww.enumeratesigners() - assert_equal(len(result['signers']), 2) - assert_equal(result['signers'][0]["fingerprint"], "00000001") - assert_equal(result['signers'][0]["name"], "trezor_t") + assert_equal(hww.getwalletinfo()["external_signer"], True) # Flag can't be set afterwards (could be added later for non-blank descriptor based watch-only wallets) self.nodes[1].createwallet(wallet_name='not_hww', disable_private_keys=True, descriptors=True, external_signer=False) not_hww = self.nodes[1].get_wallet_rpc('not_hww') + assert_equal(not_hww.getwalletinfo()["external_signer"], False) assert_raises_rpc_error(-8, "Wallet flag is immutable: external_signer", not_hww.setwalletflag, "external_signer", True) # assert_raises_rpc_error(-4, "Multiple signers found, please specify which to use", wallet_name='not_hww', disable_private_keys=True, descriptors=True, external_signer=True) @@ -123,21 +112,20 @@ class SignerTest(BitcoinTestFramework): assert_equal(address_info['ismine'], True) assert_equal(address_info['hdkeypath'], "m/44'/1'/0'/0/0") - self.log.info('Test signerdisplayaddress') - result = hww.signerdisplayaddress(address1) + self.log.info('Test walletdisplayaddress') + result = hww.walletdisplayaddress(address1) assert_equal(result, {"address": address1}) # Handle error thrown by script self.set_mock_result(self.nodes[1], "2") assert_raises_rpc_error(-1, 'RunCommandParseJSON error', - hww.signerdisplayaddress, address1 + hww.walletdisplayaddress, address1 ) self.clear_mock_result(self.nodes[1]) self.log.info('Prepare mock PSBT') self.nodes[0].sendtoaddress(address1, 1) - self.nodes[0].generate(1) - self.sync_all() + self.generate(self.nodes[0], 1) # Load private key into wallet to generate a signed PSBT for the mock self.nodes[1].createwallet(wallet_name="mock", disable_private_keys=False, blank=True, descriptors=True) @@ -213,5 +201,10 @@ class SignerTest(BitcoinTestFramework): # ) # self.clear_mock_result(self.nodes[4]) + def test_invalid_signer(self): + self.log.debug(f"-signer={self.mock_invalid_signer_path()}") + self.log.info('Test invalid external signer') + assert_raises_rpc_error(-1, "Invalid descriptor", self.nodes[1].createwallet, wallet_name='hww_invalid', disable_private_keys=True, descriptors=True, external_signer=True) + if __name__ == '__main__': - SignerTest().main() + WalletSignerTest().main() diff --git a/test/functional/wallet_signmessagewithaddress.py b/test/functional/wallet_signmessagewithaddress.py new file mode 100755 index 0000000000..74a8f2eef2 --- /dev/null +++ b/test/functional/wallet_signmessagewithaddress.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test Wallet commands for signing and verifying messages.""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_raises_rpc_error, +) + +class SignMessagesWithAddressTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [["-addresstype=legacy"]] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + message = 'This is just a test message' + + self.log.info('test signing with an address with wallet') + address = self.nodes[0].getnewaddress() + signature = self.nodes[0].signmessage(address, message) + assert self.nodes[0].verifymessage(address, signature, message) + + self.log.info('test verifying with another address should not work') + other_address = self.nodes[0].getnewaddress() + other_signature = self.nodes[0].signmessage(other_address, message) + assert not self.nodes[0].verifymessage(other_address, signature, message) + assert not self.nodes[0].verifymessage(address, other_signature, message) + + self.log.info('test parameter validity and error codes') + # signmessage has two required parameters + for num_params in [0, 1, 3, 4, 5]: + param_list = ["dummy"]*num_params + assert_raises_rpc_error(-1, "signmessage", self.nodes[0].signmessage, *param_list) + # invalid key or address provided + assert_raises_rpc_error(-5, "Invalid address", self.nodes[0].signmessage, "invalid_addr", message) + + +if __name__ == '__main__': + SignMessagesWithAddressTest().main() diff --git a/test/functional/wallet_taproot.py b/test/functional/wallet_taproot.py new file mode 100755 index 0000000000..17eab25457 --- /dev/null +++ b/test/functional/wallet_taproot.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test generation and spending of P2TR addresses.""" + +import random + +from decimal import Decimal +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal +from test_framework.descriptors import descsum_create +from test_framework.script import ( + CScript, + OP_1, + OP_CHECKSIG, + taproot_construct, +) +from test_framework.segwit_addr import encode_segwit_address + +# xprvs/xpubs, and m/* derived x-only pubkeys (created using independent implementation) +KEYS = [ + { + "xprv": "tprv8ZgxMBicQKsPeNLUGrbv3b7qhUk1LQJZAGMuk9gVuKh9sd4BWGp1eMsehUni6qGb8bjkdwBxCbgNGdh2bYGACK5C5dRTaif9KBKGVnSezxV", + "xpub": "tpubD6NzVbkrYhZ4XqNGAWGWSzmxGWFwVjVTjZxh2fioKbVYi7Jx8fdbprVWsdW7mHwqjchBVas8TLZG4Xwuz4RKU4iaCqiCvoSkFCzQptqk5Y1", + "pubs": [ + "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18", + "a30253b018ea6fca966135bf7dd8026915427f24ccf10d4e03f7870f4128569b", + "a61e5749f2f3db9dc871d7b187e30bfd3297eea2557e9be99897ea8ff7a29a21", + "8110cf482f66dc37125e619d73075af932521724ffc7108309e88f361efe8c8a", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPe98QUPieXy5KFPVjuZNpcC9JY7K7buJEm8nWvJogK4kTda7eLjK9U4PnMNbSjEkpjDJazeBZ4rhYNYD7N6GEdaysj1AYSb5", + "xpub": "tpubD6NzVbkrYhZ4XcACN3PEwNjRpR1g4tZjBVk5pdMR2B6dbd3HYhdGVZNKofAiFZd9okBserZvv58A6tBX4pE64UpXGNTSesfUW7PpW36HuKz", + "pubs": [ + "f95886b02a84928c5c15bdca32784993105f73de27fa6ad8c1a60389b999267c", + "71522134160685eb779857033bfc84c7626f13556154653a51dd42619064e679", + "48957b4158b2c5c3f4c000f51fd2cf0fd5ff8868ebfb194256f5e9131fc74bd8", + "086dda8139b3a84944010648d2b674b70447be3ae59322c09a4907bc80be62c1", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPe3ZJmcj9aJ2EPZJYYCh6Lp3v82p75wspgaXmtDZ2RBtkAtWcGnW2VQDzMHQPBkCKMoYTqh1RfJKjv4PcmWVR7KqTpjsdboN", + "xpub": "tpubD6NzVbkrYhZ4XWb6fGPjyhgLxapUhXszv7ehQYrQWDgDX4nYWcNcbgWcM2RhYo9s2mbZcfZJ8t5LzYcr24FK79zVybsw5Qj3Rtqug8jpJMy", + "pubs": [ + "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "8a104c54cd34acba60c97dd8f1f7abc89ba9587afd88dc928e91aca7b1c50d20", + "13ba6b252a4eb5ef31d39cb521724cdab19a698323f5c17093f28fb1821d052f", + "f6c2b4863fd5ba1ba09e3a890caed8b75ffbe013ebab31a06ab87cd6f72506af", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPdKziibn63Rm6aNzp7dSjDnufZMStXr71Huz7iihCRpbZZZ6Voy5HyuHCWx6foHMipzMzUq4tZrtkZ24DJwz5EeNWdsuwX5h", + "xpub": "tpubD6NzVbkrYhZ4Wo2WcFSgSqRD9QWkGxddo6WSqsVBx7uQ8QEtM7WncKDRjhFEexK119NigyCsFygA4b7sAPQxqebyFGAZ9XVV1BtcgNzbCRR", + "pubs": [ + "03a669ea926f381582ec4a000b9472ba8a17347f5fb159eddd4a07036a6718eb", + "bbf56b14b119bccafb686adec2e3d2a6b51b1626213590c3afa815d1fd36f85d", + "2994519e31bbc238a07d82f85c9832b831705d2ee4a2dbb477ecec8a3f570fe5", + "68991b5c139a4c479f8c89d6254d288c533aefc0c5b91fac6c89019c4de64988", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPen4PGtDwURYnCtVMDejyE8vVwMGhQWfVqB2FBPdekhTacDW4vmsKTsgC1wsncVqXiZdX2YFGAnKoLXYf42M78fQJFzuDYFN", + "xpub": "tpubD6NzVbkrYhZ4YF6BAXtXsqCtmv1HNyvsoSXHDsJzpnTtffH1onTEwC5SnLzCHPKPebh2i7Gxvi9kJNADcpuSmH8oM3rCYcHVtdXHjpYoKnX", + "pubs": [ + "aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247", + "c8558b7caf198e892032d91f1a48ee9bdc25462b83b4d0ac62bb7fb2a0df630e", + "8a4bcaba0e970685858d133a4d0079c8b55bbc755599e212285691eb779ce3dc", + "b0d68ada13e0d954b3921b88160d4453e9c151131c2b7c724e08f538a666ceb3", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPd91vCgRmbzA13wyip2RimYeVEkAyZvsEN5pUSB3T43SEBxPsytkxb42d64W2EiRE9CewpJQkzR8HKHLV8Uhk4dMF5yRPaTv", + "xpub": "tpubD6NzVbkrYhZ4Wc3i6L6N1Pp7cyVeyMcdLrFGXGDGzCfdCa5F4Zs3EY46N72Ws8QDEUYBVwXfDfda2UKSseSdU1fsBegJBhGCZyxkf28bkQ6", + "pubs": [ + "9b4d495b74887815a1ff623c055c6eac6b6b2e07d2a016d6526ebac71dd99744", + "8e971b781b7ce7ab742d80278f2dfe7dd330f3efd6d00047f4a2071f2e7553cb", + "b811d66739b9f07435ccda907ec5cd225355321c35e0a7c7791232f24cf10632", + "4cd27a5552c272bc80ba544e9cc6340bb906969f5e7a1510b6cef9592683fbc9", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPdEhLRxxwzTv2t18j7ruoffPeqAwVA2qXJ2P66RaMZLUWQ85SjoA7xPxdSgCB9UZ72m65qbnaLPtFTfHVP3MEmkpZk1Bv8RT", + "xpub": "tpubD6NzVbkrYhZ4Whj8KcdYPsa9T2efHC6iExzS7gynaJdv8WdripPwjq6NaH5gQJGrLmvUwHY1smhiakUosXNDTEa6qfKUQdLKV6DJBre6XvQ", + "pubs": [ + "d0c19def28bb1b39451c1a814737615983967780d223b79969ba692182c6006b", + "cb1d1b1dc62fec1894d4c3d9a1b6738e5ff9c273a64f74e9ab363095f45e9c47", + "245be588f41acfaeb9481aa132717db56ee1e23eb289729fe2b8bde8f9a00830", + "5bc4ad6d6187fa82728c85a073b428483295288f8aef5722e47305b5872f7169", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPcxbqxzcMAwQpiCD8x6qaZEJTxdKxw4w9GuMzDACTD9yhEsHGfqQcfYX4LivosLDDngTykYEp9JnTdcqY7cHqU8PpeFFKyV3", + "xpub": "tpubD6NzVbkrYhZ4WRddreGwaM4wHDj57S2V8XuFF9NGMLjY7PckqZ23PebZR1wGA4w84uX2vZphdZVsnREjij1ibYjEBTaTVQCEZCLs4xUDapx", + "pubs": [ + "065cc1b92bd99e5a3e626e8296a366b2d132688eb43aea19bc14fd8f43bf07fb", + "5b95633a7dda34578b6985e6bfd85d83ec38b7ded892a9b74a3d899c85890562", + "dc86d434b9a34495c8e845b969d51f80d19a8df03b400353ffe8036a0c22eb60", + "06c8ffde238745b29ae8a97ae533e1f3edf214bba6ec58b5e7b9451d1d61ec19", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPe6zLoU8MTTXgsdJVNBErrYGpoGwHf5VGvwUzdNc7NHeCSzkJkniCxBhZWujXjmD4HZmBBrnr3URgJjM6GxRgMmEhLdqNTWG", + "xpub": "tpubD6NzVbkrYhZ4Xa28h7nwrsBoSepRXWRmRqsc5nyb5MHfmRjmFmRhYnG4d9dC7uxixN5AfsEv1Lz3mCAuWvERyvPgKozHUVjfo8EG6foJGy7", + "pubs": [ + "d826a0a53abb6ffc60df25b9c152870578faef4b2eb5a09bdd672bbe32cdd79b", + "939365e0359ff6bc6f6404ee220714c5d4a0d1e36838b9e2081ede217674e2ba", + "4e8767edcf7d3d90258cfbbea01b784f4d2de813c4277b51279cf808bac410a2", + "d42a2c280940bfc6ede971ae72cde2e1df96c6da7dab06a132900c6751ade208", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPeB5o5oCsN2dVxM2mtJiYERQEBRc4JNwC1DFGYaEdNkmh8jJYVPU76YhkFoRoWTdh1p3yQGykG8TfDW34dKgrgSx28gswUyL", + "xpub": "tpubD6NzVbkrYhZ4Xe7aySsTmSHcXNYi3duSoj11TweMiejaqhW3Ay4DZFPZJses4sfpk4b9VHRhn8v4cKTMjugMM3hqXcqSSmRdiW8QvASXjfY", + "pubs": [ + "e360564b2e0e8d06681b6336a29d0750210e8f34afd9afb5e6fd5fe6dba26c81", + "76b4900f00a1dcce463b6d8e02b768518fce4f9ecd6679a13ad78ea1e4815ad3", + "5575556e263c8ed52e99ab02147cc05a738869afe0039911b5a60a780f4e43d2", + "593b00e2c8d4bd6dda0fd9e238888acf427bb4e128887fd5a40e0e9da78cbc01", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPfEH6jHemkGDjZRnAaKFJVGH8pQU638E6SdbX9hxit1tK2sfFPfL6KS7v8FfUKxstbfEpzSymbdfBM9Y5UkrxErF9fJaKLK3", + "xpub": "tpubD6NzVbkrYhZ4YhJtcwKN9fsr8TJ6jeSD4Zsv6vWPTQ2VH7rHn6nK4WWBCzKK7FkdVVwm3iztCU1UmStY4hX6gRbBmp9UzK9C59dQEzeXS12", + "pubs": [ + "7631cacec3343052d87ef4d0065f61dde82d7d2db0c1cc02ef61ef3c982ea763", + "c05e44a9e735d1b1bef62e2c0d886e6fb4923b2649b67828290f5cacc51c71b7", + "b33198b20701afe933226c92fd0e3d51d3f266f1113d864dbd026ae3166ef7f2", + "f99643ac3f4072ee4a949301e86963a9ca0ad57f2ef29f6b84fda037d7cac85b", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPdNWU38dT6aGxtqJR4oYS5kPpLVBcuKiiu7gqTYqMMqhUG6DP7pPahzPQu36sWSmeLCP1C4AwqcR5FX2RyRoZfd4B8pAnSdX", + "xpub": "tpubD6NzVbkrYhZ4WqYFvnJ3Vyw5TrpME8jLf3zbd1DvKbX7jbwc5wewYLKLSFRzZWV6hZj7XhsXAy7fhE5jB25DiWyNM3ztXbsXHRVCrp5BiPY", + "pubs": [ + "2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7", + "83df59d0a5c951cdd62b7ab225a62079f48d2a333a86e66c35420d101446e92e", + "2a654bf234d819055312f9ca03fad5836f9163b09cdd24d29678f694842b874a", + "aa0334ab910047387c912a21ec0dab806a47ffa38365060dbc5d47c18c6e66e7", + ] + }, + { + "xprv": "tprv8mGPkMVz5mZuJDnC2NjjAv7E9Zqa5LCgX4zawbZu5nzTtLb5kGhPwycX4H1gtW1f5ZdTKTNtQJ61hk71F2TdcQ93EFDTpUcPBr98QRji615", + "xpub": "tpubDHxRtmYEE9FaBgoyv2QKaKmLibMWEfPb6NbNE7cCW4nripqrNfWz8UEPEPbHCrakwLvwFfsqoaf4pjX4gWStp4nECRf1QwBKPkLqnY8pHbj", + "pubs": [ + "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d3", + "b2749b74d51a78f5fe3ebb3a7c0ff266a468cade143dfa265c57e325177edf00", + "6b8747a6bbe4440d7386658476da51f6e49a220508a7ec77fe7bccc3e7baa916", + "4674bf4d9ebbe01bf0aceaca2472f63198655ecf2df810f8d69b38421972318e", + ] + } +] + +CHANGE_XPRV = "tprv8ZgxMBicQKsPcyDrWwiecVnTtFmfRwbfFqEfR4ZGWvq5aTTwLBWmAm5zrbMcYtb9gQNFfhRfqhhrBG37U3nhmXxEgeEPBJGHAPrHCrAd1WX" +CHANGE_XPUB = "tpubD6NzVbkrYhZ4WSFeQbPF1uSaTHHbbGnZq8qShabZwCdUQwihxaLMMFhs2kidGF2qrRKiQVqw8VoyuTHj1bZqmMXMeciaU1gBjWA1sim2zUB" + +# Point with no known discrete log. +H_POINT = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + + +def key(hex_key): + """Construct an x-only pubkey from its hex representation.""" + return bytes.fromhex(hex_key) + +def pk(hex_key): + """Construct a script expression for taproot_construct for pk(hex_key).""" + return (None, CScript([bytes.fromhex(hex_key), OP_CHECKSIG])) + +def compute_taproot_address(pubkey, scripts): + """Compute the address for a taproot output with given inner key and scripts.""" + tap = taproot_construct(pubkey, scripts) + assert tap.scriptPubKey[0] == OP_1 + assert tap.scriptPubKey[1] == 0x20 + return encode_segwit_address("bcrt", 1, tap.scriptPubKey[2:]) + +class WalletTaprootTest(BitcoinTestFramework): + """Test generation and spending of P2TR address outputs.""" + + def set_test_params(self): + self.num_nodes = 3 + self.setup_clean_chain = True + self.extra_args = [['-keypool=100'], ['-keypool=100'], ["-vbparams=taproot:1:1"]] + self.supports_cli = False + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + self.skip_if_no_sqlite() + + def setup_network(self): + self.setup_nodes() + + def init_wallet(self, *, node): + pass + + @staticmethod + def rand_keys(n): + ret = [] + idxes = set() + for _ in range(n): + while True: + i = random.randrange(len(KEYS)) + if not i in idxes: + break + idxes.add(i) + ret.append(KEYS[i]) + return ret + + @staticmethod + def make_desc(pattern, privmap, keys, pub_only = False): + pat = pattern.replace("$H", H_POINT) + for i in range(len(privmap)): + if privmap[i] and not pub_only: + pat = pat.replace("$%i" % (i + 1), keys[i]['xprv']) + else: + pat = pat.replace("$%i" % (i + 1), keys[i]['xpub']) + return descsum_create(pat) + + @staticmethod + def make_addr(treefn, keys, i): + args = [] + for j in range(len(keys)): + args.append(keys[j]['pubs'][i]) + return compute_taproot_address(*treefn(*args)) + + def do_test_addr(self, comment, pattern, privmap, treefn, keys): + self.log.info("Testing %s address derivation" % comment) + desc = self.make_desc(pattern, privmap, keys, False) + desc_pub = self.make_desc(pattern, privmap, keys, True) + assert_equal(self.nodes[0].getdescriptorinfo(desc)['descriptor'], desc_pub) + result = self.addr_gen.importdescriptors([{"desc": desc_pub, "active": True, "timestamp": "now"}]) + assert(result[0]['success']) + for i in range(4): + addr_g = self.addr_gen.getnewaddress(address_type='bech32m') + if treefn is not None: + addr_r = self.make_addr(treefn, keys, i) + assert_equal(addr_g, addr_r) + desc_a = self.addr_gen.getaddressinfo(addr_g)['desc'] + if desc.startswith("tr("): + assert desc_a.startswith("tr(") + rederive = self.nodes[1].deriveaddresses(desc_a) + assert_equal(len(rederive), 1) + assert_equal(rederive[0], addr_g) + + # tr descriptors can be imported regardless of Taproot status + result = self.privs_tr_enabled.importdescriptors([{"desc": desc, "timestamp": "now"}]) + assert(result[0]["success"]) + result = self.pubs_tr_enabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}]) + assert(result[0]["success"]) + result = self.privs_tr_disabled.importdescriptors([{"desc": desc, "timestamp": "now"}]) + assert result[0]["success"] + result = self.pubs_tr_disabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}]) + assert result[0]["success"] + + def do_test_sendtoaddress(self, comment, pattern, privmap, treefn, keys_pay, keys_change): + self.log.info("Testing %s through sendtoaddress" % comment) + desc_pay = self.make_desc(pattern, privmap, keys_pay) + desc_change = self.make_desc(pattern, privmap, keys_change) + desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True) + desc_change_pub = self.make_desc(pattern, privmap, keys_change, True) + assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub) + assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub) + result = self.rpc_online.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}]) + assert(result[0]['success']) + result = self.rpc_online.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}]) + assert(result[0]['success']) + for i in range(4): + addr_g = self.rpc_online.getnewaddress(address_type='bech32m') + if treefn is not None: + addr_r = self.make_addr(treefn, keys_pay, i) + assert_equal(addr_g, addr_r) + boring_balance = int(self.boring.getbalance() * 100000000) + to_amnt = random.randrange(1000000, boring_balance) + self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True) + self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op) + test_balance = int(self.rpc_online.getbalance() * 100000000) + ret_amnt = random.randrange(100000, test_balance) + res = self.rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True) + self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op) + assert(self.rpc_online.gettransaction(res)["confirmations"] > 0) + + def do_test_psbt(self, comment, pattern, privmap, treefn, keys_pay, keys_change): + self.log.info("Testing %s through PSBT" % comment) + desc_pay = self.make_desc(pattern, privmap, keys_pay, False) + desc_change = self.make_desc(pattern, privmap, keys_change, False) + desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True) + desc_change_pub = self.make_desc(pattern, privmap, keys_change, True) + assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub) + assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub) + result = self.psbt_online.importdescriptors([{"desc": desc_pay_pub, "active": True, "timestamp": "now"}]) + assert(result[0]['success']) + result = self.psbt_online.importdescriptors([{"desc": desc_change_pub, "active": True, "timestamp": "now", "internal": True}]) + assert(result[0]['success']) + result = self.psbt_offline.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}]) + assert(result[0]['success']) + result = self.psbt_offline.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}]) + assert(result[0]['success']) + for i in range(4): + addr_g = self.psbt_online.getnewaddress(address_type='bech32m') + if treefn is not None: + addr_r = self.make_addr(treefn, keys_pay, i) + assert_equal(addr_g, addr_r) + boring_balance = int(self.boring.getbalance() * 100000000) + to_amnt = random.randrange(1000000, boring_balance) + self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True) + self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op) + test_balance = int(self.psbt_online.getbalance() * 100000000) + ret_amnt = random.randrange(100000, test_balance) + psbt = self.psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0]})['psbt'] + res = self.psbt_offline.walletprocesspsbt(psbt) + assert(res['complete']) + rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex'] + txid = self.nodes[0].sendrawtransaction(rawtx) + self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op) + assert(self.psbt_online.gettransaction(txid)['confirmations'] > 0) + + def do_test(self, comment, pattern, privmap, treefn, nkeys): + keys = self.rand_keys(nkeys * 4) + self.do_test_addr(comment, pattern, privmap, treefn, keys[0:nkeys]) + self.do_test_sendtoaddress(comment, pattern, privmap, treefn, keys[0:nkeys], keys[nkeys:2*nkeys]) + self.do_test_psbt(comment, pattern, privmap, treefn, keys[2*nkeys:3*nkeys], keys[3*nkeys:4*nkeys]) + + def run_test(self): + self.log.info("Creating wallets...") + self.nodes[0].createwallet(wallet_name="privs_tr_enabled", descriptors=True, blank=True) + self.privs_tr_enabled = self.nodes[0].get_wallet_rpc("privs_tr_enabled") + self.nodes[2].createwallet(wallet_name="privs_tr_disabled", descriptors=True, blank=True) + self.privs_tr_disabled=self.nodes[2].get_wallet_rpc("privs_tr_disabled") + self.nodes[0].createwallet(wallet_name="pubs_tr_enabled", descriptors=True, blank=True, disable_private_keys=True) + self.pubs_tr_enabled = self.nodes[0].get_wallet_rpc("pubs_tr_enabled") + self.nodes[2].createwallet(wallet_name="pubs_tr_disabled", descriptors=True, blank=True, disable_private_keys=True) + self.pubs_tr_disabled=self.nodes[2].get_wallet_rpc("pubs_tr_disabled") + self.nodes[0].createwallet(wallet_name="boring") + self.nodes[0].createwallet(wallet_name="addr_gen", descriptors=True, disable_private_keys=True, blank=True) + self.nodes[0].createwallet(wallet_name="rpc_online", descriptors=True, blank=True) + self.nodes[0].createwallet(wallet_name="psbt_online", descriptors=True, disable_private_keys=True, blank=True) + self.nodes[1].createwallet(wallet_name="psbt_offline", descriptors=True, blank=True) + self.boring = self.nodes[0].get_wallet_rpc("boring") + self.addr_gen = self.nodes[0].get_wallet_rpc("addr_gen") + self.rpc_online = self.nodes[0].get_wallet_rpc("rpc_online") + self.psbt_online = self.nodes[0].get_wallet_rpc("psbt_online") + self.psbt_offline = self.nodes[1].get_wallet_rpc("psbt_offline") + + self.log.info("Mining blocks...") + gen_addr = self.boring.getnewaddress() + self.generatetoaddress(self.nodes[0], 101, gen_addr, sync_fun=self.no_op) + + self.do_test( + "tr(XPRV)", + "tr($1/*)", + [True], + lambda k1: (key(k1), []), + 1 + ) + self.do_test( + "tr(H,XPRV)", + "tr($H,pk($1/*))", + [True], + lambda k1: (key(H_POINT), [pk(k1)]), + 1 + ) + self.do_test( + "wpkh(XPRV)", + "wpkh($1/*)", + [True], + None, + 1 + ) + self.do_test( + "tr(XPRV,{H,{H,XPUB}})", + "tr($1/*,{pk($H),{pk($H),pk($2/*)}})", + [True, False], + lambda k1, k2: (key(k1), [pk(H_POINT), [pk(H_POINT), pk(k2)]]), + 2 + ) + self.do_test( + "wsh(multi(1,XPRV,XPUB))", + "wsh(multi(1,$1/*,$2/*))", + [True, False], + None, + 2 + ) + self.do_test( + "tr(XPRV,{XPUB,XPUB})", + "tr($1/*,{pk($2/*),pk($2/*)})", + [True, False], + lambda k1, k2: (key(k1), [pk(k2), pk(k2)]), + 2 + ) + self.do_test( + "tr(XPRV,{{XPUB,H},{H,XPUB}})", + "tr($1/*,{{pk($2/*),pk($H)},{pk($H),pk($2/*)}})", + [True, False], + lambda k1, k2: (key(k1), [[pk(k2), pk(H_POINT)], [pk(H_POINT), pk(k2)]]), + 2 + ) + self.do_test( + "tr(XPUB,{{H,{H,XPUB}},{H,{H,{H,XPRV}}}})", + "tr($1/*,{{pk($H),{pk($H),pk($2/*)}},{pk($H),{pk($H),{pk($H),pk($3/*)}}}})", + [False, False, True], + lambda k1, k2, k3: (key(k1), [[pk(H_POINT), [pk(H_POINT), pk(k2)]], [pk(H_POINT), [pk(H_POINT), [pk(H_POINT), pk(k3)]]]]), + 3 + ) + self.do_test( + "tr(XPRV,{XPUB,{{XPUB,{H,H}},{{H,H},XPUB}}})", + "tr($1/*,{pk($2/*),{{pk($2/*),{pk($H),pk($H)}},{{pk($H),pk($H)},pk($2/*)}}})", + [True, False], + lambda k1, k2: (key(k1), [pk(k2), [[pk(k2), [pk(H_POINT), pk(H_POINT)]], [[pk(H_POINT), pk(H_POINT)], pk(k2)]]]), + 2 + ) + + self.log.info("Sending everything back...") + + txid = self.rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=self.rpc_online.getbalance(), subtractfeefromamount=True) + self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op) + assert(self.rpc_online.gettransaction(txid)["confirmations"] > 0) + + psbt = self.psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): self.psbt_online.getbalance()}], None, {"subtractFeeFromOutputs": [0]})['psbt'] + res = self.psbt_offline.walletprocesspsbt(psbt) + assert(res['complete']) + rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex'] + txid = self.nodes[0].sendrawtransaction(rawtx) + self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op) + assert(self.psbt_online.gettransaction(txid)['confirmations'] > 0) + +if __name__ == '__main__': + WalletTaprootTest().main() diff --git a/test/functional/wallet_timelock.py b/test/functional/wallet_timelock.py new file mode 100755 index 0000000000..cf233a00ef --- /dev/null +++ b/test/functional/wallet_timelock.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + + +class WalletLocktimeTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + node = self.nodes[0] + + mtp_tip = node.getblockheader(node.getbestblockhash())["mediantime"] + + self.log.info("Get new address with label") + label = "timelock⌛🔓" + address = node.getnewaddress(label=label) + + self.log.info("Send to new address with locktime") + node.send( + outputs={address: 5}, + options={"locktime": mtp_tip - 1}, + ) + self.generate(node, 1) + + self.log.info("Check that clock can not change finality of confirmed txs") + amount_before_ad = node.getreceivedbyaddress(address) + amount_before_lb = node.getreceivedbylabel(label) + list_before_ad = node.listreceivedbyaddress(address_filter=address) + list_before_lb = node.listreceivedbylabel(include_empty=False) + balance_before = node.getbalances()["mine"]["trusted"] + coin_before = node.listunspent(maxconf=1) + node.setmocktime(mtp_tip - 1) + assert_equal(node.getreceivedbyaddress(address), amount_before_ad) + assert_equal(node.getreceivedbylabel(label), amount_before_lb) + assert_equal(node.listreceivedbyaddress(address_filter=address), list_before_ad) + assert_equal(node.listreceivedbylabel(include_empty=False), list_before_lb) + assert_equal(node.getbalances()["mine"]["trusted"], balance_before) + assert_equal(node.listunspent(maxconf=1), coin_before) + + +if __name__ == "__main__": + WalletLocktimeTest().main() diff --git a/test/functional/wallet_transactiontime_rescan.py b/test/functional/wallet_transactiontime_rescan.py new file mode 100755 index 0000000000..21941084a3 --- /dev/null +++ b/test/functional/wallet_transactiontime_rescan.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test transaction time during old block rescanning +""" + +import time + +from test_framework.blocktools import COINBASE_MATURITY +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + set_node_times, +) + + +class TransactionTimeRescanTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = False + self.num_nodes = 3 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + self.log.info('Prepare nodes and wallet') + + minernode = self.nodes[0] # node used to mine BTC and create transactions + usernode = self.nodes[1] # user node with correct time + restorenode = self.nodes[2] # node used to restore user wallet and check time determination in ComputeSmartTime (wallet.cpp) + + # time constant + cur_time = int(time.time()) + ten_days = 10 * 24 * 60 * 60 + + # synchronize nodes and time + self.sync_all() + set_node_times(self.nodes, cur_time) + + # prepare miner wallet + minernode.createwallet(wallet_name='default') + miner_wallet = minernode.get_wallet_rpc('default') + m1 = miner_wallet.getnewaddress() + + # prepare the user wallet with 3 watch only addresses + wo1 = usernode.getnewaddress() + wo2 = usernode.getnewaddress() + wo3 = usernode.getnewaddress() + + usernode.createwallet(wallet_name='wo', disable_private_keys=True) + wo_wallet = usernode.get_wallet_rpc('wo') + + wo_wallet.importaddress(wo1) + wo_wallet.importaddress(wo2) + wo_wallet.importaddress(wo3) + + self.log.info('Start transactions') + + # check blockcount + assert_equal(minernode.getblockcount(), 200) + + # generate some btc to create transactions and check blockcount + initial_mine = COINBASE_MATURITY + 1 + self.generatetoaddress(minernode, initial_mine, m1) + assert_equal(minernode.getblockcount(), initial_mine + 200) + + # synchronize nodes and time + self.sync_all() + set_node_times(self.nodes, cur_time + ten_days) + # send 10 btc to user's first watch-only address + self.log.info('Send 10 btc to user') + miner_wallet.sendtoaddress(wo1, 10) + + # generate blocks and check blockcount + self.generatetoaddress(minernode, COINBASE_MATURITY, m1) + assert_equal(minernode.getblockcount(), initial_mine + 300) + + # synchronize nodes and time + self.sync_all() + set_node_times(self.nodes, cur_time + ten_days + ten_days) + # send 5 btc to our second watch-only address + self.log.info('Send 5 btc to user') + miner_wallet.sendtoaddress(wo2, 5) + + # generate blocks and check blockcount + self.generatetoaddress(minernode, COINBASE_MATURITY, m1) + assert_equal(minernode.getblockcount(), initial_mine + 400) + + # synchronize nodes and time + self.sync_all() + set_node_times(self.nodes, cur_time + ten_days + ten_days + ten_days) + # send 1 btc to our third watch-only address + self.log.info('Send 1 btc to user') + miner_wallet.sendtoaddress(wo3, 1) + + # generate more blocks and check blockcount + self.generatetoaddress(minernode, COINBASE_MATURITY, m1) + assert_equal(minernode.getblockcount(), initial_mine + 500) + + self.log.info('Check user\'s final balance and transaction count') + assert_equal(wo_wallet.getbalance(), 16) + assert_equal(len(wo_wallet.listtransactions()), 3) + + self.log.info('Check transaction times') + for tx in wo_wallet.listtransactions(): + if tx['address'] == wo1: + assert_equal(tx['blocktime'], cur_time + ten_days) + assert_equal(tx['time'], cur_time + ten_days) + elif tx['address'] == wo2: + assert_equal(tx['blocktime'], cur_time + ten_days + ten_days) + assert_equal(tx['time'], cur_time + ten_days + ten_days) + elif tx['address'] == wo3: + assert_equal(tx['blocktime'], cur_time + ten_days + ten_days + ten_days) + assert_equal(tx['time'], cur_time + ten_days + ten_days + ten_days) + + # restore user wallet without rescan + self.log.info('Restore user wallet on another node without rescan') + restorenode.createwallet(wallet_name='wo', disable_private_keys=True) + restorewo_wallet = restorenode.get_wallet_rpc('wo') + + # for descriptor wallets, the test framework maps the importaddress RPC to the + # importdescriptors RPC (with argument 'timestamp'='now'), which always rescans + # blocks of the past 2 hours, based on the current MTP timestamp; in order to avoid + # importing the last address (wo3), we advance the time further and generate 10 blocks + if self.options.descriptors: + set_node_times(self.nodes, cur_time + ten_days + ten_days + ten_days + ten_days) + self.generatetoaddress(minernode, 10, m1) + + restorewo_wallet.importaddress(wo1, rescan=False) + restorewo_wallet.importaddress(wo2, rescan=False) + restorewo_wallet.importaddress(wo3, rescan=False) + + # check user has 0 balance and no transactions + assert_equal(restorewo_wallet.getbalance(), 0) + assert_equal(len(restorewo_wallet.listtransactions()), 0) + + # proceed to rescan, first with an incomplete one, then with a full rescan + self.log.info('Rescan last history part') + restorewo_wallet.rescanblockchain(initial_mine + 350) + self.log.info('Rescan all history') + restorewo_wallet.rescanblockchain() + + self.log.info('Check user\'s final balance and transaction count after restoration') + assert_equal(restorewo_wallet.getbalance(), 16) + assert_equal(len(restorewo_wallet.listtransactions()), 3) + + self.log.info('Check transaction times after restoration') + for tx in restorewo_wallet.listtransactions(): + if tx['address'] == wo1: + assert_equal(tx['blocktime'], cur_time + ten_days) + assert_equal(tx['time'], cur_time + ten_days) + elif tx['address'] == wo2: + assert_equal(tx['blocktime'], cur_time + ten_days + ten_days) + assert_equal(tx['time'], cur_time + ten_days + ten_days) + elif tx['address'] == wo3: + assert_equal(tx['blocktime'], cur_time + ten_days + ten_days + ten_days) + assert_equal(tx['time'], cur_time + ten_days + ten_days + ten_days) + + +if __name__ == '__main__': + TransactionTimeRescanTest().main() diff --git a/test/functional/wallet_txn_clone.py b/test/functional/wallet_txn_clone.py index 6fc1d13c53..5bdde13aa4 100755 --- a/test/functional/wallet_txn_clone.py +++ b/test/functional/wallet_txn_clone.py @@ -1,15 +1,18 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet accounts properly when there are cloned transactions with malleated scriptsigs.""" -import io from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + find_vout_for_address +) +from test_framework.messages import ( + COIN, + tx_from_hex, ) -from test_framework.messages import CTransaction, COIN class TxnMallTest(BitcoinTestFramework): @@ -31,6 +34,13 @@ class TxnMallTest(BitcoinTestFramework): super().setup_network() self.disconnect_nodes(1, 2) + def spend_txid(self, txid, vout, outputs): + inputs = [{"txid": txid, "vout": vout}] + tx = self.nodes[0].createrawtransaction(inputs, outputs) + tx = self.nodes[0].fundrawtransaction(tx) + tx = self.nodes[0].signrawtransactionwithwallet(tx['hex']) + return self.nodes[0].sendrawtransaction(tx['hex']) + def run_test(self): if self.options.segwit: output_type = "p2sh-segwit" @@ -47,6 +57,7 @@ class TxnMallTest(BitcoinTestFramework): node0_address1 = self.nodes[0].getnewaddress(address_type=output_type) node0_txid1 = self.nodes[0].sendtoaddress(node0_address1, 1219) node0_tx1 = self.nodes[0].gettransaction(node0_txid1) + self.nodes[0].lockunspent(False, [{"txid":node0_txid1, "vout": find_vout_for_address(self.nodes[0], node0_txid1, node0_address1)}]) node0_address2 = self.nodes[0].getnewaddress(address_type=output_type) node0_txid2 = self.nodes[0].sendtoaddress(node0_address2, 29) @@ -59,20 +70,19 @@ class TxnMallTest(BitcoinTestFramework): node1_address = self.nodes[1].getnewaddress() # Send tx1, and another transaction tx2 that won't be cloned - txid1 = self.nodes[0].sendtoaddress(node1_address, 40) - txid2 = self.nodes[0].sendtoaddress(node1_address, 20) + txid1 = self.spend_txid(node0_txid1, find_vout_for_address(self.nodes[0], node0_txid1, node0_address1), {node1_address: 40}) + txid2 = self.spend_txid(node0_txid2, find_vout_for_address(self.nodes[0], node0_txid2, node0_address2), {node1_address: 20}) # Construct a clone of tx1, to be malleated rawtx1 = self.nodes[0].getrawtransaction(txid1, 1) clone_inputs = [{"txid": rawtx1["vin"][0]["txid"], "vout": rawtx1["vin"][0]["vout"], "sequence": rawtx1["vin"][0]["sequence"]}] - clone_outputs = {rawtx1["vout"][0]["scriptPubKey"]["addresses"][0]: rawtx1["vout"][0]["value"], - rawtx1["vout"][1]["scriptPubKey"]["addresses"][0]: rawtx1["vout"][1]["value"]} + clone_outputs = {rawtx1["vout"][0]["scriptPubKey"]["address"]: rawtx1["vout"][0]["value"], + rawtx1["vout"][1]["scriptPubKey"]["address"]: rawtx1["vout"][1]["value"]} clone_locktime = rawtx1["locktime"] clone_raw = self.nodes[0].createrawtransaction(clone_inputs, clone_outputs, clone_locktime) # createrawtransaction randomizes the order of its outputs, so swap them if necessary. - clone_tx = CTransaction() - clone_tx.deserialize(io.BytesIO(bytes.fromhex(clone_raw))) + clone_tx = tx_from_hex(clone_raw) if (rawtx1["vout"][0]["value"] == 40 and clone_tx.vout[0].nValue != 40*COIN or rawtx1["vout"][0]["value"] != 40 and clone_tx.vout[0].nValue == 40*COIN): (clone_tx.vout[0], clone_tx.vout[1]) = (clone_tx.vout[1], clone_tx.vout[0]) @@ -83,8 +93,7 @@ class TxnMallTest(BitcoinTestFramework): # Have node0 mine a block, if requested: if (self.options.mine_block): - self.nodes[0].generate(1) - self.sync_blocks(self.nodes[0:2]) + self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:2])) tx1 = self.nodes[0].gettransaction(txid1) tx2 = self.nodes[0].gettransaction(txid2) @@ -113,14 +122,13 @@ class TxnMallTest(BitcoinTestFramework): return # ... mine a block... - self.nodes[2].generate(1) + self.generate(self.nodes[2], 1, sync_fun=self.no_op) # Reconnect the split network, and sync chain: self.connect_nodes(1, 2) self.nodes[2].sendrawtransaction(node0_tx2["hex"]) self.nodes[2].sendrawtransaction(tx2["hex"]) - self.nodes[2].generate(1) # Mine another block to make sure we sync - self.sync_blocks() + self.generate(self.nodes[2], 1) # Mine another block to make sure we sync # Re-fetch transaction info: tx1 = self.nodes[0].gettransaction(txid1) diff --git a/test/functional/wallet_txn_doublespend.py b/test/functional/wallet_txn_doublespend.py index 0cb7328948..206187fb61 100755 --- a/test/functional/wallet_txn_doublespend.py +++ b/test/functional/wallet_txn_doublespend.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2020 The Bitcoin Core developers +# Copyright (c) 2014-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet accounts properly when there is a double-spend conflict.""" @@ -9,6 +9,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, find_output, + find_vout_for_address ) @@ -29,6 +30,13 @@ class TxnMallTest(BitcoinTestFramework): super().setup_network() self.disconnect_nodes(1, 2) + def spend_txid(self, txid, vout, outputs): + inputs = [{"txid": txid, "vout": vout}] + tx = self.nodes[0].createrawtransaction(inputs, outputs) + tx = self.nodes[0].fundrawtransaction(tx) + tx = self.nodes[0].signrawtransactionwithwallet(tx['hex']) + return self.nodes[0].sendrawtransaction(tx['hex']) + def run_test(self): # All nodes should start with 1,250 BTC: starting_balance = 1250 @@ -47,6 +55,7 @@ class TxnMallTest(BitcoinTestFramework): node0_address_foo = self.nodes[0].getnewaddress() fund_foo_txid = self.nodes[0].sendtoaddress(node0_address_foo, 1219) fund_foo_tx = self.nodes[0].gettransaction(fund_foo_txid) + self.nodes[0].lockunspent(False, [{"txid":fund_foo_txid, "vout": find_vout_for_address(self.nodes[0], fund_foo_txid, node0_address_foo)}]) node0_address_bar = self.nodes[0].getnewaddress() fund_bar_txid = self.nodes[0].sendtoaddress(node0_address_bar, 29) @@ -77,13 +86,12 @@ class TxnMallTest(BitcoinTestFramework): assert_equal(doublespend["complete"], True) # Create two spends using 1 50 BTC coin each - txid1 = self.nodes[0].sendtoaddress(node1_address, 40) - txid2 = self.nodes[0].sendtoaddress(node1_address, 20) + txid1 = self.spend_txid(fund_foo_txid, find_vout_for_address(self.nodes[0], fund_foo_txid, node0_address_foo), {node1_address: 40}) + txid2 = self.spend_txid(fund_bar_txid, find_vout_for_address(self.nodes[0], fund_bar_txid, node0_address_bar), {node1_address: 20}) # Have node0 mine a block: if (self.options.mine_block): - self.nodes[0].generate(1) - self.sync_blocks(self.nodes[0:2]) + self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:2])) tx1 = self.nodes[0].gettransaction(txid1) tx2 = self.nodes[0].gettransaction(txid2) @@ -111,12 +119,11 @@ class TxnMallTest(BitcoinTestFramework): self.nodes[2].sendrawtransaction(fund_bar_tx["hex"]) doublespend_txid = self.nodes[2].sendrawtransaction(doublespend["hex"]) # ... mine a block... - self.nodes[2].generate(1) + self.generate(self.nodes[2], 1, sync_fun=self.no_op) # Reconnect the split network, and sync chain: self.connect_nodes(1, 2) - self.nodes[2].generate(1) # Mine another block to make sure we sync - self.sync_blocks() + self.generate(self.nodes[2], 1) # Mine another block to make sure we sync assert_equal(self.nodes[0].gettransaction(doublespend_txid)["confirmations"], 2) # Re-fetch transaction info: diff --git a/test/functional/wallet_upgradewallet.py b/test/functional/wallet_upgradewallet.py index fbc0f995d2..36e72f2dd9 100755 --- a/test/functional/wallet_upgradewallet.py +++ b/test/functional/wallet_upgradewallet.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2020 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """upgradewallet RPC functional test Test upgradewallet RPC. Download node binaries: -test/get_previous_releases.py -b v0.19.1 v0.18.1 v0.17.2 v0.16.3 v0.15.2 - -Only v0.15.2 and v0.16.3 are required by this test. The others are used in feature_backwards_compatibility.py +Requires previous releases binaries, see test/README.md. +Only v0.15.2 and v0.16.3 are required by this test. """ import os @@ -17,6 +16,7 @@ import struct from io import BytesIO +from test_framework.blocktools import COINBASE_MATURITY from test_framework.bdb import dump_bdb_kv from test_framework.messages import deser_compact_size, deser_string from test_framework.test_framework import BitcoinTestFramework @@ -94,10 +94,11 @@ class UpgradeWalletTest(BitcoinTestFramework): def test_upgradewallet(self, wallet, previous_version, requested_version=None, expected_version=None): unchanged = expected_version == previous_version new_version = previous_version if unchanged else expected_version if expected_version else requested_version - assert_equal(wallet.getwalletinfo()["walletversion"], previous_version) + old_wallet_info = wallet.getwalletinfo() + assert_equal(old_wallet_info["walletversion"], previous_version) assert_equal(wallet.upgradewallet(requested_version), { - "wallet_name": "", + "wallet_name": old_wallet_info["walletname"], "previous_version": previous_version, "current_version": new_version, "result": "Already at latest version. Wallet version unchanged." if unchanged else "Wallet upgraded successfully from version {} to version {}.".format(previous_version, new_version), @@ -118,11 +119,10 @@ class UpgradeWalletTest(BitcoinTestFramework): assert_equal(wallet.getwalletinfo()["walletversion"], previous_version) def run_test(self): - self.nodes[0].generatetoaddress(101, self.nodes[0].getnewaddress()) - self.dumb_sync_blocks() + self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, self.nodes[0].getnewaddress(), sync_fun=lambda: self.dumb_sync_blocks()) # # Sanity check the test framework: res = self.nodes[0].getblockchaininfo() - assert_equal(res['blocks'], 101) + assert_equal(res['blocks'], COINBASE_MATURITY + 1) node_master = self.nodes[0] v16_3_node = self.nodes[1] v15_2_node = self.nodes[2] @@ -130,8 +130,7 @@ class UpgradeWalletTest(BitcoinTestFramework): # Send coins to old wallets for later conversion checks. v16_3_wallet = v16_3_node.get_wallet_rpc('wallet.dat') v16_3_address = v16_3_wallet.getnewaddress() - node_master.generatetoaddress(101, v16_3_address) - self.dumb_sync_blocks() + self.generatetoaddress(node_master, COINBASE_MATURITY + 1, v16_3_address, sync_fun=lambda: self.dumb_sync_blocks()) v16_3_balance = v16_3_wallet.getbalance() self.log.info("Test upgradewallet RPC...") @@ -233,18 +232,13 @@ class UpgradeWalletTest(BitcoinTestFramework): assert_equal(1, hd_chain_version) seed_id = bytearray(seed_id) seed_id.reverse() - old_kvs = new_kvs - # First 2 keys should still be non-HD - for i in range(0, 2): - info = wallet.getaddressinfo(wallet.getnewaddress()) - assert 'hdkeypath' not in info - assert 'hdseedid' not in info - # Next key should be HD + + # New keys (including change) should be HD (the two old keys have been flushed) info = wallet.getaddressinfo(wallet.getnewaddress()) assert_equal(seed_id.hex(), info['hdseedid']) assert_equal('m/0\'/0\'/0\'', info['hdkeypath']) prev_seed_id = info['hdseedid'] - # Change key should be the same keypool + # Change key should be HD and from the same keypool info = wallet.getaddressinfo(wallet.getrawchangeaddress()) assert_equal(prev_seed_id, info['hdseedid']) assert_equal('m/0\'/0\'/1\'', info['hdkeypath']) @@ -290,14 +284,7 @@ class UpgradeWalletTest(BitcoinTestFramework): hd_chain_version, external_counter, seed_id, internal_counter = struct.unpack('<iI20sI', hd_chain) assert_equal(2, hd_chain_version) assert_equal(2, internal_counter) - # Drain the keypool by fetching one external key and one change key. Should still be the same keypool - info = wallet.getaddressinfo(wallet.getnewaddress()) - assert 'hdseedid' not in info - assert 'hdkeypath' not in info - info = wallet.getaddressinfo(wallet.getrawchangeaddress()) - assert 'hdseedid' not in info - assert 'hdkeypath' not in info - # The next addresses are HD and should be on different HD chains + # The next addresses are HD and should be on different HD chains (the one remaining key in each pool should have been flushed) info = wallet.getaddressinfo(wallet.getnewaddress()) ext_id = info['hdseedid'] assert_equal('m/0\'/0\'/0\'', info['hdkeypath']) @@ -352,6 +339,11 @@ class UpgradeWalletTest(BitcoinTestFramework): v16_3_kvs = dump_bdb_kv(v16_3_wallet) assert b'\x0adefaultkey' not in v16_3_kvs + if self.is_sqlite_compiled(): + self.log.info("Checking that descriptor wallets do nothing, successfully") + self.nodes[0].createwallet(wallet_name="desc_upgrade", descriptors=True) + desc_wallet = self.nodes[0].get_wallet_rpc("desc_upgrade") + self.test_upgradewallet(desc_wallet, previous_version=169900, expected_version=169900) if __name__ == '__main__': UpgradeWalletTest().main() diff --git a/test/functional/wallet_watchonly.py b/test/functional/wallet_watchonly.py index c345c382d0..69c32ba54c 100755 --- a/test/functional/wallet_watchonly.py +++ b/test/functional/wallet_watchonly.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -# Copyright (c) 2018-2019 The Bitcoin Core developers +# Copyright (c) 2018-2021 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test createwallet watchonly arguments. """ +from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -36,11 +37,11 @@ class CreateWalletWatchonlyTest(BitcoinTestFramework): wo_wallet.importpubkey(pubkey=def_wallet.getaddressinfo(wo_change)['pubkey']) # generate some btc for testing - node.generatetoaddress(101, a1) + self.generatetoaddress(node, COINBASE_MATURITY + 1, a1) # send 1 btc to our watch-only address txid = def_wallet.sendtoaddress(wo_addr, 1) - self.nodes[0].generate(1) + self.generate(self.nodes[0], 1) # getbalance self.log.info('include_watchonly should default to true for watch-only wallets') |