diff --git a/configure.ac b/configure.ac index 69921afef2..b43eb715c2 100644 --- a/configure.ac +++ b/configure.ac @@ -3,9 +3,9 @@ AC_PREREQ([2.60]) define(_CLIENT_VERSION_MAJOR, 0) define(_CLIENT_VERSION_MINOR, 17) define(_CLIENT_VERSION_REVISION, 0) -define(_CLIENT_VERSION_BUILD, 0) +define(_CLIENT_VERSION_BUILD, 1) define(_CLIENT_VERSION_IS_RELEASE, true) -define(_COPYRIGHT_YEAR, 2018) +define(_COPYRIGHT_YEAR, 2019) define(_COPYRIGHT_HOLDERS,[The %s developers]) define(_COPYRIGHT_HOLDERS_SUBSTITUTION,[[Elements Project]]) AC_INIT([Elements Core],[_CLIENT_VERSION_MAJOR._CLIENT_VERSION_MINOR._CLIENT_VERSION_REVISION],[https://github.com/ElementsProject/elements/issues],[elements],[https://elementsproject.org/]) diff --git a/contrib/gitian-descriptors/gitian-liquid-win.yml b/contrib/gitian-descriptors/gitian-liquid-win.yml index 09fc0d934e..057c8ee603 100644 --- a/contrib/gitian-descriptors/gitian-liquid-win.yml +++ b/contrib/gitian-descriptors/gitian-liquid-win.yml @@ -185,6 +185,9 @@ script: | cd $BUILD_DIR/windeploy mkdir unsigned cp $OUTDIR/elements-*setup-unsigned.exe unsigned/ + cd unsigned + rename 's/elements-/liquid-/s' *-setup-unsigned.exe + cd .. find . | sort | tar --no-recursion --mode='u+rw,go+r-w,a+X' --owner=0 --group=0 -c -T - | gzip -9n > ${OUTDIR}/${DISTNAME}-win-unsigned.tar.gz mv ${OUTDIR}/${DISTNAME}-x86_64-*-debug.zip ${OUTDIR}/${DISTNAME}-win64-debug.zip mv ${OUTDIR}/${DISTNAME}-i686-*-debug.zip ${OUTDIR}/${DISTNAME}-win32-debug.zip diff --git a/contrib/verify-commits/trusted-keys b/contrib/verify-commits/trusted-keys index 6d430b508d..bfe9d3d766 100644 --- a/contrib/verify-commits/trusted-keys +++ b/contrib/verify-commits/trusted-keys @@ -1,2 +1,2 @@ 11D43A27826A421212108BF66BE2CED14A9917BC -85758DBCF05A48A4DE9EAA127FC91380BB4CE800 +DE10E82629A8CAD55B700B972F2A88D7F8D68E87 diff --git a/src/init.cpp b/src/init.cpp index 3a11c8ec5f..d93e31707f 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1879,11 +1879,6 @@ bool AppInitMain(InitInterfaces& interfaces) SetRPCWarmupFinished(); // ELEMENTS: - CScheduler::Function f2 = boost::bind(&MainchainRPCCheck, false); - unsigned int check_rpc_every = gArgs.GetArg("-recheckpeginblockinterval", 120); - if (check_rpc_every) { - scheduler.scheduleEvery(f2, check_rpc_every); - } uiInterface.InitMessage(_("Awaiting mainchain RPC warmup")); if (!MainchainRPCCheck(true)) { //Initial check, fail immediately return InitError(_("ERROR: elementsd is set to verify pegins but cannot get valid response from the mainchain daemon. Please check debug.log for more information.") @@ -1891,6 +1886,12 @@ bool AppInitMain(InitInterfaces& interfaces) + strprintf(_("If you haven't setup a %s please get the latest stable version from %s or if you do not need to validate pegins set in your elements configuration %s"), "bitcoind", "https://bitcoincore.org/en/download/", "validatepegin=0")); } + CScheduler::Function f2 = boost::bind(&MainchainRPCCheck, false); + unsigned int check_rpc_every = gArgs.GetArg("-recheckpeginblockinterval", 120); + if (check_rpc_every) { + scheduler.scheduleEvery(f2, check_rpc_every); + } + uiInterface.InitMessage(_("Done loading")); for (const auto& client : interfaces.chain_clients) { diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 5becff6d2c..cb626aa16b 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -1034,7 +1034,7 @@ UniValue combineblocksigs(const JSONRPCRequest& request) " \"complete\": true|false (numeric) If block is complete \n" "}\n" "\nExamples:\n" - + HelpExampleCli("combineblocksigs", " [\"signature1\", \"signature2\", ...]") + + HelpExampleCli("combineblocksigs", " '[{\"pubkey\":\"hex\",\"sig\":\"hex\"}, ...]'") ); if (!g_signed_blocks) { diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 9a39f59766..ef189401be 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -35,7 +34,6 @@ #include #include #include -#include #include #include @@ -1844,13 +1842,13 @@ UniValue converttopsbt(const JSONRPCRequest& request) // Remove all scriptSigs and scriptWitnesses from inputs for (CTxIn& input : tx.vin) { - if ((!input.scriptSig.empty()) && (request.params[1].isNull() || (!request.params[1].isNull() && request.params[1].get_bool()))) { + if (!input.scriptSig.empty() && !permitsigdata) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Inputs must not have scriptSigs"); } input.scriptSig.clear(); } for (CTxInWitness& witness: tx.witness.vtxinwit) { - if ((!witness.scriptWitness.IsNull()) && (request.params[1].isNull() || (!request.params[1].isNull() && request.params[1].get_bool()))) { + if (!witness.scriptWitness.IsNull() && !permitsigdata) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Inputs must not have scriptWitnesses"); } } diff --git a/src/validation.cpp b/src/validation.cpp index fae9852f1d..2f064b37bd 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -682,10 +682,27 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool } // do all inputs exist? - for (const CTxIn& txin : tx.vin) { + for (unsigned int i = 0; i < tx.vin.size(); i++) { + const CTxIn& txin = tx.vin[i]; + // ELEMENTS: - // Don't look for coins that only exist in parent chain + // For pegin inputs check whether the pegins have already been claimed before. + // This only checks the UTXO set for already claimed pegins. For mempool conflicts, + // we rely on the GetConflictTx check done above. if (txin.m_is_pegin) { + // Quick sanity check on witness first. + if (tx.witness.vtxinwit.size() <= i || + tx.witness.vtxinwit[i].m_pegin_witness.stack.size() < 6 || + uint256(tx.witness.vtxinwit[i].m_pegin_witness.stack[2]).IsNull() || + tx.vin[i].prevout.hash.IsNull()) { + return state.Invalid(false, REJECT_INVALID, "pegin-no-witness"); + } + + std::pair pegin = std::make_pair(uint256(tx.witness.vtxinwit[i].m_pegin_witness.stack[2]), tx.vin[i].prevout); + // This assumes non-null prevout and genesis block hash + if (view.IsPeginSpent(pegin)) { + return state.Invalid(false, REJECT_INVALID, "pegin-already-claimed"); + } continue; } @@ -5165,7 +5182,8 @@ bool MainchainRPCCheck(const bool init) if (!error.isNull()) { // On the first call, it's possible to node is still in // warmup; in that case, just wait and retry. - if (error["code"].get_int() == RPC_IN_WARMUP) { + // If this is not the initial call, just report failure. + if (init && error["code"].get_int() == RPC_IN_WARMUP) { MilliSleep(1000); continue; } diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 6f48a2b46f..5d431d10fe 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -4672,33 +4672,34 @@ UniValue getpeginaddress(const JSONRPCRequest& request) + HelpExampleRpc("getpeginaddress", "") ); - //Creates new address for receiving unlocked utxos - JSONRPCRequest req; - CTxDestination address = DecodeDestination(getnewaddress(req).get_str()); - - Witnessifier w(pwallet); - bool ret = boost::apply_visitor(w, address); - if (!ret) { - throw JSONRPCError(RPC_WALLET_ERROR, "Public key or redeemscript not known to wallet, or the key is uncompressed"); + if (!pwallet->IsLocked()) { + pwallet->TopUpKeyPool(); } - pwallet->SetAddressBook(w.result, "", "receive"); + // Generate a new key that is added to wallet + CPubKey newKey; + if (!pwallet->GetKeyFromPool(newKey)) { + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); + } - CScript destScript = GetScriptForDestination(address); - CScript witProg = GetScriptForWitness(destScript); + // Use native witness destination + CTxDestination dest = GetDestinationForKey(newKey, OutputType::BECH32); + + pwallet->SetAddressBook(dest, "", "receive"); + + CScript dest_script = GetScriptForDestination(dest); // Also add raw scripts to index to recognize later. - pwallet->AddCScript(witProg); - pwallet->AddCScript(destScript); + pwallet->AddCScript(dest_script); - //Call contracthashtool, get deposit address on mainchain. - CTxDestination destAddr(ScriptHash(GetScriptForWitness(calculate_contract(Params().GetConsensus().fedpegScript, witProg)))); + // Get P2CH deposit address on mainchain. + CTxDestination mainchain_dest(ScriptHash(GetScriptForWitness(calculate_contract(Params().GetConsensus().fedpegScript, dest_script)))); - UniValue fundinginfo(UniValue::VOBJ); + UniValue ret(UniValue::VOBJ); - fundinginfo.pushKV("mainchain_address", EncodeParentDestination(destAddr)); - fundinginfo.pushKV("claim_script", HexStr(witProg)); - return fundinginfo; + ret.pushKV("mainchain_address", EncodeParentDestination(mainchain_dest)); + ret.pushKV("claim_script", HexStr(dest_script)); + return ret; } //! Derive BIP32 tweak from master xpub to child pubkey. @@ -5289,7 +5290,7 @@ static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef if (txHashes.size() != 1 || txHashes[0] != txBTC.GetHash()) throw JSONRPCError(RPC_INVALID_PARAMETER, "The txoutproof must contain bitcoinTx and only bitcoinTx"); - CScript witnessProgScript; + CScript witness_script; unsigned int nOut = txBTC.vout.size(); if (request.params.size() > 2) { const std::string claim_script = request.params[2].get_str(); @@ -5298,26 +5299,19 @@ static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef } // If given manually, no need for it to be a witness script std::vector witnessBytes(ParseHex(claim_script)); - witnessProgScript = CScript(witnessBytes.begin(), witnessBytes.end()); - nOut = GetPeginTxnOutputIndex(txBTC, witnessProgScript); + witness_script = CScript(witnessBytes.begin(), witnessBytes.end()); + nOut = GetPeginTxnOutputIndex(txBTC, witness_script); if (nOut == txBTC.vout.size()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Given claim_script does not match the given Bitcoin transaction."); } } else { - // Look through address book for pegin contract value by extracting the unlderlying witness program from p2sh-p2wpkh + // Look for known wpkh address in wallet for (std::map::const_iterator iter = pwallet->mapAddressBook.begin(); iter != pwallet->mapAddressBook.end(); ++iter) { - CTxDestination sidechainAddress(CTxDestination(iter->first)); - CScript witnessProgramScript = GetScriptForWitness(GetScriptForDestination(sidechainAddress)); - int version; - std::vector witnessProgram; - // Only process witness v0 programs - if (!witnessProgramScript.IsWitnessProgram(version, witnessProgram) || version != 0) { - continue; - } - nOut = GetPeginTxnOutputIndex(txBTC, witnessProgramScript); + CScript dest_script = GetScriptForDestination(iter->first); + nOut = GetPeginTxnOutputIndex(txBTC, dest_script); if (nOut != txBTC.vout.size()) { - witnessProgScript = witnessProgramScript; + witness_script = dest_script; break; } } @@ -5325,12 +5319,12 @@ static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef if (nOut == txBTC.vout.size()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Failed to find output in bitcoinTx to the mainchain_address from getpeginaddress"); } - assert(witnessProgScript != CScript()); + assert(witness_script != CScript()); int version = -1; - std::vector witnessProgram; - if (!witnessProgScript.IsWitnessProgram(version, witnessProgram)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Given or recovered script is not a witness program."); + std::vector witness_program; + if (!witness_script.IsWitnessProgram(version, witness_program) || version != 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Given or recovered script is not a v0 witness program."); } CAmount value = 0; @@ -5380,7 +5374,7 @@ static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef stack.push_back(value_bytes); stack.push_back(std::vector(Params().GetConsensus().pegged_asset.begin(), Params().GetConsensus().pegged_asset.end())); stack.push_back(std::vector(genesisBlockHash.begin(), genesisBlockHash.end())); - stack.push_back(std::vector(witnessProgScript.begin(), witnessProgScript.end())); + stack.push_back(std::vector(witness_script.begin(), witness_script.end())); stack.push_back(txData); stack.push_back(txOutProofData); @@ -5388,7 +5382,7 @@ static UniValue createrawpegin(const JSONRPCRequest& request, T_tx_ref& txBTCRef // We re-check depth before returning with more descriptive result std::string err; if (!IsValidPeginWitness(pegin_witness, mtx.vin[0].prevout, err, false)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Constructed peg-in witness is invalid."); + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Constructed peg-in witness is invalid: %s", err)); } // Put input witness in transaction @@ -5497,6 +5491,15 @@ UniValue claimpegin(const JSONRPCRequest& request) throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); } + // To check if it's not double spending an existing pegin UTXO, we check mempool acceptance. + CValidationState acceptState; + bool accepted = ::AcceptToMemoryPool(mempool, acceptState, MakeTransactionRef(mtx), nullptr /* pfMissingInputs */, + nullptr /* plTxnReplaced */, false /* bypass_limits */, maxTxFee, true /* test_accept */); + if (!accepted) { + std::string strError = strprintf("Error: The transaction was rejected! Reason given: %s", FormatStateMessage(acceptState)); + throw JSONRPCError(RPC_WALLET_ERROR, strError); + } + // Send it CValidationState state; mapValue_t mapValue; diff --git a/test/functional/feature_fedpeg.py b/test/functional/feature_fedpeg.py index 71295fb734..78b7df497a 100755 --- a/test/functional/feature_fedpeg.py +++ b/test/functional/feature_fedpeg.py @@ -12,12 +12,18 @@ from test_framework.util import ( p2p_port, assert_raises_rpc_error, assert_equal, + bytes_to_hex_str, ) +from test_framework import util from test_framework.messages import ( + CBlock, CTransaction, CTxInWitness, FromHex, ) +from test_framework.blocktools import ( + add_witness_commitment, +) from decimal import Decimal def get_new_unconfidential_address(node, addr_type="p2sh-segwit"): @@ -172,6 +178,7 @@ class FedPegTest(BitcoinTestFramework): sidechain2 = self.nodes[3] for node in self.nodes: node.importprivkey(privkey=node.get_deterministic_priv_key().key, label="mining") + util.node_fastmerkle = sidechain parent.generate(101) sidechain.generate(101) @@ -179,13 +186,14 @@ class FedPegTest(BitcoinTestFramework): addrs = sidechain.getpeginaddress() addr = addrs["mainchain_address"] + assert_equal(sidechain.decodescript(addrs["claim_script"])["type"], "witness_v0_keyhash") txid1 = parent.sendtoaddress(addr, 24) # 10+2 confirms required to get into mempool and confirm parent.generate(1) time.sleep(2) proof = parent.gettxoutproof([txid1]) - raw = parent.getrawtransaction(txid1) + raw = parent.gettransaction(txid1)["hex"] print("Attempting peg-ins") # First attempt fails the consensus check but gives useful result @@ -219,6 +227,22 @@ class FedPegTest(BitcoinTestFramework): # 12 confirms allows in mempool parent.generate(1) + + # Make sure that a tx with a duplicate pegin claim input gets rejected. + raw_pegin = sidechain.createrawpegin(raw, proof)["hex"] + raw_pegin = FromHex(CTransaction(), raw_pegin) + raw_pegin.vin.append(raw_pegin.vin[0]) # duplicate the pegin input + raw_pegin = sidechain.signrawtransactionwithwallet(raw_pegin.serialize().hex())["hex"] + assert_raises_rpc_error(-26, "bad-txns-inputs-duplicate", sidechain.sendrawtransaction, raw_pegin) + # Also try including this tx in a block manually and submitting it. + doublespendblock = FromHex(CBlock(), sidechain.getnewblockhex()) + doublespendblock.vtx.append(FromHex(CTransaction(), raw_pegin)) + doublespendblock.hashMerkleRoot = doublespendblock.calc_merkle_root() + add_witness_commitment(doublespendblock) + doublespendblock.solve() + block_hex = bytes_to_hex_str(doublespendblock.serialize(True)) + assert_raises_rpc_error(-25, "bad-txns-inputs-duplicate", sidechain.testproposedblock, block_hex, True) + # Should succeed via wallet lookup for address match, and when given raw_pegin = sidechain.createrawpegin(raw, proof)['hex'] signed_pegin = sidechain.signrawtransactionwithwallet(raw_pegin) @@ -230,6 +254,9 @@ class FedPegTest(BitcoinTestFramework): sample_pegin_witness = sample_pegin_struct.wit.vtxinwit[0].peginWitness pegtxid1 = sidechain.claimpegin(raw, proof) + # Make sure a second pegin claim does not get accepted in the mempool when + # another mempool tx already claims that pegin. + assert_raises_rpc_error(-4, "txn-mempool-conflict", sidechain.claimpegin, raw, proof) # Will invalidate the block that confirms this transaction later self.sync_all(self.node_groups) @@ -259,6 +286,21 @@ class FedPegTest(BitcoinTestFramework): sidechain.invalidateblock(blockhash[0]) if sidechain.gettransaction(pegtxid1)["confirmations"] != 0: raise Exception("Peg-in didn't unconfirm after invalidateblock call.") + + # Create duplicate claim, put it in block along with current one in mempool + # to test duplicate-in-block claims between two txs that are in the same block. + raw_pegin = sidechain.createrawpegin(raw, proof)["hex"] + raw_pegin = sidechain.signrawtransactionwithwallet(raw_pegin)["hex"] + raw_pegin = FromHex(CTransaction(), raw_pegin) + doublespendblock = FromHex(CBlock(), sidechain.getnewblockhex()) + assert(len(doublespendblock.vtx) == 2) # coinbase and pegin + doublespendblock.vtx.append(raw_pegin) + doublespendblock.hashMerkleRoot = doublespendblock.calc_merkle_root() + add_witness_commitment(doublespendblock) + doublespendblock.solve() + block_hex = bytes_to_hex_str(doublespendblock.serialize(True)) + assert_raises_rpc_error(-25, "bad-txns-double-pegin", sidechain.testproposedblock, block_hex, True) + # Re-enters block sidechain.generate(1) if sidechain.gettransaction(pegtxid1)["confirmations"] != 1: @@ -267,6 +309,19 @@ class FedPegTest(BitcoinTestFramework): if sidechain.gettransaction(pegtxid1)["confirmations"] != 6: raise Exception("Peg-in should be back to 6 confirms.") + # Now the pegin is already claimed in a confirmed tx. + # In that case, a duplicate claim should (1) not be accepted in the mempool + # and (2) not be accepted in a block. + assert_raises_rpc_error(-4, "pegin-already-claimed", sidechain.claimpegin, raw, proof) + # For case (2), manually craft a block and include the tx. + doublespendblock = FromHex(CBlock(), sidechain.getnewblockhex()) + doublespendblock.vtx.append(raw_pegin) + doublespendblock.hashMerkleRoot = doublespendblock.calc_merkle_root() + add_witness_commitment(doublespendblock) + doublespendblock.solve() + block_hex = bytes_to_hex_str(doublespendblock.serialize(True)) + assert_raises_rpc_error(-25, "bad-txns-double-pegin", sidechain.testproposedblock, block_hex, True) + # Do multiple claims in mempool n_claims = 6 @@ -281,7 +336,7 @@ class FedPegTest(BitcoinTestFramework): txid = parent.sendtoaddress(addrs["mainchain_address"], 1) parent.generate(1) proof = parent.gettxoutproof([txid]) - raw = parent.getrawtransaction(txid) + raw = parent.gettransaction(txid)["hex"] if i % 2 == 0: parent.generate(11) pegtxs += [sidechain.claimpegin(raw, proof)] @@ -362,7 +417,7 @@ class FedPegTest(BitcoinTestFramework): txid = parent.sendtoaddress(addr, 1) parent.generate(12) proof = parent.gettxoutproof([txid]) - raw = parent.getrawtransaction(txid) + raw = parent.gettransaction(txid)["hex"] sidechain.claimpegin(raw, proof) # stuck peg sidechain.generate(1) print("Waiting to ensure block is being rejected by sidechain2") diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index c295b912b3..c7f9451c25 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -145,6 +145,7 @@ class PSBTTest(BitcoinTestFramework): # Make sure that a psbt with signatures cannot be converted signedtx = self.nodes[0].signrawtransactionwithwallet(rawtx['hex']) + assert_raises_rpc_error(-22, "Inputs must not have scriptWitnesses", self.nodes[0].converttopsbt, signedtx['hex'], False) assert_raises_rpc_error(-22, "Inputs must not have scriptWitnesses", self.nodes[0].converttopsbt, signedtx['hex']) assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].converttopsbt, signedtx['hex']) assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].converttopsbt, signedtx['hex'], False)