Dynafed RPC support, tests, and deployment for custom chains

This commit is contained in:
Gregory Sanders 2019-05-31 14:03:02 -04:00
parent aac354b5ba
commit 47db75e39f
15 changed files with 849 additions and 342 deletions

View file

@ -605,6 +605,10 @@ class CCustomParams : public CRegTestParams {
consensus.vDeployments[Consensus::DEPLOYMENT_CSV].nStartTime = args.GetArg("-con_csv_deploy_start", Consensus::BIP9Deployment::ALWAYS_ACTIVE);
consensus.vDeployments[Consensus::DEPLOYMENT_CSV].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_DYNA_FED].bit = 25;
consensus.vDeployments[Consensus::DEPLOYMENT_DYNA_FED].nStartTime = args.GetArg("-con_dyna_deploy_start", Consensus::BIP9Deployment::ALWAYS_ACTIVE);
consensus.vDeployments[Consensus::DEPLOYMENT_DYNA_FED].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
}
void SetGenesisBlock() {

View file

@ -33,6 +33,8 @@
#include <validationinterface.h>
#include <versionbitsinfo.h>
#include <warnings.h>
#include <pegins.h>
#include <dynafed.h>
#include <assert.h>
#include <stdint.h>
@ -79,6 +81,29 @@ double GetDifficulty(const CBlockIndex* blockindex)
return dDiff;
}
UniValue paramEntryToJSON(const ConsensusParamEntry& entry)
{
UniValue result(UniValue::VOBJ);
result.pushKV("signblockscript", HexStr(entry.m_signblockscript));
result.pushKV("max_block_witness", (uint64_t)entry.m_sbs_wit_limit);
result.pushKV("fedpegscript", HexStr(entry.m_fedpegscript));
UniValue result_extension(UniValue::VARR);
for (auto& item : entry.m_extension_space) {
result_extension.push_back(HexStr(item));
}
result.pushKV("extension_space", result_extension);
return result;
}
UniValue dynaParamsToJSON(const DynaFedParams& d_params)
{
AssertLockHeld(cs_main);
UniValue ret(UniValue::VOBJ);
ret.pushKV("current", paramEntryToJSON(d_params.m_current));
ret.pushKV("proposed", paramEntryToJSON(d_params.m_proposed));
return ret;
}
static int ComputeNextBlockAndDepth(const CBlockIndex* tip, const CBlockIndex* blockindex, const CBlockIndex*& next)
{
next = tip->GetAncestor(blockindex->nHeight + 1);
@ -110,6 +135,9 @@ UniValue blockheaderToJSON(const CBlockIndex* tip, const CBlockIndex* blockindex
} else {
result.pushKV("signblock_witness_asm", ScriptToAsmStr(blockindex->proof.solution));
result.pushKV("signblock_witness_hex", HexStr(blockindex->proof.solution));
if (!blockindex->d_params.IsNull()) {
result.pushKV("dynamic_parameters", dynaParamsToJSON(blockindex->d_params));
}
}
result.pushKV("nTx", (uint64_t)blockindex->nTx);
if (blockindex->pprev)
@ -154,9 +182,13 @@ UniValue blockToJSON(const CBlock& block, const CBlockIndex* tip, const CBlockIn
result.pushKV("difficulty", GetDifficulty(blockindex));
result.pushKV("chainwork", blockindex->nChainWork.GetHex());
} else {
result.pushKV("signblock_witness_asm", ScriptToAsmStr(blockindex->proof.solution));
result.pushKV("signblock_witness_hex", HexStr(blockindex->proof.solution));
result.pushKV("signblock_challenge", HexStr(blockindex->proof.challenge));
if (block.m_dyna_params.IsNull()) {
result.pushKV("signblock_witness_asm", ScriptToAsmStr(blockindex->proof.solution));
result.pushKV("signblock_witness_hex", HexStr(blockindex->proof.solution));
result.pushKV("signblock_challenge", HexStr(blockindex->proof.challenge));
} else {
result.pushKV("dynamic_parameters", dynaParamsToJSON(block.m_dyna_params));
}
}
result.pushKV("nTx", (uint64_t)blockindex->nTx);
@ -771,6 +803,24 @@ static UniValue getblockheader(const JSONRPCRequest& request)
" \"nTx\" : n, (numeric) The number of transactions in the block.\n"
" \"signblock_witness_asm\" : \"xxxx\", (string) ASM of sign block witness data.\n"
" \"signblock_witness_hex\" : \"xxxx\", (string) Hex of sign block witness data.\n"
" \"dynamic_parameters\" : (obj) Dynamic federation parameters in the block, if any.\n"
" {\n"
" \"current\" : (obj) enforced dynamic federation parameters. Do note that only the signblockscript is published for each block, while others are published only at epoch start.\n"
" {\n"
" \"signblockscript\" : \"xxxx\", (string) signblock script in hex\n"
" \"max_block_witness\" : x, (numeric) Maximum serialized size of the block witness stack\n"
" \"fedpegscript\" : \"xxxx\", (string) fedpegscript in hex\n"
" \"extension_space\" : (array) Array of hex-encoded strings\n"
" [\n"
" xxxx,\n"
" ...\n"
" ]\n"
" }\n"
" \"proposed\" : (obj) Proposed paramaters. Uninforced. Must be published in full.\n"
" {\n"
" ... same entries as current\n"
" }\n"
" }\n"
" \"previousblockhash\" : \"hash\", (string) The hash of the previous block\n"
" \"nextblockhash\" : \"hash\", (string) The hash of the next block\n"
"}\n"
@ -873,6 +923,24 @@ static UniValue getblock(const JSONRPCRequest& request)
" \"nTx\" : n, (numeric) The number of transactions in the block.\n"
" \"signblock_witness_asm\" : \"xxxx\", (string) ASM of sign block witness data.\n"
" \"signblock_witness_hex\" : \"xxxx\", (string) Hex of sign block witness data.\n"
" \"dynamic_parameters\" : (obj) Dynamic federation parameters in the block, if any.\n"
" {\n"
" \"current\" : (obj) enforced dynamic federation parameters. Do note that only the signblockscript is published for each block, while others are published only at epoch start.\n"
" {\n"
" \"signblockscript\" : \"xxxx\", (string) signblock script in hex\n"
" \"max_block_witness\" : x, (numeric) Maximum serialized size of the block witness stack\n"
" \"fedpegscript\" : \"xxxx\", (string) fedpegscript in hex\n"
" \"extension_space\" : (array) Array of hex-encoded strings\n"
" [\n"
" xxxx,\n"
" ...\n"
" ]\n"
" }\n"
" \"proposed\" : (obj) Proposed paramaters. Uninforced. Must be published in full.\n"
" {\n"
" ... same entries as current\n"
" }\n"
" }\n"
" \"previousblockhash\" : \"hash\", (string) The hash of the previous block\n"
" \"nextblockhash\" : \"hash\" (string) The hash of the next block\n"
"}\n"
@ -1310,8 +1378,14 @@ UniValue getblockchaininfo(const JSONRPCRequest& request)
" \"chainwork\": \"xxxx\" (string) total amount of work in active chain, in hexadecimal\n"
" \"size_on_disk\": xxxxxx, (numeric) the estimated size of the block and undo files on disk\n"
" \"pruned\": xx, (boolean) if the blocks are subject to pruning\n"
" \"signblock_asm\" : \"xxxx\", (string) ASM of sign block challenge data.\n"
" \"signblock_hex\" : \"xxxx\", (string) Hex of sign block challenge data.\n"
" \"signblock_asm\" : \"xxxx\", (string) ASM of sign block challenge data from genesis block.\n"
" \"signblock_hex\" : \"xxxx\", (string) Hex of sign block challenge data from genesis block.\n"
" \"current_signblock_asm\" : \"xxxx\", (string) ASM of sign block challenge data enforced on the next block.\n"
" \"current_signblock_hex\" : \"xxxx\", (string) Hex of sign block challenge data enforced on the next block.\n"
" \"max_block_witness\" : xx, (numeric) maximum sized block witness serialized size for the next block.\n"
" \"epoch_length\" : xx, (numeric) Length of dynamic federations epoch, or signaling period\n"
" \"epoch_age\" : xx, (numeric) number of blocks into a dynamic federation epoch chain tip is. This number is between 0 to epoch_length-1\n"
" \"extension_space\" : [\"xxxx\", ...], (array) Array of extension fields in dynamic blockheader\n"
" \"pruneheight\": xxxxxx, (numeric) lowest-height complete block stored (only present if pruning is enabled)\n"
" \"automatic_pruning\": xx, (boolean) whether automatic pruning is enabled (only present if pruning is enabled)\n"
" \"prune_target_size\": xxxxxx, (numeric) the target size used by pruning (only present if automatic pruning is enabled)\n"
@ -1374,6 +1448,28 @@ UniValue getblockchaininfo(const JSONRPCRequest& request)
CScript sign_block_script = chainparams.GetConsensus().signblockscript;
obj.pushKV("signblock_asm", ScriptToAsmStr(sign_block_script));
obj.pushKV("signblock_hex", HexStr(sign_block_script));
if (!IsDynaFedEnabled(chainActive.Tip(), chainparams.GetConsensus())) {
obj.pushKV("current_signblock_asm", ScriptToAsmStr(sign_block_script));
obj.pushKV("current_signblock_hex", HexStr(sign_block_script));
obj.pushKV("max_block_witness", (uint64_t)chainparams.GetConsensus().max_block_signature_size);
UniValue arr(UniValue::VARR);
for (const auto& extension : chainparams.GetConsensus().first_extension_space) {
arr.push_back(HexStr(extension));
}
obj.pushKV("extension_space", arr);
} else {
const ConsensusParamEntry entry = ComputeNextBlockFullCurrentParameters(chainActive.Tip(), chainparams.GetConsensus());
obj.pushKV("current_signblock_asm", ScriptToAsmStr(entry.m_signblockscript));
obj.pushKV("current_signblock_hex", HexStr(entry.m_signblockscript));
obj.pushKV("max_block_witness", (uint64_t)entry.m_sbs_wit_limit);
UniValue arr(UniValue::VARR);
for (const auto& extension : entry.m_extension_space) {
arr.push_back(HexStr(extension));
}
obj.pushKV("extension_space", arr);
obj.pushKV("epoch_length", (uint64_t)chainparams.GetConsensus().dynamic_epoch_length);
obj.pushKV("epoch_age", (uint64_t)(chainActive.Tip()->nHeight % chainparams.GetConsensus().dynamic_epoch_length));
}
}
if (fPruneMode) {
@ -2407,7 +2503,12 @@ UniValue getsidechaininfo(const JSONRPCRequest& request)
{},
RPCResult{
"{\n"
" \"fedpegscript\": \"xxxx\", (string) The fedpegscript in hex\n"
" \"fedpegscript\": \"xxxx\", (string) The fedpegscript in hex from genesis block\n"
" \"current_fedpegscripts\": (array) The currently-enforced fedpegscripts in hex. Peg-ins for any entries on this list are honored by consensus and policy. Oldest first. Two total entries are possible.\n"
" [\n"
" \"xxxx\", (string) Hex-encoded active fedpegscript\n"
" ...\n"
" ]\n"
" \"pegged_asset\" : \"xxxx\", (string) Pegged asset type in hex\n"
" \"min_peg_diff\" : \"xxxx\", (string) The minimum difficulty parent chain header target. Peg-in headers that have less work will be rejected as an anti-Dos measure.\n"
" \"parent_blockhash\" : \"xxxx\", (string) The parent genesis blockhash as source of pegged-in funds.\n"
@ -2432,6 +2533,13 @@ UniValue getsidechaininfo(const JSONRPCRequest& request)
UniValue obj(UniValue::VOBJ);
obj.pushKV("fedpegscript", HexStr(consensus.fedpegScript.begin(), consensus.fedpegScript.end()));
// We use mempool_validation as true to show what is enforced for *next* block
std::vector<CScript> fedpegscripts = GetValidFedpegScripts(chainActive.Tip(), consensus, true /* nextblock_validation */);
UniValue fedpeg_entries(UniValue::VARR);
for (const auto& script : fedpegscripts) {
fedpeg_entries.push_back(HexStr(script));
}
obj.pushKV("current_fedpegscripts", fedpeg_entries);
obj.pushKV("pegged_asset", consensus.pegged_asset.GetHex());
obj.pushKV("min_peg_diff", consensus.parentChainPowLimit.GetHex());
obj.pushKV("parent_blockhash", parent_blockhash.GetHex());

View file

@ -176,6 +176,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "rawissueasset", 1, "issuances" },
{ "rawreissueasset", 1, "reissuances" },
{ "getnewblockhex", 0, "min_tx_age" },
{ "getnewblockhex", 1, "proposed_parameters" },
{ "testproposedblock", 1, "acceptnonstd" },
{ "issueasset", 0, "assetamount" },
{ "issueasset", 1, "tokenamount" },

View file

@ -141,6 +141,14 @@ UniValue generateBlocks(std::shared_ptr<CReserveScript> coinbaseScript, int nGen
continue;
}
}
// Fill out block witness if dynamic federation is enabled
// since we are assuming WSH(OP_TRUE)
if (!pblock->m_dyna_params.IsNull()) {
CScript op_true(OP_TRUE);
pblock->m_signblock_witness.stack.push_back(std::vector<unsigned char>(op_true.begin(), op_true.end()));
}
std::shared_ptr<const CBlock> shared_pblock = std::make_shared<const CBlock>(*pblock);
if (!ProcessNewBlock(Params(), shared_pblock, true, nullptr))
throw JSONRPCError(RPC_INTERNAL_ERROR, "ProcessNewBlock, block not accepted");
@ -989,15 +997,27 @@ static UniValue estimaterawfee(const JSONRPCRequest& request)
UniValue getnewblockhex(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() > 1)
if (request.fHelp || request.params.size() > 2)
throw std::runtime_error(
RPCHelpMan{"getnewblockhex",
"\nGets hex representation of a proposed, unmined new block\n",
{
{"min_tx_age", RPCArg::Type::NUM, /* default */ "0", "How many seconds a transaction must have been in the mempool to be inluded in the block proposal. This may help with faster block convergence among functionaries using compact blocks."},
{"proposed_parameters", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED , "Parameters to be used in dynamic federations blocks as proposals. During a period of `-dynamic_epoch_length` blocks, 4/5 of total blocks must signal these parameters for the proposal to become activated in the next epoch.",
{
{"signblockscript", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Hex-encoded block signing script to propose"},
{"max_block_witness", RPCArg::Type::NUM, RPCArg::Optional::NO, "Total size in witness bytes that are allowed in the dynamic federations block witness for blocksigning"},
{"fedpegscript", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Hex-encoded fedpegscript for dynamic block proposal"},
{"extension_space", RPCArg::Type::ARR, RPCArg::Optional::NO, "Array of additional fields to embed in the dynamic blockheader. Has no consensus meaning aside from serialized size changes. This space is currently is only used for PAK enforcement.",
{
{"", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Hex encoded string for extension entries."},
},
},
},
"proposed_parameters"},
},
RPCResult{
"blockhex (hex) The block hex\n"
"blockhex (hex) The block hex\n"
},
RPCExamples{
HelpExampleCli("getnewblockhex", ""),
@ -1009,9 +1029,47 @@ UniValue getnewblockhex(const JSONRPCRequest& request)
throw JSONRPCError(RPC_INVALID_PARAMETER, "min_tx_age must be non-negative.");
}
// Construct proposed parameter entry, if any
ConsensusParamEntry proposed;
if (!request.params[1].isNull()) {
if (!IsDynaFedEnabled(chainActive.Tip(), Params().GetConsensus())) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Dynamic federations is not active on this network. Proposed parameters are not needed.");
}
UniValue prop = request.params[1].get_obj();
std::string sbs_str = prop["signblockscript"].get_str();
if (!IsHex(sbs_str)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "signblockscript must be hex");
}
std::vector<unsigned char> signblock_bytes = ParseHex(sbs_str);
proposed.m_signblockscript = CScript(signblock_bytes.begin(), signblock_bytes.end());
int max_sbs_wit = prop["max_block_witness"].get_int();
if (max_sbs_wit < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "max_block_witness must be non-negative");
}
proposed.m_sbs_wit_limit = max_sbs_wit;
std::string fps_str = prop["fedpegscript"].get_str();
if (!IsHex(fps_str)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "fedpegscript must be hex");
}
std::vector<unsigned char> fedpeg_bytes = ParseHex(fps_str);
proposed.m_fedpegscript = CScript(fedpeg_bytes.begin(), fedpeg_bytes.end());
UniValue extension_array = prop["extension_space"].get_array();
for (unsigned int i = 0; i < extension_array.size(); i++) {
std::string extension_str = extension_array[i].get_str();
proposed.m_extension_space.push_back(ParseHex(extension_str));
}
// All proposals are full serializations
proposed.m_serialize_type = 2;
}
CScript feeDestinationScript = Params().GetConsensus().mandatory_coinbase_destination;
if (feeDestinationScript == CScript()) feeDestinationScript = CScript() << OP_TRUE;
std::unique_ptr<CBlockTemplate> pblocktemplate(BlockAssembler(Params()).CreateNewBlock(feeDestinationScript, required_wait));
std::unique_ptr<CBlockTemplate> pblocktemplate(BlockAssembler(Params()).CreateNewBlock(feeDestinationScript, required_wait, &proposed));
if (!pblocktemplate.get()) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "Wallet keypool empty");
}
@ -1023,6 +1081,13 @@ UniValue getnewblockhex(const JSONRPCRequest& request)
IncrementExtraNonce(&pblocktemplate->block, chainActive.Tip(), nExtraNonce);
}
// If WSH(OP_TRUE) block, fill in witness
CScript op_true(OP_TRUE);
if (pblocktemplate->block.m_dyna_params.m_current.m_signblockscript ==
GetScriptForDestination(WitnessV0ScriptHash(op_true))) {
pblocktemplate->block.m_signblock_witness.stack.push_back(std::vector<unsigned char>(op_true.begin(), op_true.end()));
}
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
ssBlock << pblocktemplate->block;
return HexStr(ssBlock.begin(), ssBlock.end());
@ -1030,7 +1095,7 @@ UniValue getnewblockhex(const JSONRPCRequest& request)
UniValue combineblocksigs(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() != 2)
if (request.fHelp || request.params.size() < 2 || request.params.size() > 3)
throw std::runtime_error(
RPCHelpMan{"combineblocksigs",
"\nMerges signatures on a block proposal\n",
@ -1046,6 +1111,7 @@ UniValue combineblocksigs(const JSONRPCRequest& request)
},
},
},
{"witnessScript", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex-encoded witnessScript for the signblockscript"},
},
RPCResult{
"{\n"
@ -1087,9 +1153,22 @@ UniValue combineblocksigs(const JSONRPCRequest& request)
sig_data.signatures[pubkey.GetID()] = std::make_pair(pubkey, sig_bytes);
}
// Finalizes the signatures, has no access to keys
ProduceSignature(keystore, signature_creator, block.proof.challenge, sig_data, SCRIPT_NO_SIGHASH_BYTE);
block.proof.solution = sig_data.scriptSig;
if (!block.m_dyna_params.IsNull()) {
if (request.params[2].isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Signing dynamic blocks requires the witnessScript argument");
}
std::vector<unsigned char> witness_bytes(ParseHex(request.params[2].get_str()));
if (!witness_bytes.empty()) {
keystore.AddCScript(CScript(witness_bytes.begin(), witness_bytes.end()));
}
// Finalizes the signatures, has no access to keys
ProduceSignature(keystore, signature_creator, block.m_dyna_params.m_current.m_signblockscript, sig_data, SCRIPT_NO_SIGHASH_BYTE);
block.m_signblock_witness = sig_data.scriptWitness;
} else {
// Finalizes the signatures, has no access to keys
ProduceSignature(keystore, signature_creator, block.proof.challenge, sig_data, SCRIPT_NO_SIGHASH_BYTE);
block.proof.solution = sig_data.scriptSig;
}
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
ssBlock << block;
@ -1350,25 +1429,6 @@ UniValue testproposedblock(const JSONRPCRequest& request)
const CChainParams& chainparams = Params();
const bool acceptnonstd = !request.params[1].isNull() ? request.params[1].get_bool() : gArgs.GetBoolArg("-acceptnonstdtxn", !chainparams.RequireStandard());
if (!acceptnonstd) {
// Get PAK commitment, if any
boost::optional<CPAKList> paklist_block = GetPAKKeysFromCommitment(*block.vtx[0]);
// Possible PAK commitment mismatch between blocks and config
if (chainparams.GetEnforcePak() && g_paklist_config) {
if(paklist_block) {
if (*paklist_block != *g_paklist_config) {
throw JSONRPCError(RPC_VERIFY_ERROR, "Proposal PAK commitment and config PAK do not match.");
}
// else it may be an unnecessary commitment but that's ok.
} else {
// Waiting for block that has commitment to config list
if (*g_paklist_config != g_paklist_blockchain) {
throw JSONRPCError(RPC_VERIFY_ERROR, "Proposal does not have required PAK commitment.");
}
}
}
for (auto& transaction : block.vtx) {
if (transaction->IsCoinBase()) continue;
std::string reason;
@ -1395,7 +1455,7 @@ static const CRPCCommand commands[] =
{ "mining", "getblocktemplate", &getblocktemplate, {"template_request"} },
{ "generating", "combineblocksigs", &combineblocksigs, {"blockhex","signatures"} },
{ "mining", "submitheader", &submitheader, {"hexdata"} },
{ "generating", "getnewblockhex", &getnewblockhex, {"min_tx_age"} },
{ "generating", "getnewblockhex", &getnewblockhex, {"min_tx_age", "proposed_parameters"} },
{ "generating", "getcompactsketch", &getcompactsketch, {"block_hex"} },
{ "generating", "consumecompactsketch", &consumecompactsketch, {"sketch"} },
{ "generating", "consumegetblocktxn", &consumegetblocktxn, {"full_block", "block_tx_req"} },

View file

@ -318,12 +318,14 @@ bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams,
pindexNew->proof = diskindex.proof;
pindexNew->nStatus = diskindex.nStatus;
pindexNew->nTx = diskindex.nTx;
pindexNew->d_params = diskindex.d_params;
pindexNew->m_signblock_witness = diskindex.m_signblock_witness;
const uint256 block_hash = pindexNew->GetBlockHash();
// Only validate one of every 1000 block header for sanity check
if (pindexNew->nHeight % 1000 == 0 &&
!CheckProof(pindexNew->GetBlockHeader(), consensusParams) &&
block_hash != consensusParams.hashGenesisBlock) {
block_hash != consensusParams.hashGenesisBlock &&
!CheckProof(pindexNew->GetBlockHeader(), consensusParams)) {
return error("%s: CheckProof: %s, %s", __func__, block_hash.ToString(), pindexNew->ToString());
}
pcursor->Next();

View file

@ -19,6 +19,7 @@
#include <indirectmap.h>
#include <policy/feerate.h>
#include <primitives/transaction.h>
#include <primitives/pak.h>
#include <sync.h>
#include <random.h>
@ -588,7 +589,7 @@ public:
void removeForReorg(const CCoinsViewCache *pcoins, unsigned int nMemPoolHeight, int flags) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
void removeConflicts(const CTransaction &tx) EXCLUSIVE_LOCKS_REQUIRED(cs);
void removeForBlock(const std::vector<CTransactionRef>& vtx, unsigned int nBlockHeight,
bool pak_transition=false);
const CBlockIndex* p_block_index_new = nullptr);
void clear();
void _clear() EXCLUSIVE_LOCKS_REQUIRED(cs); //lock free

View file

@ -18,5 +18,9 @@ const struct VBDeploymentInfo VersionBitsDeploymentInfo[Consensus::MAX_VERSION_B
{
/*.name =*/ "segwit",
/*.gbt_force =*/ true,
}
},
{
/*.name =*/ "dynafed",
/*.gbt_force =*/ true,
},
};

