Compare commits

...
Author SHA1 Message Date
Richard Holland 5933167771 Merge branch 'dev' into pqdsa 2026-10-14 15:31:01 +10:00
Richard Holland 47564db6a4 missing header 2026-10-14 16:26:46 +11:00
Richard Holland 1d08aa2e99 first version of pq 2026-10-14 12:01:50 +11:00
34 changed files with 1475 additions and 68 deletions

No files matched your search

+1
View File
@@ -100,6 +100,7 @@ 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,3 +1,26 @@
# 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,6 +63,7 @@ target_link_libraries(xrpl.imports.main
absl::random_random
date::date
ed25519::ed25519
pqov::pqov
secp256k1::secp256k1
xrpl.libpb
xxHash::xxhash
+1
View File
@@ -315,6 +315,7 @@ enum keylet_code : uint32_t {
CREDENTIAL = 35,
PERMISSIONED_DOMAIN = 36,
MANIFEST = 37,
PQ_PUBLIC_KEY = 38,
};
}
+5
View File
@@ -71,6 +71,11 @@ 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;
+146
View File
@@ -0,0 +1,146 @@
//------------------------------------------------------------------------------
/*
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
+50 -4
View File
@@ -31,6 +31,9 @@
#include <boost/container/flat_set.hpp>
#include <functional>
#include <optional>
#include <utility>
#include <vector>
namespace ripple {
@@ -120,9 +123,46 @@ 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)
const;
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;
// SQL Functions with metadata.
static std::string const&
@@ -141,12 +181,18 @@ public:
private:
Expected<void, std::string>
checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const;
checkSingleSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules,
PQKeyLookup const& lookup,
PQReferencePolicy policy) const;
Expected<void, std::string>
checkMultiSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules) const;
Rules const& rules,
PQKeyLookup const& lookup,
PQReferencePolicy policy) const;
STBase*
copy(std::size_t n, void* buf) const override;
@@ -34,6 +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(RNG, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PWALoader, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
@@ -150,6 +150,25 @@ 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},
@@ -42,6 +42,7 @@ 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)
@@ -304,6 +305,7 @@ 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)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
+7
View File
@@ -95,6 +95,7 @@ enum class LedgerNameSpace : std::uint16_t {
CREDENTIAL = LEDGER_NAMESPACE2(0x01, 'D'),
PERMISSIONED_DOMAIN = 'm',
RANDOM = 0x526E, // Rn
PUBLIC_KEY = LEDGER_NAMESPACE2(0x01, 'K'),
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -205,6 +206,12 @@ 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
{
+399
View File
@@ -0,0 +1,399 @@
//------------------------------------------------------------------------------
/*
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
+147 -36
View File
@@ -25,6 +25,7 @@
#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>
@@ -212,7 +213,9 @@ STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
Expected<void, std::string>
STTx::checkSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules) const
Rules const& rules,
PQKeyLookup const& lookup,
PQReferencePolicy policy) const
{
try
{
@@ -221,8 +224,8 @@ STTx::checkSign(
// multi-signing. Otherwise we're single-signing.
Blob const& signingPubKey = getFieldVL(sfSigningPubKey);
return signingPubKey.empty()
? checkMultiSign(requireCanonicalSig, rules)
: checkSingleSign(requireCanonicalSig);
? checkMultiSign(requireCanonicalSig, rules, lookup, policy)
: checkSingleSign(requireCanonicalSig, rules, lookup, policy);
}
catch (std::exception const&)
{
@@ -230,6 +233,103 @@ 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
{
@@ -310,7 +410,11 @@ STTx::getMetaSQL(
}
Expected<void, std::string>
STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
STTx::checkSingleSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules,
PQKeyLookup const& lookup,
PQReferencePolicy policy) 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
@@ -322,33 +426,33 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
bool const isWildcardNetwork =
isFieldPresent(sfNetworkID) && getFieldU32(sfNetworkID) == 65535;
bool validSig = false;
std::optional<std::string> failure = "Invalid signature.";
try
{
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
(requireCanonicalSig == RequireFullyCanonicalSig::yes);
auto const spk = getFieldVL(sfSigningPubKey);
Blob const signature = getFieldVL(sfTxnSignature);
Blob const data = getSigningData(*this);
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);
}
failure = verifyAnyKey(
spk,
makeSlice(data),
signature,
fullyCanonical,
isWildcardNetwork,
rules,
lookup,
policy);
}
catch (std::exception const&)
{
// Assume it was a signature failure.
validSig = false;
failure = "Invalid signature.";
}
if (validSig == false)
return Unexpected("Invalid signature.");
if (failure)
return Unexpected(*failure);
// Signature was verified.
return {};
}
@@ -356,7 +460,9 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
Expected<void, std::string>
STTx::checkMultiSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules) const
Rules const& rules,
PQKeyLookup const& lookup,
PQReferencePolicy policy) const
{
// Make sure the MultiSigners are present. Otherwise they are not
// attempting multi-signing and we just have a bad SigningPubKey.
@@ -413,35 +519,40 @@ STTx::checkMultiSign(
lastAccountID = accountID;
// Verify the signature.
bool validSig = false;
std::optional<std::string> failure = "Invalid signature.";
try
{
Serializer s = dataStart;
finishMultiSigningData(accountID, s);
auto spk = signer.getFieldVL(sfSigningPubKey);
Blob const signature = signer.getFieldVL(sfTxnSignature);
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);
}
failure = verifyAnyKey(
spk,
s.slice(),
signature,
fullyCanonical,
isWildcardNetwork,
rules,
lookup,
policy);
}
catch (std::exception const&)
{
// We assume any problem lies with the signature.
validSig = false;
failure = "Invalid signature.";
}
if (!validSig)
if (failure)
{
if (*failure == "Invalid signature.")
return Unexpected(
std::string("Invalid signature on account ") +
toBase58(accountID) + ".");
return Unexpected(
std::string("Invalid signature on account ") +
toBase58(accountID) + ".");
*failure + std::string(" Account ") + toBase58(accountID) +
".");
}
}
// All signatures verified.
return {};
+27 -3
View File
@@ -4,7 +4,9 @@
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/ledger/TransactionMaster.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/STParsedJSON.h>
#include <algorithm>
#include <limits>
@@ -43,15 +45,37 @@ 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);
+13 -4
View File
@@ -2227,12 +2227,18 @@ 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;
@@ -2255,8 +2261,9 @@ 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::DID ? ripple::keylet::did(id)
: keylet_type == keylet_code::PQ_PUBLIC_KEY
? ripple::keylet::pqPublicKey(id)
: ripple::keylet::account(id);
return serialize_keylet(kl, memory, write_ptr, write_len);
@@ -3344,8 +3351,10 @@ DEFINE_HOOK_FUNCTION(
}
// Validate either an secp256k1 signature or an ed25519 signature, using the
// XRPLD convention for identifying the key type. Pointer prefixes: d = data, s
// = signature, k = public key.
// 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.
DEFINE_HOOK_FUNCTION(
int64_t,
util_verify,
+6 -2
View File
@@ -1169,7 +1169,9 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
app_.getHashRouter(),
*trans,
m_ledgerMaster.getValidatedRules(),
app_.config());
app_.config(),
tapNONE,
app_.openLedger().current());
if (validity != Validity::Valid)
{
@@ -1418,7 +1420,9 @@ NetworkOPsImp::processTransaction(
app_.getHashRouter(),
*transaction->getSTransaction(),
view->rules(),
app_.config());
app_.config(),
tapNONE,
view);
XRPL_ASSERT(
validity == Validity::Valid,
"ripple::NetworkOPsImp::processTransaction : valid validity");
+14 -1
View File
@@ -58,6 +58,18 @@ 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>
@@ -66,7 +78,8 @@ checkValidity(
STTx const& tx,
Rules const& rules,
Config const& config,
ApplyFlags const flags = tapNONE);
ApplyFlags const flags = tapNONE,
std::shared_ptr<ReadView const> const& pqKeyLedger = nullptr);
/** Ingress fee outcome for a manifest-authorized SetManifest.
@@ -23,6 +23,7 @@
#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>
@@ -493,6 +494,14 @@ 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);
+10
View File
@@ -19,6 +19,7 @@
#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>
@@ -1193,13 +1194,22 @@ 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
@@ -32,6 +32,7 @@
#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>
@@ -617,6 +618,7 @@ LedgerEntryTypesMatch::visitEntry(
case ltPERMISSIONED_DOMAIN:
case ltAPP_LOADER:
case ltMANIFEST:
case ltPUBLIC_KEY:
break;
default:
invalidTypeAdded_ = true;
@@ -2080,4 +2082,57 @@ 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
+31 -1
View File
@@ -739,6 +739,35 @@ 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,
@@ -759,7 +788,8 @@ using InvariantChecks = std::tuple<
ValidPermissionedDomain,
ValidAMM,
ValidLockedBalance,
ValidAppLoader>;
ValidAppLoader,
ValidPQPublicKey>;
/**
* @brief get a tuple of all invariant checks
+170
View File
@@ -0,0 +1,170 @@
//------------------------------------------------------------------------------
/*
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
@@ -0,0 +1,101 @@
//------------------------------------------------------------------------------
/*
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,6 +17,7 @@
*/
//==============================================================================
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpld/app/tx/detail/SetAccount.h>
#include <xrpld/core/Config.h>
#include <xrpld/ledger/View.h>
@@ -428,12 +429,16 @@ 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,6 +17,7 @@
*/
//==============================================================================
#include <xrpld/app/tx/detail/PQKeyLedger.h>
#include <xrpld/app/tx/detail/SetRegularKey.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
@@ -82,9 +83,17 @@ 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
{
@@ -97,6 +106,7 @@ SetRegularKey::doApply()
}
ctx_.view().update(sle);
pqkey::adjustReferences(view(), oldKey, newKey, j_);
return tesSUCCESS;
}
+7 -1
View File
@@ -201,7 +201,8 @@ SetSignerList::removeSignersFromLedger(
Keylet const& accountKeylet,
Keylet const& ownerDirKeylet,
Keylet const& signerListKeylet,
beast::Journal j)
beast::Journal j,
bool releasePQReferences)
{
// We have to examine the current SignerList so we know how much to
// reduce the OwnerCount.
@@ -211,6 +212,11 @@ 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.
+25 -2
View File
@@ -21,6 +21,7 @@
#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>
@@ -78,7 +79,8 @@ public:
Keylet const& accountKeylet,
Keylet const& ownerDirKeylet,
Keylet const& signerListKeylet,
beast::Journal j);
beast::Journal j,
bool releasePQReferences = true);
static int
signerCountBasedOwnerCountDelta(std::size_t entryCount, Rules const& rules);
@@ -132,11 +134,26 @@ 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))
app,
view,
accountKeylet,
ownerDirKeylet,
signerListKeylet,
j,
/*releasePQReferences=*/false))
return ter;
auto const sle = view.peek(accountKeylet);
@@ -186,6 +203,12 @@ 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;
}
};
+127 -8
View File
@@ -24,6 +24,7 @@
#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>
@@ -37,6 +38,7 @@
#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>
@@ -441,6 +443,19 @@ 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) +
@@ -877,6 +892,13 @@ 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();
@@ -931,12 +953,62 @@ 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();
if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(pkSigner)))
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)))))
{
JLOG(ctx.j.trace())
<< "checkSingleSign: signing public key type is unknown";
@@ -954,9 +1026,25 @@ Transactor::checkSingleSign(PreclaimContext const& ctx)
XRPL_ASSERT(
(ctx.flags & tapDRY_RUN) || !pkSigner.empty(),
"ripple::Transactor::checkSingleSign : non-empty signer or simulation");
auto const idSigner = pkSigner.empty()
? idAccount
: calcAccountID(PublicKey(makeSlice(pkSigner)));
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;
bool const isMasterDisabled = sleAccount->isFlag(lsfDisableMaster);
if (ctx.view.rules().enabled(fixMasterKeyAsRegularKey))
@@ -1078,8 +1166,12 @@ 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)))
if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk)) &&
!(pqEnabled &&
(pq::isInlineKey(makeSlice(spk)) ||
pq::isKeyReference(makeSlice(spk)))))
{
JLOG(ctx.j.trace())
<< "checkMultiSign: signing public key type is unknown";
@@ -1091,9 +1183,29 @@ Transactor::checkMultiSign(PreclaimContext const& ctx)
(ctx.flags & tapDRY_RUN) || !spk.empty(),
"ripple::Transactor::checkMultiSign : non-empty signer or "
"simulation");
AccountID const signingAcctIDFromPubKey = spk.empty()
? txSignerAcctID
: calcAccountID(PublicKey(makeSlice(spk)));
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;
// Verify that the signingAcctID and the signingAcctIDFromPubKey
// belong together. Here are the rules:
@@ -1361,7 +1473,14 @@ 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};
}
+32 -2
View File
@@ -25,6 +25,7 @@
#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>
@@ -45,7 +46,8 @@ checkValidity(
STTx const& tx,
Rules const& rules,
Config const& config,
ApplyFlags applyFlags)
ApplyFlags applyFlags,
std::shared_ptr<ReadView const> const& pqKeyLedger)
{
auto const id = tx.getTransactionID();
auto const flags = router.getFlags(id);
@@ -111,7 +113,35 @@ checkValidity(
// Signature is known bad
return {Validity::SigBad, "Transaction has bad signature."};
if (!(flags & SF_SIGGOOD))
// 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))
{
// Don't know signature state. Check it.
auto const requireCanonicalSig =
+3 -1
View File
@@ -1563,7 +1563,9 @@ checkXUSHTransaction(
app.getHashRouter(),
*stx,
app.getLedgerMaster().getValidatedRules(),
app.config());
app.config(),
tapNONE,
app.openLedger().current());
validity != Validity::Valid)
{
JLOG(j.trace())
+4 -1
View File
@@ -21,6 +21,7 @@
#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>
@@ -2995,7 +2996,9 @@ PeerImp::checkTransaction(
app_.getHashRouter(),
*stx,
app_.getLedgerMaster().getValidatedRules(),
app_.config());
app_.config(),
tapNONE,
app_.openLedger().current());
valid != Validity::Valid)
{
if (!validReason.empty())
+6 -1
View File
@@ -632,7 +632,12 @@ transactionConstructImpl(
sttxNew->getTransactionID(),
Validity::SigGoodOnly);
if (checkValidity(
app.getHashRouter(), *sttxNew, rules, app.config())
app.getHashRouter(),
*sttxNew,
rules,
app.config(),
tapNONE,
app.openLedger().current())
.first != Validity::Valid)
{
ret.first = RPC::make_error(rpcINTERNAL, "Invalid signature.");
+15
View File
@@ -683,6 +683,20 @@ 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)
{
@@ -1153,6 +1167,7 @@ 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
+3 -1
View File
@@ -218,7 +218,9 @@ doSubmit(RPC::JsonContext& context)
context.app.getHashRouter(),
*stTx,
context.ledgerMaster.getCurrentLedger()->rules(),
context.app.config());
context.app.config(),
tapNONE,
context.app.openLedger().current());
if (validity != Validity::Valid)
{
jvResult[jss::error] = "invalidTransaction";