Compare commits

..
3 Commits
Author SHA1 Message Date
Richard Holland 2b0c651761 history tree 2026-10-20 17:22:40 +11:00
Richard Holland c6173a74ff init history tree 2026-10-20 17:10:35 +11:00
Richard Holland 3dc41b2a59 ignore amendment cfg option (#900)
Use like so, on testnet:
[ignore_amendments]
HookOnV2
2026-10-17 09:19:39 +10:00
55 changed files with 941 additions and 1492 deletions

No files matched your search

-1
View File
@@ -100,7 +100,6 @@ set(SECP256K1_INSTALL TRUE)
add_subdirectory(external/secp256k1)
add_library(secp256k1::secp256k1 ALIAS secp256k1)
add_subdirectory(external/ed25519-donna)
add_subdirectory(external/pqov)
add_subdirectory(external/antithesis-sdk)
find_package(gRPC REQUIRED)
find_package(lz4 REQUIRED)
-23
View File
@@ -1,26 +1,3 @@
# PQSignatures (unreleased, amendment, DefaultNo)
Adds post-quantum transaction signatures using UOV OV(256,119,45) with
compressed public keys (NIST additional-signatures round 3 candidate; pqov
round-3 parameters). Everything is gated behind the `PQSignatures` amendment.
- New `SigningPubKey` encodings: inline `0xF0 | type | key` (46,593 bytes) and
reference `0xF1 | AccountID` (21 bytes). Signatures are 135 raw bytes.
- New ledger object `PublicKey` (`ltPUBLIC_KEY`, 0x004B), keyed by the key's
AccountID (`keylet::pqPublicKey`), with no owner and no owner directory. It
is created by the first transaction that signs with the key inline, costs one
drop per key byte (load-scaled, charged on `tes` and `tec`), and is
reference counted like `HookDefinition`: master key, `RegularKey` and
`SignerEntry` slots hold references; deleted at zero.
- Reference-form signatures are verified at ingress against the open ledger
(never cached as bad) and again in preclaim against the applying view.
- Hook API: `util_verify` accepts inline and reference PQ keys;
`util_keylet` adds `PQ_PUBLIC_KEY` (38). `ledger_entry` adds
`pq_public_key`.
- Validator keys are unchanged (classical) in this release.
- Vendors `external/pqov` with portable + AVX2 (x86-64) / NEON (aarch64)
builds selected at run time.
# Release Notes
This document contains the release notes for `xahaud`, the reference server implementation of the Xahau protocol. To learn more about how to build, run or update a `xahaud` server, visit https://docs.xahau.network/infrastructure/peering/connect-to-xahau-mainnet
-1
View File
@@ -63,7 +63,6 @@ target_link_libraries(xrpl.imports.main
absl::random_random
date::date
ed25519::ed25519
pqov::pqov
secp256k1::secp256k1
xrpl.libpb
xxHash::xxhash
+8
View File
@@ -345,6 +345,14 @@ dice(uint32_t sides);
extern int64_t
util_random(uint32_t write_ptr, uint32_t write_len);
extern int64_t
history_verify(
uint32_t write_ptr,
uint32_t write_len,
uint32_t read_ptr,
uint32_t read_len,
uint64_t leaf_index);
#ifdef __cplusplus
}
#endif
+5
View File
@@ -112,6 +112,8 @@
#define sfAccountIndex ((3U << 16U) + 98U)
#define sfAccountCount ((3U << 16U) + 99U)
#define sfRewardAccumulator ((3U << 16U) + 100U)
#define sfHistoryLeafIndex ((3U << 16U) + 101U)
#define sfHistoryLeafCount ((3U << 16U) + 102U)
#define sfEmailHash ((4U << 16U) + 1U)
#define sfTakerPaysCurrency ((17U << 16U) + 1U)
#define sfTakerPaysIssuer ((17U << 16U) + 2U)
@@ -165,6 +167,7 @@
#define sfEmittedTxnID ((5U << 16U) + 97U)
#define sfGovernanceMarks ((5U << 16U) + 98U)
#define sfGovernanceFlags ((5U << 16U) + 99U)
#define sfHistoryProofDigest ((5U << 16U) + 100U)
#define sfNumber ((9U << 16U) + 1U)
#define sfAmount ((6U << 16U) + 1U)
#define sfBalance ((6U << 16U) + 2U)
@@ -231,6 +234,7 @@
#define sfHookName ((7U << 16U) + 97U)
#define sfRemarkValue ((7U << 16U) + 98U)
#define sfRemarkName ((7U << 16U) + 99U)
#define sfHistoryProof ((7U << 16U) + 100U)
#define sfAccount ((8U << 16U) + 1U)
#define sfOwner ((8U << 16U) + 2U)
#define sfDestination ((8U << 16U) + 3U)
@@ -257,6 +261,7 @@
#define sfHookNamespaces ((19U << 16U) + 5U)
#define sfCredentialIDs ((19U << 16U) + 6U)
#define sfURITokenIDs ((19U << 16U) + 99U)
#define sfHistoryPeaks ((19U << 16U) + 100U)
#define sfPaths ((18U << 16U) + 1U)
#define sfBaseAsset ((26U << 16U) + 1U)
#define sfQuoteAsset ((26U << 16U) + 2U)
+1
View File
@@ -62,6 +62,7 @@
#define ttPERMISSIONED_DOMAIN_SET 71
#define ttPERMISSIONED_DOMAIN_DELETE 72
#define ttENTROPY 89
#define ttHISTORY_PROOF 90
#define ttMANIFEST_SET 91
#define ttCRON 92
#define ttCRON_SET 93
+1 -1
View File
@@ -16,6 +16,7 @@
#define featureHooksUpdate1 "1"
#define featureHooksUpdate2 "1"
#define featureRNG "1"
#define featureHistoryTree "1"
#define fix20250131 "1"
#define fixGuardDepth32 "1"
namespace hook_api {
@@ -315,7 +316,6 @@ enum keylet_code : uint32_t {
CREDENTIAL = 35,
PERMISSIONED_DOMAIN = 36,
MANIFEST = 37,
PQ_PUBLIC_KEY = 38,
};
}
+4
View File
@@ -306,4 +306,8 @@ HOOK_API_DEFINITION(
HOOK_API_DEFINITION(
int64_t, util_random, (uint32_t write_ptr, uint32_t write_len),
featureRNG)
HOOK_API_DEFINITION(
int64_t, history_verify, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint64_t leaf_index),
featureHistoryTree)
// clang-format on
+6
View File
@@ -96,6 +96,12 @@ enum class HashPrefix : std::uint32_t {
/** Credentials signature */
credential = detail::make_hash_prefix('C', 'R', 'D'),
/** featureHistoryTree: history tree leaf */
historyLeaf = detail::make_hash_prefix('H', 'L', 'F'),
/** featureHistoryTree: history tree interior node */
historyNode = detail::make_hash_prefix('H', 'N', 'D'),
};
template <class Hasher>
+120
View File
@@ -0,0 +1,120 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_PROTOCOL_HISTORYTREE_H_INCLUDED
#define RIPPLE_PROTOCOL_HISTORYTREE_H_INCLUDED
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <cstdint>
#include <optional>
#include <vector>
namespace ripple {
namespace history {
/** featureHistoryTree: an append-only Merkle Mountain Range over every
transaction ever applied, in ledger order then TransactionIndex order.
Validators keep only the peaks (one hash per set bit of the leaf count)
in the ltHISTORY_TREE singleton; nobody needs a leaf to append or verify.
Leaf i for a transaction applied at TransactionIndex t of ledger s is
sha512Half(HashPrefix::historyLeaf, u32 s, u32 t, VL tx, VL meta)
where tx is the stored (storable, see STTx::addStorable) serialization and
meta is the stored metadata. Interior nodes are
sha512Half(HashPrefix::historyNode, left, right).
Peaks are kept tallest-first, i.e. in the order of the set bits of the
leaf count from most to least significant, so that peak p covers the
p-th set bit.
*/
/// Hash of one leaf.
uint256
leafHash(std::uint32_t ledgerSeq, std::uint32_t txIndex, Slice tx, Slice meta);
/// Hash of one interior node.
uint256
nodeHash(uint256 const& left, uint256 const& right);
/// Append a leaf, merging peaks as the binary carry of count demands.
void
append(std::vector<uint256>& peaks, std::uint64_t& count, uint256 const& leaf);
/// True if peaks could be the peaks of a tree with count leaves.
bool
peaksValid(std::vector<uint256> const& peaks, std::uint64_t count);
/** Which mountain holds a leaf.
peak: index into the peaks vector; height: path length to that peak;
offset: the leaf's position within the mountain (its path bits).
*/
struct Position
{
std::size_t peak;
unsigned height;
std::uint64_t offset;
};
std::optional<Position>
locate(std::uint64_t count, std::uint64_t leafIndex);
/** The proof a host submits in sfHistoryProof.
Wire format (big endian, VL = XRPL variable-length prefix):
u8 version (currently 1)
u32 ledger sequence
u32 transaction index
VL transaction (storable serialization)
VL metadata
u8 path length
32 * path length bytes of sibling hashes, leaf-side first
*/
constexpr std::uint8_t proofVersion = 1;
constexpr std::size_t maxProofPath = 64;
struct Proof
{
std::uint32_t ledgerSeq;
std::uint32_t txIndex;
Slice tx;
Slice meta;
std::vector<uint256> path;
};
std::optional<Proof>
parseProof(Slice blob);
/** Verify that proof places its leaf at leafIndex under the current peaks.
On success the leaf's hash is written to leaf (if non-null).
*/
bool
verifyProof(
std::vector<uint256> const& peaks,
std::uint64_t count,
std::uint64_t leafIndex,
Proof const& proof,
uint256* leaf = nullptr);
} // namespace history
} // namespace ripple
#endif
+4 -5
View File
@@ -71,11 +71,6 @@ hook(AccountID const& id) noexcept;
Keylet
hookState(AccountID const& id, uint256 const& key, uint256 const& ns) noexcept;
/** The ltPUBLIC_KEY object holding the post-quantum public key whose
AccountID is `id` (featurePQSignatures). */
Keylet
pqPublicKey(AccountID const& id) noexcept;
Keylet
hookStateDir(AccountID const& id, uint256 const& ns) noexcept;
@@ -127,6 +122,10 @@ UNLReport() noexcept;
Keylet const&
random() noexcept;
/** The (fixed) index of the featureHistoryTree history tree object. */
Keylet const&
historyTree() noexcept;
/** The beginning of an order book */
struct book_t
{
-146
View File
@@ -1,146 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of xahaud: https://github.com/Xahau/xahaud
Copyright (c) 2026 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_PROTOCOL_PQPUBLICKEY_H_INCLUDED
#define RIPPLE_PROTOCOL_PQPUBLICKEY_H_INCLUDED
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/AccountID.h>
#include <cstddef>
#include <cstdint>
#include <optional>
namespace ripple {
namespace pq {
/** Post-quantum public keys (featurePQSignatures).
Classical keys are 33 bytes and fit inside every transaction. A UOV
public key is 46,591 bytes, so it is sent once and stored in the ledger
as an ltPUBLIC_KEY object keyed by the key's AccountID; later
transactions refer to it by that AccountID.
Two new encodings may appear wherever a classical public key is accepted
(SigningPubKey of a transaction or of a Signers entry, the key argument
of the util_verify Hook API):
inline: 0xF0 | type (1 byte) | raw public key (publicKeyBytes(type))
reference: 0xF1 | AccountID (20 bytes)
The AccountID of a key is calcAccountID() of its *inline* encoding, so a
master-key signer's Account is derived from the key exactly as for
classical keys, and the type byte is part of what is hashed.
Signatures are the scheme's raw signature bytes: there is no prefix, the
scheme is known from the key.
*/
constexpr std::uint8_t inlinePrefix = 0xF0;
constexpr std::uint8_t referencePrefix = 0xF1;
/** Scheme + parameter set. Values are never reused: a parameter tweak is a
new type, so existing ledger objects and signatures stay verifiable. */
enum class KeyType : std::uint8_t {
/// UOV, OV(256,119,45), compressed public key (pqov round-3 parameters)
uovIp = 0x01,
};
constexpr std::size_t referenceBytes = 1 + AccountID::bytes; // 21
std::optional<KeyType>
keyTypeFromByte(std::uint8_t b) noexcept;
/// Size of the raw public key for a type (46,591 for uovIp)
std::size_t
publicKeyBytes(KeyType t) noexcept;
/// Size of a signature for a type (135 for uovIp)
std::size_t
signatureBytes(KeyType t) noexcept;
/// Size of the expanded public key kept in the verification cache
std::size_t
expandedPublicKeyBytes(KeyType t) noexcept;
/** Returns the key type if `key` is a well-formed inline encoding:
prefix, known type, and exactly 2 + publicKeyBytes(type) bytes. */
std::optional<KeyType>
inlineKeyType(Slice const& key) noexcept;
inline bool
isInlineKey(Slice const& key) noexcept
{
return inlineKeyType(key).has_value();
}
/// True if `key` is exactly 0xF1 followed by 20 bytes
bool
isKeyReference(Slice const& key) noexcept;
/// The AccountID named by a well-formed reference (precondition: isKeyReference)
AccountID
referenceAccountID(Slice const& ref) noexcept;
/// The AccountID of a well-formed inline key (precondition: isInlineKey)
AccountID
calcAccountID(Slice const& inlineKey) noexcept;
/// Build the inline encoding from a type and the raw key bytes
Blob
makeInlineKey(KeyType t, Slice const& rawKey);
/// Build a 21 byte reference
Blob
makeReference(AccountID const& id);
/// The raw key bytes of a well-formed inline key
inline Slice
rawKey(Slice const& inlineKey) noexcept
{
return Slice(inlineKey.data() + 2, inlineKey.size() - 2);
}
/** Verify `sig` over `msg` with a well-formed inline key.
Expanded forms of recently used keys are cached process-wide (keyed by
the key's AccountID), so repeated verifications against the same key run
at the expanded-key speed. Returns false for malformed input. */
bool
verify(Slice const& inlineKey, Slice const& msg, Slice const& sig) noexcept;
/** Derive the inline public key from a 32 byte secret seed. Returns
std::nullopt on failure. Used by tooling and tests; xahaud never signs. */
std::optional<Blob>
publicKeyFromSeed(KeyType t, Slice const& seed);
/** Sign `msg` with a 32 byte secret seed. UOV signatures are salted and are
therefore not deterministic. */
std::optional<Blob>
sign(KeyType t, Slice const& seed, Slice const& msg);
/// Name of the SIMD variant in use ("avx2", "neon" or "portable")
char const*
implementationName() noexcept;
} // namespace pq
} // namespace ripple
#endif
+33 -50
View File
@@ -31,9 +31,6 @@
#include <boost/container/flat_set.hpp>
#include <functional>
#include <optional>
#include <utility>
#include <vector>
namespace ripple {
@@ -51,6 +48,9 @@ class STTx final : public STObject, public CountedObject<STTx>
uint256 tid_;
TxType tx_type_;
uint256
computeTransactionID() const;
public:
static std::size_t const minMultiSigners = 1;
@@ -110,6 +110,32 @@ public:
uint256
getTransactionID() const;
/** featureHistoryTree: transient fields.
A transient field travels with the transaction on the wire and is
visible to transactors and Hooks while it applies, but is not signed,
is not part of the transaction ID, and is clipped from the form stored
in the ledger (see addStorable). Its content is bound to the ID by a
digest field that is signed. Only sfHistoryProof is transient.
*/
static bool
isTransientField(SField const& f);
/// True if this transaction carries any transient field.
bool
hasTransientFields() const;
/// Serialize the storable form: everything except transient fields.
void
addStorable(Serializer& s) const;
/** Throw if a transient field is present and its digest does not match.
Called by the constructors so a tampered relay copy never reaches the
HashRouter under the real transaction's ID.
*/
void
checkTransientFields() const;
Json::Value
getJson(JsonOptions options) const override;
@@ -123,46 +149,9 @@ public:
@return `true` if valid signature. If invalid, the error message string.
*/
enum class RequireFullyCanonicalSig : bool { no, yes };
/** Resolves a post-quantum key reference (featurePQSignatures).
Given the AccountID named by a 0xF1 reference, returns the key's
inline encoding (0xF0 | type | key) from a ledger, or std::nullopt
if that ledger holds no ltPUBLIC_KEY for it. */
using PQKeyLookup = std::function<std::optional<Blob>(AccountID const&)>;
/** How checkSign() treats a transaction whose SigningPubKey (or a
Signers entry's) is a post-quantum key *reference* when no lookup
is supplied: the signature cannot be verified without a ledger. */
enum class PQReferencePolicy : bool {
/// Report "Unverified PQ key reference": caller cannot verify here.
reject,
/// Treat as verified; the caller guarantees preclaim verifies it
/// against the ledger the transaction is applied to.
defer
};
/** Check the signature.
@param lookup resolves PQ key references; may be empty.
@return `true` if valid signature. If invalid, the error message string.
*/
Expected<void, std::string>
checkSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules,
PQKeyLookup const& lookup = {},
PQReferencePolicy policy = PQReferencePolicy::reject) const;
/** True if the transaction's SigningPubKey or any Signers entry's
SigningPubKey is a post-quantum key reference (0xF1 form). */
bool
usesPQKeyReference() const;
/** Inline post-quantum keys carried by this transaction: the
SigningPubKey and each Signers entry's SigningPubKey in 0xF0 form,
with the AccountID each hashes to. */
std::vector<std::pair<AccountID, Blob>>
inlinePQKeys() const;
checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules)
const;
// SQL Functions with metadata.
static std::string const&
@@ -181,18 +170,12 @@ public:
private:
Expected<void, std::string>
checkSingleSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules,
PQKeyLookup const& lookup,
PQReferencePolicy policy) const;
checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const;
Expected<void, std::string>
checkMultiSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules,
PQKeyLookup const& lookup,
PQReferencePolicy policy) const;
Rules const& rules) const;
STBase*
copy(std::size_t n, void* buf) const override;
+1 -1
View File
@@ -34,7 +34,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(PQSignatures, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HistoryTree, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(RNG, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PWALoader, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
@@ -150,25 +150,6 @@ LEDGER_ENTRY(ltIMPORT_VLSEQ, 0x0049, ImportVLSequence, import_vlseq, ({
DeleteAccount erases it explicitly. AppLoaderSequence equals the owner's
AccountRoot AppLoaderSequence as of the AccountSet that wrote it.
*/
/** featurePQSignatures: a post-quantum public key stored once in the ledger.
Content-addressed: keyed by the AccountID of the key (see
keylet::pqPublicKey and pq::calcAccountID). Like ltHOOK_DEFINITION it has
no owner and no owner directory; it is created by the first transaction
that signs with the key inline, paid for per byte, reference counted by
the AccountRoot / RegularKey / SignerEntry slots that name the key, and
deleted when the count reaches zero.
\sa keylet::pqPublicKey
*/
LEDGER_ENTRY(ltPUBLIC_KEY, 0x004B, PublicKey, pq_public_key, ({
{sfPublicKeyType, soeREQUIRED},
{sfPublicKeyData, soeREQUIRED},
{sfReferenceCount, soeREQUIRED},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
LEDGER_ENTRY(ltAPP_LOADER, 0x004c, AppLoader, app_loader, ({
{sfOwner, soeREQUIRED},
{sfAppLoader, soeREQUIRED},
@@ -235,6 +216,20 @@ LEDGER_ENTRY(ltRANDOM, 0x526E, Random, random, ({
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** featureHistoryTree: the history Merkle Mountain Range.
A singleton. Validators append one leaf per applied transaction at ledger
close (Ledger::updateHistoryTree) and keep only the peaks. Hooks may load
it; the genesis Hook drives the retention game from sfHistoryLeafCount.
\sa keylet::historyTree
*/
LEDGER_ENTRY(ltHISTORY_TREE, 0x4874, HistoryTree, history_tree, ({
{sfLedgerSequence, soeREQUIRED},
{sfHistoryLeafCount, soeREQUIRED},
{sfHistoryPeaks, soeOPTIONAL},
}))
/** A ledger object which contains a signer list for an account.
\sa keylet::signers
+9 -2
View File
@@ -42,7 +42,6 @@ TYPED_SFIELD(sfTickSize, UINT8, 16)
TYPED_SFIELD(sfUNLModifyDisabling, UINT8, 17)
TYPED_SFIELD(sfHookResult, UINT8, 18)
TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19)
TYPED_SFIELD(sfPublicKeyType, UINT8, 20)
// 16-bit integers (common)
TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::sMD_Never)
@@ -161,6 +160,9 @@ TYPED_SFIELD(sfTouchCount, UINT64, 97)
TYPED_SFIELD(sfAccountIndex, UINT64, 98)
TYPED_SFIELD(sfAccountCount, UINT64, 99)
TYPED_SFIELD(sfRewardAccumulator, UINT64, 100)
// featureHistoryTree
TYPED_SFIELD(sfHistoryLeafIndex, UINT64, 101)
TYPED_SFIELD(sfHistoryLeafCount, UINT64, 102)
// 128-bit
TYPED_SFIELD(sfEmailHash, UINT128, 1)
@@ -225,6 +227,8 @@ TYPED_SFIELD(sfHookCanEmit, UINT256, 96)
TYPED_SFIELD(sfEmittedTxnID, UINT256, 97)
TYPED_SFIELD(sfGovernanceMarks, UINT256, 98)
TYPED_SFIELD(sfGovernanceFlags, UINT256, 99)
// featureHistoryTree: sha512Half of sfHistoryProof, signed and part of the txid
TYPED_SFIELD(sfHistoryProofDigest, UINT256, 100)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
@@ -305,7 +309,9 @@ TYPED_SFIELD(sfAppLoader, VL, 96)
TYPED_SFIELD(sfHookName, VL, 97)
TYPED_SFIELD(sfRemarkValue, VL, 98)
TYPED_SFIELD(sfRemarkName, VL, 99)
TYPED_SFIELD(sfPublicKeyData, VL, 95)
// featureHistoryTree: transient. Not signed, not part of the txid, clipped from
// the stored transaction (see STTx::isTransientField).
TYPED_SFIELD(sfHistoryProof, VL, 100, SField::sMD_Default, SField::notSigning)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
@@ -339,6 +345,7 @@ TYPED_SFIELD(sfNFTokenOffers, VECTOR256, 4)
TYPED_SFIELD(sfHookNamespaces, VECTOR256, 5)
TYPED_SFIELD(sfCredentialIDs, VECTOR256, 6)
TYPED_SFIELD(sfURITokenIDs, VECTOR256, 99)
TYPED_SFIELD(sfHistoryPeaks, VECTOR256, 100)
// path set
UNTYPED_SFIELD(sfPaths, PATHSET, 1)
@@ -517,6 +517,22 @@ TRANSACTION(ttENTROPY, 89, Entropy, ({
{sfRandomData, soeOPTIONAL},
}))
/** featureHistoryTree: a host proves it holds one leaf of the history tree.
sfHistoryProof carries the leaf (tx + meta) and its sibling path and is
verified against the ltHISTORY_TREE peaks in preclaim. It is transient:
excluded from the signature and the txid (sfHistoryProofDigest stands in
for it) and clipped from the stored transaction, so history does not grow
with proofs of history. Hooks on sfDestination (strong TSH) see the full
transaction and may call history_verify on it.
*/
TRANSACTION(ttHISTORY_PROOF, 90, HistoryProof, ({
{sfHistoryLeafIndex, soeREQUIRED},
{sfHistoryProofDigest, soeREQUIRED},
{sfHistoryProof, soeOPTIONAL},
{sfDestination, soeOPTIONAL},
}))
/* sfAccount is supplied by TxFormats::commonFields; listing it here as well
makes the SOTemplate reject the format at construction. */
TRANSACTION(ttMANIFEST_SET, 91, SetManifest, ({
+10 -8
View File
@@ -94,8 +94,8 @@ enum class LedgerNameSpace : std::uint16_t {
MANIFEST_DIR = LEDGER_NAMESPACE2(0x01, 'M'),
CREDENTIAL = LEDGER_NAMESPACE2(0x01, 'D'),
PERMISSIONED_DOMAIN = 'm',
RANDOM = 0x526E, // Rn
PUBLIC_KEY = LEDGER_NAMESPACE2(0x01, 'K'),
RANDOM = 0x526E, // Rn
HISTORY_TREE = 0x4874, // Ht
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -206,12 +206,6 @@ hookDefinition(uint256 const& hash) noexcept
ltHOOK_DEFINITION, indexHash(LedgerNameSpace::HOOK_DEFINITION, hash)};
}
Keylet
pqPublicKey(AccountID const& id) noexcept
{
return {ltPUBLIC_KEY, indexHash(LedgerNameSpace::PUBLIC_KEY, id)};
}
Keylet
hookState(AccountID const& id, uint256 const& key, uint256 const& ns) noexcept
{
@@ -293,6 +287,14 @@ random() noexcept
return ret;
}
Keylet const&
historyTree() noexcept
{
static Keylet const ret{
ltHISTORY_TREE, indexHash(LedgerNameSpace::HISTORY_TREE)};
return ret;
}
Keylet
book_t::operator()(Book const& b) const
{
-399
View File
@@ -1,399 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of xahaud: https://github.com/Xahau/xahaud
Copyright (c) 2026 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/protocol/PQPublicKey.h>
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/protocol/digest.h>
#include <cstring>
#include <list>
#include <memory>
#include <mutex>
#include <unordered_map>
// pqov shim symbols (external/pqov/pqov_shim.c), one set per SIMD variant.
extern "C" {
int
pqov_portable_verify_cpk(
const std::uint8_t* msg,
std::size_t mlen,
const std::uint8_t* sig,
const std::uint8_t* cpk);
int
pqov_portable_expand_public_key(std::uint8_t* pk, const std::uint8_t* cpk);
int
pqov_portable_verify_pk(
const std::uint8_t* msg,
std::size_t mlen,
const std::uint8_t* sig,
const std::uint8_t* pk);
int
pqov_portable_keypair_from_seed(std::uint8_t* cpk, const std::uint8_t* seed);
int
pqov_portable_sign_from_seed(
std::uint8_t* sig,
const std::uint8_t* seed,
const std::uint8_t* msg,
std::size_t mlen);
#if PQOV_HAVE_AVX2
int
pqov_avx2_verify_cpk(
const std::uint8_t* msg,
std::size_t mlen,
const std::uint8_t* sig,
const std::uint8_t* cpk);
int
pqov_avx2_expand_public_key(std::uint8_t* pk, const std::uint8_t* cpk);
int
pqov_avx2_verify_pk(
const std::uint8_t* msg,
std::size_t mlen,
const std::uint8_t* sig,
const std::uint8_t* pk);
int
pqov_avx2_keypair_from_seed(std::uint8_t* cpk, const std::uint8_t* seed);
int
pqov_avx2_sign_from_seed(
std::uint8_t* sig,
const std::uint8_t* seed,
const std::uint8_t* msg,
std::size_t mlen);
#endif
#if PQOV_HAVE_NEON
int
pqov_neon_verify_cpk(
const std::uint8_t* msg,
std::size_t mlen,
const std::uint8_t* sig,
const std::uint8_t* cpk);
int
pqov_neon_expand_public_key(std::uint8_t* pk, const std::uint8_t* cpk);
int
pqov_neon_verify_pk(
const std::uint8_t* msg,
std::size_t mlen,
const std::uint8_t* sig,
const std::uint8_t* pk);
int
pqov_neon_keypair_from_seed(std::uint8_t* cpk, const std::uint8_t* seed);
int
pqov_neon_sign_from_seed(
std::uint8_t* sig,
const std::uint8_t* seed,
const std::uint8_t* msg,
std::size_t mlen);
#endif
}
namespace ripple {
namespace pq {
namespace {
// OV(256,119,45)-pkc
constexpr std::size_t uovIpCpkBytes = 46591;
constexpr std::size_t uovIpPkBytes = 321300;
constexpr std::size_t uovIpSigBytes = 135;
constexpr std::size_t seedBytes = 32;
// Number of expanded public keys (321,300 bytes each) kept in memory.
// 64 entries is ~20 MB.
constexpr std::size_t expandedCacheEntries = 64;
struct Impl
{
char const* name;
int (*verifyCpk)(
const std::uint8_t*,
std::size_t,
const std::uint8_t*,
const std::uint8_t*);
int (*expand)(std::uint8_t*, const std::uint8_t*);
int (*verifyPk)(
const std::uint8_t*,
std::size_t,
const std::uint8_t*,
const std::uint8_t*);
int (*keypair)(std::uint8_t*, const std::uint8_t*);
int (*sign)(
std::uint8_t*,
const std::uint8_t*,
const std::uint8_t*,
std::size_t);
};
Impl const&
impl() noexcept
{
// Chosen once per process. Every variant computes the same function;
// only the speed differs.
static Impl const chosen = []() -> Impl {
#if PQOV_HAVE_AVX2
#if defined(__GNUC__) || defined(__clang__)
if (__builtin_cpu_supports("avx2") && __builtin_cpu_supports("aes") &&
__builtin_cpu_supports("ssse3"))
return Impl{
"avx2",
pqov_avx2_verify_cpk,
pqov_avx2_expand_public_key,
pqov_avx2_verify_pk,
pqov_avx2_keypair_from_seed,
pqov_avx2_sign_from_seed};
#endif
#endif
#if PQOV_HAVE_NEON
return Impl{
"neon",
pqov_neon_verify_cpk,
pqov_neon_expand_public_key,
pqov_neon_verify_pk,
pqov_neon_keypair_from_seed,
pqov_neon_sign_from_seed};
#endif
return Impl{
"portable",
pqov_portable_verify_cpk,
pqov_portable_expand_public_key,
pqov_portable_verify_pk,
pqov_portable_keypair_from_seed,
pqov_portable_sign_from_seed};
}();
return chosen;
}
/** LRU cache of expanded public keys, keyed by the key's AccountID.
Expanding a compressed UOV key is most of the cost of a verification
(roughly 180k of 250k cycles with AVX2, and nearly all of it on the
portable build). Validators and active accounts verify against the same
few keys over and over, so caching the expanded form makes those
verifications run at the classic-key speed. */
class ExpandedKeyCache
{
using Entry = std::pair<AccountID, std::unique_ptr<std::uint8_t[]>>;
std::mutex mutex_;
std::list<Entry> lru_;
std::unordered_map<AccountID, std::list<Entry>::iterator, hardened_hash<>>
index_;
public:
// Returns a shared view of the expanded key, expanding (and caching) it
// on a miss. Copies out of the cache to avoid holding the lock during
// verification; 321 KB memcpy is far cheaper than a key expansion.
bool
verify(
AccountID const& id,
Slice const& cpk,
Slice const& msg,
Slice const& sig)
{
auto pk = std::make_unique<std::uint8_t[]>(uovIpPkBytes);
bool hit = false;
{
std::lock_guard lock(mutex_);
if (auto it = index_.find(id); it != index_.end())
{
lru_.splice(lru_.begin(), lru_, it->second);
std::memcpy(pk.get(), it->second->second.get(), uovIpPkBytes);
hit = true;
}
}
if (!hit)
{
if (impl().expand(pk.get(), cpk.data()) != 0)
return false;
auto copy = std::make_unique<std::uint8_t[]>(uovIpPkBytes);
std::memcpy(copy.get(), pk.get(), uovIpPkBytes);
std::lock_guard lock(mutex_);
if (index_.find(id) == index_.end())
{
lru_.emplace_front(id, std::move(copy));
index_[id] = lru_.begin();
while (lru_.size() > expandedCacheEntries)
{
index_.erase(lru_.back().first);
lru_.pop_back();
}
}
}
return impl().verifyPk(msg.data(), msg.size(), sig.data(), pk.get()) ==
0;
}
};
ExpandedKeyCache&
cache()
{
static ExpandedKeyCache c;
return c;
}
} // namespace
std::optional<KeyType>
keyTypeFromByte(std::uint8_t b) noexcept
{
switch (b)
{
case static_cast<std::uint8_t>(KeyType::uovIp):
return KeyType::uovIp;
default:
return std::nullopt;
}
}
std::size_t
publicKeyBytes(KeyType t) noexcept
{
switch (t)
{
case KeyType::uovIp:
return uovIpCpkBytes;
}
return 0;
}
std::size_t
signatureBytes(KeyType t) noexcept
{
switch (t)
{
case KeyType::uovIp:
return uovIpSigBytes;
}
return 0;
}
std::size_t
expandedPublicKeyBytes(KeyType t) noexcept
{
switch (t)
{
case KeyType::uovIp:
return uovIpPkBytes;
}
return 0;
}
std::optional<KeyType>
inlineKeyType(Slice const& key) noexcept
{
if (key.size() < 2 || key[0] != inlinePrefix)
return std::nullopt;
auto const t = keyTypeFromByte(key[1]);
if (!t || key.size() != 2 + publicKeyBytes(*t))
return std::nullopt;
return t;
}
bool
isKeyReference(Slice const& key) noexcept
{
return key.size() == referenceBytes && key[0] == referencePrefix;
}
AccountID
referenceAccountID(Slice const& ref) noexcept
{
AccountID id;
std::memcpy(id.data(), ref.data() + 1, AccountID::bytes);
return id;
}
AccountID
calcAccountID(Slice const& inlineKey) noexcept
{
static_assert(AccountID::bytes == sizeof(ripesha_hasher::result_type));
ripesha_hasher rsh;
rsh(inlineKey.data(), inlineKey.size());
return AccountID{static_cast<ripesha_hasher::result_type>(rsh)};
}
Blob
makeInlineKey(KeyType t, Slice const& rawKey)
{
Blob b;
b.reserve(2 + rawKey.size());
b.push_back(inlinePrefix);
b.push_back(static_cast<std::uint8_t>(t));
b.insert(b.end(), rawKey.data(), rawKey.data() + rawKey.size());
return b;
}
Blob
makeReference(AccountID const& id)
{
Blob b;
b.reserve(referenceBytes);
b.push_back(referencePrefix);
b.insert(b.end(), id.data(), id.data() + AccountID::bytes);
return b;
}
bool
verify(Slice const& inlineKey, Slice const& msg, Slice const& sig) noexcept
{
auto const t = inlineKeyType(inlineKey);
if (!t || sig.size() != signatureBytes(*t))
return false;
try
{
switch (*t)
{
case KeyType::uovIp:
return cache().verify(
calcAccountID(inlineKey), rawKey(inlineKey), msg, sig);
}
}
catch (std::exception const&)
{
}
return false;
}
std::optional<Blob>
publicKeyFromSeed(KeyType t, Slice const& seed)
{
if (t != KeyType::uovIp || seed.size() != seedBytes)
return std::nullopt;
Blob raw(uovIpCpkBytes);
if (impl().keypair(raw.data(), seed.data()) != 0)
return std::nullopt;
return makeInlineKey(t, makeSlice(raw));
}
std::optional<Blob>
sign(KeyType t, Slice const& seed, Slice const& msg)
{
if (t != KeyType::uovIp || seed.size() != seedBytes)
return std::nullopt;
Blob sig(uovIpSigBytes);
if (impl().sign(sig.data(), seed.data(), msg.data(), msg.size()) != 0)
return std::nullopt;
return sig;
}
char const*
implementationName() noexcept
{
return impl().name;
}
} // namespace pq
} // namespace ripple
+106 -151
View File
@@ -25,7 +25,6 @@
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/PQPublicKey.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/STAccount.h>
@@ -34,6 +33,7 @@
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <boost/format.hpp>
@@ -63,7 +63,8 @@ STTx::STTx(STObject&& object) : STObject(std::move(object))
{
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // may throw
tid_ = getHash(HashPrefix::transactionID);
checkTransientFields();
tid_ = computeTransactionID();
}
STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
@@ -79,7 +80,8 @@ STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // May throw
tid_ = getHash(HashPrefix::transactionID);
checkTransientFields();
tid_ = computeTransactionID();
}
STTx::STTx(TxType type, std::function<void(STObject&)> assembler)
@@ -97,7 +99,70 @@ STTx::STTx(TxType type, std::function<void(STObject&)> assembler)
if (tx_type_ != type)
LogicError("Transaction type was mutated during assembly");
tid_ = getHash(HashPrefix::transactionID);
checkTransientFields();
tid_ = computeTransactionID();
}
namespace {
uint256
computeStorableTxID(STTx const& tx)
{
if (!tx.hasTransientFields())
return tx.getHash(HashPrefix::transactionID);
// The ID is the hash of the storable form, so the clipped transaction
// stored in the ledger hashes to the same ID it was relayed and applied
// under. The digest field inside that form commits to the clipped data.
Serializer s;
s.add32(HashPrefix::transactionID);
tx.addStorable(s);
return s.getSHA512Half();
}
} // namespace
bool
STTx::isTransientField(SField const& f)
{
return f == sfHistoryProof;
}
bool
STTx::hasTransientFields() const
{
return isFieldPresent(sfHistoryProof);
}
void
STTx::addStorable(Serializer& s) const
{
if (!hasTransientFields())
{
add(s);
return;
}
STObject clipped(*this);
clipped.makeFieldAbsent(sfHistoryProof);
clipped.add(s);
}
void
STTx::checkTransientFields() const
{
if (!isFieldPresent(sfHistoryProof))
return;
if (!isFieldPresent(sfHistoryProofDigest) ||
sha512Half(makeSlice(getFieldVL(sfHistoryProof))) !=
getFieldH256(sfHistoryProofDigest))
Throw<std::runtime_error>(
"Transaction transient field digest mismatch");
}
uint256
STTx::computeTransactionID() const
{
return computeStorableTxID(*this);
}
STBase*
@@ -207,15 +272,13 @@ STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data));
setFieldVL(sfTxnSignature, sig);
tid_ = getHash(HashPrefix::transactionID);
tid_ = computeTransactionID();
}
Expected<void, std::string>
STTx::checkSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules,
PQKeyLookup const& lookup,
PQReferencePolicy policy) const
Rules const& rules) const
{
try
{
@@ -224,8 +287,8 @@ STTx::checkSign(
// multi-signing. Otherwise we're single-signing.
Blob const& signingPubKey = getFieldVL(sfSigningPubKey);
return signingPubKey.empty()
? checkMultiSign(requireCanonicalSig, rules, lookup, policy)
: checkSingleSign(requireCanonicalSig, rules, lookup, policy);
? checkMultiSign(requireCanonicalSig, rules)
: checkSingleSign(requireCanonicalSig);
}
catch (std::exception const&)
{
@@ -233,103 +296,6 @@ STTx::checkSign(
return Unexpected("Internal signature check failure.");
}
bool
STTx::usesPQKeyReference() const
{
if (pq::isKeyReference(makeSlice(getSigningPubKey())))
return true;
if (isFieldPresent(sfSigners))
for (auto const& signer : getFieldArray(sfSigners))
if (signer.isFieldPresent(sfSigningPubKey) &&
pq::isKeyReference(
makeSlice(signer.getFieldVL(sfSigningPubKey))))
return true;
return false;
}
std::vector<std::pair<AccountID, Blob>>
STTx::inlinePQKeys() const
{
std::vector<std::pair<AccountID, Blob>> keys;
auto consider = [&keys](Blob const& spk) {
auto const s = makeSlice(spk);
if (pq::isInlineKey(s))
keys.emplace_back(pq::calcAccountID(s), spk);
};
consider(getSigningPubKey());
if (isFieldPresent(sfSigners))
for (auto const& signer : getFieldArray(sfSigners))
if (signer.isFieldPresent(sfSigningPubKey))
consider(signer.getFieldVL(sfSigningPubKey));
return keys;
}
namespace {
/** Verify one signature with a key in any of the supported encodings.
Returns an error string, or std::nullopt when the signature verified
(or was deferred under PQReferencePolicy::defer). */
std::optional<std::string>
verifyAnyKey(
Blob const& spk,
Slice const& data,
Blob const& signature,
bool fullyCanonical,
bool isWildcardNetwork,
Rules const& rules,
STTx::PQKeyLookup const& lookup,
STTx::PQReferencePolicy policy)
{
auto const key = makeSlice(spk);
if (publicKeyType(key))
{
if (isWildcardNetwork ||
verify(PublicKey(key), data, makeSlice(signature), fullyCanonical))
return std::nullopt;
return std::string("Invalid signature.");
}
if (!rules.enabled(featurePQSignatures))
return std::string("Invalid signature.");
if (pq::isInlineKey(key))
{
if (isWildcardNetwork || pq::verify(key, data, makeSlice(signature)))
return std::nullopt;
return std::string("Invalid signature.");
}
if (pq::isKeyReference(key))
{
if (isWildcardNetwork)
return std::nullopt;
if (!lookup)
{
if (policy == STTx::PQReferencePolicy::defer)
return std::nullopt;
return std::string("Unverified PQ key reference.");
}
auto const resolved = lookup(pq::referenceAccountID(key));
if (!resolved)
return std::string("Unknown PQ key reference.");
auto const inlineKey = makeSlice(*resolved);
// The ledger (or whoever answered the lookup) is not trusted: the
// key must hash to the AccountID the reference names.
if (!pq::isInlineKey(inlineKey) ||
pq::calcAccountID(inlineKey) != pq::referenceAccountID(key))
return std::string("PQ key reference does not match key.");
if (pq::verify(inlineKey, data, makeSlice(signature)))
return std::nullopt;
return std::string("Invalid signature.");
}
return std::string("Invalid signature.");
}
} // namespace
Json::Value
STTx::getJson(JsonOptions options) const
{
@@ -410,11 +376,7 @@ STTx::getMetaSQL(
}
Expected<void, std::string>
STTx::checkSingleSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules,
PQKeyLookup const& lookup,
PQReferencePolicy policy) const
STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
{
// We don't allow both a non-empty sfSigningPubKey and an sfSigners.
// That would allow the transaction to be signed two ways. So if both
@@ -426,33 +388,33 @@ STTx::checkSingleSign(
bool const isWildcardNetwork =
isFieldPresent(sfNetworkID) && getFieldU32(sfNetworkID) == 65535;
std::optional<std::string> failure = "Invalid signature.";
bool validSig = false;
try
{
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
(requireCanonicalSig == RequireFullyCanonicalSig::yes);
auto const spk = getFieldVL(sfSigningPubKey);
Blob const signature = getFieldVL(sfTxnSignature);
Blob const data = getSigningData(*this);
failure = verifyAnyKey(
spk,
makeSlice(data),
signature,
fullyCanonical,
isWildcardNetwork,
rules,
lookup,
policy);
if (publicKeyType(makeSlice(spk)))
{
Blob const signature = getFieldVL(sfTxnSignature);
Blob const data = getSigningData(*this);
validSig = isWildcardNetwork ||
verify(PublicKey(makeSlice(spk)),
makeSlice(data),
makeSlice(signature),
fullyCanonical);
}
}
catch (std::exception const&)
{
// Assume it was a signature failure.
failure = "Invalid signature.";
validSig = false;
}
if (failure)
return Unexpected(*failure);
if (validSig == false)
return Unexpected("Invalid signature.");
// Signature was verified.
return {};
}
@@ -460,9 +422,7 @@ STTx::checkSingleSign(
Expected<void, std::string>
STTx::checkMultiSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules,
PQKeyLookup const& lookup,
PQReferencePolicy policy) const
Rules const& rules) const
{
// Make sure the MultiSigners are present. Otherwise they are not
// attempting multi-signing and we just have a bad SigningPubKey.
@@ -519,40 +479,35 @@ STTx::checkMultiSign(
lastAccountID = accountID;
// Verify the signature.
std::optional<std::string> failure = "Invalid signature.";
bool validSig = false;
try
{
Serializer s = dataStart;
finishMultiSigningData(accountID, s);
auto spk = signer.getFieldVL(sfSigningPubKey);
Blob const signature = signer.getFieldVL(sfTxnSignature);
failure = verifyAnyKey(
spk,
s.slice(),
signature,
fullyCanonical,
isWildcardNetwork,
rules,
lookup,
policy);
if (publicKeyType(makeSlice(spk)))
{
Blob const signature = signer.getFieldVL(sfTxnSignature);
// wildcard network gets a free pass
validSig = isWildcardNetwork ||
verify(PublicKey(makeSlice(spk)),
s.slice(),
makeSlice(signature),
fullyCanonical);
}
}
catch (std::exception const&)
{
// We assume any problem lies with the signature.
failure = "Invalid signature.";
validSig = false;
}
if (failure)
{
if (*failure == "Invalid signature.")
return Unexpected(
std::string("Invalid signature on account ") +
toBase58(accountID) + ".");
if (!validSig)
return Unexpected(
*failure + std::string(" Account ") + toBase58(accountID) +
".");
}
std::string("Invalid signature on account ") +
toBase58(accountID) + ".");
}
// All signatures verified.
return {};
+74
View File
@@ -1271,6 +1271,79 @@ public:
BEAST_EXPECT(table->needValidatedLedger(257));
}
void
testIgnoredUnsupported()
{
testcase("ignore_amendments");
using namespace std::chrono_literals;
weeks constexpr w(1);
test::jtx::Env env{*this, makeConfig()};
static std::vector<AmendmentTable::FeatureInfo> const supported =
makeDefaultYes(yes_);
// Ignore every unsupported_ amendment except the first one.
BEAST_EXPECT(unsupported_.size() > 1);
std::vector<std::string> const ignoredNames(
unsupported_.begin() + 1, unsupported_.end());
auto table = make_AmendmentTable(
env.app(),
w,
supported,
emptySection_,
emptySection_,
journal_,
makeSection(ignoredNames));
for (auto const& a : ignoredNames)
BEAST_EXPECT(table->isSupported(amendmentId(a)));
BEAST_EXPECT(!table->isSupported(amendmentId(unsupported_[0])));
majorityAmendments_t const majority;
// Enabling only ignored amendments must not block.
std::set<uint256> enabled;
for (auto const& a : ignoredNames)
enabled.insert(amendmentId(a));
table->doValidatedLedger(1, enabled, majority);
BEAST_EXPECT(!table->hasUnsupportedEnabled());
BEAST_EXPECT(table->unsupportedAmendments().empty());
// An ignored amendment must never be voted for.
auto const desired = table->getDesired();
for (auto const& a : ignoredNames)
BEAST_EXPECT(
std::find(desired.begin(), desired.end(), amendmentId(a)) ==
desired.end());
// A non-ignored unsupported amendment still blocks.
enabled.insert(amendmentId(unsupported_[0]));
table->doValidatedLedger(2, enabled, majority);
BEAST_EXPECT(table->hasUnsupportedEnabled());
// Bad entries are rejected.
Section bad("ignore_amendments");
bad.append("NotARealFeatureName");
try
{
make_AmendmentTable(
env.app(),
w,
supported,
emptySection_,
emptySection_,
journal_,
bad);
fail("expected bad [ignore_amendments] entry to throw");
}
catch (std::runtime_error const&)
{
pass();
}
}
void
testFeature(FeatureBitset const& feat)
{
@@ -1294,6 +1367,7 @@ public:
testBadConfig();
testEnableVeto();
testHasUnsupported();
testIgnoredUnsupported();
testFeature(all - fixMajorityCalc);
testFeature(all);
}
+8
View File
@@ -501,6 +501,14 @@ public:
Expected<Bytes, HookReturnCode>
util_random(uint32_t len) const;
/// featureHistoryTree (genesis Hook only until per-API billing exists)
// Verify a history tree proof against the current ltHISTORY_TREE peaks
// and return the leaf hash. An empty blob means "the originating
// transaction's own sfHistoryProof and sfHistoryLeafIndex".
Expected<uint256, HookReturnCode>
history_verify(Slice blob, uint64_t leafIndex) const;
Expected<Keylet, HookReturnCode>
ledger_keylet(Keylet const& klLo, Keylet const& klHi) const;
+44 -27
View File
@@ -3,11 +3,13 @@
#include <xrpld/app/hook/HookAPI.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/ledger/TransactionMaster.h>
#include <xrpld/app/tx/detail/HistoryProof.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpld/ledger/ApplyViewImpl.h>
#include <xrpl/protocol/PQPublicKey.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <algorithm>
#include <limits>
@@ -45,37 +47,15 @@ Expected<bool, HookReturnCode>
HookAPI::util_verify(Slice const& data, Slice const& sig, Slice const& key)
const
{
if (key.size() != 33)
return Unexpected(INVALID_KEY);
if (data.size() == 0)
return Unexpected(TOO_SMALL);
if (sig.size() < 30)
return Unexpected(TOO_SMALL);
auto const& view = hookCtx.applyCtx.view();
if (view.rules().enabled(featurePQSignatures) && key.size() != 33)
{
if (pq::isInlineKey(key))
return pq::verify(key, data, sig);
if (pq::isKeyReference(key))
{
auto const id = pq::referenceAccountID(key);
auto const inlineKey = pqkey::readInlineKey(view, id);
if (!inlineKey)
return Unexpected(DOESNT_EXIST);
// Never trust the stored bytes blindly: they must hash to the
// AccountID the reference names.
if (pq::calcAccountID(makeSlice(*inlineKey)) != id)
return Unexpected(INVALID_KEY);
return pq::verify(makeSlice(*inlineKey), data, sig);
}
return Unexpected(INVALID_KEY);
}
if (key.size() != 33)
return Unexpected(INVALID_KEY);
if (!publicKeyType(key))
return Unexpected(INVALID_KEY);
@@ -2015,6 +1995,43 @@ HookAPI::rng_next() const
return out;
}
// featureHistoryTree
Expected<uint256, HookReturnCode>
HookAPI::history_verify(Slice blob, uint64_t leafIndex) const
{
auto& applyCtx = hookCtx.applyCtx;
auto const& view = applyCtx.view();
if (!view.rules().enabled(featureHistoryTree))
return Unexpected(NOT_IMPLEMENTED);
// Until per-API billing exists only the genesis account's Hooks may pay
// for a native verification.
static auto const genesisAccountId = calcAccountID(
generateKeyPair(KeyType::secp256k1, generateSeed("masterpassphrase"))
.first);
if (hookCtx.result.account != genesisAccountId)
return Unexpected(NOT_AUTHORIZED);
Blob own;
if (blob.empty())
{
auto const& tx = applyCtx.tx;
if (tx.getTxnType() != ttHISTORY_PROOF ||
!tx.isFieldPresent(sfHistoryProof))
return Unexpected(INVALID_TXN);
own = tx.getFieldVL(sfHistoryProof);
blob = makeSlice(own);
leafIndex = tx.getFieldU64(sfHistoryLeafIndex);
}
auto const leaf = HistoryProof::verify(view, leafIndex, blob);
if (!leaf)
return Unexpected(DOES_NOT_MATCH);
return *leaf;
}
Expected<uint64_t, HookReturnCode>
HookAPI::dice(uint32_t sides) const
{
+39 -14
View File
@@ -340,7 +340,8 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttCHECK_CREATE:
case ttACCOUNT_DELETE:
case ttPAYCHAN_CREATE:
case ttINVOKE: {
case ttINVOKE:
case ttHISTORY_PROOF: {
if (destAcc)
ADD_TSH(*destAcc, tshSTRONG);
break;
@@ -2227,18 +2228,12 @@ DEFINE_HOOK_FUNCTION(
case keylet_code::SIGNERS:
case keylet_code::ACCOUNT:
case keylet_code::HOOK:
case keylet_code::PQ_PUBLIC_KEY:
case keylet_code::DID: {
if (keylet_type == keylet_code::DID)
{
if (!applyCtx.view().rules().enabled(featureDID))
return INVALID_ARGUMENT;
}
if (keylet_type == keylet_code::PQ_PUBLIC_KEY)
{
if (!applyCtx.view().rules().enabled(featurePQSignatures))
return INVALID_ARGUMENT;
}
if (a == 0 || b == 0)
return INVALID_ARGUMENT;
@@ -2261,9 +2256,8 @@ DEFINE_HOOK_FUNCTION(
? ripple::keylet::signers(id)
: keylet_type == keylet_code::OWNER_DIR
? ripple::keylet::ownerDir(id)
: keylet_type == keylet_code::DID ? ripple::keylet::did(id)
: keylet_type == keylet_code::PQ_PUBLIC_KEY
? ripple::keylet::pqPublicKey(id)
: keylet_type == keylet_code::DID
? ripple::keylet::did(id)
: ripple::keylet::account(id);
return serialize_keylet(kl, memory, write_ptr, write_len);
@@ -2933,6 +2927,39 @@ DEFINE_HOOK_FUNCTION(
HOOK_TEARDOWN();
}
// featureHistoryTree: verify a proof (read_len == 0: the otxn's own proof)
// and write the 32 byte leaf hash
DEFINE_HOOK_FUNCTION(
int64_t,
history_verify,
uint32_t write_ptr,
uint32_t write_len,
uint32_t read_ptr,
uint32_t read_len,
uint64_t leaf_index)
{
HOOK_SETUP();
if (write_len < 32)
return TOO_SMALL;
if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length) ||
NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
return OUT_OF_BOUNDS;
auto const result = api.history_verify(
ripple::Slice{memory + read_ptr, read_len}, leaf_index);
if (!result)
return result.error();
auto const& leaf = result.value();
WRITE_WASM_MEMORY_AND_RETURN(
write_ptr, 32, leaf.data(), 32, memory, memory_length);
HOOK_TEARDOWN();
}
DEFINE_HOOK_FUNCTION(
int64_t,
ledger_keylet,
@@ -3351,10 +3378,8 @@ DEFINE_HOOK_FUNCTION(
}
// Validate either an secp256k1 signature or an ed25519 signature, using the
// XRPLD convention for identifying the key type. With featurePQSignatures the
// key may also be a post-quantum key, inline (0xF0 | type | key) or by
// reference (0xF1 | AccountID) to an ltPUBLIC_KEY object in the ledger.
// Pointer prefixes: d = data, s = signature, k = public key.
// XRPLD convention for identifying the key type. Pointer prefixes: d = data, s
// = signature, k = public key.
DEFINE_HOOK_FUNCTION(
int64_t,
util_verify,
+67
View File
@@ -43,6 +43,7 @@
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/HistoryTree.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
@@ -50,6 +51,7 @@
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <boost/optional.hpp>
#include <algorithm>
#include <utility>
#include <vector>
@@ -992,6 +994,71 @@ Ledger::updateSkipList()
rawReplace(sle);
}
// featureHistoryTree
void
Ledger::updateHistoryTree()
{
if (!rules().enabled(featureHistoryTree))
return;
auto const k = keylet::historyTree();
auto sle = peek(k);
bool const created = !sle;
if (created)
{
sle = std::make_shared<SLE>(k);
sle->setFieldU64(sfHistoryLeafCount, 0);
}
// Leaves are appended in TransactionIndex order, which is the order the
// transactions were applied, so every node computes the same tree.
std::vector<std::pair<std::uint32_t, uint256>> leaves;
for (auto const& item : txMap_)
{
SerialIter sit(item.slice());
Slice const tx = sit.getSlice(sit.getVLDataLength());
Slice const meta = sit.getSlice(sit.getVLDataLength());
std::uint32_t index = 0;
{
SerialIter ms(meta);
STObject const m(ms, sfMetadata);
index = m.getFieldU32(sfTransactionIndex);
}
leaves.emplace_back(
index, history::leafHash(info_.seq, index, tx, meta));
}
if (leaves.empty() && !created)
return;
std::sort(leaves.begin(), leaves.end(), [](auto const& a, auto const& b) {
return a.first < b.first;
});
std::vector<uint256> peaks;
if (sle->isFieldPresent(sfHistoryPeaks))
peaks = static_cast<std::vector<uint256>>(
sle->getFieldV256(sfHistoryPeaks));
std::uint64_t count = sle->getFieldU64(sfHistoryLeafCount);
for (auto const& [index, leaf] : leaves)
history::append(peaks, count, leaf);
sle->setFieldU64(sfHistoryLeafCount, count);
sle->setFieldU32(sfLedgerSequence, info_.seq);
if (peaks.empty())
sle->makeFieldAbsent(sfHistoryPeaks);
else
sle->setFieldV256(sfHistoryPeaks, STVector256(peaks));
if (created)
rawInsert(sle);
else
rawReplace(sle);
}
bool
Ledger::isFlagLedger() const
{
+7
View File
@@ -352,6 +352,13 @@ public:
void
updateSkipList();
/** featureHistoryTree: append this ledger's transactions, in
TransactionIndex order, to the history MMR kept in ltHISTORY_TREE.
Called once at close after all transactions are applied.
*/
void
updateHistoryTree();
bool
walkLedger(beast::Journal j, bool parallel = false) const;
+8 -1
View File
@@ -64,6 +64,7 @@ buildLedgerImpl(
}
built->updateSkipList();
built->updateHistoryTree();
{
// Write the final version of all modified SHAMap
// nodes to the node store to preserve the new LCL
@@ -271,7 +272,13 @@ buildLedger(
j,
[&](OpenView& accum, std::shared_ptr<Ledger> const& built) {
for (auto& tx : replayData.orderedTxns())
applyTransaction(app, accum, *tx.second, false, applyFlags, j);
applyTransaction(
app,
accum,
*tx.second,
false,
applyFlags | tapHISTORY_REPLAY,
j);
});
}
+17 -1
View File
@@ -1260,13 +1260,29 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
Section const& upVoted = config_->section(SECTION_AMENDMENTS);
Section const& ignored = config_->section(SECTION_IGNORE_AMENDMENTS);
// [ignore_amendments] exists to keep long-lived test networks syncable
// after an amendment they activated is removed from the codebase. It
// must never be usable on a production network.
if (!ignored.lines().empty() &&
(config_->NETWORK_ID == 0 || config_->NETWORK_ID == 21337))
{
JLOG(m_journal.fatal()) << "[" SECTION_IGNORE_AMENDMENTS
"] is not permitted on "
"network_id "
<< config_->NETWORK_ID;
return false;
}
m_amendmentTable = make_AmendmentTable(
*this,
config().AMENDMENT_MAJORITY_TIME,
supported,
upVoted,
downVoted,
logs_->journal("Amendments"));
logs_->journal("Amendments"),
ignored);
}
Pathfinder::initPathTable();
+2 -1
View File
@@ -211,7 +211,8 @@ make_AmendmentTable(
std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal);
beast::Journal journal,
Section const& ignored = Section{});
} // namespace ripple
+2 -6
View File
@@ -1169,9 +1169,7 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
app_.getHashRouter(),
*trans,
m_ledgerMaster.getValidatedRules(),
app_.config(),
tapNONE,
app_.openLedger().current());
app_.config());
if (validity != Validity::Valid)
{
@@ -1420,9 +1418,7 @@ NetworkOPsImp::processTransaction(
app_.getHashRouter(),
*transaction->getSTransaction(),
view->rules(),
app_.config(),
tapNONE,
view);
app_.config());
XRPL_ASSERT(
validity == Validity::Valid,
"ripple::NetworkOPsImp::processTransaction : valid validity");
+78 -6
View File
@@ -302,6 +302,15 @@ struct AmendmentState
/** Indicates an amendment that this server has code support for. */
bool supported = false;
/** Indicates an amendment listed in [ignore_amendments]. The server has
no code for it, but must not become amendment blocked if it is (or
becomes) enabled on the ledger. Such an amendment is reported as
`supported` internally so every blocking check (LedgerMaster,
NetworkOPs, Change) passes, but is never voted for and is shown as
unsupported + ignored over RPC.
*/
bool ignored = false;
/** The name of this amendment, possibly empty. */
std::string name;
@@ -476,7 +485,8 @@ public:
std::vector<FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal);
beast::Journal journal,
Section const& ignored);
uint256
find(std::string const& name) const override;
@@ -543,7 +553,8 @@ AmendmentTableImpl::AmendmentTableImpl(
std::vector<FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal)
beast::Journal journal,
Section const& ignored)
: lastUpdateSeq_(0)
, majorityTime_(majorityTime)
, unsupportedEnabled_(false)
@@ -586,6 +597,59 @@ AmendmentTableImpl::AmendmentTableImpl(
<< " voted by default if not enabled on the ledger.";
}
// Parse ignored amendments from config. Each line is either a registered
// feature name (e.g. "HookOnV2") or "<64 hex amendment ID> [name]". These
// are deliberately NOT persisted to wallet.db: removing the line from the
// config must restore normal amendment-blocking behaviour.
for (auto const& line : ignored.lines())
{
std::string const trimmed = boost::algorithm::trim_copy(line);
if (trimmed.empty())
continue;
std::vector<std::string> parts;
boost::algorithm::split(
parts,
trimmed,
boost::algorithm::is_space(),
boost::algorithm::token_compress_on);
uint256 id;
std::string name;
if (id.parseHex(parts[0]))
name = parts.size() > 1 ? parts[1] : std::string{};
else if (auto const f = getRegisteredFeature(parts[0]))
{
id = *f;
name = parts[0];
}
else
Throw<std::runtime_error>(
"Invalid entry '" + line + "' in [" + ignored.name() +
"]: expected a registered feature name or amendment ID");
AmendmentState& s = add(id, lock);
if (s.supported)
{
JLOG(j_.warn()) << "[" << ignored.name() << "] lists amendment "
<< id << " (" << s.name
<< ") which this server supports; entry has no "
"effect.";
continue;
}
if (!name.empty())
s.name = name;
s.supported = true;
s.ignored = true;
s.vote = AmendmentVote::obsolete;
JLOG(j_.warn()) << "Amendment " << id << " (" << s.name
<< ") is unsupported but listed in [" << ignored.name()
<< "]: this server will NOT become amendment blocked "
"if it is enabled on the ledger.";
}
hash_set<uint256> detect_conflict;
// Parse enabled amendments from config
for (std::pair<uint256, std::string> const& a : parseSection(enabled))
@@ -775,7 +839,12 @@ AmendmentTableImpl::enable(uint256 const& amendment)
s.enabled = true;
if (!s.supported)
if (s.ignored)
{
JLOG(j_.warn()) << "Ignored unsupported amendment " << amendment << " ("
<< s.name << ") is enabled on the ledger.";
}
else if (!s.supported)
{
JLOG(j_.error()) << "Unsupported amendment " << amendment
<< " activated.";
@@ -1044,7 +1113,9 @@ AmendmentTableImpl::injectJson(
if (!fs.name.empty())
v[jss::name] = fs.name;
v[jss::supported] = fs.supported;
v[jss::supported] = fs.supported && !fs.ignored;
if (fs.ignored)
v["ignored"] = true;
if (!fs.enabled && isAdmin)
{
if (fs.vote == AmendmentVote::obsolete)
@@ -1113,10 +1184,11 @@ make_AmendmentTable(
std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal)
beast::Journal journal,
Section const& ignored)
{
return std::make_unique<AmendmentTableImpl>(
app, majorityTime, supported, enabled, vetoed, journal);
app, majorityTime, supported, enabled, vetoed, journal, ignored);
}
} // namespace ripple
+1 -14
View File
@@ -58,18 +58,6 @@ enum class Validity {
@return `std::pair`, where `.first` is the status, and
`.second` is the reason if appropriate.
@param pqKeyLedger A ledger used to resolve post-quantum key
*references* (featurePQSignatures, SigningPubKey of the form
0xF1 | AccountID). Every ingress path (network, RPC submit, sign)
must pass its open ledger, so that unverifiable transactions are
not relayed. A reference whose key is missing from that ledger is
reported SigBad but is NOT cached as bad: the key may exist in the
ledger the transaction is eventually applied to. When no ledger is
given (the transactor engine's preflight), reference signatures are
deferred to preclaim, where Transactor::checkSign verifies them
against the view the transaction is applied to; inline keys are
verified here as usual.
@see Validity
*/
std::pair<Validity, std::string>
@@ -78,8 +66,7 @@ checkValidity(
STTx const& tx,
Rules const& rules,
Config const& config,
ApplyFlags const flags = tapNONE,
std::shared_ptr<ReadView const> const& pqKeyLedger = nullptr);
ApplyFlags const flags = tapNONE);
/** Ingress fee outcome for a manifest-authorized SetManifest.
@@ -23,7 +23,6 @@
#include <xrpld/app/tx/detail/DeleteOracle.h>
#include <xrpld/app/tx/detail/DepositPreauth.h>
#include <xrpld/app/tx/detail/NFTokenUtils.h>
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpld/app/tx/detail/SetSignerList.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
@@ -494,14 +493,6 @@ DeleteAccount::doApply()
if (mSourceBalance > XRPAmount(0) && dst->isFlag(lsfPasswordSpent))
dst->clearFlag(lsfPasswordSpent);
// featurePQSignatures: release the references held by this account's
// master key (if enabled) and regular key slots. The signer list's
// entries were released when the list was deleted above.
if (!src->isFlag(lsfDisableMaster))
pqkey::removeReference(view(), account_, j_);
if (src->isFieldPresent(sfRegularKey))
pqkey::removeReference(view(), src->getAccountID(sfRegularKey), j_);
view().update(dst);
view().erase(src);
+159
View File
@@ -0,0 +1,159 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/HistoryProof.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/st.h>
namespace ripple {
std::optional<uint256>
HistoryProof::verify(ReadView const& view, std::uint64_t leafIndex, Slice blob)
{
auto const sle = view.read(keylet::historyTree());
if (!sle)
return std::nullopt;
auto const proof = history::parseProof(blob);
if (!proof)
return std::nullopt;
std::vector<uint256> peaks;
if (sle->isFieldPresent(sfHistoryPeaks))
peaks = static_cast<std::vector<uint256>>(
sle->getFieldV256(sfHistoryPeaks));
uint256 leaf;
if (!history::verifyProof(
peaks,
sle->getFieldU64(sfHistoryLeafCount),
leafIndex,
*proof,
&leaf))
return std::nullopt;
return leaf;
}
NotTEC
HistoryProof::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureHistoryTree))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
auto const& tx = ctx.tx;
if (tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
bool const replay = (ctx.flags & tapHISTORY_REPLAY) != 0;
if (!tx.isFieldPresent(sfHistoryProof))
{
// Only the stored, clipped form lacks the proof, and only a replay of
// an already validated ledger may apply that form.
if (!replay)
{
JLOG(ctx.j.warn()) << "HistoryProof: missing proof.";
return temMALFORMED;
}
}
else
{
auto const blob = tx.getFieldVL(sfHistoryProof);
// STTx's constructors already enforce this; keep it as a belt for
// transactions assembled in-process.
if (sha512Half(makeSlice(blob)) !=
tx.getFieldH256(sfHistoryProofDigest))
return temMALFORMED;
if (!history::parseProof(makeSlice(blob)))
{
JLOG(ctx.j.warn()) << "HistoryProof: unparseable proof.";
return temMALFORMED;
}
}
return preflight2(ctx);
}
TER
HistoryProof::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
if (tx.isFieldPresent(sfDestination))
{
auto const sleDest = ctx.view.read(keylet::account(tx[sfDestination]));
if (!sleDest)
return tecNO_TARGET;
}
if (!tx.isFieldPresent(sfHistoryProof))
{
// Replay of a validated ledger (preflight admitted this form only
// under tapHISTORY_REPLAY): the proof was checked when the ledger
// was first built, trust it.
return tesSUCCESS;
}
if (!ctx.view.read(keylet::historyTree()))
return tecINTERNAL; // feature enabled, object missing
if (!verify(
ctx.view,
tx.getFieldU64(sfHistoryLeafIndex),
makeSlice(tx.getFieldVL(sfHistoryProof))))
{
JLOG(ctx.j.debug()) << "HistoryProof: proof does not verify.";
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
HistoryProof::doApply()
{
// The proof was verified in preclaim; everything else is the Hook's.
return tesSUCCESS;
}
XRPAmount
HistoryProof::calculateBaseFee(ReadView const& view, STTx const& tx)
{
// The proof is not stored, so it costs relay bandwidth and a verification
// rather than ledger space: one drop per 32 bytes.
XRPAmount extra{0};
if (tx.isFieldPresent(sfHistoryProof))
extra = XRPAmount{static_cast<std::int64_t>(
tx.getFieldVL(sfHistoryProof).size() / 32)};
return Transactor::calculateBaseFee(view, tx) + extra;
}
} // namespace ripple
+66
View File
@@ -0,0 +1,66 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_HISTORYPROOF_H_INCLUDED
#define RIPPLE_TX_HISTORYPROOF_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpl/protocol/HistoryTree.h>
namespace ripple {
/** featureHistoryTree: prove possession of one history tree leaf.
The transactor only establishes that the proof is genuine against the
current ltHISTORY_TREE peaks. Who was challenged, who gets paid and who is
slashed is the genesis Hook's business; it runs as the strong TSH on
sfDestination and can read the full transaction, including the transient
sfHistoryProof, before the stored form is clipped.
*/
class HistoryProof : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit HistoryProof(ApplyContext& ctx) : Transactor(ctx)
{
}
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
/** Verify a proof blob against the tree in view. Shared with the
history_verify Hook API. Returns the leaf hash on success.
*/
static std::optional<uint256>
verify(ReadView const& view, std::uint64_t leafIndex, Slice blob);
};
} // namespace ripple
#endif
-10
View File
@@ -19,7 +19,6 @@
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpld/app/tx/detail/SetSignerList.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
@@ -1194,22 +1193,13 @@ Import::doRegularKey(std::shared_ptr<SLE>& sle, STTx const& stpTrans)
JLOG(ctx_.journal.trace()) << "Import: clearing SetRegularKey "
<< " acc: " << id;
if (sle->isFieldPresent(sfRegularKey))
{
pqkey::removeReference(
ctx_.view(), sle->getAccountID(sfRegularKey), ctx_.journal);
sle->makeFieldAbsent(sfRegularKey);
}
return;
}
AccountID rk = stpTrans.getAccountID(sfRegularKey);
JLOG(ctx_.journal.trace())
<< "Import: actioning SetRegularKey " << rk << " acc: " << id;
// featurePQSignatures: keep the key object reference counts in step.
std::vector<AccountID> const oldKey = sle->isFieldPresent(sfRegularKey)
? std::vector<AccountID>{sle->getAccountID(sfRegularKey)}
: std::vector<AccountID>{};
pqkey::adjustReferences(ctx_.view(), oldKey, {rk}, ctx_.journal);
sle->setAccountID(sfRegularKey, rk);
// always set this flag if they have done any regular keying
+1 -55
View File
@@ -32,7 +32,6 @@
#include <xrpl/protocol/AppLoader.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/FeeUnits.h>
#include <xrpl/protocol/PQPublicKey.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TxFormats.h>
@@ -606,6 +605,7 @@ LedgerEntryTypesMatch::visitEntry(
case ltIMPORT_VLSEQ:
case ltUNL_REPORT:
case ltRANDOM:
case ltHISTORY_TREE:
case ltAMM:
case ltBRIDGE:
case ltXCHAIN_OWNED_CLAIM_ID:
@@ -618,7 +618,6 @@ LedgerEntryTypesMatch::visitEntry(
case ltPERMISSIONED_DOMAIN:
case ltAPP_LOADER:
case ltMANIFEST:
case ltPUBLIC_KEY:
break;
default:
invalidTypeAdded_ = true;
@@ -2082,57 +2081,4 @@ ValidAppLoader::finalize(
return true;
}
//------------------------------------------------------------------------------
void
ValidPQPublicKey::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
auto const& sle = after ? after : before;
if (!sle || sle->getType() != ltPUBLIC_KEY)
return;
touched_ = true;
if (!isDelete)
live_.push_back(after);
}
bool
ValidPQPublicKey::finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
if (!touched_)
return true;
auto fail = [&](char const* why) {
JLOG(j.fatal()) << "Invariant failed: PublicKey " << why;
return false;
};
if (!view.rules().enabled(featurePQSignatures))
return fail("changed before featurePQSignatures was enabled");
for (auto const& sle : live_)
{
auto const type = pq::keyTypeFromByte((*sle)[sfPublicKeyType]);
if (!type)
return fail("has an unknown key type");
auto const& data = sle->getFieldVL(sfPublicKeyData);
if (data.size() != pq::publicKeyBytes(*type))
return fail("has key data of the wrong size");
auto const id = pq::calcAccountID(
makeSlice(pq::makeInlineKey(*type, makeSlice(data))));
if (sle->key() != keylet::pqPublicKey(id).key)
return fail("is not keyed at the AccountID of its key");
if ((*sle)[sfReferenceCount] < 1)
return fail("has a zero reference count");
}
return true;
}
} // namespace ripple
+1 -31
View File
@@ -739,35 +739,6 @@ public:
beast::Journal const&);
};
/**
* @brief Invariant: every ltPUBLIC_KEY object is well formed.
*
* For every ltPUBLIC_KEY created or changed by a transaction:
* featurePQSignatures is enabled; its key is the keylet of the AccountID the
* stored key hashes to; the key data has the exact size for its type; and
* its ReferenceCount is at least one.
*/
class ValidPQPublicKey
{
bool touched_ = false;
std::vector<std::shared_ptr<SLE const>> live_;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
using InvariantChecks = std::tuple<
TransactionFeeCheck,
AccountRootsNotDeleted,
@@ -788,8 +759,7 @@ using InvariantChecks = std::tuple<
ValidPermissionedDomain,
ValidAMM,
ValidLockedBalance,
ValidAppLoader,
ValidPQPublicKey>;
ValidAppLoader>;
/**
* @brief get a tuple of all invariant checks
-170
View File
@@ -1,170 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of xahaud: https://github.com/Xahau/xahaud
Copyright (c) 2026 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PQPublicKey.h>
#include <xrpl/protocol/TER.h>
#include <algorithm>
#include <iterator>
namespace ripple {
namespace pqkey {
std::shared_ptr<SLE const>
read(ReadView const& view, AccountID const& id)
{
if (!view.rules().enabled(featurePQSignatures))
return nullptr;
return view.read(keylet::pqPublicKey(id));
}
std::optional<Blob>
readInlineKey(ReadView const& view, AccountID const& id)
{
auto const sle = read(view, id);
if (!sle)
return std::nullopt;
auto const type = pq::keyTypeFromByte(sle->getFieldU8(sfPublicKeyType));
if (!type)
return std::nullopt;
return pq::makeInlineKey(*type, makeSlice(sle->getFieldVL(sfPublicKeyData)));
}
STTx::PQKeyLookup
makeLookup(ReadView const& view)
{
return [&view](AccountID const& id) { return readInlineKey(view, id); };
}
std::uint64_t
unrevealedKeyBytes(ReadView const& view, STTx const& tx)
{
if (!view.rules().enabled(featurePQSignatures))
return 0;
std::uint64_t bytes = 0;
for (auto const& [id, key] : tx.inlinePQKeys())
if (!view.exists(keylet::pqPublicKey(id)))
bytes += key.size() - 2; // the prefix and type byte are not stored
return bytes;
}
TER
reveal(ApplyView& view, STTx const& tx, beast::Journal j)
{
if (!view.rules().enabled(featurePQSignatures))
return tesSUCCESS;
for (auto const& [id, key] : tx.inlinePQKeys())
{
auto const k = keylet::pqPublicKey(id);
if (view.exists(k))
continue;
auto const inlineKey = makeSlice(key);
auto const type = pq::inlineKeyType(inlineKey);
if (!type)
return tefINTERNAL; // LCOV_EXCL_LINE: inlinePQKeys() filtered
auto sle = std::make_shared<SLE>(k);
sle->setFieldU8(sfPublicKeyType, static_cast<std::uint8_t>(*type));
sle->setFieldVL(sfPublicKeyData, pq::rawKey(inlineKey));
sle->setFieldU64(sfReferenceCount, 1);
view.insert(sle);
JLOG(j.trace()) << "PQ public key revealed for " << toBase58(id);
}
return tesSUCCESS;
}
void
addReference(ApplyView& view, AccountID const& id, beast::Journal j)
{
if (!view.rules().enabled(featurePQSignatures))
return;
auto sle = view.peek(keylet::pqPublicKey(id));
if (!sle)
return;
sle->setFieldU64(sfReferenceCount, sle->getFieldU64(sfReferenceCount) + 1);
view.update(sle);
JLOG(j.trace()) << "PQ public key " << toBase58(id) << " refcount +1";
}
void
removeReference(ApplyView& view, AccountID const& id, beast::Journal j)
{
if (!view.rules().enabled(featurePQSignatures))
return;
auto sle = view.peek(keylet::pqPublicKey(id));
if (!sle)
return;
auto const count = sle->getFieldU64(sfReferenceCount);
if (count <= 1)
{
view.erase(sle);
JLOG(j.trace()) << "PQ public key " << toBase58(id) << " deleted";
return;
}
sle->setFieldU64(sfReferenceCount, count - 1);
view.update(sle);
JLOG(j.trace()) << "PQ public key " << toBase58(id) << " refcount -1";
}
void
adjustReferences(
ApplyView& view,
std::vector<AccountID> before,
std::vector<AccountID> after,
beast::Journal j)
{
if (!view.rules().enabled(featurePQSignatures))
return;
auto normalize = [](std::vector<AccountID>& v) {
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());
};
normalize(before);
normalize(after);
std::vector<AccountID> removed;
std::vector<AccountID> added;
std::set_difference(
before.begin(),
before.end(),
after.begin(),
after.end(),
std::back_inserter(removed));
std::set_difference(
after.begin(),
after.end(),
before.begin(),
before.end(),
std::back_inserter(added));
for (auto const& id : removed)
removeReference(view, id, j);
for (auto const& id : added)
addReference(view, id, j);
}
} // namespace pqkey
} // namespace ripple
-101
View File
@@ -1,101 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of xahaud: https://github.com/Xahau/xahaud
Copyright (c) 2026 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_PQKEYLEDGER_H_INCLUDED
#define RIPPLE_TX_PQKEYLEDGER_H_INCLUDED
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/ReadView.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/XRPAmount.h>
#include <optional>
#include <vector>
namespace ripple {
/** The ltPUBLIC_KEY ledger object (featurePQSignatures).
Every function here is a no-op unless featurePQSignatures is enabled in
the view's rules, so callers in consensus code need no extra gating.
Reference counting follows ltHOOK_DEFINITION: it is a garbage-collection
hint, not something correctness depends on. The count is incremented
when a slot that names the key (AccountRoot master key, RegularKey,
SignerEntry) is written while the object exists and decremented when
such a slot is cleared while it exists; the object is deleted at zero.
A slot written *before* the key was revealed is simply not counted, and
the worst case for a stale reference is that the key must be revealed
inline again (paying the byte fee again). Verification of a reference
always requires the object to be present, so a stale count can never
make a bad signature pass.
*/
namespace pqkey {
/// The ltPUBLIC_KEY object for the key whose AccountID is `id`, if any
std::shared_ptr<SLE const>
read(ReadView const& view, AccountID const& id);
/// The inline (0xF0 | type | key) encoding stored for `id`, if any
std::optional<Blob>
readInlineKey(ReadView const& view, AccountID const& id);
/** A lookup for STTx::checkSign that resolves references against `view`.
The view must outlive the returned function. */
STTx::PQKeyLookup
makeLookup(ReadView const& view);
/** Bytes of inline PQ keys in `tx` that `view` does not yet hold an
object for. The publication charge is one drop per byte. */
std::uint64_t
unrevealedKeyBytes(ReadView const& view, STTx const& tx);
/** Create an ltPUBLIC_KEY object (ReferenceCount = 1) for every inline PQ
key in `tx` that the view does not yet hold. Called by Transactor after
the fee is charged, on the tes and the tec path alike. */
TER
reveal(ApplyView& view, STTx const& tx, beast::Journal j);
/** If `view` holds a key object for `id`, increment its reference count. */
void
addReference(ApplyView& view, AccountID const& id, beast::Journal j);
/** If `view` holds a key object for `id`, decrement its reference count,
deleting the object when it reaches zero. */
void
removeReference(ApplyView& view, AccountID const& id, beast::Journal j);
/** Net reference adjustment when a set of referencing slots changes from
`before` to `after` (e.g. a SignerList replacement): ids only in
`before` lose a reference, ids only in `after` gain one. Both vectors
may be unsorted and may contain duplicates. */
void
adjustReferences(
ApplyView& view,
std::vector<AccountID> before,
std::vector<AccountID> after,
beast::Journal j);
} // namespace pqkey
} // namespace ripple
#endif
-5
View File
@@ -17,7 +17,6 @@
*/
//==============================================================================
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpld/app/tx/detail/SetAccount.h>
#include <xrpld/core/Config.h>
#include <xrpld/ledger/View.h>
@@ -429,16 +428,12 @@ SetAccount::doApply()
JLOG(j_.trace()) << "Set lsfDisableMaster.";
uFlagsOut |= lsfDisableMaster;
// featurePQSignatures: an enabled master key references its key
// object; disabling the master releases that reference.
pqkey::removeReference(view(), account_, j_);
}
if ((uClearFlag == asfDisableMaster) && (uFlagsIn & lsfDisableMaster))
{
JLOG(j_.trace()) << "Clear lsfDisableMaster.";
uFlagsOut &= ~lsfDisableMaster;
pqkey::addReference(view(), account_, j_);
}
//
-10
View File
@@ -17,7 +17,6 @@
*/
//==============================================================================
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpld/app/tx/detail/SetRegularKey.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
@@ -83,17 +82,9 @@ SetRegularKey::doApply()
if (!minimumFee(ctx_.app, ctx_.baseFee, view().fees(), view().flags()))
sle->setFlag(lsfPasswordSpent);
// featurePQSignatures: the RegularKey slot is a reference to a
// post-quantum key object (if one exists for that AccountID).
std::vector<AccountID> const oldKey = sle->isFieldPresent(sfRegularKey)
? std::vector<AccountID>{sle->getAccountID(sfRegularKey)}
: std::vector<AccountID>{};
std::vector<AccountID> newKey;
if (ctx_.tx.isFieldPresent(sfRegularKey))
{
sle->setAccountID(sfRegularKey, ctx_.tx.getAccountID(sfRegularKey));
newKey.push_back(ctx_.tx.getAccountID(sfRegularKey));
}
else
{
@@ -106,7 +97,6 @@ SetRegularKey::doApply()
}
ctx_.view().update(sle);
pqkey::adjustReferences(view(), oldKey, newKey, j_);
return tesSUCCESS;
}
+1 -7
View File
@@ -201,8 +201,7 @@ SetSignerList::removeSignersFromLedger(
Keylet const& accountKeylet,
Keylet const& ownerDirKeylet,
Keylet const& signerListKeylet,
beast::Journal j,
bool releasePQReferences)
beast::Journal j)
{
// We have to examine the current SignerList so we know how much to
// reduce the OwnerCount.
@@ -212,11 +211,6 @@ SetSignerList::removeSignersFromLedger(
if (!signers)
return tesSUCCESS;
// featurePQSignatures: each entry held a reference to its key object.
if (releasePQReferences)
for (auto const& e : signers->getFieldArray(sfSignerEntries))
pqkey::removeReference(view, e.getAccountID(sfAccount), j);
// There are two different ways that the OwnerCount could be managed.
// If the lsfOneOwnerCount bit is set then remove just one owner count.
// Otherwise use the pre-MultiSignReserve amendment calculation.
+2 -25
View File
@@ -21,7 +21,6 @@
#define RIPPLE_TX_SETSIGNERLIST_H_INCLUDED
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpld/app/tx/detail/SignerEntries.h>
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpl/basics/Log.h>
@@ -79,8 +78,7 @@ public:
Keylet const& accountKeylet,
Keylet const& ownerDirKeylet,
Keylet const& signerListKeylet,
beast::Journal j,
bool releasePQReferences = true);
beast::Journal j);
static int
signerCountBasedOwnerCountDelta(std::size_t entryCount, Rules const& rules);
@@ -134,26 +132,11 @@ public:
auto const ownerDirKeylet = keylet::ownerDir(acc);
auto const signerListKeylet = keylet::signers(acc);
// featurePQSignatures: the signer entries are references to
// post-quantum key objects. Adjust by the net change, so that
// replacing a list with one that keeps a signer does not drop (and
// possibly delete) that signer's key object only to re-reference it.
std::vector<AccountID> oldSigners;
if (auto const old = view.read(signerListKeylet))
for (auto const& e : old->getFieldArray(sfSignerEntries))
oldSigners.push_back(e.getAccountID(sfAccount));
// This may be either a create or a replace. Preemptively remove any
// old signer list. May reduce the reserve, so this is done before
// checking the reserve.
if (TER const ter = removeSignersFromLedger(
app,
view,
accountKeylet,
ownerDirKeylet,
signerListKeylet,
j,
/*releasePQReferences=*/false))
app, view, accountKeylet, ownerDirKeylet, signerListKeylet, j))
return ter;
auto const sle = view.peek(accountKeylet);
@@ -203,12 +186,6 @@ public:
// If we succeeded, the new entry counts against the
// creator's reserve.
adjustOwnerCount(view, sle, addedOwnerCount, j);
std::vector<AccountID> newSigners;
for (auto const& e : signers)
newSigners.push_back(e.account);
pqkey::adjustReferences(
view, std::move(oldSigners), std::move(newSigners), j);
return tesSUCCESS;
}
};
+8 -127
View File
@@ -24,7 +24,6 @@
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/NFTokenUtils.h>
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpld/app/tx/detail/SetHook.h>
#include <xrpld/app/tx/detail/SignerEntries.h>
#include <xrpld/app/tx/detail/Transactor.h>
@@ -38,7 +37,6 @@
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PQPublicKey.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/UintTypes.h>
@@ -443,19 +441,6 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
}
}
// featurePQSignatures: publishing a post-quantum public key to the
// ledger costs one drop per byte, charged on the transaction that
// first carries the key inline. Like memo bytes it is part of the
// accumulator and therefore scales with the load fee.
if (view.rules().enabled(featurePQSignatures))
{
XRPAmount const keyBytes{
static_cast<std::int64_t>(pqkey::unrevealedKeyBytes(view, tx))};
if (accumulator + keyBytes < accumulator)
return XRPAmount{INITIAL_XRP.drops()};
accumulator += keyBytes;
}
// RH NOTE: hookExecutionFee = 0, burden = 1 if hooks is not enabled
// The calculation is (baseFee * burden) + (signerCount * baseFee) +
@@ -892,13 +877,6 @@ Transactor::apply()
sle->setFieldH256(sfAccountTxnID, ctx_.tx.getTransactionID());
view().update(sle);
// featurePQSignatures: the byte fee was just paid, so publish any
// inline post-quantum keys now. reset() repeats this on the tec
// path so that a claimed fee always buys the key object.
result = pqkey::reveal(view(), ctx_.tx, j_);
if (!isTesSuccess(result))
return result;
}
return doApply();
@@ -953,62 +931,12 @@ Transactor::checkSign(PreclaimContext const& ctx)
return checkSingleSign(ctx);
}
// featurePQSignatures. The AccountID a signing key stands for, in any of
// the supported encodings, or std::nullopt if the key is not acceptable.
// For a post-quantum key *reference* the ledger object must exist; its
// signature is then verified here, against this view, because the
// signature check at ingress / preflight could not (see checkValidity).
static std::optional<AccountID>
signerAccountID(
PreclaimContext const& ctx,
Blob const& spk,
std::function<Expected<void, std::string>(STTx::PQKeyLookup const&)>
verifyWithLedger)
{
auto const key = makeSlice(spk);
if (publicKeyType(key))
return calcAccountID(PublicKey(key));
if (!ctx.view.rules().enabled(featurePQSignatures))
return std::nullopt;
if (pq::isInlineKey(key))
return pq::calcAccountID(key);
if (pq::isKeyReference(key))
{
auto const id = pq::referenceAccountID(key);
if (!pqkey::read(ctx.view, id))
{
JLOG(ctx.j.trace())
<< "PQ key reference " << toBase58(id) << " not in ledger";
return std::nullopt;
}
if (!(ctx.flags & tapDRY_RUN))
{
auto const ok = verifyWithLedger(pqkey::makeLookup(ctx.view));
if (!ok)
{
JLOG(ctx.j.trace())
<< "PQ key reference signature: " << ok.error();
return std::nullopt;
}
}
return id;
}
return std::nullopt;
}
NotTEC
Transactor::checkSingleSign(PreclaimContext const& ctx)
{
// Check that the value in the signing key slot is a public key.
auto const pkSigner = ctx.tx.getSigningPubKey();
bool const pqEnabled = ctx.view.rules().enabled(featurePQSignatures);
if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(pkSigner)) &&
!(pqEnabled &&
(pq::isInlineKey(makeSlice(pkSigner)) ||
pq::isKeyReference(makeSlice(pkSigner)))))
if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(pkSigner)))
{
JLOG(ctx.j.trace())
<< "checkSingleSign: signing public key type is unknown";
@@ -1026,25 +954,9 @@ Transactor::checkSingleSign(PreclaimContext const& ctx)
XRPL_ASSERT(
(ctx.flags & tapDRY_RUN) || !pkSigner.empty(),
"ripple::Transactor::checkSingleSign : non-empty signer or simulation");
std::optional<AccountID> idSignerOpt;
if (pkSigner.empty())
idSignerOpt = idAccount;
else
idSignerOpt = signerAccountID(
ctx, pkSigner, [&ctx](STTx::PQKeyLookup const& lookup) {
auto const requireCanonicalSig =
ctx.view.rules().enabled(featureRequireFullyCanonicalSig)
? STTx::RequireFullyCanonicalSig::yes
: STTx::RequireFullyCanonicalSig::no;
return ctx.tx.checkSign(
requireCanonicalSig,
ctx.view.rules(),
lookup,
STTx::PQReferencePolicy::reject);
});
if (!idSignerOpt)
return tefBAD_AUTH;
auto const idSigner = *idSignerOpt;
auto const idSigner = pkSigner.empty()
? idAccount
: calcAccountID(PublicKey(makeSlice(pkSigner)));
bool const isMasterDisabled = sleAccount->isFlag(lsfDisableMaster);
if (ctx.view.rules().enabled(fixMasterKeyAsRegularKey))
@@ -1166,12 +1078,8 @@ Transactor::checkMultiSign(PreclaimContext const& ctx)
// need to compute the accountID that is associated with the signer's
// public key.
auto const spk = txSigner.getFieldVL(sfSigningPubKey);
bool const pqEnabled = ctx.view.rules().enabled(featurePQSignatures);
if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk)) &&
!(pqEnabled &&
(pq::isInlineKey(makeSlice(spk)) ||
pq::isKeyReference(makeSlice(spk)))))
if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk)))
{
JLOG(ctx.j.trace())
<< "checkMultiSign: signing public key type is unknown";
@@ -1183,29 +1091,9 @@ Transactor::checkMultiSign(PreclaimContext const& ctx)
(ctx.flags & tapDRY_RUN) || !spk.empty(),
"ripple::Transactor::checkMultiSign : non-empty signer or "
"simulation");
std::optional<AccountID> fromPubKey;
if (spk.empty())
fromPubKey = txSignerAcctID;
else
fromPubKey = signerAccountID(
ctx, spk, [&ctx](STTx::PQKeyLookup const& lookup) {
// Verifies every signer; the whole Signers array is
// checked once per reference-form signer, which is
// cheap next to the verification itself.
auto const requireCanonicalSig =
ctx.view.rules().enabled(
featureRequireFullyCanonicalSig)
? STTx::RequireFullyCanonicalSig::yes
: STTx::RequireFullyCanonicalSig::no;
return ctx.tx.checkSign(
requireCanonicalSig,
ctx.view.rules(),
lookup,
STTx::PQReferencePolicy::reject);
});
if (!fromPubKey)
return tefBAD_SIGNATURE;
AccountID const signingAcctIDFromPubKey = *fromPubKey;
AccountID const signingAcctIDFromPubKey = spk.empty()
? txSignerAcctID
: calcAccountID(PublicKey(makeSlice(spk)));
// Verify that the signingAcctID and the signingAcctIDFromPubKey
// belong together. Here are the rules:
@@ -1473,14 +1361,7 @@ Transactor::reset(XRPAmount fee)
isTesSuccess(ter), "ripple::Transactor::reset : result is tesSUCCESS");
if (isTesSuccess(ter))
{
view().update(txnAcct);
// featurePQSignatures: the view was discarded, so the inline key
// objects created in apply() are gone; recreate them, the fee for
// them has just been charged again.
if (TER const r = pqkey::reveal(view(), ctx_.tx, j_); !isTesSuccess(r))
return {r, fee};
}
return {ter, fee};
}
+2 -32
View File
@@ -25,7 +25,6 @@
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/applySteps.h>
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
@@ -46,8 +45,7 @@ checkValidity(
STTx const& tx,
Rules const& rules,
Config const& config,
ApplyFlags applyFlags,
std::shared_ptr<ReadView const> const& pqKeyLedger)
ApplyFlags applyFlags)
{
auto const id = tx.getTransactionID();
auto const flags = router.getFlags(id);
@@ -113,35 +111,7 @@ checkValidity(
// Signature is known bad
return {Validity::SigBad, "Transaction has bad signature."};
// featurePQSignatures: a post-quantum key *reference* can only be
// verified against a ledger. With one, verify here (ingress); without
// one, leave it to preclaim (Transactor::checkSign), which verifies
// against the exact view the transaction is applied to. Either way the
// result is never cached: the referenced object can appear or vanish
// between ledgers.
bool const pqReference =
rules.enabled(featurePQSignatures) && tx.usesPQKeyReference();
if (pqReference)
{
if (pqKeyLedger)
{
auto const requireCanonicalSig =
rules.enabled(featureRequireFullyCanonicalSig)
? STTx::RequireFullyCanonicalSig::yes
: STTx::RequireFullyCanonicalSig::no;
auto const sigVerify = tx.checkSign(
requireCanonicalSig,
rules,
pqkey::makeLookup(*pqKeyLedger),
STTx::PQReferencePolicy::reject);
if (!sigVerify)
return {Validity::SigBad, sigVerify.error()};
}
// Deferred (or verified for this ledger only): fall through to the
// local checks without touching the signature flags.
}
else if (!(flags & SF_SIGGOOD))
if (!(flags & SF_SIGGOOD))
{
// Don't know signature state. Check it.
auto const requireCanonicalSig =
+1
View File
@@ -44,6 +44,7 @@
#include <xrpld/app/tx/detail/DepositPreauth.h>
#include <xrpld/app/tx/detail/Escrow.h>
#include <xrpld/app/tx/detail/GenesisMint.h>
#include <xrpld/app/tx/detail/HistoryProof.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/app/tx/detail/Invoke.h>
#include <xrpld/app/tx/detail/LedgerStateFix.h>
+3
View File
@@ -102,6 +102,9 @@ struct ConfigSection
#define SECTION_VALIDATORS "validators"
#define SECTION_VALIDATOR_TOKEN "validator_token"
#define SECTION_VETO_AMENDMENTS "veto_amendments"
// Testnet-only: amendments enabled on-ledger that this node should not treat
// as unsupported (i.e. should not become amendment blocked over).
#define SECTION_IGNORE_AMENDMENTS "ignore_amendments"
#define SECTION_WORKERS "workers"
} // namespace ripple
+6 -1
View File
@@ -46,7 +46,12 @@ enum ApplyFlags : std::uint32_t {
// Transaction shouldn't be applied
// Signatures shouldn't be checked
tapDRY_RUN = 0x1000
tapDRY_RUN = 0x1000,
// featureHistoryTree: a validated ledger is being replayed from its
// stored (clipped) transactions; trust that transient fields were present
// and verified when the ledger was first built.
tapHISTORY_REPLAY = 0x2000
};
constexpr ApplyFlags
+2 -1
View File
@@ -313,8 +313,9 @@ ApplyStateTable::apply(
beast::Journal j)
{
// Build metadata and insert
// featureHistoryTree: transient fields are clipped from the stored form.
auto const sTx = std::make_shared<Serializer>();
tx.add(*sTx);
tx.addStorable(*sTx);
std::shared_ptr<Serializer> sMeta;
std::optional<TxMeta> metadata;
if (!to.open() || isDryRun)
+1 -3
View File
@@ -1563,9 +1563,7 @@ checkXUSHTransaction(
app.getHashRouter(),
*stx,
app.getLedgerMaster().getValidatedRules(),
app.config(),
tapNONE,
app.openLedger().current());
app.config());
validity != Validity::Valid)
{
JLOG(j.trace())
+1 -4
View File
@@ -21,7 +21,6 @@
#include <xrpld/app/ledger/InboundLedgers.h>
#include <xrpld/app/ledger/InboundTransactions.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/ledger/TransactionMaster.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
@@ -2996,9 +2995,7 @@ PeerImp::checkTransaction(
app_.getHashRouter(),
*stx,
app_.getLedgerMaster().getValidatedRules(),
app_.config(),
tapNONE,
app_.openLedger().current());
app_.config());
valid != Validity::Valid)
{
if (!validReason.empty())
+1 -6
View File
@@ -632,12 +632,7 @@ transactionConstructImpl(
sttxNew->getTransactionID(),
Validity::SigGoodOnly);
if (checkValidity(
app.getHashRouter(),
*sttxNew,
rules,
app.config(),
tapNONE,
app.openLedger().current())
app.getHashRouter(), *sttxNew, rules, app.config())
.first != Validity::Valid)
{
ret.first = RPC::make_error(rpcINTERNAL, "Invalid signature.");
-15
View File
@@ -683,20 +683,6 @@ parseDID(Json::Value const& params, Json::Value& jvResult)
return keylet::did(*account).key;
}
// featurePQSignatures: "pq_public_key": "<r-address of the key>"
static std::optional<uint256>
parsePQPublicKey(Json::Value const& params, Json::Value& jvResult)
{
auto const account = parseBase58<AccountID>(params.asString());
if (!account || account->isZero())
{
jvResult[jss::error] = "malformedAddress";
return std::nullopt;
}
return keylet::pqPublicKey(*account).key;
}
static std::optional<uint256>
parseOracle(Json::Value const& params, Json::Value& jvResult)
{
@@ -1167,7 +1153,6 @@ doLedgerEntry(RPC::JsonContext& context)
{jss::permissioned_domain,
parsePermissionedDomains,
ltPERMISSIONED_DOMAIN},
{jss::pq_public_key, parsePQPublicKey, ltPUBLIC_KEY},
{jss::random, parseRandom, ltRANDOM},
{jss::ripple_state, parseRippleState, ltRIPPLE_STATE},
// This is an alias, since the `ledger_data` filter uses jss::state
+1 -3
View File
@@ -218,9 +218,7 @@ doSubmit(RPC::JsonContext& context)
context.app.getHashRouter(),
*stTx,
context.ledgerMaster.getCurrentLedger()->rules(),
context.app.config(),
tapNONE,
context.app.openLedger().current());
context.app.config());
if (validity != Validity::Valid)
{
jvResult[jss::error] = "invalidTransaction";