View file

@ -4730,12 +4730,13 @@ UniValue signblock(const JSONRPCRequest& request)
if (!EnsureWalletIsAvailable(pwallet, request.fHelp))
return NullUniValue;
if (request.fHelp || request.params.size() != 1)
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
throw std::runtime_error(
RPCHelpMan{"signblock",
"\nSigns a block proposal, checking that it would be accepted first. Errors if it cannot sign the block.\n",
"\nSigns a block proposal, checking that it would be accepted first. Errors if it cannot sign the block. Note that this call adds the witnessScript to your wallet for signing purposes! This function is intended for QA and testing.\n",
{
{"blockhex", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex-encoded block from getnewblockhex"},
{"witnessScript", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex-encoded witness script. Required for dynamic federation blocks. Argument is \"\" when the block is P2WPKH."},
},
RPCResult{
"[\n"
@ -4781,7 +4782,20 @@ UniValue signblock(const JSONRPCRequest& request)
// Expose SignatureData internals in return value in lieu of "Partially Signed Bitcoin Blocks"
SignatureData block_sigs;
GenericSignScript(*pwallet, block.GetBlockHeader(), block.proof.challenge, block_sigs);
if (block.m_dyna_params.IsNull()) {
GenericSignScript(*pwallet, block.GetBlockHeader(), block.proof.challenge, block_sigs);
} else {
if (request.params[1].isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Signing dynamic blocks requires the witnessScript argument");
}
std::vector<unsigned char> witness_bytes(ParseHex(request.params[1].get_str()));
// Note that we're adding the signblockscript to the wallet so it can actually
// satisfy witness program scriptpubkeys
if (!witness_bytes.empty()) {
pwallet->AddCScript(CScript(witness_bytes.begin(), witness_bytes.end()));
}
GenericSignScript(*pwallet, block.GetBlockHeader(), block.m_dyna_params.m_current.m_signblockscript, block_sigs);
}
// Error if sig data didn't "grow"
if (!block_sigs.complete && block_sigs.signatures.empty()) {
@ -4842,8 +4856,9 @@ UniValue getpeginaddress(const JSONRPCRequest& request)
// Also add raw scripts to index to recognize later.
pwallet->AddCScript(dest_script);
// Get P2CH deposit address on mainchain.
CTxDestination mainchain_dest(ScriptHash(GetScriptForWitness(calculate_contract(Params().GetConsensus().fedpegScript, dest_script))));
// Get P2CH deposit address on mainchain from most recent fedpegscript.
const std::vector<CScript>& fedpegscripts = GetValidFedpegScripts(chainActive.Tip(), Params().GetConsensus(), true /* nextblock_validation */);
CTxDestination mainchain_dest(ScriptHash(GetScriptForWitness(calculate_contract(fedpegscripts.front(), dest_script))));
UniValue ret(UniValue::VOBJ);
@ -5430,7 +5445,7 @@ static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef
T_tx txBTC(*txBTCRef);
std::vector<unsigned char> txOutProofData = ParseHex(request.params[1].get_str());
CDataStream ssTxOutProof(txOutProofData, SER_NETWORK, PROTOCOL_VERSION);
CDataStream ssTxOutProof(txOutProofData, SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS);
try {
ssTxOutProof >> merkleBlock;
}
@ -6696,7 +6711,7 @@ static const CRPCCommand commands[] =
{ "wallet", "dumpblindingkey", &dumpblindingkey, {"address"}},
{ "wallet", "dumpmasterblindingkey", &dumpmasterblindingkey, {}},
{ "wallet", "dumpissuanceblindingkey", &dumpissuanceblindingkey, {"txid", "vin"}},
{ "wallet", "signblock", &signblock, {"blockhex"}},
{ "wallet", "signblock", &signblock, {"blockhex", "witnessScript"}},
{ "wallet", "listissuances", &listissuances, {"asset"}},
{ "wallet", "issueasset", &issueasset, {"assetamount", "tokenamount", "blind"}},
{ "wallet", "reissueasset", &reissueasset, {"asset", "assetamount"}},

View file

@ -322,6 +322,7 @@ def initialize_datadir(dirname, n):
f.write("pubkeyprefix=111\n")
f.write("scriptprefix=196\n")
f.write("bech32_hrp=bcrt\n")
f.write("con_dyna_deploy_start="+str(2**31)+"\n") # Never starts
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
return datadir

View file

@ -14,7 +14,8 @@ from test_framework import (
# Generate wallet import format from private key.
def wif(pk):
# Base58Check version for regtest WIF keys is 0xef = 239
return address.byte_to_base58(pk, 239)
pk_compressed = pk + bytes([0x1])
return address.byte_to_base58(pk_compressed, 239)
# The signblockscript is a Bitcoin Script k-of-n multisig script.
def make_signblockscript(num_nodes, required_signers, keys):
@ -22,11 +23,10 @@ def make_signblockscript(num_nodes, required_signers, keys):
script = "{}".format(50 + required_signers)
for i in range(num_nodes):
k = keys[i]
script += "41"
script += "21"
script += codecs.encode(k.get_pubkey(), 'hex_codec').decode("utf-8")
script += "{}".format(50 + num_nodes) # num keys
script += "ae" # OP_CHECKMULTISIG
print('signblockscript', script)
return script
class BlockSignTest(BitcoinTestFramework):
@ -45,6 +45,10 @@ class BlockSignTest(BitcoinTestFramework):
As well as syncing blocks over p2p
This test covers both pre-dynafed and post.
TODO: Show block max witness actually limits the witness
"""
def skip_test_if_missing_module(self):
@ -58,12 +62,10 @@ class BlockSignTest(BitcoinTestFramework):
k = key.CECKey()
pk_bytes = hashlib.sha256(str(random.getrandbits(256)).encode('utf-8')).digest()
k.set_secretbytes(pk_bytes)
k.set_compressed(True)
w = wif(pk_bytes)
print("generated key {}: \n pub: {}\n wif: {}".format(i+1,
codecs.encode(k.get_pubkey(), 'hex_codec').decode("utf-8"),
w))
self.keys.append(k)
self.wifs.append(wif(pk_bytes))
self.wifs.append(w)
def set_test_params(self):
self.num_nodes = 5
@ -72,10 +74,12 @@ class BlockSignTest(BitcoinTestFramework):
self.setup_clean_chain = True
self.init_keys(self.num_nodes-1) # Last node cannot sign and is connected to all via p2p
signblockscript = make_signblockscript(self.num_keys, self.required_signers, self.keys)
self.witnessScript = signblockscript # post-dynafed this becomes witnessScript
self.extra_args = [[
"-signblockscript={}".format(signblockscript),
"-con_max_block_sig_size={}".format(self.required_signers*74),
"-anyonecanspendaremine=1"
"-anyonecanspendaremine=1",
"-con_dyna_deploy_start=0",
]] * self.num_nodes
def setup_network(self):
@ -97,9 +101,16 @@ class BlockSignTest(BitcoinTestFramework):
miner_next = self.nodes[mineridx_next]
blockcount = miner.getblockcount()
# If dynafed is enabled, this means signblockscript has been WSH-wrapped
blockchain_info = self.nodes[0].getblockchaininfo()
is_dyna = blockchain_info['bip9_softforks']['dynafed']['status'] == "active"
if is_dyna:
wsh_wrap = self.nodes[0].decodescript(self.witnessScript)['segwit']['hex']
assert_equal(wsh_wrap, blockchain_info['current_signblock_hex'])
assert blockchain_info['current_signblock_hex'] != blockchain_info['signblock_hex']
# Make a few transactions to make non-empty blocks for compact transmission
if make_transactions:
print(mineridx)
for i in range(5):
miner.sendtoaddress(miner_next.getnewaddress(), int(miner.getbalance()['bitcoin']/10), "", "", True)
# miner makes a block
@ -121,20 +132,20 @@ class BlockSignTest(BitcoinTestFramework):
self.nodes[i].testproposedblock(final_block)
# non-signing node can not sign
assert_raises_rpc_error(-25, "Could not sign the block.", self.nodes[-1].signblock, block)
assert_raises_rpc_error(-25, "Could not sign the block.", self.nodes[-1].signblock, block, self.witnessScript)
# collect num_keys signatures from signers, reduce to required_signers sigs during combine
sigs = []
for i in range(self.num_keys):
result = miner.combineblocksigs(block, sigs)
sigs = sigs + self.nodes[i].signblock(block)
result = miner.combineblocksigs(block, sigs, self.witnessScript)
sigs = sigs + self.nodes[i].signblock(block, self.witnessScript)
assert_equal(result["complete"], i >= self.required_signers)
# submitting should have no effect pre-threshhold
if i < self.required_signers:
miner.submitblock(result["hex"])
self.check_height(blockcount)
result = miner.combineblocksigs(block, sigs)
result = miner.combineblocksigs(block, sigs, self.witnessScript)
assert_equal(result["complete"], True)
# All signing nodes must submit... we're not connected!
@ -160,11 +171,11 @@ class BlockSignTest(BitcoinTestFramework):
self.check_height(0)
# mine a block with no transactions
print("Mining and signing 101 blocks to unlock funds")
self.log.info("Mining and signing 101 blocks to unlock funds")
self.mine_blocks(101, False)
# mine blocks with transactions
print("Mining and signing non-empty blocks")
self.log.info("Mining and signing non-empty blocks")
self.mine_blocks(10, True)
# Height check also makes sure non-signing, p2p connected node gets block
@ -185,5 +196,18 @@ class BlockSignTest(BitcoinTestFramework):
assert_equal(info['signblock_asm'], self.nodes[0].decodescript(signblockscript)['asm'])
assert_equal(info['signblock_hex'], signblockscript)
assert_equal(info['bip9_softforks']['dynafed']['status'], "defined")
# Next let's activate dynafed
blocks_til_dynafed = 431 - self.nodes[0].getblockcount()
self.mine_blocks(blocks_til_dynafed, False)
self.check_height(111+blocks_til_dynafed)
assert_equal(self.nodes[0].getblockchaininfo()['bip9_softforks']['dynafed']['status'], "active")
self.log.info("Mine some dynamic federation blocks without and with txns")
self.mine_blocks(50, False)
self.mine_blocks(50, True)
if __name__ == '__main__':
BlockSignTest().main()

View file

@ -0,0 +1,424 @@
#!/usr/bin/env python3
# Copyright (c) 2019 The Elements Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test dynamic federations state machine logic
NOTE: This test is not testing the behavior not related to transitions themselves.
That is for other tests such as feature_pak, feature_fedpeg, feature_blocksign
1) Test "legacy" params are still in play before versionbits activation
2) Test transition to dynafed preserves expected chainparams
3) Test a full epoch with no votes
4) Test full epoch with just under 4/5 votes, with competing random proposals
5) Test full epoch with just at 4/5 votes, with competing random proposals
6) Test full epoch with 5/5 votes
7) Test that peg-outs(PAK) and peg-ins are ejected from mempool block before transition
and rejected when re-submitted if there is a parameter mis-match
8) Test that reorging a transition results in transitions being undone,
previously ejected transactions are allowed back into the mempool when appropriate
"""
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_raises_rpc_error, assert_equal, sync_blocks
# Hardcoded PAK to make sure PAK is enforced even when dynafed is not
initial_pubkey = "02fcba7ecf41bc7e1be4ee122d9d22e3333671eb0a3a87b5cdf099d59874e1940f"
initial_extension = [initial_pubkey+initial_pubkey]
def go_to_epoch_end(node):
epoch_info = node.getblockchaininfo()
blocks_to_mine = epoch_info["epoch_length"] - epoch_info["epoch_age"] - 1
node.generatetoaddress(blocks_to_mine, node.getnewaddress())
def validate_no_vote_op_true(node, block):
block_info = node.getblock(block)
dynamic_parameters = block_info["dynamic_parameters"]
block_height = block_info["height"]
assert "current" in dynamic_parameters
assert "proposed" in dynamic_parameters
# signblockscript is now the P2WSH-ification of OP_TRUE
WSH_OP_TRUE = node.decodescript("51")["segwit"]["hex"]
assert_equal(dynamic_parameters["current"]["signblockscript"], WSH_OP_TRUE)
if block_height % 10 == 0:
assert_equal(dynamic_parameters["current"]["fedpegscript"], "51")
assert_equal(dynamic_parameters["current"]["extension_space"], initial_extension)
else:
assert_equal(dynamic_parameters["current"]["fedpegscript"], "")
assert_equal(dynamic_parameters["current"]["extension_space"], [])
# TODO workshop this bump, or commit to new value in chainparams instead
assert_equal(dynamic_parameters["current"]["max_block_witness"], 75)
# nothing was proposed, null fields make impossible to be valid blockheader
# due to script rules requiring bool true on stack
assert_equal(dynamic_parameters["proposed"]["signblockscript"], "")
assert_equal(dynamic_parameters["proposed"]["fedpegscript"], "")
assert_equal(dynamic_parameters["proposed"]["max_block_witness"], 0)
assert_equal(dynamic_parameters["proposed"]["extension_space"], [])
class DynaFedTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 2
# We want to test activation of dynafed
self.extra_args = [["-con_dyna_deploy_start=0", "-enforce_pak=1", "-con_parent_chain_signblockscript=51", "-peginconfirmationdepth=1", "-parentscriptprefix=75"] for i in range(self.num_nodes)]
# second node will not mine transactions
self.extra_args[1].append("-blocksonly=1")
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def test_legacy_params(self):
self.log.info("Testing legacy parameters...")
for i in range(self.num_nodes):
assert_equal(self.nodes[i].getblockcount(), 0)
# Check deployment exists and is not active
dyna_activate = self.nodes[i].getblockchaininfo()["bip9_softforks"]["dynafed"]
assert_equal(dyna_activate["status"], "defined")
# fedpegscript is OP_TRUE
legacy_sc_info = self.nodes[i].getsidechaininfo()
assert_equal(legacy_sc_info["fedpegscript"], "51")
# No history yet, only one "live" fedpegscript
assert_equal(legacy_sc_info["current_fedpegscripts"], ["51"])
# blocksigner is OP_TRUE, extension space is hardcoded one in chainparams
signblock_info = self.nodes[i].getblockchaininfo()
assert_equal(signblock_info["signblock_hex"], "51")
assert_equal(signblock_info["current_signblock_hex"], "51")
assert_equal(signblock_info["max_block_witness"], 74)
assert_equal(signblock_info["extension_space"], initial_extension)
pak_info = self.nodes[i].getpakinfo()
assert_equal(pak_info["block_paklist"]["reject"], False)
assert_equal(pak_info["block_paklist"]["online"], [initial_pubkey])
assert_equal(pak_info["block_paklist"]["offline"], [initial_pubkey])
# can not put proposed params into blockheader pre-dynafed
assert_raises_rpc_error(-8, "Dynamic federations is not active on this network. Proposed parameters are not needed.", self.nodes[i].getnewblockhex, 0, {})
# TODO Reject serialized dynamic federations blocks before activation
def test_dynafed_activation(self):
self.log.info("Testing dynafed versionbits activation...")
# Move chain forward to activation, any new blocks will be enforced
blocks = self.nodes[0].generatetoaddress(431, self.nodes[0].getnewaddress())
self.sync_all()
assert_equal(self.nodes[0].getblockchaininfo()["bip9_softforks"]["dynafed"]["status"], "active")
# Existing blocks should have null dynafed fields
for block in blocks:
assert "dynamic_parameters" not in self.nodes[0].getblock(block)
# Next block is first dynamic federation block
block = self.nodes[0].generatetoaddress(1, self.nodes[0].getnewaddress())[0]
self.sync_all()
for i in range(self.num_nodes):
validate_no_vote_op_true(self.nodes[i], block)
def test_no_vote(self):
self.log.info("Testing no-vote epoch...")
go_to_epoch_end(self.nodes[0])
# Mine epoch_length blocks with no proposals
blocks = self.nodes[0].generatetoaddress(10, self.nodes[0].getnewaddress())
self.sync_all()
for i in range(self.num_nodes):
for block in blocks:
validate_no_vote_op_true(self.nodes[i], block)
# Now transition using vanilla getnewblockhex, nothing changed
block = self.nodes[0].generatetoaddress(1, self.nodes[0].getnewaddress())[0]
self.sync_all()
for i in range(self.num_nodes):
validate_no_vote_op_true(self.nodes[i], block)
def test_under_vote(self):
self.log.info("Testing failed voting epoch...")
go_to_epoch_end(self.nodes[0])
# Mine 7 blocks with agreeing proposals for single-sig, falls short of 4/5 of 10
new_signblock = self.nodes[0].getaddressinfo(self.nodes[0].getnewaddress("", "bech32"))["scriptPubKey"]
cur_height = self.nodes[0].getblockcount()
for _ in range(7):
prop_block = self.nodes[0].getnewblockhex(0, {"signblockscript":new_signblock, "max_block_witness":100, "fedpegscript":"52", "extension_space":["01", "02"]})
self.nodes[0].submitblock(prop_block)
self.sync_all()
assert_equal(self.nodes[0].getblockcount(), cur_height+7)
# Now mine 3 blank blocks
self.nodes[0].generatetoaddress(3, self.nodes[0].getnewaddress())
# No transition will take place, generatetoaddress still works for new epoch
block = self.nodes[0].generatetoaddress(1, self.nodes[0].getnewaddress())[0]
self.sync_all()
for i in range(self.num_nodes):
validate_no_vote_op_true(self.nodes[i], block)
def test_four_fifth_vote(self):
self.log.info("Testing just-successful transition epoch...")
go_to_epoch_end(self.nodes[0])
# Mine 8 blocks with agreeing proposals for single-sig, triggering transition
new_signblock = self.nodes[0].getaddressinfo(self.nodes[0].getnewaddress("", "bech32"))["scriptPubKey"]
cur_height = self.nodes[0].getblockcount()
WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"]
for _ in range(8):
# Check that things don't change until the 10th block is submitted
for i in range(self.num_nodes):
chain_info = self.nodes[i].getblockchaininfo()
fedpeg_info = self.nodes[i].getsidechaininfo()
assert_equal(chain_info["current_signblock_hex"], WSH_OP_TRUE)
assert_equal(chain_info["max_block_witness"], 75)
assert_equal(chain_info["extension_space"], initial_extension)
assert_equal(fedpeg_info["current_fedpegscripts"], ["51", "51"])
prop_block = self.nodes[0].getnewblockhex(0, {"signblockscript":new_signblock, "max_block_witness":107, "fedpegscript":"52", "extension_space":["01", "02"]})
self.nodes[0].submitblock(prop_block)
self.sync_all()
assert_equal(self.nodes[0].getblockcount(), cur_height+8)
# Now mine 1 blank block
self.nodes[0].generatetoaddress(1, self.nodes[0].getnewaddress())
self.sync_all()
# Old parameters still enforced for next block...
for i in range(self.num_nodes):
chain_info = self.nodes[i].getblockchaininfo()
fedpeg_info = self.nodes[i].getsidechaininfo()
assert_equal(chain_info["current_signblock_hex"], WSH_OP_TRUE)
assert_equal(chain_info["max_block_witness"], 75)
assert_equal(chain_info["extension_space"], initial_extension)
assert_equal(fedpeg_info["current_fedpegscripts"], ["51", "51"])
# Last blank block
self.nodes[0].generatetoaddress(1, self.nodes[0].getnewaddress())
self.sync_all()
# We have now transitioned, next block must have signature
unsigned_block = self.nodes[0].getnewblockhex()
assert_equal(self.nodes[0].submitblock(unsigned_block), "block-proof-invalid")
assert_equal(self.nodes[0].getblockcount(), cur_height+10)
# New params now enforced
for i in range(self.num_nodes):
chain_info = self.nodes[i].getblockchaininfo()
fedpeg_info = self.nodes[i].getsidechaininfo()
assert_equal(chain_info["current_signblock_hex"], new_signblock)
assert_equal(chain_info["max_block_witness"], 107) # 72+33+2
assert_equal(chain_info["extension_space"], ["01", "02"])
assert_equal(fedpeg_info["current_fedpegscripts"], ["52", "51"])
def test_all_vote(self):
self.log.info("Testing unanimous transition epoch...")
# We have now transitioned to single-sig blocks from node 0
# Let's transition node 1's key with all votes to it
cur_height = self.nodes[0].getblockcount()
new_signblock = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress("", "bech32"))["scriptPubKey"]
for _ in range(10):
# Check that things don't change until the 10th block is submitted
for i in range(self.num_nodes):
chain_info = self.nodes[i].getblockchaininfo()
fedpeg_info = self.nodes[i].getsidechaininfo()
assert chain_info["current_signblock_hex"] != new_signblock
assert_equal(chain_info["max_block_witness"], 107)
assert_equal(chain_info["extension_space"], ["01", "02"])
assert_equal(fedpeg_info["current_fedpegscripts"], ["52", "51"])
block = self.nodes[1].getnewblockhex(0, {"signblockscript":new_signblock, "max_block_witness":108, "fedpegscript":"53", "extension_space":["01", "03"]})
sig = self.nodes[0].signblock(block, "")
assert_raises_rpc_error(-25, "Could not sign the block.", self.nodes[1].signblock, block, "")
comb_result = self.nodes[0].combineblocksigs(block, sig, "")
assert comb_result["complete"]
self.nodes[1].submitblock(comb_result["hex"])
self.sync_all()
self.sync_all()
assert_equal(self.nodes[0].getblockcount(), cur_height+10)
chain_info = self.nodes[0].getblockchaininfo()
fedpeg_info = self.nodes[0].getsidechaininfo()
assert_equal(chain_info["current_signblock_hex"], new_signblock)
assert_equal(chain_info["max_block_witness"], 108)
assert_equal(chain_info["extension_space"], ["01", "03"])
assert_equal(fedpeg_info["current_fedpegscripts"], ["53", "52"])
# Note: Extension spaces with entries that are not 2 concat hex pubkeys
# are treated as PAK reject mode
assert self.nodes[0].getpakinfo()["block_paklist"]["reject"]
# Now node 1 is the signer
block = self.nodes[0].getnewblockhex()
sig = self.nodes[1].signblock(block, "")
assert_raises_rpc_error(-25, "Could not sign the block.", self.nodes[0].signblock, block, "")
comb_result = self.nodes[1].combineblocksigs(block, sig, "")
assert comb_result["complete"]
self.nodes[0].submitblock(comb_result["hex"])
assert_equal(self.nodes[0].getblockcount(), cur_height+11)
def test_transition_mempool_eject(self):
self.log.info("Testing mempool (r)ejection policy on transitions...")
# node 1 is still signer, let's transition to something we can PAK peg-out to
# and OP_TRUE fedpegscript, and set signblockscript back to OP_TRUE
WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"]
xpub = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
init_details = self.nodes[0].initpegoutwallet(xpub)
pak_entry = init_details["pakentry"]
# stitch the extension space together using the relevant keys
extension_space = [pak_entry[4:4+66]+pak_entry[4+66+1:]]
pak_prop = {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":extension_space}
epoch_info = self.nodes[0].getblockchaininfo()
blocks_to_end = epoch_info["epoch_length"] - epoch_info["epoch_age"] - 1
for _ in range(blocks_to_end):
block = self.nodes[1].getnewblockhex(0, pak_prop)
sig = self.nodes[1].signblock(block, "")
comb_result = self.nodes[1].combineblocksigs(block, sig, "")
assert comb_result["complete"]
self.nodes[1].submitblock(comb_result["hex"])
self.sync_all()
assert_equal(self.nodes[1].getblockchaininfo()["current_signblock_hex"], WSH_OP_TRUE)
assert_equal(self.nodes[1].getsidechaininfo()["current_fedpegscripts"], ["51", "53"])
# Transactions
# Peg-in prep:
# hack: since we're not validating peg-ins in parent chain, just make
# both the funding and claim tx on same chain (printing money)
fund_info = self.nodes[0].getpeginaddress()
peg_id = self.nodes[0].sendtoaddress(fund_info["mainchain_address"], 1)
peg_tx = self.nodes[0].gettransaction(peg_id)["hex"]
self.nodes[0].testmempoolaccept([peg_tx])
# only one confirm needed in this setup, we do 10 to sync with epoch_length
self.nodes[0].generatetoaddress(10, self.nodes[0].getnewaddress())
proof = self.nodes[0].gettxoutproof([peg_id])
raw_tx = self.nodes[0].gettransaction(peg_id)["hex"]
# Now, peg-in and PAK peg-out in node 0 mempool
# We need this transaction to get into the mempool, then transition
# to new fedpegscript, then wait another epoch, to get dumped.
claim_id = self.nodes[0].claimpegin(raw_tx, proof, fund_info["claim_script"])
# saving for re-submission later
raw_claim = self.nodes[0].gettransaction(claim_id)["hex"]
# This transaction will be dumped as soon as transition activates
pegout_id = self.nodes[0].sendtomainchain("", 1)["txid"]
# Chain payment, this should get "recursively" kicked on transition
pegout_child_id = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), self.nodes[0].getbalance()['bitcoin'], "", "", True)
raw_pegout = self.nodes[0].gettransaction(pegout_id)["hex"]
raw_pool = self.nodes[0].getrawmempool()
assert claim_id in raw_pool
assert pegout_id in raw_pool
assert pegout_child_id in raw_pool
# node 1 is blocksonly, no mempool so it won't mine node 0's transactions
assert_equal(self.nodes[1].getrawmempool(), [])
# Now generate an epoch of blocks on node 1 to show that non-transitions don't dump
# PAK or peg-in transactions from mempool
self.nodes[1].generatetoaddress(10, self.nodes[1].getnewaddress())
sync_blocks(self.nodes)
assert_equal(self.nodes[0].getblockchaininfo()["epoch_age"], 9)
# Transactions are still in mempool
raw_pool = self.nodes[0].getrawmempool()
assert claim_id in raw_pool
assert pegout_id in raw_pool
assert pegout_child_id in raw_pool
# Now have node 1 transition to exact same pak and fedpegscript
for _ in range(10):
block = self.nodes[1].getnewblockhex(0, pak_prop)
assert_equal(self.nodes[1].submitblock(block), None)
sync_blocks(self.nodes)
assert_equal(self.nodes[0].getblockchaininfo()["epoch_age"], 9)
# After the 10th block, nothing gets the boot
raw_pool = self.nodes[0].getrawmempool()
assert claim_id in raw_pool
assert pegout_id in raw_pool
assert pegout_child_id in raw_pool
# Now have node 1 transition to new pak and fedpegscript
pak_prop["fedpegscript"] = "52"
pak_prop["extension_space"] = ["deadbeef"]
for _ in range(10):
raw_pool = self.nodes[0].getrawmempool()
assert claim_id in raw_pool
assert pegout_id in raw_pool
assert pegout_child_id in raw_pool
block = self.nodes[1].getnewblockhex(0, pak_prop)
assert_equal(self.nodes[1].submitblock(block), None)
sync_blocks(self.nodes)
assert_equal(self.nodes[0].getblockchaininfo()["epoch_age"], 9)
# After 10 blocks, PAK and child is booted, peg-in still lingers for 1 more epoch
raw_pool = self.nodes[0].getrawmempool()
assert claim_id in raw_pool
assert pegout_id not in raw_pool
assert pegout_child_id not in raw_pool
# Re-submission fails
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[0].sendrawtransaction, raw_pegout)
for _ in range(10):
assert claim_id in self.nodes[0].getrawmempool()
self.nodes[1].submitblock(self.nodes[1].getnewblockhex())
sync_blocks(self.nodes)
# After 10 blocks(no proposal), peg-in is finally dumped
assert claim_id not in self.nodes[0].getrawmempool()
# Both claim and peg-out rejected from submission as well
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[0].sendrawtransaction, raw_pegout)
assert_raises_rpc_error(-26, "bad-pegin-witness, Peg-in tx is invalid.", self.nodes[0].sendrawtransaction, raw_claim)
# Now we test reorg behavior
best_blockhash = self.nodes[0].getbestblockhash()
# Invalidate tip, peg-in should be allowed back into mempool but not pegout
self.nodes[0].invalidateblock(best_blockhash)
self.nodes[0].sendrawtransaction(raw_claim)
assert claim_id in self.nodes[0].getrawmempool()
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[0].sendrawtransaction, raw_pegout)
# Reconsider best block, should be booted and invalid again
self.nodes[0].reconsiderblock(best_blockhash)
assert claim_id not in self.nodes[0].getrawmempool()
# Go back 20 blocks to let peg-out back in
old_blockhash = self.nodes[0].getblockhash(self.nodes[0].getblockcount()-20)
self.nodes[0].invalidateblock(old_blockhash)
self.nodes[0].sendrawtransaction(raw_claim)
self.nodes[0].sendrawtransaction(raw_pegout)
assert claim_id in self.nodes[0].getrawmempool()
assert pegout_id in self.nodes[0].getrawmempool()
# Again go back to tip, both booted and not let back in
self.nodes[0].reconsiderblock(best_blockhash)
assert claim_id not in self.nodes[0].getrawmempool()
assert pegout_id not in self.nodes[0].getrawmempool()
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[0].sendrawtransaction, raw_pegout)
assert_raises_rpc_error(-26, "bad-pegin-witness, Peg-in tx is invalid.", self.nodes[0].sendrawtransaction, raw_claim)
def run_test(self):
self.test_legacy_params()
self.test_dynafed_activation()
self.test_no_vote()
self.test_under_vote()
self.test_four_fifth_vote()
self.test_all_vote()
self.test_transition_mempool_eject()
if __name__ == '__main__':
DynaFedTest().main()

