diff --git a/doc/elements-tx-format.md b/doc/elements-tx-format.md index 945ac48bbfc..c97c7a9cb20 100644 --- a/doc/elements-tx-format.md +++ b/doc/elements-tx-format.md @@ -104,7 +104,7 @@ SegWit transactions have one such witness for each input. | Script Witness | Yes | Varies | `Vector` | | The vector represents the witness stack.
Can be empty (length of 0). | | Peg-in Witness | Yes | Varies | `Vector` | | The vector represents the witness stack.
Can be empty (length of 0). | -The range proofs must be empty if their asociated amounts (issuance / inflation keys) are explicit. +The range proofs must be empty if their associated amounts (issuance / inflation keys) are explicit. Refer [here](https://elementsproject.org/features/confidential-transactions/investigation) for more details on range proofs. A non-empty peg-in witness stack should always have a length of 6, and the items should be interpreted as follows: diff --git a/src/miner.cpp b/src/miner.cpp index fba1b22ba9e..0f8b2f31a6b 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -112,7 +112,7 @@ void BlockAssembler::resetBlock() nFees = 0; } -std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn, std::chrono::seconds min_tx_age, DynaFedParamEntry* proposed_entry, CScript const* commit_script) +std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn, std::chrono::seconds min_tx_age, DynaFedParamEntry* proposed_entry, const std::vector* commit_scripts) { assert(min_tx_age >= std::chrono::seconds(0)); int64_t nTimeStart = GetTimeMicros(); @@ -206,8 +206,10 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc } coinbaseTx.vin[0].scriptSig = CScript() << nHeight << OP_0; // Non-consensus commitment output before finishing coinbase transaction - if (commit_script) { - coinbaseTx.vout.insert(coinbaseTx.vout.begin(), CTxOut(policyAsset, 0, *commit_script)); + if (commit_scripts && !commit_scripts->empty()) { + for (auto commit_script: *commit_scripts) { + coinbaseTx.vout.push_back(CTxOut(policyAsset, 0, commit_script)); + } } pblock->vtx[0] = MakeTransactionRef(std::move(coinbaseTx)); pblocktemplate->vchCoinbaseCommitment = GenerateCoinbaseCommitment(*pblock, pindexPrev, chainparams.GetConsensus()); diff --git a/src/miner.h b/src/miner.h index b7187a15eb6..03182683d4f 100644 --- a/src/miner.h +++ b/src/miner.h @@ -159,7 +159,7 @@ class BlockAssembler explicit BlockAssembler(CChainState& chainstate, const CTxMemPool& mempool, const CChainParams& params, const Options& options); /** Construct a new block template with coinbase to scriptPubKeyIn */ - std::unique_ptr CreateNewBlock(const CScript& scriptPubKeyIn, std::chrono::seconds min_tx_age = std::chrono::seconds(0), DynaFedParamEntry* = nullptr, CScript const* commit_script = nullptr); + std::unique_ptr CreateNewBlock(const CScript& scriptPubKeyIn, std::chrono::seconds min_tx_age = std::chrono::seconds(0), DynaFedParamEntry* = nullptr, const std::vector* commit_scripts = nullptr); inline static std::optional m_last_block_num_txs{}; inline static std::optional m_last_block_weight{}; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index deb06b0ff65..9a6b0cd8cc0 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -207,6 +207,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "rawreissueasset", 1, "reissuances" }, { "getnewblockhex", 0, "min_tx_age" }, { "getnewblockhex", 1, "proposed_parameters" }, + { "getnewblockhex", 2, "commit_data" }, { "testproposedblock", 1, "acceptnonstd" }, { "issueasset", 0, "assetamount" }, { "issueasset", 1, "tokenamount" }, diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 5d2e9012898..981a85ad254 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -1277,7 +1277,7 @@ static RPCHelpMan getnewblockhex() "\nGets hex representation of a proposed, unmined new block\n", { {"min_tx_age", RPCArg::Type::NUM, RPCArg::Default{0}, "How many seconds a transaction must have been in the mempool to be included in the block proposal. This may help with faster block convergence among functionaries using compact blocks."}, - {"proposed_parameters", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED , "Parameters to be used in dynamic federations blocks as proposals. During a period of `-dynamic_epoch_length` blocks, 4/5 of total blocks must signal these parameters for the proposal to become activated in the next epoch.", + {"proposed_parameters", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "Parameters to be used in dynamic federations blocks as proposals. During a period of `-dynamic_epoch_length` blocks, 4/5 of total blocks must signal these parameters for the proposal to become activated in the next epoch.", { {"signblockscript", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Hex-encoded block signing script to propose"}, {"max_block_witness", RPCArg::Type::NUM, RPCArg::Optional::NO, "Total size in witness bytes that are allowed in the dynamic federations block witness for blocksigning"}, @@ -1289,7 +1289,11 @@ static RPCHelpMan getnewblockhex() }, }, "proposed_parameters"}, - {"commit_data", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED_NAMED_ARG, "Data in hex to be committed to in an additional coinbase output."}, + {"commit_data", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "Array of data in hex to be committed to in additional coinbase outputs.", + { + {"", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Hex encoded string for commit data"}, + }, + }, }, RPCResult{ RPCResult::Type::STR_HEX, "blockhex", "the block hex", @@ -1346,18 +1350,31 @@ static RPCHelpMan getnewblockhex() proposed.m_serialize_type = 2; } - // Any commitment required for non-consensus reasons. - // This will be placed in the first coinbase output. - CScript data_commitment; + // Any commitments required for non-consensus reasons. + // These will be placed in the first coinbase outputs. + std::vector data_commitments; if (!request.params[2].isNull()) { - std::vector data_bytes = ParseHex(request.params[2].get_str()); - data_commitment = CScript() << OP_RETURN << data_bytes; + UniValue commitments(UniValue::VARR); + + // backwards compatibility: attempt to parse as a string first + if (request.params[2].isStr()) { + UniValue hex = request.params[2].get_str(); + commitments.push_back(hex); + } else { + commitments = request.params[2].get_array(); + } + + for (unsigned int i = 0; i < commitments.size(); i++) { + std::vector data_bytes = ParseHex(commitments[i].get_str()); + CScript data_commitment = CScript() << OP_RETURN << data_bytes; + data_commitments.push_back(data_commitment); + } } CScript feeDestinationScript = Params().GetConsensus().mandatory_coinbase_destination; if (feeDestinationScript == CScript()) feeDestinationScript = CScript() << OP_TRUE; const NodeContext& node = EnsureAnyNodeContext(request.context); - std::unique_ptr pblocktemplate(BlockAssembler(chainman.ActiveChainstate(), *node.mempool, Params()).CreateNewBlock(feeDestinationScript, std::chrono::seconds(required_wait), &proposed, data_commitment.empty() ? nullptr : &data_commitment)); + std::unique_ptr pblocktemplate(BlockAssembler(chainman.ActiveChainstate(), *node.mempool, Params()).CreateNewBlock(feeDestinationScript, std::chrono::seconds(required_wait), &proposed, data_commitments.empty() ? nullptr : &data_commitments)); if (!pblocktemplate.get()) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Wallet keypool empty"); } diff --git a/test/functional/rpc_getnewblockhex.py b/test/functional/rpc_getnewblockhex.py new file mode 100755 index 00000000000..118920fc366 --- /dev/null +++ b/test/functional/rpc_getnewblockhex.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Test getnewblockhex +""" +from io import BytesIO + +from test_framework.messages import CBlock +from test_framework.p2p import ( + P2PDataStore, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + hex_str_to_bytes, +) + +class GetNewBlockHexTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = False + self.num_nodes = 1 + + def run_test(self): + self.log.info("Starting test...") + self.bootstrap_p2p() + + node = self.nodes[0] + + height = node.getblockcount() + assert_equal(height, 200) + + self.log.info("Call getnewblockhex with no args") + hex = node.getnewblockhex() + (height, hash) = self.mine_block(hex) + assert_equal(height, 201) + + self.log.info("Call getnewblockhex with single string commitment") + hex = node.getnewblockhex(0, None, "1234") + assert "6a021234" in hex + (height, hash) = self.mine_block(hex) + assert_equal(height, 202) + block = node.getblock(hash, 2) + vout = block["tx"][0]["vout"] + assert_equal(vout[1]["scriptPubKey"]["hex"], "6a021234") + + self.log.info("Call getnewblockhex with single string commitment with spaces") + # ParseHex only validates hex chars, so spaces skipped + hex = node.getnewblockhex(0, None, "1234 5678") + assert "6a0412345678" in hex + (height, hash) = self.mine_block(hex) + assert_equal(height, 203) + block = node.getblock(hash, 2) + vout = block["tx"][0]["vout"] + assert_equal(vout[1]["scriptPubKey"]["hex"], "6a0412345678") + + self.log.info("Call getnewblockhex with single commitment") + hex = node.getnewblockhex(0, None, ["1234"]) + assert "6a021234" in hex + (height, hash) = self.mine_block(hex) + assert_equal(height, 204) + block = node.getblock(hash, 2) + vout = block["tx"][0]["vout"] + assert_equal(vout[1]["scriptPubKey"]["hex"], "6a021234") + + self.log.info("Call getnewblockhex with multiple commitments") + hex = node.getnewblockhex(0, None, ["1234", "deadbeef"]) + assert "6a021234" in hex + assert "6a04deadbeef" in hex + (height, hash) = self.mine_block(hex) + assert_equal(height, 205) + block = node.getblock(hash, 2) + vout = block["tx"][0]["vout"] + assert_equal(vout[1]["scriptPubKey"]["hex"], "6a021234") + assert_equal(vout[2]["scriptPubKey"]["hex"], "6a04deadbeef") + + hex = node.getnewblockhex(0, None, ["1234", "dead", "cafe", "babe"]) + assert "6a021234" in hex + assert "6a02dead" in hex + assert "6a02cafe" in hex + assert "6a02babe" in hex + (height, hash) = self.mine_block(hex) + assert_equal(height, 206) + block = node.getblock(hash, 2) + vout = block["tx"][0]["vout"] + assert_equal(vout[1]["scriptPubKey"]["hex"], "6a021234") + assert_equal(vout[2]["scriptPubKey"]["hex"], "6a02dead") + assert_equal(vout[3]["scriptPubKey"]["hex"], "6a02cafe") + assert_equal(vout[4]["scriptPubKey"]["hex"], "6a02babe") + + self.log.info("Done.") + + def mine_block(self, hex): + """Mine and submit a block from a given hex template. + + Returns a tuple of the new chain height and the block hash.""" + + bytes = hex_str_to_bytes(hex) + block = CBlock() + block.deserialize(BytesIO(bytes)) + block.solve() + self.send_blocks([block], success=True) + height = self.nodes[0].getblockcount() + return (height, block.hash) + + def bootstrap_p2p(self, timeout=10): + """Add a P2P connection to the node. + + Helper to connect and wait for version handshake.""" + self.helper_peer = self.nodes[0].add_p2p_connection(P2PDataStore()) + # We need to wait for the initial getheaders from the peer before we + # start populating our blockstore. If we don't, then we may run ahead + # to the next subtest before we receive the getheaders. We'd then send + # an INV for the next block and receive two getheaders - one for the + # IBD and one for the INV. We'd respond to both and could get + # unexpectedly disconnected if the DoS score for that error is 50. + self.helper_peer.wait_for_getheaders(timeout=timeout) + + def reconnect_p2p(self, timeout=60): + """Tear down and bootstrap the P2P connection to the node. + + The node gets disconnected several times in this test. This helper + method reconnects the p2p and restarts the network thread.""" + self.nodes[0].disconnect_p2ps() + self.bootstrap_p2p(timeout=timeout) + + def send_blocks(self, blocks, success=True, reject_reason=None, force_send=False, reconnect=False, timeout=960): + """Sends blocks to test node. Syncs and verifies that tip has advanced to most recent block. + + Call with success = False if the tip shouldn't advance to the most recent block.""" + self.helper_peer.send_blocks_and_test(blocks, self.nodes[0], success=success, reject_reason=reject_reason, force_send=force_send, timeout=timeout, expect_disconnect=reconnect) + + if reconnect: + self.reconnect_p2p(timeout=timeout) + + +if __name__ == '__main__': + GetNewBlockHexTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 9e4eaa58c32..ab0268f9222 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -108,6 +108,7 @@ 'feature_assetsdir.py', 'feature_initial_reissuance_token.py', 'feature_progress.py', + 'rpc_getnewblockhex.py', # Longest test should go first, to favor running tests in parallel 'wallet_hd.py --legacy-wallet', 'wallet_hd.py --descriptors',