Skip to content

Commit

Permalink
Add anchor argument to transaction creation rpcs
Browse files Browse the repository at this point in the history
  • Loading branch information
instagibbs committed Nov 9, 2023
1 parent e8fd440 commit 97988e8
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/interfaces/chain.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/rpc/rawtransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ static std::vector<RPCArg> 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"},
Expand Down
11 changes: 11 additions & 0 deletions src/rpc/rawtransaction_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in)
// Duplicate checking
std::set<CTxDestination> destinations;
bool has_data{false};
bool has_anchor{false};

for (const std::string& name_ : outputs.getKeys()) {
if (name_ == "data") {
Expand All @@ -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<unsigned char>{0x4e, 0x73});
rawTx.vout.push_back(out);
} else {
CTxDestination destination = DecodeDestination(name_);
if (!IsValidDestination(destination)) {
Expand Down
5 changes: 5 additions & 0 deletions src/wallet/rpc/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,11 @@ static std::vector<RPCArg> 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},
},
},
};
}

Expand Down
5 changes: 5 additions & 0 deletions src/wallet/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,11 @@ static util::Result<CreatedTransactionResult> 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);
}

Expand Down
53 changes: 52 additions & 1 deletion test/functional/rpc_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
MAX_BIP125_RBF_SEQUENCE,
WITNESS_SCALE_FACTOR,
ser_compact_size,
tx_from_hex,
)
from test_framework.psbt import (
PSBT,
Expand All @@ -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,
Expand Down Expand Up @@ -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()

0 comments on commit 97988e8

Please sign in to comment.