View file

@ -3,206 +3,33 @@
# Distributed under the MIT/X11 software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_raises_rpc_error, connect_nodes_bi, sync_blocks, Decimal
import copy
import time
from test_framework.util import assert_equal, assert_raises_rpc_error, connect_nodes_bi, sync_blocks, Decimal, assert_greater_than, sync_mempools
def pak_to_option(pak):
return list(map(lambda x: "-pak=%s:%s" % (x[0], x[1]), pak))
'''
This test focuses on enforcement of PAK, not on transitioning lists
or RPC return values for PAK enforcement settings which is covered
in feature_dynafed.py
# This tests a PAK list transition from the genesis state ('reject') to pak1 to
# 'reject' and finally to pak2. There are 5 nodes each with different
# configurations
# All nodes validate pegouts but the first one
# The node at index 0 doesn't validate pegouts, just normal standardness
i_novalidate = 0
# The node at index 1 has no paklist in config
i_undefined = 1
# Paklist 1 in config
i_pak1 = 2
# Paklist 2 in config
i_pak2 = 3
# Reject in config
i_reject = 4
# The two conflicting pak lists
pak1 = [("02fcba7ecf41bc7e1be4ee122d9d22e3333671eb0a3a87b5cdf099d59874e1940f", "02a28b3078b6fe9d2b0f098ffb491b8e98a7fe56ebe321ba52f90becdd06507bbf"),
("02101bed11081c19b25e02dd618da53af1ba29849bbe4006fb3d6e2d3b0d874405", "02c9cf4bdef23d38e6c9ae73b83001711debea113573cfbe0fb729ff81638549da")]
pak2 = [("03767a74373b7207c5ae1214295197a88ec2abdf92e9e2a29daf024c322fae9fcb", "033e4740d0ba639e28963f3476157b7cf2fb7c6fdf4254f97099cf8670b505ea59"),
("02f4a7445f9c48ee8590a930d3fc4f0f5763e3d1d003fdf5fc822e7ba18f380632", "036b3786f029751ada9f02f519a86c7e02fb2963a7013e7e668eb5f7ec069b9e7e")]
# Args that will be re-used in slightly different ways across runs
args = [["-acceptnonstdtxn=0", "-initialfreecoins=100000000", "-parent_bech32_hrp=lol", "-pubkeyprefix=112", "-scriptprefix=197"]] \
+ [["-acceptnonstdtxn=0", "-enforce_pak=1", "-initialfreecoins=100000000", "-parent_bech32_hrp=lol", "-pubkeyprefix=112", "-scriptprefix=197"]]*4
args[i_reject] = args[i_reject] + ['-pak=reject']
# Novalidate has pak entry, should not act on it ever
args[i_novalidate] = args[i_novalidate] + pak_to_option(pak1)
TODO: Test non-activated dynafed means no block enforcement of PAK
'''
class PAKTest (BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 5
self.num_nodes = 3
self.setup_clean_chain = True
self.extra_args = copy.deepcopy(args)
self.extra_args[i_pak1] = self.extra_args[i_pak1] + pak_to_option(pak1)
self.extra_args[i_pak2] = self.extra_args[i_pak2] + pak_to_option(pak2)
self.extra_args = [["-enforce_pak=1", "-con_dyna_deploy_start=-1", "-initialfreecoins=210000000000000", "-anyonecanspendaremine=1", "-parent_bech32_hrp=lol", "-pubkeyprefix=112", "-scriptprefix=197", "-con_connect_genesis_outputs=1"] for i in range(self.num_nodes)]
# First node doesn't enforce PAK, a "HF" of the other two nodes
self.extra_args[0] = self.extra_args[0][1:]
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def run_test(self):
for node in self.nodes:
addr = node.getnewaddress()
unconf = node.validateaddress(addr)["unconfidential"]
privkey = node.dumpprivkey(addr)
node.set_deterministic_priv_key(unconf, privkey)
# Give novalidate 50 BTC
self.nodes[i_novalidate].generate(101)
self.sync_all()
# Transitioning PAK lists and checking lists in RPC tested in feature_dynafed
# This function tests the result of the getpakinfo RPC.
# *_pak is either False (undefined paklist), "reject" or a list of
# (online, offline) tuples
def test_pak(node, config_pak, block_pak):
getpakinfo = node.getpakinfo()
def compare(actual, expected):
if expected is False:
assert_equal(actual, {})
elif "reject" in expected:
assert_equal(actual['offline'], [])
assert_equal(actual['online'], [])
assert_equal(actual['reject'], True)
else:
offline = list(map(lambda x: x[0], expected))
online = list(map(lambda x: x[1], expected))
assert_equal(actual['offline'], offline)
assert_equal(actual['online'], online)
assert_equal(actual['reject'], False)
compare(getpakinfo['config_paklist'], config_pak)
compare(getpakinfo['block_paklist'], block_pak)
# In the beginning the blockchain paklist is "reject"
test_pak(self.nodes[i_novalidate], pak1, "reject")
test_pak(self.nodes[i_undefined], False, "reject")
test_pak(self.nodes[i_pak1], pak1, "reject")
test_pak(self.nodes[i_pak2], pak2, "reject")
test_pak(self.nodes[i_reject], "reject", "reject")
# i_novalidate creates block without a commitment
block_proposal = self.nodes[i_novalidate].getnewblockhex()
assert_equal(self.nodes[i_novalidate].testproposedblock(block_proposal), None)
assert_equal(self.nodes[i_undefined].testproposedblock(block_proposal), None)
assert_raises_rpc_error(-25, "Proposal does not have required PAK commitment.", self.nodes[i_pak1].testproposedblock, block_proposal)
# i_undefined creates a block without a commitment
block_proposal = self.nodes[i_undefined].getnewblockhex()
assert_equal(self.nodes[i_novalidate].testproposedblock(block_proposal), None)
assert_equal(self.nodes[i_undefined].testproposedblock(block_proposal), None)
assert_raises_rpc_error(-25, "Proposal does not have required PAK commitment.", self.nodes[i_pak1].testproposedblock, block_proposal)
# PAK transition: reject -> pak1
# Create a new block with node i_pak1. Because it contains a commitment
# to pak1 it should be rejected by i_pak2 and i_reject.
block_proposal = self.nodes[i_pak1].getnewblockhex()
assert_equal(self.nodes[i_novalidate].testproposedblock(block_proposal), None)
assert_equal(self.nodes[i_undefined].testproposedblock(block_proposal), None)
assert_equal(self.nodes[i_pak1].testproposedblock(block_proposal), None)
assert_raises_rpc_error(-25, "Proposal PAK commitment and config PAK do not match.", self.nodes[i_pak2].testproposedblock, block_proposal)
assert_raises_rpc_error(-25, "Proposal PAK commitment and config PAK do not match.", self.nodes[i_reject].testproposedblock, block_proposal)
# Submit block with commitment to pak1 and check each node's state.
self.nodes[i_undefined].submitblock(block_proposal)
self.sync_all()
test_pak(self.nodes[i_novalidate], pak1, pak1)
test_pak(self.nodes[i_undefined], False, pak1)
test_pak(self.nodes[i_pak1], pak1, pak1)
test_pak(self.nodes[i_pak2], pak2, pak1)
test_pak(self.nodes[i_reject], "reject", pak1)
# Check that another block by i_pak1 (without a commitment) is valid to
# i_pak1 but invalid to i_pak2 and i_reject
block_proposal = self.nodes[i_undefined].getnewblockhex()
assert_equal(self.nodes[i_novalidate].testproposedblock(block_proposal), None)
assert_equal(self.nodes[i_undefined].testproposedblock(block_proposal), None)
assert_equal(self.nodes[i_pak1].testproposedblock(block_proposal), None)
assert_raises_rpc_error(-25, "Proposal does not have required PAK commitment.", self.nodes[i_pak2].testproposedblock, block_proposal)
assert_raises_rpc_error(-25, "Proposal does not have required PAK commitment.", self.nodes[i_reject].testproposedblock, block_proposal)
# PAK transition: pak1 -> reject
# Create a new block with i_reject which should have a "reject" commitment
# and check that it's correctly rejected or accepted.
block_proposal = self.nodes[i_reject].getnewblockhex()
assert_equal(self.nodes[i_novalidate].testproposedblock(block_proposal), None)
assert_equal(self.nodes[i_undefined].testproposedblock(block_proposal), None)
assert_raises_rpc_error(-25, "Proposal PAK commitment and config PAK do not match.", self.nodes[i_pak1].testproposedblock, block_proposal)
assert_equal(self.nodes[i_reject].testproposedblock(block_proposal), None)
# Submit "reject" block and check state.
self.nodes[i_undefined].submitblock(block_proposal)
self.sync_all()
test_pak(self.nodes[i_novalidate], pak1, "reject")
test_pak(self.nodes[i_undefined], False, "reject")
test_pak(self.nodes[i_pak1], pak1, "reject")
test_pak(self.nodes[i_pak2], pak2, "reject")
test_pak(self.nodes[i_reject], "reject", "reject")
# Check that another block by i_reject (without a commitment) is valid to i_reject.
block_proposal = self.nodes[i_reject].getnewblockhex()
assert_equal(self.nodes[i_reject].testproposedblock(block_proposal), None)
# Check that i_undefined can't peg-out because of the pegout freeze.
assert_raises_rpc_error(-5, "Pegout freeze is under effect", self.nodes[i_undefined].sendtomainchain, "", 1)
assert_raises_rpc_error(-3, "`address` argument must be \"\" for PAK-enabled networks as the address is generated automatically.", self.nodes[i_undefined].sendtomainchain, "n3NkSZqoPMCQN5FENxUBw4qVATbytH6FDK", 1)
# PAK transition: reject -> pak2
# Restart nodes while putting pak2 in i_pak1's config instead of pak1.
self.stop_nodes()
extra_args = copy.deepcopy(args)
extra_args[i_pak1] = extra_args[i_pak1] + pak_to_option(pak2)
extra_args[i_pak2] = extra_args[i_pak2] + pak_to_option(pak2)
# Also test novalidate behaves correctly when set to reject after removing
# the two pak entries
extra_args[i_novalidate] = extra_args[i_novalidate][:-2] + ['-pak=reject']
# Restart and connect peers
self.start_nodes(extra_args)
connect_nodes_bi(self.nodes,0,1)
connect_nodes_bi(self.nodes,1,2)
connect_nodes_bi(self.nodes,2,3)
connect_nodes_bi(self.nodes,3,4)
# Check current state of i_pak1
test_pak(self.nodes[i_pak1], pak2, "reject")
# Create a new block with i_pak1 which should have a commitment to pak2
# and check that it's correctly rejected or accepted.
block_proposal = self.nodes[i_pak1].getnewblockhex()
assert_equal(self.nodes[i_novalidate].testproposedblock(block_proposal), None)
assert_equal(self.nodes[i_undefined].testproposedblock(block_proposal), None)
assert_equal(self.nodes[i_pak1].testproposedblock(block_proposal), None)
assert_equal(self.nodes[i_pak2].testproposedblock(block_proposal), None)
assert_raises_rpc_error(-25, "Proposal PAK commitment and config PAK do not match.", self.nodes[i_reject].testproposedblock, block_proposal)
# Submit block with commitment to pak2 and check state.
self.nodes[i_pak1].submitblock(block_proposal)
self.sync_all()
test_pak(self.nodes[i_novalidate], "reject", pak2)
test_pak(self.nodes[i_undefined], False, pak2)
test_pak(self.nodes[i_pak1], pak2, pak2)
test_pak(self.nodes[i_pak2], pak2, pak2)
test_pak(self.nodes[i_reject], "reject", pak2)
# Reset PAK conf arguments to start to test mempool acceptance and wallet
self.log.info("Test wallet PAK")
# We will re-use the same xpub, but each wallet will create its own online pak
# so the lists will be incompatible, even if all else was synced
@ -210,7 +37,7 @@ class PAKTest (BitcoinTestFramework):
xpub_desc = "pkh("+xpub+"/0/*)" # Transform this into a descriptor
init_results = []
info_results = []
for i in range(5):
for i in range(self.num_nodes):
if i == 0:
assert_raises_rpc_error(-8, "PAK enforcement is not enabled on this network.", self.nodes[i].initpegoutwallet, xpub)
init_results += [None]
@ -232,127 +59,133 @@ class PAKTest (BitcoinTestFramework):
# Use custom derivation counter values, check if stored correctly,
# address lookahead looks correct and that new liquid_pak was chosen
assert_raises_rpc_error(-8, "bip32_counter must be between 0 and 1,000,000,000, inclusive.", self.nodes[i_undefined].initpegoutwallet, xpub, -1)
assert_raises_rpc_error(-8, "bip32_counter must be between 0 and 1,000,000,000, inclusive.", self.nodes[1].initpegoutwallet, xpub, -1)
assert_raises_rpc_error(-8, "bip32_counter must be between 0 and 1,000,000,000, inclusive.", self.nodes[1].initpegoutwallet, xpub, 1000000001)
assert_raises_rpc_error(-8, "bip32_counter must be between 0 and 1,000,000,000, inclusive.", self.nodes[i_undefined].initpegoutwallet, xpub, 1000000001)
new_init = self.nodes[1].initpegoutwallet(xpub, 2)
assert_equal(self.nodes[1].getwalletpakinfo()["bip32_counter"], "2")
assert_equal(new_init["address_lookahead"][0], init_results[1]["address_lookahead"][2])
assert(new_init["liquid_pak"] != init_results[1]["liquid_pak"])
new_init = self.nodes[i_undefined].initpegoutwallet(xpub, 2)
assert_equal(self.nodes[i_undefined].getwalletpakinfo()["bip32_counter"], "2")
assert_equal(new_init["address_lookahead"][0], init_results[i_undefined]["address_lookahead"][2])
assert(new_init["liquid_pak"] != init_results[i_undefined]["liquid_pak"])
# Load additional pak entry for each, restart (reject node disallows pak list in conf)
# By adding different pak entries, all nodes that validate the list should conflict
# Restart and connect peers to check wallet persistence
self.stop_nodes()
extra_args = copy.deepcopy(args)
extra_args[i_pak1] = extra_args[i_pak1]+["-"+init_results[i_pak1]["pakentry"]]
extra_args[i_pak2] = extra_args[i_pak2]+["-"+init_results[i_pak2]["pakentry"]]
# Restart and connect peers
self.start_nodes(extra_args)
self.start_nodes()
connect_nodes_bi(self.nodes,0,1)
connect_nodes_bi(self.nodes,1,2)
connect_nodes_bi(self.nodes,2,3)
connect_nodes_bi(self.nodes,3,4)
# Check PAK settings persistence in wallet across restart
restarted_info = self.nodes[i_undefined].getwalletpakinfo()
restarted_info = self.nodes[1].getwalletpakinfo()
assert_equal(restarted_info["bitcoin_descriptor"], xpub_desc)
assert_equal(restarted_info["liquid_pak"], new_init["liquid_pak"])
assert_equal(restarted_info["bip32_counter"], "2")
# Have nodes send pegouts, check it fails to enter mempool of other nodes with incompatible
# PAK settings
self.nodes[i_novalidate].sendmany("", {self.nodes[i_undefined].getnewaddress():10, self.nodes[i_pak1].getnewaddress():10, self.nodes[i_pak2].getnewaddress():10, self.nodes[i_reject].getnewaddress():10})
self.nodes[i_novalidate].generate(1)
# Compile list of extension space entries for pak enforcement
extension_space_proposal = []
for entry in init_results:
if entry is not None:
pakentry = entry["pakentry"]
extension_space_proposal += [pakentry[4:4+66]+pakentry[4+66+1:]]
self.log.info("Test mempool enforcement of PAK peg-outs")
# Transition to a pak list that only node 1 can peg-out to
WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"]
for _ in range(9):
block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":[extension_space_proposal[0]]})
assert_equal(self.nodes[1].submitblock(block), None)
self.sync_all()
assert_equal(self.nodes[0].getblockchaininfo()["extension_space"], [extension_space_proposal[0]])
# pak1 generates a block, creating block commitment
self.nodes[i_pak1].generate(1)
self.sync_all()
# node 1 has wrong pak entry in wallet
assert_raises_rpc_error(-4, "Given online key is not in Pegout Authorization Key List", self.nodes[1].sendtomainchain, "", 1)
# pak1 will now create a pegout.
pak1_pegout_txid = self.nodes[i_pak1].sendtomainchain("", 1)["txid"]
assert_equal(self.nodes[i_pak1].getwalletpakinfo()["bip32_counter"], "1")
# Also spend the change to make chained payment that will be rejected as well
pak1_child_txid = self.nodes[i_pak1].sendtoaddress(self.nodes[i_pak1].getnewaddress(), self.nodes[i_pak1].getbalance()['bitcoin'], "", "", True)
# put back init_info version that's in pak list
self.nodes[1].initpegoutwallet(xpub, 0, init_results[1]["liquid_pak"])
# Node 1 will now make a PAK peg-out, accepted in all mempools and blocks
pegout_info = self.nodes[1].sendtomainchain("", 1)
raw_node1_pegout = self.nodes[1].gettransaction(pegout_info["txid"])["hex"]
self.sync_all() # mempool sync
self.nodes[1].generatetoaddress(1, self.nodes[0].getnewaddress())
self.sync_all() # block sync
assert_greater_than(self.nodes[1].gettransaction(pegout_info["txid"])["confirmations"], 0)
# Wait for node("follow the leader" conf-undefined) to get transaction in
time_to_wait = 15
while time_to_wait > 0:
# novalidate doesn't allow >80 byte op_return outputs due to no enforce_pak
if (pak1_pegout_txid not in self.nodes[i_novalidate].getrawmempool() and
pak1_pegout_txid in self.nodes[i_undefined].getrawmempool() and
pak1_pegout_txid not in self.nodes[i_pak2].getrawmempool() and
pak1_pegout_txid not in self.nodes[i_reject].getrawmempool()):
break
time_to_wait -= 1
time.sleep(1)
assert(time_to_wait > 0)
# Re-org keep node 1 peg-out unconfirmed and transition to "full list"
# then check peg-out fails
# pak_reject will make a block commitment, causing all validating nodes to dump
# the peg transaction
self.nodes[i_reject].generate(1)
# Invalidate back to block 1, then make 9 new blocks to hit transition
# If you roll back to genesis block p2p code gets flakey
num_block_rollback = self.nodes[1].getblockcount()-2
fork_hash = self.nodes[1].getblockhash(2)
for i in range(self.num_nodes):
self.nodes[i].invalidateblock(fork_hash)
sync_blocks(self.nodes)
assert_equal(pak1_pegout_txid in self.nodes[i_novalidate].getrawmempool(), False)
assert_equal(pak1_pegout_txid in self.nodes[i_undefined].getrawmempool(), False)
assert_equal(pak1_pegout_txid in self.nodes[i_pak1].getrawmempool(), True)
assert_equal(pak1_pegout_txid in self.nodes[i_pak2].getrawmempool(), False)
assert_equal(pak1_pegout_txid in self.nodes[i_reject].getrawmempool(), False)
for _ in range(num_block_rollback):
block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":extension_space_proposal})
self.nodes[1].submitblock(block)
assert_equal(self.nodes[i_pak1].gettransaction(pak1_pegout_txid)["confirmations"], 0)
sync_blocks(self.nodes)
# Make sure child payment also bumped from mempool
assert_equal(pak1_child_txid in self.nodes[i_novalidate].getrawmempool(), False)
assert_equal(pak1_child_txid in self.nodes[i_undefined].getrawmempool(), False)
assert_equal(pak1_child_txid in self.nodes[i_pak1].getrawmempool(), True)
assert_equal(pak1_child_txid in self.nodes[i_pak2].getrawmempool(), False)
assert_equal(pak1_child_txid in self.nodes[i_reject].getrawmempool(), False)
# node 0 puts the peg-out back in its mempool, can't sync all
self.nodes[0].sendrawtransaction(raw_node1_pegout)
sync_mempools(self.nodes[1:])
# rejected in mempool
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[1].sendrawtransaction, raw_node1_pegout)
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[2].sendrawtransaction, raw_node1_pegout)
# node 0 tries to make bad block
wrong_pak_prop = self.nodes[0].getnewblockhex()
# rejected in blocks
assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[1].testproposedblock, wrong_pak_prop, True)
assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[2].testproposedblock, wrong_pak_prop, True)
self.log.info("Test various RPC arguments")
assert_equal(self.nodes[i_pak1].gettransaction(pak1_child_txid)["confirmations"], 0)
# Fail to peg-out too-small value
assert_raises_rpc_error(-8, "Invalid amount for send, must send more than 0.0001 BTC", self.nodes[i_undefined].sendtomainchain, "", Decimal('0.0009'))
assert_raises_rpc_error(-8, "Invalid amount for send, must send more than 0.0001 BTC", self.nodes[1].sendtomainchain, "", Decimal('0.0009'))
# Use wrong network's extended pubkey
mainnetxpub = "xpub6AATBi58516uxLogbuaG3jkom7x1qyDoZzMN2AePBuQnMFKUV9xC2BW9vXsFJ9rELsvbeGQcFWhtbyM4qDeijM22u3AaSiSYEvuMZkJqtLn"
assert_raises_rpc_error(-8, "bitcoin_descriptor is not a valid descriptor string.", self.nodes[i_undefined].initpegoutwallet, mainnetxpub)
assert_raises_rpc_error(-8, "bitcoin_descriptor is not a valid descriptor string.", self.nodes[1].initpegoutwallet, mainnetxpub)
# Test fixed online pubkey
init_info = self.nodes[i_pak1].initpegoutwallet(xpub)
init_info2 = self.nodes[i_pak1].initpegoutwallet(xpub, 0, init_info['liquid_pak'])
init_info = self.nodes[1].initpegoutwallet(xpub)
init_info2 = self.nodes[1].initpegoutwallet(xpub, 0, init_info['liquid_pak'])
assert_equal(init_info, init_info2)
init_info3 = self.nodes[i_pak1].initpegoutwallet(xpub)
init_info3 = self.nodes[1].initpegoutwallet(xpub)
assert(init_info != init_info3)
# Test Descriptor PAK Support
# Non-supported descriptors
assert_raises_rpc_error(-8, "bitcoin_descriptor is not of any type supported: pkh(<xpub>), sh(wpkh(<xpub>)), wpkh(<xpub>), or <xpub>.", self.nodes[i_pak1].initpegoutwallet, "pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)")
assert_raises_rpc_error(-8, "bitcoin_descriptor is not of any type supported: pkh(<xpub>), sh(wpkh(<xpub>)), wpkh(<xpub>), or <xpub>.", self.nodes[1].initpegoutwallet, "pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)")
assert_raises_rpc_error(-8, "bitcoin_descriptor must be a ranged descriptor.", self.nodes[i_pak1].initpegoutwallet, "pkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)")
assert_raises_rpc_error(-8, "bitcoin_descriptor must be a ranged descriptor.", self.nodes[1].initpegoutwallet, "pkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)")
# Peg out with each new type, check that destination script matches
wpkh_desc = "wpkh("+xpub+"/0/*)"
wpkh_info = self.nodes[i_pak1].initpegoutwallet(wpkh_desc)
wpkh_pak_info = self.nodes[i_pak1].getwalletpakinfo()
wpkh_info = self.nodes[1].initpegoutwallet(wpkh_desc)
wpkh_pak_info = self.nodes[1].getwalletpakinfo()
# Add to pak list for pak1, restart
self.stop_nodes()
extra_args = copy.deepcopy(args)
extra_args[i_pak1] = extra_args[i_pak1]+["-"+wpkh_info["pakentry"]]
self.start_nodes(extra_args)
# Transition to wpkh entry list
wpkh_pak_entry = wpkh_info["pakentry"]
wpkh_pak_prop = [wpkh_pak_entry[4:4+66]+wpkh_pak_entry[4+66+1:]]
for _ in range(10):
block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":wpkh_pak_prop})
self.nodes[1].submitblock(block)
sync_blocks(self.nodes)
# Make block commitment and get some block subsidy
self.nodes[i_pak1].generate(101)
wpkh_stmc = self.nodes[i_pak1].sendtomainchain("", 1)
# Get some block subsidy and send off
self.nodes[1].generatetoaddress(101, self.nodes[1].getnewaddress())
wpkh_stmc = self.nodes[1].sendtomainchain("", 1)
wpkh_txid = wpkh_stmc['txid']
# Also check some basic return fields of sendtomainchain with pak
assert_equal(wpkh_stmc["bitcoin_address"], wpkh_info["address_lookahead"][0])
validata = self.nodes[i_pak1].validateaddress(wpkh_stmc["bitcoin_address"])
validata = self.nodes[1].validateaddress(wpkh_stmc["bitcoin_address"])
assert(not validata["isvalid"])
assert(validata["isvalid_parent"])
assert(not validata["parent_address_info"]["isscript"])
@ -361,32 +194,28 @@ class PAKTest (BitcoinTestFramework):
assert_equal(wpkh_pak_info["bitcoin_descriptor"], wpkh_stmc["bitcoin_descriptor"])
sh_wpkh_desc = "sh(wpkh("+xpub+"/0/1/*))"
sh_wpkh_info = self.nodes[i_pak1].initpegoutwallet(sh_wpkh_desc)
sh_wpkh_info = self.nodes[1].initpegoutwallet(sh_wpkh_desc)
validata = self.nodes[i_pak1].validateaddress(sh_wpkh_info["address_lookahead"][0])
validata = self.nodes[1].validateaddress(sh_wpkh_info["address_lookahead"][0])
assert(not validata["isvalid"])
assert(validata["isvalid_parent"])
assert(validata["parent_address_info"]["isscript"])
assert(not validata["parent_address_info"]["iswitness"])
# Add to pak list for pak1, restart
self.stop_nodes()
extra_args = copy.deepcopy(args)
extra_args[i_pak1] = extra_args[i_pak1]+["-"+sh_wpkh_info["pakentry"]]
# Transition to sh_wpkh entry list
sh_wpkh_pak_entry = sh_wpkh_info["pakentry"]
sh_wpkh_pak_prop = [sh_wpkh_pak_entry[4:4+66]+sh_wpkh_pak_entry[4+66+1:]]
for _ in range(10):
block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":sh_wpkh_pak_prop})
self.nodes[1].submitblock(block)
sync_blocks(self.nodes)
# Restart and connect peers
self.start_nodes(extra_args)
connect_nodes_bi(self.nodes,0,1)
connect_nodes_bi(self.nodes,1,2)
connect_nodes_bi(self.nodes,2,3)
connect_nodes_bi(self.nodes,3,4)
self.nodes[i_pak1].generate(1)
sh_wpkh_txid = self.nodes[i_pak1].sendtomainchain("", 1)['txid']
self.nodes[1].generatetoaddress(1, self.nodes[1].getnewaddress())
sh_wpkh_txid = self.nodes[1].sendtomainchain("", 1)['txid']
# Make sure peg-outs look correct
wpkh_raw = self.nodes[i_pak1].decoderawtransaction(self.nodes[i_pak1].gettransaction(wpkh_txid)['hex'])
sh_wpkh_raw = self.nodes[i_pak1].decoderawtransaction(self.nodes[i_pak1].gettransaction(sh_wpkh_txid)['hex'])
wpkh_raw = self.nodes[1].decoderawtransaction(self.nodes[1].gettransaction(wpkh_txid)['hex'])
sh_wpkh_raw = self.nodes[1].decoderawtransaction(self.nodes[1].gettransaction(sh_wpkh_txid)['hex'])
peg_out_found = False
for output in wpkh_raw["vout"]:
@ -410,9 +239,36 @@ class PAKTest (BitcoinTestFramework):
raise Exception("Found unexpected peg-out output")
assert(peg_out_found)
# Make sure they all confirm
self.nodes[1].generatetoaddress(1, self.nodes[0].getnewaddress())
for tx_id in [wpkh_txid, sh_wpkh_txid]:
assert_greater_than(self.nodes[1].gettransaction(tx_id)["confirmations"], 0)
self.log.info("Test that pak-less pegouts are rejected")
# Last test of a pak-less peg-out failing to get into mempool/block
# Note it leaves a transaction in node 0's mempool, so sync_all cannot
# work after unless it's somehow booted.
# node 0 will now create a pegout, will fail to enter mempool of node 1 or 2
# since it's pak-less
nopak_pegout_txid = self.nodes[0].sendtomainchain("n3NkSZqoPMCQN5FENxUBw4qVATbytH6FDK", 1)
raw_pakless_pegout = self.nodes[0].gettransaction(nopak_pegout_txid)["hex"]
assert nopak_pegout_txid in self.nodes[0].getrawmempool()
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[1].sendrawtransaction, raw_pakless_pegout)
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[2].sendrawtransaction, raw_pakless_pegout)
# node 0 makes a block that includes the pakless pegout, rejected by others
# by consensus
bad_prop = self.nodes[0].getnewblockhex()
assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[1].testproposedblock, bad_prop, True)
assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[2].testproposedblock, bad_prop, True)
# Test that subtracting fee from output works
self.nodes[i_pak1].sendtomainchain("", self.nodes[i_pak1].getbalance()["bitcoin"], True)
assert_equal(self.nodes[i_pak1].getbalance()["bitcoin"], 0)
self.nodes[1].generatetoaddress(101, self.nodes[1].getnewaddress())
self.nodes[1].sendtomainchain("", self.nodes[1].getbalance()["bitcoin"], True)
assert_equal(self.nodes[1].getbalance()["bitcoin"], 0)
# TODO: create rawsendtomainchain to do transaction surgery for testing

