diff --git a/contrib/p2ah_cron_stress_test.sh b/contrib/p2ah_cron_stress_test.sh new file mode 100755 index 0000000000..1275116bac --- /dev/null +++ b/contrib/p2ah_cron_stress_test.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# P2AH continuous validation harness for cron/CI. +# +# Uses a PERSISTENT regtest chain at P2AH_DATADIR — each cron tick appends more +# blocks/transactions instead of starting over. +# +# Example crontab (every 5 minutes): +# */5 * * * * /opt/Ravencoin/contrib/p2ah_cron_stress_test.sh >> /opt/Ravencoin/logs/p2ah-stress/cron.log 2>&1 +# +# Environment overrides: +# P2AH_REPO — repo root (default: parent of contrib/) +# P2AH_DATADIR — persistent node datadirs (default: $P2AH_REPO/logs/p2ah-stress/chain) +# P2AH_LOG_DIR — log directory (default: $P2AH_REPO/logs/p2ah-stress) +# P2AH_STRESS_ROUNDS — scenario rounds per cron tick (default: 1) +# P2AH_RUN_RELATED — set to 0 to skip adjacent asset functional tests (default: 0) +# P2AH_RESET_CHAIN — set to 1 to wipe P2AH_DATADIR before the stress run +# P2AH_SKIP_BUILD — set to 1 to skip auto-build when binaries missing +# P2AH_JOBS — parallel make jobs (default: nproc) + +set -euo pipefail + +P2AH_REPO="${P2AH_REPO:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +P2AH_DATADIR="${P2AH_DATADIR:-${P2AH_REPO}/logs/p2ah-stress/chain}" +P2AH_LOG_DIR="${P2AH_LOG_DIR:-${P2AH_REPO}/logs/p2ah-stress}" +P2AH_STRESS_ROUNDS="${P2AH_STRESS_ROUNDS:-1}" +P2AH_RUN_RELATED="${P2AH_RUN_RELATED:-0}" +P2AH_JOBS="${P2AH_JOBS:-$(nproc)}" +LOCK_FILE="${P2AH_LOCK_FILE:-/tmp/p2ah_cron_stress_test.lock}" +PTHREAD_SHIM="${P2AH_PTHREAD_SHIM:-/tmp/pthread_yield_compat.c}" + +RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)" +RUN_LOG="${P2AH_LOG_DIR}/run-${RUN_ID}.log" +SUMMARY_LOG="${P2AH_LOG_DIR}/summary.log" + +mkdir -p "${P2AH_LOG_DIR}" "${P2AH_DATADIR}" + +exec 9>"${LOCK_FILE}" +if ! flock -n 9; then + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) SKIP already running (lock ${LOCK_FILE})" | tee -a "${SUMMARY_LOG}" + exit 0 +fi + +exec > >(tee -a "${RUN_LOG}") 2>&1 + +echo "================================================================" +echo "P2AH stress run ${RUN_ID}" +echo "repo=${P2AH_REPO} datadir=${P2AH_DATADIR} rounds=${P2AH_STRESS_ROUNDS}" +echo "================================================================" + +cd "${P2AH_REPO}" + +TEST_BIN="${P2AH_REPO}/src/test/test_raven" +RAVEND_BIN="${P2AH_REPO}/src/ravend" +PYTHON="${PYTHON:-python3}" + +TOTAL=0 +PASSED=0 +FAILED=0 +SKIPPED=0 + +ensure_pthread_shim() { + if [[ ! -f /tmp/pthread_yield_compat.o ]]; then + if [[ ! -f "${PTHREAD_SHIM}" ]]; then + cat > "${PTHREAD_SHIM}" <<'EOF' +#include +int pthread_yield(void) { return sched_yield(); } +EOF + fi + gcc -c "${PTHREAD_SHIM}" -o /tmp/pthread_yield_compat.o + fi +} + +ensure_binaries() { + if [[ -x "${TEST_BIN}" && -x "${RAVEND_BIN}" ]]; then + return 0 + fi + if [[ "${P2AH_SKIP_BUILD:-0}" == "1" ]]; then + echo "ERROR: binaries missing and P2AH_SKIP_BUILD=1" + return 1 + fi + echo "Building ravend + test_raven..." + ensure_pthread_shim + if [[ ! -f Makefile ]]; then + ./autogen.sh + BDB_LIBS='-L/opt/db4/lib -ldb_cxx-4.8 -lpthread' \ + BDB_CFLAGS='-I/opt/db4/include' \ + LDFLAGS='-lpthread' \ + ./configure --disable-shared --with-pic --enable-benchmark=no --with-bignum=no --enable-module-recovery + fi + make -j"${P2AH_JOBS}" -C src ravend test/test_raven LIBS="/tmp/pthread_yield_compat.o" +} + +run_step() { + local name="$1" + shift + TOTAL=$((TOTAL + 1)) + local start end elapsed rc + start=$(date +%s) + echo "" + echo "---- [${TOTAL}] ${name} ----" + set +e + "$@" + rc=$? + set -e + end=$(date +%s) + elapsed=$((end - start)) + if [[ ${rc} -eq 0 ]]; then + PASSED=$((PASSED + 1)) + echo "PASS ${name} (${elapsed}s)" + printf '%s PASS %s (%ss)\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${name}" "${elapsed}" >> "${SUMMARY_LOG}" + else + FAILED=$((FAILED + 1)) + echo "FAIL ${name} exit=${rc} (${elapsed}s)" + printf '%s FAIL %s exit=%s (%ss)\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${name}" "${rc}" "${elapsed}" >> "${SUMMARY_LOG}" + fi + return 0 +} + +run_boost_suite() { + local suite="$1" + "${TEST_BIN}" --run_test="${suite}" +} + +run_persistent_stress() { + local extra_args=() + extra_args+=(--persistent-dir="${P2AH_DATADIR}") + extra_args+=(--stress-rounds="${P2AH_STRESS_ROUNDS}") + extra_args+=(--nocleanup) + if [[ "${P2AH_RESET_CHAIN:-0}" == "1" ]]; then + extra_args+=(--reset-chain) + fi + export RAVEND="${RAVEND_BIN}" + export RAVENCLI="${P2AH_REPO}/src/raven-cli" + (cd "${P2AH_REPO}" && "${PYTHON}" test/functional/feature_assetauth_stress.py "${extra_args[@]}") +} + +ensure_binaries + +echo "ravend: $("${RAVEND_BIN}" --version | head -1)" +echo "test_raven: ${TEST_BIN}" +if [[ -f "${P2AH_DATADIR}/p2ah_stress_run_counter" ]]; then + echo "persistent run counter: $(cat "${P2AH_DATADIR}/p2ah_stress_run_counter")" +fi + +# --- Unit tests (stateless; run every tick) --- +UNIT_SUITES=( + assetauth_tests + base58_tests + asset_tests + asset_tx_tests + script_standard_tests +) + +for suite in "${UNIT_SUITES[@]}"; do + run_step "unit:${suite}" run_boost_suite "${suite}" +done + +# --- Persistent chain stress (appends history every tick) --- +run_step "functional:persistent_stress" run_persistent_stress + +# --- Optional: related functional tests on ephemeral tmpdirs (do not touch persistent chain) --- +RELATED_FUNCTIONAL=( + feature_assets.py + rpc_signrawtransaction.py +) + +for script in "${RELATED_FUNCTIONAL[@]}"; do + if [[ "${P2AH_RUN_RELATED}" == "1" ]]; then + run_step "functional:ephemeral:${script}" \ + bash -c "cd '${P2AH_REPO}' && '${PYTHON}' test/functional/test_runner.py '${script}'" + else + SKIPPED=$((SKIPPED + 1)) + echo "SKIP functional:ephemeral:${script} P2AH_RUN_RELATED=0" + fi +done + +# --- Extra P2AH unit passes --- +for loop in $(seq 1 2); do + run_step "unit:assetauth_tests:repeat${loop}" run_boost_suite assetauth_tests +done + +if [[ -d "${P2AH_DATADIR}/node0/regtest" ]]; then + echo "" + if [[ -f "${P2AH_DATADIR}/p2ah_chain_height" ]]; then + echo "Persistent chain height: $(cat "${P2AH_DATADIR}/p2ah_chain_height")" + else + echo -n "Persistent chain height: " + "${P2AH_REPO}/src/raven-cli" -regtest -datadir="${P2AH_DATADIR}/node0" getblockcount 2>/dev/null || echo "?" + fi + echo -n "Persistent stress runs completed: " + cat "${P2AH_DATADIR}/p2ah_stress_run_counter" 2>/dev/null || echo "0" +fi + +echo "" +echo "================================================================" +echo "P2AH stress run ${RUN_ID} complete" +echo "total=${TOTAL} passed=${PASSED} failed=${FAILED} skipped=${SKIPPED}" +echo "datadir=${P2AH_DATADIR} log=${RUN_LOG}" +echo "================================================================" + +printf '%s DONE total=%s passed=%s failed=%s datadir=%s log=%s\n' \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${TOTAL}" "${PASSED}" "${FAILED}" "${P2AH_DATADIR}" "${RUN_LOG}" >> "${SUMMARY_LOG}" + +if [[ ${FAILED} -gt 0 ]]; then + exit 1 +fi + +exit 0 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/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/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/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/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/assetauth.cpp b/src/rpc/assetauth.cpp new file mode 100644 index 0000000000..fc8cfb1a78 --- /dev/null +++ b/src/rpc/assetauth.cpp @@ -0,0 +1,1156 @@ +// 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 + +#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; +} + +#ifdef ENABLE_WALLET + +/** UTXO held at a watched P2AH address */ +struct P2AHUtxo { + COutPoint outpoint; + CTxOut txout; + std::string assetName; // empty if RVN-only + CAmount assetAmount; +}; + +/** Key-held owner asset used as an authorization root */ +struct AuthKeyInput { + std::string assetName; + COutput output; +}; + +/** Watched P2AH input spent to authorize a chained spend; owner token returns to this P2AH */ +struct AuthP2AHInput { + COutPoint outpoint; + CAssetAuthPreimage preimage; + CTxDestination p2ahDest; + std::string assetReturned; // owner asset held at this P2AH that must move back +}; + +/** Find a watched P2AH UTXO holding the given owner asset (wallet must know the preimage) */ +static bool FindOwnerAssetAtP2AH(CWallet* pwallet, const std::string& ownerName, + const std::set& usedOutpoints, COutPoint& outpointOut, CAssetAuthPreimage& preimageOut, + CTxDestination& p2ahDestOut) +{ + 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++) { + if (pwallet->IsSpent(entry.first, i)) + continue; + + const CTxOut& txout = wtx.tx->vout[i]; + if (!txout.scriptPubKey.IsAssetScript()) + continue; + + std::string strName; + CAmount nAmount; + if (!GetAssetInfoFromScript(txout.scriptPubKey, strName, nAmount)) + continue; + if (!IsAssetNameAnOwner(strName) || strName != ownerName) + continue; + + CTxDestination outDest; + if (!ExtractDestination(txout.scriptPubKey, outDest)) + continue; + const CAssetAuthID* outID = boost::get(&outDest); + if (!outID) + continue; + + std::vector vchPreimage; + if (!pwallet->GetAssetAuthPreimage(*outID, vchPreimage)) + continue; + + CAssetAuthPreimage candidate; + CDataStream ssPreimage(vchPreimage, SER_NETWORK, PROTOCOL_VERSION); + try { + ssPreimage >> candidate; + } catch (const std::exception&) { + continue; + } + std::string strPreimageError; + if (!candidate.IsValid(strPreimageError)) + continue; + + COutPoint outpoint(entry.first, i); + if (usedOutpoints.count(outpoint)) + continue; + + outpointOut = outpoint; + preimageOut = candidate; + p2ahDestOut = outDest; + return true; + } + } + return false; +} + +/** + * Select inputs that satisfy a P2AH preimage's authorization requirement. + * Owner tokens on a parent P2AH are preferred (chained auth); those tokens are + * returned to that parent P2AH in outputs. Key-held roots move to fresh addresses. + */ +static bool ResolveAssetAuth(CWallet* pwallet, + const std::map >& mapAssetCoins, + const CAssetAuthPreimage& preimage, std::vector& keyInputs, + std::vector& p2ahInputs, std::set& usedP2AH, + std::set >& usedKey) +{ + int nFound = 0; + for (const std::string& ownerName : preimage.vOwnerAssetNames) { + if (nFound >= preimage.nRequired) + break; + + bool satisfied = false; + + COutPoint p2ahOutpoint; + CAssetAuthPreimage parentPreimage; + CTxDestination parentDest; + if (FindOwnerAssetAtP2AH(pwallet, ownerName, usedP2AH, p2ahOutpoint, parentPreimage, parentDest)) { + std::vector subKeys; + std::vector subP2ah; + if (ResolveAssetAuth(pwallet, mapAssetCoins, parentPreimage, subKeys, subP2ah, usedP2AH, usedKey)) { + keyInputs.insert(keyInputs.end(), subKeys.begin(), subKeys.end()); + p2ahInputs.insert(p2ahInputs.end(), subP2ah.begin(), subP2ah.end()); + AuthP2AHInput link; + link.outpoint = p2ahOutpoint; + link.preimage = parentPreimage; + link.p2ahDest = parentDest; + link.assetReturned = ownerName; + if (usedP2AH.insert(p2ahOutpoint).second) + p2ahInputs.push_back(link); + nFound++; + satisfied = true; + } + } + + if (!satisfied) { + auto it = mapAssetCoins.find(ownerName); + if (it != mapAssetCoins.end()) { + for (const COutput& out : it->second) { + auto id = std::make_pair(out.tx->GetHash(), out.i); + if (!usedKey.insert(id).second) + continue; + keyInputs.push_back({ownerName, out}); + nFound++; + satisfied = true; + break; + } + } + } + + if (!satisfied) + return false; + } + return nFound >= preimage.nRequired; +} + +#endif // ENABLE_WALLET + +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.\n" + "Key-held authorization roots are moved to fresh addresses. When an owner token is held at a\n" + "parent P2AH address (chained authorization), that parent P2AH input is spent and the token is\n" + "returned to the same parent P2AH address. The wallet must hold at least nrequired of the owner\n" + "assets (or be able to reach them through a watched P2AH chain).\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) Where each moved owner asset was sent (fresh key or parent P2AH)\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"); + + // ---- Resolve authorization inputs (key roots and/or parent P2AH chain) ---- + std::map > mapAssetCoins; + pwallet->AvailableAssets(mapAssetCoins, true, nullptr); + + std::vector vKeyAuthInputs; + std::vector vP2AHAuthInputs; + std::set usedP2AHAuth; + std::set > usedKeyAuth; + if (!ResolveAssetAuth(pwallet, mapAssetCoins, preimage, vKeyAuthInputs, vP2AHAuthInputs, + usedP2AHAuth, usedKeyAuth)) { + std::string strNeed; + for (const auto& name : preimage.vOwnerAssetNames) + strNeed += (strNeed.empty() ? "" : ", ") + name; + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Wallet cannot authorize this spend. Need %d of [%s] from keys or a watched P2AH chain", + preimage.nRequired, strNeed)); + } + + // ---- Collect P2AH UTXOs at the from address ---- + 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)"); + + // ---- Select UTXOs at the from address (inputs added after authorization inputs) ---- + std::vector vTargetP2AHInputs; + + 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; + vTargetP2AHInputs.push_back(utxo); + nGathered += utxo.assetAmount; + } + 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))); + } + + std::sort(vP2AHRvn.begin(), vP2AHRvn.end(), + [](const P2AHUtxo& a, const P2AHUtxo& b) { return a.txout.nValue > b.txout.nValue; }); + + CAmount nRvnFromTarget = 0; + for (const auto& utxo : vP2AHAssets) + nRvnFromTarget += utxo.txout.nValue; + + const int nP2AHInputsEstimate = 1 + (int)vP2AHAuthInputs.size() + (int)vTargetP2AHInputs.size(); + CAmount nFeeEstimate = 10000 * (1 + (int)vDestOuts.size() + nP2AHInputsEstimate + (int)vKeyAuthInputs.size()); + + size_t nRvnUtxoIdx = 0; + while (nRvnFromTarget < nTotalRvnOut + nFeeEstimate && nRvnUtxoIdx < vP2AHRvn.size()) { + nRvnFromTarget += vP2AHRvn[nRvnUtxoIdx].txout.nValue; + vTargetP2AHInputs.push_back(vP2AHRvn[nRvnUtxoIdx]); + nRvnUtxoIdx++; + } + + // ---- Build the transaction ---- + CMutableTransaction mtx; + CAmount nRvnIn = 0; + std::map mapAssetsIn; + std::set setAuthAssetsMoved; + + for (const auto& keyIn : vKeyAuthInputs) { + mtx.vin.push_back(CTxIn(COutPoint(keyIn.output.tx->GetHash(), keyIn.output.i))); + mapAssetsIn[keyIn.assetName] += OWNER_ASSET_AMOUNT; + setAuthAssetsMoved.insert(keyIn.assetName); + } + + for (const auto& p2ahIn : vP2AHAuthInputs) { + mtx.vin.push_back(CTxIn(p2ahIn.outpoint)); + if (!p2ahIn.assetReturned.empty()) { + mapAssetsIn[p2ahIn.assetReturned] += OWNER_ASSET_AMOUNT; + setAuthAssetsMoved.insert(p2ahIn.assetReturned); + } + } + + for (const auto& utxo : vTargetP2AHInputs) { + mtx.vin.push_back(CTxIn(utxo.outpoint)); + nRvnIn += utxo.txout.nValue; + if (!utxo.assetName.empty()) + mapAssetsIn[utxo.assetName] += utxo.assetAmount; + } + + 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; + 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. Authorization roots move to fresh keys; chained tokens return to their parent P2AH + UniValue ownerDestinations(UniValue::VARR); + UniValue ownerAssetsMoved(UniValue::VARR); + for (const auto& keyIn : vKeyAuthInputs) { + 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(keyIn.assetName, OWNER_ASSET_AMOUNT); + ownerTransfer.ConstructTransaction(ownerScript); + mtx.vout.push_back(CTxOut(0, ownerScript)); + + ownerAssetsMoved.push_back(keyIn.assetName); + ownerDestinations.push_back(EncodeDestination(newKey.GetID())); + } + + for (const auto& p2ahIn : vP2AHAuthInputs) { + if (p2ahIn.assetReturned.empty()) + continue; + + CScript ownerScript = GetScriptForDestination(p2ahIn.p2ahDest); + CAssetTransfer ownerTransfer(p2ahIn.assetReturned, OWNER_ASSET_AMOUNT); + ownerTransfer.ConstructTransaction(ownerScript); + mtx.vout.push_back(CTxOut(0, ownerScript)); + + ownerAssetsMoved.push_back(p2ahIn.assetReturned); + ownerDestinations.push_back(EncodeDestination(p2ahIn.p2ahDest)); + } + + // 3. Asset change (back to the P2AH address or the change address) + for (const auto& assetIn : mapAssetsIn) { + if (setAuthAssetsMoved.count(assetIn.first)) + 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/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/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/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/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/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/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..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; @@ -118,6 +108,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 +431,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/test/assets/assetauth_tests.cpp b/src/test/assets/assetauth_tests.cpp new file mode 100644 index 0000000000..095b5662cd --- /dev/null +++ b/src/test/assets/assetauth_tests.cpp @@ -0,0 +1,567 @@ +// 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