From a7911ed101ff5b6b624a207f1526b1c347bf635f Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Tue, 4 Mar 2025 09:55:57 -0500 Subject: [PATCH 1/3] test: introduce VARINT (de)serialization routines --- test/functional/test_framework/messages.py | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 9ebb683a9db..1ba48f9a480 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -120,6 +120,26 @@ def deser_compact_size(f): return nit +def ser_varint(l): + r = b"" + while True: + r = bytes([(l & 0x7f) | (0x80 if len(r) > 0 else 0x00)]) + r + if l <= 0x7f: + return r + l = (l >> 7) - 1 + + +def deser_varint(f): + n = 0 + while True: + dat = f.read(1)[0] + n = (n << 7) | (dat & 0x7f) + if (dat & 0x80) > 0: + n += 1 + else: + return n + + def deser_string(f): nit = deser_compact_size(f) return f.read(nit) @@ -1913,3 +1933,20 @@ class TestFrameworkScript(unittest.TestCase): check_addrv2("2bqghnldu6mcug4pikzprwhtjjnsyederctvci6klcwzepnjd46ikjyd.onion", CAddress.NET_TORV3) check_addrv2("255fhcp6ajvftnyo7bwz3an3t4a4brhopm3bamyh2iu5r3gnr2rq.b32.i2p", CAddress.NET_I2P) check_addrv2("fc32:17ea:e415:c3bf:9808:149d:b5a2:c9aa", CAddress.NET_CJDNS) + + def test_varint_encode_decode(self): + def check_varint(num, expected_encoding_hex): + expected_encoding = bytes.fromhex(expected_encoding_hex) + self.assertEqual(ser_varint(num), expected_encoding) + self.assertEqual(deser_varint(BytesIO(expected_encoding)), num) + + # test cases from serialize_tests.cpp:varint_bitpatterns + check_varint(0, "00") + check_varint(0x7f, "7f") + check_varint(0x80, "8000") + check_varint(0x1234, "a334") + check_varint(0xffff, "82fe7f") + check_varint(0x123456, "c7e756") + check_varint(0x80123456, "86ffc7e756") + check_varint(0xffffffff, "8efefefe7f") + check_varint(0xffffffffffffffff, "80fefefefefefefefe7f") From b34fdb5ade0b48384636f8c7c9673554bf82cedf Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Tue, 4 Mar 2025 09:57:49 -0500 Subject: [PATCH 2/3] test: introduce output amount (de)compression routines --- .../feature_framework_unit_tests.py | 1 + test/functional/test_framework/compressor.py | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 test/functional/test_framework/compressor.py diff --git a/test/functional/feature_framework_unit_tests.py b/test/functional/feature_framework_unit_tests.py index 14d83f8a707..4ee2143ad6c 100755 --- a/test/functional/feature_framework_unit_tests.py +++ b/test/functional/feature_framework_unit_tests.py @@ -18,6 +18,7 @@ TEST_FRAMEWORK_MODULES = [ "address", "crypto.bip324_cipher", "blocktools", + "compressor", "crypto.chacha20", "crypto.ellswift", "key", diff --git a/test/functional/test_framework/compressor.py b/test/functional/test_framework/compressor.py new file mode 100644 index 00000000000..1c30d749df5 --- /dev/null +++ b/test/functional/test_framework/compressor.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Routines for compressing transaction output amounts and scripts.""" +import unittest + +from .messages import COIN + + +def compress_amount(n): + if n == 0: + return 0 + e = 0 + while ((n % 10) == 0) and (e < 9): + n //= 10 + e += 1 + if e < 9: + d = n % 10 + assert (d >= 1 and d <= 9) + n //= 10 + return 1 + (n*9 + d - 1)*10 + e + else: + return 1 + (n - 1)*10 + 9 + + +def decompress_amount(x): + if x == 0: + return 0 + x -= 1 + e = x % 10 + x //= 10 + n = 0 + if e < 9: + d = (x % 9) + 1 + x //= 9 + n = x * 10 + d + else: + n = x + 1 + while e > 0: + n *= 10 + e -= 1 + return n + + +class TestFrameworkCompressor(unittest.TestCase): + def test_amount_compress_decompress(self): + def check_amount(amount, expected_compressed): + self.assertEqual(compress_amount(amount), expected_compressed) + self.assertEqual(decompress_amount(expected_compressed), amount) + + # test cases from compress_tests.cpp:compress_amounts + check_amount(0, 0x0) + check_amount(1, 0x1) + check_amount(1000000, 0x7) + check_amount(COIN, 0x9) + check_amount(50*COIN, 0x32) + check_amount(21000000*COIN, 0x1406f40) From e5ff4e416ecc8a51367022eb8a7a291f8cbc0c65 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Wed, 19 Feb 2025 09:46:06 -0500 Subject: [PATCH 3/3] qa: use a clearer and documented amount error in malleated snapshot In the assumeutxo functional tests, the final test case with alternated UTxO data tests the error raised when deserializing a snapshot that contains a coin with an amount not in range (<0 or >MAX_MONEY). The current malleation uses an undocumented byte string and offset which makes it hard to maintain. In addition, the undocumented offset is set surprisingly high (39 bytes is well into the serialization of the amount which starts at offset 36). Similarly the value is surprisingly small, presumably one was adjusted for the other. But there is no comment explaining how they were chosen, why not in a clearer manner and what they are supposed to represent. Instead replace this seemingly magic value with a clear one, MAX_MONEY + 1, serialize the whole value for the amount field at the correct offset, and document the whole thing for the next person around. --- test/functional/feature_assumeutxo.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 2d4e33e5d87..41c6bda3569 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -16,11 +16,16 @@ from test_framework.blocktools import ( create_block, create_coinbase ) +from test_framework.compressor import ( + compress_amount, +) from test_framework.messages import ( CBlockHeader, from_hex, msg_headers, - tx_from_hex + tx_from_hex, + ser_varint, + MAX_MONEY, ) from test_framework.p2p import ( P2PInterface, @@ -139,7 +144,14 @@ class AssumeutxoTest(BitcoinTestFramework): [b"\x81", 34, "3da966ba9826fb6d2604260e01607b55ba44e1a5de298606b08704bc62570ea8", None], # wrong coin code VARINT [b"\x80", 34, "091e893b3ccb4334378709578025356c8bcb0a623f37c7c4e493133c988648e5", None], # another wrong coin code [b"\x84\x58", 34, None, "Bad snapshot data after deserializing 0 coins"], # wrong coin case with height 364 and coinbase 0 - [b"\xCA\xD2\x8F\x5A", 39, None, "Bad snapshot data after deserializing 0 coins - bad tx out value"], # Amount exceeds MAX_MONEY + [ + # compressed txout value + scriptpubkey + ser_varint(compress_amount(MAX_MONEY + 1)) + ser_varint(0), + # txid + coins per txid + vout + coin height + 32 + 1 + 1 + 2, + None, + "Bad snapshot data after deserializing 0 coins - bad tx out value" + ], # Amount exceeds MAX_MONEY ] for content, offset, wrong_hash, custom_message in cases: