diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index dea868f844da4..30ab185510737 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -206,6 +206,9 @@ class Chain //! Check if transaction has descendants in mempool. virtual bool hasDescendantsInMempool(const uint256& txid) = 0; + //! Check if ephemeral anchors are allowed. + virtual bool allowsEphemeralAnchors() = 0; + //! Transaction is added to memory pool, if the transaction fee is below the //! amount specified by max_tx_fee, and broadcast to all peers if relay is set to true. //! Return false if the transaction could not be added due to the fee or for another reason. diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index f6dbe4f008707..5187f0b0819c7 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -651,6 +651,12 @@ class ChainImpl : public Chain auto it = m_node.mempool->GetIter(txid); return it && (*it)->GetCountWithDescendants() > 1; } + bool allowsEphemeralAnchors() override + { + if (!m_node.mempool) return false; + LOCK(m_node.mempool->cs); + return m_node.mempool->m_permit_anchors; + } bool broadcastTransaction(const CTransactionRef& tx, const CAmount& max_tx_fee, bool relay, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 4fda05191630c..52b9273681fba 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -163,6 +163,11 @@ static std::vector CreateTxDoc() {"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A key-value pair. The key must be \"data\", the value is hex-encoded data"}, }, }, + {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"anchor", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "Creates an ephemeral anchor. A key-value pair. The key must be \"anchor\", the value is the amount in " + CURRENCY_UNIT}, + }, + }, }, RPCArgOptions{.skip_type_check = true}}, {"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"}, diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp index 3a6fa39e4dc70..1aba136c1f651 100644 --- a/src/rpc/rawtransaction_util.cpp +++ b/src/rpc/rawtransaction_util.cpp @@ -98,6 +98,7 @@ void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in) // Duplicate checking std::set destinations; bool has_data{false}; + bool has_anchor{false}; for (const std::string& name_ : outputs.getKeys()) { if (name_ == "data") { @@ -109,6 +110,16 @@ void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in) CTxOut out(0, CScript() << OP_RETURN << data); rawTx.vout.push_back(out); + } else if (name_ == "anchor") { + if (has_anchor) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, duplicate key: anchor"); + } + has_anchor = true; + rawTx.nVersion = 3; + CAmount nAmount = AmountFromValue(outputs[name_]); + + CTxOut out(nAmount, CScript() << OP_TRUE << std::vector{0x4e, 0x73}); + rawTx.vout.push_back(out); } else { CTxDestination destination = DecodeDestination(name_); if (!IsValidDestination(destination)) { diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index 6b96fc4e49f13..06e51931141ed 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -973,6 +973,11 @@ static std::vector OutputsDoc() {"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A key-value pair. The key must be \"data\", the value is hex-encoded data"}, }, }, + {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"anchor", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "Creates an ephemeral anchor. A key-value pair. The key must be \"anchor\", the value is the amount in " + CURRENCY_UNIT}, + }, + }, }; } diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 8314a2ddfab42..63ec7226811e0 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -1086,6 +1086,11 @@ static util::Result CreateTransactionInternal( if (IsDust(txout, wallet.chain().relayDustFee())) { return util::Error{_("Transaction amount too small")}; } + + if (recipient.scriptPubKey.IsPayToAnchor() && !wallet.chain().allowsEphemeralAnchors()) { + return util::Error{_("Anchor outputs are not allowed for relay: check -ephemeralanchors option")}; + } + txNew.vout.push_back(txout); } diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 1fd938d18a9a6..9bd10414c4671 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -18,6 +18,7 @@ MAX_BIP125_RBF_SEQUENCE, WITNESS_SCALE_FACTOR, ser_compact_size, + tx_from_hex, ) from test_framework.psbt import ( PSBT, @@ -31,7 +32,7 @@ PSBT_IN_WITNESS_UTXO, PSBT_OUT_TAP_TREE, ) -from test_framework.script import CScript, OP_TRUE +from test_framework.script import CScript, OP_TRUE, EPHEMERAL_ANCHOR_SCRIPT from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_approx, @@ -990,5 +991,55 @@ def test_psbt_input_keys(psbt_input, keys): assert_raises_rpc_error(-8, "all is not a valid sighash parameter.", self.nodes[2].descriptorprocesspsbt, psbt, [descriptor], sighashtype="all") + self.log.info("Test that PSBT can have ephemeral anchor added in rpc") + # Fake input to avoid tripping up segwit deserialization error + raw_anchor = self.nodes[0].createrawtransaction([{"txid": "ff"*32, "vout": 0}], [{"anchor":"0.00000001"}]) + anchor_tx = tx_from_hex(raw_anchor) + + # Is restricted to V3 + assert_equal(anchor_tx.nVersion, 3) + assert_equal(len(anchor_tx.vout), 1) + assert_equal(anchor_tx.vout[0].nValue, 1) + assert_equal(anchor_tx.vout[0].scriptPubKey, bytes([OP_TRUE, 0x02, 0x4e, 0x73])) + + psbt_anchor = self.nodes[0].createpsbt([{"txid": "ff"*32, "vout": 0}], [{"anchor":"0.00000001"}]) + anchor = PSBT.from_base64(psbt_anchor) + + assert_equal(anchor.g.map[0], anchor_tx.serialize()) + + utxos = self.nodes[0].listunspent() + + # Choose a utxo to fund it + funded_anchor = self.nodes[0].walletcreatefundedpsbt([{"txid": utxos[0]["txid"], "vout": utxos[0]["vout"]}], [{"anchor": "0.00000001"}], 0, {"fee_rate": "0"}) + + funded_decoded = self.nodes[0].decodepsbt(funded_anchor["psbt"])["tx"] + anchor_idx = 0 if funded_decoded["vout"][0]["scriptPubKey"]["address"] == "bcrt1pfeesnyr2tx" else 1 + assert_equal(funded_decoded["vout"][anchor_idx]["scriptPubKey"]["address"], "bcrt1pfeesnyr2tx") + assert_equal(funded_decoded["vout"][anchor_idx]["scriptPubKey"]["type"], "anchor") + assert_equal(funded_decoded["vout"][anchor_idx]["value"], Decimal("0.00000001")) + + anchor_tx = self.nodes[0].finalizepsbt(self.nodes[0].walletprocesspsbt(psbt=funded_anchor["psbt"])["psbt"])["hex"] + anchor_decoded = self.nodes[0].decoderawtransaction(anchor_tx) + anchor_index = 0 if anchor_decoded["vout"][0]["value"] == Decimal("0.00000001") else 1 + + # Parent tx is not "in wallet" or utxo set or mempool, so we must create spend manually + # Take second utxo to bump + bump = self.nodes[0].createpsbt([{"txid": anchor_decoded["txid"], "vout": anchor_index}, {"txid": utxos[1]["txid"], "vout": utxos[1]["vout"]}], [{self.nodes[0].getnewaddress(): utxos[1]["amount"] - 1}]) + + # Need to switch to v3 to spend v3 parent, and inject OP_TRUE utxo to extract later + acs_prevout = CTxOut(nValue=1, scriptPubKey=CScript([OP_TRUE])) + bump_edit = PSBT.from_base64(bump) + bump_tx = tx_from_hex(bump_edit.g.map[0].hex()) + bump_tx.nVersion = 3 + bump_edit.g.map[0] = bump_tx.serialize() + bump_edit.i = [PSBTMap({bytes([PSBT_IN_WITNESS_UTXO]) : acs_prevout.serialize()}), PSBTMap()] + bump = bump_edit.to_base64() + bump_signed = self.nodes[0].walletprocesspsbt(bump) + bump_final = self.nodes[0].finalizepsbt(bump_signed["psbt"]) + + # Submit both as a package successfully + self.nodes[0].submitpackage([anchor_tx, bump_final["hex"]]) + + if __name__ == '__main__': PSBTTest().main()