As suggested in #25787, this PR adds tests for datacarrier and datacarriersize initialization options.
Close #25787.
Concept ACK
Thanks for improving test coverage! Seems like the implementation can be simplified in many ways, I'd suggest to:
ADDRESS_OP_TRUE), since we are only concerned with modifying outputs here; there is no need to use the P2PK modeself.wallet.rescan_utxos() (see many other tests, e.g. p2p_filter.py)create_null_data_transaction helper by creating a zero-fee template tx with MiniWallet's create_self_transfer (we are not interested in setting the inputs manually), append the datacarrier output and then deduct the fee manually, e.g. something like this should be sufficient (admittedly, still a bit hacky though):tx = self.wallet.create_self_transfer(fee_rate=0)["tx"]
tx.vout.append(CTxOut(nValue=0, scriptPubKey=script))
tx.vout[0].nValue -= tx.get_vsize() # simply pay 1sat/vbyte fee
return tx
319 | @@ -320,6 +320,7 @@ 320 | 'feature_unsupported_utxo_db.py', 321 | 'feature_logging.py', 322 | 'feature_anchors.py', 323 | + 'feature_datacarrier.py',
I'd maybe suggest naming this mempool_datacarrier.py.
47 | + tx = self.create_null_data_transaction(script) 48 | + 49 | + self.test_transaction(tx, success=True) 50 | + 51 | + self.log.info("Testing a null data transaction larger than allowed by the default -datacarriersize value.") 52 | + data = b"Hello world!" * 10
data = b"Hello world!" * 7 would have worked just as well here.
I'd recommend making these values just above the limits to detect off-by-one errors. For example, instead of composing it of "hello world" strings, use a MAX_OP_RETURN_RELAY-length string.
Done in https://github.com/bitcoin/bitcoin/pull/25792/commits/10bfe1204fed71ff10d6bbac90912804894d454b. Thanks for the review.
Concept ACK, thanks for working on this.
To test differences between a default and a configured node, I'd recommend you create 2 nodes with different policies and submit transactions to both, instead of restarting.
Concept ACK
37 | + tx.vout[0].nValue -= tx.get_vsize() # simply pay 1sat/vbyte fee 38 | + return tx 39 | + 40 | + def run_test(self): 41 | + self.node = self.nodes[0] 42 | + self.wallet = MiniWallet(self.nodes[0])
nit (feel free to ignore)
self.wallet = MiniWallet(self.node)
Or if self.node is initialized in set_test_params(), test_transaction() can also use it.
14 | +from test_framework.messages import ( 15 | + CTxOut, 16 | +) 17 | +from test_framework.util import ( 18 | + assert_raises_rpc_error, 19 | +)
nit: I think one-line alphabetically sorted is a bit easier to read
from test_framework.messages import CTxOut
from test_framework.script import CScript, OP_RETURN
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_raises_rpc_error
from test_framework.wallet import MiniWallet
AFAIK, we usually prefer multi-line over single-line imports in order to avoid potential merge conflicts. I opened a PR to hopefully get some consensus on how strict this (so far unwritten) rule should be followed: https://github.com/bitcoin/bitcoin/pull/25811
16 | +) 17 | +from test_framework.util import ( 18 | + assert_raises_rpc_error, 19 | +) 20 | + 21 | +class DataCarrierTest(BitcoinTestFramework):
nit: PEP8 requires 2 blank lines before class definition
25 | + def test_transaction(self, tx, success): 26 | + tx_hex = tx.serialize().hex() 27 | + 28 | + if success: 29 | + self.wallet.sendrawtransaction(from_node=self.nodes[0], tx_hex=tx_hex) 30 | + assert(tx.rehash() in self.nodes[0].getrawmempool(True))
adding a short message can help debugging:
assert(tx.rehash() in self.nodes[0].getrawmempool(True), f"{tx_hex} not in mempool")
66 | + self.restart_node(0, extra_args=["-datacarrier=true", "-datacarriersize=5"]) 67 | + 68 | + tx4 = self.create_null_data_transaction(script) 69 | + 70 | + self.test_transaction(tx4, success=False) 71 | +
nit: missing whiteline
29 | + self.wallet.sendrawtransaction(from_node=self.nodes[0], tx_hex=tx_hex) 30 | + assert(tx.rehash() in self.nodes[0].getrawmempool(True)) 31 | + else: 32 | + assert_raises_rpc_error(-26, "scriptpubkey", self.wallet.sendrawtransaction, from_node=self.nodes[0], tx_hex=tx_hex) 33 | + 34 | + def create_null_data_transaction(self, script):
nit: the style guidelines suggest adding type hints
def create_null_data_transaction(self, script: CScript) -> CTransaction:
48 | + 49 | + self.test_transaction(tx, success=True) 50 | + 51 | + self.log.info("Testing a null data transaction larger than allowed by the default -datacarriersize value.") 52 | + data = b"Hello world!" * 10 53 | + script2 = CScript([OP_RETURN, data])
nit: I think this variable name could be a bit more descriptive (and script could be renamed to short_script)
too_long_script = CScript([OP_RETURN, data])
Done in https://github.com/bitcoin/bitcoin/commit/10bfe1204fed71ff10d6bbac90912804894d454b. Thanks for the review.
Approach ACK 8677781f4
Just left a few style nits
38 | + MAX_OP_RETURN_DATA_SIZE = 80 39 | + 40 | + self.wallet = MiniWallet(self.nodes[0]) 41 | + self.wallet.rescan_utxos() 42 | + 43 | + data = os.urandom(MAX_OP_RETURN_DATA_SIZE)
Would suggest to use the deterministic random_bytes helper instead (to be found in test_framework.util). (Background: also used the urandom approach for generating random data recently in a PR and got a review with good counter-arguments: #25625 (review))
Great suggestions! All have been addressed in 1650f9457bc1c3cfecde554522f3c38ca18ef04c.
LGTM. Some more suggestions (feel free to ignore, or do in a follow-up):
create_null_data_transaction method would be IMHO more logical if only the null-data payload (type bytes) is passed; the scriptPubKey with prepended OP_RETURN could then be created in there instead of the need to do that repeatedly at the caller's sidedatacarriersize limitMAX_OP_RETURN_RELAY (which is 83 and not 80 though, as it denotes the serialized size of the whole scriptPubKey; "+1 for OP_RETURN, 2 for the pushdata opcodes")<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--174a7506f384e20aa4161008e828411d-->
No conflicts as of last run.
ACK 1650f9457bc1c3cfecde554522f3c38ca18ef04c
edit: modulo MarcoFalke's comment regarding -datacarrier=true
25 | + def set_test_params(self): 26 | + self.num_nodes = 3 27 | + self.extra_args = [ 28 | + [], 29 | + ["-datacarrier=false"], 30 | + ["-datacarrier=true", f"-datacarriersize={MAX_OP_RETURN_RELAY - 1}"]
This parses as =0 (The test fails regardless of the second option)
Probably a good time to make this an init error finally.
FYI for anyone else confused about this, see the docs of InterpretBool():
non-numeric string values like "foo", return 0
so -datacarrier=false and -datacarrier=true all resolve to -datacarrier=0.
As such, should be:
["-datacarrier=0"],
["-datacarrier=1", f"-datacarriersize={MAX_OP_RETURN_RELAY - 1}"]
Good catch MarcoFalke!
Interesting! I applied the requested change and added a new test that checks that "-datacarrier=1" works.
Rebased.
0 | @@ -0,0 +1,79 @@ 1 | +#!/usr/bin/env python3 2 | +# Copyright (c) 2020-2021 The Bitcoin Core developers 3 | +# Distributed under the MIT software license, see the accompanying 4 | +# file COPYING or http://www.opensource.org/licenses/mit-license.php. 5 | +"""Test datacarrier functionality""" 6 | +from test_framework.messages import ( 7 | + CTxOut, 8 | + CTransaction,
yocto-nit: sorted order
CTransaction,
CTxOut,
ACK 4bebb81622eb71a83db1d46ca84c09f7e1ab7f9a
I think the methods test_transaction and create_null_data_transaction could be merged into one (e.g. something like test_null_data_transaction with node, data and success parameters) to reduce it to even less code at the callers side, since we don't reuse any of the transactions anyway, but no hard feelings.
re-ACK 4bebb81 - verified changes are limited to fixing the startup arguments and adding a new test to check the happy path with a lower-than-default datacarriersize
<details> <summary>git range-diff 93999a5fb 1650f94 4bebb81</summary>
1: 1650f9457 ! 1: 4bebb8162 test: add tests for `datacarrier` and `datacarriersize` options
@@ test/functional/mempool_datacarrier.py (new)
+ self.num_nodes = 3
+ self.extra_args = [
+ [],
-+ ["-datacarrier=false"],
-+ ["-datacarrier=true", f"-datacarriersize={MAX_OP_RETURN_RELAY - 1}"]
++ ["-datacarrier=0"],
++ ["-datacarrier=1", f"-datacarriersize={MAX_OP_RETURN_RELAY - 1}"]
+ ]
+
+ def test_transaction(self, node: TestNode, tx: CTransaction, success: bool):
@@ test/functional/mempool_datacarrier.py (new)
+ # By default, only 80 bytes are used for data (+1 for OP_RETURN, +2 for the pushdata opcodes).
+ default_size_data = random_bytes(MAX_OP_RETURN_RELAY - 3)
+ too_long_data = random_bytes(MAX_OP_RETURN_RELAY - 2)
++ small_data = random_bytes(MAX_OP_RETURN_RELAY - 4)
+
+ self.log.info("Testing null data transaction with default -datacarrier and -datacarriersize values.")
+ tx = self.create_null_data_transaction(default_size_data)
@@ test/functional/mempool_datacarrier.py (new)
+ tx3 = self.create_null_data_transaction(default_size_data)
+ self.test_transaction(self.nodes[1], tx3, success=False)
+
-+ self.log.info("Testing a null data transaction with a small -datacarriersize value.")
++ self.log.info("Testing a null data transaction with a size larger than accepted by -datacarriersize.")
+ tx4 = self.create_null_data_transaction(default_size_data)
+ self.test_transaction(self.nodes[2], tx4, success=False)
+
++ self.log.info("Testing a null data transaction with a size smaller than accepted by -datacarriersize.")
++ tx5 = self.create_null_data_transaction(small_data)
++ self.test_transaction(self.nodes[2], tx5, success=True)
++
+
+if __name__ == '__main__':
+ DataCarrierTest().main()
## test/functional/test_framework/messages.py ##
-@@ test/functional/test_framework/messages.py: FILTER_TYPE_BASIC = 0
-
- WITNESS_SCALE_FACTOR = 4
+@@ test/functional/test_framework/messages.py: WITNESS_SCALE_FACTOR = 4
+ DEFAULT_ANCESTOR_LIMIT = 25 # default max number of in-mempool ancestors
+ DEFAULT_DESCENDANT_LIMIT = 25 # default max number of in-mempool descendants
+# Default setting for -datacarriersize. 80 bytes of data, +1 for OP_RETURN, +2 for the pushdata opcodes.
+MAX_OP_RETURN_RELAY = 83
</details>
Co-authored-by: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
re-ACK 8b3d2bbd0d3c87f1a68586822ddc468e0c2f9b52
67 | @@ -68,6 +68,9 @@ 68 | DEFAULT_ANCESTOR_LIMIT = 25 # default max number of in-mempool ancestors 69 | DEFAULT_DESCENDANT_LIMIT = 25 # default max number of in-mempool descendants 70 | 71 | +# Default setting for -datacarriersize. 80 bytes of data, +1 for OP_RETURN, +2 for the pushdata opcodes. 72 | +MAX_OP_RETURN_RELAY = 83
Not sure if we want to use messages.py for default values of settings. Maybe leave them in the only test that needs them for now (if there is only one), or create a new default_settings.py module (or a mempool.py one?)