View file

@ -63,8 +63,12 @@ class BlockchainTest(BitcoinTestFramework):
'bip9_softforks',
'blocks',
'chain',
'current_signblock_asm',
'current_signblock_hex',
'extension_space',
'headers',
'initialblockdownload',
'max_block_witness',
'mediantime',
'pruned',
'signblock_asm',
@ -92,6 +96,7 @@ class BlockchainTest(BitcoinTestFramework):
self.restart_node(0, ['-stopatheight=207'])
res = self.nodes[0].getblockchaininfo()
# should have exact keys
assert_equal(sorted(res.keys()), keys)

View file

@ -330,6 +330,7 @@ def initialize_datadir(dirname, n, chain):
f.write("con_bip66height=1251\n")
f.write("con_csv_deploy_start=0\n") # Enhance tests if removing this line
f.write("blindedaddresses=0\n") # Set to minimize broken tests in favor of custom
f.write("con_dyna_deploy_start="+str(2**31)+"\n") # Never starts unless overridden
#f.write("pubkeyprefix=111\n")
#f.write("scriptprefix=196\n")
#f.write("bech32_hrp=bcrt\n")

View file

@ -74,7 +74,7 @@ BASE_SCRIPTS = [
'feature_block_subsidy.py',
'feature_connect_genesis_outputs.py',
'feature_block_v4.py',
#TODO re-enable 'feature_pak.py',
'feature_pak.py',
'feature_blocksign.py',
'rpc_calcfastmerkleroot.py',
'feature_txwitness.py',
@ -85,6 +85,7 @@ BASE_SCRIPTS = [
'feature_assetsdir.py',
'feature_initial_reissuance_token.py',
'feature_progress.py',
'feature_dynafed.py',
# Longest test should go first, to favor running tests in parallel
'wallet_hd.py',
'wallet_backup.py',