From 705d8400db2ea7925bcd8fc17363006790717679 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Wed, 18 Mar 2026 11:29:29 +0700 Subject: [PATCH] feat: add proof module for XPOP construction New module at src/xrpld/app/proof/ with layered design: - ProofBuilder: SHAMap merkle proof extraction (extractProofV1) Binary trie proof with 16-way branching, root hash verification. - LedgerProof: proof-of-ledger (header fields + tx blob/meta + merkle proof) buildLedgerProof() extracts everything from a closed Ledger. - XPOPv1: JSON format builder matching Import.cpp expectations buildXPOPv1() creates complete XPOP with validation signatures. Designed for versioning: v1 JSON (current Import compat), future v2 binary proofs and account state proofs layer on the same core. --- src/xrpld/app/proof/LedgerProof.h | 52 ++++ src/xrpld/app/proof/ProofBuilder.h | 68 +++++ src/xrpld/app/proof/XPOPv1.h | 72 +++++ src/xrpld/app/proof/detail/LedgerProof.cpp | 72 +++++ src/xrpld/app/proof/detail/ProofBuilder.cpp | 305 ++++++++++++++++++++ src/xrpld/app/proof/detail/XPOPv1.cpp | 123 ++++++++ 6 files changed, 692 insertions(+) create mode 100644 src/xrpld/app/proof/LedgerProof.h create mode 100644 src/xrpld/app/proof/ProofBuilder.h create mode 100644 src/xrpld/app/proof/XPOPv1.h create mode 100644 src/xrpld/app/proof/detail/LedgerProof.cpp create mode 100644 src/xrpld/app/proof/detail/ProofBuilder.cpp create mode 100644 src/xrpld/app/proof/detail/XPOPv1.cpp diff --git a/src/xrpld/app/proof/LedgerProof.h b/src/xrpld/app/proof/LedgerProof.h new file mode 100644 index 000000000..bc80476f9 --- /dev/null +++ b/src/xrpld/app/proof/LedgerProof.h @@ -0,0 +1,52 @@ +#ifndef RIPPLE_APP_PROOF_LEDGERPROOF_H_INCLUDED +#define RIPPLE_APP_PROOF_LEDGERPROOF_H_INCLUDED + +#include +#include +#include +#include +#include + +namespace ripple { +namespace proof { + +/// Proof-of-ledger: everything needed to prove a transaction (or account +/// state) was in a specific closed ledger. This is the core building +/// block for XPOPs and future proof formats. +struct LedgerProof +{ + // --- Ledger Header --- + std::uint32_t ledgerIndex{0}; + std::uint64_t totalCoins{0}; + uint256 parentHash; + uint256 txRoot; + uint256 accountRoot; + std::uint32_t parentCloseTime{0}; + std::uint32_t closeTime{0}; + std::uint8_t closeTimeResolution{0}; + std::uint8_t closeFlags{0}; + + /// Recompute the ledger hash from header fields. + uint256 + computeLedgerHash() const; + + // --- Transaction Proof --- + /// The transaction blob (serialized STTx). + Blob txBlob; + + /// The transaction metadata blob. + Blob metaBlob; + + /// Merkle proof from the transaction tree. + std::optional txProof; +}; + +/// Build a LedgerProof for a specific transaction in a closed ledger. +/// Returns nullopt if the transaction is not found. +std::optional +buildLedgerProof(Ledger const& ledger, uint256 const& txHash); + +} // namespace proof +} // namespace ripple + +#endif diff --git a/src/xrpld/app/proof/ProofBuilder.h b/src/xrpld/app/proof/ProofBuilder.h new file mode 100644 index 000000000..9db9016b5 --- /dev/null +++ b/src/xrpld/app/proof/ProofBuilder.h @@ -0,0 +1,68 @@ +#ifndef RIPPLE_APP_PROOF_PROOFBUILDER_H_INCLUDED +#define RIPPLE_APP_PROOF_PROOFBUILDER_H_INCLUDED + +#include +#include +#include +#include +#include + +namespace ripple { +namespace proof { + +/// A single node in a binary trie merkle proof. +/// Each inner node has 16 branches (SHAMap is a 16-way radix trie). +/// The target branch contains either a child ProofNode or is the leaf. +/// Non-target branches store their hash (for reconstruction). +struct ProofNode +{ + /// Branch hashes. All 16 branches are present. + /// The target branch is identified by `targetBranch`. + std::array branches; + + /// Which branch (0-15) leads deeper toward the target leaf. + /// Only meaningful for inner nodes in the path. + std::uint8_t targetBranch{0}; + + /// True if this is the deepest inner node (branches[targetBranch] + /// is the leaf hash, not another inner node). + bool isLeafParent{false}; +}; + +/// A merkle proof: path from root to a specific leaf in a SHAMap. +/// Verifiable by hashing from leaf up through each ProofNode. +struct MerkleProof +{ + /// The key (hash) of the target item. + uint256 key; + + /// The leaf hash (hash of the item's content). + uint256 leafHash; + + /// Path of inner nodes from root to the leaf's parent. + std::vector path; + + /// Reconstruct the root hash from this proof. + /// Returns nullopt if the proof is malformed. + std::optional + computeRoot() const; + + /// Verify this proof against an expected root hash. + bool + verify(uint256 const& expectedRoot) const; + + /// Serialize to JSON (v1 compatibility — nested arrays). + Json::Value + toJsonV1() const; +}; + +/// Extract a merkle proof for a specific item from a SHAMap. +/// V1 leaf hash: SHA512Half(txNode prefix + item_data + item_key). +/// Returns nullopt if the item is not in the map. +std::optional +extractProofV1(SHAMap const& map, uint256 const& key); + +} // namespace proof +} // namespace ripple + +#endif diff --git a/src/xrpld/app/proof/XPOPv1.h b/src/xrpld/app/proof/XPOPv1.h new file mode 100644 index 000000000..e617e5b04 --- /dev/null +++ b/src/xrpld/app/proof/XPOPv1.h @@ -0,0 +1,72 @@ +#ifndef RIPPLE_APP_PROOF_XPOPV1_H_INCLUDED +#define RIPPLE_APP_PROOF_XPOPV1_H_INCLUDED + +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace proof { + +/// Validator key pair for signing validations in an XPOP. +struct ValidatorKeys +{ + PublicKey masterPublic; + SecretKey masterSecret; + PublicKey signingPublic; // ephemeral (manifest signing key) + SecretKey signingSecret; + std::string manifest; // base64-encoded manifest +}; + +/// Validator List (VL) data for the XPOP. +struct VLData +{ + PublicKey masterPublic; // VL master key + SecretKey masterSecret; + std::string manifest; // base64-encoded VL manifest + std::string blob; // base64-encoded VL blob (validator list) + std::string signature; // hex signature of the blob + std::uint32_t version{1}; +}; + +/// Build an XPOP v1 JSON object from a LedgerProof. +/// +/// The XPOP format matches what Import.cpp::syntaxCheckXPOP expects: +/// { +/// "ledger": { index, coins, phash, txroot, acroot, pclose, close, cres, +/// flags }, "transaction": { blob, meta, proof }, "validation": { data: { +/// pubkey: validation_hex, ... }, unl: { ... } } +/// } +/// +/// @param proof The ledger proof containing tx blob, meta, and merkle +/// proof +/// @param validators Validator keys to sign the ledger hash +/// @param vl Validator list data +/// @return XPOP as a Json::Value +Json::Value +buildXPOPv1( + LedgerProof const& proof, + std::vector const& validators, + VLData const& vl); + +/// Convenience: build XPOP from a ReadView + tx hash + validator keys. +/// Combines buildLedgerProof + buildXPOPv1. +Json::Value +buildXPOPv1( + ReadView const& ledger, + uint256 const& txHash, + std::vector const& validators, + VLData const& vl); + +/// Encode an XPOP Json::Value to the hex blob format that ttIMPORT expects +/// in sfBlob. +std::string +xpopToHex(Json::Value const& xpop); + +} // namespace proof +} // namespace ripple + +#endif diff --git a/src/xrpld/app/proof/detail/LedgerProof.cpp b/src/xrpld/app/proof/detail/LedgerProof.cpp new file mode 100644 index 000000000..e92874874 --- /dev/null +++ b/src/xrpld/app/proof/detail/LedgerProof.cpp @@ -0,0 +1,72 @@ +#include +#include +#include + +namespace ripple { +namespace proof { + +uint256 +LedgerProof::computeLedgerHash() const +{ + return sha512Half( + HashPrefix::ledgerMaster, + ledgerIndex, + totalCoins, + parentHash, + txRoot, + accountRoot, + parentCloseTime, + closeTime, + closeTimeResolution, + closeFlags); +} + +std::optional +buildLedgerProof(Ledger const& ledger, uint256 const& txHash) +{ + auto const& info = ledger.info(); + + // Find the transaction in the ledger. + auto const txResult = ledger.txRead(txHash); + if (!txResult.first) + return std::nullopt; + + LedgerProof proof; + + // Ledger header fields. + proof.ledgerIndex = info.seq; + proof.totalCoins = info.drops.drops(); + proof.parentHash = info.parentHash; + proof.txRoot = info.txHash; + proof.accountRoot = info.accountHash; + proof.parentCloseTime = info.parentCloseTime.time_since_epoch().count(); + proof.closeTime = info.closeTime.time_since_epoch().count(); + proof.closeTimeResolution = info.closeTimeResolution.count(); + proof.closeFlags = info.closeFlags; + + // Transaction blob. + { + Serializer s; + txResult.first->add(s); + proof.txBlob = s.peekData(); + } + + // Transaction metadata. + if (txResult.second) + { + Serializer s; + txResult.second->add(s); + proof.metaBlob = s.peekData(); + } + + // Merkle proof from the transaction SHAMap (v1 format). + auto const& txMap = ledger.txMap(); + auto const txKey = + sha512Half(HashPrefix::transactionID, makeSlice(proof.txBlob)); + proof.txProof = extractProofV1(txMap, txKey); + + return proof; +} + +} // namespace proof +} // namespace ripple diff --git a/src/xrpld/app/proof/detail/ProofBuilder.cpp b/src/xrpld/app/proof/detail/ProofBuilder.cpp new file mode 100644 index 000000000..46cf4580f --- /dev/null +++ b/src/xrpld/app/proof/detail/ProofBuilder.cpp @@ -0,0 +1,305 @@ +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace proof { + +// --- MerkleProof --- + +std::optional +MerkleProof::computeRoot() const +{ + if (path.empty()) + return std::nullopt; + + // Start with the leaf hash and work up through the path. + uint256 currentHash = leafHash; + + // Path is root-to-leaf, so iterate in reverse. + for (auto it = path.rbegin(); it != path.rend(); ++it) + { + sha512_half_hasher h; + using beast::hash_append; + hash_append(h, HashPrefix::innerNode); + + for (int i = 0; i < 16; ++i) + { + if (i == it->targetBranch) + hash_append(h, currentHash); + else + hash_append(h, it->branches[i]); + } + + currentHash = static_cast(h); + } + + return currentHash; +} + +bool +MerkleProof::verify(uint256 const& expectedRoot) const +{ + auto const computed = computeRoot(); + return computed && *computed == expectedRoot; +} + +Json::Value +MerkleProof::toJsonV1() const +{ + // Build the nested array format that Import.cpp expects. + // Start from the deepest level and work up. + // Each level is an array of 16 entries: 15 hex hashes + 1 nested + // array (or the leaf hash at the bottom level). + + // Build bottom-up: the deepest ProofNode's target branch + // contains the leaf hash directly. + Json::Value current; + + for (auto it = path.rbegin(); it != path.rend(); ++it) + { + Json::Value level(Json::arrayValue); + for (int i = 0; i < 16; ++i) + { + if (i == it->targetBranch) + { + if (it == path.rbegin()) + { + // Deepest level: the target branch IS the leaf hash + level.append(to_string(leafHash)); + } + else + { + // Inner level: the target branch is the nested proof + level.append(current); + } + } + else + { + level.append(to_string(it->branches[i])); + } + } + current = level; + } + + return current; +} + +// --- extractProof --- + +namespace { + +/// Hash a transaction blob: SHA512Half(TXN prefix + blob) +uint256 +hashTxBlob(Slice const& txBlob) +{ + return sha512Half(HashPrefix::transactionID, txBlob); +} + +/// Hash a tx+meta pair: SHA512Half(SND prefix + vl(tx) + tx + vl(meta) + meta + +/// hashTxBlob(tx)) This matches SHAMapInnerNode's leaf hash computation. +uint256 +hashTxAndMeta(Slice const& txBlob, Slice const& metaBlob) +{ + Serializer s(txBlob.size() + metaBlob.size() + 256); + + // The SHAMap stores tx leaves as: + // HashPrefix::txNode + vl(tx) + vl(meta) + txHash + // where vl() is the variable-length encoding. + + s.addRaw(txBlob); + Serializer metaSer; + metaSer.addRaw(metaBlob); + + return sha512Half( + HashPrefix::txNode, + makeSlice(s.peekData()), + makeSlice(metaSer.peekData()), + hashTxBlob(txBlob)); +} + +/// Simple in-memory radix trie node for proof construction. +struct TrieNode +{ + uint256 hash; + uint256 key; + std::array, 16> children; + bool isLeaf{false}; + + bool + hasChildren() const + { + for (auto const& c : children) + if (c) + return true; + return false; + } + + int + childCount() const + { + int count = 0; + for (auto const& c : children) + if (c) + ++count; + return count; + } +}; + +/// Get the nibble at position `depth` in a 256-bit key. +int +getNibble(uint256 const& key, int depth) +{ + // Each byte has 2 nibbles. depth 0 = high nibble of byte 0. + int byteIdx = depth / 2; + int nibbleIdx = depth % 2; + auto byte = key.data()[byteIdx]; + return nibbleIdx == 0 ? (byte >> 4) & 0x0F : byte & 0x0F; +} + +/// Insert a leaf into the trie. +void +trieInsert(TrieNode& root, uint256 const& key, uint256 const& leafHash) +{ + TrieNode* node = &root; + int depth = 0; + + while (depth < 64) // 256 bits / 4 bits per nibble = 64 max depth + { + int nibble = getNibble(key, depth); + + if (!node->children[nibble]) + { + // Empty slot — insert leaf here. + node->children[nibble] = std::make_unique(); + node->children[nibble]->hash = leafHash; + node->children[nibble]->key = key; + node->children[nibble]->isLeaf = true; + return; + } + + if (node->children[nibble]->isLeaf) + { + // Collision — need to split. + auto existing = std::move(node->children[nibble]); + node->children[nibble] = std::make_unique(); + auto* newInner = node->children[nibble].get(); + + // Re-insert the existing leaf deeper. + int existingNibble = getNibble(existing->key, depth + 1); + newInner->children[existingNibble] = std::move(existing); + + // Continue inserting the new leaf. + node = newInner; + ++depth; + continue; + } + + // Inner node — descend. + node = node->children[nibble].get(); + ++depth; + } +} + +/// Compute hashes bottom-up for the trie. +uint256 +trieComputeHash(TrieNode& node) +{ + if (node.isLeaf) + return node.hash; + + sha512_half_hasher h; + using beast::hash_append; + hash_append(h, HashPrefix::innerNode); + + for (int i = 0; i < 16; ++i) + { + if (node.children[i]) + hash_append(h, trieComputeHash(*node.children[i])); + else + hash_append(h, uint256{}); + } + + node.hash = static_cast(h); + return node.hash; +} + +/// Extract proof path from root to a specific key. +bool +trieExtractProof( + TrieNode const& node, + uint256 const& key, + int depth, + std::vector& path, + uint256& outLeafHash) +{ + if (node.isLeaf) + { + if (node.key == key) + { + outLeafHash = node.hash; + return true; + } + return false; + } + + int nibble = getNibble(key, depth); + if (!node.children[nibble]) + return false; + + ProofNode pn; + pn.targetBranch = static_cast(nibble); + + for (int i = 0; i < 16; ++i) + { + if (node.children[i]) + pn.branches[i] = node.children[i]->hash; + // else: already zero-initialized + } + + if (node.children[nibble]->isLeaf) + { + if (node.children[nibble]->key != key) + return false; + pn.isLeafParent = true; + path.push_back(pn); + outLeafHash = node.children[nibble]->hash; + return true; + } + + path.push_back(pn); + return trieExtractProof( + *node.children[nibble], key, depth + 1, path, outLeafHash); +} + +} // namespace + +std::optional +extractProofV1(SHAMap const& map, uint256 const& key) +{ + // Build an in-memory trie from all SHAMap leaves. + // V1 leaf hash: SHA512Half(txNode prefix + item_data + item_key) + TrieNode root; + + map.visitLeaves([&](boost::intrusive_ptr const& item) { + auto const itemHash = + sha512Half(HashPrefix::txNode, item->slice(), item->key()); + trieInsert(root, item->key(), itemHash); + }); + + trieComputeHash(root); + + MerkleProof proof; + proof.key = key; + + if (!trieExtractProof(root, key, 0, proof.path, proof.leafHash)) + return std::nullopt; + + return proof; +} + +} // namespace proof +} // namespace ripple diff --git a/src/xrpld/app/proof/detail/XPOPv1.cpp b/src/xrpld/app/proof/detail/XPOPv1.cpp new file mode 100644 index 000000000..c18c6942a --- /dev/null +++ b/src/xrpld/app/proof/detail/XPOPv1.cpp @@ -0,0 +1,123 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace proof { + +namespace { + +/// Build a validation STObject, sign it, and serialize to hex. +std::string +buildValidationHex( + uint256 const& ledgerHash, + std::uint32_t ledgerSeq, + PublicKey const& signingPublic, + SecretKey const& signingSecret) +{ + auto val = std::make_shared( + NetClock::time_point{}, + signingPublic, + signingSecret, + calcNodeID(signingPublic), + [&](STValidation& v) { + v.setFieldH256(sfLedgerHash, ledgerHash); + v.setFieldU32(sfLedgerSequence, ledgerSeq); + v.setFlag(vfFullValidation); + }); + + auto const serialized = val->getSerialized(); + return strHex(makeSlice(serialized)); +} + +} // namespace + +Json::Value +buildXPOPv1( + LedgerProof const& proof, + std::vector const& validators, + VLData const& vl) +{ + Json::Value xpop(Json::objectValue); + + // --- ledger section --- + Json::Value ledger(Json::objectValue); + ledger[jss::index] = proof.ledgerIndex; + ledger[jss::coins] = std::to_string(proof.totalCoins); + ledger[jss::phash] = to_string(proof.parentHash); + ledger[jss::txroot] = to_string(proof.txRoot); + ledger[jss::acroot] = to_string(proof.accountRoot); + ledger[jss::pclose] = proof.parentCloseTime; + ledger[jss::close] = proof.closeTime; + ledger[jss::cres] = proof.closeTimeResolution; + ledger[jss::flags] = proof.closeFlags; + xpop[jss::ledger] = ledger; + + // --- transaction section --- + Json::Value transaction(Json::objectValue); + transaction[jss::blob] = strHex(makeSlice(proof.txBlob)); + transaction[jss::meta] = strHex(makeSlice(proof.metaBlob)); + + if (proof.txProof) + transaction[jss::proof] = proof.txProof->toJsonV1(); + else + transaction[jss::proof] = Json::Value(Json::arrayValue); + + xpop[jss::transaction] = transaction; + + // --- validation section --- + auto const ledgerHash = proof.computeLedgerHash(); + + Json::Value validation(Json::objectValue); + + // validation.data: map of nodepub → serialized validation hex + Json::Value data(Json::objectValue); + for (auto const& vk : validators) + { + auto const nodepub = toBase58(TokenType::NodePublic, vk.signingPublic); + data[nodepub] = buildValidationHex( + ledgerHash, proof.ledgerIndex, vk.signingPublic, vk.signingSecret); + } + validation[jss::data] = data; + + // validation.unl: VL data + Json::Value unl(Json::objectValue); + unl[jss::public_key] = strHex(vl.masterPublic); + unl[jss::manifest] = vl.manifest; + unl[jss::blob] = vl.blob; + unl[jss::signature] = vl.signature; + unl[jss::version] = vl.version; + validation[jss::unl] = unl; + + xpop[jss::validation] = validation; + + return xpop; +} + +Json::Value +buildXPOPv1( + Ledger const& ledger, + uint256 const& txHash, + std::vector const& validators, + VLData const& vl) +{ + auto proof = buildLedgerProof(ledger, txHash); + if (!proof) + return Json::Value{}; + return buildXPOPv1(*proof, validators, vl); +} + +std::string +xpopToHex(Json::Value const& xpop) +{ + auto const json = Json::FastWriter().write(xpop); + return strHex(json); +} + +} // namespace proof +} // namespace ripple