diff --git a/src/wallet/rpcnames.cpp b/src/wallet/rpcnames.cpp index 28d1fc70b3..93230186a9 100644 --- a/src/wallet/rpcnames.cpp +++ b/src/wallet/rpcnames.cpp @@ -33,8 +33,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -355,10 +357,309 @@ saltMatchesHash(const valtype& name, const valtype& rand, const valtype& expecte return (Hash160(toHash) == uint160(expectedHash)); } +bool existsName(const valtype& name, const ChainstateManager& chainman) +{ + LOCK(cs_main); + const auto& coinsTip = chainman.ActiveChainstate().CoinsTip(); + CNameData oldData; + return (coinsTip.GetName(name, oldData) && !oldData.isExpired(chainman.ActiveHeight())); +} + +bool existsName(const std::string& name, const ChainstateManager& chainman) +{ + return existsName(valtype(name.begin(), name.end()), chainman); +} + } // anonymous namespace /* ************************************************************************** */ +RPCHelpMan +name_autoregister() +{ + NameOptionsHelp optHelp; + optHelp + .withNameEncoding() + .withWriteOptions() + .withArg("allowExisting", RPCArg::Type::BOOL, "false", + "If set, then the name_new is sent even if the name exists already") + .withArg("delegate", RPCArg::Type::BOOL, "false", + "If set, register a dd/ or idd/ name and delegate the name there. Name must be in d/ or id/ namespace."); + + return RPCHelpMan("name_autoregister", + "\nAutomatically registers the given name; performs the first half and queues the second." + + HELP_REQUIRING_PASSPHRASE, + { + {"name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name to register"}, + {"value", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "Value for the name"}, + optHelp.buildRpcArg(), + }, + RPCResult {RPCResult::Type::ARR_FIXED, "", "", + { + {RPCResult::Type::STR_HEX, "txid", "the txid, used in name_firstupdate"}, + {RPCResult::Type::STR_HEX, "rand", "random value, as in name_firstupdate"}, + }, + }, + RPCExamples { + HelpExampleCli("name_autoregister", "\"myname\"") + + HelpExampleRpc("name_autoregister", "\"myname\"") + }, + [&] (const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) + return NullUniValue; + CWallet* const pwallet = wallet.get(); + + RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VSTR, UniValue::VOBJ}); + const auto& chainman = EnsureChainman(EnsureAnyNodeContext(request)); + + // build a name_new + // broadcast it + // build a name_fu with nSequence + // queue it + // return the txid and rand from name_new, as an array + // + // if delegate=true: + // build nn1 = d/something + // build nn2 = dd/something (if already exist, something else) + // broadcast nn1,nn2 + // build nfu1 = "d/something points to dd/xxx" + // build nfu2 = "d/xxx is the value" + // queue nfu1,nfu2 + // return txid from nn1 + + UniValue options(UniValue::VOBJ); + if (request.params.size() >= 3) + options = request.params[2].get_obj(); + RPCTypeCheckObj(options, + { + {"allowExisting", UniValueType(UniValue::VBOOL)}, + {"delegate", UniValueType(UniValue::VBOOL)}, + }, + true, false); + + const valtype name = DecodeNameFromRPCOrThrow(request.params[0], options); + if (name.size() > MAX_NAME_LENGTH) + throw JSONRPCError(RPC_INVALID_PARAMETER, "the name is too long"); + + 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"); + + if (!options["allowExisting"].isTrue()) + { + LOCK(cs_main); + if (existsName(name, chainman)) + throw JSONRPCError(RPC_TRANSACTION_ERROR, "this name exists already"); + } + + // TODO: farm out to somewhere else for namespace parsing + + bool isDelegated = options["delegate"].isTrue(); + std::string delegatedName; + std::string delegatedValue; + + if (isDelegated) + { + bool isDomain; + bool isIdentity; + std::string nameStr(name.begin(), name.end()); + isDomain = nameStr.rfind("d/", 0) == 0; + isIdentity = nameStr.rfind("id/", 0) == 0; + + if (!isDomain && !isIdentity) + throw JSONRPCError(RPC_INVALID_PARAMETER, "delegation requested, but name neither d/ nor id/"); + + assert(!(isDomain && isIdentity)); + + size_t slashIdx = nameStr.find_first_of('/'); + assert(slashIdx != std::string::npos); + + std::string mainLabel = nameStr.substr(slashIdx, std::string::npos); + + std::string prefix = isDomain ? "dd" : "idd"; + + std::string suffix(""); + + // Attempt to generate name like dd/name, dd/name5, dd/name73 + do { + delegatedName = prefix + mainLabel + suffix; + valtype rand(1); + GetRandBytes(&rand[0], rand.size()); + suffix += std::string(1, '0' + (rand[0] % 10)); + } while (existsName(delegatedName, chainman) && delegatedName.size() <= MAX_NAME_LENGTH); + + // Fallback. This could happen if the base name is 254 characters, for instance + while (existsName(delegatedName, chainman) || delegatedName.size() > MAX_NAME_LENGTH) { + // Attempt to generate name like dd/f7f5fdbd + valtype rand(4); + GetRandBytes(&rand[0], rand.size()); + delegatedName = strprintf("%s/%hh02x%hh02x%hh02x%hh02x", prefix, rand[0], rand[1], rand[2], rand[3]); + // TODO: Escape properly for JSON. + } + + delegatedValue = strprintf("{\"import\":\"%s\"}", delegatedName); + } + + UniValue res(UniValue::VARR); + + /* 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(); + + auto issue_nn = [&] (const valtype name, bool push) { + LOCK(pwallet->cs_wallet); + EnsureWalletIsUnlocked(*pwallet); + + DestinationAddressHelper destHelper(*pwallet); + destHelper.setOptions(options); + + const CScript output = destHelper.getScript(); + + valtype rand(20); + if (!getNameSalt(pwallet, name, output, rand)) + GetRandBytes(&rand[0], rand.size()); + + const CScript newScript + = CNameScript::buildNameNew(output, name, rand); + + const UniValue txidVal + = SendNameOutput(request, *pwallet, newScript, nullptr, options); + // TODO: Refactor away SendNameOutput and move the locking here. + destHelper.finalise(); + + const std::string randStr = HexStr(rand); + const std::string txid = txidVal.get_str(); + LogPrintf("name_new: name=%s, rand=%s, tx=%s\n", + EncodeNameForMessage(name), randStr.c_str(), txid.c_str()); + + if (push) { + res.push_back(txid); + res.push_back(randStr); + } + + return std::make_pair(uint256S(txid), rand); + }; + + auto locktxid = [&] (const uint256 txid) { + LOCK(pwallet->cs_wallet); + + const CWalletTx* wtx = pwallet->GetWalletTx(txid); + CTxIn txIn; + + for (auto& in : wtx->tx->vin) { + pwallet->LockCoin(in.prevout); + } + for (auto& in : wtx->tx->vin) { + CWalletTx &coin = pwallet->mapWallet.at(in.prevout.hash); + coin.MarkDirty(); + pwallet->NotifyTransactionChanged(coin.GetHash(), CT_UPDATED); + } + + pwallet->CommitTransaction(wtx->tx, {}, {}); + }; + + auto queue_nfu = [&] (const valtype name, const valtype value, const auto info) { + LOCK(pwallet->cs_wallet); + const int TWELVE_PLUS_ONE = 13; + + uint256 txid = info.first; + valtype rand = info.second; + + const CWalletTx* wtx = pwallet->GetWalletTx(txid); + CTxIn txIn; + + for (unsigned int i = 0; i < wtx->tx->vout.size(); i++) + if (CNameScript::isNameScript(wtx->tx->vout[i].scriptPubKey)) { + txIn = CTxIn(COutPoint(txid, i), wtx->tx->vout[i].scriptPubKey, /* nSequence */ TWELVE_PLUS_ONE); + // nSequence = 13 => only broadcast name_firstupdate when name_new is mature (12 blocks) + // Note: nSequence is basically ornamental here, see comment below + } + + EnsureWalletIsUnlocked(*pwallet); + + DestinationAddressHelper destHelper(*pwallet); + destHelper.setOptions(options); + + // if delegated, use delegationValue for value, and use value in dd/ name + const CScript nameScript + = CNameScript::buildNameFirstupdate(destHelper.getScript(), name, value, rand); + + CAmount nFeeRequired = 0; + int nChangePosRet = -1; + bilingual_str error; + CTransactionRef tx; + FeeCalculation fee_calc_out; + + CCoinControl coin_control; + + std::vector recipients; + recipients.push_back({nameScript, NAME_LOCKED_AMOUNT, false}); + + const bool created_ok = CreateTransaction(*pwallet, recipients, &txIn, tx, nFeeRequired, nChangePosRet, error, coin_control, fee_calc_out, /* sign */ false); + if (!created_ok) + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, error.original); + // Not sure if this can ever happen. + + // No need to sign; sigature will be discarded. + + // Kludge: Since CreateTransaction discards nSequence of txIn, manually add it back in again. + + CMutableTransaction mtx(*tx); + + for (unsigned int i = 0; i < mtx.vin.size(); i++) + if (mtx.vin[i].prevout == txIn.prevout) + mtx.vin[i].nSequence = TWELVE_PLUS_ONE; + + // Sign it for real + bool complete = pwallet->SignTransaction(mtx); + if (!complete) + throw JSONRPCError(RPC_WALLET_ERROR, "Error signing transaction"); + // This should never happen. + + // TODO: Mark all inputs unspendable. + + CTransactionRef txr = MakeTransactionRef(mtx); + + pwallet->AddToWallet(txr, /* confirm */ {}, /* update_wtx */ nullptr, /* fFlushOnClose */ true); + // If the transaction is not added to the wallet, the inputs will continue to + // be considered spendable, causing us to double-spend the most preferable input if delegating. + + const bool queued_ok = pwallet->WriteQueuedTransaction(mtx.GetHash(), mtx); + if (!queued_ok) + throw JSONRPCError(RPC_WALLET_ERROR, "Error queueing transaction"); + + destHelper.finalise(); + + return std::make_pair(txr->GetHash(), nullptr); + }; + + auto info = issue_nn(name, true); + locktxid(info.first); + if (isDelegated) { + auto info2 = issue_nn(valtype(delegatedName.begin(), delegatedName.end()), false); + locktxid(info2.first); + auto ia = queue_nfu(name, valtype(delegatedValue.begin(), delegatedValue.end()), info); + locktxid(ia.first); + auto ib = queue_nfu(valtype(delegatedName.begin(), delegatedName.end()), value, info2); + locktxid(ib.first); + } + else { + queue_nfu(name, value, info); + } + + return res; +} + ); +} + +/* ************************************************************************** */ + RPCHelpMan name_new () { @@ -409,15 +710,9 @@ name_new () if (name.size () > MAX_NAME_LENGTH) throw JSONRPCError (RPC_INVALID_PARAMETER, "the name is too long"); - if (!options["allowExisting"].isTrue ()) - { - 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"); - } + if (!options["allowExisting"].isTrue () && + existsName (name, chainman)) + throw JSONRPCError (RPC_TRANSACTION_ERROR, "this name exists already"); /* 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. */ @@ -585,16 +880,10 @@ name_firstupdate () "this name is already being registered"); } - if (request.params.size () < 6 || !request.params[5].get_bool ()) - { - 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 is already active"); - } + if ((request.params.size () < 6 || !request.params[5].get_bool ()) && + existsName (name, chainman)) + throw JSONRPCError (RPC_TRANSACTION_ERROR, + "this name is already active"); uint256 prevTxid = uint256::ZERO; // if it can't find a txid, force an error if (fixedTxid) @@ -798,12 +1087,12 @@ name_update () { LOCK (cs_main); - CNameData oldData; - const auto& coinsTip = chainman.ActiveChainstate ().CoinsTip (); - if (!coinsTip.GetName (name, oldData) - || oldData.isExpired (chainman.ActiveHeight ())) + if (!existsName (name, chainman)) throw JSONRPCError (RPC_TRANSACTION_ERROR, "this name can not be updated"); + CNameData oldData; + const auto& coinsTip = chainman.ActiveChainstate ().CoinsTip (); + coinsTip.GetName (name, oldData); if (isDefaultVal) value = oldData.getValue(); outp = oldData.getUpdateOutpoint (); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 00b46534d2..f73c41d98c 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5054,6 +5054,7 @@ RPCHelpMan importdescriptors(); RPCHelpMan listdescriptors(); extern RPCHelpMan name_list(); // in rpcnames.cpp +extern RPCHelpMan name_autoregister(); extern RPCHelpMan name_new(); extern RPCHelpMan name_firstupdate(); extern RPCHelpMan name_update(); @@ -5139,6 +5140,7 @@ static const CRPCCommand commands[] = // Name-related wallet calls. { "names", &name_list, }, + { "names", &name_autoregister, }, { "names", &name_new, }, { "names", &name_firstupdate, }, { "names", &name_update, }, diff --git a/test/functional/name_autoregister.py b/test/functional/name_autoregister.py new file mode 100755 index 0000000000..839ad87844 --- /dev/null +++ b/test/functional/name_autoregister.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# Licensed under CC0 (Public domain) + +# Test that name_autoregister works + +from test_framework.names import NameTestFramework +from test_framework.util import * + +class NameAutoregisterTest(NameTestFramework): + + def set_test_params(self): + self.setup_name_test ([[]] * 1) + self.setup_clean_chain = True + + def run_test(self): + node = self.nodes[0] + node.generate(200) + + self.log.info("Register a name.") + node.name_autoregister("d/name", "value") + assert(len(node.listqueuedtransactions()) == 1) + self.log.info("Queue contains 1 transaction.") + self.log.info("Wait 12 blocks.") + node.generate(12) + assert(len(node.listqueuedtransactions()) == 1) + node.generate(1) + assert(len(node.listqueuedtransactions()) == 0) + self.log.info("Queue is empty.") + self.log.info("Wait 1 more block so transaction can get mined.") + node.generate(1) + self.checkNameData(node.name_show("d/name"), "d/name", "value", 30, False) + self.log.info("Name is registered.") + + self.log.info("Check delegation: normal case.") + node.name_autoregister("d/delegated", "value", {"delegate": True}) + assert(len(node.listqueuedtransactions()) == 2) + node.generate(12) + assert(len(node.listqueuedtransactions()) == 2) + node.generate(1) + assert(len(node.listqueuedtransactions()) != 2) + assert(len(node.listqueuedtransactions()) == 0) + node.generate(1) + self.checkNameData(node.name_show("d/delegated"), "d/delegated", '{"import":"dd/delegated"}', 30, False) + self.checkNameData(node.name_show("dd/delegated"), "dd/delegated", 'value', 30, False) + + self.log.info("Check delegation: appending digits.") + node.name_autoregister("dd/delegated2", "value") + node.generate(14) + node.generate(1) # hack + node.name_autoregister("d/delegated2", "value", {"delegate": True}) + node.generate(14) + node.generate(1) + self.log.info("Value is: " + node.name_show("d/delegated2")['value']) + assert(node.name_show("d/delegated2")['value'] != '{"import":"dd/delegated2"}') + +if __name__ == '__main__': + NameAutoregisterTest ().main ()