Merge ElementsProject/elements#1499: ELIP 202: Implementation of optional sidechain peg-in subsidy and minimum peg-in amount

a18a88b31b grammar: prefer peg-in to pegin in messages (Byron Hambly)
90b2b3bbb0 test: add pegin subsidy functional test (Byron Hambly)
186bb25a08 validation: check for peg-in subsidy and minimum (Byron Hambly)
51c89c65d0 subsidy: implementation for claimpegin, createrawpegin, and RPCs (Byron Hambly)
94cde59dc3 subsidy: add chainparams and init (Byron Hambly)
f3b63f4b8c DecomposePeginWitness: fix deserialization flags for MerkleBlock proof (Byron Hambly)

Pull request description:

  Implementation for [ELIP 202](https://github.com/ElementsProject/ELIPs/blob/main/elip-0202.mediawiki)

ACKs for top commit:
  jsarenik:
    Running ACK a18a88b
  tomt1664:
    Tested ACK a18a88b31b

Tree-SHA512: 984fe2aa32e6814e14c9535e9fea7e03d3b6cf7a35621ccfb2a081a6ebab01906c5e883c5b505ced53f254401d30b6e975cb05e88efc8d3fbedc79abd0be72a2
This commit is contained in:
merge-script 2026-02-09 09:14:44 -08:00
commit 7791d31bcf
No known key found for this signature in database
GPG key ID: 2F656B0610604482
13 changed files with 1388 additions and 28 deletions

View file

@ -7,12 +7,13 @@
#include <chainparamsseeds.h>
#include <consensus/merkle.h>
#include <crypto/sha256.h>
#include <deploymentinfo.h>
#include <hash.h> // for signet block challenge hash
#include <issuance.h>
#include <primitives/transaction.h>
#include <util/moneystr.h>
#include <util/system.h>
#include <crypto/sha256.h>
#include <assert.h>
@ -232,6 +233,8 @@ public:
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 @@ public:
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 @@ public:
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 @@ public:
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<int>::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<CAmount> 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<int>::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<CAmount> 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 @@ protected:
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("Peg-in subsidy threshold (%s) must be greater than or equal to peg-in minimum amount (%s)", FormatMoney(pegin_subsidy.threshold), FormatMoney(pegin_minimum.amount)));
}
// Calculate pegged Bitcoin asset
std::vector<unsigned char> commit = CommitToArguments(consensus, strNetworkID);
@ -1178,6 +1225,11 @@ public:
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("Peg-in subsidy threshold (%s) must be greater than or equal to peg-in 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 @@ public:
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("Peg-in subsidy threshold (%s) must be greater than or equal to peg-in minimum amount (%s)", FormatMoney(pegin_subsidy.threshold), FormatMoney(pegin_minimum.amount)));
}
if (args.IsArgSet("-parentgenesisblockhash")) {
parentGenesisBlockHash = uint256S(args.GetArg("-parentgenesisblockhash", ""));

View file

@ -28,6 +28,27 @@ struct CCheckpointData {
}
};
// ELEMENTS
struct PeginSubsidy {
int height{std::numeric_limits<int>::max()};
CAmount threshold{0};
PeginSubsidy() {};
bool IsDefined() {
return threshold > 0 || height < std::numeric_limits<int>::max();
};
};
struct PeginMinimum {
int height{std::numeric_limits<int>::max()};
CAmount amount{0};
PeginMinimum() {};
bool IsDefined() {
return amount > 0 || height < std::numeric_limits<int>::max();
};
};
struct AssumeutxoHash : public BaseHash<uint256> {
explicit AssumeutxoHash(const uint256& hash) : BaseHash(hash) {}
};
@ -138,6 +159,8 @@ public:
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 @@ protected:
bool multi_data_permitted;
bool accept_discount_ct;
bool create_discount_ct;
PeginSubsidy pegin_subsidy;
PeginMinimum pegin_minimum;
};
/**

View file

@ -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<unsigned char> 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;
}

View file

@ -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

View file

@ -629,7 +629,7 @@ void SetupServerArgs(ArgsManager& argsman)
argsman.AddArg("-mainchainrpcpassword=<pwd>", "The rpc password which the daemon will use to connect to the trusted mainchain daemon to validate peg-ins, if enabled. (default: cookie auth)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::ELEMENTS);
argsman.AddArg("-mainchainrpccookiefile=<file>", "The bitcoind cookie auth path which the daemon will use to connect to the trusted mainchain daemon to validate peg-ins. (default: `<datadir>/regtest/.cookie`)", ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS);
argsman.AddArg("-mainchainrpctimeout=<n>", strprintf("Timeout in seconds during mainchain RPC requests, or 0 for no timeout. (default: %d)", DEFAULT_HTTP_CLIENT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS);
argsman.AddArg("-peginconfirmationdepth=<n>", strprintf("Pegin claims must be this deep to be considered valid. (default: %d)", DEFAULT_PEGIN_CONFIRMATION_DEPTH), ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS);
argsman.AddArg("-peginconfirmationdepth=<n>", strprintf("Peg-in claims must be this deep to be considered valid. (default: %d)", DEFAULT_PEGIN_CONFIRMATION_DEPTH), ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS);
argsman.AddArg("-parentpubkeyprefix", strprintf("The byte prefix, in decimal, of the parent chain's base58 pubkey address. (default: %d)", 111), ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-parentscriptprefix", strprintf("The byte prefix, in decimal, of the parent chain's base58 script address. (default: %d)", 196), ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-parent_bech32_hrp", strprintf("The human-readable part of the parent chain's bech32 encoding. (default: %s)", "bc"), ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
@ -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). The subsidy is an OP_RETURN output, with its value equal to the feerate of the parent transaction multiplied by the vsize of spending the P2WSH output created by the peg-in (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=<mode>", "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);
@ -1964,7 +1968,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
if (gArgs.GetBoolArg("-validatepegin", Params().GetConsensus().has_parent_chain)) {
uiInterface.InitMessage(_("Awaiting mainchain RPC warmup").translated);
if (!MainchainRPCCheck()) {
const std::string err_msg = "ERROR: elements is set to verify pegins but cannot get a valid response from the mainchain daemon. Please check debug.log for more information.\n\nIf you haven't setup a bitcoind please get the latest stable version from https://bitcoincore.org/en/download/ or if you do not need to validate pegins set in your elements configuration validatepegin=0";
const std::string err_msg = "ERROR: elements is set to verify peg-ins but cannot get a valid response from the mainchain daemon. Please check debug.log for more information.\n\nIf you haven't setup a bitcoind please get the latest stable version from https://bitcoincore.org/en/download/ or if you do not need to validate peg-ins set in your elements configuration validatepegin=0";
// We fail immediately if this node has RPC server enabled
if (gArgs.GetBoolArg("-server", false)) {
InitError(Untranslated(err_msg));
@ -1975,6 +1979,23 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
gArgs.SoftSetArg("-validatepegin", "0");
}
}
// if we are validating peg-in 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 peg-in subsidy/minimum validation. Found version: %s", version);
InitError(Untranslated(err));
return false;
}
}
}
}
// Call ActivateBestChain every 30 seconds. This is almost always a

View file

@ -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;

View file

@ -40,6 +40,7 @@
#include <txdb.h>
#include <txmempool.h>
#include <undo.h>
#include <util/moneystr.h>
#include <util/strencodings.h>
#include <util/string.h>
#include <util/translation.h>
@ -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<int>::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<int>::max()) {
obj.pushKV("pegin_subsidy_height", pegin_subsidy.height);
obj.pushKV("pegin_subsidy_active", chainman.ActiveTip()->nHeight >= pegin_subsidy.height);
}
return obj;
},
};

View file

@ -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

View file

@ -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 <test/util/setup_common.h>
#include <string>
#include <boost/test/unit_test.hpp>
#include <dynafed.h>
#include <primitives/block.h>
#include <script/script.h>
#include <serialize.h>
#include <string>
#include <test/util/setup_common.h>
BOOST_FIXTURE_TEST_SUITE(dynafed_tests, BasicTestingSetup)
@ -41,6 +42,48 @@ BOOST_AUTO_TEST_CASE(dynafed_params_root)
);
}
BOOST_AUTO_TEST_CASE(parse_fedpegscript_multisig)
{
int t = 0;
int n = 0;
auto simplebytes = ParseHex("512103dff4923d778550cc13ce0d887d737553b4b58f4e8e886507fc39f5e447b2186451ae");
CScript simple{simplebytes.begin(), simplebytes.end()};
BOOST_CHECK(ParseFedPegQuorum(simple, t, n));
BOOST_CHECK_EQUAL(t, 1);
BOOST_CHECK_EQUAL(n, 1);
auto liquidv1bytes = ParseHex("5b21020e0338c96a8870479f2396c373cc7696ba124e8635d41b0ea581112b678172612102675333a4e4b8fb51d9d4e22fa5a8eaced3fdac8a8cbf9be8c030f75712e6af992102896807d54bc55c24981f24a453c60ad3e8993d693732288068a23df3d9f50d4821029e51a5ef5db3137051de8323b001749932f2ff0d34c82e96a2c2461de96ae56c2102a4e1a9638d46923272c266631d94d36bdb03a64ee0e14c7518e49d2f29bc401021031c41fdbcebe17bec8d49816e00ca1b5ac34766b91c9f2ac37d39c63e5e008afb2103079e252e85abffd3c401a69b087e590a9b86f33f574f08129ccbd3521ecf516b2103111cf405b627e22135b3b3733a4a34aa5723fb0f58379a16d32861bf576b0ec2210318f331b3e5d38156da6633b31929c5b220349859cc9ca3d33fb4e68aa08401742103230dae6b4ac93480aeab26d000841298e3b8f6157028e47b0897c1e025165de121035abff4281ff00660f99ab27bb53e6b33689c2cd8dcd364bc3c90ca5aea0d71a62103bd45cddfacf2083b14310ae4a84e25de61e451637346325222747b157446614c2103cc297026b06c71cbfa52089149157b5ff23de027ac5ab781800a578192d175462103d3bde5d63bdb3a6379b461be64dad45eabff42f758543a9645afd42f6d4248282103ed1e8d5109c9ed66f7941bc53cc71137baa76d50d274bda8d5e8ffbd6e61fe9a5fae736402c00fb269522103aab896d53a8e7d6433137bbba940f9c521e085dd07e60994579b64a6d992cf79210291b7d0b1b692f8f524516ed950872e5da10fb1b808b5a526dedc6fed1cf29807210386aa9372fbab374593466bc5451dc59954e90787f08060964d95c87ef34ca5bb53ae68");
CScript liquidv1{liquidv1bytes.begin(), liquidv1bytes.end()};
t = 0;
n = 0;
BOOST_CHECK(ParseFedPegQuorum(liquidv1, t, n));
BOOST_CHECK_EQUAL(t, 11);
BOOST_CHECK_EQUAL(n, 15);
auto optruebytes = ParseHex("51");
CScript optrue{optruebytes.begin(), optruebytes.end()};
t = 0;
n = 0;
BOOST_CHECK(ParseFedPegQuorum(optrue, t, n));
BOOST_CHECK_EQUAL(t, 1);
BOOST_CHECK_EQUAL(n, 0);
auto op3bytes = ParseHex("53");
CScript op3{op3bytes.begin(), op3bytes.end()};
t = 0;
n = 0;
BOOST_CHECK(ParseFedPegQuorum(op3, t, n));
BOOST_CHECK_EQUAL(t, 3);
BOOST_CHECK_EQUAL(n, 0);
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -663,6 +663,135 @@ private:
return true;
}
// ELEMENTS: check if peg-in transaction pays the burn subsidy, and if minimum peg-in amount is met
bool CheckPeginSubsidyAndMinimum(TxValidationState& state, const CTransaction& tx, const std::vector<unsigned int>& pegin_indices)
{
// pegin_indices was calculated directly from the tx in prechecks, assert this invariant
assert(tx.witness.vtxinwit.size() >= pegin_indices.size());
if (pegin_indices.size() > 0) {
// calculate the burned subsidy value from the tx
CAmount subsidy = 0;
for (const CTxOut& txout : tx.vout) {
if (txout.scriptPubKey.IsUnspendable() && txout.nAsset.GetAsset() == Params().GetConsensus().pegged_asset && !txout.IsFee()) {
subsidy += txout.nValue.GetAmount();
}
}
// calculate the peg-in value, fee, and vsize from the parent chain if we are validating peg-ins
// if validatepegin=0 then the minimum 1 sat/vb subsidy applies
CAmount value = 0;
CAmount parent_fee = 0;
uint32_t parent_vsize = 0;
for (size_t i = 0; i < pegin_indices.size(); ++i) {
// get the parent txid and blockhash from the peg-in witness data
CAmount pvalue;
CAsset passet;
uint256 pgenesis;
CScript pscript;
std::variant<std::monostate, Sidechain::Bitcoin::CTransactionRef, CTransactionRef> ptx;
std::variant<std::monostate, Sidechain::Bitcoin::CMerkleBlock, CMerkleBlock> pmerkle;
if (!DecomposePeginWitness(tx.witness.vtxinwit[pegin_indices[i]].m_pegin_witness, pvalue, passet, pgenesis, pscript, ptx, pmerkle)) {
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "pegin-invalid-parent-tx", "failed to decompose the parent peg-in witness");
}
if (passet != Params().GetConsensus().pegged_asset) {
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "pegin-invalid-asset", "pegin asset is not the pegged asset");
}
value += pvalue;
uint256 txid;
switch (ptx.index()) {
case 0:
// this should never happen, but fail gracefully by rejecting
return false;
case 1:
txid = std::get<Sidechain::Bitcoin::CTransactionRef>(ptx)->GetHash();
break;
case 2:
txid = std::get<CTransactionRef>(ptx)->GetHash();
break;
}
uint256 blockhash;
switch (pmerkle.index()) {
case 0:
return false;
case 1:
blockhash = std::get<Sidechain::Bitcoin::CMerkleBlock>(pmerkle).header.GetHash();
break;
case 2:
blockhash = std::get<CMerkleBlock>(pmerkle).header.GetHash();
break;
}
// get the parent transaction fee, to calculate the fee rate
if (gArgs.GetBoolArg("-validatepegin", Params().GetConsensus().has_parent_chain)) {
UniValue params(UniValue::VARR);
params.push_back(txid.GetHex());
params.push_back(2);
params.push_back(blockhash.GetHex());
UniValue result = CallMainChainRPC("getrawtransaction", params);
if (result["error"].isStr()) {
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "pegin-subsidy-mainchain-error", result["error"]["message"].get_str());
} else {
parent_vsize += result["result"]["vsize"].get_int64();
if (result["result"]["fee"].isNum()) {
// bitcoin core v25+ returns the fee amount
parent_fee += static_cast<CAmount>(std::round(result["result"]["fee"].get_real() * COIN));
} else if (result["result"]["fee"].isObject()) {
// elements returns a fee object
std::string asset = Params().GetConsensus().parent_pegged_asset.GetHex();
if (result["result"]["fee"][asset].isNum()) {
parent_fee += static_cast<CAmount>(std::round(result["result"]["fee"][asset].get_real() * COIN));
}
}
}
}
}
// check if the peg-in value meets the minimum amount
PeginMinimum pegin_minimum = Params().GetPeginMinimum();
if (m_active_chainstate.m_chain.Height() >= pegin_minimum.height && value < pegin_minimum.amount) {
// peg-in value is lower than the minimum
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "pegin-value-too-low", strprintf("peg-in value: %d, minimum peg-in value: %d", FormatMoney(value), FormatMoney(pegin_minimum.amount)));
}
if (m_active_chainstate.m_chain.Height() >= Params().GetPeginSubsidy().height) {
// subsidy is required at this height
// when parent feerate is less than 1 sat/vb, use 1 sat/vb for the calculation
CFeeRate parent_feerate = std::max(CFeeRate{parent_fee, parent_vsize}, CFeeRate{1000});
// for peg-ins below the subsidy threshold, check for enough subsidy
CAmount threshold = Params().GetPeginSubsidy().threshold;
// calculate the subsidy as the amount required to spend the P2WSH output
const auto& fedpegscripts = GetValidFedpegScripts(m_active_chainstate.m_chain.Tip(), Params().GetConsensus(), true /* nextblock_validation */);
int t = 0;
int n = 0;
if (fedpegscripts.size() == 0) return false;
if (!ParseFedPegQuorum(fedpegscripts[0].second, t, n)) return false;
// each P2WSH input is 41 bytes: txid (32) + vout (4) + scriptsig len (1) + sequence (4)
// the witness to spend is `t` signatures + the script size
unsigned int weight = WITNESS_SCALE_FACTOR * (32 + 4 + 1 + 4) + (t * 72 + fedpegscripts[0].second.size());
unsigned int vbytes = (weight + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR;
// multiply by the number of peg-ins
CAmount expected_subsidy = parent_feerate.GetFee(pegin_indices.size() * vbytes);
if (value < threshold && subsidy < expected_subsidy) {
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "pegin-subsidy-too-low",
strprintf("peg-in value: %d, subsidy threshold: %d, subsidy value: %d, expected subsidy: %d, parent feerate: %s",
FormatMoney(value), FormatMoney(threshold), FormatMoney(subsidy), FormatMoney(expected_subsidy), parent_feerate.ToString()));
}
}
}
// either validatepegin=0 and we can't calculate peg-in value and parent feerate
// or we're below the subsidy height
// or the peg-in value is above the subsidy threshold
// or the peg-in paid enough subsidy
return true;
}
private:
CTxMemPool& m_pool;
CCoinsViewCache m_view;
@ -795,6 +924,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
std::vector<std::pair<CScript, CScript>> fedpegscripts = GetValidFedpegScripts(m_active_chainstate.m_chain.Tip(), chainparams.GetConsensus(), true /* nextblock_validation */);
const CCoinsViewCache& coins_cache = m_active_chainstate.CoinsTip();
std::vector<unsigned int> pegin_indices;
// do all inputs exist?
for (unsigned int i = 0; i < tx.vin.size(); i++) {
const CTxIn& txin = tx.vin[i];
@ -816,6 +946,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
if (m_view.IsPeginSpent(pegin)) {
return state.Invalid(TxValidationResult::TX_CONSENSUS, "pegin-already-claimed");
}
pegin_indices.push_back(i);
continue;
}
@ -922,6 +1053,8 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
// ELEMENTS: accept discounted fees for Confidential Transactions only, if enabled.
int64_t package_size = Params().GetAcceptDiscountCT() ? GetDiscountVirtualTransactionSize(tx) : ws.m_vsize;
if (!bypass_limits && !CheckFeeRate(package_size, ws.m_modified_fees, state)) return false;
// ELEMENTS: check if peg-in subsidy is required and min peg-in amount is met
if (!CheckPeginSubsidyAndMinimum(state, tx, pegin_indices)) return false;
ws.m_iters_conflicting = m_pool.GetIterSet(ws.m_conflicts);
// Calculate in-mempool ancestors, up to a limit.

