From 51ce91f7ab651da451517bc991d2984e6d95d6a7 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Fri, 17 Apr 2026 03:05:48 +0700 Subject: [PATCH 01/15] feat: add utils to parse platform bech32m destinations --- src/chainparams.cpp | 12 ++++++ src/chainparams.h | 3 ++ src/key_io.cpp | 97 +++++++++++++++++++++++++++++++++++++++++++++ src/key_io.h | 31 +++++++++++++++ 4 files changed, 143 insertions(+) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 377b6b5af5b7..2d7910e34672 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -260,6 +260,9 @@ class CMainParams : public CChainParams { // Dash BIP32 prvkeys start with 'xprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4}; + // DIP-18 Dash Platform address HRP (bech32m) + bech32_platform_hrp = "dash"; + // Dash BIP44 coin type is '5' nExtCoinType = 5; @@ -452,6 +455,9 @@ class CTestNetParams : public CChainParams { // Testnet Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; + // DIP-18 Dash Platform address HRP (bech32m) + bech32_platform_hrp = "tdash"; + // Testnet Dash BIP44 coin type is '1' (All coin's testnet default) nExtCoinType = 1; @@ -625,6 +631,9 @@ class CDevNetParams : public CChainParams { // Testnet Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; + // DIP-18 Dash Platform address HRP (bech32m) + bech32_platform_hrp = "tdash"; + // Testnet Dash BIP44 coin type is '1' (All coin's testnet default) nExtCoinType = 1; @@ -900,6 +909,9 @@ class CRegTestParams : public CChainParams { // Regtest Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; + // DIP-18 Dash Platform address HRP (bech32m) + bech32_platform_hrp = "tdash"; + // Regtest Dash BIP44 coin type is '1' (All coin's testnet default) nExtCoinType = 1; diff --git a/src/chainparams.h b/src/chainparams.h index 7b5528b9b550..058570124306 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -124,6 +124,8 @@ class CChainParams /** Return the list of hostnames to look up for DNS seeds */ const std::vector& DNSSeeds() const { return vSeeds; } const std::vector& Base58Prefix(Base58Type type) const { return base58Prefixes[type]; } + /** DIP-18 Platform address bech32m HRP: "dash" on mainnet, "tdash" on test chains */ + const std::string& Bech32PlatformHRP() const { return bech32_platform_hrp; } int ExtCoinType() const { return nExtCoinType; } const std::vector& FixedSeeds() const { return vFixedSeeds; } const CCheckpointData& Checkpoints() const { return checkpointData; } @@ -164,6 +166,7 @@ class CChainParams uint64_t m_assumed_chain_state_size; std::vector vSeeds; std::vector base58Prefixes[MAX_BASE58_TYPES]; + std::string bech32_platform_hrp; int nExtCoinType; std::string strNetworkID; CBlock genesis; diff --git a/src/key_io.cpp b/src/key_io.cpp index 0136857973fa..b90c08bb13dd 100644 --- a/src/key_io.cpp +++ b/src/key_io.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -176,3 +177,99 @@ bool IsValidDestinationString(const std::string& str) { return IsValidDestinationString(str, Params()); } + +namespace { +constexpr uint8_t DIP18_TYPE_BYTE_P2PKH = 0xb0; +constexpr uint8_t DIP18_TYPE_BYTE_P2SH = 0x80; +constexpr size_t DIP18_PAYLOAD_SIZE = 21; // 1 type byte + 20-byte HASH160 + +std::string EncodePlatformBech32m(const CChainParams& params, uint8_t type_byte, const BaseHash& hash) +{ + std::vector payload; + payload.reserve(DIP18_PAYLOAD_SIZE); + payload.push_back(type_byte); + payload.insert(payload.end(), hash.begin(), hash.end()); + std::vector values; + values.reserve(((DIP18_PAYLOAD_SIZE * 8) + 4) / 5); + ConvertBits<8, 5, true>([&](uint8_t v) { values.push_back(v); }, payload.begin(), payload.end()); + return bech32::Encode(bech32::Encoding::BECH32M, params.Bech32PlatformHRP(), values); +} + +class PlatformDestinationEncoder +{ +private: + const CChainParams& m_params; + +public: + explicit PlatformDestinationEncoder(const CChainParams& params) : m_params(params) {} + + std::string operator()(const PlatformP2PKHDestination& id) const + { + return EncodePlatformBech32m(m_params, DIP18_TYPE_BYTE_P2PKH, id); + } + std::string operator()(const PlatformP2SHDestination& id) const + { + return EncodePlatformBech32m(m_params, DIP18_TYPE_BYTE_P2SH, id); + } + std::string operator()(const CNoDestination&) const { return {}; } +}; +} // namespace + +bool IsValidPlatformDestination(const PlatformDestination& dest) +{ + return !std::holds_alternative(dest); +} + +std::string EncodePlatformDestination(const PlatformDestination& dest) +{ + return std::visit(PlatformDestinationEncoder(Params()), dest); +} + +PlatformDestination DecodePlatformDestination(const std::string& str, const CChainParams& params, std::string& error_str) +{ + error_str.clear(); + const bech32::DecodeResult dec = bech32::Decode(str); + if (dec.encoding == bech32::Encoding::INVALID) { + error_str = "Invalid bech32m encoding"; + return CNoDestination(); + } + if (dec.encoding != bech32::Encoding::BECH32M) { + error_str = "DIP-18 Platform addresses require bech32m checksum"; + return CNoDestination(); + } + if (dec.hrp != params.Bech32PlatformHRP()) { + error_str = "Invalid Platform HRP for the selected network"; + return CNoDestination(); + } + std::vector payload; + payload.reserve((dec.data.size() * 5) / 8); + if (!ConvertBits<5, 8, false>([&](uint8_t b) { payload.push_back(b); }, dec.data.begin(), dec.data.end())) { + error_str = "Invalid Platform address payload encoding"; + return CNoDestination(); + } + if (payload.size() != DIP18_PAYLOAD_SIZE) { + error_str = "Invalid Platform address payload length"; + return CNoDestination(); + } + uint160 hash; + std::copy(payload.begin() + 1, payload.end(), hash.begin()); + switch (payload[0]) { + case DIP18_TYPE_BYTE_P2PKH: + return PlatformP2PKHDestination(hash); + case DIP18_TYPE_BYTE_P2SH: + return PlatformP2SHDestination(hash); + } + error_str = "Unknown DIP-18 type byte"; + return CNoDestination(); +} + +PlatformDestination DecodePlatformDestination(const std::string& str, std::string& error_str) +{ + return DecodePlatformDestination(str, Params(), error_str); +} + +PlatformDestination DecodePlatformDestination(const std::string& str) +{ + std::string error_str; + return DecodePlatformDestination(str, error_str); +} diff --git a/src/key_io.h b/src/key_io.h index 605a087433f3..11bbe5cd6fb1 100644 --- a/src/key_io.h +++ b/src/key_io.h @@ -28,4 +28,35 @@ CTxDestination DecodeDestination(const std::string& str, std::string& error_msg) bool IsValidDestinationString(const std::string& str); bool IsValidDestinationString(const std::string& str, const CChainParams& params); +/** + * DIP-18 Dash Platform addresses (bech32m). + * + * Platform addresses decode to a 20-byte HASH160 prefixed by a type byte: + * 0xb0 -> Platform P2PKH (addresses of the form dash1k... / tdash1k...) + * 0x80 -> Platform P2SH (addresses of the form dash1s... / tdash1s...) + * + * Unlike base58 Dash addresses, Platform destinations have no on-chain + * scriptPubKey: they are only valid as credit output recipients of an + * asset-lock special transaction (see DIP-27 and src/evo/assetlocktx.h). + */ +struct PlatformP2PKHDestination : public BaseHash +{ + PlatformP2PKHDestination() = default; + explicit PlatformP2PKHDestination(const uint160& hash) : BaseHash(hash) {} +}; + +struct PlatformP2SHDestination : public BaseHash +{ + PlatformP2SHDestination() = default; + explicit PlatformP2SHDestination(const uint160& hash) : BaseHash(hash) {} +}; + +using PlatformDestination = std::variant; + +bool IsValidPlatformDestination(const PlatformDestination& dest); +std::string EncodePlatformDestination(const PlatformDestination& dest); +PlatformDestination DecodePlatformDestination(const std::string& str); +PlatformDestination DecodePlatformDestination(const std::string& str, std::string& error_str); +PlatformDestination DecodePlatformDestination(const std::string& str, const CChainParams& params, std::string& error_str); + #endif // BITCOIN_KEY_IO_H From c3261a9ae7ce7daa76bc8a6c1d9522bd46791244 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Fri, 17 Apr 2026 03:24:50 +0700 Subject: [PATCH 02/15] test: regression tests for platform addresses --- src/test/key_io_tests.cpp | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/test/key_io_tests.cpp b/src/test/key_io_tests.cpp index 827c6cc71e18..55a0b22f55fc 100644 --- a/src/test/key_io_tests.cpp +++ b/src/test/key_io_tests.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -146,4 +147,102 @@ BOOST_AUTO_TEST_CASE(key_io_invalid) } } +// DIP-18: Dash Platform bech32m address encoding. +BOOST_AUTO_TEST_CASE(dip18_platform_roundtrip) +{ + struct Sample { + std::string hash_hex; + std::string address; + std::string chain; + bool is_p2sh; + }; + // Samples from DIP-0018 (Test Vectors section). + const Sample samples[] = { + {"f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", "dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs", CBaseChainParams::MAIN, false}, + {"a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", "dash1kzjl7qzxy9lar37j8r37z3kvt07epqe20ckxfezw", CBaseChainParams::MAIN, false}, + {"6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", "dash1kpkeye606ez89g7lelp7hnldwwpt76va0v3j6x28", CBaseChainParams::MAIN, false}, + {"f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", "tdash1krma5z3ttj75la4m93xcndna9ullamq9y5fzq2j7", CBaseChainParams::TESTNET, false}, + {"a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", "tdash1kzjl7qzxy9lar37j8r37z3kvt07epqe20cxp68nq", CBaseChainParams::TESTNET, false}, + {"6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", "tdash1kpkeye606ez89g7lelp7hnldwwpt76va0vp4fcmf", CBaseChainParams::TESTNET, false}, + {"43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636", "dash1sppl5xpu70aka8nacc4kj2htflydspzkxch4cad6", CBaseChainParams::MAIN, true}, + {"43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636", "tdash1sppl5xpu70aka8nacc4kj2htflydspzkxc8jtru5", CBaseChainParams::TESTNET, true}, + }; + for (const auto& s : samples) { + SelectParams(s.chain); + std::string err; + PlatformDestination dest = DecodePlatformDestination(s.address, err); + BOOST_REQUIRE_MESSAGE(IsValidPlatformDestination(dest), + std::string{"decode failed: "} + s.address + " err=" + err); + std::vector got_hash; + if (s.is_p2sh) { + BOOST_REQUIRE(std::holds_alternative(dest)); + const auto& h = std::get(dest); + got_hash.assign(h.begin(), h.end()); + } else { + BOOST_REQUIRE(std::holds_alternative(dest)); + const auto& h = std::get(dest); + got_hash.assign(h.begin(), h.end()); + } + BOOST_CHECK_EQUAL(HexStr(got_hash), std::string(s.hash_hex)); + BOOST_CHECK_EQUAL(EncodePlatformDestination(dest), std::string(s.address)); + } + SelectParams(CBaseChainParams::MAIN); +} + +BOOST_AUTO_TEST_CASE(dip18_platform_invalid) +{ + SelectParams(CBaseChainParams::MAIN); + std::string err; + + // Wrong HRP for the selected network (testnet string on mainnet). + BOOST_CHECK(!IsValidPlatformDestination( + DecodePlatformDestination("tdash1krma5z3ttj75la4m93xcndna9ullamq9y5fzq2j7", err))); + + // Mixed case is forbidden by BIP-173. + BOOST_CHECK(!IsValidPlatformDestination( + DecodePlatformDestination("Dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs", err))); + + // Bech32 (BIP-173) checksum MUST be rejected; only bech32m is valid for DIP-18. + // Re-encode the same 21-byte payload with the BIP-173 generator and verify rejection. + { + std::vector payload = ParseHex("b0f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525"); + std::vector values; + ConvertBits<8, 5, true>([&](uint8_t b) { values.push_back(b); }, payload.begin(), payload.end()); + const std::string bech32_str = bech32::Encode(bech32::Encoding::BECH32, "dash", values); + BOOST_REQUIRE(!bech32_str.empty()); + BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination(bech32_str, err))); + } + + // Unknown DIP-18 type byte (0x00) must be rejected. + { + std::vector payload = ParseHex("00f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525"); + std::vector values; + ConvertBits<8, 5, true>([&](uint8_t b) { values.push_back(b); }, payload.begin(), payload.end()); + const std::string bad = bech32::Encode(bech32::Encoding::BECH32M, "dash", values); + BOOST_REQUIRE(!bad.empty()); + BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination(bad, err))); + } + + // Wrong payload length (19-byte hash) must be rejected. + { + std::vector payload = ParseHex("b0f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec05"); + std::vector values; + ConvertBits<8, 5, true>([&](uint8_t b) { values.push_back(b); }, payload.begin(), payload.end()); + const std::string bad = bech32::Encode(bech32::Encoding::BECH32M, "dash", values); + BOOST_REQUIRE(!bad.empty()); + BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination(bad, err))); + } + + // Empty / garbage inputs. + BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination("", err))); + BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination("not-an-address", err))); + + // Mainnet address on testnet must fail. + SelectParams(CBaseChainParams::TESTNET); + BOOST_CHECK(!IsValidPlatformDestination( + DecodePlatformDestination("dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs", err))); + + SelectParams(CBaseChainParams::MAIN); +} + BOOST_AUTO_TEST_SUITE_END() From 06e78447ae46a4a1303b98f53e1417dafcbad70c Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Fri, 29 Sep 2017 10:11:01 +0200 Subject: [PATCH 03/15] partial Merge #11167: Full BIP173 (Bech32) support 8213838 [Qt] tolerate BIP173/bech32 addresses during input validation (Jonas Schnelli) 06eaca6 [RPC] Wallet: test importing of native witness scripts (NicolasDorier) fd0041a Use BIP173 addresses in segwit.py test (Pieter Wuille) e278f12 Support BIP173 in addwitnessaddress (Pieter Wuille) c091b99 Implement BIP173 addresses and tests (Pieter Wuille) bd355b8 Add regtest testing to base58_tests (Pieter Wuille) 6565c55 Convert base58_tests from type/payload to scriptPubKey comparison (Pieter Wuille) 8fd2267 Import Bech32 C++ reference code & tests (Pieter Wuille) 1e46ebd Implement {Encode,Decode}Destination without CBitcoinAddress (Pieter Wuille) Pull request description: Builds on top of #11117. This adds support for: * Creating BIP173 addresses for testing (through `addwitnessaddress`, though by default it still produces P2SH versions) * Sending to BIP173 addresses (including non-v0 ones) * Analysing BIP173 addresses (through `validateaddress`) It includes a reformatted version of the [C++ Bech32 reference code](https://github.com/sipa/bech32/tree/master/ref/c%2B%2B) and an independent implementation of the address encoding/decoding logic (integrated with CTxDestination). All BIP173 test vectors are included. Not included (and intended for other PRs): * Full wallet support for SegWit (which would include automatically adding witness scripts to the wallet during automatic keypool topup, SegWit change outputs, ...) [see #11403] * Splitting base58.cpp and tests/base58_tests.cpp up into base58-specific code, and "address encoding"-code [see #11372] * Error locating in UI for BIP173 addresses. Tree-SHA512: 238031185fd07f3ac873c586043970cc2db91bf7735c3c168cb33a3db39a7bda81d4891b649685bb17ef90dc63af0328e7705d8cd3e8dafd6c4d3c08fb230341 --- test/functional/test_framework/segwit_addr.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 test/functional/test_framework/segwit_addr.py diff --git a/test/functional/test_framework/segwit_addr.py b/test/functional/test_framework/segwit_addr.py new file mode 100644 index 000000000000..02368e938fbb --- /dev/null +++ b/test/functional/test_framework/segwit_addr.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# 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.""" + + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in 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 + + +def bech32_create_checksum(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 + 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) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + + +def bech32_decode(bech): + """Validate a Bech32 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) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (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]) + + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) + if decode(hrp, ret) == (None, None): + return None + return ret From e6569adff105ec444e03de72facec1c248dc8dfb Mon Sep 17 00:00:00 2001 From: MarcoFalke Date: Thu, 1 Oct 2020 09:43:00 +0200 Subject: [PATCH 04/15] Merge #19253: Tests: tidy up address.py and segwit_addr.py BACKPORT NOTE: missing changes for segwit_address.py and enabling this unit test 825fcae484f31182041dfacbf820e818d759b130 [tests] Replace bytes literals with hex literals (John Newbery) 64eca45100536579a3849631e59d4277bbc25be1 [tests] Fix pep8 style violations in address.py (John Newbery) b230f8b3f3adcb1e2ae299094f9ae0a8bc7cc3d0 [tests] Correct docstring for address.py (John Newbery) ea70e6a2ca0e183ef40cdb9b3b86f39e94366015 [tests] Tidy up imports in address.py (John Newbery) 7f639df0b8a15aaeccedab00b634925f568c2c9a [tests] Remove unused optional verify_checksum parameter (John Newbery) 011e784f74411bd5d5dbccfd3af39e0937fd8933 [tests] Rename segwit encode and decode functions (John Newbery) e4557133f595f357df5e16ae4f2f19c579631396 [tests] Move bech32 unit tests to test framework (John Newbery) Pull request description: Lots of small fixes: - moving unit tests to test_framework implementation files - renaming functions to be clearer - removing multiple imports - removing unreadable byte literals from the code - fixing pep8 violations - correcting out-of-date docstring ACKs for top commit: jonatack: re-ACK 825fcae484f31182041dfacbf820e818d759b130 per `git range-diff a0a422c 7edcdcd 825fcae` and verified `wallet_address_types.py` and `wallet_basic.py --descriptors` (the failure on one travis job) are green locally. MarcoFalke: ACK 825fcae484f31182041dfacbf820e818d759b130 fanquake: ACK 825fcae484f31182041dfacbf820e818d759b130 - looks ok to me. Tree-SHA512: aea509c27c1bcb94bef11205b6a79836c39c62249672815efc9822f411bc2e2336ceb3d72b3b861c3f4054a08e16edb28c6edd3aa5eff72eec1d60ea6ca82dc4 --- test/functional/test_framework/segwit_addr.py | 22 +++++++++++++++---- test/functional/test_runner.py | 1 + 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/test/functional/test_framework/segwit_addr.py b/test/functional/test_framework/segwit_addr.py index 02368e938fbb..00c0d8a91932 100644 --- a/test/functional/test_framework/segwit_addr.py +++ b/test/functional/test_framework/segwit_addr.py @@ -3,7 +3,7 @@ # 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.""" - +import unittest CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" @@ -84,7 +84,7 @@ def convertbits(data, frombits, tobits, pad=True): return ret -def decode(hrp, addr): +def decode_segwit_address(hrp, addr): """Decode a segwit address.""" hrpgot, data = bech32_decode(addr) if hrpgot != hrp: @@ -99,9 +99,23 @@ def decode(hrp, addr): return (data[0], decoded) -def encode(hrp, witver, witprog): +def encode_segwit_address(hrp, witver, witprog): """Encode a segwit address.""" ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) - if decode(hrp, ret) == (None, None): + if decode_segwit_address(hrp, ret) == (None, None): return None return ret + +class TestFrameworkScript(unittest.TestCase): + def test_segwit_encode_decode(self): + def test_python_bech32(addr): + hrp = addr[:4] + self.assertEqual(hrp, "bcrt") + (witver, witprog) = decode_segwit_address(hrp, addr) + self.assertEqual(encode_segwit_address(hrp, witver, witprog), addr) + + # P2WPKH + test_python_bech32('bcrt1qthmht0k2qnh3wy7336z05lu2km7emzfpm3wg46') + # P2WSH + test_python_bech32('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj') + test_python_bech32('bcrt1qft5p2uhsdcdc3l2ua4ap5qqfg4pjaqlp250x7us7a8qqhrxrxfsqseac85') diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 90c8fbe8e49e..ff7b7998e932 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -83,6 +83,7 @@ "crypto.poly1305", "crypto.ripemd160", "script", + "segwit_addr", ] EXTENDED_SCRIPTS = [ From 7f3f636d1a82f0f07c54c15e33b0b1d6f54aa918 Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Thu, 18 Mar 2021 20:21:00 +0100 Subject: [PATCH 05/15] partial Merge #20861: BIP 350: Implement Bech32m and use it for v1+ segwit addresses BACKPORT NOTE: only test/functional/test_framework/segwit_addr.py changes 03346022d611871f2cc185440b19d928b9264d9d naming nits (Fabian Jahr) 2e7c80fb5be82ad4a3f737cab65b31f70a772a23 Add signet support to gen_key_io_test_vectors.py (Pieter Wuille) fe5e495c31de47b0ec732b943db11fe345d874af Use Bech32m encoding for v1+ segwit addresses (Pieter Wuille) 25b1c6e13ddf1626210d5e3d37298d1f3a78a94f Add Bech32m test vectors (Pieter Wuille) da2bb6976dadeec682d163c258c9afecc87d6428 Implement Bech32m encoding/decoding (Pieter Wuille) Pull request description: This implements [BIP 350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki): * For segwit v1+ addresses, a new checksum algorithm called Bech32m is used. * Segwit v0 address keep using Bech32 as specified in [BIP 173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki). ACKs for top commit: Sjors: utACK 0334602 jnewbery: utACK 03346022d6 achow101: ACK 0334602 fjahr: re-ACK 0334602 benthecarman: ACK 03346022d611871f2cc185440b19d928b9264d9d Tree-SHA512: 4424cfd44869d813d6152fb3ed867b204036736bc2344a039b93700b6f36a43e9110478f138eb81c97c77ab27ecb776dada5ba632cb5a3a9d244924d2540a557 --- test/functional/test_framework/segwit_addr.py | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/test/functional/test_framework/segwit_addr.py b/test/functional/test_framework/segwit_addr.py index 00c0d8a91932..861ca2b949bd 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 @@ def test_python_bech32(addr): # P2WSH test_python_bech32('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj') test_python_bech32('bcrt1qft5p2uhsdcdc3l2ua4ap5qqfg4pjaqlp250x7us7a8qqhrxrxfsqseac85') + # P2TR + test_python_bech32('bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6') From 3130ed697ce00fc9ba7d6543aa432a3098885ec6 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 21 Apr 2026 20:37:05 +0700 Subject: [PATCH 06/15] test: dashify segwit_addr.py and remove bitcoin's specific code --- test/functional/test_framework/segwit_addr.py | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/test/functional/test_framework/segwit_addr.py b/test/functional/test_framework/segwit_addr.py index 861ca2b949bd..16d5f5719431 100644 --- a/test/functional/test_framework/segwit_addr.py +++ b/test/functional/test_framework/segwit_addr.py @@ -99,43 +99,54 @@ def convertbits(data, frombits, tobits, pad=True): return ret -def decode_segwit_address(hrp, addr): - """Decode a segwit address.""" +DIP18_TYPE_P2PKH = 0xb0 +DIP18_TYPE_P2SH = 0x80 + + +def encode_platform_p2pkh(hrp, keyhash): + """Encode a 20-byte keyhash as a DIP-18 Platform P2PKH bech32m address.""" + assert len(keyhash) == 20 + payload = [DIP18_TYPE_P2PKH] + list(keyhash) + data = convertbits(payload, 8, 5) + return bech32_encode(Encoding.BECH32M, hrp, data) + + +def encode_platform_p2sh(hrp, scripthash): + """Encode a 20-byte scripthash as a DIP-18 Platform P2SH bech32m address.""" + assert len(scripthash) == 20 + payload = [DIP18_TYPE_P2SH] + list(scripthash) + data = convertbits(payload, 8, 5) + return bech32_encode(Encoding.BECH32M, hrp, data) + + +def decode_platform_address(hrp, addr): + """Decode a DIP-18 bech32m platform address. Returns (type_byte, hash_bytes) or (None, None).""" encoding, hrpgot, data = bech32_decode(addr) - if hrpgot != hrp: + if encoding != Encoding.BECH32M or hrpgot != hrp: return (None, None) - decoded = convertbits(data[1:], 5, 8, False) - if decoded is None or len(decoded) < 2 or len(decoded) > 40: + payload = convertbits(data, 5, 8, pad=False) + if payload is None or len(payload) != 21: return (None, None) - if data[0] > 16: + type_byte = payload[0] + if type_byte not in (DIP18_TYPE_P2PKH, DIP18_TYPE_P2SH): 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) - + return (type_byte, bytes(payload[1:])) -def encode_segwit_address(hrp, witver, witprog): - """Encode a segwit address.""" - 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 class TestFrameworkScript(unittest.TestCase): - def test_segwit_encode_decode(self): - def test_python_bech32(addr): - hrp = addr[:4] - self.assertEqual(hrp, "bcrt") - (witver, witprog) = decode_segwit_address(hrp, addr) - self.assertEqual(encode_segwit_address(hrp, witver, witprog), addr) - - # P2WPKH - test_python_bech32('bcrt1qthmht0k2qnh3wy7336z05lu2km7emzfpm3wg46') - # P2WSH - test_python_bech32('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj') - test_python_bech32('bcrt1qft5p2uhsdcdc3l2ua4ap5qqfg4pjaqlp250x7us7a8qqhrxrxfsqseac85') - # P2TR - test_python_bech32('bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6') + def test_platform_encode_decode(self): + def test_platform_roundtrip(hrp, addr, expected_type): + typ, payload = decode_platform_address(hrp, addr) + self.assertIsNotNone(typ) + self.assertEqual(typ, expected_type) + if expected_type == DIP18_TYPE_P2PKH: + self.assertEqual(encode_platform_p2pkh(hrp, payload), addr) + else: + self.assertEqual(encode_platform_p2sh(hrp, payload), addr) + + # DIP-18 P2PKH + test_platform_roundtrip('dash', 'dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs', DIP18_TYPE_P2PKH) + test_platform_roundtrip('tdash', 'tdash1krma5z3ttj75la4m93xcndna9ullamq9y5fzq2j7', DIP18_TYPE_P2PKH) + # DIP-18 P2SH + test_platform_roundtrip('dash', 'dash1sppl5xpu70aka8nacc4kj2htflydspzkxch4cad6', DIP18_TYPE_P2SH) + test_platform_roundtrip('tdash', 'tdash1sppl5xpu70aka8nacc4kj2htflydspzkxc8jtru5', DIP18_TYPE_P2SH) From 7a257dc337d4e6b5660e89da8f1f3d476baa84b2 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Fri, 17 Apr 2026 23:25:23 +0700 Subject: [PATCH 07/15] refactor: inline CheckAssetLockUnlockTx function to creditpool --- src/evo/assetlocktx.cpp | 15 --------------- src/evo/assetlocktx.h | 1 - src/evo/creditpool.cpp | 8 ++------ 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/src/evo/assetlocktx.cpp b/src/evo/assetlocktx.cpp index dace5ae1113d..cba0bdc55534 100644 --- a/src/evo/assetlocktx.cpp +++ b/src/evo/assetlocktx.cpp @@ -23,21 +23,6 @@ using node::BlockManager; -/** - * Common code for Asset Lock and Asset Unlock - */ -bool CheckAssetLockUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null pindexPrev, const std::optional& indexes, TxValidationState& state) -{ - switch (tx.nType) { - case TRANSACTION_ASSET_LOCK: - return CheckAssetLockTx(tx, state); - case TRANSACTION_ASSET_UNLOCK: - return CheckAssetUnlockTx(blockman, qman, tx, pindexPrev, indexes, state); - default: - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-not-asset-locks-at-all"); - } -} - /** * Asset Lock Transaction */ diff --git a/src/evo/assetlocktx.h b/src/evo/assetlocktx.h index 0f3c688cd83c..6a00f605f2c0 100644 --- a/src/evo/assetlocktx.h +++ b/src/evo/assetlocktx.h @@ -156,7 +156,6 @@ class CAssetUnlockPayload bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state); bool CheckAssetUnlockTx(const node::BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null pindexPrev, const std::optional& indexes, TxValidationState& state); -bool CheckAssetLockUnlockTx(const node::BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null pindexPrev, const std::optional& indexes, TxValidationState& state); bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state); #endif // BITCOIN_EVO_ASSETLOCKTX_H diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index 287560d936ac..dc49629d1e9b 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -300,18 +300,14 @@ bool CCreditPoolDiff::Unlock(const CTransaction& tx, TxValidationState& state) bool CCreditPoolDiff::ProcessLockUnlockTransaction(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, TxValidationState& state) { if (!tx.IsSpecialTxVersion()) return true; - if (tx.nType != TRANSACTION_ASSET_LOCK && tx.nType != TRANSACTION_ASSET_UNLOCK) return true; - - if (!CheckAssetLockUnlockTx(blockman, qman, tx, pindexPrev, pool.indexes, state)) { - // pass the state returned by the function above - return false; - } try { switch (tx.nType) { case TRANSACTION_ASSET_LOCK: + if (!CheckAssetLockTx(tx, state)) return false; // pass the state set by CheckAssetLockTx return Lock(tx, state); case TRANSACTION_ASSET_UNLOCK: + if (!CheckAssetUnlockTx(blockman, qman, tx, pindexPrev, pool.indexes, state)) return false; // pass the state set by CheckAssetUnlockTx return Unlock(tx, state); default: return true; From 89cadefecb5926a10d45869573139152c111ed17 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sat, 18 Apr 2026 02:25:30 +0700 Subject: [PATCH 08/15] refactor: remove re-validation of asset lock transactions for existing blocks in the chain --- src/evo/creditpool.cpp | 12 ++++-------- src/evo/creditpool.h | 12 ++++-------- src/evo/specialtxman.cpp | 2 +- src/node/miner.cpp | 17 +++++++++++++++-- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index dc49629d1e9b..6faa197d9e59 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -22,7 +22,6 @@ #include #include -using node::BlockManager; using node::ReadBlockFromDisk; // Forward declaration to prevent a new circular dependencies through masternode/payments.h @@ -248,8 +247,7 @@ CCreditPoolManager::~CCreditPoolManager() = default; CCreditPoolDiff::CCreditPoolDiff(CCreditPool starter, const CBlockIndex* pindexPrev, const Consensus::Params& consensusParams, const CAmount blockSubsidy) : - pool(std::move(starter)), - pindexPrev(pindexPrev) + pool(std::move(starter)) { assert(pindexPrev); @@ -297,17 +295,15 @@ bool CCreditPoolDiff::Unlock(const CTransaction& tx, TxValidationState& state) return true; } -bool CCreditPoolDiff::ProcessLockUnlockTransaction(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, TxValidationState& state) +bool CCreditPoolDiff::ProcessLockUnlockTransaction(const CTransaction& tx, TxValidationState& state) { if (!tx.IsSpecialTxVersion()) return true; try { switch (tx.nType) { case TRANSACTION_ASSET_LOCK: - if (!CheckAssetLockTx(tx, state)) return false; // pass the state set by CheckAssetLockTx return Lock(tx, state); case TRANSACTION_ASSET_UNLOCK: - if (!CheckAssetUnlockTx(blockman, qman, tx, pindexPrev, pool.indexes, state)) return false; // pass the state set by CheckAssetUnlockTx return Unlock(tx, state); default: return true; @@ -318,7 +314,7 @@ bool CCreditPoolDiff::ProcessLockUnlockTransaction(const BlockManager& blockman, } } -std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpoolman, const BlockManager& blockman, const llmq::CQuorumManager& qman, +std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpoolman, const CBlock& block, const CBlockIndex* pindexPrev, const Consensus::Params& consensusParams, const CAmount blockSubsidy, BlockValidationState& state) { @@ -329,7 +325,7 @@ std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpo for (size_t i = 1; i < block.vtx.size(); ++i) { const auto& tx = *block.vtx[i]; TxValidationState tx_state; - if (!creditPoolDiff.ProcessLockUnlockTransaction(blockman, qman, tx, tx_state)) { + if (!creditPoolDiff.ProcessLockUnlockTransaction(tx, tx_state)) { assert(tx_state.GetResult() == TxValidationResult::TX_CONSENSUS); state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(), strprintf("Process Lock/Unlock Transaction failed at Credit Pool (tx hash %s) %s", tx.GetHash().ToString(), tx_state.GetDebugMessage())); diff --git a/src/evo/creditpool.h b/src/evo/creditpool.h index e3eb066ecd75..fec44ab680cb 100644 --- a/src/evo/creditpool.h +++ b/src/evo/creditpool.h @@ -33,9 +33,6 @@ struct Params; namespace llmq { class CQuorumManager; } // namespace llmq -namespace node { -class BlockManager; -} // namespace node struct CCreditPool { CAmount locked{0}; @@ -70,16 +67,15 @@ struct CCreditPool { * limits should stay same and depends only on the previous block. */ class CCreditPoolDiff { -private: +public: const CCreditPool pool; +private: std::unordered_set newIndexes; CAmount sessionLocked{0}; CAmount sessionUnlocked{0}; CAmount platformReward{0}; - const CBlockIndex *pindexPrev{nullptr}; - public: explicit CCreditPoolDiff(CCreditPool starter, const CBlockIndex *pindexPrev, const Consensus::Params& consensusParams, @@ -90,7 +86,7 @@ class CCreditPoolDiff { * to change amount of credit pool * @return true if transaction can be included in this block */ - bool ProcessLockUnlockTransaction(const node::BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, TxValidationState& state); + bool ProcessLockUnlockTransaction(const CTransaction& tx, TxValidationState& state); /** * this function returns total amount of credits for the next block @@ -147,7 +143,7 @@ class CCreditPoolManager EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); }; -std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpoolman, const node::BlockManager& blockman, const llmq::CQuorumManager& qman, +std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpoolman, const CBlock& block, const CBlockIndex* pindexPrev, const Consensus::Params& consensusParams, const CAmount blockSubsidy, BlockValidationState& state); diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index 515c1dd06dec..65b1e6c4e2c6 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -743,7 +743,7 @@ bool CSpecialTxProcessor::CheckCreditPoolDiffForBlock(const CBlock& block, const try { const CAmount blockSubsidy = GetBlockSubsidy(pindex, m_consensus_params); - const auto creditPoolDiff = GetCreditPoolDiffForBlock(m_cpoolman, m_chainman.m_blockman, m_qman, block, + const auto creditPoolDiff = GetCreditPoolDiffForBlock(m_cpoolman, block, pindex->pprev, m_consensus_params, blockSubsidy, state); if (!creditPoolDiff.has_value()) return false; diff --git a/src/node/miner.cpp b/src/node/miner.cpp index d93c320eb195..f0d68a595034 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -305,7 +305,7 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc LogPrintf("CreateNewBlock() h[%d] CbTx failed to find best CL. Inserting null CL\n", nHeight); } BlockValidationState state; - const auto creditPoolDiff = GetCreditPoolDiffForBlock(*m_chain_helper.credit_pool_manager, m_blockman, m_qman, *pblock, pindexPrev, chainparams.GetConsensus(), blockSubsidy, state); + const auto creditPoolDiff = GetCreditPoolDiffForBlock(*m_chain_helper.credit_pool_manager, *pblock, pindexPrev, chainparams.GetConsensus(), blockSubsidy, state); if (creditPoolDiff == std::nullopt) { throw std::runtime_error(strprintf("%s: GetCreditPoolDiffForBlock failed: %s", __func__, state.ToString())); } @@ -544,7 +544,20 @@ void BlockAssembler::addPackageTxs(const CTxMemPool& mempool, int& nPackagesSele // `state` is local here because used only to log info about this specific tx TxValidationState state; - if (!creditPoolDiff->ProcessLockUnlockTransaction(m_blockman, m_qman, iter->GetTx(), state)) { + if (!iter->GetTx().IsSpecialTxVersion() && iter->GetTx().nType == TRANSACTION_ASSET_UNLOCK) { + // ASSET_UNLOCK transactions may expire after being added to mempool + // They should not be included to the block + if (!CheckAssetUnlockTx(m_blockman, m_qman, iter->GetTx(), pindexPrev, creditPoolDiff->pool.indexes, state)) { + if (fUsingModified) { + mapModifiedTx.get().erase(modit); + failedTx.insert(iter); + } + LogPrintf("%s: asset unlock tx %s is skipped due %s\n", + __func__, iter->GetTx().GetHash().ToString(), state.ToString()); + continue; + } + } + if (!creditPoolDiff->ProcessLockUnlockTransaction(iter->GetTx(), state)) { if (fUsingModified) { mapModifiedTx.get().erase(modit); failedTx.insert(iter); From 899636320b3c8a41d5abbca1a53a34426fe0e504 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sat, 18 Apr 2026 01:35:48 +0700 Subject: [PATCH 09/15] feat: add v2 asset lock tx --- src/evo/assetlocktx.cpp | 15 ++++++++++++--- src/evo/assetlocktx.h | 5 +++-- src/evo/specialtxman.cpp | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/evo/assetlocktx.cpp b/src/evo/assetlocktx.cpp index cba0bdc55534..2e391388f55b 100644 --- a/src/evo/assetlocktx.cpp +++ b/src/evo/assetlocktx.cpp @@ -26,7 +26,7 @@ using node::BlockManager; /** * Asset Lock Transaction */ -bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state) +bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state, bool is_v24_active) { if (tx.nType != TRANSACTION_ASSET_LOCK) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-type"); @@ -56,6 +56,9 @@ bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state) if (opt_assetLockTx->getVersion() == 0 || opt_assetLockTx->getVersion() > CAssetLockPayload::CURRENT_VERSION) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-version"); } + if (!is_v24_active && opt_assetLockTx->getVersion() == CAssetLockPayload::CURRENT_VERSION) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-version-2"); + } if (opt_assetLockTx->getCreditOutputs().empty()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-emptycreditoutputs"); @@ -68,8 +71,14 @@ bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state) } creditOutputsAmount += out.nValue; - if (!out.scriptPubKey.IsPayToPublicKeyHash()) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-pubKeyHash"); + if (opt_assetLockTx->getVersion() >= 2) { + if (!out.scriptPubKey.IsPayToPublicKeyHash() && !out.scriptPubKey.IsPayToScriptHash()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-script-pubkey"); + } + } else { + if (!out.scriptPubKey.IsPayToPublicKeyHash()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-pubKeyHash"); + } } } if (creditOutputsAmount != returnAmount) { diff --git a/src/evo/assetlocktx.h b/src/evo/assetlocktx.h index 6a00f605f2c0..8bb123150d32 100644 --- a/src/evo/assetlocktx.h +++ b/src/evo/assetlocktx.h @@ -28,7 +28,8 @@ class BlockManager; class CAssetLockPayload { public: - static constexpr uint8_t CURRENT_VERSION = 1; + static constexpr uint8_t INITIAL_VERSION = 1; + static constexpr uint8_t CURRENT_VERSION = 2; static constexpr auto SPECIALTX_TYPE = TRANSACTION_ASSET_LOCK; private: @@ -154,7 +155,7 @@ class CAssetUnlockPayload } }; -bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state); +bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state, bool is_v24_active = false); bool CheckAssetUnlockTx(const node::BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null pindexPrev, const std::optional& indexes, TxValidationState& state); bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state); diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index 65b1e6c4e2c6..bf6521585ee6 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -138,7 +138,7 @@ static bool CheckSpecialTxInner(CDeterministicMNManager& dmnman, llmq::CQuorumSn case TRANSACTION_MNHF_SIGNAL: return CheckMNHFTx(chainman, qman, tx, pindexPrev, state); case TRANSACTION_ASSET_LOCK: - return CheckAssetLockTx(tx, state); + return CheckAssetLockTx(tx, state, DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24)); case TRANSACTION_ASSET_UNLOCK: return CheckAssetUnlockTx(chainman.m_blockman, qman, tx, pindexPrev, indexes, state); } From beff1d72458aedb4ef7c2d8c8d1c15be7326bc54 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sat, 18 Apr 2026 03:17:01 +0700 Subject: [PATCH 10/15] test: regression tests for asset lock v2 --- src/evo/assetlocktx.h | 4 +- src/test/evo_assetlocks_tests.cpp | 187 +++++++++++++++++++++++------- 2 files changed, 145 insertions(+), 46 deletions(-) diff --git a/src/evo/assetlocktx.h b/src/evo/assetlocktx.h index 8bb123150d32..09462734e1e4 100644 --- a/src/evo/assetlocktx.h +++ b/src/evo/assetlocktx.h @@ -37,8 +37,8 @@ class CAssetLockPayload std::vector creditOutputs; public: - explicit CAssetLockPayload(const std::vector& creditOutputs) : - creditOutputs(creditOutputs) + explicit CAssetLockPayload(const std::vector& creditOutputs, uint8_t nVersion = CURRENT_VERSION) : + nVersion(nVersion), creditOutputs(creditOutputs) {} CAssetLockPayload() = default; diff --git a/src/test/evo_assetlocks_tests.cpp b/src/test/evo_assetlocks_tests.cpp index d21e8df73a03..f0be30a15b08 100644 --- a/src/test/evo_assetlocks_tests.cpp +++ b/src/test/evo_assetlocks_tests.cpp @@ -70,7 +70,7 @@ static CMutableTransaction CreateAssetLockTx(FillableSigningProvider& keystore, creditOutputs[1].nValue = 13 * CENT; creditOutputs[1].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey())); - CAssetLockPayload assetLockTx(creditOutputs); + CAssetLockPayload assetLockTx(creditOutputs, CAssetLockPayload::INITIAL_VERSION); CMutableTransaction tx; tx.nVersion = 3; @@ -124,9 +124,8 @@ static CMutableTransaction CreateAssetUnlockTx(FillableSigningProvider& keystore BOOST_FIXTURE_TEST_SUITE(evo_assetlocks_tests, TestChain100Setup) -BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup) +static void CheckAssetLockCommon(uint8_t version, bool is_v24_active) { - LOCK(cs_main); FillableSigningProvider keystore; CCoinsView coinsDummy; @@ -135,7 +134,13 @@ BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup) CKey key; key.MakeNewKey(true); - const CTransaction tx{CreateAssetLockTx(keystore, coins, key)}; + CMutableTransaction baseTx = CreateAssetLockTx(keystore, coins, key); + const auto basePayload = GetTxPayload(CTransaction(baseTx)); + const std::vector creditOutputs = basePayload->getCreditOutputs(); + + SetTxPayload(baseTx, CAssetLockPayload(creditOutputs, version)); + + const CTransaction tx{baseTx}; std::string reason; BOOST_CHECK(IsStandardTx(CTransaction(tx), reason)); @@ -144,7 +149,7 @@ BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup) BOOST_CHECK_MESSAGE(CheckTransaction(CTransaction(tx), tx_state), strTest); BOOST_CHECK(tx_state.IsValid()); - BOOST_CHECK(CheckAssetLockTx(CTransaction(tx), tx_state)); + BOOST_CHECK(CheckAssetLockTx(CTransaction(tx), tx_state, is_v24_active)); BOOST_CHECK(AreInputsStandard(CTransaction(tx), coins)); @@ -155,14 +160,14 @@ BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup) const auto opt_payload = GetTxPayload(tx); BOOST_CHECK(opt_payload.has_value()); - BOOST_CHECK(opt_payload->getVersion() == 1); + BOOST_CHECK(opt_payload->getVersion() == version); } { // Wrong type "Asset Unlock TX" instead "Asset Lock TX" CMutableTransaction txWrongType(tx); txWrongType.nType = TRANSACTION_ASSET_UNLOCK; - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txWrongType), tx_state)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txWrongType), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-type"); } @@ -176,40 +181,35 @@ BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup) BOOST_CHECK(inSum == outSum); // Outputs should not be bigger than inputs - CMutableTransaction txBigOutput(tx); + CMutableTransaction txBigOutput(baseTx); txBigOutput.vout[0].nValue += 1; - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txBigOutput), tx_state)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txBigOutput), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-creditamount"); // Smaller outputs are allown - CMutableTransaction txSmallOutput(tx); + CMutableTransaction txSmallOutput(baseTx); txSmallOutput.vout[1].nValue -= 1; - BOOST_CHECK(CheckAssetLockTx(CTransaction(txSmallOutput), tx_state)); + BOOST_CHECK(CheckAssetLockTx(CTransaction(txSmallOutput), tx_state, is_v24_active)); } - const auto assetLockPayload = GetTxPayload(tx); - const std::vector creditOutputs = assetLockPayload->getCreditOutputs(); - { // Sum of credit output greater than OP_RETURN std::vector wrongOutput = creditOutputs; wrongOutput[0].nValue += CENT; - CAssetLockPayload greaterCreditsPayload(wrongOutput); - CMutableTransaction txGreaterCredits(tx); - SetTxPayload(txGreaterCredits, greaterCreditsPayload); + CMutableTransaction txGreaterCredits(baseTx); + SetTxPayload(txGreaterCredits, CAssetLockPayload(wrongOutput, version)); - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txGreaterCredits), tx_state)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txGreaterCredits), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-creditamount"); // Sum of credit output less than OP_RETURN wrongOutput[1].nValue -= 2 * CENT; - CAssetLockPayload lessCreditsPayload(wrongOutput); - CMutableTransaction txLessCredits(tx); - SetTxPayload(txLessCredits, lessCreditsPayload); + CMutableTransaction txLessCredits(baseTx); + SetTxPayload(txLessCredits, CAssetLockPayload(wrongOutput, version)); - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txLessCredits), tx_state)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txLessCredits), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-creditamount"); } @@ -217,24 +217,23 @@ BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup) // Credit output is out-of-range std::vector creditOutputsOutOfRange = creditOutputs; creditOutputsOutOfRange[0].nValue = 0; - CAssetLockPayload invalidOutputsPayload(creditOutputsOutOfRange); - CMutableTransaction txInvalidOutputs(tx); - SetTxPayload(txInvalidOutputs, invalidOutputsPayload); + CMutableTransaction txInvalidOutputs(baseTx); + SetTxPayload(txInvalidOutputs, CAssetLockPayload(creditOutputsOutOfRange, version)); - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-credit-outofrange"); // one of output is out of range creditOutputsOutOfRange[0].nValue = MAX_MONEY + 1; - SetTxPayload(txInvalidOutputs, CAssetLockPayload{creditOutputsOutOfRange}); - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state)); + SetTxPayload(txInvalidOutputs, CAssetLockPayload(creditOutputsOutOfRange, version)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-credit-outofrange"); // sum of some of output is out of range creditOutputsOutOfRange[0].nValue = MAX_MONEY + 1 - creditOutputsOutOfRange[1].nValue; - SetTxPayload(txInvalidOutputs, CAssetLockPayload{creditOutputsOutOfRange}); - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state)); + SetTxPayload(txInvalidOutputs, CAssetLockPayload(creditOutputsOutOfRange, version)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-credit-outofrange"); } @@ -242,60 +241,160 @@ BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup) // One credit output keys is not pub key std::vector creditOutputsNotPubkey = creditOutputs; creditOutputsNotPubkey[0].scriptPubKey = CScript() << OP_1; - CAssetLockPayload notPubkeyPayload(creditOutputsNotPubkey); - CMutableTransaction txNotPubkey(tx); - SetTxPayload(txNotPubkey, notPubkeyPayload); + CMutableTransaction txNotPubkey(baseTx); + SetTxPayload(txNotPubkey, CAssetLockPayload(creditOutputsNotPubkey, version)); - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txNotPubkey), tx_state)); - BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-pubKeyHash"); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txNotPubkey), tx_state, is_v24_active)); + if (version >= 2) { + BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-script-pubkey"); + } else { + BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-pubKeyHash"); + } } { // OP_RETURN must be only one, not more - CMutableTransaction txMultipleReturn(tx); + CMutableTransaction txMultipleReturn(baseTx); txMultipleReturn.vout[1].scriptPubKey = CScript() << OP_RETURN << ParseHex(""); - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txMultipleReturn), tx_state)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txMultipleReturn), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-multiple-return"); } { // zero/negative OP_RETURN - CMutableTransaction txReturnOutOfRange(tx); + CMutableTransaction txReturnOutOfRange(baseTx); txReturnOutOfRange.vout[0].nValue = 0; - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnOutOfRange), tx_state)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnOutOfRange), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-opreturn-outofrange"); txReturnOutOfRange.vout[0].nValue = MAX_MONEY + 1; - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnOutOfRange), tx_state)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnOutOfRange), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-opreturn-outofrange"); } { // OP_RETURN is missing - CMutableTransaction txNoReturn(tx); + CMutableTransaction txNoReturn(baseTx); txNoReturn.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey())); - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txNoReturn), tx_state)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txNoReturn), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-no-return"); } { // OP_RETURN should not have any data - CMutableTransaction txReturnData(tx); + CMutableTransaction txReturnData(baseTx); txReturnData.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("abcd"); - BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnData), tx_state)); + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnData), tx_state, is_v24_active)); BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-non-empty-return"); } } +BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup) +{ + CheckAssetLockCommon(CAssetLockPayload::INITIAL_VERSION, /*is_v24_active=*/false); + CheckAssetLockCommon(CAssetLockPayload::INITIAL_VERSION, /*is_v24_active=*/true); + CheckAssetLockCommon(CAssetLockPayload::CURRENT_VERSION, /*is_v24_active=*/true); +} + +BOOST_FIXTURE_TEST_CASE(evo_assetlock_v2, TestChain100Setup) +{ + LOCK(cs_main); + FillableSigningProvider keystore; + CCoinsView coinsDummy; + CCoinsViewCache coins(&coinsDummy); + + CKey key; + key.MakeNewKey(true); + + CMutableTransaction tx = CreateAssetLockTx(keystore, coins, key); + TxValidationState tx_state; + + const auto v1Payload = GetTxPayload(CTransaction(tx)); + const std::vector creditOutputs = v1Payload->getCreditOutputs(); + + // Build v2 payload with same P2PKH credit outputs + CAssetLockPayload v2Payload(creditOutputs); + BOOST_CHECK(v2Payload.getVersion() == CAssetLockPayload::CURRENT_VERSION); + SetTxPayload(tx, v2Payload); + + { + // v2 P2PKH: accepted with is_v24_active=true + BOOST_CHECK(CheckAssetLockTx(CTransaction(tx), tx_state, /*is_v24_active=*/true)); + } + + { + // v2 P2PKH: rejected pre-v24 + BOOST_CHECK(!CheckAssetLockTx(CTransaction(tx), tx_state, /*is_v24_active=*/false)); + BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-version-2"); + } + + { + // v2 with P2SH credit output: accepted with is_v24_active=true + CScript p2sh_script = GetScriptForDestination(ScriptHash(CScript() << OP_1)); + std::vector p2shOutputs(1); + p2shOutputs[0].nValue = 30 * CENT; + p2shOutputs[0].scriptPubKey = p2sh_script; + + CMutableTransaction txP2SH(tx); + SetTxPayload(txP2SH, CAssetLockPayload(p2shOutputs)); + + BOOST_CHECK(CheckAssetLockTx(CTransaction(txP2SH), tx_state, /*is_v24_active=*/true)); + } + + { + // v1 with P2SH credit output: rejected even post-v24 + CScript p2sh_script = GetScriptForDestination(ScriptHash(CScript() << OP_1)); + std::vector p2shOutputs(1); + p2shOutputs[0].nValue = 30 * CENT; + p2shOutputs[0].scriptPubKey = p2sh_script; + + CMutableTransaction txV1P2SH(tx); + SetTxPayload(txV1P2SH, CAssetLockPayload(p2shOutputs, CAssetLockPayload::INITIAL_VERSION)); + + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txV1P2SH), tx_state, /*is_v24_active=*/true)); + BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-pubKeyHash"); + } + + { + // v2 with non-P2PKH/non-P2SH credit output: rejected + std::vector badOutputs(1); + badOutputs[0].nValue = 30 * CENT; + badOutputs[0].scriptPubKey = CScript() << OP_1; + + CMutableTransaction txBad(tx); + SetTxPayload(txBad, CAssetLockPayload(badOutputs)); + + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txBad), tx_state, /*is_v24_active=*/true)); + BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-script-pubkey"); + } + + { + // v1 still works post-v24 + CMutableTransaction txV1(tx); + SetTxPayload(txV1, CAssetLockPayload(creditOutputs, CAssetLockPayload::INITIAL_VERSION)); + + BOOST_CHECK(CheckAssetLockTx(CTransaction(txV1), tx_state, /*is_v24_active=*/true)); + } + + { + // version 3 (future): rejected even post-v24 + CMutableTransaction txV3(tx); + SetTxPayload(txV3, CAssetLockPayload(creditOutputs, /*nVersion=*/3)); + + BOOST_CHECK(!CheckAssetLockTx(CTransaction(txV3), tx_state, /*is_v24_active=*/true)); + BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-version"); + } +} + BOOST_FIXTURE_TEST_CASE(evo_assetunlock, TestChain100Setup) { LOCK(cs_main); From ac59b066d2f7c23a96f8e7a5f8f69d56173f8973 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 20 Apr 2026 15:18:58 +0700 Subject: [PATCH 11/15] feat: print asset lock txes with evo1... address for rpc outputs --- src/evo/core_write.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/evo/core_write.cpp b/src/evo/core_write.cpp index 1ccfc998840b..1f019b1cfecb 100644 --- a/src/evo/core_write.cpp +++ b/src/evo/core_write.cpp @@ -16,7 +16,9 @@ #include #include +#include #include +#include