From 662c151165782c24564c2712a5940c8ca8fa2881 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 21 Oct 2025 16:31:56 +0200 Subject: [PATCH 1/5] DecomposePeginWitness: fix deserialization flags for MerkleBlock proof In CreatePeginWitnessInner, the MerkleBlock is always serialized without witness: PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS In DecomposePeginWitness before this change, the MerkleBlock was deserialized with witness: PROTOCOL_VERSION This was only noticed as an issue in the pegin subsidy implementation, in a failure in the feature_dynafed functional test. In the test_transition_mempool_eject test case, the Merkle block proof is coming from the same chain where we are creating a pegin. See the comment: "hack: since we're not validating peg-ins in parent chain, just make both the funding and claim tx on same chain (printing money)" I haven't investigated enough to explain why this causes a deserialization failure in this specific case, but presumably this change is correct since we're always serializing without witness. Before this DecomposePeginWitness was only used in src/psbt.cpp --- src/pegins.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pegins.cpp b/src/pegins.cpp index 72551d2c3b5..8e449363983 100644 --- a/src/pegins.cpp +++ b/src/pegins.cpp @@ -577,7 +577,7 @@ bool DecomposePeginWitness(const CScriptWitness& witness, CAmount& value, CAsset tx = elem_tx; } - CDataStream ss_proof(stack[5], SER_NETWORK, PROTOCOL_VERSION); + CDataStream ss_proof(stack[5], SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS); if (Params().GetConsensus().ParentChainHasPow()) { Sidechain::Bitcoin::CMerkleBlock tx_proof; ss_proof >> tx_proof; From a6465612f100dfd9b4f51d1daee309aba7a3328a Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Wed, 22 Oct 2025 15:53:12 +0200 Subject: [PATCH 2/5] subsidy: add chainparams and init --- src/chainparams.cpp | 59 ++++++++++++++++++++++++++++++++++++++++++++- src/chainparams.h | 25 +++++++++++++++++++ src/init.cpp | 21 ++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index a2b4c418af2..e3490d57f8b 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -7,12 +7,13 @@ #include #include +#include #include #include // for signet block challenge hash #include #include +#include #include -#include #include @@ -232,6 +233,8 @@ class CMainParams : public CChainParams { multi_data_permitted = false; accept_discount_ct = false; create_discount_ct = false; + pegin_subsidy = PeginSubsidy(); + pegin_minimum = PeginMinimum(); consensus.has_parent_chain = false; g_signed_blocks = false; g_con_elementsmode = false; @@ -379,6 +382,8 @@ class CTestNetParams : public CChainParams { multi_data_permitted = false; accept_discount_ct = false; create_discount_ct = false; + pegin_subsidy = PeginSubsidy(); + pegin_minimum = PeginMinimum(); consensus.has_parent_chain = false; g_signed_blocks = false; g_con_elementsmode = false; @@ -544,6 +549,8 @@ class SigNetParams : public CChainParams { multi_data_permitted = false; accept_discount_ct = false; create_discount_ct = false; + pegin_subsidy = PeginSubsidy(); + pegin_minimum = PeginMinimum(); consensus.has_parent_chain = false; g_signed_blocks = false; // lol g_con_elementsmode = false; @@ -648,6 +655,8 @@ class CRegTestParams : public CChainParams { multi_data_permitted = false; accept_discount_ct = false; create_discount_ct = false; + pegin_subsidy = PeginSubsidy(); + pegin_minimum = PeginMinimum(); consensus.has_parent_chain = false; g_signed_blocks = false; g_con_elementsmode = false; @@ -792,6 +801,39 @@ void CRegTestParams::UpdateActivationParametersFromArgs(const ArgsManager& args) } } +// ELEMENTS +PeginSubsidy ParsePeginSubsidy(const ArgsManager& args) { + PeginSubsidy pegin_subsidy; + + pegin_subsidy.height = args.GetIntArg("-peginsubsidyheight", std::numeric_limits::max()); + if (pegin_subsidy.height < 0) { + throw std::runtime_error(strprintf("Invalid block height (%d) for -peginsubsidyheight. Must be positive.", pegin_subsidy.height)); + } + if (std::optional amount = ParseMoney(args.GetArg("-peginsubsidythreshold", "0"))) { + pegin_subsidy.threshold = amount.value(); + } else { + throw std::runtime_error("Invalid -peginsubsidythreshold"); + } + + return pegin_subsidy; +}; + +PeginMinimum ParsePeginMinimum(const ArgsManager& args) { + PeginMinimum pegin_minimum; + + pegin_minimum.height = args.GetIntArg("-peginminheight", std::numeric_limits::max()); + if (pegin_minimum.height < 0) { + throw std::runtime_error(strprintf("Invalid block height (%d) for -peginminheight. Must be positive.", pegin_minimum.height)); + } + if (std::optional amount = ParseMoney(args.GetArg("-peginminamount", "0"))) { + pegin_minimum.amount = amount.value(); + } else { + throw std::runtime_error("Invalid -peginminamount"); + } + + return pegin_minimum; +}; + /** * Custom params for testing. */ @@ -932,6 +974,11 @@ class CCustomParams : public CRegTestParams { consensus.start_p2wsh_script = args.GetIntArg("-con_start_p2wsh_script", consensus.start_p2wsh_script); create_discount_ct = args.GetBoolArg("-creatediscountct", create_discount_ct); accept_discount_ct = args.GetBoolArg("-acceptdiscountct", accept_discount_ct) || create_discount_ct; + pegin_subsidy = ParsePeginSubsidy(args); + pegin_minimum = ParsePeginMinimum(args); + if (pegin_subsidy.threshold < pegin_minimum.amount) { + throw std::runtime_error(strprintf("Pegin subsidy threshold (%s) must be greater than or equal to pegin minimum amount (%s)", FormatMoney(pegin_subsidy.threshold), FormatMoney(pegin_minimum.amount))); + } // Calculate pegged Bitcoin asset std::vector commit = CommitToArguments(consensus, strNetworkID); @@ -1178,6 +1225,11 @@ class CLiquidV1Params : public CChainParams { multi_data_permitted = true; create_discount_ct = args.GetBoolArg("-creatediscountct", false); accept_discount_ct = args.GetBoolArg("-acceptdiscountct", true) || create_discount_ct; + pegin_subsidy = ParsePeginSubsidy(args); + pegin_minimum = ParsePeginMinimum(args); + if (pegin_subsidy.threshold < pegin_minimum.amount) { + throw std::runtime_error(strprintf("Pegin subsidy threshold (%s) must be greater than or equal to pegin minimum amount (%s)", FormatMoney(pegin_subsidy.threshold), FormatMoney(pegin_minimum.amount))); + } parentGenesisBlockHash = uint256S("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"); const bool parent_genesis_is_null = parentGenesisBlockHash == uint256(); @@ -1538,6 +1590,11 @@ class CLiquidV1TestParams : public CLiquidV1Params { multi_data_permitted = args.GetBoolArg("-multi_data_permitted", multi_data_permitted); create_discount_ct = args.GetBoolArg("-creatediscountct", create_discount_ct); accept_discount_ct = args.GetBoolArg("-acceptdiscountct", accept_discount_ct) || create_discount_ct; + pegin_subsidy = ParsePeginSubsidy(args); + pegin_minimum = ParsePeginMinimum(args); + if (pegin_subsidy.threshold < pegin_minimum.amount) { + throw std::runtime_error(strprintf("Pegin subsidy threshold (%s) must be greater than or equal to pegin minimum amount (%s)", FormatMoney(pegin_subsidy.threshold), FormatMoney(pegin_minimum.amount))); + } if (args.IsArgSet("-parentgenesisblockhash")) { parentGenesisBlockHash = uint256S(args.GetArg("-parentgenesisblockhash", "")); diff --git a/src/chainparams.h b/src/chainparams.h index 840a22ba5c4..3e318577eca 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -28,6 +28,27 @@ struct CCheckpointData { } }; +// ELEMENTS +struct PeginSubsidy { + int height{std::numeric_limits::max()}; + CAmount threshold{0}; + + PeginSubsidy() {}; + bool IsDefined() { + return threshold > 0 || height < std::numeric_limits::max(); + }; +}; + +struct PeginMinimum { + int height{std::numeric_limits::max()}; + CAmount amount{0}; + + PeginMinimum() {}; + bool IsDefined() { + return amount > 0 || height < std::numeric_limits::max(); + }; +}; + struct AssumeutxoHash : public BaseHash { explicit AssumeutxoHash(const uint256& hash) : BaseHash(hash) {} }; @@ -138,6 +159,8 @@ class CChainParams bool GetMultiDataPermitted() const { return multi_data_permitted; } bool GetAcceptDiscountCT() const { return accept_discount_ct; } bool GetCreateDiscountCT() const { return create_discount_ct; } + PeginSubsidy GetPeginSubsidy() const { return pegin_subsidy; } + PeginMinimum GetPeginMinimum() const { return pegin_minimum; } protected: CChainParams() {} @@ -173,6 +196,8 @@ class CChainParams bool multi_data_permitted; bool accept_discount_ct; bool create_discount_ct; + PeginSubsidy pegin_subsidy; + PeginMinimum pegin_minimum; }; /** diff --git a/src/init.cpp b/src/init.cpp index 27addf275aa..f2bbc8ed903 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -642,6 +642,10 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-ct_exponent", strprintf("The hiding exponent. (default: %s)", 0), ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-acceptdiscountct", "Accept discounted fees for Confidential Transactions (default: 1 in liquidtestnet and liquidv1, 0 otherwise)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-creatediscountct", "Create Confidential Transactions with discounted fees (default: 0). Setting this to 1 will also set 'acceptdiscountct' to 1.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); + argsman.AddArg("-peginsubsidyheight", "The block height at which peg-in transactions must have a burn subsidy (default: not active). This is an OP_RETURN output with value of the parent transaction feerate times the cost of spending the WSH output (feerate * 396 sats for liquidv1). ", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); + argsman.AddArg("-peginsubsidythreshold", "The output value below which peg-in transactions must have a burn subsidy (default: 0). Peg-ins above this value do not require the subsidy.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); + argsman.AddArg("-peginminheight", "The block height at which a minimum peg-in value is enforced (default: not active).", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); + argsman.AddArg("-peginminamount", "The minimum value for a peg-in transaction after peginminheight (default: unset).", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); #if defined(USE_SYSCALL_SANDBOX) argsman.AddArg("-sandbox=", "Use the experimental syscall sandbox in the specified mode (-sandbox=log-and-abort or -sandbox=abort). Allow only expected syscalls to be used by bitcoind. Note that this is an experimental new feature that may cause bitcoind to exit or crash unexpectedly: use with caution. In the \"log-and-abort\" mode the invocation of an unexpected syscall results in a debug handler being invoked which will log the incident and terminate the program (without executing the unexpected syscall). In the \"abort\" mode the invocation of an unexpected syscall results in the entire process being killed immediately by the kernel without executing the unexpected syscall.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); @@ -1975,6 +1979,23 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) gArgs.SoftSetArg("-validatepegin", "0"); } } + // if we are validating pegin subsidy or minimum then we require bitcoind >= v25 + if (Params().GetPeginSubsidy().IsDefined() || Params().GetPeginMinimum().IsDefined()) { + UniValue params(UniValue::VARR); + UniValue reply = CallMainChainRPC("getnetworkinfo", params); + if (reply["error"].isStr()) { + InitError(Untranslated(reply["error"].get_str())); + return false; + } else { + const int version = reply["result"]["version"].get_int(); + const std::string& subversion = reply["result"]["subversion"].get_str(); + if (version < 250000 && subversion.find("Satoshi") != std::string::npos) { + const std::string err = strprintf("ERROR: parent bitcoind must be version 25 or newer for pegin subsidy/minimum validation. Found version: %s", version); + InitError(Untranslated(err)); + return false; + } + } + } } // Call ActivateBestChain every 30 seconds. This is almost always a From a5fa3ac97d2ad6a968ac2a2235e0444924d88c0d Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Wed, 22 Oct 2025 15:55:04 +0200 Subject: [PATCH 3/5] subsidy: implementation for claimpegin, createrawpegin, and RPCs --- src/dynafed.cpp | 31 +++++++++ src/dynafed.h | 5 ++ src/rpc/blockchain.cpp | 20 ++++++ src/rpc/client.cpp | 2 + src/test/dynafed_tests.cpp | 49 +++++++++++++- src/wallet/rpc/elements.cpp | 129 ++++++++++++++++++++++++++++++++---- 6 files changed, 219 insertions(+), 17 deletions(-) diff --git a/src/dynafed.cpp b/src/dynafed.cpp index d7197951491..cb288a83ed7 100644 --- a/src/dynafed.cpp +++ b/src/dynafed.cpp @@ -123,3 +123,34 @@ DynaFedParamEntry ComputeNextBlockCurrentParameters(const CBlockIndex* pindexPre } } +bool ParseFedPegQuorum(const CScript& fedpegscript, int& t, int& n) { + CScript::const_iterator it = fedpegscript.begin(); + std::vector vch; + opcodetype opcode; + + // parse the required threshold number + if (!fedpegscript.GetOp(it, opcode, vch)) return false; + t = CScript::DecodeOP_N(opcode); + if (t < 1 || t > MAX_PUBKEYS_PER_MULTISIG) return false; + + // support a fedpegscript like OP_TRUE if we're at the end of the script + if (it == fedpegscript.end()) return true; + + // count the pubkeys + int pubkeys = 0; + while (fedpegscript.GetOp(it, opcode, vch)) { + if (opcode != 0x21) break; + if (vch.size() != 33) return false; + pubkeys++; + } + + // parse the total number of pubkeys + n = CScript::DecodeOP_N(opcode); + if (n < 1 || n > MAX_PUBKEYS_PER_MULTISIG || n < t) return false; + if (pubkeys != n) return false; + + // the next opcode must be OP_CHECKMULTISIG + if (!fedpegscript.GetOp(it, opcode, vch)) return false; + + return opcode == OP_CHECKMULTISIG; +} diff --git a/src/dynafed.h b/src/dynafed.h index ea651631fb0..373fdb2e97a 100644 --- a/src/dynafed.h +++ b/src/dynafed.h @@ -15,5 +15,10 @@ DynaFedParamEntry ComputeNextBlockFullCurrentParameters(const CBlockIndex* pinde * publish signblockscript-related fields */ DynaFedParamEntry ComputeNextBlockCurrentParameters(const CBlockIndex* pindexPrev, const Consensus::Params& consensus); +/* Get the threshold (t) and maybe the total pubkeys (n) of the first OP_CHECKMULTISIG in the fedpegscript. + * Assumes the fedpegscript starts with the threshold, otherwise returns false. + * Uses CScript::DecodeOP_N, so only supports up to a threshold of 16, otherwise asserts. + * Supports a fedpegscript like OP_TRUE by returning early. */ +bool ParseFedPegQuorum(const CScript& fedpegscript, int& t, int& n); #endif // BITCOIN_DYNAFED_H diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index b20765a9e29..602cd5d9528 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -3134,6 +3135,25 @@ static RPCHelpMan getsidechaininfo() obj.pushKV("parent_chain_signblockscript_hex", HexStr(consensus.parent_chain_signblockscript)); obj.pushKV("parent_pegged_asset", consensus.parent_pegged_asset.GetHex()); } + + PeginMinimum pegin_minimum = Params().GetPeginMinimum(); + if (pegin_minimum.amount > 0) { + obj.pushKV("pegin_min_amount", FormatMoney(pegin_minimum.amount)); + } + if (pegin_minimum.height < std::numeric_limits::max()) { + obj.pushKV("pegin_min_height", pegin_minimum.height); + obj.pushKV("pegin_min_active", chainman.ActiveTip()->nHeight >= pegin_minimum.height); + } + + PeginSubsidy pegin_subsidy = Params().GetPeginSubsidy(); + if (pegin_subsidy.threshold > 0) { + obj.pushKV("pegin_subsidy_threshold", FormatMoney(pegin_subsidy.threshold)); + } + if (pegin_subsidy.height < std::numeric_limits::max()) { + obj.pushKV("pegin_subsidy_height", pegin_subsidy.height); + obj.pushKV("pegin_subsidy_active", chainman.ActiveTip()->nHeight >= pegin_subsidy.height); + } + return obj; }, }; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 5d8da167220..3b65133921e 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -239,6 +239,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "calculateasset", 3, "blind_reissuance" }, { "updatepsbtpegin", 1, "input" }, { "updatepsbtpegin", 2, "value" }, + { "claimpegin", 3, "fee_rate" }, + { "createrawpegin", 3, "fee_rate" }, }; // clang-format on diff --git a/src/test/dynafed_tests.cpp b/src/test/dynafed_tests.cpp index 3a186db51ca..f9a8099ae19 100644 --- a/src/test/dynafed_tests.cpp +++ b/src/test/dynafed_tests.cpp @@ -2,12 +2,13 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include -#include #include +#include #include #include