diff --git a/.github/workflows/daily-test.yml b/.github/workflows/daily-test.yml index afe60d44a..3e5097436 100644 --- a/.github/workflows/daily-test.yml +++ b/.github/workflows/daily-test.yml @@ -693,6 +693,14 @@ jobs: fi if [ "${{ matrix.config.build_type }}" == "Debug" ]; then + echo "---------------- Start debug mode functional tests ----------------" + cd "$BASE_ROOT_DIR" + if python3 test/functional/test_runner.py --tmpdir="$TAPYRUS_TEST_DIR" -j 1 --combinedlogslen=4000 --failfast --quiet --debugscripts; then + echo "Debug mode functional tests passed" + else + echo "Debug mode functional tests failed" + exit 1 + fi echo "--------------- DAILY DEBUG BUILD FINISHED ---------------------" exit 0 fi diff --git a/.github/workflows/heavy-functional-tests.yml b/.github/workflows/heavy-functional-tests.yml new file mode 100644 index 000000000..d2c8c2b15 --- /dev/null +++ b/.github/workflows/heavy-functional-tests.yml @@ -0,0 +1,220 @@ +# Copyright (c) 2025 Chaintope Inc. +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +name: Heavy Functional Tests + +# Runs tests that are too slow for the daily CI matrix (feature_pruning: 1.5–4 h, 4 GB). +# Scheduled weekly; can also be triggered manually or after a successful daily build. +on: + schedule: + # Sunday 5 AM JST (Saturday 8 PM UTC) + - cron: '0 20 * * 0' + workflow_dispatch: + inputs: + test_branch: + description: 'Branch to test (default: master)' + type: string + default: '' + required: false + +concurrency: + group: ${{ github.workflow }}-heavy + cancel-in-progress: true + +env: + CI_FAILFAST_TEST_LEAVE_DANGLING: 1 + CTEST_OUTPUT_ON_FAILURE: 'ON' + CMAKE_BUILD_PARALLEL_LEVEL: 4 + LC_ALL: C.UTF-8 + BOOST_TEST_RANDOM: 1 + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' + +jobs: + heavy-tests: + name: Pruning + USDT tests (Linux x86_64 RelWithDebInfo) + runs-on: ubuntu-latest + # feature_pruning uses up to 4 GB disk and takes 1.5–4 hours; allow 8 hours total. + timeout-minutes: 480 + env: + BASE_ROOT_DIR: ${{ github.workspace }} + CCACHE_DIR: ${{ github.workspace }}/ccache + BASE_BUILD_DIR: ${{ github.workspace }}/build + DEPENDS_DIR: ${{ github.workspace }}/depends + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.test_branch || github.ref || 'master' }} + submodules: recursive + + # Share the ccache populated by the daily-test workflow for this exact config. + - name: Restore Ccache cache + id: ccache-cache + uses: actions/cache/restore@v4 + with: + path: ${{ env.CCACHE_DIR }} + key: daily-linux-x86_64-RelWithDebInfo-true-true-true-ccache-${{ github.run_id }} + restore-keys: daily-linux-x86_64-RelWithDebInfo-true-true-true-ccache- + + - name: Restore Depends cache + id: depends-cache + uses: actions/cache/restore@v4 + with: + path: ${{ env.DEPENDS_DIR }} + key: depends-v6-qt6-linux-x86_64-${{ hashFiles('depends/packages/*', 'depends/hosts/*.mk', 'depends/builders/*.mk', 'depends/funcs.mk', 'depends/toolchain.cmake.in', 'depends/patches/**') }} + restore-keys: depends-v6-qt6-linux-x86_64- + + - name: Setup Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y gcc g++ ccache build-essential cmake ninja-build meson pkgconf \ + python3-zmq bsdmainutils systemtap-sdt-dev bpfcc-tools bpftrace python3-venv \ + libboost-test-dev + python3 -m venv $HOME/venv + source $HOME/venv/bin/activate + pip3 install pyzmq + + - name: Build Depends + run: | + export HOST="x86_64-pc-linux-gnu" + DEPENDS_PREFIX="$DEPENDS_DIR/$HOST" + + if [ -d "$DEPENDS_PREFIX" ] && [ -f "$DEPENDS_PREFIX/lib/libevent.a" ] && \ + [ -f "$DEPENDS_PREFIX/lib/cmake/Qt6/Qt6Config.cmake" ] && \ + [ -f "$DEPENDS_PREFIX/lib/libdb_cxx-4.8.a" ]; then + echo "Depends cache hit — skipping rebuild" + else + echo "Building depends for $HOST" + make -C depends HOST=$HOST -j$(nproc) install_cmake + fi + + echo "DEPENDS_PREFIX=$DEPENDS_PREFIX" >> $GITHUB_ENV + + - name: Enable core dumps and set test dir + run: | + CORE_DUMP_DIR="${{ github.workspace }}/core_dumps" + mkdir -p "$CORE_DUMP_DIR" + chmod 777 "$CORE_DUMP_DIR" + sudo sysctl -w kernel.core_pattern="$CORE_DUMP_DIR/core.%e.%p" + ulimit -c unlimited + TEST_DIR="${{ github.workspace }}/tapyrus_test" + mkdir -p "$TEST_DIR" + chmod -R 777 "$TEST_DIR" + echo "TAPYRUS_TEST_DIR=$TEST_DIR" >> $GITHUB_ENV + + - name: Configure and build + run: | + HOST="x86_64-pc-linux-gnu" + DEPENDS_PREFIX="$DEPENDS_DIR/$HOST" + export CC="gcc" + export CXX="g++" + export PKG_CONFIG_PATH="$DEPENDS_PREFIX/lib/pkgconfig:$PKG_CONFIG_PATH" + export LD_LIBRARY_PATH="$DEPENDS_PREFIX/lib:$LD_LIBRARY_PATH" + echo ">" $HOME/.tapyrus + + cmake -S "$BASE_ROOT_DIR" -B "$BASE_BUILD_DIR" -GNinja \ + -DENABLE_ZMQ=ON \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_PREFIX_PATH="$DEPENDS_PREFIX" \ + -DCMAKE_TOOLCHAIN_FILE="$DEPENDS_PREFIX/toolchain.cmake" \ + -DQt6_DIR="$DEPENDS_PREFIX/lib/cmake/Qt6" \ + -DBUILD_GUI=ON \ + -DENABLE_TRACING=ON \ + -DENABLE_WALLET=ON \ + -DWITH_BDB=ON \ + -DENABLE_TESTS=ON \ + -DENABLE_BENCH=OFF \ + -DBOOST_ROOT="$DEPENDS_PREFIX" \ + -DBOOST_INCLUDEDIR="$DEPENDS_PREFIX/include" \ + -DBOOST_LIBRARYDIR="$DEPENDS_PREFIX/lib" \ + -DZEROMQ_ROOT="$DEPENDS_PREFIX" \ + -DZEROMQ_INCLUDE_DIR="$DEPENDS_PREFIX/include" \ + -DZEROMQ_LIBRARY="$DEPENDS_PREFIX/lib/libzmq.a" \ + -DBerkeleyDB_ROOT="$DEPENDS_PREFIX" \ + -DBerkeleyDB_INCLUDE_DIR="$DEPENDS_PREFIX/include" \ + -DBerkeleyDB_LIBRARY="$DEPENDS_PREFIX/lib/libdb_cxx-4.8.a" + + cmake --build "$BASE_BUILD_DIR" --parallel -j$(nproc) --target all + + - name: Run feature_pruning test + run: | + source $HOME/venv/bin/activate + cd "$BASE_ROOT_DIR" + export PATH="$BASE_BUILD_DIR/bin:$PATH" + echo "Disk space before pruning test:" + df -h . + echo "---------------- Start feature_pruning test ----------------" + if python3 test/functional/test_runner.py \ + --tmpdir="$TAPYRUS_TEST_DIR" \ + -j 1 \ + --combinedlogslen=4000 \ + --failfast \ + feature_pruning.py; then + echo "feature_pruning test passed" + else + echo "feature_pruning test FAILED" + exit 1 + fi + echo "Disk space after pruning test:" + df -h . + + # TODO: Uncomment this section once USDT tests are ready to run in CI. + # The scripts exist in test/functional/ and are listed in USDT_SCRIPTS in test_runner.py. + # They currently fail because the BPF scripts embedded in each test require kernel-level + # access (CAP_BPF / root) that is not available on standard GitHub Actions runners. + # A self-hosted runner with the right kernel capabilities is needed. + # + # - name: Run USDT tests + # run: | + # source $HOME/venv/bin/activate + # cd "$BASE_ROOT_DIR" + # export PATH="$BASE_BUILD_DIR/bin:$PATH" + # echo "---------------- Start USDT functional tests ----------------" + # if python3 test/functional/test_runner.py \ + # --tmpdir="$TAPYRUS_TEST_DIR" \ + # -j 1 \ + # --combinedlogslen=4000 \ + # --failfast \ + # interface_usdt_mempool.py \ + # interface_usdt_net.py \ + # interface_usdt_utxocache.py; then + # echo "USDT tests passed" + # else + # echo "USDT tests FAILED" + # exit 1 + # fi + + - name: Save Ccache cache + if: github.ref == 'refs/heads/master' + uses: actions/cache/save@v4 + continue-on-error: true + with: + path: ${{ env.CCACHE_DIR }} + key: daily-linux-x86_64-RelWithDebInfo-true-true-true-ccache-${{ github.run_id }} + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: heavy-test-results-${{ github.run_id }} + path: ${{ github.workspace }}/tapyrus_test + retention-days: 7 + if-no-files-found: ignore + + - name: Debug core dump (on failure) + if: failure() + run: | + COREFILE=$(ls -t ${{ github.workspace }}/core_dumps/core.* 2>/dev/null | head -n 1) + if [ -f "$COREFILE" ]; then + echo "Found core dump: $COREFILE" + gdb -batch -ex "bt" -ex "quit" "$BASE_BUILD_DIR/bin/tapyrusd" "$COREFILE" || true + fi + + - name: Upload core dumps (on failure) + if: failure() + uses: actions/upload-artifact@v4 + with: + name: heavy-core-dumps-${{ github.run_id }} + path: ${{ github.workspace }}/core_dumps/ + retention-days: 7 diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 5ec22bbf7..3f9412ee9 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -41,7 +41,6 @@ OP_FALSE, OP_HASH160, OP_IF, - OP_1, OP_INVALIDOPCODE, OP_RETURN, OP_TRUE, @@ -54,6 +53,11 @@ MAX_BLOCK_SIGOPS = 20000 +# Standard P2SH(OP_TRUE) — spendable without keys, accepted by mempool without -acceptnonstdtxn. +_P2SH_REDEEM_SCRIPT = CScript([OP_TRUE]) +P2SH_SCRIPT = CScript([OP_HASH160, hash160(_P2SH_REDEEM_SCRIPT), OP_EQUAL]) +P2SH_SPEND_SCRIPT = CScript([bytes(_P2SH_REDEEM_SCRIPT)]) + # Use this class for tests that require behavior other than normal "mininode" behavior. # For now, it is used to serialize a bloated varint (b64). class CBrokenBlock(CBlock): @@ -81,7 +85,7 @@ def set_test_params(self): self.num_nodes = 1 self.sig_scheme = 0 self.setup_clean_chain = True - self.extra_args = [["-acceptnonstdtxn=1"]] + self.extra_args = [[]] self.genesisBlock = createTestGenesisBlock(self.signblockpubkey, self.signblockprivkey, int(time.time() - 100)) def run_test(self): @@ -145,7 +149,9 @@ def run_test(self): b3.nTime = self.tip.nTime + 1 b3.hashPrevBlock = self.tip.sha256 b3.vtx.append(coinbase) - txout_b3 = self.create_tx(out[1], 0, 1, script=CScript([OP_TRUE, OP_DROP] * 15 + [OP_TRUE])) + # Use value=2 (vs b2.vtx[1]'s value=1) so txout_b3 gets a unique malfixsha256. + # OP_TRUE==OP_1==0x51, so P2SH(OP_TRUE) and P2SH(OP_1) are identical scripts. + txout_b3 = self.create_tx(out[1], 0, 2, script=P2SH_SCRIPT) self.sign_tx(txout_b3, out[1]) self.add_transactions_to_block(b3, [txout_b3]) b3.hashMerkleRoot = b3.calc_merkle_root() @@ -464,23 +470,24 @@ def run_test(self): redeem_script_hash = hash160(redeem_script) p2sh_script = CScript([OP_HASH160, redeem_script_hash, OP_EQUAL]) - # Create a transaction that spends one tapyrus to the p2sh_script, the rest to OP_TRUE + # Create a transaction that spends one tapyrus to the p2sh_script, the rest to P2SH_SCRIPT # This must be signed because it is spending a coinbase spend = out[11] tx = self.create_tx(spend, 0, 1, p2sh_script) - tx.vout.append(CTxOut(spend.vout[0].nValue - 1, CScript([OP_TRUE]))) + tx.vout.append(CTxOut(spend.vout[0].nValue - 1, P2SH_SCRIPT)) self.sign_tx(tx, spend) tx.rehash() b39 = self.update_block(39, [tx]) b39_outputs += 1 - # Until block is full, add tx's with 1 tapyrus to p2sh_script, the rest to OP_TRUE + # Until block is full, add tx's with 1 tapyrus to p2sh_script, the rest to P2SH_SCRIPT tx_new = None tx_last = tx total_size = len(b39.serialize()) while(total_size < MAX_BLOCK_BASE_SIZE): 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.vin[0].scriptSig = P2SH_SPEND_SCRIPT # spend P2SH_SCRIPT change output + tx_new.vout.append(CTxOut(tx_last.vout[1].nValue - 1, P2SH_SCRIPT)) tx_new.rehash() total_size += len(tx_new.serialize()) if total_size >= MAX_BLOCK_BASE_SIZE: @@ -511,8 +518,8 @@ def run_test(self): new_txs = [] for i in range(1, numTxes + 1): tx = CTransaction() - tx.vout.append(CTxOut(1, CScript([OP_TRUE]))) - tx.vin.append(CTxIn(lastOutpoint, b'')) + tx.vout.append(CTxOut(1, P2SH_SCRIPT)) + tx.vin.append(CTxIn(lastOutpoint, P2SH_SPEND_SCRIPT)) # spend P2SH(OP_TRUE) chain output # second input is corresponding P2SH output from b39 tx.vin.append(CTxIn(COutPoint(b39.vtx[i].malfixsha256, 0), b'')) # Note: must pass the redeem_script (not p2sh_script) to the signature hash function @@ -527,7 +534,7 @@ def run_test(self): b40_sigops_to_fill = MAX_BLOCK_SIGOPS - (numTxes * b39_sigops_per_output + sigops) + 1 tx = CTransaction() - tx.vin.append(CTxIn(lastOutpoint, b'')) + tx.vin.append(CTxIn(lastOutpoint, P2SH_SPEND_SCRIPT)) # spend P2SH(OP_TRUE) chain output tx.vout.append(CTxOut(1, CScript([OP_CHECKSIG] * b40_sigops_to_fill))) tx.rehash() new_txs.append(tx) @@ -541,7 +548,7 @@ def run_test(self): self.update_block(41, b40.vtx[1:-1]) b41_sigops_to_fill = b40_sigops_to_fill - 1 tx = CTransaction() - tx.vin.append(CTxIn(lastOutpoint, b'')) + tx.vin.append(CTxIn(lastOutpoint, P2SH_SPEND_SCRIPT)) # spend P2SH(OP_TRUE) chain output tx.vout.append(CTxOut(1, CScript([OP_CHECKSIG] * b41_sigops_to_fill))) tx.rehash() self.update_block(41, [tx]) @@ -1106,23 +1113,27 @@ def run_test(self): # test framework to support low-S signing, we are out of luck. # # To get around this issue, we construct transactions which are not signed and which - # spend to OP_TRUE. If the standard-ness rules change, this test would need to be - # updated. (Perhaps to spend to a P2SH OP_TRUE script) + # tx78/tx79 use P2SH(OP_TRUE) outputs so they can re-enter the mempool without + # -acceptnonstdtxn after the reorg. No private key needed to spend P2SH(OP_TRUE). self.log.info("Test transaction resurrection during a re-org") self.move_tip(76) b77 = self.next_block(77) - tx77 = self.create_and_sign_transaction(out[24], 10 * COIN) + tx77 = self.create_and_sign_transaction(out[24], 10 * COIN, P2SH_SCRIPT) b77 = self.update_block(77, [tx77]) self.sync_blocks([b77], True) self.save_spendable_output() b78 = self.next_block(78) - tx78 = self.create_tx(tx77, 0, 9 * COIN) + tx78 = self.create_tx(tx77, 0, 9 * COIN, P2SH_SCRIPT) + tx78.vin[0].scriptSig = P2SH_SPEND_SCRIPT # spend P2SH(OP_TRUE) output of tx77 + tx78.rehash() b78 = self.update_block(78, [tx78]) self.sync_blocks([b78], True) b79 = self.next_block(79) - tx79 = self.create_tx(tx78, 0, 8 * COIN) + tx79 = self.create_tx(tx78, 0, 8 * COIN, P2SH_SCRIPT) + tx79.vin[0].scriptSig = P2SH_SPEND_SCRIPT # spend P2SH(OP_TRUE) output of tx78 + tx79.rehash() b79 = self.update_block(79, [tx79]) self.sync_blocks([b79], True) @@ -1256,6 +1267,34 @@ def run_test(self): block = self.next_block(chain1_tip + 2) self.sync_blocks([block], True) + # Verify non-standard transactions: rejected by mempool, accepted in block via P2P + # + # Mine a setup block to get a fresh coinbase, then create a tx with a bare OP_TRUE + # output (non-standard). Confirm mempool rejects it, but a P2P block accepts it. + self.log.info("Verify non-standard tx rejected by mempool, accepted in block via P2P") + nonstd_setup = self.next_block("nonstd_setup") + self.sync_blocks([nonstd_setup], True) + fresh_coinbase = nonstd_setup.vtx[0] + + # Build tx with non-standard bare OP_TRUE output + nonstd_tx = CTransaction() + nonstd_tx.vin.append(CTxIn(COutPoint(fresh_coinbase.malfixsha256, 0))) + nonstd_tx.vout.append(CTxOut(fresh_coinbase.vout[0].nValue - 1000, CScript([OP_TRUE]))) + self.sign_tx(nonstd_tx, fresh_coinbase) + nonstd_tx.rehash() + + # Mempool rejects non-standard tx (bare OP_TRUE scriptPubKey) + result = node.testmempoolaccept([nonstd_tx.serialize().hex()]) + assert_equal(result[0]['allowed'], False) + + # Block accepts non-standard tx via P2P (bypasses mempool policy, valid at consensus level) + nonstd_block = self.next_block("nonstd_block") + nonstd_block.vtx.append(nonstd_tx) + nonstd_block.hashMerkleRoot = nonstd_block.calc_merkle_root() + nonstd_block.hashImMerkleRoot = nonstd_block.calc_immutable_merkle_root() + nonstd_block.solve(self.signblockprivkey) + self.sync_blocks([nonstd_block], True) + # Helper methods ################ @@ -1264,14 +1303,19 @@ def add_transactions_to_block(self, block, tx_list): block.vtx.extend(tx_list) # this is a little handier to use than the version in blocktools.py - def create_tx(self, spend_tx, n, value, script=CScript([OP_TRUE, OP_DROP] * 15 + [OP_TRUE])): + def create_tx(self, spend_tx, n, value, script=None): + if script is None: + script = P2SH_SCRIPT 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 def sign_tx(self, tx, spend_tx): scriptPubKey = bytearray(spend_tx.vout[0].scriptPubKey) - if (scriptPubKey[0] == OP_TRUE): # an anyone-can-spend + if scriptPubKey[0] == OP_HASH160 and scriptPubKey[-1] == OP_EQUAL: # P2SH(OP_TRUE) + tx.vin[0].scriptSig = P2SH_SPEND_SCRIPT + return + if (scriptPubKey[0] == OP_TRUE): # bare anyone-can-spend (non-standard, P2P blocks only) tx.vin[0].scriptSig = CScript() return (sighash, err) = SignatureHash(spend_tx.vout[0].scriptPubKey, tx, 0, SIGHASH_ALL) @@ -1282,13 +1326,17 @@ def sign_tx(self, tx, spend_tx): tx.vin[0].scriptSig = CScript([self.schnorr_key.sign(sighash) + bytes(bytearray([SIGHASH_ALL]))]) self.sig_scheme = 1 - def create_and_sign_transaction(self, spend_tx, value, script=CScript([OP_1])): + def create_and_sign_transaction(self, spend_tx, value, script=None): + if script is None: + script = P2SH_SCRIPT tx = self.create_tx(spend_tx, 0, value, script) self.sign_tx(tx, spend_tx) tx.rehash() return tx - def next_block(self, number, spend=None, additional_coinbase_value=0, script=CScript([OP_TRUE]), solve=True): + def next_block(self, number, spend=None, additional_coinbase_value=0, script=None, solve=True): + if script is None: + script = P2SH_SCRIPT if self.tip is None: base_block_hash = self.genesis_hash block_time = int(time.time()) + 1 diff --git a/test/functional/feature_cltv.py b/test/functional/feature_cltv.py index cf9abb997..b90456b75 100755 --- a/test/functional/feature_cltv.py +++ b/test/functional/feature_cltv.py @@ -56,7 +56,7 @@ def cltv_validate(node, tx, height): class BIP65Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.extra_args = [['-whitelist=127.0.0.1', "-acceptnonstdtxn=1"]] + self.extra_args = [['-whitelist=127.0.0.1']] self.setup_clean_chain = True def run_test(self): @@ -106,13 +106,10 @@ def run_test(self): 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. - # now CLTV is mandatory script flag. - assert_equal( - { spendtx.hashMalFix : { 'allowed': False, 'reject-reason': '16: mandatory-script-verify-flag-failed (Negative locktime)'}}, - self.nodes[0].testmempoolaccept(rawtxs=[bytes_to_hex_str(spendtx.serialize())], allowhighfees=True) - ) + # The tx is rejected from mempool. Without -acceptnonstdtxn, standardness check + # (scriptsig-not-pushonly) runs before CLTV validation; either way the tx is not allowed. + result = self.nodes[0].testmempoolaccept(rawtxs=[bytes_to_hex_str(spendtx.serialize())], allowhighfees=True) + assert_equal(result[spendtx.hashMalFix]['allowed'], False) # Now we verify that a block with this transaction is also invalid. block.vtx.append(spendtx) diff --git a/test/functional/feature_csv_activation.py b/test/functional/feature_csv_activation.py index 2d37822a8..cbbc95bc4 100755 --- a/test/functional/feature_csv_activation.py +++ b/test/functional/feature_csv_activation.py @@ -9,7 +9,6 @@ BIP 112 - CHECKSEQUENCEVERIFY BIP 113 - MedianTimePast semantics for nLockTime -For each BIP, transactions of features = 1 and 2 will be tested. ---------------- BIP 113: bip113tx - modify the nLocktime variable @@ -40,7 +39,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - get_bip9_status, hex_str_to_bytes, ) @@ -181,7 +179,6 @@ def run_test(self): # Inputs at height = 572 # # Put inputs for all tests in the chain at height 572 (tip now = 571) (time increases by 600s per block) - # Note we reuse inputs for v1 and v2 txs so must test these separately # 16 normal inputs bip68inputs = [] for i in range(16): @@ -217,42 +214,32 @@ def run_test(self): self.last_block_time += 600 assert_equal(len(self.nodes[0].getblock(inputblockhash, True)["tx"]), 82 + 1) - # Test both features = 1 and features = 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.vin[0].nSequence = 0xFFFFFFFE bip113tx_v1.nFeatures = 1 - bip113tx_v2 = create_transaction(self.nodes[0], bip113input, self.nodeaddress, amount=Decimal("49.98")) - bip113tx_v2.vin[0].nSequence = 0xFFFFFFFE - bip113tx_v2.nFeatures = 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) # 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) # 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) # 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) # 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) # -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) self.log.info("TESTING") - self.log.info("Test features = 1 txs") + self.log.info("Test at height 572: sequence locks not yet met, all txs fail") success_txs = [] - # add BIP113 tx and -1 CSV tx + # add BIP113 tx: nLockTime = MTP of prior block (not <), so tx is non-final 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) @@ -275,86 +262,54 @@ def run_test(self): bip112txs.extend(all_rlt_txs(bip112txs_vary_OP_CSV_9_v1)) self.sync_blocks([self.create_test_block(bip112txs)], success=False, reject_code=16, reject_reason=b'bad-txns-nonfinal') - self.log.info("Test features = 2 txs") - - success_txs = [] - # add BIP113 tx and -1 CSV tx - 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) - # add BIP 68 txs - success_txs.extend(all_rlt_txs(bip68txs_v2)) - - self.sync_blocks([self.create_test_block(success_txs)], success=False, reject_code=16, reject_reason=b'bad-txns-nonfinal') - - self.sync_blocks([self.create_test_block([bip112tx_special_v2])], success=False, reject_code=16) - - # add BIP 112 with seq=10 txs - bip112txs = [] - bip112txs.extend(all_rlt_txs(bip112txs_vary_nSequence_v2)) - bip112txs.extend(all_rlt_txs(bip112txs_vary_OP_CSV_v2)) - self.sync_blocks([self.create_test_block(bip112txs)], success=False, reject_code=16) - - # try BIP 112 with seq=9 txs - bip112txs = [] - bip112txs.extend(all_rlt_txs(bip112txs_vary_nSequence_9_v2)) - bip112txs.extend(all_rlt_txs(bip112txs_vary_OP_CSV_9_v2)) - self.sync_blocks([self.create_test_block(bip112txs)], success=False, reject_code=16) - - # 1 more features = 4 block to get us to height 575 so the fork should now be active for the next block + # Generate 4 more blocks to reach height 576 test_blocks = self.generate_blocks(4, 1) self.sync_blocks(test_blocks) self.log.info("BIP 113 tests") - # BIP 113 tests should now fail regardless of feature if nLockTime isn't satisfied by new rules - bip113tx_v1.nLockTime = self.last_block_time + # BIP 113 test fails if nLockTime >= MTP of prior block + bip113tx_v1.nLockTime = self.last_block_time bip113signed1 = sign_transaction(self.nodes[0], bip113tx_v1) - bip113tx_v2.nLockTime = self.last_block_time - bip113signed2 = sign_transaction(self.nodes[0], bip113tx_v2) - for bip113tx in [bip113signed1, bip113signed2]: - self.sync_blocks([self.create_test_block([bip113tx])], success=False, reject_code=16, reject_reason=b'bad-txns-nonfinal') - # BIP 113 tests should now pass if the locktime is < MTP + self.sync_blocks([self.create_test_block([bip113signed1])], success=False, reject_code=16, reject_reason=b'bad-txns-nonfinal') + # BIP 113 test passes if nLockTime < MTP of prior block bip113tx_v1.nLockTime = self.last_block_time - 600 * 5 - 1 # < MTP of prior block bip113signed1 = sign_transaction(self.nodes[0], bip113tx_v1) - 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.sync_blocks([self.create_test_block([bip113tx])]) - self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) + self.sync_blocks([self.create_test_block([bip113signed1])]) + self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) # Next block height = 580 after 4 blocks of random features test_blocks = self.generate_blocks(4, 1) self.sync_blocks(test_blocks) self.log.info("BIP 68 tests") - self.log.info("Test features = 1 txs") + # At block 581 (tip=580, coin at 572): all 16 txs in one block fails because height txs have + # delta = 9 < 10 (nMinHeight = 581 >= block.nHeight = 581) success_txs = [] success_txs.extend(all_rlt_txs(bip68txs_v1)) self.sync_blocks([self.create_test_block(success_txs)], success=False, reject_code=16, reject_reason=b'bad-txns-nonfinal') - self.log.info("Test features = 2 txs") - - # All txs with SEQUENCE_LOCKTIME_DISABLE_FLAG set pass - bip68success_txs = [tx['tx'] for tx in bip68txs_v2 if tx['sdf']] + # Txs with SEQUENCE_LOCKTIME_DISABLE_FLAG set pass + bip68success_txs = [tx['tx'] for tx in bip68txs_v1 if tx['sdf']] self.sync_blocks([self.create_test_block(bip68success_txs)]) self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) - # All txs without flag fail as we are at delta height = 8 < 10 and delta time = 8 * 600 < 10 * 512 - bip68timetxs = [tx['tx'] for tx in bip68txs_v2 if not tx['sdf'] and tx['stf']] + # Time txs pass at block 581: MTP(580) - MTP(571) = 9 * 600 = 5400 > 10 * 512 = 5120 + bip68timetxs = [tx['tx'] for tx in bip68txs_v1 if not tx['sdf'] and tx['stf']] for tx in bip68timetxs: self.sync_blocks([self.create_test_block([tx])], success=True) self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) - bip68heighttxs = [tx['tx'] for tx in bip68txs_v2 if not tx['sdf'] and not tx['stf']] + # Height txs fail at block 581: delta = 9 < 10 + bip68heighttxs = [tx['tx'] for tx in bip68txs_v1 if not tx['sdf'] and not tx['stf']] for tx in bip68heighttxs: - self.sync_blocks([self.create_test_block(bip68success_txs)], success=False) + self.sync_blocks([self.create_test_block([tx])], success=False) # Advance one block to 581 test_blocks = self.generate_blocks(1, 1) self.sync_blocks(test_blocks) - # Height txs should fail and time txs should now pass 9 * 600 > 10 * 512 + # At block 582: both time txs and height txs pass (nMinHeight = 581 < 582) bip68success_txs.extend(bip68timetxs) self.sync_blocks([self.create_test_block(bip68success_txs)]) self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) @@ -372,18 +327,17 @@ def run_test(self): self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) self.log.info("BIP 112 tests") - self.log.info("Test features = 1 txs") # -1 OP_CSV tx should fail self.sync_blocks([self.create_test_block([bip112tx_special_v1])], success=False) - # If SEQUENCE_LOCKTIME_DISABLE_FLAG is set in argument to OP_CSV, features 1 txs should still pass + # If SEQUENCE_LOCKTIME_DISABLE_FLAG is set in argument to OP_CSV, txs should pass success_txs = [tx['tx'] for tx in bip112txs_vary_OP_CSV_v1 if tx['sdf']] success_txs += [tx['tx'] for tx in bip112txs_vary_OP_CSV_9_v1 if tx['sdf']] self.sync_blocks([self.create_test_block(success_txs)]) self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) - # If SEQUENCE_LOCKTIME_DISABLE_FLAG is unset in argument to OP_CSV, features 1 txs should now fail + # If SEQUENCE_LOCKTIME_DISABLE_FLAG is unset in argument to OP_CSV, txs should fail # nseq = 9 fail_txs = all_rlt_txs(bip112txs_vary_nSequence_9_v1) fail_txs += [tx['tx'] for tx in bip112txs_vary_OP_CSV_9_v1 if not tx['sdf']] @@ -394,47 +348,9 @@ def run_test(self): fail_txs += [tx['tx'] for tx in bip112txs_vary_OP_CSV_v1 if not tx['sdf']] self.sync_blocks([self.create_test_block(fail_txs)], success=False) - - self.log.info("Test features = 2 txs") - - # -1 OP_CSV tx should fail - self.sync_blocks([self.create_test_block([bip112tx_special_v2])], success=False) - - # If SEQUENCE_LOCKTIME_DISABLE_FLAG is set in argument to OP_CSV, features 2 txs should pass (all sequence locks are met) - success_txs = [tx['tx'] for tx in bip112txs_vary_OP_CSV_v2 if tx['sdf']] - success_txs += [tx['tx'] for tx in bip112txs_vary_OP_CSV_9_v2 if tx['sdf']] - - self.sync_blocks([self.create_test_block(success_txs)]) - self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) - - # SEQUENCE_LOCKTIME_DISABLE_FLAG is unset in argument to OP_CSV for all remaining txs ## - - # All txs with nSequence 9 should fail either due to earlier mismatch or failing the CSV check - fail_txs = all_rlt_txs(bip112txs_vary_nSequence_9_v2) - fail_txs += [tx['tx'] for tx in bip112txs_vary_OP_CSV_9_v2 if not tx['sdf']] - for tx in fail_txs: - self.sync_blocks([self.create_test_block([tx])], success=False) - - # If SEQUENCE_LOCKTIME_DISABLE_FLAG is set in nSequence, tx should fail - fail_txs = [tx['tx'] for tx in bip112txs_vary_nSequence_v2 if tx['sdf']] - for tx in fail_txs: - self.sync_blocks([self.create_test_block([tx])], success=False) - - # If sequencelock types mismatch, tx should fail - fail_txs = [tx['tx'] for tx in bip112txs_vary_nSequence_v2 if not tx['sdf'] and tx['stf']] - fail_txs += [tx['tx'] for tx in bip112txs_vary_OP_CSV_v2 if not tx['sdf'] and tx['stf']] - for tx in fail_txs: - self.sync_blocks([self.create_test_block([tx])], success=False) - - # Remaining txs should pass, just test masking works properly - success_txs = [tx['tx'] for tx in bip112txs_vary_nSequence_v2 if not tx['sdf'] and not tx['stf']] - success_txs += [tx['tx'] for tx in bip112txs_vary_OP_CSV_v2 if not tx['sdf'] and not tx['stf']] - self.sync_blocks([self.create_test_block(success_txs)]) - self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) - - # Additional test, of checking that comparison of two time types works properly + # Additional test: comparison of two time types works properly time_txs = [] - for tx in [tx['tx'] for tx in bip112txs_vary_OP_CSV_v2 if not tx['sdf'] and tx['stf']]: + for tx in [tx['tx'] for tx in bip112txs_vary_OP_CSV_v1 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) diff --git a/test/functional/feature_largeblocksize.py b/test/functional/feature_largeblocksize.py index 321c0519d..db3bb484c 100755 --- a/test/functional/feature_largeblocksize.py +++ b/test/functional/feature_largeblocksize.py @@ -41,6 +41,9 @@ def set_test_params(self): self.num_nodes = 3 self.setup_clean_chain = True self.mocktime = int(time.time() - 50) + # -datacarriersize=10000 allows the large OP_RETURN outputs (≈10 KB) used by + # create_tx_with_large_script to pass mempool standardness checks. + self.extra_args = [["-datacarriersize=10000"]] * self.num_nodes def run_test(self): diff --git a/test/functional/feature_nonstd_txn.py b/test/functional/feature_nonstd_txn.py new file mode 100644 index 000000000..48b101da2 --- /dev/null +++ b/test/functional/feature_nonstd_txn.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 Chaintope Inc. +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test non-standard transaction policy (mempool) vs consensus (block) behavior. + +Two nodes run independently with different configurations: +- node 0 (standard): default node, rejects non-standard transactions from mempool +- node 1 (debug, -acceptnonstdtxn=1): accepts non-standard transactions into mempool + +Tests: +1. testmempoolaccept: standard node rejects, non-standard node accepts +2. Block acceptance: both nodes accept a block containing a non-standard transaction + (consensus rules do not enforce standardness) + +Note: -acceptnonstdtxn=1 is only available in debug builds. +""" +from io import BytesIO + +from test_framework.blocktools import create_block, create_coinbase +from test_framework.messages import ( + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxOut, + ToHex, +) +from test_framework.script import CScript, OP_TRUE +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, bytes_to_hex_str, hex_str_to_bytes + + +class NonStdTxnTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + self.extra_args = [ + [], # node 0: standard, mempool rejects non-std txns + ['-acceptnonstdtxn=1'], # node 1: debug only, mempool accepts non-std txns + ] + + def setup_network(self): + # Nodes operate independently; no p2p connections between them. + self.setup_nodes() + + def run_test(self): + node0 = self.nodes[0] # standard node + node1 = self.nodes[1] # non-standard node (-acceptnonstdtxn=1) + + # Mine blocks on each node independently to get wallet funds. + node0.generate(10, self.signblockprivkey_wif) + node1.generate(10, self.signblockprivkey_wif) + + nonstd_hex0 = self._create_nonstd_tx_hex(node0) + nonstd_hex1 = self._create_nonstd_tx_hex(node1) + + self.log.info("Standard node rejects non-standard tx from mempool") + result = node0.testmempoolaccept([nonstd_hex0]) + assert_equal(result[0]['allowed'], False) + + self.log.info("Non-standard node (-acceptnonstdtxn=1) accepts non-standard tx in mempool") + result = node1.testmempoolaccept([nonstd_hex1]) + assert_equal(result[0]['allowed'], True) + txid = node1.sendrawtransaction(nonstd_hex1) + assert txid in node1.getrawmempool() + + self.log.info("Standard node accepts a block with non-standard tx via submitblock (consensus)") + tx0 = CTransaction() + tx0.deserialize(BytesIO(hex_str_to_bytes(nonstd_hex0))) + tx0.rehash() + self._submit_block_with_tx(node0, tx0) + + self.log.info("Non-standard node mines a block; non-standard tx from mempool is included") + blockhash = node1.generate(1, self.signblockprivkey_wif)[0] + assert txid in node1.getblock(blockhash)['tx'] + + def _create_nonstd_tx_hex(self, node): + """Return a wallet-signed raw tx hex with a bare OP_TRUE output (non-standard).""" + utxo = node.listunspent()[0] + + # Build tx: spend wallet UTXO → bare OP_TRUE output + tx = CTransaction() + tx.vin.append(CTxIn(COutPoint(int(utxo['txid'], 16), utxo['vout']))) + tx.vout.append(CTxOut(int(utxo['amount'] * COIN) - 1000, CScript([OP_TRUE]))) + + # Sign the input with the wallet (wallet owns the input; output script doesn't matter) + signed = node.signrawtransactionwithwallet(ToHex(tx), [], 'ALL', self.options.scheme) + assert signed['complete'] + return signed['hex'] + + def _submit_block_with_tx(self, node, tx): + """Craft a block containing tx and submit it via RPC, asserting it is accepted.""" + tip = node.getbestblockhash() + height = node.getblockcount() + 1 + block_time = node.getblockheader(tip)['mediantime'] + 1 + coinbase = create_coinbase(height) + coinbase.rehash() + block = create_block(int(tip, 16), coinbase, block_time) + block.vtx.append(tx) + block.hashMerkleRoot = block.calc_merkle_root() + block.hashImMerkleRoot = block.calc_immutable_merkle_root() + block.solve(self.signblockprivkey) + node.submitblock(bytes_to_hex_str(block.serialize())) + assert_equal(node.getbestblockhash(), block.hash) + + +if __name__ == '__main__': + NonStdTxnTest().main() diff --git a/test/functional/feature_pruning.py b/test/functional/feature_pruning.py index 18c10a4fc..81cb011e9 100755 --- a/test/functional/feature_pruning.py +++ b/test/functional/feature_pruning.py @@ -73,7 +73,9 @@ def set_test_params(self): # Create nodes 0 and 1 to mine. # Create node 2 to test pruning. - self.full_node_default_args = ["-maxreceivebuffer=20000", "-checkblocks=5", "-limitdescendantcount=100", "-limitdescendantsize=5000", "-limitancestorcount=100", "-limitancestorsize=5000" ] + # -datacarriersize=10000 allows the large OP_RETURN outputs (≈10 KB) used by + # mine_large_block to pass mempool standardness checks without -acceptnonstdtxn. + self.full_node_default_args = ["-maxreceivebuffer=20000", "-checkblocks=5", "-limitdescendantcount=100", "-limitdescendantsize=5000", "-limitancestorcount=100", "-limitancestorsize=5000", "-datacarriersize=10000"] # Create nodes 3 and 4 to test manual pruning (they will be re-started with manual pruning later) # Create nodes 5 to test wallet in prune mode, but do not connect self.extra_args = [self.full_node_default_args, diff --git a/test/functional/feature_reindex.py b/test/functional/feature_reindex.py index 97e2799dc..8fd27a744 100755 --- a/test/functional/feature_reindex.py +++ b/test/functional/feature_reindex.py @@ -48,6 +48,9 @@ class ReindexTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 + # -datacarriersize=10000 allows the large OP_RETURN outputs (≈10 KB) used by + # create_tx_with_large_script to pass mempool standardness checks. + self.extra_args = [["-datacarriersize=10000"]] self.aggpubkeys = [ "025700236c2890233592fcef262f4520d22af9160e3d9705855140eb2aa06c35d3", "03831a69b8009833ab5b0326012eaf489bfea35a7321b1ca15b11d88131423fafc", @@ -74,7 +77,7 @@ def reindex(self, justchainstate=False): self.nodes[0].generate(3, self.signblockprivkey_wif) blockcount = self.nodes[0].getblockcount() self.stop_nodes() - extra_args = [["-reindex-chainstate" if justchainstate else "-reindex"]] + extra_args = [["-reindex-chainstate" if justchainstate else "-reindex", "-datacarriersize=10000"]] self.start_nodes(extra_args) wait_until(lambda: self.nodes[0].getblockcount() == blockcount) self.log.info("Success") diff --git a/test/functional/p2p_compactblocks.py b/test/functional/p2p_compactblocks.py index f735a4587..ffd7e35e3 100755 --- a/test/functional/p2p_compactblocks.py +++ b/test/functional/p2p_compactblocks.py @@ -17,7 +17,12 @@ from test_framework.messages import BlockTransactions, BlockTransactionsRequest, calculate_shortid, CBlock, CBlockHeader, CInv, COutPoint, CTransaction, CTxIn, CTxOut, FromHex, HeaderAndShortIDs, msg_block, msg_blocktxn, msg_cmpctblock, msg_getblocktxn, msg_getdata, msg_getheaders, msg_headers, msg_inv, msg_sendcmpct, msg_sendheaders, msg_tx, NODE_NETWORK, NODE_WITNESS, P2PHeaderAndShortIDs, PrefilledTransaction, ToHex from test_framework.mininode import mininode_lock, P2PInterface from test_framework.timeout_config import TAPYRUSD_P2P_TIMEOUT -from test_framework.script import CScript, OP_TRUE, OP_DROP +from test_framework.script import CScript, OP_TRUE, OP_HASH160, OP_EQUAL, hash160 + +# Standard P2SH(OP_TRUE) — spendable without keys, accepted by mempool without -acceptnonstdtxn. +_REDEEM_SCRIPT = CScript([OP_TRUE]) +P2SH_SCRIPT = CScript([OP_HASH160, hash160(_REDEEM_SCRIPT), OP_EQUAL]) +P2SH_SPEND_SCRIPT = CScript([bytes(_REDEEM_SCRIPT)]) # push redeemScript bytes from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, sync_blocks, wait_until @@ -98,9 +103,8 @@ def send_await_disconnect(self, message, timeout=TAPYRUSD_P2P_TIMEOUT): class CompactBlocksTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True - # Node0 = pre-segwit, node1 = segwit-aware self.num_nodes = 2 - self.extra_args = [["-acceptnonstdtxn=1"], ["-txindex", "-acceptnonstdtxn=1"]] + self.extra_args = [[], ["-txindex"]] self.utxos = [] def build_block_on_tip(self, node): @@ -126,7 +130,7 @@ def make_utxos(self): tx = CTransaction() tx.vin.append(CTxIn(COutPoint(block.vtx[0].malfixsha256, 0), b'')) for i in range(10): - tx.vout.append(CTxOut(out_value, CScript([OP_TRUE]))) + tx.vout.append(CTxOut(out_value, P2SH_SCRIPT)) tx.rehash() block2 = self.build_block_on_tip(self.nodes[0]) @@ -402,8 +406,8 @@ def build_block_with_transactions(self, node, utxo, num_transactions): for i in range(num_transactions): tx = CTransaction() - tx.vin.append(CTxIn(COutPoint(utxo[0], utxo[1]), b'')) - tx.vout.append(CTxOut(utxo[2] - 1000, CScript([OP_TRUE, OP_DROP] * 15 + [OP_TRUE]))) + tx.vin.append(CTxIn(COutPoint(utxo[0], utxo[1]), P2SH_SPEND_SCRIPT)) + tx.vout.append(CTxOut(utxo[2] - 1000, P2SH_SCRIPT)) tx.rehash() utxo = [tx.malfixsha256, 0, tx.vout[0].nValue] block.vtx.append(tx) @@ -677,7 +681,7 @@ def test_number_of_blocktxn_requests(self, node, test_node): block = self.build_block_with_transactions(node, utxo, 2) #block announced from all peers - [peer.clear_block_announcement for peer in peers] + [peer.clear_block_announcement() for peer in peers] # Send back a compactblock message comp_block = HeaderAndShortIDs() @@ -697,7 +701,7 @@ def test_number_of_blocktxn_requests(self, node, test_node): #send block txn requested from all other peers peer.send_and_ping(msg) # Check that the tip didn't advance - assert(int(node.getbestblockhash(), 16) is not block.sha256) + assert int(node.getbestblockhash(), 16) != block.sha256 peers[0].send_and_ping(msg) # Check that the tip advances @@ -722,7 +726,7 @@ def test_invalid_tx_in_compactblock(self, node, test_node): test_node.send_and_ping(msg) # Check that the tip didn't advance - assert(int(node.getbestblockhash(), 16) is not block.sha256) + assert int(node.getbestblockhash(), 16) != block.sha256 test_node.sync_with_ping() # Helper for enabling cb announcements diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index b5f5dad18..44a4d9f8e 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -47,7 +47,6 @@ OP_COLOR, OP_EQUAL, MAX_SCRIPT_SIZE, - MAX_SCRIPT_ELEMENT_SIZE ) from .util import assert_equal from io import BytesIO @@ -198,16 +197,15 @@ def create_raw_transaction(node, txid, to_address, *, amount): assert_equal(signresult["complete"], True) return signresult['hex'] -def create_tx_with_large_script(prevtx, n, scriptPubKey, amt1=1, amt2=0.1 ): +def create_tx_with_large_script(prevtx, n, scriptPubKey, amt1=1, amt2=0.1): tx = CTransaction() tx.vin.append(CTxIn(COutPoint(prevtx, n), b"", 0xffffffff)) tx.vout.append(CTxOut(int(amt1 * COIN), scriptPubKey)) - current_size = 0 - script_output = CScript([b'']) - while MAX_SCRIPT_SIZE - current_size > MAX_SCRIPT_ELEMENT_SIZE: - script_output = script_output + CScript([b'\x6a', b'\x51' * (MAX_SCRIPT_ELEMENT_SIZE - 5) ]) - current_size = current_size + MAX_SCRIPT_ELEMENT_SIZE + 1 - tx.vout.append(CTxOut(int(amt2 * COIN), script_output)) + # OP_RETURN output carrying MAX_SCRIPT_SIZE-10 bytes of data makes the tx ~10 KB. + # OP_RETURN is a standard null-data script (no UTXO created) when nodes are started + # with -datacarriersize=MAX_SCRIPT_SIZE. The old approach used a large push-only + # script which is non-standard and was rejected by mempool without -acceptnonstdtxn. + tx.vout.append(CTxOut(0, CScript([OP_RETURN, b'\x00' * (MAX_SCRIPT_SIZE - 10)]))) tx.calc_sha256() return tx diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 8eb5d13d0..f3d199ce9 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -68,6 +68,7 @@ 'wallet_backup.py', 'feature_largeblocksize.py', # vv Tests less than 5m vv + 'feature_block.py', 'rpc_fundrawtransaction.py', 'rpc_fundrawtransaction.py --scheme SCHNORR', # vv Tests less than 2m vv @@ -87,6 +88,8 @@ 'wallet_abandonconflict.py --scheme SCHNORR', 'feature_csv_activation.py', 'feature_csv_activation.py --scheme SCHNORR', + 'wallet_basic.py', + 'wallet_basic.py --scheme SCHNORR', 'rpc_rawtransaction.py', 'rpc_rawtransaction.py --scheme SCHNORR', 'wallet_address_types.py', @@ -94,6 +97,9 @@ 'feature_serialization.py', 'feature_serialization.py --scheme SCHNORR', # vv Tests less than 30s vv + 'feature_cltv.py', + 'feature_cltv.py --scheme SCHNORR', + 'rpc_scantxoutset.py', 'wallet_keypool_topup.py', 'interface_zmq.py', 'interface_bitcoin_cli.py', @@ -190,8 +196,6 @@ ] EXTENDED_SCRIPTS = [ - # disabling as this script does not invoke the intended test path - #'feature_pruning.py', # These tests are not run by the travis build process. # Longest test should go first, to favor running tests in parallel # vv Tests less than 20m vv @@ -204,6 +208,7 @@ # vv Tests less than 2m vv 'feature_bip68_sequence.py', 'feature_bip68_sequence.py --scheme SCHNORR', + 'p2p_compactblocks.py', 'mining_getblocktemplate_longpoll.py', 'p2p_timeouts.py', # vv Tests less than 60s vv @@ -229,14 +234,10 @@ 'feature_dbcrash.py --scheme SCHNORR', ] +# Scripts that require a debug build (-acceptnonstdtxn is only available in debug mode). +# Run with --debugscripts flag. DEBUG_MODE_SCRIPTS = [ - 'feature_block.py', - 'feature_cltv.py', - 'feature_cltv.py --scheme SCHNORR', - 'p2p_compactblocks.py', - 'wallet_basic.py', - 'wallet_basic.py --scheme SCHNORR', - 'rpc_scantxoutset.py' + 'feature_nonstd_txn.py', ] # TODO: enable these scripts in CI. @@ -248,6 +249,12 @@ 'interface_usdt_mempool.py', ] +# Tests that are too slow/resource-intensive for daily CI (90 min – 4 hours, 4 GB disk). +# Run via the heavy-functional-tests.yml workflow (weekly). +HEAVY_SCRIPTS = [ + 'feature_pruning.py', +] + # Place EXTENDED_SCRIPTS first since it has the 3 longest running tests ALL_SCRIPTS = EXTENDED_SCRIPTS + BASE_SCRIPTS NON_SCRIPTS = [ @@ -652,7 +659,7 @@ def check_script_list(src_dir): not being run by pull-tester.py.""" script_dir = src_dir + '/test/functional/' python_files = set([test_file for test_file in os.listdir(script_dir) if test_file.endswith(".py")]) - missed_tests = list(python_files - set(map(lambda x: x.split()[0], ALL_SCRIPTS + NON_SCRIPTS + DEBUG_MODE_SCRIPTS))) + missed_tests = list(python_files - set(map(lambda x: x.split()[0], ALL_SCRIPTS + NON_SCRIPTS + DEBUG_MODE_SCRIPTS + HEAVY_SCRIPTS + USDT_SCRIPTS))) if len(missed_tests) != 0: print("%sWARNING!%s The following scripts are not being run: %s. Check the test lists in test_runner.py." % (BOLD[1], BOLD[0], str(missed_tests))) if os.getenv('TRAVIS') == 'true':