From ce74acf82bb438ef43735073ff7354f8dc84e3ad Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sat, 18 Apr 2026 01:35:48 +0700 Subject: [PATCH 1/7] 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 cd530d1b4843802ae1f185b24038286a0ce15ab4 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sat, 18 Apr 2026 03:17:01 +0700 Subject: [PATCH 2/7] 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 bbbecee24c16f863031b1e7980517c9dfb69715e Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 20 Apr 2026 15:18:58 +0700 Subject: [PATCH 3/7] 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