View file

@ -6,6 +6,7 @@
#include <block_proof.h>
#include <core_io.h>
#include <deploymentstatus.h>
#include <dynafed.h>
#include <issuance.h>
#include <key_io.h>
#include <mainchainrpc.h>
@ -15,10 +16,11 @@
#include <script/generic.hpp>
#include <script/pegins.h>
#include <secp256k1.h>
#include <util/moneystr.h>
#include <wallet/coincontrol.h>
#include <wallet/fees.h>
#include <wallet/rpc/util.h>
#include <wallet/receive.h>
#include <wallet/rpc/util.h>
#include <wallet/spend.h>
#include <wallet/wallet.h>
@ -220,6 +222,25 @@ RPCHelpMan getpeginaddress()
ret.pushKV("mainchain_address", EncodeParentDestination(mainchain_dest));
ret.pushKV("claim_script", HexStr(dest_script));
PeginMinimum pegin_minimum = Params().GetPeginMinimum();
if (pegin_minimum.amount > 0) {
ret.pushKV("pegin_min_amount", FormatMoney(pegin_minimum.amount));
}
if (pegin_minimum.height < std::numeric_limits<int>::max()) {
ret.pushKV("pegin_min_height", pegin_minimum.height);
ret.pushKV("pegin_min_active", wallet->chain().getTip()->nHeight >= pegin_minimum.height);
}
PeginSubsidy pegin_subsidy = Params().GetPeginSubsidy();
if (pegin_subsidy.threshold > 0) {
ret.pushKV("pegin_subsidy_threshold", FormatMoney(pegin_subsidy.threshold));
}
if (pegin_subsidy.height < std::numeric_limits<int>::max()) {
ret.pushKV("pegin_subsidy_height", pegin_subsidy.height);
ret.pushKV("pegin_subsidy_active", wallet->chain().getTip()->nHeight >= pegin_subsidy.height);
}
return ret;
},
};
@ -432,7 +453,7 @@ RPCHelpMan initpegoutwallet()
RPCHelpMan sendtomainchain_base()
{
return RPCHelpMan{"sendtomainchain",
"\nSends sidechain funds to the given mainchain address, through the federated pegin mechanism\n"
"\nSends sidechain funds to the given mainchain address, through the federated peg-in mechanism\n"
+ wallet::HELP_REQUIRING_PASSPHRASE,
{
{"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The destination address on Bitcoin mainchain"},
@ -787,7 +808,7 @@ RPCHelpMan sendtomainchain()
extern UniValue signrawtransaction(const JSONRPCRequest& request);
extern UniValue sendrawtransaction(const JSONRPCRequest& request);
template<typename T_tx_ref, typename T_merkle_block>
template <typename T_tx_ref, typename T_merkle_block>
static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef, T_merkle_block& merkleBlock)
{
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
@ -821,9 +842,62 @@ static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef
// Make the tx
CMutableTransaction mtx;
// Construct pegin input
// Construct peg-in input
CreatePegInInput(mtx, 0, txBTCRef, merkleBlock, claim_scripts, txData, txOutProofData, wallet->chain().getTip());
// Get value for peg-in output
CAmount value = 0;
if (!GetAmountFromParentChainPegin(value, *txBTCRef, mtx.vin[0].prevout.n)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Amounts to peg-in must be explicit and asset must be %s", Params().GetConsensus().parent_pegged_asset.GetHex()));
}
const PeginMinimum pegin_minimum = Params().GetPeginMinimum();
if (pwallet->chain().getTip()->nHeight >= pegin_minimum.height && value < pegin_minimum.amount) {
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Peg-in amount (%d) is lower than the minimum peg-in amount for this chain (%d).", FormatMoney(value), FormatMoney(pegin_minimum.amount)));
}
const PeginSubsidy pegin_subsidy = Params().GetPeginSubsidy();
bool subsidy_required = pwallet->chain().getTip()->nHeight >= pegin_subsidy.height && value < pegin_subsidy.threshold;
if (subsidy_required && !gArgs.GetBoolArg("-validatepegin", Params().GetConsensus().has_parent_chain) && request.params[3].isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Bitcoin transaction fee rate must be supplied, because validatepegin is off and this peg-in requires a burn subsidy.");
}
CAmount fee = 0;
uint32_t parent_vsize = 0;
CFeeRate feerate = CFeeRate{0};
if (gArgs.GetBoolArg("-validatepegin", false) && subsidy_required) {
std::string txid = txBTCRef->GetHash().ToString();
std::string blockhash = merkleBlock.header.GetHash().ToString();
UniValue params(UniValue::VARR);
params.push_back(txid);
params.push_back(2);
params.push_back(blockhash);
UniValue result = CallMainChainRPC("getrawtransaction", params);
if (result["error"].isStr()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, result["error"]["message"].get_str());
} else {
parent_vsize = result["result"]["vsize"].get_int64();
if (result["result"]["fee"].isNum()) {
fee = static_cast<CAmount>(std::round(result["result"]["fee"].get_real() * COIN));
} else if (result["result"]["fee"].isObject()) {
std::string asset = Params().GetConsensus().parent_pegged_asset.GetHex();
if (result["result"]["fee"][asset].isNum()) {
fee = static_cast<CAmount>(std::round(result["result"]["fee"][asset].get_real() * COIN));
} else {
throw JSONRPCError(RPC_MISC_ERROR, "No fee result for the parent pegged asset.");
}
} else {
throw JSONRPCError(RPC_MISC_ERROR, "Fee result is not a number or object.");
}
// when parent feerate is less than 1 sat/vb, use 1 sat/vb for the calculation
feerate = std::max(CFeeRate{fee, parent_vsize}, CFeeRate{1000});
}
} else if (!request.params[3].isNull()) {
// manual feerate, specified in sats/vb but CFeeRate takes sats/Kvb
CAmount satsperk = static_cast<CAmount>(std::round(request.params[3].get_real() * 1000));
feerate = std::max(CFeeRate{satsperk}, CFeeRate{1000});
}
// Manually construct peg-in transaction, sign it, and send it off.
// Decrement the output value as much as needed given the total vsize to
// pay the fees.
@ -838,19 +912,17 @@ static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, error.original);
}
// Get value for output
CAmount value = 0;
if (!GetAmountFromParentChainPegin(value, *txBTCRef, mtx.vin[0].prevout.n)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Amounts to pegin must be explicit and asset must be %s", Params().GetConsensus().parent_pegged_asset.GetHex()));
}
// one wallet output and one fee output
// add a wallet output for the peg-in value
mtx.vout.push_back(CTxOut(Params().GetConsensus().pegged_asset, value, GetScriptForDestination(wpkhash)));
if (subsidy_required) {
// add an op_return for the peg-in fee subsidy
mtx.vout.push_back(CTxOut(Params().GetConsensus().pegged_asset, 0, CScript() << OP_RETURN));
}
// add a fee output
mtx.vout.push_back(CTxOut(Params().GetConsensus().pegged_asset, 0, CScript()));
// Estimate fee for transaction, decrement fee output(including witness data)
unsigned int nBytes = GetVirtualTransactionSize(CTransaction(mtx)) +
(1+1+72+1+33)/WITNESS_SCALE_FACTOR;
// Estimate fee for transaction, decrement fee output (including witness data)
unsigned int nBytes = GetVirtualTransactionSize(CTransaction(mtx)) + (1 + 1 + 72 + 1 + 33) / WITNESS_SCALE_FACTOR;
CCoinControl coin_control;
FeeCalculation feeCalc;
CAmount nFeeNeeded = GetMinimumFee(*pwallet, nBytes, coin_control, &feeCalc);
@ -859,8 +931,35 @@ static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
}
mtx.vout[0].nValue = mtx.vout[0].nValue.GetAmount() - nFeeNeeded;
mtx.vout[1].nValue = mtx.vout[1].nValue.GetAmount() + nFeeNeeded;
if (subsidy_required) {
CHECK_NONFATAL(mtx.vout.size() == 3);
// calculate the subsidy as the amount required to spend the P2WSH output
const auto& fedpegscripts = GetValidFedpegScripts(pwallet->chain().getTip(), Params().GetConsensus(), true /* nextblock_validation */);
int t = 0;
int n = 0;
CHECK_NONFATAL(fedpegscripts.size() > 0);
CHECK_NONFATAL(ParseFedPegQuorum(fedpegscripts[0].second, t, n));
// P2WSH input is 41 bytes: txid (32) + vout (4) + scriptsig len (1) + sequence (4)
// the witness to spend is `t` signatures + the script size
unsigned int weight = WITNESS_SCALE_FACTOR * (32 + 4 + 1 + 4) + (t * 72 + fedpegscripts[0].second.size());
unsigned int vbytes = (weight + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR;
CAmount subsidy = feerate.GetFee(vbytes);
CAmount value = mtx.vout[0].nValue.GetAmount() - nFeeNeeded - subsidy;
mtx.vout[0].nValue = value;
mtx.vout[1].nValue = subsidy;
mtx.vout[2].nValue = nFeeNeeded;
if (IsDust(mtx.vout[0], pwallet->chain().relayDustFee())) {
throw JSONRPCError(RPC_WALLET_ERROR, "Peg-in transaction would create dust output.");
}
} else {
CHECK_NONFATAL(mtx.vout.size() == 2);
mtx.vout[0].nValue = mtx.vout[0].nValue.GetAmount() - nFeeNeeded;
mtx.vout[1].nValue = nFeeNeeded;
}
UniValue ret(UniValue::VOBJ);
@ -886,13 +985,14 @@ static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef
RPCHelpMan createrawpegin()
{
return RPCHelpMan{"createrawpegin",
"\nCreates a raw transaction to claim coins from the main chain by creating a pegin transaction with the necessary metadata after the corresponding Bitcoin transaction.\n"
"\nCreates a raw transaction to claim coins from the main chain by creating a peg-in transaction with the necessary metadata after the corresponding Bitcoin transaction.\n"
"Note that this call will not sign the transaction.\n"
"If a transaction is not relayed it may require manual addition to a functionary mempool in order for it to be mined.\n",
{
{"bitcoin_tx", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The raw bitcoin transaction (in hex) depositing bitcoin to the mainchain_address generated by getpeginaddress"},
{"txoutproof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A rawtxoutproof (in hex) generated by the mainchain daemon's `gettxoutproof` containing a proof of only bitcoin_tx"},
{"claim_script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED_NAMED_ARG, "The witness program generated by getpeginaddress. Only needed if not in wallet."},
{"fee_rate", RPCArg::Type::AMOUNT, RPCArg::Optional::OMITTED_NAMED_ARG, "The fee rate of the Bitcoin transaction in sats/vb, only necessary when validatepegin=0."},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
@ -935,13 +1035,14 @@ RPCHelpMan createrawpegin()
RPCHelpMan claimpegin()
{
return RPCHelpMan{"claimpegin",
"\nClaim coins from the main chain by creating a pegin transaction with the necessary metadata after the corresponding Bitcoin transaction.\n"
"\nClaim coins from the main chain by creating a peg-in transaction with the necessary metadata after the corresponding Bitcoin transaction.\n"
"Note that the transaction will not be relayed unless it is buried at least 102 blocks deep.\n"
"If a transaction is not relayed it may require manual addition to a functionary mempool in order for it to be mined.\n",
{
{"bitcoin_tx", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The raw bitcoin transaction (in hex) depositing bitcoin to the mainchain_address generated by getpeginaddress"},
{"txoutproof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A rawtxoutproof (in hex) generated by the mainchain daemon's `gettxoutproof` containing a proof of only bitcoin_tx"},
{"claim_script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED_NAMED_ARG, "The witness program generated by getpeginaddress. Only needed if not in wallet."},
{"fee_rate", RPCArg::Type::AMOUNT, RPCArg::Optional::OMITTED_NAMED_ARG, "The fee rate of the Bitcoin transaction in sats/vb, only necessary when validatepegin=0."},
},
RPCResult{
RPCResult::Type::STR_HEX, "txid", "txid of the resulting sidechain transaction",
@ -996,7 +1097,7 @@ RPCHelpMan claimpegin()
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed");
}
// To check if it's not double spending an existing pegin UTXO, we check mempool acceptance.
// To check if it's not double spending an existing peg-in UTXO, we check mempool acceptance.
const MempoolAcceptResult res = pwallet->chain().testPeginClaimAcceptance(MakeTransactionRef(mtx));
if (res.m_result_type != MempoolAcceptResult::ResultType::VALID) {
bilingual_str error = Untranslated(strprintf("Error: The transaction was rejected! Reason given: %s", res.m_state.ToString()));
@ -1202,7 +1303,7 @@ RPCHelpMan blindrawtransaction()
for (size_t nIn = 0; nIn < tx.vin.size(); ++nIn) {
COutPoint prevout = tx.vin[nIn].prevout;
// Special handling for pegin inputs: no blinds and explicit amount/asset.
// Special handling for peg-in inputs: no blinds and explicit amount/asset.
if (tx.vin[nIn].m_is_pegin) {
std::string err;
if (tx.witness.vtxinwit.size() != tx.vin.size() || !IsValidPeginWitness(tx.witness.vtxinwit[nIn].m_pegin_witness, fedpegscripts, prevout, err, false)) {

View file

@ -0,0 +1,921 @@
#!/usr/bin/env python3
# can be run with parent bitcoind node
# tested with bitcoind v28.2 and v29.0
# test/functional/feature_pegin_subsidy.py --parent_bitcoin --parent_binpath="/path/to/bitcoind" --nosandbox
from decimal import Decimal
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_raises_rpc_error,
find_vout_for_address,
get_auth_cookie,
get_datadir_path,
rpc_port,
p2p_port,
assert_equal,
)
from test_framework import util
PEGIN_MINIMUM_HEIGHT = 102
PEGIN_SUBSIDY_HEIGHT = 150
def get_new_unconfidential_address(node, addr_type="bech32"):
addr = node.getnewaddress("", addr_type)
val_addr = node.getaddressinfo(addr)
if "unconfidential" in val_addr:
return val_addr["unconfidential"]
return val_addr["address"]
class PeginSubsidyTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 3
self.disable_syscall_sandbox = True
def add_options(self, parser):
parser.add_argument(
"--parent_binpath",
dest="parent_binpath",
default="",
help="Use a different binary for launching nodes",
)
parser.add_argument(
"--parent_bitcoin",
dest="parent_bitcoin",
default=False,
action="store_true",
help="Parent nodes are Bitcoin",
)
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def setup_network(self, split=False):
if self.options.parent_bitcoin and self.options.parent_binpath == "":
raise Exception("Can't run with --parent_bitcoin without specifying --parent_binpath")
self.nodes = []
# Setup parent nodes
parent_chain = "elementsregtest" if not self.options.parent_bitcoin else "regtest"
parent_binary = [self.options.parent_binpath] if self.options.parent_binpath != "" else None
extra_args = [
"-port=" + str(p2p_port(0)),
"-rpcport=" + str(rpc_port(0)),
# to test minimum parent tx fee
"-minrelaytxfee=0.00000100",
"-blockmintxfee=0.00000100",
"-mintxfee=0.00000100",
]
if self.options.parent_bitcoin:
# bitcoind can't read elements.conf config files
extra_args.extend(
[
"-regtest=1",
"-printtoconsole=0",
"-server=1",
"-discover=0",
"-keypool=1",
"-listenonion=0",
"-addresstype=legacy", # To make sure bitcoind gives back p2pkh no matter version
"-fallbackfee=0.0002",
"-deprecatedrpc=create_bdb",
]
)
self.expected_stderr = (
f"Error: Unable to bind to 127.0.0.1:{p2p_port(1)} on this computer. Elements Core is probably already running."
)
else:
extra_args.extend(
[
"-validatepegin=0",
"-initialfreecoins=0",
"-anyonecanspendaremine=1",
"-signblockscript=51", # OP_TRUE
"-dustrelayfee=0.00003000", # use the Bitcoin default dust relay fee rate for the parent nodes
]
)
self.expected_stderr = ""
self.add_nodes(1, [extra_args], chain=[parent_chain], binary=parent_binary)
self.start_node(0)
self.log.info(f"Node 0 started (mainchain: {'bitcoind' if self.options.parent_bitcoin else 'elementsd'})")
# set hard-coded mining keys for non-Elements chains
if self.options.parent_bitcoin:
self.nodes[0].set_deterministic_priv_key(
"2Mysp7FKKe52eoC2JmU46irt1dt58TpCvhQ",
"cTNbtVJmhx75RXomhYWSZAafuNNNKPd1cr2ZiUcAeukLNGrHWjvJ",
)
self.parentgenesisblockhash = self.nodes[0].getblockhash(0)
if not self.options.parent_bitcoin:
parent_pegged_asset = self.nodes[0].getsidechaininfo()["pegged_asset"]
# Setup sidechain nodes
# use the current liquidv1 fedpegscript for testing purposes
self.fedpegscript = "5b21020e0338c96a8870479f2396c373cc7696ba124e8635d41b0ea581112b678172612102675333a4e4b8fb51d9d4e22fa5a8eaced3fdac8a8cbf9be8c030f75712e6af992102896807d54bc55c24981f24a453c60ad3e8993d693732288068a23df3d9f50d4821029e51a5ef5db3137051de8323b001749932f2ff0d34c82e96a2c2461de96ae56c2102a4e1a9638d46923272c266631d94d36bdb03a64ee0e14c7518e49d2f29bc401021031c41fdbcebe17bec8d49816e00ca1b5ac34766b91c9f2ac37d39c63e5e008afb2103079e252e85abffd3c401a69b087e590a9b86f33f574f08129ccbd3521ecf516b2103111cf405b627e22135b3b3733a4a34aa5723fb0f58379a16d32861bf576b0ec2210318f331b3e5d38156da6633b31929c5b220349859cc9ca3d33fb4e68aa08401742103230dae6b4ac93480aeab26d000841298e3b8f6157028e47b0897c1e025165de121035abff4281ff00660f99ab27bb53e6b33689c2cd8dcd364bc3c90ca5aea0d71a62103bd45cddfacf2083b14310ae4a84e25de61e451637346325222747b157446614c2103cc297026b06c71cbfa52089149157b5ff23de027ac5ab781800a578192d175462103d3bde5d63bdb3a6379b461be64dad45eabff42f758543a9645afd42f6d4248282103ed1e8d5109c9ed66f7941bc53cc71137baa76d50d274bda8d5e8ffbd6e61fe9a5fae736402c00fb269522103aab896d53a8e7d6433137bbba940f9c521e085dd07e60994579b64a6d992cf79210291b7d0b1b692f8f524516ed950872e5da10fb1b808b5a526dedc6fed1cf29807210386aa9372fbab374593466bc5451dc59954e90787f08060964d95c87ef34ca5bb53ae68"
for n in range(2):
validatepegin = "1" if n == 0 else "0"
extra_args = [
"-printtoconsole=0",
"-port=" + str(p2p_port(1 + n)),
"-rpcport=" + str(rpc_port(1 + n)),
"-validatepegin=%s" % validatepegin,
"-fallbackfee=0.00001000",
"-fedpegscript=%s" % self.fedpegscript,
"-minrelaytxfee=0",
"-blockmintxfee=0",
"-initialfreecoins=0",
"-peginconfirmationdepth=10",
"-mainchainrpchost=127.0.0.1",
"-mainchainrpcport=%s" % rpc_port(0),
"-parentgenesisblockhash=%s" % self.parentgenesisblockhash,
"-parentpubkeyprefix=111",
"-parentscriptprefix=196",
"-parent_bech32_hrp=bcrt",
# Turn of consistency checks that can cause assert when parent node stops
# and a peg-in transaction fails this belt-and-suspenders check.
# NOTE: This can cause spurious problems in regtest, and should be dealt with in a better way.
"-checkmempool=0",
"-peginsubsidyheight=%s" % PEGIN_SUBSIDY_HEIGHT,
"-peginsubsidythreshold=2.0",
"-peginminheight=%s" % PEGIN_MINIMUM_HEIGHT,
"-peginminamount=1.0",
]
if not self.options.parent_bitcoin:
extra_args.extend(
[
"-parentpubkeyprefix=235",
"-parentscriptprefix=75",
"-parent_bech32_hrp=ert",
"-con_parent_chain_signblockscript=51",
"-con_parent_pegged_asset=%s" % parent_pegged_asset,
]
)
# Use rpcuser auth only for first parent.
if n == 0:
# Extract username and password from cookie file and use directly.
datadir = get_datadir_path(self.options.tmpdir, n)
rpc_u, rpc_p = get_auth_cookie(datadir, parent_chain)
extra_args.extend(
[
"-mainchainrpcuser=%s" % rpc_u,
"-mainchainrpcpassword=%s" % rpc_p,
]
)
else:
# Need to specify where to find parent cookie file
datadir = get_datadir_path(self.options.tmpdir, n)
extra_args.append("-mainchainrpccookiefile=" + datadir + "/" + parent_chain + "/.cookie")
self.add_nodes(1, [extra_args], chain=["elementsregtest"])
self.start_node(1 + n)
self.log.info(f"Node {1 + n} started (sidechain: elementsd)")
# We only connect the same-chain nodes, so sync_all works correctly
self.connect_nodes(1, 2)
self.node_groups = [
[self.nodes[0]],
[self.nodes[1], self.nodes[2]],
]
for node_group in self.node_groups:
self.sync_all(node_group)
self.log.info("Setting up network done")
def run_test(self):
self.import_deterministic_coinbase_privkeys() # Create wallets for all nodes
parent = self.nodes[0]
sidechain = self.nodes[1]
sidechain2 = self.nodes[2]
assert_equal(sidechain.getsidechaininfo()["pegin_confirmation_depth"], 10) # 10+2 confirms required to get into mempool and confirm
parent.importprivkey(privkey=parent.get_deterministic_priv_key().key, label="mining")
sidechain.importprivkey(privkey=sidechain.get_deterministic_priv_key().key, label="mining")
util.node_fastmerkle = sidechain
self.generate(parent, 101, sync_fun=self.no_op)
self.generate(sidechain, 101, sync_fun=self.no_op)
def sync_sidechain():
return self.sync_all([sidechain, sidechain2])
DEFAULT_FEERATE = 1.0
def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE):
address = node.getpeginaddress()
mainchain_address, claim_script = (
address["mainchain_address"],
address["claim_script"],
)
txid = parent.sendtoaddress(address=mainchain_address, amount=amount, fee_rate=feerate)
vout = find_vout_for_address(parent, txid, mainchain_address)
self.generate(parent, 12, sync_fun=self.no_op)
txoutproof = parent.gettxoutproof([txid])
bitcoin_txhex = parent.gettransaction(txid)["hex"]
return (
txid,
vout,
txoutproof,
bitcoin_txhex,
claim_script,
)
self.log.info("check new fields for getpeginaddress and getsidechaininfo")
result = sidechain.getpeginaddress()
assert_equal(result["pegin_min_amount"], "1.00")
assert_equal(result["pegin_min_height"], PEGIN_MINIMUM_HEIGHT)
assert_equal(result["pegin_min_active"], False)
assert_equal(result["pegin_subsidy_threshold"], "2.00")
assert_equal(result["pegin_subsidy_height"], PEGIN_SUBSIDY_HEIGHT)
assert_equal(result["pegin_subsidy_active"], False)
result = sidechain.getsidechaininfo()
assert_equal(result["pegin_min_amount"], "1.00")
assert_equal(result["pegin_min_height"], PEGIN_MINIMUM_HEIGHT)
assert_equal(result["pegin_min_active"], False)
assert_equal(result["pegin_subsidy_threshold"], "2.00")
assert_equal(result["pegin_subsidy_height"], PEGIN_SUBSIDY_HEIGHT)
assert_equal(result["pegin_subsidy_active"], False)
self.log.info("check min peg-in amount before minimum peg-in height")
assert_equal(sidechain.getblockchaininfo()["blocks"], PEGIN_MINIMUM_HEIGHT - 1)
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=0.5)
pegin_txid = sidechain.claimpegin(bitcoin_txhex, txoutproof, claim_script)
self.generate(sidechain, 1, sync_fun=sync_sidechain)
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(pegin_tx["confirmations"], 1)
assert_equal(sidechain.getblockchaininfo()["blocks"], PEGIN_MINIMUM_HEIGHT)
result = sidechain.getpeginaddress()
assert_equal(result["pegin_min_amount"], "1.00")
assert_equal(result["pegin_min_height"], PEGIN_MINIMUM_HEIGHT)
assert_equal(result["pegin_min_active"], True)
assert_equal(result["pegin_subsidy_threshold"], "2.00")
assert_equal(result["pegin_subsidy_height"], PEGIN_SUBSIDY_HEIGHT)
assert_equal(result["pegin_subsidy_active"], False)
result = sidechain.getsidechaininfo()
assert_equal(result["pegin_min_amount"], "1.00")
assert_equal(result["pegin_min_height"], PEGIN_MINIMUM_HEIGHT)
assert_equal(result["pegin_min_active"], True)
assert_equal(result["pegin_subsidy_threshold"], "2.00")
assert_equal(result["pegin_subsidy_height"], PEGIN_SUBSIDY_HEIGHT)
assert_equal(result["pegin_subsidy_active"], False)
self.log.info("check min peg-in amount after minimum peg-in height")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=0.5)
assert_raises_rpc_error(
-4,
"Peg-in amount (0.50) is lower than the minimum peg-in amount for this chain (1.00).",
sidechain.claimpegin,
bitcoin_txhex,
txoutproof,
claim_script,
)
# check manually constructed
inputs = [
{
"txid": txid,
"vout": vout,
"pegin_bitcoin_tx": bitcoin_txhex,
"pegin_txout_proof": txoutproof,
"pegin_claim_script": claim_script,
},
]
fee = Decimal("0.00000363")
outputs = [
{sidechain.getnewaddress(): Decimal("0.5") - fee},
{"fee": fee},
]
raw = sidechain.createrawtransaction(inputs, outputs)
signed = sidechain.signrawtransactionwithwallet(raw)
assert_equal(signed["complete"], True)
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-value-too-low")
self.log.info("createrawpegin before enforcement, with validatepegin, below threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=1.0, feerate=2.0)
pegintx = sidechain.createrawpegin(bitcoin_txhex, txoutproof, claim_script)
signed = sidechain.signrawtransactionwithwallet(pegintx["hex"])
assert_equal(signed["complete"], True)
pegin_txid = sidechain.sendrawtransaction(signed["hex"])
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain, 1, sync_fun=sync_sidechain)
self.log.info("createrawpegin before enforcement, with validatepegin, above threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=2.0, feerate=2.0)
pegintx = sidechain.createrawpegin(bitcoin_txhex, txoutproof, claim_script)
signed = sidechain.signrawtransactionwithwallet(pegintx["hex"])
assert_equal(signed["complete"], True)
pegin_txid = sidechain.sendrawtransaction(signed["hex"])
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain, 1, sync_fun=sync_sidechain)
self.log.info("createrawpegin before enforcement, without validatepegin, below threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, amount=1.0, feerate=2.0)
pegintx = sidechain2.createrawpegin(bitcoin_txhex, txoutproof, claim_script)
signed = sidechain2.signrawtransactionwithwallet(pegintx["hex"])
assert_equal(signed["complete"], True)
pegin_txid = sidechain2.sendrawtransaction(signed["hex"])
pegin_tx = sidechain2.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
self.log.info("createrawpegin before enforcement, without validatepegin, above threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, amount=2.0, feerate=2.0)
pegintx = sidechain2.createrawpegin(bitcoin_txhex, txoutproof, claim_script)
signed = sidechain2.signrawtransactionwithwallet(pegintx["hex"])
assert_equal(signed["complete"], True)
pegin_txid = sidechain2.sendrawtransaction(signed["hex"])
pegin_tx = sidechain2.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
self.log.info("claimpegin before enforcement, with validatepegin, below threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=1.0, feerate=2.0)
pegin_txid = sidechain.claimpegin(bitcoin_txhex, txoutproof, claim_script)
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain, 1, sync_fun=sync_sidechain)
self.log.info("claimpegin before enforcement, with validatepegin, above threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=2.0, feerate=2.0)
pegin_txid = sidechain.claimpegin(bitcoin_txhex, txoutproof, claim_script)
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain, 1, sync_fun=sync_sidechain)
self.log.info("claimpegin before enforcement, without validatepegin, below threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, amount=1.0, feerate=2.0)
pegin_txid = sidechain2.claimpegin(bitcoin_txhex, txoutproof, claim_script)
pegin_tx = sidechain2.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
self.log.info("claimpegin before enforcement, without validatepegin, above threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, amount=2.0, feerate=2.0)
pegin_txid = sidechain2.claimpegin(bitcoin_txhex, txoutproof, claim_script)
pegin_tx = sidechain2.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
num = PEGIN_SUBSIDY_HEIGHT - sidechain.getblockchaininfo()["blocks"]
assert num > 0
self.generate(sidechain, num, sync_fun=sync_sidechain)
self.log.info(f"===== peg-in subsidy enforcement at height {PEGIN_SUBSIDY_HEIGHT} =====")
assert_equal(sidechain.getblockchaininfo()["blocks"], PEGIN_SUBSIDY_HEIGHT)
self.log.info("check new fields for getpeginaddress and getsidechaininfo")
result = sidechain.getpeginaddress()
assert_equal(result["pegin_min_amount"], "1.00")
assert_equal(result["pegin_min_height"], PEGIN_MINIMUM_HEIGHT)
assert_equal(result["pegin_min_active"], True)
assert_equal(result["pegin_subsidy_threshold"], "2.00")
assert_equal(result["pegin_subsidy_height"], PEGIN_SUBSIDY_HEIGHT)
assert_equal(result["pegin_subsidy_active"], True)
result = sidechain.getsidechaininfo()
assert_equal(result["pegin_min_amount"], "1.00")
assert_equal(result["pegin_min_height"], PEGIN_MINIMUM_HEIGHT)
assert_equal(result["pegin_min_active"], True)
assert_equal(result["pegin_subsidy_threshold"], "2.00")
assert_equal(result["pegin_subsidy_height"], PEGIN_SUBSIDY_HEIGHT)
assert_equal(result["pegin_subsidy_active"], True)
# blinded pegins
self.log.info("blinded pegin below threshold, with validatepegin, subsidy too low")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=1.0, feerate=1.0)
addr = sidechain.getnewaddress(address_type="blech32")
utxo = sidechain.listunspent()[0]
changeaddr = sidechain.getrawchangeaddress(address_type="blech32")
inputs = [
{
"txid": txid,
"vout": vout,
"pegin_bitcoin_tx": bitcoin_txhex,
"pegin_txout_proof": txoutproof,
"pegin_claim_script": claim_script,
},
{
"txid": utxo["txid"],
"vout": utxo["vout"],
},
]
fee = Decimal("0.00000363")
subsidy = Decimal("0.00000395")
outputs = [
{addr: Decimal("1.0") - fee - subsidy},
{changeaddr: utxo["amount"]},
{"burn": subsidy},
{"fee": fee},
]
raw = sidechain.createrawtransaction(inputs, outputs)
blinded = sidechain.blindrawtransaction(hexstring=raw, ignoreblindfail=False)
signed = sidechain.signrawtransactionwithwallet(blinded)
assert_equal(signed["complete"], True)
# node 2 can't validatepegin but checks the minimum subsidy
accept = sidechain2.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low")
# node 1 will reject
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low")
assert_raises_rpc_error(
-26,
"pegin-subsidy-too-low",
sidechain.sendrawtransaction,
signed["hex"],
)
self.log.info("blinded peg-in above threshold, with validatepegin")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=2.0, feerate=1.0)
addr = sidechain.getnewaddress(address_type="blech32")
utxo = sidechain.listunspent()[0]
changeaddr = sidechain.getrawchangeaddress(address_type="blech32")
inputs = [
{
"txid": txid,
"vout": vout,
"pegin_bitcoin_tx": bitcoin_txhex,
"pegin_txout_proof": txoutproof,
"pegin_claim_script": claim_script,
},
{
"txid": utxo["txid"],
"vout": utxo["vout"],
},
]
fee = Decimal("0.00000363")
outputs = [
{addr: Decimal("2.0") - fee},
{changeaddr: utxo["amount"]},
{"fee": fee},
]
raw = sidechain.createrawtransaction(inputs, outputs)
blinded = sidechain.blindrawtransaction(hexstring=raw, ignoreblindfail=False)
signed = sidechain.signrawtransactionwithwallet(blinded)
assert_equal(signed["complete"], True)
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], True)
# =======
self.log.info("createrawpegin after enforcement, with validatepegin, below threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain)
pegintx = sidechain.createrawpegin(bitcoin_txhex, txoutproof, claim_script)
signed = sidechain.signrawtransactionwithwallet(pegintx["hex"])
pegin_txid = sidechain.sendrawtransaction(signed["hex"])
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 3)
# WSH input 41 bytes * 4 = 164 weight
# Witness (11 * 72 bytes signatures + 626 bytes script size) = 1418 weight
# (164 + 1418 + 3) / 4 = 396 vbytes
assert_equal(pegin_tx["decoded"]["vout"][1]["value"], Decimal("0.00000396"))
self.generate(sidechain, 1, sync_fun=sync_sidechain)
self.log.info("createrawpegin after enforcement, with validatepegin, above threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=3.0, feerate=2.0)
pegintx = sidechain.createrawpegin(bitcoin_txhex, txoutproof, claim_script)
signed = sidechain.signrawtransactionwithwallet(pegintx["hex"])
pegin_txid = sidechain.sendrawtransaction(signed["hex"])
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain, 1, sync_fun=sync_sidechain)
self.log.info("createrawpegin after enforcement, without validatepegin, below threshold")
feerate = 2.0
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, 1.0, feerate)
assert_raises_rpc_error(
-8,
"Bitcoin transaction fee rate must be supplied, because validatepegin is off and this peg-in requires a burn subsidy.",
sidechain2.createrawpegin,
bitcoin_txhex,
txoutproof,
claim_script,
)
pegintx = sidechain2.createrawpegin(bitcoin_txhex, txoutproof, claim_script, feerate)
signed = sidechain2.signrawtransactionwithwallet(pegintx["hex"])
pegin_txid = sidechain2.sendrawtransaction(signed["hex"])
pegin_tx = sidechain2.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 3)
assert_equal(pegin_tx["decoded"]["vout"][1]["value"], Decimal("0.00000792"))
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
self.log.info("createrawpegin after enforcement, without validatepegin, above threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, amount=2.0, feerate=2.0)
pegintx = sidechain2.createrawpegin(bitcoin_txhex, txoutproof, claim_script, feerate)
signed = sidechain2.signrawtransactionwithwallet(pegintx["hex"])
assert_equal(signed["complete"], True)
pegin_txid = sidechain2.sendrawtransaction(signed["hex"])
pegin_tx = sidechain2.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
self.log.info("claimpegin after enforcement, with validatepegin, below threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=1.0, feerate=2.0)
pegin_txid = sidechain.claimpegin(bitcoin_txhex, txoutproof, claim_script)
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 3)
assert_equal(pegin_tx["decoded"]["vout"][1]["value"], Decimal("0.00000792"))
self.generate(sidechain, 1, sync_fun=sync_sidechain)
self.log.info("claimpegin after enforcement, with validatepegin, above threshold")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=2.0, feerate=2.0)
pegin_txid = sidechain.claimpegin(bitcoin_txhex, txoutproof, claim_script)
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain, 1, sync_fun=sync_sidechain)
self.log.info("claimpegin after enforcement, without validatepegin, below threshold")
feerate = 2.0
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, 1.0, feerate)
assert_raises_rpc_error(
-8,
"Bitcoin transaction fee rate must be supplied, because validatepegin is off and this peg-in requires a burn subsidy.",
sidechain2.claimpegin,
bitcoin_txhex,
txoutproof,
claim_script,
)
pegin_txid = sidechain2.claimpegin(bitcoin_txhex, txoutproof, claim_script, feerate)
pegin_tx = sidechain2.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 3)
assert_equal(pegin_tx["decoded"]["vout"][1]["value"], Decimal("0.00000792"))
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
self.log.info("claimpegin after enforcement, without validatepegin, above threshold")
feerate = 2.0
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, 2.0, feerate)
pegin_txid = sidechain2.claimpegin(bitcoin_txhex, txoutproof, claim_script, feerate)
pegin_tx = sidechain2.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 2)
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
self.log.info("claimpegin after enforcement, without validatepegin, below threshold, with incorrect subsidy output")
# should be accepted by sidechain2 but rejected by the node that is validating pegins
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, amount=1.0, feerate=2.0)
assert_raises_rpc_error(
-8,
"Bitcoin transaction fee rate must be supplied, because validatepegin is off and this peg-in requires a burn subsidy.",
sidechain2.claimpegin,
bitcoin_txhex,
txoutproof,
claim_script,
)
low_feerate = 1.0
pegin_txid = sidechain2.claimpegin(bitcoin_txhex, txoutproof, claim_script, low_feerate)
pegin_tx = sidechain2.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 3)
accept = sidechain.testmempoolaccept([pegin_tx["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low")
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, amount=1.99999999, feerate=2.0)
assert_raises_rpc_error(
-8,
"Bitcoin transaction fee rate must be supplied, because validatepegin is off and this peg-in requires a burn subsidy.",
sidechain2.claimpegin,
bitcoin_txhex,
txoutproof,
claim_script,
)
low_feerate = 1.0
pegin_txid = sidechain2.claimpegin(bitcoin_txhex, txoutproof, claim_script, low_feerate)
pegin_tx = sidechain2.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 3)
accept = sidechain.testmempoolaccept([pegin_tx["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low")
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
# sidechain2 should accept a 1 sat/vb subsidy for a higher feerate parent, but validating node should reject
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, amount=1.0, feerate=2.0)
# first try with less than 1 sat/vb subsidy
inputs = [
{
"txid": txid,
"vout": vout,
"pegin_bitcoin_tx": bitcoin_txhex,
"pegin_txout_proof": txoutproof,
"pegin_claim_script": claim_script,
},
]
fee = Decimal("0.00000363")
addr = get_new_unconfidential_address(sidechain)
# subsidy less than 1 sat/vb
subsidy = Decimal("0.00000395")
outputs = [
{addr: Decimal("1.0") - fee - subsidy},
{"burn": subsidy},
{"fee": fee},
]
raw = sidechain2.createrawtransaction(inputs, outputs)
signed = sidechain2.signrawtransactionwithwallet(raw)
assert_equal(signed["complete"], True)
accept = sidechain2.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low")
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low")
# subsidy for 1 sat/vb accepted by sidechain2, but rejected by validating node
subsidy = Decimal("0.00000396")
outputs = [
{addr: Decimal("1.0") - fee - subsidy},
{"burn": subsidy},
{"fee": fee},
]
raw = sidechain2.createrawtransaction(inputs, outputs)
signed = sidechain2.signrawtransactionwithwallet(raw)
assert_equal(signed["complete"], True)
accept = sidechain2.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], True)
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low")
# sub 1 sat/vb parent feerate should use 1 sat/vb for subsidy calculation
self.log.info("claimpegin after enforcement, with validatepegin, below threshold, sub 1 sat/vb parent")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=1, feerate=0.1)
pegin_txid = sidechain.claimpegin(bitcoin_txhex, txoutproof, claim_script)
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 3)
assert_equal(pegin_tx["decoded"]["vout"][1]["value"], Decimal("0.00000396"))
# check manually constructed peg-in from a sub 1 sat/vb parent
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=1, feerate=0.1)
inputs = [
{
"txid": txid,
"vout": vout,
"pegin_bitcoin_tx": bitcoin_txhex,
"pegin_txout_proof": txoutproof,
"pegin_claim_script": claim_script,
},
]
fee = Decimal("0.00000363")
addr = get_new_unconfidential_address(sidechain)
# subsidy too low
subsidy = Decimal("0.00000395")
outputs = [
{addr: Decimal("1.0") - fee - subsidy},
{"burn": subsidy},
{"fee": fee},
]
raw = sidechain.createrawtransaction(inputs, outputs)
signed = sidechain.signrawtransactionwithwallet(raw)
assert_equal(signed["complete"], True)
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low")
# subsidy accepted
subsidy = Decimal("0.00000396")
outputs = [
{addr: Decimal("1.0") - fee - subsidy},
{"burn": subsidy},
{"fee": fee},
]
raw = sidechain.createrawtransaction(inputs, outputs)
signed = sidechain.signrawtransactionwithwallet(raw)
assert_equal(signed["complete"], True)
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], True)
sidechain.sendrawtransaction(signed["hex"])
self.generate(sidechain, 1, sync_fun=sync_sidechain)
# =================================
self.log.info("construct a multi-pegin tx, below threshold")
feerate = 2.0
txid1, vout1, txoutproof1, bitcoin_txhex1, claim_script1 = parent_pegin(parent, sidechain, 0.5, feerate)
txid2, vout2, txoutproof2, bitcoin_txhex2, claim_script2 = parent_pegin(parent, sidechain, 1.0, feerate)
addr1 = get_new_unconfidential_address(sidechain)
addr2 = get_new_unconfidential_address(sidechain)
inputs = [
{
"txid": txid1,
"vout": vout1,
"pegin_bitcoin_tx": bitcoin_txhex1,
"pegin_txout_proof": txoutproof1,
"pegin_claim_script": claim_script1,
},
{
"txid": txid2,
"vout": vout2,
"pegin_bitcoin_tx": bitcoin_txhex2,
"pegin_txout_proof": txoutproof2,
"pegin_claim_script": claim_script2,
},
]
fee = Decimal("0.00000363")
subsidy = Decimal("0.00001583")
outputs = [
{addr1: Decimal("0.5") - fee - subsidy},
{addr2: 1.0},
{"burn": subsidy},
{"fee": fee},
]
raw = sidechain.createrawtransaction(inputs, outputs)
signed = sidechain.signrawtransactionwithwallet(raw)
assert_equal(signed["complete"], True)
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low")
subsidy = Decimal("0.00001584")
outputs = [
{addr1: Decimal("0.5") - fee - subsidy},
{addr2: 1.0},
{"burn": subsidy},
{"fee": fee},
]
raw = sidechain.createrawtransaction(inputs, outputs)
signed = sidechain.signrawtransactionwithwallet(raw)
assert_equal(signed["complete"], True)
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], True)
sidechain.sendrawtransaction(signed["hex"])
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
self.log.info("construct a multi-pegin tx, above threshold")
txid1, vout1, txoutproof1, bitcoin_txhex1, claim_script1 = parent_pegin(parent, sidechain, amount=0.5, feerate=1.0)
txid2, vout2, txoutproof2, bitcoin_txhex2, claim_script2 = parent_pegin(parent, sidechain, amount=1.5, feerate=2.0)
addr1 = get_new_unconfidential_address(sidechain)
addr2 = get_new_unconfidential_address(sidechain)
inputs = [
{
"txid": txid1,
"vout": vout1,
"pegin_bitcoin_tx": bitcoin_txhex1,
"pegin_txout_proof": txoutproof1,
"pegin_claim_script": claim_script1,
},
{
"txid": txid2,
"vout": vout2,
"pegin_bitcoin_tx": bitcoin_txhex2,
"pegin_txout_proof": txoutproof2,
"pegin_claim_script": claim_script2,
},
]
fee = Decimal("0.00000363")
outputs = [
{addr1: 0.5},
{addr2: Decimal("1.5") - fee},
{"fee": fee},
]
raw = sidechain.createrawtransaction(inputs, outputs)
signed = sidechain.signrawtransactionwithwallet(raw)
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], True)
sidechain.sendrawtransaction(signed["hex"])
self.generate(sidechain2, 1, sync_fun=sync_sidechain)
# minimum peg-in amount is 1.0
self.log.info("claimpegin below minimum peg-in amount")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=0.99999999)
assert_raises_rpc_error(
-4,
"Peg-in amount (0.99999999) is lower than the minimum peg-in amount for this chain (1.00).",
sidechain2.claimpegin,
bitcoin_txhex,
txoutproof,
claim_script,
)
# check minimum peg-in amount in mempool validation by constructing manually
self.log.info("rawtransaction below minimum peg-in amount")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, amount=0.99999999)
addr = sidechain2.getnewaddress()
inputs = [
{
"txid": txid,
"vout": vout,
"pegin_bitcoin_tx": bitcoin_txhex,
"pegin_txout_proof": txoutproof,
"pegin_claim_script": claim_script,
},
]
fee = Decimal("0.00000363")
subsidy = Decimal("0.00000396")
outputs = [
{addr: Decimal("0.99999999") - fee - subsidy},
{"burn": subsidy},
{"fee": fee},
]
raw = sidechain2.createrawtransaction(inputs, outputs)
signed = sidechain2.signrawtransactionwithwallet(raw)
assert_equal(signed["complete"], True)
accept = sidechain2.testmempoolaccept([signed["hex"]])
# node2 can check the min peg-in amount as the peg-in amount is in the witness
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-value-too-low")
# node1 rejects below the min peg-in amount with validatepegin
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "pegin-value-too-low")
assert_raises_rpc_error(
-26,
"pegin-value-too-low",
sidechain.sendrawtransaction,
signed["hex"],
)
self.generate(sidechain, 1, sync_fun=sync_sidechain)
# test various feerates
self.log.info("claimpegin with validatepegin, below threshold, at various feerates")
for feerate in [1.0, 1.5, 2.0, 2.3, 3.9, 4.6, 5.2, 6.1, 10.01, 20.7, 22.22, 24.18]:
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, 1.0, feerate)
pegin_txid = sidechain.claimpegin(bitcoin_txhex, txoutproof, claim_script)
pegin_tx = sidechain.gettransaction(pegin_txid, True, True)
assert_equal(len(pegin_tx["decoded"]["vout"]), 3)
self.generate(sidechain, 1, sync_fun=sync_sidechain)
# dust error
# restart node1 with no min peg-in amount
self.stop_node(1, expected_stderr=self.expected_stderr) # when running with bitcoind as parent node this stderr can occur
self.start_node(1, extra_args=sidechain.extra_args + ["-peginminamount=0"])
self.log.info("claimpegin dust error")
amount = Decimal("0.00000546") if self.options.parent_bitcoin else Decimal("0.00000645")
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount)
assert_raises_rpc_error(
-4,
"Peg-in transaction would create dust output.",
sidechain.claimpegin,
bitcoin_txhex,
txoutproof,
claim_script,
)
# check dust mempool validation by constructing manually
txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain2, amount=0.00001570)
addr = sidechain2.getnewaddress()
inputs = [
{
"txid": txid,
"vout": vout,
"pegin_bitcoin_tx": bitcoin_txhex,
"pegin_txout_proof": txoutproof,
"pegin_claim_script": claim_script,
},
]
fee = Decimal("0.00000363")
subsidy = Decimal("0.00001194")
outputs = [
{addr: Decimal("0.00001570") - fee - subsidy}, # 14 sats is dust at 0.1 sat/vb dustrelayfee
{"burn": subsidy},
{"fee": fee},
]
raw = sidechain2.createrawtransaction(inputs, outputs)
signed = sidechain2.signrawtransactionwithwallet(raw)
assert_equal(signed["complete"], True)
accept = sidechain2.testmempoolaccept([signed["hex"]])
# mempool validation already checks for dust outputs
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "dust")
accept = sidechain.testmempoolaccept([signed["hex"]])
assert_equal(accept[0]["allowed"], False)
assert_equal(accept[0]["reject-reason"], "dust")
assert_raises_rpc_error(
-26,
"dust",
sidechain.sendrawtransaction,
signed["hex"],
)
# Manually stop sidechains first, then the parent chain.
self.stop_node(2)
self.stop_node(1, expected_stderr=self.expected_stderr) # when running with bitcoind as parent node this stderr can occur
self.stop_node(0)
if __name__ == "__main__":
PeginSubsidyTest().main()

View file

@ -191,6 +191,7 @@ BASE_SCRIPTS = [
# ELEMENTS: discounted Confidential Transactions
'feature_discount_ct.py',
'feature_discount_ct_ordering.py',
'feature_pegin_subsidy.py --legacy-wallet',
'wallet_multiwallet.py --legacy-wallet',
'wallet_multiwallet.py --descriptors',
'wallet_multiwallet.py --usecli',