mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-27 14:17:40 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5933167771 | ||
|
|
47564db6a4 | ||
|
|
1d08aa2e99 |
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -345,14 +345,6 @@ 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
|
||||
|
||||
@@ -112,8 +112,6 @@
|
||||
#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)
|
||||
@@ -167,7 +165,6 @@
|
||||
#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)
|
||||
@@ -234,7 +231,6 @@
|
||||
#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)
|
||||
@@ -261,7 +257,6 @@
|
||||
#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)
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
#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
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#define featureHooksUpdate1 "1"
|
||||
#define featureHooksUpdate2 "1"
|
||||
#define featureRNG "1"
|
||||
#define featureHistoryTree "1"
|
||||
#define fix20250131 "1"
|
||||
#define fixGuardDepth32 "1"
|
||||
namespace hook_api {
|
||||
@@ -316,6 +315,7 @@ enum keylet_code : uint32_t {
|
||||
CREDENTIAL = 35,
|
||||
PERMISSIONED_DOMAIN = 36,
|
||||
MANIFEST = 37,
|
||||
PQ_PUBLIC_KEY = 38,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -306,8 +306,4 @@ 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
|
||||
@@ -96,12 +96,6 @@ 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>
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
@@ -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;
|
||||
|
||||
@@ -122,10 +127,6 @@ 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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -31,6 +31,9 @@
|
||||
#include <boost/container/flat_set.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -48,9 +51,6 @@ 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,32 +110,6 @@ 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;
|
||||
|
||||
@@ -149,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&
|
||||
@@ -170,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,7 +34,7 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(HistoryTree, Supported::yes, VoteBehavior::DefaultNo)
|
||||
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},
|
||||
@@ -216,20 +235,6 @@ 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
|
||||
|
||||
@@ -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)
|
||||
@@ -160,9 +161,6 @@ 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)
|
||||
@@ -227,8 +225,6 @@ 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)
|
||||
@@ -309,9 +305,7 @@ TYPED_SFIELD(sfAppLoader, VL, 96)
|
||||
TYPED_SFIELD(sfHookName, VL, 97)
|
||||
TYPED_SFIELD(sfRemarkValue, VL, 98)
|
||||
TYPED_SFIELD(sfRemarkName, VL, 99)
|
||||
// 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)
|
||||
TYPED_SFIELD(sfPublicKeyData, VL, 95)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
@@ -345,7 +339,6 @@ 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,22 +517,6 @@ 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, ({
|
||||
|
||||
@@ -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
|
||||
HISTORY_TREE = 0x4874, // Ht
|
||||
RANDOM = 0x526E, // Rn
|
||||
PUBLIC_KEY = LEDGER_NAMESPACE2(0x01, 'K'),
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -206,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
|
||||
{
|
||||
@@ -287,14 +293,6 @@ 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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
+151
-106
@@ -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>
|
||||
@@ -33,7 +34,6 @@
|
||||
#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,8 +63,7 @@ STTx::STTx(STObject&& object) : STObject(std::move(object))
|
||||
{
|
||||
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
|
||||
applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // may throw
|
||||
checkTransientFields();
|
||||
tid_ = computeTransactionID();
|
||||
tid_ = getHash(HashPrefix::transactionID);
|
||||
}
|
||||
|
||||
STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
|
||||
@@ -80,8 +79,7 @@ STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
|
||||
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
|
||||
|
||||
applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // May throw
|
||||
checkTransientFields();
|
||||
tid_ = computeTransactionID();
|
||||
tid_ = getHash(HashPrefix::transactionID);
|
||||
}
|
||||
|
||||
STTx::STTx(TxType type, std::function<void(STObject&)> assembler)
|
||||
@@ -99,70 +97,7 @@ STTx::STTx(TxType type, std::function<void(STObject&)> assembler)
|
||||
if (tx_type_ != type)
|
||||
LogicError("Transaction type was mutated during assembly");
|
||||
|
||||
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);
|
||||
tid_ = getHash(HashPrefix::transactionID);
|
||||
}
|
||||
|
||||
STBase*
|
||||
@@ -272,13 +207,15 @@ STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data));
|
||||
|
||||
setFieldVL(sfTxnSignature, sig);
|
||||
tid_ = computeTransactionID();
|
||||
tid_ = getHash(HashPrefix::transactionID);
|
||||
}
|
||||
|
||||
Expected<void, std::string>
|
||||
STTx::checkSign(
|
||||
RequireFullyCanonicalSig requireCanonicalSig,
|
||||
Rules const& rules) const
|
||||
Rules const& rules,
|
||||
PQKeyLookup const& lookup,
|
||||
PQReferencePolicy policy) const
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -287,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&)
|
||||
{
|
||||
@@ -296,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
|
||||
{
|
||||
@@ -376,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
|
||||
@@ -388,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 {};
|
||||
}
|
||||
@@ -422,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.
|
||||
@@ -479,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 {};
|
||||
|
||||
@@ -1271,79 +1271,6 @@ 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)
|
||||
{
|
||||
@@ -1367,7 +1294,6 @@ public:
|
||||
testBadConfig();
|
||||
testEnableVeto();
|
||||
testHasUnsupported();
|
||||
testIgnoredUnsupported();
|
||||
testFeature(all - fixMajorityCalc);
|
||||
testFeature(all);
|
||||
}
|
||||
|
||||
@@ -501,14 +501,6 @@ 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;
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
#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/AccountID.h>
|
||||
#include <xrpl/protocol/PQPublicKey.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
@@ -47,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);
|
||||
|
||||
@@ -1995,43 +2015,6 @@ 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
|
||||
{
|
||||
|
||||
@@ -340,8 +340,7 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
case ttCHECK_CREATE:
|
||||
case ttACCOUNT_DELETE:
|
||||
case ttPAYCHAN_CREATE:
|
||||
case ttINVOKE:
|
||||
case ttHISTORY_PROOF: {
|
||||
case ttINVOKE: {
|
||||
if (destAcc)
|
||||
ADD_TSH(*destAcc, tshSTRONG);
|
||||
break;
|
||||
@@ -2228,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;
|
||||
|
||||
@@ -2256,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);
|
||||
@@ -2927,39 +2933,6 @@ 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,
|
||||
@@ -3378,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,
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
#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>
|
||||
@@ -51,7 +50,6 @@
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/optional.hpp>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -994,71 +992,6 @@ 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
|
||||
{
|
||||
|
||||
@@ -352,13 +352,6 @@ 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;
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ buildLedgerImpl(
|
||||
}
|
||||
|
||||
built->updateSkipList();
|
||||
built->updateHistoryTree();
|
||||
{
|
||||
// Write the final version of all modified SHAMap
|
||||
// nodes to the node store to preserve the new LCL
|
||||
@@ -272,13 +271,7 @@ buildLedger(
|
||||
j,
|
||||
[&](OpenView& accum, std::shared_ptr<Ledger> const& built) {
|
||||
for (auto& tx : replayData.orderedTxns())
|
||||
applyTransaction(
|
||||
app,
|
||||
accum,
|
||||
*tx.second,
|
||||
false,
|
||||
applyFlags | tapHISTORY_REPLAY,
|
||||
j);
|
||||
applyTransaction(app, accum, *tx.second, false, applyFlags, j);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1260,29 +1260,13 @@ 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"),
|
||||
ignored);
|
||||
logs_->journal("Amendments"));
|
||||
}
|
||||
|
||||
Pathfinder::initPathTable();
|
||||
|
||||
@@ -211,8 +211,7 @@ make_AmendmentTable(
|
||||
std::vector<AmendmentTable::FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal,
|
||||
Section const& ignored = Section{});
|
||||
beast::Journal journal);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -302,15 +302,6 @@ 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;
|
||||
|
||||
@@ -485,8 +476,7 @@ public:
|
||||
std::vector<FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal,
|
||||
Section const& ignored);
|
||||
beast::Journal journal);
|
||||
|
||||
uint256
|
||||
find(std::string const& name) const override;
|
||||
@@ -553,8 +543,7 @@ AmendmentTableImpl::AmendmentTableImpl(
|
||||
std::vector<FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal,
|
||||
Section const& ignored)
|
||||
beast::Journal journal)
|
||||
: lastUpdateSeq_(0)
|
||||
, majorityTime_(majorityTime)
|
||||
, unsupportedEnabled_(false)
|
||||
@@ -597,59 +586,6 @@ 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))
|
||||
@@ -839,12 +775,7 @@ AmendmentTableImpl::enable(uint256 const& amendment)
|
||||
|
||||
s.enabled = true;
|
||||
|
||||
if (s.ignored)
|
||||
{
|
||||
JLOG(j_.warn()) << "Ignored unsupported amendment " << amendment << " ("
|
||||
<< s.name << ") is enabled on the ledger.";
|
||||
}
|
||||
else if (!s.supported)
|
||||
if (!s.supported)
|
||||
{
|
||||
JLOG(j_.error()) << "Unsupported amendment " << amendment
|
||||
<< " activated.";
|
||||
@@ -1113,9 +1044,7 @@ AmendmentTableImpl::injectJson(
|
||||
if (!fs.name.empty())
|
||||
v[jss::name] = fs.name;
|
||||
|
||||
v[jss::supported] = fs.supported && !fs.ignored;
|
||||
if (fs.ignored)
|
||||
v["ignored"] = true;
|
||||
v[jss::supported] = fs.supported;
|
||||
if (!fs.enabled && isAdmin)
|
||||
{
|
||||
if (fs.vote == AmendmentVote::obsolete)
|
||||
@@ -1184,11 +1113,10 @@ make_AmendmentTable(
|
||||
std::vector<AmendmentTable::FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal,
|
||||
Section const& ignored)
|
||||
beast::Journal journal)
|
||||
{
|
||||
return std::make_unique<AmendmentTableImpl>(
|
||||
app, majorityTime, supported, enabled, vetoed, journal, ignored);
|
||||
app, majorityTime, supported, enabled, vetoed, journal);
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
@@ -1,66 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
@@ -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>
|
||||
@@ -605,7 +606,6 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltIMPORT_VLSEQ:
|
||||
case ltUNL_REPORT:
|
||||
case ltRANDOM:
|
||||
case ltHISTORY_TREE:
|
||||
case ltAMM:
|
||||
case ltBRIDGE:
|
||||
case ltXCHAIN_OWNED_CLAIM_ID:
|
||||
@@ -618,6 +618,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltPERMISSIONED_DOMAIN:
|
||||
case ltAPP_LOADER:
|
||||
case ltMANIFEST:
|
||||
case ltPUBLIC_KEY:
|
||||
break;
|
||||
default:
|
||||
invalidTypeAdded_ = true;
|
||||
@@ -2081,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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
#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>
|
||||
|
||||
@@ -102,9 +102,6 @@ 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
|
||||
|
||||
@@ -46,12 +46,7 @@ enum ApplyFlags : std::uint32_t {
|
||||
|
||||
// Transaction shouldn't be applied
|
||||
// Signatures shouldn't be checked
|
||||
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
|
||||
tapDRY_RUN = 0x1000
|
||||
};
|
||||
|
||||
constexpr ApplyFlags
|
||||
|
||||
@@ -313,9 +313,8 @@ 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.addStorable(*sTx);
|
||||
tx.add(*sTx);
|
||||
std::shared_ptr<Serializer> sMeta;
|
||||
std::optional<TxMeta> metadata;
|
||||
if (!to.open() || isDryRun)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user