From ab1ed1be9c17f317a43da741abde42b06efbeda7 Mon Sep 17 00:00:00 2001 From: Tyler Hess Date: Sat, 30 May 2026 15:51:32 -0600 Subject: [PATCH 1/8] Fix build with modern boost::filesystem Replace APIs removed in newer Boost versions: - path::is_complete() -> path::is_absolute() - fs::basename()/fs::extension() -> path::filename() - fs::copy_option::overwrite_if_exists -> fs::copy_options::overwrite_existing Co-Authored-By: Claude Opus 4.8 --- src/rpc/protocol.cpp | 2 +- src/util.cpp | 4 ++-- src/wallet/db.cpp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rpc/protocol.cpp b/src/rpc/protocol.cpp index 6db357c2a0..7d517e3bb3 100644 --- a/src/rpc/protocol.cpp +++ b/src/rpc/protocol.cpp @@ -75,7 +75,7 @@ static fs::path GetAuthCookieFile(bool temp=false) arg += ".tmp"; } fs::path path(arg); - if (!path.is_complete()) path = GetDataDir() / path; + if (!path.is_absolute()) path = GetDataDir() / path; return path; } diff --git a/src/util.cpp b/src/util.cpp index 7542cc2d75..a5ed1ec0c3 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -620,7 +620,7 @@ void ClearDatadirCache() fs::path GetConfigFile(const std::string &confPath) { fs::path pathConfigFile(confPath); - if (!pathConfigFile.is_complete()) + if (!pathConfigFile.is_absolute()) pathConfigFile = GetDataDir(false) / pathConfigFile; return pathConfigFile; @@ -657,7 +657,7 @@ void ArgsManager::ReadConfigFile(const std::string &confPath) fs::path GetPidFile() { fs::path pathPidFile(gArgs.GetArg("-pid", RAVEN_PID_FILENAME)); - if (!pathPidFile.is_complete()) pathPidFile = GetDataDir() / pathPidFile; + if (!pathPidFile.is_absolute()) pathPidFile = GetDataDir() / pathPidFile; return pathPidFile; } diff --git a/src/wallet/db.cpp b/src/wallet/db.cpp index 26e0b1ca1e..fd37c2747f 100644 --- a/src/wallet/db.cpp +++ b/src/wallet/db.cpp @@ -267,7 +267,7 @@ bool CDB::VerifyEnvironment(const std::string& walletFile, const fs::path& dataD LogPrintf("Using wallet %s\n", walletFile); // Wallet file must be a plain filename without a directory - if (walletFile != fs::basename(walletFile) + fs::extension(walletFile)) + if (walletFile != fs::path(walletFile).filename().string()) { errorStr = strprintf(_("Wallet %s resides outside data directory %s"), walletFile, dataDir.string()); return false; @@ -706,7 +706,7 @@ bool CWalletDBWrapper::Backup(const std::string& strDest) pathDest /= strFile; try { - fs::copy_file(pathSrc, pathDest, fs::copy_option::overwrite_if_exists); + fs::copy_file(pathSrc, pathDest, fs::copy_options::overwrite_existing); LogPrintf("copied %s to %s\n", strFile, pathDest.string()); return true; } catch (const fs::filesystem_error& e) { From b94002967c5bb7c2e89c75406a49d1c1dfbfa6b7 Mon Sep 17 00:00:00 2001 From: Tyler Hess Date: Sat, 30 May 2026 15:51:32 -0600 Subject: [PATCH 2/8] Add P2AH (pay-to-asset-hash) consensus, script, and wallet support P2AH is a new output type whose spending authorization is the movement of one or more asset owner tokens (admin assets) through the spending transaction, rather than an ECDSA signature. This generalizes the existing sub-asset issuance pattern (owner token must be present in inputs and outputs) into a spending condition for arbitrary UTXOs. - New 25-byte base script: OP_DUP OP_HASH160 OP_EQUAL OP_NIP where preimage = serialized (m, sorted owner asset names). Spending reveals the preimage in the scriptSig; consensus requires >= m of the named owner tokens to move through the tx. Asset transfer data can be appended to the base script exactly like P2PKH, so P2AH outputs can hold assets too. - New address type (CAssetAuthID / TX_ASSET_AUTH) with base58 prefixes: mainnet 'H', testnet/regtest 'J'. - Consensus check CheckTxAssetAuthInputs with iterative authorization, enabling chains (key -> moves A! -> authorizes B! -> spends P2AH(B!)) while rejecting authorization cycles. - SCRIPT_VERIFY_REQUIRE_SIGHASH_ALL: txs spending P2AH inputs require all signatures to be SIGHASH_ALL so outputs can't be rewritten by miners. - BIP9 deployment DEPLOYMENT_P2AH (bit 11): regtest/testnet only; mainnet start time set far in the future. - Wallet/keystore storage for P2AH preimages (assetauthpre wallet records). - Policy: P2AH outputs/inputs nonstandard until deployment activates. Co-Authored-By: Claude Opus 4.8 --- src/assets/assets.cpp | 247 ++++++++++++++++++++++++++++++++++++ src/assets/assets.h | 23 ++++ src/assets/assettypes.h | 52 ++++++++ src/base58.cpp | 16 ++- src/base58.h | 1 + src/chainparams.cpp | 22 ++++ src/chainparams.h | 1 + src/consensus/consensus.h | 1 + src/consensus/params.h | 3 +- src/consensus/tx_verify.cpp | 28 ++++ src/keystore.cpp | 28 ++++ src/keystore.h | 11 ++ src/policy/policy.cpp | 20 +++ src/rpc/blockchain.cpp | 1 + src/rpc/misc.cpp | 26 ++++ src/script/interpreter.cpp | 7 + src/script/interpreter.h | 9 ++ src/script/ismine.cpp | 8 ++ src/script/script.cpp | 26 ++++ src/script/script.h | 4 + src/script/sign.cpp | 17 +++ src/script/standard.cpp | 36 +++++- src/script/standard.h | 14 +- src/validation.cpp | 27 ++++ src/validation.h | 3 + src/versionbits.cpp | 6 +- src/wallet/rpcwallet.cpp | 5 + src/wallet/wallet.cpp | 23 ++++ src/wallet/wallet.h | 5 + src/wallet/walletdb.cpp | 17 +++ src/wallet/walletdb.h | 2 + 31 files changed, 684 insertions(+), 5 deletions(-) diff --git a/src/assets/assets.cpp b/src/assets/assets.cpp index 495624968b..ae058ab183 100644 --- a/src/assets/assets.cpp +++ b/src/assets/assets.cpp @@ -1629,6 +1629,253 @@ void CAssetTransfer::ConstructTransaction(CScript& script) const script << OP_RVN_ASSET << ToByteVector(vchMessage) << OP_DROP; } +CAssetAuthPreimage::CAssetAuthPreimage(const uint8_t& nRequired, const std::vector& vOwnerAssetNames) +{ + SetNull(); + this->nRequired = nRequired; + this->vOwnerAssetNames = vOwnerAssetNames; +} + +bool CAssetAuthPreimage::IsValid(std::string& strError) const +{ + strError = ""; + + if (nRequired < 1) { + strError = "Invalid parameter: required number of owner assets must be at least 1"; + return false; + } + + if (vOwnerAssetNames.empty()) { + strError = "Invalid parameter: list of owner asset names can't be empty"; + return false; + } + + if (vOwnerAssetNames.size() > MAX_ASSET_AUTH_NAMES) { + strError = strprintf("Invalid parameter: list of owner asset names can't contain more than %d names", MAX_ASSET_AUTH_NAMES); + return false; + } + + if (nRequired > vOwnerAssetNames.size()) { + strError = "Invalid parameter: required number of owner assets can't be larger than the number of owner asset names"; + return false; + } + + // Names must be valid owner asset names, sorted ascending and unique so that + // a given set of names always serializes to the same preimage and hash + for (size_t i = 0; i < vOwnerAssetNames.size(); i++) { + if (!IsAssetNameAnOwner(vOwnerAssetNames[i])) { + strError = strprintf("Invalid parameter: %s is not a valid owner asset name", vOwnerAssetNames[i]); + return false; + } + + if (i > 0) { + if (vOwnerAssetNames[i] == vOwnerAssetNames[i - 1]) { + strError = strprintf("Invalid parameter: duplicate owner asset name %s", vOwnerAssetNames[i]); + return false; + } + if (vOwnerAssetNames[i] < vOwnerAssetNames[i - 1]) { + strError = "Invalid parameter: owner asset names must be sorted in ascending order"; + return false; + } + } + } + + // The preimage must fit in a single scriptSig push + CDataStream ssPreimage(SER_NETWORK, PROTOCOL_VERSION); + ssPreimage << *this; + if (ssPreimage.size() > MAX_SCRIPT_ELEMENT_SIZE) { + strError = strprintf("Invalid parameter: serialized preimage is larger than the max script element size of %d bytes", MAX_SCRIPT_ELEMENT_SIZE); + return false; + } + + return true; +} + +uint160 CAssetAuthPreimage::GetHash() const +{ + CDataStream ssPreimage(SER_NETWORK, PROTOCOL_VERSION); + ssPreimage << *this; + return Hash160(ssPreimage.begin(), ssPreimage.end()); +} + +void CAssetAuthPreimage::ConstructTransaction(CScript& script) const +{ + script.clear(); + uint160 hash = GetHash(); + script << OP_DUP << OP_HASH160 << ToByteVector(hash) << OP_EQUAL << OP_NIP; +} + +bool AssetAuthPreimageFromScriptSig(const CScript& scriptSig, CAssetAuthPreimage& preimage) +{ + // The scriptSig of a P2AH input must be a single push of the serialized preimage + opcodetype opcode; + std::vector vchPreimage; + CScript::const_iterator pc = scriptSig.begin(); + if (!scriptSig.GetOp(pc, opcode, vchPreimage)) + return false; + + if (opcode > OP_PUSHDATA4 || vchPreimage.empty()) + return false; + + if (pc != scriptSig.end()) // Must be exactly one push + return false; + + CDataStream ssPreimage(vchPreimage, SER_NETWORK, PROTOCOL_VERSION); + try { + ssPreimage >> preimage; + } catch(std::exception& e) { + return false; + } + + if (!ssPreimage.empty()) // No trailing data allowed + return false; + + return true; +} + +bool AssetAuthHashFromScript(const CScript& scriptPubKey, uint160& hashRet) +{ + if (!scriptPubKey.IsAssetAuthScript()) + return false; + + std::vector vchHash(scriptPubKey.begin() + 3, scriptPubKey.begin() + 23); + hashRet = uint160(vchHash); + return true; +} + +bool CheckTxAssetAuthInputs(const CTransaction& tx, const CCoinsViewCache& inputs, std::string& strError, std::vector* vInfoRet) +{ + strError = ""; + + // Pass 1: classify the inputs. + // - P2AH inputs: parse and verify the revealed preimage, add to the pending list + // - Non-P2AH inputs that hold owner assets: these are authorization roots. They are + // protected by their own scripts (signatures), and the asset input/output balance + // rules guarantee that any owner asset present in the inputs also appears in the + // outputs (it "moves" through the transaction) + struct PendingInput { + size_t nIndex; + CAssetAuthPreimage preimage; + std::string strOwnerAssetHeld; // owner asset held at this P2AH output, if any + bool fAuthorized; + }; + + std::vector vPending; + std::set setValidAuthorizers; + + for (size_t i = 0; i < tx.vin.size(); i++) { + const COutPoint& prevout = tx.vin[i].prevout; + const Coin& coin = inputs.AccessCoin(prevout); + if (coin.IsSpent()) { + strError = "bad-txns-assetauth-inputs-missing-or-spent"; + return false; + } + + // Get the owner asset held at this input, if any + std::string strAssetHeld = ""; + if (coin.IsAsset()) { + std::string strName; + CAmount nAmount; + if (GetAssetInfoFromScript(coin.out.scriptPubKey, strName, nAmount)) { + if (IsAssetNameAnOwner(strName)) + strAssetHeld = strName; + } + } + + if (coin.out.scriptPubKey.IsAssetAuthScript()) { + PendingInput pending; + pending.nIndex = i; + pending.fAuthorized = false; + pending.strOwnerAssetHeld = strAssetHeld; + + // The scriptSig must reveal a preimage that is valid and hashes to the committed value + if (!AssetAuthPreimageFromScriptSig(tx.vin[i].scriptSig, pending.preimage)) { + strError = "bad-txns-assetauth-bad-preimage"; + return false; + } + + std::string strPreimageError; + if (!pending.preimage.IsValid(strPreimageError)) { + strError = "bad-txns-assetauth-bad-preimage"; + return false; + } + + uint160 hashCommitted; + if (!AssetAuthHashFromScript(coin.out.scriptPubKey, hashCommitted)) { + strError = "bad-txns-assetauth-bad-script"; + return false; + } + + if (pending.preimage.GetHash() != hashCommitted) { + strError = "bad-txns-assetauth-hash-mismatch"; + return false; + } + + vPending.push_back(pending); + } else { + // Owner assets entering the transaction from key-protected (non-P2AH) inputs + // are authorization roots + if (!strAssetHeld.empty()) + setValidAuthorizers.insert(strAssetHeld); + } + } + + if (vPending.empty()) { + return true; + } + + // Pass 2: iteratively authorize P2AH inputs. An owner asset held at a P2AH output + // only becomes a valid authorizer once that P2AH input is itself authorized. This + // implements chaining (key -> moves A! -> authorizes moving B! -> authorizes spending + // P2AH(B!) outputs) and rejects authorization cycles, because a cycle has no + // key-protected root and can never make progress + bool fProgress = true; + size_t nAuthorized = 0; + while (fProgress && nAuthorized < vPending.size()) { + fProgress = false; + for (auto& pending : vPending) { + if (pending.fAuthorized) + continue; + + uint8_t nFound = 0; + for (const auto& name : pending.preimage.vOwnerAssetNames) { + if (setValidAuthorizers.count(name)) + nFound++; + } + + if (nFound >= pending.preimage.nRequired) { + pending.fAuthorized = true; + nAuthorized++; + fProgress = true; + // The owner asset held at this P2AH output can now authorize others + if (!pending.strOwnerAssetHeld.empty()) + setValidAuthorizers.insert(pending.strOwnerAssetHeld); + } + } + } + + if (vInfoRet) { + for (const auto& pending : vPending) { + CAssetAuthInputInfo info; + info.nIndex = pending.nIndex; + info.preimage = pending.preimage; + info.fAuthorized = pending.fAuthorized; + for (const auto& name : pending.preimage.vOwnerAssetNames) { + if (setValidAuthorizers.count(name)) + info.vAuthorizingAssets.push_back(name); + } + vInfoRet->push_back(info); + } + } + + if (nAuthorized < vPending.size()) { + strError = "bad-txns-assetauth-insufficient-owner-movement"; + return false; + } + + return true; +} + CReissueAsset::CReissueAsset(const std::string &strAssetName, const CAmount &nAmount, const int &nUnits, const int &nReissuable, const std::string &strIPFSHash) { diff --git a/src/assets/assets.h b/src/assets/assets.h index 8eeb58244b..c430b5e393 100644 --- a/src/assets/assets.h +++ b/src/assets/assets.h @@ -55,6 +55,7 @@ class CDataStream; class CTransaction; class CTxOut; class Coin; +class CCoinsViewCache; class CWallet; class CReserveKey; class CWalletTx; @@ -454,6 +455,28 @@ bool AssetNullDataFromScript(const CScript& scriptPubKey, CNullAssetTxData& asse bool AssetNullVerifierDataFromScript(const CScript& scriptPubKey, CNullAssetTxVerifierString& verifierData); bool GlobalAssetNullDataFromScript(const CScript& scriptPubKey, CNullAssetTxData& assetData); +//! Pay-to-asset-hash (P2AH) helpers +//! Parse the preimage revealed in a P2AH input's scriptSig (must be a single push) +bool AssetAuthPreimageFromScriptSig(const CScript& scriptSig, CAssetAuthPreimage& preimage); +//! Extract the committed preimage hash from a P2AH scriptPubKey +bool AssetAuthHashFromScript(const CScript& scriptPubKey, uint160& hashRet); + +//! Per-input result details from CheckTxAssetAuthInputs, used by RPC to report why a +//! P2AH spend is or isn't authorized +struct CAssetAuthInputInfo { + size_t nIndex; // index into tx.vin + CAssetAuthPreimage preimage; // preimage revealed in the scriptSig + bool fAuthorized; // whether enough committed owner assets move in this tx + std::vector vAuthorizingAssets; // the committed owner assets that do move +}; + +//! Consensus check for P2AH inputs: every P2AH input being spent must reveal a preimage +//! matching its committed hash, and at least preimage.nRequired of the committed owner +//! assets must move through the transaction (be present in inputs, from key-protected +//! inputs or from already-authorized P2AH inputs). Used by both consensus validation +//! (Consensus::CheckTxAssets) and the verifyassetauth RPC so they can never disagree +bool CheckTxAssetAuthInputs(const CTransaction& tx, const CCoinsViewCache& inputs, std::string& strError, std::vector* vInfoRet = nullptr); + //! Check to make sure the script contains the burn transaction bool CheckIssueBurnTx(const CTxOut& txOut, const AssetType& type, const int numberIssued); bool CheckIssueBurnTx(const CTxOut& txOut, const AssetType& type); diff --git a/src/assets/assettypes.h b/src/assets/assettypes.h index 1567ae03c0..c983866b40 100644 --- a/src/assets/assettypes.h +++ b/src/assets/assettypes.h @@ -304,6 +304,58 @@ class CNullAssetTxData { void ConstructGlobalRestrictionTransaction(CScript &script) const; }; +/** The maximum number of owner asset names that a P2AH preimage may commit to */ +#define MAX_ASSET_AUTH_NAMES 15 + +/** + * Pay-to-asset-hash (P2AH) preimage. + * + * Commits to an m-of-n set of owner asset names ("ASSET!"). A P2AH output's + * script contains Hash160 of the serialization of this object. To spend a + * P2AH output, the spending transaction reveals this preimage in the input's + * scriptSig and must transfer (move through inputs and outputs) at least + * nRequired of the named owner assets. + */ +class CAssetAuthPreimage +{ +public: + uint8_t nRequired; // m: how many of the named owner assets must move + std::vector vOwnerAssetNames; // n: owner asset names, sorted ascending and unique + + CAssetAuthPreimage() + { + SetNull(); + } + + CAssetAuthPreimage(const uint8_t& nRequired, const std::vector& vOwnerAssetNames); + + void SetNull() + { + nRequired = 0; + vOwnerAssetNames.clear(); + } + + bool IsNull() const + { + return nRequired == 0 && vOwnerAssetNames.empty(); + } + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(nRequired); + READWRITE(vOwnerAssetNames); + } + + bool IsValid(std::string& strError) const; + /** Hash160 of the serialization of this preimage. Only call on valid preimages */ + uint160 GetHash() const; + /** Construct the 25 byte P2AH base scriptPubKey that commits to this preimage */ + void ConstructTransaction(CScript& script) const; +}; + class CNullAssetTxVerifierString { public: diff --git a/src/base58.cpp b/src/base58.cpp index 6dd39dd175..bce2ce3828 100644 --- a/src/base58.cpp +++ b/src/base58.cpp @@ -224,6 +224,7 @@ class CRavenAddressVisitor : public boost::static_visitor bool operator()(const CKeyID& id) const { return addr->Set(id); } bool operator()(const CScriptID& id) const { return addr->Set(id); } + bool operator()(const CAssetAuthID& id) const { return addr->Set(id); } bool operator()(const CNoDestination& no) const { return false; } }; @@ -241,6 +242,12 @@ bool CRavenAddress::Set(const CScriptID& id) return true; } +bool CRavenAddress::Set(const CAssetAuthID& id) +{ + SetData(GetParams().Base58Prefix(CChainParams::ASSET_AUTH_ADDRESS), &id, 20); + return true; +} + bool CRavenAddress::Set(const CTxDestination& dest) { return boost::apply_visitor(CRavenAddressVisitor(this), dest); @@ -255,7 +262,8 @@ bool CRavenAddress::IsValid(const CChainParams& params) const { bool fCorrectSize = vchData.size() == 20; bool fKnownVersion = vchVersion == params.Base58Prefix(CChainParams::PUBKEY_ADDRESS) || - vchVersion == params.Base58Prefix(CChainParams::SCRIPT_ADDRESS); + vchVersion == params.Base58Prefix(CChainParams::SCRIPT_ADDRESS) || + vchVersion == params.Base58Prefix(CChainParams::ASSET_AUTH_ADDRESS); return fCorrectSize && fKnownVersion; } @@ -269,6 +277,8 @@ CTxDestination CRavenAddress::Get() const return CKeyID(id); else if (vchVersion == GetParams().Base58Prefix(CChainParams::SCRIPT_ADDRESS)) return CScriptID(id); + else if (vchVersion == GetParams().Base58Prefix(CChainParams::ASSET_AUTH_ADDRESS)) + return CAssetAuthID(id); else return CNoDestination(); } @@ -285,6 +295,10 @@ bool CRavenAddress::GetIndexKey(uint160& hashBytes, int& type) const memcpy(&hashBytes, &vchData[0], 20); type = 2; return true; + } else if (vchVersion == GetParams().Base58Prefix(CChainParams::ASSET_AUTH_ADDRESS)) { + memcpy(&hashBytes, &vchData[0], 20); + type = 3; + return true; } return false; diff --git a/src/base58.h b/src/base58.h index 8997daee43..786d0d49ca 100644 --- a/src/base58.h +++ b/src/base58.h @@ -104,6 +104,7 @@ class CRavenAddress : public CBase58Data { public: bool Set(const CKeyID &id); bool Set(const CScriptID &id); + bool Set(const CAssetAuthID &id); bool Set(const CTxDestination &dest); bool IsValid() const; bool IsValid(const CChainParams ¶ms) const; diff --git a/src/chainparams.cpp b/src/chainparams.cpp index d4a24f5785..b6f6230352 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -166,6 +166,13 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nTimeout = 1812844799; // UTC: Sat June 12 2027 23:59:59 consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nOverrideRuleChangeActivationThreshold = 1411; // Approx 70% of 2016 consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nOverrideMinerConfirmationWindow = 2016; + // P2AH (pay-to-asset-hash) is NOT scheduled for mainnet activation. + // The start time is set far in the future so the deployment can never start signalling. + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].bit = 12; + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nStartTime = 4102444800LL; // UTC: Jan 1 2100 - effectively never + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nTimeout = 4133980800LL; // UTC: Jan 1 2101 + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nOverrideRuleChangeActivationThreshold = 1714; // Approx 85% of 2016 + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nOverrideMinerConfirmationWindow = 2016; // The best chain should have at least this much work @@ -202,6 +209,7 @@ class CMainParams : public CChainParams { base58Prefixes[SECRET_KEY] = std::vector(1,128); base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x88, 0xB2, 0x1E}; base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4}; + base58Prefixes[ASSET_AUTH_ADDRESS] = std::vector(1,40); // P2AH addresses start with 'H' // Raven BIP44 cointype in mainnet is '175' nExtCoinType = 175; @@ -336,6 +344,12 @@ class CTestNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nTimeout = 1812844799; // UTC: Sat June 12 2027 23:59:59 consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nOverrideRuleChangeActivationThreshold = 1411; // Approx 70% of 2016 consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nOverrideMinerConfirmationWindow = 2016; + // P2AH (pay-to-asset-hash) deployment on testnet + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].bit = 12; + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nStartTime = 1767290400; // UTC: Thu Jan 01 2026 18:00:00 + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nTimeout = 1798826400; // UTC: Fri Jan 01 2027 18:00:00 + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nOverrideRuleChangeActivationThreshold = 1310; + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nOverrideMinerConfirmationWindow = 2016; // The best chain should have at least this much work. consensus.nMinimumChainWork = uint256S("0x000000000000000000000000000000000000000000000000000168050db560b4"); @@ -433,6 +447,7 @@ class CTestNetParams : public CChainParams { base58Prefixes[SECRET_KEY] = std::vector(1,239); base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x35, 0x87, 0xCF}; base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; + base58Prefixes[ASSET_AUTH_ADDRESS] = std::vector(1,43); // P2AH addresses start with 'J' // Raven BIP44 cointype in testnet nExtCoinType = 1; @@ -561,6 +576,12 @@ class CRegTestParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nTimeout = 999999999999ULL; consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nOverrideRuleChangeActivationThreshold = 400; consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nOverrideMinerConfirmationWindow = 500; + // P2AH (pay-to-asset-hash) deployment on regtest - always available for signalling + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].bit = 12; + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nStartTime = 0; + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nTimeout = 999999999999ULL; + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nOverrideRuleChangeActivationThreshold = 108; + consensus.vDeployments[Consensus::DEPLOYMENT_P2AH].nOverrideMinerConfirmationWindow = 144; // The best chain should have at least this much work. consensus.nMinimumChainWork = uint256S("0x00"); @@ -665,6 +686,7 @@ class CRegTestParams : public CChainParams { base58Prefixes[SECRET_KEY] = std::vector(1,239); base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x35, 0x87, 0xCF}; base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; + base58Prefixes[ASSET_AUTH_ADDRESS] = std::vector(1,43); // P2AH addresses start with 'J' // Raven BIP44 cointype in regtest nExtCoinType = 1; diff --git a/src/chainparams.h b/src/chainparams.h index ee9e028d2b..dab07559bc 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -54,6 +54,7 @@ class CChainParams SECRET_KEY, EXT_PUBLIC_KEY, EXT_SECRET_KEY, + ASSET_AUTH_ADDRESS, // Pay-to-asset-hash (P2AH) addresses MAX_BASE58_TYPES }; diff --git a/src/consensus/consensus.h b/src/consensus/consensus.h index 01900a6456..2391465719 100644 --- a/src/consensus/consensus.h +++ b/src/consensus/consensus.h @@ -40,6 +40,7 @@ UNUSED_VAR static bool fTransferScriptIsActive = false; UNUSED_VAR static bool fEnforcedValuesIsActive = false; UNUSED_VAR static bool fCheckCoinbaseAssetsIsActive = false; UNUSED_VAR static bool fCheckTransferOverflowIsActive = false; +UNUSED_VAR static bool fAssetAuthIsActive = false; unsigned int GetMaxBlockWeight(); unsigned int GetMaxBlockSerializedSize(); diff --git a/src/consensus/params.h b/src/consensus/params.h index 64c9e91431..b8bde8f078 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -21,7 +21,8 @@ enum DeploymentPos DEPLOYMENT_TRANSFER_SCRIPT_SIZE, DEPLOYMENT_ENFORCE_VALUE, DEPLOYMENT_COINBASE_ASSETS, - DEPLOYMENT_TRANSFER_OVERFLOW, // Deployment of asset transfer qty overflow check + DEPLOYMENT_TRANSFER_OVERFLOW, // Deployment of asset transfer qty overflow check + DEPLOYMENT_P2AH, // Deployment of pay-to-asset-hash (P2AH) // DEPLOYMENT_CSV, // Deployment of BIP68, BIP112, and BIP113. // DEPLOYMENT_SEGWIT, // Deployment of BIP141, BIP143, and BIP147. // NOTE: Also add new deployments to VersionBitsDeploymentInfo in versionbits.cpp diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index d5e5750658..df3c8c8b01 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -665,6 +665,28 @@ bool Consensus::CheckTxAssets(const CTransaction& tx, CValidationState& state, c } } + /** RVN START - Pay-to-asset-hash (P2AH) input authorization */ + { + bool fHasAssetAuthInput = false; + for (unsigned int i = 0; i < tx.vin.size(); ++i) { + const Coin& coin = inputs.AccessCoin(tx.vin[i].prevout); + if (coin.out.scriptPubKey.IsAssetAuthScript()) { + fHasAssetAuthInput = true; + break; + } + } + + if (fHasAssetAuthInput) { + if (!AreAssetAuthDeployed()) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-assetauth-not-active", false, "", tx.GetHash()); + + std::string strAssetAuthError; + if (!CheckTxAssetAuthInputs(tx, inputs, strAssetAuthError)) + return state.DoS(100, false, REJECT_INVALID, strAssetAuthError, false, "", tx.GetHash()); + } + } + /** RVN END */ + // Create map that stores the amount of an asset transaction output. Used to verify no assets are burned std::map totalOutputs; int index = 0; @@ -683,6 +705,12 @@ bool Consensus::CheckTxAssets(const CTransaction& tx, CValidationState& state, c if (fIsAsset && !AreAssetsDeployed()) return state.DoS(100, false, REJECT_INVALID, "bad-txns-is-asset-and-asset-not-active"); + // Reject the creation of P2AH outputs before the deployment is active. This is + // only enforced for mempool acceptance: blocks containing P2AH outputs are not + // rejected pre-activation to avoid splitting against miners who don't relay them + if (fCheckMempool && txout.scriptPubKey.IsAssetAuthScript() && !AreAssetAuthDeployed()) + return state.DoS(0, false, REJECT_NONSTANDARD, "bad-txns-assetauth-not-active", false, "", tx.GetHash()); + if (txout.scriptPubKey.IsNullAsset()) { if (!AreRestrictedAssetsDeployed()) return state.DoS(100, false, REJECT_INVALID, diff --git a/src/keystore.cpp b/src/keystore.cpp index b1895f192a..b464013148 100644 --- a/src/keystore.cpp +++ b/src/keystore.cpp @@ -65,6 +65,34 @@ bool CBasicKeyStore::GetCScript(const CScriptID &hash, CScript& redeemScriptOut) return false; } +bool CBasicKeyStore::AddAssetAuthPreimage(const std::vector& vchPreimage) +{ + if (vchPreimage.size() > MAX_SCRIPT_ELEMENT_SIZE) + return error("CBasicKeyStore::AddAssetAuthPreimage(): preimages > %i bytes are invalid", MAX_SCRIPT_ELEMENT_SIZE); + + LOCK(cs_KeyStore); + mapAssetAuthPreimages[Hash160(vchPreimage)] = vchPreimage; + return true; +} + +bool CBasicKeyStore::HaveAssetAuthPreimage(const uint160& hash) const +{ + LOCK(cs_KeyStore); + return mapAssetAuthPreimages.count(hash) > 0; +} + +bool CBasicKeyStore::GetAssetAuthPreimage(const uint160& hash, std::vector& vchPreimageOut) const +{ + LOCK(cs_KeyStore); + AssetAuthPreimageMap::const_iterator mi = mapAssetAuthPreimages.find(hash); + if (mi != mapAssetAuthPreimages.end()) + { + vchPreimageOut = (*mi).second; + return true; + } + return false; +} + static bool ExtractPubKey(const CScript &dest, CPubKey& pubKeyOut) { //TODO: Use Solver to extract this? diff --git a/src/keystore.h b/src/keystore.h index 644491a048..774f1dfa65 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -39,6 +39,11 @@ class CKeyStore virtual bool HaveCScript(const CScriptID &hash) const =0; virtual bool GetCScript(const CScriptID &hash, CScript& redeemScriptOut) const =0; + //! Support for Pay-to-asset-hash (P2AH) preimages, stored like P2SH redeem scripts + virtual bool AddAssetAuthPreimage(const std::vector& vchPreimage) =0; + virtual bool HaveAssetAuthPreimage(const uint160& hash) const =0; + virtual bool GetAssetAuthPreimage(const uint160& hash, std::vector& vchPreimageOut) const =0; + //! Support for Watch-only addresses virtual bool AddWatchOnly(const CScript &dest) =0; virtual bool RemoveWatchOnly(const CScript &dest) =0; @@ -50,6 +55,7 @@ typedef std::map KeyMap; typedef std::map WatchKeyMap; typedef std::map ScriptMap; typedef std::set WatchOnlySet; +typedef std::map > AssetAuthPreimageMap; /** Basic key store, that keeps keys in an address->secret map */ class CBasicKeyStore : public CKeyStore @@ -59,6 +65,7 @@ class CBasicKeyStore : public CKeyStore WatchKeyMap mapWatchKeys; ScriptMap mapScripts; WatchOnlySet setWatchOnly; + AssetAuthPreimageMap mapAssetAuthPreimages; uint256 nWordHash; std::vector vchWords; @@ -103,6 +110,10 @@ class CBasicKeyStore : public CKeyStore bool HaveCScript(const CScriptID &hash) const override; bool GetCScript(const CScriptID &hash, CScript& redeemScriptOut) const override; + bool AddAssetAuthPreimage(const std::vector& vchPreimage) override; + bool HaveAssetAuthPreimage(const uint160& hash) const override; + bool GetAssetAuthPreimage(const uint160& hash, std::vector& vchPreimageOut) const override; + bool AddWatchOnly(const CScript &dest) override; bool RemoveWatchOnly(const CScript &dest) override; bool HaveWatchOnly(const CScript &dest) const override; diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index 0850230af8..66c127d00a 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -80,6 +80,13 @@ bool IsStandard(const CScript& scriptPubKey, txnouttype& whichType, const bool w return false; else if (!witnessEnabled && (whichType == TX_WITNESS_V0_KEYHASH || whichType == TX_WITNESS_V0_SCRIPTHASH)) return false; + // Pay-to-asset-hash (P2AH) outputs are only standard once the deployment is active + else if (whichType == TX_ASSET_AUTH && !AreAssetAuthDeployed()) + return false; + // Asset data appended to a P2AH base script is also only standard once active + else if ((whichType == TX_TRANSFER_ASSET || whichType == TX_NEW_ASSET || whichType == TX_REISSUE_ASSET) && + scriptPubKey.IsAssetAuthScript() && !AreAssetAuthDeployed()) + return false; return whichType != TX_NONSTANDARD ; } @@ -202,6 +209,19 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs) return false; } } + else if (whichType == TX_ASSET_AUTH || + (prevScript.IsAssetAuthScript() && + (whichType == TX_TRANSFER_ASSET || whichType == TX_NEW_ASSET || whichType == TX_REISSUE_ASSET))) + { + // Pay-to-asset-hash (P2AH) inputs are only standard once the deployment is + // active, and the scriptSig must be a single push of the preimage + if (!AreAssetAuthDeployed()) + return false; + if (tx.vin[i].scriptSig.size() > MAX_SCRIPT_ELEMENT_SIZE + 3) + return false; + if (!tx.vin[i].scriptSig.IsPushOnly()) + return false; + } } return true; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 96a3246a76..d2340dc24d 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1505,6 +1505,7 @@ UniValue getblockchaininfo(const JSONRPCRequest& request) BIP9SoftForkDescPushBack(bip9_softforks, "enforce", consensusParams, Consensus::DEPLOYMENT_ENFORCE_VALUE); BIP9SoftForkDescPushBack(bip9_softforks, "coinbase", consensusParams, Consensus::DEPLOYMENT_COINBASE_ASSETS); BIP9SoftForkDescPushBack(bip9_softforks, "transfer_overflow", consensusParams, Consensus::DEPLOYMENT_TRANSFER_OVERFLOW); + BIP9SoftForkDescPushBack(bip9_softforks, "assetauth", consensusParams, Consensus::DEPLOYMENT_P2AH); obj.push_back(Pair("softforks", softforks)); obj.push_back(Pair("bip9_softforks", bip9_softforks)); diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index cde11e331a..e3330d8a18 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -164,6 +164,32 @@ class DescribeAddressVisitor : public boost::static_visitor } return obj; } + + UniValue operator()(const CAssetAuthID &assetAuthID) const { + UniValue obj(UniValue::VOBJ); + obj.push_back(Pair("isscript", false)); + obj.push_back(Pair("isassetauth", true)); + if (pwallet) { + std::vector vchPreimage; + if (pwallet->GetAssetAuthPreimage(assetAuthID, vchPreimage)) { + CAssetAuthPreimage preimage; + CDataStream ssPreimage(vchPreimage, SER_NETWORK, PROTOCOL_VERSION); + try { + ssPreimage >> preimage; + } catch (const std::exception&) { + return obj; // Stored preimage failed to deserialize; report nothing extra + } + obj.push_back(Pair("preimage", HexStr(vchPreimage))); + obj.push_back(Pair("sigsrequired", preimage.nRequired)); + UniValue a(UniValue::VARR); + for (const std::string& name : preimage.vOwnerAssetNames) { + a.push_back(name); + } + obj.push_back(Pair("owner_assets", a)); + } + } + return obj; + } }; #endif diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index eee46e8d62..9ea37498bf 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -233,6 +233,13 @@ bool CheckSignatureEncoding(const std::vector &vchSig, unsigned i { return set_error(serror, SCRIPT_ERR_SIG_HASHTYPE); } + // Pay-to-asset-hash (P2AH): transactions that spend P2AH inputs require every + // signature to commit to the whole transaction with exactly SIGHASH_ALL + else if ((flags & SCRIPT_VERIFY_REQUIRE_SIGHASH_ALL) != 0 && vchSig.size() > 0 && + vchSig[vchSig.size() - 1] != SIGHASH_ALL) + { + return set_error(serror, SCRIPT_ERR_SIG_HASHTYPE); + } return true; } diff --git a/src/script/interpreter.h b/src/script/interpreter.h index 310130cf81..3feb40a5a3 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -110,6 +110,15 @@ enum // Public keys in segregated witness scripts must be compressed // SCRIPT_VERIFY_WITNESS_PUBKEYTYPE = (1U << 15), + + // All ECDSA signatures in the transaction must use exactly SIGHASH_ALL + // (no SIGHASH_NONE, SIGHASH_SINGLE or SIGHASH_ANYONECANPAY). + // + // Used for transactions spending pay-to-asset-hash (P2AH) inputs: P2AH inputs carry + // no signature of their own, so the transaction's integrity comes entirely from the + // signatures on the other (authorizing) inputs. Those signatures must commit to the + // whole transaction, otherwise a miner could rewrite the outputs and steal P2AH value. + SCRIPT_VERIFY_REQUIRE_SIGHASH_ALL = (1U << 16), }; bool CheckSignatureEncoding(const std::vector &vchSig, unsigned int flags, ScriptError *serror); diff --git a/src/script/ismine.cpp b/src/script/ismine.cpp index 0e647c119e..063ef386b6 100644 --- a/src/script/ismine.cpp +++ b/src/script/ismine.cpp @@ -193,6 +193,14 @@ isminetype IsMine(const CKeyStore &keystore, const CScript& scriptPubKey, bool& return ISMINE_SPENDABLE; break; } + + case TX_ASSET_AUTH: { + // P2AH outputs are never ISMINE_SPENDABLE even if the wallet knows the + // preimage. They are not spendable by signatures, only by moving the + // committed owner assets, so they must never be selected as inputs by + // normal wallet coin selection. Wallets track them as watch-only. + break; + } /** RVN END*/ } diff --git a/src/script/script.cpp b/src/script/script.cpp index e7fa150520..fbaaa66bfc 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -353,6 +353,32 @@ bool CScript::IsNullAssetVerifierTxDataScript() const (*this)[1] == OP_RESERVED && (*this)[2] != OP_RESERVED); } + +bool CScript::IsPayToAssetAuthHash() const +{ + // Extra-fast test for pay-to-asset-hash (P2AH) CScripts: + // The base script is exactly 25 bytes so that asset transfer data can be + // appended after it the same way it is appended to P2PKH scripts (the + // asset parsing code expects OP_RVN_ASSET at index 25). + return (this->size() == 25 && + (*this)[0] == OP_DUP && + (*this)[1] == OP_HASH160 && + (*this)[2] == 0x14 && + (*this)[23] == OP_EQUAL && + (*this)[24] == OP_NIP); +} + +bool CScript::IsAssetAuthScript() const +{ + // A P2AH script with or without asset transfer data appended after the + // 25 byte base script + return (this->size() >= 25 && + (*this)[0] == OP_DUP && + (*this)[1] == OP_HASH160 && + (*this)[2] == 0x14 && + (*this)[23] == OP_EQUAL && + (*this)[24] == OP_NIP); +} /** RVN END */ bool CScript::IsPayToWitnessScriptHash() const diff --git a/src/script/script.h b/src/script/script.h index 18fa14005e..8eff598791 100644 --- a/src/script/script.h +++ b/src/script/script.h @@ -676,6 +676,10 @@ class CScript : public CScriptBase bool IsNullAssetTxDataScript() const; bool IsNullAssetVerifierTxDataScript() const; bool IsNullGlobalRestrictionAssetTxDataScript() const; + /** Pay-to-asset-hash (P2AH): exact 25 byte base script that commits to a hash of owner asset names */ + bool IsPayToAssetAuthHash() const; + /** Pay-to-asset-hash base script with or without asset transfer data appended after it */ + bool IsAssetAuthScript() const; /** RVN END */ /** Used for obsolete pay-to-pubkey addresses indexing. */ diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 68804c62aa..a178a27d72 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -118,6 +118,18 @@ static bool SignStep(const BaseSignatureCreator& creator, const CScript& scriptP ret.push_back(ToByteVector(vch)); } return true; + + case TX_ASSET_AUTH: { + // Pay-to-asset-hash: the "signature" is the preimage that hashes to the + // committed value. The actual authorization (owner asset movement) is + // enforced by consensus, not by this script + std::vector vchPreimage; + if (creator.KeyStore().GetAssetAuthPreimage(uint160(vSolutions[0]), vchPreimage)) { + ret.push_back(vchPreimage); + return true; + } + return false; + } /** RVN END */ case TX_PUBKEY: keyID = CPubKey(vSolutions[0]).GetID(); @@ -429,6 +441,11 @@ static Stacks CombineSignatures(const CScript& scriptPubKey, const BaseSignature if (sigs1.script.empty() || sigs1.script[0].empty()) return sigs2; return sigs1; + case TX_ASSET_AUTH: + // Preimages are bigger than placeholders or empty scripts: + if (sigs1.script.empty() || sigs1.script[0].empty()) + return sigs2; + return sigs1; default: return Stacks(); diff --git a/src/script/standard.cpp b/src/script/standard.cpp index 5b8e19766e..058a15f5b0 100644 --- a/src/script/standard.cpp +++ b/src/script/standard.cpp @@ -39,6 +39,7 @@ const char* GetTxnOutputType(txnouttype t) case TX_NEW_ASSET: return ASSET_NEW_STRING; case TX_TRANSFER_ASSET: return ASSET_TRANSFER_STRING; case TX_REISSUE_ASSET: return ASSET_REISSUE_STRING; + case TX_ASSET_AUTH: return "assetauth"; /** RVN END */ } return nullptr; @@ -72,6 +73,16 @@ bool Solver(const CScript& scriptPubKey, txnouttype& typeRet, std::vector hashBytes(scriptPubKey.begin()+3, scriptPubKey.begin()+23); + vSolutionsRet.push_back(hashBytes); + return true; + } + int nType = 0; bool fIsOwner = false; if (scriptPubKey.IsAssetScript(nType, fIsOwner)) { @@ -234,8 +245,17 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet) addressRet = CScriptID(uint160(vSolutions[0])); return true; /** RVN START */ + } else if (whichType == TX_ASSET_AUTH) { + addressRet = CAssetAuthID(uint160(vSolutions[0])); + return true; } else if (whichType == TX_NEW_ASSET || whichType == TX_REISSUE_ASSET || whichType == TX_TRANSFER_ASSET) { - addressRet = CKeyID(uint160(vSolutions[0])); + // Asset data can be appended to either a P2PKH base script or a P2AH base script. + // Check the base script type so asset balances at P2AH addresses are tracked + // under the P2AH address + if (scriptPubKey.IsAssetAuthScript()) + addressRet = CAssetAuthID(uint160(vSolutions[0])); + else + addressRet = CKeyID(uint160(vSolutions[0])); return true; } else if (whichType == TX_RESTRICTED_ASSET_DATA) { if (vSolutions.size()) { @@ -313,6 +333,14 @@ class CScriptVisitor : public boost::static_visitor *script << OP_HASH160 << ToByteVector(scriptID) << OP_EQUAL; return true; } + + bool operator()(const CAssetAuthID &assetAuthID) const { + script->clear(); + // Pay-to-asset-hash: same 25 byte layout as P2PKH so asset data can be appended, + // but ends in OP_EQUAL OP_NIP so the preimage push satisfies the script + *script << OP_DUP << OP_HASH160 << ToByteVector(assetAuthID) << OP_EQUAL << OP_NIP; + return true; + } }; } // namespace @@ -341,6 +369,12 @@ namespace *script << OP_RVN_ASSET << ToByteVector(scriptID); return true; } + + bool operator()(const CAssetAuthID &assetAuthID) const { + script->clear(); + *script << OP_RVN_ASSET << ToByteVector(assetAuthID); + return true; + } }; } // namespace diff --git a/src/script/standard.h b/src/script/standard.h index dda17270cc..5af67374f1 100644 --- a/src/script/standard.h +++ b/src/script/standard.h @@ -28,6 +28,16 @@ class CScriptID : public uint160 CScriptID(const uint160& in) : uint160(in) {} }; +/** RVN START */ +/** A reference to a P2AH asset authorization set: the Hash160 of the serialized CAssetAuthPreimage */ +class CAssetAuthID : public uint160 +{ +public: + CAssetAuthID() : uint160() {} + CAssetAuthID(const uint160& in) : uint160(in) {} +}; +/** RVN END */ + /** * Default setting for nMaxDatacarrierBytes. 80 bytes of data, +1 for OP_RETURN, * +2 for the pushdata opcodes. @@ -70,6 +80,7 @@ enum txnouttype TX_REISSUE_ASSET = 9, TX_TRANSFER_ASSET = 10, TX_RESTRICTED_ASSET_DATA = 11, //!< unspendable OP_RAVEN_ASSET script that carries data + TX_ASSET_AUTH = 12, //!< pay-to-asset-hash (P2AH): spendable by moving committed owner assets /** RVN END */ }; @@ -84,9 +95,10 @@ class CNoDestination { * * CNoDestination: no destination set * * CKeyID: TX_PUBKEYHASH destination * * CScriptID: TX_SCRIPTHASH destination + * * CAssetAuthID: TX_ASSET_AUTH (P2AH) destination * A CTxDestination is the internal data type encoded in a ravencoin address */ -typedef boost::variant CTxDestination; +typedef boost::variant CTxDestination; /** Check whether a CTxDestination is a CNoDestination. */ bool IsValidDestination(const CTxDestination& dest); diff --git a/src/validation.cpp b/src/validation.cpp index 6ce999a138..9d78eefa56 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1611,6 +1611,21 @@ bool CheckInputs(const CTransaction& tx, CValidationState &state, const CCoinsVi // correct (ie that the transaction hash which is in tx's prevouts // properly commits to the scriptPubKey in the inputs view of that // transaction). + /** RVN START - Pay-to-asset-hash (P2AH) */ + // If this transaction spends any P2AH input, every signature in the transaction + // must commit to the whole transaction with SIGHASH_ALL. P2AH inputs carry no + // signature, so the transaction is only bound by the other inputs' signatures + if (AreAssetAuthDeployed()) { + for (unsigned int i = 0; i < tx.vin.size(); i++) { + const Coin& coin = inputs.AccessCoin(tx.vin[i].prevout); + if (!coin.IsSpent() && coin.out.scriptPubKey.IsAssetAuthScript()) { + flags |= SCRIPT_VERIFY_REQUIRE_SIGHASH_ALL; + break; + } + } + } + /** RVN END */ + uint256 hashCacheEntry; // We only use the first 19 bytes of nonce to avoid a second SHA // round - giving us 19 + 32 + 4 = 55 bytes (+ 8 + 1 = 64) @@ -5830,6 +5845,18 @@ bool AreRestrictedAssetsDeployed() { return IsRip5Active(); } +bool AreAssetAuthDeployed() +{ + if (fAssetAuthIsActive) + return true; + + const ThresholdState thresholdState = VersionBitsTipState(GetParams().GetConsensus(), Consensus::DEPLOYMENT_P2AH); + if (thresholdState == THRESHOLD_ACTIVE) + fAssetAuthIsActive = true; + + return fAssetAuthIsActive; +} + bool IsDGWActive(unsigned int nBlockNumber) { return nBlockNumber >= GetParams().DGWActivationBlock(); } diff --git a/src/validation.h b/src/validation.h index 68bad0a088..ce71bb189f 100644 --- a/src/validation.h +++ b/src/validation.h @@ -611,6 +611,9 @@ bool IsRip5Active(); bool AreTransferScriptsSizeDeployed(); +//! Check if the pay-to-asset-hash (P2AH) deployment is active +bool AreAssetAuthDeployed(); + bool IsDGWActive(unsigned int nBlockNumber); bool IsMessagingActive(unsigned int nBlockNumber); bool IsRestrictedActive(unsigned int nBlockNumber); diff --git a/src/versionbits.cpp b/src/versionbits.cpp index 57dcfe1c1e..bd261eaf4c 100644 --- a/src/versionbits.cpp +++ b/src/versionbits.cpp @@ -38,7 +38,11 @@ const struct VBDeploymentInfo VersionBitsDeploymentInfo[Consensus::MAX_VERSION_B { /*.name =*/ "transfer_overflow", /*.gbt_force =*/ true, - } + }, + { + /*.name =*/ "assetauth", + /*.gbt_force =*/ true, + } }; ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex* pindexPrev, const Consensus::Params& params, ThresholdConditionCache& cache) const diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index a1997356c1..d2d640e0ae 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -1369,6 +1369,11 @@ class Witnessifier : public boost::static_visitor } return false; } + + bool operator()(const CAssetAuthID &assetAuthID) { + // P2AH addresses cannot be wrapped in witness scripts + return false; + } }; UniValue addwitnessaddress(const JSONRPCRequest& request) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 70bcb8f5ca..efcab688da 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -129,6 +129,10 @@ class CAffectedKeysVisitor : public boost::static_visitor { Process(script); } + void operator()(const CAssetAuthID &assetAuthId) { + // P2AH destinations are not backed by keys + } + void operator()(const CNoDestination &none) {} }; @@ -399,6 +403,25 @@ bool CWallet::LoadCScript(const CScript& redeemScript) return CCryptoKeyStore::AddCScript(redeemScript); } +bool CWallet::AddAssetAuthPreimage(const std::vector& vchPreimage) +{ + if (!CCryptoKeyStore::AddAssetAuthPreimage(vchPreimage)) + return false; + return CWalletDB(*dbw).WriteAssetAuthPreimage(Hash160(vchPreimage), vchPreimage); +} + +bool CWallet::LoadAssetAuthPreimage(const std::vector& vchPreimage) +{ + if (vchPreimage.size() > MAX_SCRIPT_ELEMENT_SIZE) + { + LogPrintf("%s: Warning: This wallet contains a P2AH preimage of size %i which exceeds maximum size %i and can never be used.\n", + __func__, vchPreimage.size(), MAX_SCRIPT_ELEMENT_SIZE); + return true; + } + + return CCryptoKeyStore::AddAssetAuthPreimage(vchPreimage); +} + bool CWallet::AddWatchOnly(const CScript& dest) { if (!CCryptoKeyStore::AddWatchOnly(dest)) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index a1563e071c..7c753e4c55 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -939,6 +939,11 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface bool AddCScript(const CScript& redeemScript) override; bool LoadCScript(const CScript& redeemScript); + //! Adds a P2AH preimage to the store, and saves it to disk + bool AddAssetAuthPreimage(const std::vector& vchPreimage) override; + //! Adds a P2AH preimage to the store, without saving it to disk (used by LoadWallet) + bool LoadAssetAuthPreimage(const std::vector& vchPreimage); + //! Adds a destination data tuple to the store, and saves it to disk bool AddDestData(const CTxDestination &dest, const std::string &key, const std::string &value); //! Erases a destination data tuple in the store and on disk diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index a18b4b1101..8b5c2a6c7f 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -98,6 +98,11 @@ bool CWalletDB::WriteCScript(const uint160& hash, const CScript& redeemScript) return WriteIC(std::make_pair(std::string("cscript"), hash), redeemScript, false); } +bool CWalletDB::WriteAssetAuthPreimage(const uint160& hash, const std::vector& vchPreimage) +{ + return WriteIC(std::make_pair(std::string("assetauthpre"), hash), vchPreimage, false); +} + bool CWalletDB::WriteWatchOnly(const CScript &dest, const CKeyMetadata& keyMeta) { if (!WriteIC(std::make_pair(std::string("watchmeta"), dest), keyMeta)) { @@ -490,6 +495,18 @@ bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, return false; } } + else if (strType == "assetauthpre") + { + uint160 hash; + ssKey >> hash; + std::vector vchPreimage; + ssValue >> vchPreimage; + if (!pwallet->LoadAssetAuthPreimage(vchPreimage)) + { + strErr = "Error reading wallet database: LoadAssetAuthPreimage failed"; + return false; + } + } else if (strType == "orderposnext") { ssValue >> pwallet->nOrderPosNext; diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 3465a95d80..bef2def1dc 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -213,6 +213,8 @@ class CWalletDB bool WriteCScript(const uint160& hash, const CScript& redeemScript); + bool WriteAssetAuthPreimage(const uint160& hash, const std::vector& vchPreimage); + bool WriteWatchOnly(const CScript &script, const CKeyMetadata &keymeta); bool EraseWatchOnly(const CScript &script); From 452257ddd977b62de62389761e812c441a2b437e Mon Sep 17 00:00:00 2001 From: Tyler Hess Date: Sat, 30 May 2026 16:10:05 -0600 Subject: [PATCH 3/8] Add P2AH RPC commands and raw transaction support New RPC commands in src/rpc/assetauth.cpp: - createassetauthaddress: create a P2AH address from m-of-n owner asset names - addassetauthaddress: same + store preimage in wallet and watch the address - getassetauthinfo: decode a P2AH address or preimage - spendassetauth: wallet spend from a P2AH address (auto-selects owner tokens, moves them to fresh addresses, builds/signs/broadcasts) - verifyassetauth: verify the P2AH authorization of a raw transaction using the same consensus check (CheckTxAssetAuthInputs) - listassetauthutxos: list UTXOs at a watched P2AH address Raw transaction support: - signrawtransaction prevtxs accepts assetAuthPreimage for P2AH inputs - decoderawtransaction/gettxout show assetauth info on outputs and decode preimages on inputs - SignStep handles asset-carrying P2AH scripts (preimage instead of key sig) Verified end-to-end on regtest: 1-of-1 spends, 2-of-3 multisig, chained authorization (ROOT! -> LEAF! -> RVN in one tx), and rejection of spends without owner-token movement. Co-Authored-By: Claude Opus 4.8 --- src/Makefile.am | 1 + src/core_write.cpp | 30 ++ src/rpc/assetauth.cpp | 1007 ++++++++++++++++++++++++++++++++++++ src/rpc/client.cpp | 6 + src/rpc/rawtransaction.cpp | 20 + src/rpc/register.h | 3 + src/script/sign.cpp | 28 +- 7 files changed, 1076 insertions(+), 19 deletions(-) create mode 100644 src/rpc/assetauth.cpp diff --git a/src/Makefile.am b/src/Makefile.am index a44cc99c66..7aebe3882c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -277,6 +277,7 @@ libraven_server_a_SOURCES = \ policy/rbf.cpp \ pow.cpp \ rest.cpp \ + rpc/assetauth.cpp \ rpc/assets.cpp \ rpc/blockchain.cpp \ rpc/messages.cpp \ diff --git a/src/core_write.cpp b/src/core_write.cpp index 2de6c47435..4a9a42426e 100644 --- a/src/core_write.cpp +++ b/src/core_write.cpp @@ -262,6 +262,18 @@ void ScriptPubKeyToUniv(const CScript& scriptPubKey, out.pushKV("asset_data", assetInfo); } + + // Pay-to-asset-hash (P2AH): show the committed preimage hash for both bare P2AH outputs + // and P2AH outputs that carry asset data + if (scriptPubKey.IsAssetAuthScript()) { + UniValue authInfo(UniValue::VOBJ); + uint160 hash; + if (AssetAuthHashFromScript(scriptPubKey, hash)) { + authInfo.pushKV("hash", hash.GetHex()); + authInfo.pushKV("address", EncodeDestination(CAssetAuthID(hash))); + } + out.pushKV("assetauth", authInfo); + } /** RVN END */ UniValue a(UniValue::VARR); @@ -300,6 +312,24 @@ void TxToUniv(const CTransaction& tx, const uint256& hashBlock, UniValue& entry, } in.pushKV("txinwitness", txinwitness); } + + /** RVN START */ + // Pay-to-asset-hash (P2AH): if the scriptSig decodes as a P2AH preimage, show its contents + { + CAssetAuthPreimage preimage; + if (AssetAuthPreimageFromScriptSig(txin.scriptSig, preimage)) { + UniValue p(UniValue::VOBJ); + p.pushKV("nrequired", preimage.nRequired); + p.pushKV("total", (int)preimage.vOwnerAssetNames.size()); + UniValue assets(UniValue::VARR); + for (const std::string& name : preimage.vOwnerAssetNames) + assets.push_back(name); + p.pushKV("owner_assets", assets); + p.pushKV("hash", preimage.GetHash().GetHex()); + in.pushKV("assetAuthPreimage", p); + } + } + /** RVN END */ } in.pushKV("sequence", (int64_t)txin.nSequence); vin.push_back(in); diff --git a/src/rpc/assetauth.cpp b/src/rpc/assetauth.cpp new file mode 100644 index 0000000000..565dc9fb61 --- /dev/null +++ b/src/rpc/assetauth.cpp @@ -0,0 +1,1007 @@ +// Copyright (c) 2021 The Raven Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "assets/assets.h" +#include "assets/assettypes.h" + +#include "amount.h" +#include "base58.h" +#include "chain.h" +#include "consensus/tx_verify.h" +#include "consensus/validation.h" +#include "core_io.h" +#include "policy/policy.h" +#include "rpc/safemode.h" +#include "rpc/server.h" +#include "script/script.h" +#include "script/standard.h" +#include "script/sign.h" +#include "txmempool.h" +#include "util.h" +#include "utilmoneystr.h" +#include "utilstrencodings.h" +#include "validation.h" +#include "net.h" + +#ifdef ENABLE_WALLET +#include "wallet/coincontrol.h" +#include "wallet/fees.h" +#include "wallet/wallet.h" +#include "wallet/rpcwallet.h" +#endif + +#include + +std::string AssetAuthActivationWarning() +{ + return AreAssetAuthDeployed() ? "" : "\nTHIS COMMAND IS NOT YET ACTIVE! P2AH (pay-to-asset-hash) has not been activated on this network.\n"; +} + +/** + * Used by createassetauthaddress / addassetauthaddress: + * Parses and validates (nrequired, [owner asset names]) into a canonical preimage + */ +static CAssetAuthPreimage _createassetauth_preimage(const UniValue& params) +{ + int nRequired = params[0].get_int(); + const UniValue& names = params[1].get_array(); + + if (nRequired < 1) + throw JSONRPCError(RPC_INVALID_PARAMETER, "a P2AH address must require at least one owner asset to authorize spends"); + if ((int)names.size() < nRequired) + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("not enough owner assets supplied (got %u assets, but need at least %d to authorize)", names.size(), nRequired)); + if (names.size() > MAX_ASSET_AUTH_NAMES) + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("number of owner assets in a P2AH address can't be larger than %d", MAX_ASSET_AUTH_NAMES)); + + std::vector vNames; + for (unsigned int i = 0; i < names.size(); i++) { + std::string name = names[i].get_str(); + if (!IsAssetNameAnOwner(name)) + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("%s is not a valid owner asset name (owner asset names end with '%s')", name, OWNER_TAG)); + vNames.push_back(name); + } + + // Canonicalize: sort ascending and reject duplicates so a given set of names + // always produces the same preimage and address + std::sort(vNames.begin(), vNames.end()); + for (size_t i = 1; i < vNames.size(); i++) { + if (vNames[i] == vNames[i - 1]) + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("duplicate owner asset name: %s", vNames[i])); + } + + CAssetAuthPreimage preimage((uint8_t)nRequired, vNames); + + std::string strError; + if (!preimage.IsValid(strError)) + throw JSONRPCError(RPC_INVALID_PARAMETER, strError); + + return preimage; +} + +static std::vector SerializePreimage(const CAssetAuthPreimage& preimage) +{ + CDataStream ssPreimage(SER_NETWORK, PROTOCOL_VERSION); + ssPreimage << preimage; + return std::vector(ssPreimage.begin(), ssPreimage.end()); +} + +static UniValue PreimageToUniValue(const CAssetAuthPreimage& preimage) +{ + UniValue result(UniValue::VOBJ); + CAssetAuthID id(preimage.GetHash()); + std::vector vchPreimage = SerializePreimage(preimage); + + result.push_back(Pair("address", EncodeDestination(id))); + result.push_back(Pair("hash", id.GetHex())); + result.push_back(Pair("preimage", HexStr(vchPreimage))); + result.push_back(Pair("nrequired", preimage.nRequired)); + result.push_back(Pair("total", (int)preimage.vOwnerAssetNames.size())); + + UniValue assets(UniValue::VARR); + for (const std::string& name : preimage.vOwnerAssetNames) + assets.push_back(name); + result.push_back(Pair("owner_assets", assets)); + + return result; +} + +UniValue createassetauthaddress(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 2) + throw std::runtime_error( + "createassetauthaddress nrequired [\"owner_asset\",...]\n" + + AssetAuthActivationWarning() + + "\nCreates a pay-to-asset-hash (P2AH) address that requires nrequired of the given owner assets\n" + "to move through any transaction that spends from it. Does not modify the wallet.\n" + "\nKEEP THE RETURNED PREIMAGE: it is required to spend from the address.\n" + + "\nArguments:\n" + "1. nrequired (numeric, required) The number of owner assets that must move in the spending transaction\n" + "2. \"owner_assets\" (array, required) A json array of owner asset names (each must end with '!')\n" + " [\n" + " \"asset_name!\" (string) owner asset name\n" + " ,...\n" + " ]\n" + + "\nResult:\n" + "{\n" + " \"address\":\"address\", (string) The P2AH address\n" + " \"hash\":\"hex\", (string) The hash160 of the preimage\n" + " \"preimage\":\"hex\", (string) The serialized preimage. KEEP THIS - it is required to spend\n" + " \"nrequired\": n, (numeric) Number of owner assets that must move to authorize a spend\n" + " \"total\": n, (numeric) Total number of owner assets committed to\n" + " \"owner_assets\": [...] (array) The canonical (sorted) owner asset names\n" + "}\n" + + "\nExamples:\n" + + HelpExampleCli("createassetauthaddress", "1 \"[\\\"MYASSET!\\\"]\"") + + HelpExampleCli("createassetauthaddress", "2 \"[\\\"ALPHA!\\\",\\\"BETA!\\\",\\\"GAMMA!\\\"]\"") + + HelpExampleRpc("createassetauthaddress", "2, \"[\\\"ALPHA!\\\",\\\"BETA!\\\",\\\"GAMMA!\\\"]\"") + ); + + CAssetAuthPreimage preimage = _createassetauth_preimage(request.params); + return PreimageToUniValue(preimage); +} + +UniValue getassetauthinfo(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 1) + throw std::runtime_error( + "getassetauthinfo \"address_or_hex\"\n" + + AssetAuthActivationWarning() + + "\nDecodes a P2AH address or a hex-encoded P2AH preimage.\n" + "\nIf an address is given, the preimage is looked up in the wallet (if available).\n" + "If a hex preimage is given, it is decoded directly.\n" + + "\nArguments:\n" + "1. \"address_or_hex\" (string, required) A P2AH address or hex-encoded preimage\n" + + "\nResult (preimage known):\n" + "{\n" + " \"address\":\"address\", (string) The P2AH address\n" + " \"hash\":\"hex\", (string) The hash160 of the preimage\n" + " \"known\": true, (boolean) Whether the preimage is known\n" + " \"preimage\":\"hex\", (string) The serialized preimage\n" + " \"nrequired\": n, (numeric) Number of owner assets that must move to authorize a spend\n" + " \"total\": n, (numeric) Total number of owner assets committed to\n" + " \"owner_assets\": [...] (array) The owner asset names\n" + "}\n" + "\nResult (preimage unknown):\n" + "{\n" + " \"address\":\"address\", (string) The P2AH address\n" + " \"hash\":\"hex\", (string) The committed hash\n" + " \"known\": false (boolean) The preimage is not known to this node\n" + "}\n" + + "\nExamples:\n" + + HelpExampleCli("getassetauthinfo", "\"address\"") + + HelpExampleRpc("getassetauthinfo", "\"hexpreimage\"") + ); + + std::string param = request.params[0].get_str(); + + // Case 1: hex preimage + if (IsHex(param)) { + std::vector vchPreimage = ParseHex(param); + CDataStream ssPreimage(vchPreimage, SER_NETWORK, PROTOCOL_VERSION); + CAssetAuthPreimage preimage; + try { + ssPreimage >> preimage; + } catch (const std::exception&) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Failed to decode hex as a P2AH preimage"); + } + + std::string strError; + if (!preimage.IsValid(strError)) + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Decoded preimage is not valid: %s", strError)); + + UniValue result = PreimageToUniValue(preimage); + result.push_back(Pair("known", true)); + return result; + } + + // Case 2: P2AH address + CTxDestination dest = DecodeDestination(param); + if (!IsValidDestination(dest)) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid address or hex preimage: ") + param); + + const CAssetAuthID* assetAuthID = boost::get(&dest); + if (!assetAuthID) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Not a P2AH address: ") + param); + + UniValue result(UniValue::VOBJ); + result.push_back(Pair("address", param)); + result.push_back(Pair("hash", assetAuthID->GetHex())); + +#ifdef ENABLE_WALLET + CWallet* const pwallet = GetWalletForJSONRPCRequest(request); + if (pwallet) { + std::vector vchPreimage; + if (pwallet->GetAssetAuthPreimage(*assetAuthID, vchPreimage)) { + CDataStream ssPreimage(vchPreimage, SER_NETWORK, PROTOCOL_VERSION); + CAssetAuthPreimage preimage; + try { + ssPreimage >> preimage; + UniValue full = PreimageToUniValue(preimage); + full.push_back(Pair("known", true)); + return full; + } catch (const std::exception&) { + // fall through to unknown + } + } + } +#endif + + result.push_back(Pair("known", false)); + return result; +} + +UniValue verifyassetauth(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) + throw std::runtime_error( + "verifyassetauth \"hexstring\" ( [{\"txid\":\"id\",\"vout\":n,\"scriptPubKey\":\"hex\"},...] )\n" + + AssetAuthActivationWarning() + + "\nVerifies the P2AH (pay-to-asset-hash) authorization of a raw transaction.\n" + "\nFor each P2AH input, reports whether the revealed preimage matches the committed hash and\n" + "whether enough of the committed owner assets move through the transaction to authorize the spend.\n" + "\nThe transaction's inputs are looked up in the UTXO set and mempool. Inputs that are not found\n" + "there can be provided through the prevtxs parameter.\n" + + "\nArguments:\n" + "1. \"hexstring\" (string, required) The hex string of the raw transaction\n" + "2. \"prevtxs\" (array, optional) An array of previous dependent transaction outputs\n" + " [\n" + " {\n" + " \"txid\":\"id\", (string, required) The transaction id\n" + " \"vout\":n, (numeric, required) The output number\n" + " \"scriptPubKey\": \"hex\", (string, required) The output script\n" + " \"amount\": value (numeric, optional) The amount spent\n" + " }\n" + " ,...\n" + " ]\n" + + "\nResult:\n" + "{\n" + " \"valid\": true|false, (boolean) Whether every P2AH input in the transaction is authorized\n" + " \"active\": true|false, (boolean) Whether the P2AH deployment is active\n" + " \"inputs\": [ (array) Details for each P2AH input\n" + " {\n" + " \"vin\": n, (numeric) The input index\n" + " \"txid\": \"id\", (string) The previous transaction id\n" + " \"vout\": n, (numeric) The previous output index\n" + " \"nrequired\": n, (numeric) Owner assets required to move\n" + " \"total\": n, (numeric) Total owner assets committed to\n" + " \"owner_assets\": [...], (array) The committed owner asset names\n" + " \"moved\": [...], (array) The committed owner assets that move in this transaction\n" + " \"authorized\": true|false (boolean) Whether this input is authorized\n" + " }\n" + " ,...\n" + " ]\n" + "}\n" + + "\nExamples:\n" + + HelpExampleCli("verifyassetauth", "\"hexstring\"") + + HelpExampleRpc("verifyassetauth", "\"hexstring\"") + ); + + ObserveSafeMode(); + + CMutableTransaction mtx; + if (!DecodeHexTx(mtx, request.params[0].get_str(), true)) + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + CTransaction tx(mtx); + + // Build a view of the inputs from the UTXO set, mempool, and any provided prevtxs + CCoinsView viewDummy; + CCoinsViewCache view(&viewDummy); + { + LOCK2(cs_main, mempool.cs); + CCoinsViewCache &viewChain = *pcoinsTip; + CCoinsViewMemPool viewMempool(&viewChain, mempool); + view.SetBackend(viewMempool); // temporarily switch cache backend to db+mempool view + + for (const CTxIn& txin : tx.vin) { + view.AccessCoin(txin.prevout); // Load entries from viewChain into view; can fail. + } + + view.SetBackend(viewDummy); // switch back to avoid locking mempool for too long + } + + // Overlay user-provided prevouts + if (request.params.size() > 1 && !request.params[1].isNull()) { + UniValue prevTxs = request.params[1].get_array(); + for (unsigned int idx = 0; idx < prevTxs.size(); idx++) { + const UniValue& p = prevTxs[idx]; + if (!p.isObject()) + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "expected object with {\"txid\",\"vout\",\"scriptPubKey\"}"); + + UniValue prevOut = p.get_obj(); + RPCTypeCheckObj(prevOut, + { + {"txid", UniValueType(UniValue::VSTR)}, + {"vout", UniValueType(UniValue::VNUM)}, + {"scriptPubKey", UniValueType(UniValue::VSTR)}, + }); + + uint256 txid = ParseHashO(prevOut, "txid"); + int nOut = find_value(prevOut, "vout").get_int(); + if (nOut < 0) + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "vout must be positive"); + + COutPoint out(txid, nOut); + std::vector pkData(ParseHexO(prevOut, "scriptPubKey")); + CScript scriptPubKey(pkData.begin(), pkData.end()); + + Coin newcoin; + newcoin.out.scriptPubKey = scriptPubKey; + newcoin.out.nValue = 0; + if (prevOut.exists("amount")) { + newcoin.out.nValue = AmountFromValue(find_value(prevOut, "amount")); + } + newcoin.nHeight = 1; + view.AddCoin(out, std::move(newcoin), true); + } + } + + // Make sure all inputs are available; report which are missing + UniValue inputs(UniValue::VARR); + bool fAllInputsAvailable = true; + for (size_t i = 0; i < tx.vin.size(); i++) { + const Coin& coin = view.AccessCoin(tx.vin[i].prevout); + if (coin.IsSpent()) { + fAllInputsAvailable = false; + UniValue input(UniValue::VOBJ); + input.push_back(Pair("vin", (int)i)); + input.push_back(Pair("txid", tx.vin[i].prevout.hash.GetHex())); + input.push_back(Pair("vout", (int)tx.vin[i].prevout.n)); + input.push_back(Pair("error", "input not found in UTXO set, mempool, or prevtxs")); + inputs.push_back(input); + } + } + + UniValue result(UniValue::VOBJ); + result.push_back(Pair("active", AreAssetAuthDeployed())); + + if (!fAllInputsAvailable) { + result.push_back(Pair("valid", false)); + result.push_back(Pair("inputs", inputs)); + return result; + } + + // Run the same authorization check that consensus runs + std::string strError; + std::vector vInfo; + bool fValid = CheckTxAssetAuthInputs(tx, view, strError, &vInfo); + + for (const auto& info : vInfo) { + UniValue input(UniValue::VOBJ); + input.push_back(Pair("vin", (int)info.nIndex)); + input.push_back(Pair("txid", tx.vin[info.nIndex].prevout.hash.GetHex())); + input.push_back(Pair("vout", (int)tx.vin[info.nIndex].prevout.n)); + input.push_back(Pair("nrequired", info.preimage.nRequired)); + input.push_back(Pair("total", (int)info.preimage.vOwnerAssetNames.size())); + + UniValue assets(UniValue::VARR); + for (const std::string& name : info.preimage.vOwnerAssetNames) + assets.push_back(name); + input.push_back(Pair("owner_assets", assets)); + + UniValue moved(UniValue::VARR); + for (const std::string& name : info.vAuthorizingAssets) + moved.push_back(name); + input.push_back(Pair("moved", moved)); + + input.push_back(Pair("authorized", info.fAuthorized)); + inputs.push_back(input); + } + + result.push_back(Pair("valid", fValid)); + if (!fValid && !strError.empty()) + result.push_back(Pair("error", strError)); + result.push_back(Pair("inputs", inputs)); + return result; +} + +#ifdef ENABLE_WALLET + +UniValue addassetauthaddress(const JSONRPCRequest& request) +{ + CWallet* const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() < 2 || request.params.size() > 3) + throw std::runtime_error( + "addassetauthaddress nrequired [\"owner_asset\",...] ( \"account\" )\n" + + AssetAuthActivationWarning() + + "\nCreates a pay-to-asset-hash (P2AH) address, stores the preimage in the wallet, and starts\n" + "watching the address so its UTXOs are tracked. Returns the same information as createassetauthaddress.\n" + + "\nArguments:\n" + "1. nrequired (numeric, required) The number of owner assets that must move in the spending transaction\n" + "2. \"owner_assets\" (array, required) A json array of owner asset names (each must end with '!')\n" + " [\n" + " \"asset_name!\" (string) owner asset name\n" + " ,...\n" + " ]\n" + "3. \"account\" (string, optional) DEPRECATED. An account to assign the address to\n" + + "\nResult:\n" + "{\n" + " \"address\":\"address\", (string) The P2AH address\n" + " \"hash\":\"hex\", (string) The hash160 of the preimage\n" + " \"preimage\":\"hex\", (string) The serialized preimage (also stored in the wallet)\n" + " \"nrequired\": n, (numeric) Number of owner assets that must move to authorize a spend\n" + " \"total\": n, (numeric) Total number of owner assets committed to\n" + " \"owner_assets\": [...] (array) The canonical (sorted) owner asset names\n" + "}\n" + + "\nExamples:\n" + + HelpExampleCli("addassetauthaddress", "1 \"[\\\"MYASSET!\\\"]\"") + + HelpExampleRpc("addassetauthaddress", "2, \"[\\\"ALPHA!\\\",\\\"BETA!\\\",\\\"GAMMA!\\\"]\"") + ); + + LOCK2(cs_main, pwallet->cs_wallet); + + std::string strAccount; + if (request.params.size() > 2 && !request.params[2].isNull()) { + strAccount = request.params[2].get_str(); + if (strAccount == "*") + throw JSONRPCError(RPC_WALLET_INVALID_ACCOUNT_NAME, "Invalid account name"); + } + + CAssetAuthPreimage preimage = _createassetauth_preimage(request.params); + std::vector vchPreimage = SerializePreimage(preimage); + CAssetAuthID id(preimage.GetHash()); + + // Store the preimage so the wallet can spend from this address later + if (!pwallet->AddAssetAuthPreimage(vchPreimage)) + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to store P2AH preimage in wallet"); + + // Watch the base script so the wallet records UTXOs sent to this address + CScript script = GetScriptForDestination(id); + if (!pwallet->HaveWatchOnly(script)) { + if (!pwallet->AddWatchOnly(script, 0)) + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to add P2AH address to wallet watch list"); + } + + pwallet->SetAddressBook(id, strAccount, "send"); + + return PreimageToUniValue(preimage); +} + +UniValue listassetauthutxos(const JSONRPCRequest& request) +{ + CWallet* const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() != 1) + throw std::runtime_error( + "listassetauthutxos \"address\"\n" + + AssetAuthActivationWarning() + + "\nLists the UTXOs held at a P2AH address that this wallet is watching.\n" + "The address must have been added with addassetauthaddress.\n" + + "\nArguments:\n" + "1. \"address\" (string, required) The P2AH address\n" + + "\nResult:\n" + "[\n" + " {\n" + " \"txid\": \"id\", (string) The transaction id\n" + " \"vout\": n, (numeric) The output index\n" + " \"amount\": x.xxx, (numeric) The RVN amount\n" + " \"confirmations\": n, (numeric) The number of confirmations\n" + " \"asset\": { (object, optional) Asset held at this output, if any\n" + " \"name\": \"name\", (string) The asset name\n" + " \"amount\": x.xxx (numeric) The asset amount\n" + " }\n" + " }\n" + " ,...\n" + "]\n" + + "\nExamples:\n" + + HelpExampleCli("listassetauthutxos", "\"address\"") + + HelpExampleRpc("listassetauthutxos", "\"address\"") + ); + + ObserveSafeMode(); + LOCK2(cs_main, pwallet->cs_wallet); + + CTxDestination dest = DecodeDestination(request.params[0].get_str()); + const CAssetAuthID* assetAuthID = boost::get(&dest); + if (!assetAuthID) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Not a P2AH address"); + + UniValue results(UniValue::VARR); + + for (const auto& entry : pwallet->mapWallet) { + const CWalletTx& wtx = entry.second; + if (wtx.IsCoinBase() && wtx.GetBlocksToMaturity() > 0) + continue; + + int nDepth = wtx.GetDepthInMainChain(); + if (nDepth < 0) + continue; + + for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) { + const CTxOut& txout = wtx.tx->vout[i]; + + CTxDestination outDest; + if (!ExtractDestination(txout.scriptPubKey, outDest)) + continue; + + const CAssetAuthID* outID = boost::get(&outDest); + if (!outID || *outID != *assetAuthID) + continue; + + if (pwallet->IsSpent(entry.first, i)) + continue; + + UniValue utxo(UniValue::VOBJ); + utxo.push_back(Pair("txid", entry.first.GetHex())); + utxo.push_back(Pair("vout", (int)i)); + utxo.push_back(Pair("amount", ValueFromAmount(txout.nValue))); + utxo.push_back(Pair("confirmations", nDepth)); + + // Report any asset held at this output + if (txout.scriptPubKey.IsAssetScript()) { + std::string strName; + CAmount nAmount; + if (GetAssetInfoFromScript(txout.scriptPubKey, strName, nAmount)) { + UniValue asset(UniValue::VOBJ); + asset.push_back(Pair("name", strName)); + asset.push_back(Pair("amount", ValueFromAmount(nAmount))); + utxo.push_back(Pair("asset", asset)); + } + } + + results.push_back(utxo); + } + } + + return results; +} + +UniValue spendassetauth(const JSONRPCRequest& request) +{ + CWallet* const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || !AreAssetAuthDeployed() || request.params.size() < 2 || request.params.size() > 4) + throw std::runtime_error( + "spendassetauth \"from_address\" outputs ( \"preimage\" \"change_address\" )\n" + + AssetAuthActivationWarning() + + "\nSpends UTXOs held at a P2AH (pay-to-asset-hash) address.\n" + "\nThe wallet automatically selects the owner asset UTXO(s) needed to authorize the spend and\n" + "moves them to fresh addresses in the same transaction. The wallet must hold at least nrequired\n" + "of the owner assets that the P2AH address commits to.\n" + + "\nArguments:\n" + "1. \"from_address\" (string, required) The P2AH address to spend from\n" + "2. \"outputs\" (object, required) The outputs to create\n" + " {\n" + " \"address\": x.xxx, (numeric) RVN amount to send to the address\n" + " \"address\": {\"transfer\":{\"NAME\":qty}} (object) asset amount to send to the address\n" + " ,...\n" + " }\n" + "3. \"preimage\" (string, optional) The hex preimage. Required if not stored in the wallet\n" + "4. \"change_address\" (string, optional) Address for RVN/asset change. Defaults to the P2AH address itself\n" + + "\nResult:\n" + "{\n" + " \"txid\": \"id\", (string) The transaction id\n" + " \"owner_assets_moved\": [...], (array) The owner assets used to authorize the spend\n" + " \"owner_asset_destinations\": [...], (array) The fresh addresses the owner assets were moved to\n" + " \"fee\": x.xxx (numeric) The transaction fee\n" + "}\n" + + "\nExamples:\n" + + HelpExampleCli("spendassetauth", "\"p2ah_address\" \"{\\\"destination_address\\\": 5.0}\"") + + HelpExampleCli("spendassetauth", "\"p2ah_address\" \"{\\\"destination_address\\\": {\\\"transfer\\\": {\\\"SOMEASSET\\\": 100}}}\"") + + HelpExampleRpc("spendassetauth", "\"p2ah_address\", {\"destination_address\": 5.0}") + ); + + ObserveSafeMode(); + LOCK2(cs_main, pwallet->cs_wallet); + EnsureWalletIsUnlocked(pwallet); + + // ---- Parse the P2AH address ---- + std::string strFromAddress = request.params[0].get_str(); + CTxDestination fromDest = DecodeDestination(strFromAddress); + const CAssetAuthID* assetAuthID = boost::get(&fromDest); + if (!assetAuthID) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Not a P2AH address: ") + strFromAddress); + + // ---- Resolve the preimage ---- + std::vector vchPreimage; + if (request.params.size() > 2 && !request.params[2].isNull() && !request.params[2].get_str().empty()) { + vchPreimage = ParseHexV(request.params[2], "preimage"); + if (Hash160(vchPreimage) != *assetAuthID) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Provided preimage does not hash to the P2AH address"); + } else if (!pwallet->GetAssetAuthPreimage(*assetAuthID, vchPreimage)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Preimage not found in wallet. Provide the preimage parameter or use addassetauthaddress first"); + } + + CAssetAuthPreimage preimage; + { + CDataStream ssPreimage(vchPreimage, SER_NETWORK, PROTOCOL_VERSION); + try { + ssPreimage >> preimage; + } catch (const std::exception&) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Failed to decode P2AH preimage"); + } + } + + std::string strPreimageError; + if (!preimage.IsValid(strPreimageError)) + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid preimage: %s", strPreimageError)); + + // Make sure the preimage is in the wallet keystore so ProduceSignature can find it when signing + pwallet->AddAssetAuthPreimage(vchPreimage); + + // ---- Parse change address ---- + CTxDestination changeDest = fromDest; // default: change goes back to the P2AH address + if (request.params.size() > 3 && !request.params[3].isNull() && !request.params[3].get_str().empty()) { + changeDest = DecodeDestination(request.params[3].get_str()); + if (!IsValidDestination(changeDest)) + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid change address: ") + request.params[3].get_str()); + } + + // ---- Parse outputs ---- + UniValue outputs = request.params[1].get_obj(); + std::vector vDestOuts; + CAmount nTotalRvnOut = 0; + std::map mapAssetsOut; // asset name -> total amount requested + + for (const std::string& name_ : outputs.getKeys()) { + CTxDestination destination = DecodeDestination(name_); + if (!IsValidDestination(destination)) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Raven address: ") + name_); + + CScript scriptPubKey = GetScriptForDestination(destination); + const UniValue& value = outputs[name_]; + + if (value.isNum() || value.isStr()) { + // Plain RVN output + CAmount nAmount = AmountFromValue(value); + vDestOuts.push_back(CTxOut(nAmount, scriptPubKey)); + nTotalRvnOut += nAmount; + } else if (value.isObject()) { + // Asset transfer output: {"transfer": {"NAME": qty}} + UniValue obj = value.get_obj(); + if (!obj.exists("transfer")) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Output objects must contain a \"transfer\" key"); + + UniValue transferObj = obj["transfer"].get_obj(); + for (const std::string& assetName : transferObj.getKeys()) { + CAmount nAssetAmount = AmountFromValue(transferObj[assetName]); + if (nAssetAmount <= 0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Asset amount must be positive"); + + CScript assetScript = scriptPubKey; + CAssetTransfer assetTransfer(assetName, nAssetAmount); + assetTransfer.ConstructTransaction(assetScript); + vDestOuts.push_back(CTxOut(0, assetScript)); + + mapAssetsOut[assetName] += nAssetAmount; + } + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Output values must be an amount or a transfer object"); + } + } + + if (vDestOuts.empty()) + throw JSONRPCError(RPC_INVALID_PARAMETER, "No outputs specified"); + + // ---- Select the owner asset UTXOs needed for authorization ---- + std::map > mapAssetCoins; + pwallet->AvailableAssets(mapAssetCoins, true, nullptr); + + std::vector > vOwnerInputs; // (owner asset name, utxo) + std::vector vHave; + for (const std::string& ownerName : preimage.vOwnerAssetNames) { + if ((int)vOwnerInputs.size() >= preimage.nRequired) + break; + auto it = mapAssetCoins.find(ownerName); + if (it != mapAssetCoins.end() && !it->second.empty()) { + vOwnerInputs.push_back(std::make_pair(ownerName, it->second[0])); + vHave.push_back(ownerName); + } + } + + if ((int)vOwnerInputs.size() < preimage.nRequired) { + std::string strNeed; + for (const auto& name : preimage.vOwnerAssetNames) + strNeed += (strNeed.empty() ? "" : ", ") + name; + std::string strHave; + for (const auto& name : vHave) + strHave += (strHave.empty() ? "" : ", ") + name; + if (strHave.empty()) + strHave = "none"; + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Wallet does not hold enough of the required owner assets. Need %d of [%s], have: %s", + preimage.nRequired, strNeed, strHave)); + } + + // ---- Collect P2AH UTXOs at the from address ---- + struct P2AHUtxo { + COutPoint outpoint; + CTxOut txout; + std::string assetName; // empty if RVN-only + CAmount assetAmount; + }; + + std::vector vP2AHRvn; + std::vector vP2AHAssets; + + for (const auto& entry : pwallet->mapWallet) { + const CWalletTx& wtx = entry.second; + if (wtx.IsCoinBase() && wtx.GetBlocksToMaturity() > 0) + continue; + if (wtx.GetDepthInMainChain() < 0) + continue; + + for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) { + const CTxOut& txout = wtx.tx->vout[i]; + + CTxDestination outDest; + if (!ExtractDestination(txout.scriptPubKey, outDest)) + continue; + const CAssetAuthID* outID = boost::get(&outDest); + if (!outID || *outID != *assetAuthID) + continue; + if (pwallet->IsSpent(entry.first, i)) + continue; + + P2AHUtxo utxo; + utxo.outpoint = COutPoint(entry.first, i); + utxo.txout = txout; + utxo.assetAmount = 0; + + if (txout.scriptPubKey.IsAssetScript()) { + std::string strName; + CAmount nAmount; + if (GetAssetInfoFromScript(txout.scriptPubKey, strName, nAmount)) { + utxo.assetName = strName; + utxo.assetAmount = nAmount; + vP2AHAssets.push_back(utxo); + continue; + } + } + vP2AHRvn.push_back(utxo); + } + } + + if (vP2AHRvn.empty() && vP2AHAssets.empty()) + throw JSONRPCError(RPC_WALLET_ERROR, "No spendable UTXOs found at the P2AH address (is the address being watched? use addassetauthaddress)"); + + // ---- Build the transaction ---- + CMutableTransaction mtx; + + // Track totals + CAmount nRvnIn = 0; + std::map mapAssetsIn; + + // Select asset-bearing P2AH UTXOs to cover requested asset outputs + for (const auto& assetOut : mapAssetsOut) { + CAmount nNeeded = assetOut.second; + CAmount nGathered = 0; + for (const auto& utxo : vP2AHAssets) { + if (utxo.assetName != assetOut.first) + continue; + if (nGathered >= nNeeded) + break; + mtx.vin.push_back(CTxIn(utxo.outpoint)); + nGathered += utxo.assetAmount; + mapAssetsIn[utxo.assetName] += utxo.assetAmount; + nRvnIn += utxo.txout.nValue; + } + if (nGathered < nNeeded) + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Not enough of asset %s at the P2AH address (need %s, have %s)", + assetOut.first, FormatMoney(nNeeded), FormatMoney(nGathered))); + } + + // Select RVN-bearing P2AH UTXOs (largest first) to cover RVN outputs + estimated fee + std::sort(vP2AHRvn.begin(), vP2AHRvn.end(), + [](const P2AHUtxo& a, const P2AHUtxo& b) { return a.txout.nValue > b.txout.nValue; }); + + // Rough fee estimate: P2AH inputs are large because of the preimage push. Use a generous estimate + // and adjust the change output after sizing + CAmount nFeeEstimate = 10000 * (1 + (int)vDestOuts.size() + (int)preimage.nRequired); // refined below + + size_t nRvnUtxoIdx = 0; + while (nRvnIn < nTotalRvnOut + nFeeEstimate && nRvnUtxoIdx < vP2AHRvn.size()) { + const auto& utxo = vP2AHRvn[nRvnUtxoIdx++]; + mtx.vin.push_back(CTxIn(utxo.outpoint)); + nRvnIn += utxo.txout.nValue; + } + + // Add the owner asset inputs (authorization) + for (const auto& ownerInput : vOwnerInputs) { + const COutput& out = ownerInput.second; + mtx.vin.push_back(CTxIn(COutPoint(out.tx->GetHash(), out.i))); + // Owner asset coins carry no RVN value but track the asset + mapAssetsIn[ownerInput.first] += OWNER_ASSET_AMOUNT; + } + + // If P2AH RVN isn't enough to cover outputs+fee, add wallet RVN coins + if (nRvnIn < nTotalRvnOut + nFeeEstimate) { + std::vector vAvailableCoins; + pwallet->AvailableCoins(vAvailableCoins, true, nullptr); + for (const COutput& out : vAvailableCoins) { + if (nRvnIn >= nTotalRvnOut + nFeeEstimate) + break; + if (!out.fSpendable) + continue; + // Skip asset outputs + if (out.tx->tx->vout[out.i].scriptPubKey.IsAssetScript()) + continue; + mtx.vin.push_back(CTxIn(COutPoint(out.tx->GetHash(), out.i))); + nRvnIn += out.tx->tx->vout[out.i].nValue; + } + + if (nRvnIn < nTotalRvnOut + nFeeEstimate) + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds to cover outputs and fee"); + } + + // ---- Build outputs ---- + // 1. Requested destination outputs + for (const auto& out : vDestOuts) + mtx.vout.push_back(out); + + // 2. Owner assets move to fresh addresses (replay hygiene: each authorization moves the + // owner token to a brand new address) + UniValue ownerDestinations(UniValue::VARR); + UniValue ownerAssetsMoved(UniValue::VARR); + for (const auto& ownerInput : vOwnerInputs) { + CPubKey newKey; + if (!pwallet->GetKeyFromPool(newKey)) + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Keypool ran out, please call keypoolrefill first"); + + CScript ownerScript = GetScriptForDestination(newKey.GetID()); + CAssetTransfer ownerTransfer(ownerInput.first, OWNER_ASSET_AMOUNT); + ownerTransfer.ConstructTransaction(ownerScript); + mtx.vout.push_back(CTxOut(0, ownerScript)); + + ownerAssetsMoved.push_back(ownerInput.first); + ownerDestinations.push_back(EncodeDestination(newKey.GetID())); + } + + // 3. Asset change (back to the P2AH address or the change address) + for (const auto& assetIn : mapAssetsIn) { + // Skip owner assets used for authorization; they were already sent to fresh addresses + bool fIsAuthAsset = false; + for (const auto& ownerInput : vOwnerInputs) { + if (ownerInput.first == assetIn.first) { + fIsAuthAsset = true; + break; + } + } + if (fIsAuthAsset) + continue; + + CAmount nChange = assetIn.second - (mapAssetsOut.count(assetIn.first) ? mapAssetsOut.at(assetIn.first) : 0); + if (nChange > 0) { + CScript changeScript = GetScriptForDestination(changeDest); + CAssetTransfer changeTransfer(assetIn.first, nChange); + changeTransfer.ConstructTransaction(changeScript); + mtx.vout.push_back(CTxOut(0, changeScript)); + } + } + + // 4. RVN change placeholder (value set after fee calculation) + CScript rvnChangeScript = GetScriptForDestination(changeDest); + int nChangeOutputIndex = -1; + if (nRvnIn > nTotalRvnOut) { + mtx.vout.push_back(CTxOut(0, rvnChangeScript)); + nChangeOutputIndex = (int)mtx.vout.size() - 1; + } + + // ---- Sign and size the transaction (two passes for fee accuracy) ---- + auto signTransaction = [&](CMutableTransaction& tx) -> bool { + const CTransaction txConst(tx); + for (unsigned int i = 0; i < tx.vin.size(); i++) { + CTxIn& txin = tx.vin[i]; + + // Find the prevout + CTxOut prevOut; + const auto mi = pwallet->mapWallet.find(txin.prevout.hash); + if (mi != pwallet->mapWallet.end() && txin.prevout.n < mi->second.tx->vout.size()) { + prevOut = mi->second.tx->vout[txin.prevout.n]; + } else { + return false; + } + + SignatureData sigdata; + if (!ProduceSignature(MutableTransactionSignatureCreator(pwallet, &tx, i, prevOut.nValue, SIGHASH_ALL), + prevOut.scriptPubKey, sigdata)) + return false; + UpdateTransaction(tx, i, sigdata); + } + return true; + }; + + // First pass: sign with placeholder change to get an accurate size + CMutableTransaction mtxForSize = mtx; + if (nChangeOutputIndex >= 0) + mtxForSize.vout[nChangeOutputIndex].nValue = nRvnIn - nTotalRvnOut; + if (!signTransaction(mtxForSize)) + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to sign transaction (missing keys or preimage?)"); + + // Compute the fee from the actual signed size, with a safety margin: ECDSA signature + // sizes can vary by a byte per input between the sizing pass and the final signing pass + size_t nTxBytes = GetVirtualTransactionSize(CTransaction(mtxForSize)) + mtx.vin.size() * 2; + CAmount nFee = GetMinimumFee(nTxBytes, CCoinControl(), ::mempool, ::feeEstimator, nullptr); + + if (nRvnIn < nTotalRvnOut + nFee) + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, + strprintf("Insufficient funds to cover fee of %s", FormatMoney(nFee))); + + // Set the real change value (or drop the change output if it would be dust) + CAmount nChange = nRvnIn - nTotalRvnOut - nFee; + if (nChangeOutputIndex >= 0) { + if (nChange > 546) { // dust threshold + mtx.vout[nChangeOutputIndex].nValue = nChange; + } else { + mtx.vout.erase(mtx.vout.begin() + nChangeOutputIndex); + nFee += nChange; + } + } + + // Final signing pass on the real transaction + if (!signTransaction(mtx)) + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to sign transaction (missing keys or preimage?)"); + + // ---- Broadcast ---- + CWalletTx wtxNew; + wtxNew.fTimeReceivedIsTxTime = true; + wtxNew.BindWallet(pwallet); + wtxNew.fFromMe = true; + wtxNew.SetTx(MakeTransactionRef(std::move(mtx))); + + CReserveKey reservekey(pwallet); + CValidationState state; + if (!pwallet->CommitTransaction(wtxNew, reservekey, g_connman.get(), state)) + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Transaction was rejected: %s", state.GetRejectReason())); + + UniValue result(UniValue::VOBJ); + result.push_back(Pair("txid", wtxNew.GetHash().GetHex())); + result.push_back(Pair("owner_assets_moved", ownerAssetsMoved)); + result.push_back(Pair("owner_asset_destinations", ownerDestinations)); + result.push_back(Pair("fee", ValueFromAmount(nFee))); + return result; +} + +#endif // ENABLE_WALLET + +static const CRPCCommand commands[] = +{ // category name actor (function) argNames + // ------------- ------------------------- ------------------------- ---------- + { "assetauth", "createassetauthaddress", &createassetauthaddress, {"nrequired", "owner_assets"} }, + { "assetauth", "getassetauthinfo", &getassetauthinfo, {"address_or_hex"} }, + { "assetauth", "verifyassetauth", &verifyassetauth, {"hexstring", "prevtxs"} }, +#ifdef ENABLE_WALLET + { "assetauth", "addassetauthaddress", &addassetauthaddress, {"nrequired", "owner_assets", "account"} }, + { "assetauth", "listassetauthutxos", &listassetauthutxos, {"address"} }, + { "assetauth", "spendassetauth", &spendassetauth, {"from_address", "outputs", "preimage", "change_address"} }, +#endif +}; + +void RegisterAssetAuthRPCCommands(CRPCTable &t) +{ + for (unsigned int vcidx = 0; vcidx < ARRAYLEN(commands); vcidx++) + t.appendCommand(commands[vcidx].name, &commands[vcidx]); +} diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 119471a06d..9aaa74cf4b 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -119,6 +119,12 @@ static const CRPCConvertParam vRPCConvertParams[] = { "addmultisigaddress", 1, "keys" }, { "createmultisig", 0, "nrequired" }, { "createmultisig", 1, "keys" }, + { "createassetauthaddress", 0, "nrequired" }, + { "createassetauthaddress", 1, "owner_assets" }, + { "addassetauthaddress", 0, "nrequired" }, + { "addassetauthaddress", 1, "owner_assets" }, + { "spendassetauth", 1, "outputs" }, + { "verifyassetauth", 1, "prevtxs" }, { "listunspent", 0, "minconf" }, { "listunspent", 1, "maxconf" }, { "listunspent", 2, "addresses" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index ee0206d8a1..66e595b932 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1817,6 +1817,7 @@ UniValue signrawtransaction(const JSONRPCRequest& request) " \"vout\":n, (numeric, required) The output number\n" " \"scriptPubKey\": \"hex\", (string, required) script key\n" " \"redeemScript\": \"hex\", (string, required for P2SH or P2WSH) redeem script\n" + " \"assetAuthPreimage\": \"hex\", (string, required for P2AH) pay-to-asset-hash preimage\n" " \"amount\": value (numeric, required) The amount spent\n" " }\n" " ,...\n" @@ -1968,6 +1969,25 @@ UniValue signrawtransaction(const JSONRPCRequest& request) tempKeystore.AddCScript(redeemScript); } } + + /** RVN START */ + // if assetAuthPreimage given for a P2AH (pay-to-asset-hash) input, add it to the + // tempKeystore so the input's scriptSig can be filled with the preimage push + if (scriptPubKey.IsAssetAuthScript()) { + UniValue v = find_value(prevOut, "assetAuthPreimage"); + if (!v.isNull()) { + std::vector preimageData(ParseHexV(v, "assetAuthPreimage")); + tempKeystore.AddAssetAuthPreimage(preimageData); +#ifdef ENABLE_WALLET + // When signing with the wallet, the preimage needs to be visible to the + // wallet keystore as well (in-memory only; not persisted unless the user + // calls addassetauthaddress) + if (!fGivenKeys && pwallet) + pwallet->LoadAssetAuthPreimage(preimageData); +#endif + } + } + /** RVN END */ } } diff --git a/src/rpc/register.h b/src/rpc/register.h index 67c3319bcf..14ac5a0816 100644 --- a/src/rpc/register.h +++ b/src/rpc/register.h @@ -26,6 +26,8 @@ void RegisterAssetRPCCommands(CRPCTable &tableRPC); void RegisterMessageRPCCommands(CRPCTable &tableRPC); /** Register rewards RPC commands */ void RegisterRewardsRPCCommands(CRPCTable &tableRPC); +/** Register pay-to-asset-hash (P2AH) RPC commands */ +void RegisterAssetAuthRPCCommands(CRPCTable &tableRPC); static inline void RegisterAllCoreRPCCommands(CRPCTable &t) { @@ -37,6 +39,7 @@ static inline void RegisterAllCoreRPCCommands(CRPCTable &t) RegisterAssetRPCCommands(t); RegisterMessageRPCCommands(t); RegisterRewardsRPCCommands(t); + RegisterAssetAuthRPCCommands(t); } #endif diff --git a/src/script/sign.cpp b/src/script/sign.cpp index a178a27d72..0f7134068a 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -85,29 +85,19 @@ static bool SignStep(const BaseSignatureCreator& creator, const CScript& scriptP return false; /** RVN START */ case TX_NEW_ASSET: - keyID = CKeyID(uint160(vSolutions[0])); - if (!Sign1(keyID, creator, scriptPubKey, ret, sigversion)) - return false; - else - { - CPubKey vch; - creator.KeyStore().GetPubKey(keyID, vch); - ret.push_back(ToByteVector(vch)); - } - return true; case TX_TRANSFER_ASSET: - keyID = CKeyID(uint160(vSolutions[0])); - if (!Sign1(keyID, creator, scriptPubKey, ret, sigversion)) + case TX_REISSUE_ASSET: + // Asset data can be appended to a P2AH (pay-to-asset-hash) base script as well as a + // P2PKH base script. P2AH-based asset scripts are satisfied by the preimage, not a key + if (scriptPubKey.IsAssetAuthScript()) { + std::vector vchPreimage; + if (creator.KeyStore().GetAssetAuthPreimage(uint160(vSolutions[0]), vchPreimage)) { + ret.push_back(vchPreimage); + return true; + } return false; - else - { - CPubKey vch; - creator.KeyStore().GetPubKey(keyID, vch); - ret.push_back(ToByteVector(vch)); } - return true; - case TX_REISSUE_ASSET: keyID = CKeyID(uint160(vSolutions[0])); if (!Sign1(keyID, creator, scriptPubKey, ret, sigversion)) return false; From 4485931b7af68b001222ba4830477caae8be9816 Mon Sep 17 00:00:00 2001 From: Tyler Hess Date: Sat, 30 May 2026 16:23:51 -0600 Subject: [PATCH 4/8] Add P2AH unit and functional tests Unit tests (src/test/assets/assetauth_tests.cpp, 11 cases): - Preimage validation, serialization, and canonical hashing - Script recognition (Solver, ExtractDestination, address round-trip) - ScriptSig preimage parsing - Consensus authorization: valid spends, missing movement, wrong preimage, m-of-n thresholds, chained authorization, cycle rejection, and one token authorizing multiple inputs Functional test (test/functional/feature_assetauth.py, 12 scenarios): - Pre-activation policy rejection and BIP9 activation - Address creation, canonicalization, and validation - 1-of-1 and 2-of-3 spends via spendassetauth - Consensus rejection of spends without owner-token movement - P2AH outputs holding assets - Chained authorization across P2AH addresses (and rejection without root) - verifyassetauth reporting, wallet preimage persistence across restart Also updates one base58_keys_invalid.json vector whose version byte (40) is now the valid mainnet P2AH address prefix; the replacement uses unused version byte 41 to preserve the test's intent. Co-Authored-By: Claude Opus 4.8 --- src/Makefile.test.include | 1 + src/test/assets/assetauth_tests.cpp | 566 +++++++++++++++++++++++++ src/test/data/base58_keys_invalid.json | 2 +- test/functional/feature_assetauth.py | 504 ++++++++++++++++++++++ test/functional/test_runner.py | 1 + 5 files changed, 1073 insertions(+), 1 deletion(-) create mode 100644 src/test/assets/assetauth_tests.cpp create mode 100755 test/functional/feature_assetauth.py diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 54775b819c..331f3af91d 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -26,6 +26,7 @@ GENERATED_TEST_FILES = $(JSON_TEST_FILES:.json=.json.h) $(RAW_TEST_FILES:.raw=.r # test_raven binary # RAVEN_TESTS =\ test/assets/asset_tests.cpp \ + test/assets/assetauth_tests.cpp \ test/assets/serialization_tests.cpp \ test/assets/asset_tx_tests.cpp \ test/assets/cache_tests.cpp \ diff --git a/src/test/assets/assetauth_tests.cpp b/src/test/assets/assetauth_tests.cpp new file mode 100644 index 0000000000..262fcc72bb --- /dev/null +++ b/src/test/assets/assetauth_tests.cpp @@ -0,0 +1,566 @@ +// Copyright (c) 2021 The Raven Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include + +#include + +#include +#include +#include +#include