From ab6995f3c7ed2b780e2f801646c4a943c3d27852 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 24 Mar 2026 20:23:53 +1100 Subject: [PATCH] Add registername RPC with shared name_new commitment helper Add a new 'registername' RPC method that provides a simple single-call interface for name registration: registername "myname" "my-value" Unlike name_firstupdate (which requires a prior name_new and takes rand/tx/value as separate positional args), registername takes just a name and value, handling the commitment automatically: 1. Scan the wallet for a matching name_new commitment. 2. If none is found, create one automatically via name_new. 3. Build the name_firstupdate transaction. 4. If mempool-eligible, broadcast; otherwise commit to wallet for automatic rebroadcast after the name_new matures. Refactors the wallet scanning logic from name_firstupdate into a shared findNameNewCommitment() helper that is reused by both name_firstupdate and registername. This eliminates the duplicated wallet scan code and makes the commitment-finding logic testable independently. name_firstupdate is completely unchanged in behavior -- power users can still do the two-step manual procedure. Returns {txid, rand} as a JSON object. Includes functional tests covering: - Basic registration with auto name_new - Reuse of existing name_new commitment - Duplicate name rejection - Default (empty) value - Parameter validation (name too long, value too long) - Return format verification Ref: namecoin#576, namecoin#579, namecoin#581 --- src/wallet/rpc/wallet.cpp | 2 + src/wallet/rpc/walletnames.cpp | 297 +++++++++++++++++++++++---- test/functional/name_registername.py | 88 ++++++++ test/functional/test_runner.py | 1 + 4 files changed, 353 insertions(+), 35 deletions(-) create mode 100755 test/functional/name_registername.py diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index eb6e5e9b68..564ac47848 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -909,6 +909,7 @@ RPCHelpMan name_list(); RPCHelpMan name_new(); RPCHelpMan name_firstupdate(); RPCHelpMan name_update(); +RPCHelpMan registername(); RPCHelpMan queuerawtransaction(); RPCHelpMan dequeuetransaction(); RPCHelpMan listqueuedtransactions(); @@ -984,6 +985,7 @@ std::span GetWalletRPCCommands() {"names", &name_new}, {"names", &name_firstupdate}, {"names", &name_update}, + {"names", ®istername}, {"names", &queuerawtransaction}, {"names", &dequeuetransaction}, {"names", &listqueuedtransactions}, diff --git a/src/wallet/rpc/walletnames.cpp b/src/wallet/rpc/walletnames.cpp index 6f70fbd4b5..526811c47b 100644 --- a/src/wallet/rpc/walletnames.cpp +++ b/src/wallet/rpc/walletnames.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -363,6 +364,66 @@ saltMatchesHash(const valtype& name, const valtype& rand, const valtype& expecte return (Hash160(toHash) == uint160(expectedHash)); } +/** + * Scan the wallet for a name_new commitment matching the given name. + * This is the core logic shared between name_firstupdate and registername. + * @param pwallet The wallet to scan + * @param name The name to find a commitment for + * @param[out] rand The salt value (set only if found) + * @param[out] prevTxid The txid of the matching name_new transaction + * @return True if a matching name_new output was found + */ +bool +findNameNewCommitment(CWallet* const pwallet, const valtype& name, + valtype& rand, Txid& prevTxid) +{ + AssertLockHeld(pwallet->cs_wallet); + + for (const auto& item : pwallet->mapWallet) + { + const CWalletTx& tx = item.second; + if (!tx.tx->IsNamecoin()) + continue; + + CScript output; + CNameScript nameOp; + bool found = false; + for (const CTxOut& curOutput : tx.tx->vout) + { + const CNameScript cur(curOutput.scriptPubKey); + if (!cur.isNameOp()) + continue; + if (cur.getNameOp() != OP_NAME_NEW) + continue; + if (found) + { + LogDebug(BCLog::NAMES, "%s: wallet contains tx with multiple name outputs", __func__); + continue; + } + nameOp = cur; + found = true; + output = curOutput.scriptPubKey; + } + + if (!found) + continue; + + // Try to derive the salt for this name using the wallet's key + if (!getNameSalt(pwallet, name, output, rand)) + continue; + + // Verify the salt matches the hash + if (!saltMatchesHash(name, rand, nameOp.getOpHash())) + continue; + + // Found a valid commitment + prevTxid = tx.GetHash(); + return true; + } + + return false; +} + } // anonymous namespace /* ************************************************************************** */ @@ -611,55 +672,53 @@ name_firstupdate () } else { - // Code slightly duplicates name_scan, but not enough to be able to refactor. /* Make sure the results are valid at least up to the most recent block the user could have gotten from another RPC command prior to now. */ pwallet->BlockUntilSyncedToCurrentChain (); LOCK2 (pwallet->cs_wallet, cs_main); - for (const auto& item : pwallet->mapWallet) + if (!fixedRand) { - const CWalletTx& tx = item.second; - if (!tx.tx->IsNamecoin ()) - continue; - - CScript output; - CNameScript nameOp; - bool found = false; - for (CTxOut curOutput : tx.tx->vout) + /* Use the shared helper to scan for a matching name_new commitment. */ + findNameNewCommitment (pwallet, name, rand, prevTxid); + } + else + { + /* fixedRand but not fixedTxid: scan for a name_new that matches the + caller-supplied rand. */ + for (const auto& item : pwallet->mapWallet) { - CScript curScript = curOutput.scriptPubKey; - const CNameScript cur(curScript); - if (!cur.isNameOp ()) + const CWalletTx& tx = item.second; + if (!tx.tx->IsNamecoin ()) continue; - if (cur.getNameOp () != OP_NAME_NEW) - continue; - if (found) { - LogDebug (BCLog::NAMES, "%s: wallet contains tx with multiple name outputs", __func__); - continue; - } - nameOp = cur; - found = true; - output = curScript; - } - if (!found) - continue; // no name outputs found + CNameScript nameOp; + bool found = false; + for (const CTxOut& curOutput : tx.tx->vout) + { + const CNameScript cur(curOutput.scriptPubKey); + if (!cur.isNameOp ()) + continue; + if (cur.getNameOp () != OP_NAME_NEW) + continue; + if (found) { + LogDebug (BCLog::NAMES, "%s: wallet contains tx with multiple name outputs", __func__); + continue; + } + nameOp = cur; + found = true; + } - if (!fixedRand) - { - if (!getNameSalt (pwallet, name, output, rand)) // we don't have the private key for that output + if (!found) continue; - } - if (!saltMatchesHash (name, rand, nameOp.getOpHash ())) - continue; - - // found it - prevTxid = tx.GetHash (); + if (!saltMatchesHash (name, rand, nameOp.getOpHash ())) + continue; - break; // if there be more than one match, the behavior is undefined + prevTxid = tx.GetHash (); + break; + } } } @@ -992,6 +1051,174 @@ listqueuedtransactions () }; } +/* ************************************************************************** */ +/* registername: Auto-register a name with automatic name_new */ +/* ************************************************************************** */ + +RPCHelpMan +registername () +{ + NameOptionsHelp optHelp; + optHelp + .withNameEncoding () + .withValueEncoding () + .withWriteOptions (); + + return RPCHelpMan ("registername", + "\nRegisters a name atomically. Performs name_new and name_firstupdate" + "\nwith the firstupdate broadcast handled by the wallet's automatic" + "\nrebroadcast mechanism if the name_new has not yet matured." + + HELP_REQUIRING_PASSPHRASE, + { + {"name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name to register"}, + {"value", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Value for the name"}, + optHelp.buildRpcArg (), + }, + RPCResult {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "txid", "the txid of the name_new transaction"}, + {RPCResult::Type::STR_HEX, "rand", "the random value used"}, + },}, + RPCExamples { + HelpExampleCli ("registername", "\"myname\"") + + HelpExampleCli ("registername", "\"myname\" \"my-value\"") + + HelpExampleRpc ("registername", "\"myname\", \"my-value\"") + }, + [&] (const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest (request); + if (!wallet) + return NullUniValue; + CWallet* const pwallet = wallet.get (); + + const auto& node = EnsureAnyNodeContext (request); + const auto& chainman = EnsureChainman (node); + + UniValue options(UniValue::VOBJ); + if (request.params.size () >= 3) + options = request.params[2].get_obj (); + + const valtype name = DecodeNameFromRPCOrThrow (request.params[0], options); + if (name.size () > MAX_NAME_LENGTH) + throw JSONRPCError (RPC_INVALID_PARAMETER, "the name is too long"); + + /* Check if name already exists */ + { + LOCK (cs_main); + CNameData oldData; + const auto& coinsTip = chainman.ActiveChainstate ().CoinsTip (); + if (coinsTip.GetName (name, oldData) + && !oldData.isExpired (chainman.ActiveHeight ())) + throw JSONRPCError (RPC_TRANSACTION_ERROR, "this name exists already"); + } + + const bool isDefaultVal = (request.params.size () < 2 || request.params[1].isNull ()); + const valtype value = isDefaultVal ? + valtype (): + DecodeValueFromRPCOrThrow (request.params[1], options); + + if (value.size () > MAX_VALUE_LENGTH_UI) + throw JSONRPCError (RPC_INVALID_PARAMETER, "the value is too long"); + + /* Make sure the results are valid at least up to the most recent block + the user could have gotten from another RPC command prior to now. */ + pwallet->BlockUntilSyncedToCurrentChain (); + + LOCK (pwallet->cs_wallet); + + EnsureWalletIsUnlocked (*pwallet); + + DestinationAddressHelper destHelper(*pwallet); + destHelper.setOptions (options); + + const CTxDestination dest = destHelper.getDest (); + + valtype rand(20); + + /* Check if we have a matching name_new commitment in the wallet */ + Txid existingNewTxid; + bool haveCommitment = findNameNewCommitment (pwallet, name, rand, existingNewTxid); + + /* If not found, create one */ + if (!haveCommitment) + { + if (!getNameSalt (pwallet, name, GetScriptForDestination (dest), rand)) + GetRandBytes (rand); + + const CScript nameOp = CNameScript::buildNameNew (CScript (), name, rand); + + const UniValue txidVal + = SendNameOutput (request, *pwallet, dest, nameOp, nullptr, options); + destHelper.finalise (); + + const std::string randStr = HexStr (rand); + const std::string txid = txidVal.get_str (); + LogInfo ("registername: created name_new, name=%s, rand=%s, tx=%s\n", + EncodeNameForMessage (name), randStr.c_str (), txid.c_str ()); + + /* Build and commit the name_firstupdate transaction. + The name_new output is not yet in CoinsTip (it's just in the mempool + or wallet), so we look it up from the wallet transaction directly. */ + const auto newTxid = Txid::FromHex (txid); + const auto* wtx = pwallet->GetWalletTx (*newTxid); + if (!wtx) + throw JSONRPCError (RPC_TRANSACTION_ERROR, "name_new not found in wallet"); + + CTxOut prevOut; + CTxIn txIn; + for (unsigned i = 0; i < wtx->tx->vout.size (); ++i) + { + if (CNameScript::isNameScript (wtx->tx->vout[i].scriptPubKey)) + { + prevOut = wtx->tx->vout[i]; + txIn = CTxIn (COutPoint (*newTxid, i)); + break; + } + } + + if (prevOut.IsNull ()) + throw JSONRPCError (RPC_TRANSACTION_ERROR, "could not find name_new output"); + + const CScript nfuScript = CNameScript::buildNameFirstupdate (CScript (), name, value, rand); + + /* Use SendNameOutput which handles destination, coin control, + and the full send flow including CommitTransaction. */ + SendNameOutput (request, *pwallet, dest, nfuScript, &txIn, options); + + UniValue res(UniValue::VOBJ); + res.pushKV ("txid", txid); + res.pushKV ("rand", randStr); + + return res; + } + + /* We have a commitment, just build the name_firstupdate using it */ + + CTxOut prevOut; + CTxIn txIn; + { + LOCK (cs_main); + if (!getNamePrevout (chainman.ActiveChainstate (), existingNewTxid, prevOut, txIn)) + throw JSONRPCError (RPC_TRANSACTION_ERROR, "previous txid not found"); + } + + const CScript nameOp + = CNameScript::buildNameFirstupdate (CScript (), name, value, rand); + + const UniValue txidVal + = SendNameOutput (request, *pwallet, destHelper.getDest (), nameOp, + &txIn, options); + destHelper.finalise (); + + UniValue res(UniValue::VOBJ); + res.pushKV ("txid", txidVal.get_str()); + res.pushKV ("rand", HexStr(rand)); + + return res; +} + ); +} + /* ************************************************************************** */ RPCHelpMan diff --git a/test/functional/name_registername.py b/test/functional/name_registername.py new file mode 100755 index 0000000000..625818706f --- /dev/null +++ b/test/functional/name_registername.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 The Namecoin developers +# Distributed under the MIT/X11 software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# Test the registername RPC method. + +from test_framework.names import NameTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error + + +class RegisterNameTest(NameTestFramework): + + def set_test_params(self): + self.setup_name_test([[]] * 1) + self.setup_clean_chain = True + + def run_test(self): + node = self.nodes[0] + self.generate(node, 200) + + self.log.info("Test basic registername with auto name_new.") + result = node.registername("d/regtest", "hello-world") + assert "txid" in result + assert "rand" in result + txid = result["txid"] + rand_val = result["rand"] + assert_equal(len(txid), 64) + assert_equal(len(rand_val), 40) + + self.log.info("Check name_new is in mempool.") + mempool = node.getrawmempool() + assert txid in mempool + + self.log.info("Mine name_new + firstupdate into a block.") + self.generate(node, 1) + + self.log.info("Wait for maturity (12 blocks after name_new confirms).") + self.generate(node, 12) + + self.log.info("Mine the firstupdate.") + self.generate(node, 1) + + self.log.info("Verify the name is registered.") + self.checkName(0, "d/regtest", "hello-world", 30, False) + + self.log.info("Test that registering an existing name fails.") + assert_raises_rpc_error(-25, "exists already", + node.registername, "d/regtest", "new-val") + + self.log.info("Test registername with default (empty) value.") + result2 = node.registername("d/regtest2") + assert "txid" in result2 + assert "rand" in result2 + self.generate(node, 14) + self.checkName(0, "d/regtest2", "", 30, False) + + self.log.info("Test that too-long name is rejected.") + assert_raises_rpc_error(-8, "name is too long", + node.registername, "x" * 256) + + self.log.info("Test that too-long value is rejected.") + assert_raises_rpc_error(-8, "value is too long", + node.registername, "d/longval", "x" * 521) + + self.log.info("Test registername with existing name_new commitment.") + new_data = node.name_new("d/existing_new") + self.generate(node, 12) + + result3 = node.registername("d/existing_new", "reuse-commitment") + assert "txid" in result3 + assert "rand" in result3 + self.generate(node, 1) + self.checkName(0, "d/existing_new", "reuse-commitment", 30, False) + + self.log.info("Test return format is JSON object (not array).") + result4 = node.registername("d/format_test", "test-value") + assert isinstance(result4, dict) + assert "txid" in result4 + assert "rand" in result4 + assert isinstance(result4["txid"], str) + assert isinstance(result4["rand"], str) + + self.log.info("All tests passed!") + + +if __name__ == '__main__': + RegisterNameTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 707eeac305..d611349841 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -418,6 +418,7 @@ 'name_psbt.py', 'name_rawtx.py', 'name_registration.py', + 'name_registername.py', 'name_reorg.py', 'name_scanning.py', 'name_segwit.py',