Merge remote-tracking branch 'Transia-RnD-rippled/feature-p256' into develop

# Conflicts:
#	include/xrpl/protocol/Indexes.h
#	include/xrpl/protocol/KeyType.h
#	include/xrpl/protocol/PublicKey.h
#	src/libxrpl/protocol/Indexes.cpp
#	src/libxrpl/protocol/PublicKey.cpp
#	src/libxrpl/protocol/SecretKey.cpp
This commit is contained in:
Denis Angell
2026-09-13 19:25:42 -04:00
26 changed files with 1943 additions and 29 deletions

View File

@@ -205,4 +205,20 @@ base64Decode(std::string_view data)
return dest;
}
std::string
base64urlDecode(std::string_view data)
{
std::string b64(data);
for (auto& c : b64)
{
if (c == '-')
c = '+';
else if (c == '_')
c = '/';
}
while (b64.size() % 4 != 0)
b64 += '=';
return base64Decode(b64);
}
} // namespace xrpl

View File

@@ -103,6 +103,7 @@ enum class LedgerNameSpace : std::uint16_t {
Vault = 'V',
LoanBroker = 'l', // lower-case L
Loan = 'L',
PasskeyList = 'k',
Sponsorship = '>',
ContractSource = 'Z',
@@ -631,6 +632,18 @@ contractData(AccountID const& owner, AccountID const& contractAccount) noexcept
return {ltCONTRACT_DATA, indexHash(LedgerNameSpace::ContractData, owner, contractAccount)};
}
static Keylet
passkeyList(AccountID const& account, std::uint32_t page) noexcept
{
return {ltPASSKEY_LIST, indexHash(LedgerNameSpace::PasskeyList, account, page)};
}
Keylet
passkeyList(AccountID const& account) noexcept
{
return passkeyList(account, 0);
}
} // namespace keylet
} // namespace xrpl

View File

@@ -153,6 +153,22 @@ InnerObjectFormats::InnerObjectFormats()
{sfBookNode, SoeRequired},
});
add(sfPasskey.jsonName,
sfPasskey.getCode(),
{
{sfPasskeyID, SoeRequired},
{sfPublicKey, SoeRequired},
});
add(sfPasskeySignature.jsonName,
sfPasskeySignature.getCode(),
{
{sfPasskeyID, SoeRequired},
{sfAuthenticatorData, SoeRequired},
{sfClientDataJSON, SoeRequired},
{sfSignature, SoeRequired},
});
add(sfCounterpartySignature.jsonName,
sfCounterpartySignature.getCode(),
{

View File

@@ -13,10 +13,16 @@
#include <boost/multiprecision/number.hpp>
#include <openssl/bn.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/obj_mac.h>
#include <ed25519.h>
#include <secp256k1.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <optional>
@@ -187,10 +193,10 @@ ed25519Canonical(Slice const& sig)
PublicKey::PublicKey(Slice const& slice)
{
if (slice.size() < kSize)
if (slice.size() > kMaxSize)
{
logicError(
"PublicKey::PublicKey - Input slice cannot be an undersized "
"PublicKey::PublicKey - Input slice cannot be an oversized "
"buffer");
}
@@ -237,6 +243,9 @@ publicKeyType(Slice const& slice)
return KeyType::Dilithium;
}
if (slice.size() == 65 && slice[0] == 0xF6)
return KeyType::P256;
return std::nullopt;
}
@@ -312,6 +321,128 @@ verifyDigest(
}
}
struct ECDSASignature
{
std::array<uint8_t, 32> r;
std::array<uint8_t, 32> s;
};
static std::optional<ECDSASignature>
parseDERSignature(Slice const& derSig) noexcept
{
if (derSig.size() < 8)
return std::nullopt;
uint8_t const* data = derSig.data();
size_t offset = 0;
// Check sequence tag
if (data[offset++] != 0x30)
return std::nullopt;
// Skip total length
offset++;
// Parse R
if (data[offset++] != 0x02)
return std::nullopt;
uint8_t rLen = data[offset++];
if (offset + rLen >= derSig.size())
return std::nullopt;
ECDSASignature result{};
// Copy R, handling leading zeros
int rStart = (rLen > 32 && data[offset] == 0x00) ? 1 : 0;
int rCopyLen = std::min(32, static_cast<int>(rLen - rStart));
std::memcpy(result.r.data() + (32 - rCopyLen), data + offset + rStart, rCopyLen);
offset += rLen;
// Parse S
if (data[offset++] != 0x02)
return std::nullopt;
uint8_t sLen = data[offset++];
if (offset + sLen > derSig.size())
return std::nullopt;
// Copy S, handling leading zeros
int sStart = (sLen > 32 && data[offset] == 0x00) ? 1 : 0;
int sCopyLen = std::min(32, static_cast<int>(sLen - sStart));
std::memcpy(result.s.data() + (32 - sCopyLen), data + offset + sStart, sCopyLen);
return result;
}
static bool
verifyP256ECDSA(
uint8_t const* hash,
size_t hashLen,
uint8_t const* r,
size_t rLen,
uint8_t const* s,
size_t sLen,
uint8_t const* x,
size_t xLen,
uint8_t const* y,
size_t yLen) noexcept
{
if (hashLen != 32 || rLen > 32 || sLen > 32 || xLen > 32 || yLen > 32)
return false;
// Create curve object
EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1);
if (!group)
return false;
// Set group to EC_KEY
EC_KEY* key = EC_KEY_new();
if (!key)
{
EC_GROUP_free(group);
return false;
}
EC_KEY_set_group(key, group);
// Restore public key point from coordinates
EC_POINT* point = EC_POINT_new(group);
BIGNUM* bnX = BN_bin2bn(x, xLen, nullptr);
BIGNUM* bnY = BN_bin2bn(y, yLen, nullptr);
bool success = false;
if (point && bnX && bnY &&
EC_POINT_set_affine_coordinates_GFp(group, point, bnX, bnY, nullptr) == 1 &&
EC_KEY_set_public_key(key, point) == 1)
{
// Pack r/s into ECDSA_SIG structure
ECDSA_SIG* sig = ECDSA_SIG_new();
BIGNUM* bnR = BN_bin2bn(r, rLen, nullptr);
BIGNUM* bnS = BN_bin2bn(s, sLen, nullptr);
if (sig && bnR && bnS && ECDSA_SIG_set0(sig, bnR, bnS) == 1)
{
// Verify (ECDSA_SIG_set0 takes ownership of bnR, bnS)
int verified = ECDSA_do_verify(hash, hashLen, sig, key);
success = (verified == 1);
bnR = nullptr; // ownership transferred
bnS = nullptr; // ownership transferred
}
ECDSA_SIG_free(sig);
if (bnR)
BN_free(bnR);
if (bnS)
BN_free(bnS);
}
EC_POINT_free(point);
BN_free(bnX);
BN_free(bnY);
EC_KEY_free(key);
EC_GROUP_free(group);
return success;
}
bool
verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept
{
@@ -338,6 +469,37 @@ verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept
size_t ctxlen = 0;
return crypto_sign_verify(sig.data(), sig.size(), m.data(), m.size(), ctx, ctxlen, publicKey.data()) == 0;
}
if (*type == KeyType::P256)
{
// Parse DER signature to extract r and s values
auto parsedSig = parseDERSignature(sig);
if (!parsedSig)
return false;
// Hash the message with SHA-256 (P-256 uses ECDSA-SHA256)
auto hash = sha256(m);
// We internally prefix P-256 keys with a prefix byte
// so strip it to get the raw public key coordinates
if (publicKey.size() != 65) // 1 prefix + 32-byte x + 32-byte y
return false;
// Extract x and y coordinates (skip prefix byte)
uint8_t const* xCoord = publicKey.data() + 1;
uint8_t const* yCoord = publicKey.data() + 33;
return verifyP256ECDSA(
hash.data(),
hash.size(),
parsedSig->r.data(),
32, // r component
parsedSig->s.data(),
32, // s component
xCoord,
32, // x coordinate
yCoord,
32); // y coordinate
}
}
return false;
}

View File

@@ -3,12 +3,14 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Batch.h>
@@ -30,6 +32,7 @@
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <boost/container/flat_set.hpp>
@@ -187,6 +190,14 @@ STTx::getSignature(STObject const& sigObject)
{
try
{
auto const spk = sigObject.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)) == KeyType::P256 &&
sigObject.isFieldPresent(sfPasskeySignature))
{
auto const& passkeySig =
static_cast<STObject const&>(sigObject.peekAtField(sfPasskeySignature));
return passkeySig.getFieldVL(sfSignature);
}
return sigObject.getFieldVL(sfTxnSignature);
}
catch (std::exception const&)
@@ -430,6 +441,93 @@ STTx::getMetaSQL(
escapedMetaData);
}
/** Verify a P-256 passkey signature with WebAuthn challenge validation.
Validates that the clientDataJSON challenge matches the expected signing
data, then verifies the ECDSA signature against the WebAuthn authenticator
data. Returns true if the signature is valid, false otherwise.
*/
static bool
verifyPasskeySignature(
Slice const& publicKey,
STObject const& passkeySig,
Slice const& expectedSigningData) noexcept
{
try
{
auto const authenticatorData = passkeySig.getFieldVL(sfAuthenticatorData);
auto const clientDataJSON = passkeySig.getFieldVL(sfClientDataJSON);
// Validate that the WebAuthn challenge in clientDataJSON matches
// the transaction signing data. Without this, a passkey signature
// from any website could be replayed to authorize transactions.
std::string const cdj(clientDataJSON.begin(), clientDataJSON.end());
json::Value parsed;
json::Reader reader;
if (!reader.parse(cdj, parsed) || !parsed.isObject())
return false;
// Verify type is "webauthn.get"
if (!parsed.isMember("type") || parsed["type"].asString() != "webauthn.get")
return false;
// Verify challenge field exists
if (!parsed.isMember("challenge") || !parsed["challenge"].isString())
return false;
// Decode the base64url-encoded challenge and compare
auto const challengeBytes = base64urlDecode(parsed["challenge"].asString());
if (challengeBytes.size() != expectedSigningData.size() ||
!std::equal(challengeBytes.begin(), challengeBytes.end(), expectedSigningData.data()))
return false;
// Build WebAuthn signing data: authenticatorData || SHA-256(clientDataJSON)
auto const clientDataHash = sha256(makeSlice(clientDataJSON));
Blob signingData(authenticatorData.begin(), authenticatorData.end());
signingData.insert(
signingData.end(),
clientDataHash.data(),
clientDataHash.data() + clientDataHash.size());
Blob const signature = passkeySig.getFieldVL(sfSignature);
return verify(PublicKey(publicKey), makeSlice(signingData), makeSlice(signature));
}
catch (std::exception const&)
{
return false;
}
}
/** Verify a signature on a signing object.
Handles both standard signatures (sfTxnSignature) and P-256 passkey
signatures (sfPasskeySignature with WebAuthn challenge validation).
*/
static bool
verifySigObject(STObject const& sigObject, Slice const& data) noexcept
{
try
{
auto const spk = sigObject.getFieldVL(sfSigningPubKey);
auto const keyType = publicKeyType(makeSlice(spk));
if (!keyType)
return false;
if (*keyType == KeyType::P256 && sigObject.isFieldPresent(sfPasskeySignature))
{
auto const& passkeySig =
static_cast<STObject const&>(sigObject.peekAtField(sfPasskeySignature));
return verifyPasskeySignature(makeSlice(spk), passkeySig, data);
}
Blob const signature = sigObject.getFieldVL(sfTxnSignature);
return verify(PublicKey(makeSlice(spk)), data, makeSlice(signature));
}
catch (std::exception const&)
{
return false;
}
}
static std::expected<void, std::string>
singleSignHelper(STObject const& sigObject, Slice const& data)
{
@@ -439,22 +537,7 @@ singleSignHelper(STObject const& sigObject, Slice const& data)
if (sigObject.isFieldPresent(sfSigners))
return std::unexpected("Cannot both single- and multi-sign.");
bool validSig = false;
try
{
auto const spk = sigObject.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)))
{
Blob const signature = sigObject.getFieldVL(sfTxnSignature);
validSig = verify(PublicKey(makeSlice(spk)), data, makeSlice(signature));
}
}
catch (std::exception const&)
{
validSig = false;
}
if (!validSig)
if (!verifySigObject(sigObject, data))
return std::unexpected("Invalid signature.");
return {};
@@ -528,13 +611,8 @@ multiSignHelper(
std::optional<std::string> errorWhat;
try
{
auto spk = signer.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)))
{
Blob const signature = signer.getFieldVL(sfTxnSignature);
validSig = verify(
PublicKey(makeSlice(spk)), makeMsg(accountID).slice(), makeSlice(signature));
}
auto const msgSerializer = makeMsg(accountID);
validSig = verifySigObject(signer, msgSerializer.slice());
}
catch (std::exception const& e)
{

View File

@@ -17,6 +17,11 @@
#include <boost/utility/string_view.hpp>
#include <openssl/bn.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/obj_mac.h>
#include <ed25519.h>
#include <secp256k1.h>
@@ -376,6 +381,84 @@ sign(PublicKey const& pk, SecretKey const& sk, Slice const& m)
crypto_sign_signature(sig, &len, m.data(), m.size(), ctx, ctxlen, sk.data());
return Buffer{sig, len};
}
case KeyType::P256: {
// Hash the message with SHA-256 (P-256 uses ECDSA-SHA256)
auto digest = sha256(m);
// Create curve object
EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1);
if (!group)
logicError("sign: EC_GROUP_new_by_curve_name failed");
// Create EC_KEY and set the group
EC_KEY* key = EC_KEY_new();
if (!key)
{
EC_GROUP_free(group);
logicError("sign: EC_KEY_new failed");
}
if (EC_KEY_set_group(key, group) != 1)
{
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("sign: EC_KEY_set_group failed");
}
// Convert secret key to BIGNUM and set as private key
BIGNUM* privKey =
BN_bin2bn(reinterpret_cast<unsigned char const*>(sk.data()), sk.size(), nullptr);
if (!privKey || EC_KEY_set_private_key(key, privKey) != 1)
{
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("sign: failed to set private key");
}
// Sign the digest
ECDSA_SIG* sigObj = ECDSA_do_sign(
reinterpret_cast<unsigned char const*>(digest.data()), digest.size(), key);
if (!sigObj)
{
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("sign: ECDSA_do_sign failed");
}
// Convert signature to DER format
unsigned char sig[72];
int len = i2d_ECDSA_SIG(sigObj, nullptr);
if (len <= 0 || len > 72)
{
ECDSA_SIG_free(sigObj);
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("sign: i2d_ECDSA_SIG length check failed");
}
unsigned char* sigPtr = sig;
if (i2d_ECDSA_SIG(sigObj, &sigPtr) != len)
{
ECDSA_SIG_free(sigObj);
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("sign: i2d_ECDSA_SIG serialization failed");
}
// Cleanup
ECDSA_SIG_free(sigObj);
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
return Buffer{sig, static_cast<size_t>(len)};
}
default:
logicError("sign: invalid type");
}
@@ -555,6 +638,14 @@ generateSecretKey(KeyType type, Seed const& seed)
return sk;
}
if (type == KeyType::P256)
{
auto key = detail::deriveDeterministicRootKey(seed);
SecretKey const sk{Slice{key.data(), key.size()}};
secureErase(key.data(), key.size());
return sk;
}
logicError("generateSecretKey: unknown key type");
}
@@ -593,6 +684,119 @@ derivePublicKey(KeyType type, SecretKey const& sk)
return PublicKey{Slice{pk_data, CRYPTO_PUBLICKEYBYTES}};
}
case KeyType::P256: {
// Create curve object
EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1);
if (!group)
logicError("derivePublicKey: EC_GROUP_new_by_curve_name failed");
// Create EC_KEY and set the group
EC_KEY* key = EC_KEY_new();
if (!key)
{
EC_GROUP_free(group);
logicError("derivePublicKey: EC_KEY_new failed");
}
if (EC_KEY_set_group(key, group) != 1)
{
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("derivePublicKey: EC_KEY_set_group failed");
}
// Convert secret key to BIGNUM
BIGNUM* privKey =
BN_bin2bn(reinterpret_cast<unsigned char const*>(sk.data()), sk.size(), nullptr);
if (!privKey)
{
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("derivePublicKey: BN_bin2bn failed");
}
// Set the private key
if (EC_KEY_set_private_key(key, privKey) != 1)
{
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("derivePublicKey: EC_KEY_set_private_key failed");
}
// Generate the public key from the private key
EC_POINT* pubKeyPoint = EC_POINT_new(group);
if (!pubKeyPoint)
{
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("derivePublicKey: EC_POINT_new failed");
}
if (EC_POINT_mul(group, pubKeyPoint, privKey, nullptr, nullptr, nullptr) != 1)
{
EC_POINT_free(pubKeyPoint);
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("derivePublicKey: EC_POINT_mul failed");
}
// Extract x and y coordinates
BIGNUM* x = BN_new();
BIGNUM* y = BN_new();
if (!x || !y ||
EC_POINT_get_affine_coordinates_GFp(group, pubKeyPoint, x, y, nullptr) != 1)
{
BN_free(x);
BN_free(y);
EC_POINT_free(pubKeyPoint);
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("derivePublicKey: EC_POINT_get_affine_coordinates_GFp failed");
}
// Convert coordinates to bytes
unsigned char buf[65]; // 1 prefix + 32-byte x + 32-byte y
buf[0] = 0xF6; // P-256 prefix byte
// Convert x coordinate to 32 bytes
if (BN_bn2binpad(x, &buf[1], 32) != 32)
{
BN_free(x);
BN_free(y);
EC_POINT_free(pubKeyPoint);
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("derivePublicKey: BN_bn2binpad failed for x coordinate");
}
// Convert y coordinate to 32 bytes
if (BN_bn2binpad(y, &buf[33], 32) != 32)
{
BN_free(x);
BN_free(y);
EC_POINT_free(pubKeyPoint);
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
logicError("derivePublicKey: BN_bn2binpad failed for y coordinate");
}
// Cleanup
BN_free(x);
BN_free(y);
EC_POINT_free(pubKeyPoint);
BN_free(privKey);
EC_KEY_free(key);
EC_GROUP_free(group);
return PublicKey{Slice{buf, sizeof(buf)}};
}
default:
logicError("derivePublicKey: bad key type");
};
@@ -607,6 +811,10 @@ generateKeyPair(KeyType type, Seed const& seed)
detail::Generator const g(seed);
return g(0);
}
case KeyType::P256: {
auto const sk = generateSecretKey(type, seed);
return {derivePublicKey(type, sk), sk};
}
case KeyType::Ed25519: {
auto const sk = generateSecretKey(type, seed);
return {derivePublicKey(type, sk), sk};

View File

@@ -30,6 +30,7 @@ TxFormats::getCommonFields()
{sfSigners, SoeOptional}, // submit_multisigned
{sfNetworkID, SoeOptional},
{sfDelegate, SoeOptional},
{sfPasskeySignature, SoeOptional},
{sfSponsor, SoeOptional},
{sfSponsorFlags, SoeOptional},
{sfSponsorSignature, SoeOptional},

View File

@@ -1051,6 +1051,28 @@ Transactor::checkSingleSign(
return tefMASTER_DISABLED;
}
// Check passkey list.
{
std::shared_ptr<STLedgerEntry const> slePasskeyList =
view.read(keylet::passkeyList(idAccount));
if (slePasskeyList)
{
auto const passkeys =
slePasskeyList->getFieldArray(sfPasskeys);
auto hasMatchingPasskey = std::any_of(
passkeys.begin(),
passkeys.end(),
[&idSigner](STObject const& passkey) {
return passkey.isFieldPresent(sfPublicKey) &&
calcAccountID(PublicKey(makeSlice(
passkey.getFieldVL(sfPublicKey)))) ==
idSigner;
});
if (hasMatchingPasskey)
return tesSUCCESS;
}
}
// Signed with any other key.
return tefBAD_AUTH;
}

View File

@@ -0,0 +1,118 @@
#include <xrpl/tx/transactors/account/SetPasskeyList.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <memory>
#include <set>
namespace xrpl {
NotTEC
SetPasskeyList::preflight(PreflightContext const& ctx)
{
auto const& passkeys = ctx.tx.getFieldArray(sfPasskeys);
if (passkeys.empty())
{
JLOG(ctx.j.debug()) << "SetPasskeyList: empty passkeys array.";
return temMALFORMED;
}
// Validate each passkey entry and check for duplicates
std::set<Blob> seenPasskeyIDs;
std::set<Blob> seenPublicKeys;
for (auto const& passkey : passkeys)
{
if (!passkey.isFieldPresent(sfPasskeyID) || !passkey.isFieldPresent(sfPublicKey))
{
JLOG(ctx.j.debug()) << "SetPasskeyList: missing required fields.";
return temMALFORMED;
}
// Check for duplicate PasskeyIDs
auto const passkeyID = passkey.getFieldVL(sfPasskeyID);
if (!seenPasskeyIDs.insert(passkeyID).second)
{
JLOG(ctx.j.debug()) << "SetPasskeyList: duplicate PasskeyID.";
return temMALFORMED;
}
// Check for duplicate PublicKeys
auto const pk = passkey.getFieldVL(sfPublicKey);
if (!seenPublicKeys.insert(pk).second)
{
JLOG(ctx.j.debug()) << "SetPasskeyList: duplicate PublicKey.";
return temMALFORMED;
}
// Validate public key is a valid P256 key
auto const keyType = publicKeyType(makeSlice(pk));
if (!keyType || *keyType != KeyType::P256)
{
JLOG(ctx.j.debug()) << "SetPasskeyList: invalid P256 public key.";
return temMALFORMED;
}
}
return tesSUCCESS;
}
TER
SetPasskeyList::doApply()
{
auto viewJ = ctx_.registry.get().getJournal("View");
auto const sleAccount = ctx_.view().peek(keylet::account(accountID_));
if (!sleAccount)
return tecINTERNAL;
auto const passkeyKeylet = keylet::passkeyList(accountID_);
auto sle = std::make_shared<SLE>(passkeyKeylet);
sle->setAccountID(sfOwner, ctx_.tx.getAccountID(sfAccount));
auto const& passkeys = ctx_.tx.getFieldArray(sfPasskeys);
sle->setFieldArray(sfPasskeys, passkeys);
auto page = ctx_.view().dirInsert(
keylet::ownerDir(accountID_), sle->key(), describeOwnerDir(accountID_));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*sle)[sfOwnerNode] = *page;
increaseOwnerCount(ctx_.view(), sleAccount, {}, 1, viewJ);
ctx_.view().insert(sle);
return tesSUCCESS;
}
void
SetPasskeyList::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
// No transaction-specific invariants yet (future work).
}
bool
SetPasskeyList::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,338 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 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 <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/fee.h>
#include <test/jtx/multisign.h>
#include <test/jtx/noop.h>
#include <test/jtx/pay.h>
#include <test/jtx/sig.h>
#include <test/jtx/ter.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/jss.h>
#include <string>
#include <utility>
#include <vector>
namespace xrpl {
class PasskeyListSet_test : public beast::unit_test::Suite
{
json::Value
passkeyListSet(test::jtx::Account const& account)
{
json::Value jv;
jv[sfAccount.jsonName] = account.human();
jv[sfTransactionType.jsonName] = jss::PasskeyListSet;
jv[sfPasskeys.jsonName] = json::ValueType::Array;
jv[sfPasskeys.jsonName][0u][sfPasskey.jsonName][sfPasskeyID.jsonName] = "DEADBEEF";
jv[sfPasskeys.jsonName][0u][sfPasskey.jsonName][sfPublicKey.jsonName] =
strHex(account.pk());
return jv;
}
json::Value
passkeyListSetMulti(
test::jtx::Account const& account,
std::vector<std::pair<std::string, std::string>> const& entries)
{
json::Value jv;
jv[sfAccount.jsonName] = account.human();
jv[sfTransactionType.jsonName] = jss::PasskeyListSet;
jv[sfPasskeys.jsonName] = json::ValueType::Array;
for (json::UInt i = 0; i < entries.size(); ++i)
{
jv[sfPasskeys.jsonName][i][sfPasskey.jsonName][sfPasskeyID.jsonName] =
entries[i].first;
jv[sfPasskeys.jsonName][i][sfPasskey.jsonName][sfPublicKey.jsonName] =
entries[i].second;
}
return jv;
}
public:
void
testBasicPasskeyListSet(FeatureBitset features)
{
using namespace test::jtx;
testcase("basic passkey list set");
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::P256};
env.fund(XRP(1000), alice);
env.close();
env(passkeyListSet(alice));
env.close();
}
void
testValidMultiplePasskeys(FeatureBitset features)
{
using namespace test::jtx;
testcase("valid multiple passkeys");
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::P256};
Account const bob{"bob", KeyType::P256};
env.fund(XRP(1000), alice, bob);
env.close();
// Two valid entries with different IDs and different PublicKeys
auto jv = passkeyListSetMulti(
alice, {{"DEADBEEF01", strHex(alice.pk())}, {"DEADBEEF02", strHex(bob.pk())}});
env(jv);
env.close();
}
void
testEmptyPasskeyList(FeatureBitset features)
{
using namespace test::jtx;
testcase("empty passkey list rejected");
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::P256};
env.fund(XRP(1000), alice);
env.close();
json::Value jv;
jv[sfAccount.jsonName] = alice.human();
jv[sfTransactionType.jsonName] = jss::PasskeyListSet;
jv[sfPasskeys.jsonName] = json::ValueType::Array;
env(jv, Ter(temMALFORMED));
}
void
testDuplicatePasskeyID(FeatureBitset features)
{
using namespace test::jtx;
testcase("duplicate passkey ID rejected");
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::P256};
Account const bob{"bob", KeyType::P256};
env.fund(XRP(1000), alice, bob);
env.close();
// Two entries with the same PasskeyID but different PublicKeys
auto jv = passkeyListSetMulti(
alice, {{"DEADBEEF", strHex(alice.pk())}, {"DEADBEEF", strHex(bob.pk())}});
env(jv, Ter(temMALFORMED));
}
void
testDuplicatePublicKey(FeatureBitset features)
{
using namespace test::jtx;
testcase("duplicate public key rejected");
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::P256};
env.fund(XRP(1000), alice);
env.close();
// Two entries with different PasskeyIDs but the same PublicKey
auto jv = passkeyListSetMulti(
alice, {{"DEADBEEF01", strHex(alice.pk())}, {"DEADBEEF02", strHex(alice.pk())}});
env(jv, Ter(temMALFORMED));
}
void
testInvalidKeyType(FeatureBitset features)
{
using namespace test::jtx;
testcase("non-P256 key rejected");
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::P256};
Account const bob{"bob"}; // secp256k1
env.fund(XRP(1000), alice, bob);
env.close();
// A secp256k1 key should be rejected
auto jv = passkeyListSetMulti(alice, {{"DEADBEEF", strHex(bob.pk())}});
env(jv, Ter(temMALFORMED));
}
void
testEd25519KeyRejected(FeatureBitset features)
{
using namespace test::jtx;
testcase("ed25519 key rejected");
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::P256};
Account const carol{"carol", KeyType::Ed25519};
env.fund(XRP(1000), alice, carol);
env.close();
// An ed25519 key should be rejected
auto jv = passkeyListSetMulti(alice, {{"DEADBEEF", strHex(carol.pk())}});
env(jv, Ter(temMALFORMED));
}
void
testInvalidKeyPrefix(FeatureBitset features)
{
using namespace test::jtx;
testcase("invalid P256 prefix rejected");
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::P256};
env.fund(XRP(1000), alice);
env.close();
// Create a 65-byte key with wrong prefix (0x04 instead of 0xF6)
auto pkHex = strHex(alice.pk());
pkHex[0] = '0';
pkHex[1] = '4';
auto jv = passkeyListSetMulti(alice, {{"DEADBEEF", pkHex}});
env(jv, Ter(temMALFORMED));
}
void
testPasskeyPayment(FeatureBitset features)
{
using namespace test::jtx;
testcase("payment with passkey signer");
Env env{*this, envconfig(), features};
Account const alice{"alice"};
Account const bob{"bob"};
Account const dave{"dave", KeyType::P256};
env.fund(XRP(1000), alice, bob, dave);
env.close();
// Register dave's P-256 key as a passkey for alice's account.
env(passkeyListSetMulti(alice, {{"DEADBEEF", strHex(dave.pk())}}));
env(pay(alice, bob, XRP(100)), Sig(dave));
env.close();
BEAST_EXPECT(env.balance(bob) == XRP(1100));
}
void
testMultisignWithP256(FeatureBitset features)
{
using namespace test::jtx;
testcase("multisign with P256 signers");
Env env{*this, envconfig(), features};
Account const alice{"alice"};
Account const bob{"bob", KeyType::P256};
Account const carol{"carol", KeyType::P256};
env.fund(XRP(1000), alice, bob, carol);
env.close();
// Set up a signer list with P-256 accounts
env(signers(alice, 1, {{bob, 1}, {carol, 1}}));
env.close();
auto const baseFee = env.current()->fees().base;
// Multi-sign with one P-256 signer
env(noop(alice), Msig(bob), Fee(2 * baseFee));
env.close();
// Multi-sign with both P-256 signers
env(noop(alice), Msig(bob, carol), Fee(3 * baseFee));
env.close();
}
void
testMultisignMixedKeyTypes(FeatureBitset features)
{
using namespace test::jtx;
testcase("multisign with mixed key types including P256");
Env env{*this, envconfig(), features};
Account const alice{"alice"};
Account const bob{"bob"}; // secp256k1
Account const carol{"carol", KeyType::Ed25519};
Account const dave{"dave", KeyType::P256};
env.fund(XRP(1000), alice, bob, carol, dave);
env.close();
// Set up a signer list with mixed key types
env(signers(alice, 2, {{bob, 1}, {carol, 1}, {dave, 1}}));
env.close();
auto const baseFee = env.current()->fees().base;
// Multi-sign with secp256k1 + P-256
env(noop(alice), Msig(bob, dave), Fee(3 * baseFee));
env.close();
// Multi-sign with ed25519 + P-256
env(noop(alice), Msig(carol, dave), Fee(3 * baseFee));
env.close();
// Multi-sign with all three key types
env(noop(alice), Msig(bob, carol, dave), Fee(4 * baseFee));
env.close();
}
void
run() override
{
using namespace test::jtx;
auto const sa = testableAmendments();
testBasicPasskeyListSet(sa);
testValidMultiplePasskeys(sa);
testEmptyPasskeyList(sa);
testDuplicatePasskeyID(sa);
testDuplicatePublicKey(sa);
testInvalidKeyType(sa);
testEd25519KeyRejected(sa);
testInvalidKeyPrefix(sa);
testPasskeyPayment(sa);
testMultisignWithP256(sa);
testMultisignMixedKeyTypes(sa);
}
};
BEAST_DEFINE_TESTSUITE(PasskeyListSet, app, xrpl);
} // namespace xrpl

View File

@@ -0,0 +1,143 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 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 <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/pay.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <array>
#include <cstdint>
namespace xrpl {
class PassKey_test : public beast::unit_test::Suite
{
void
testP256KeyTypeDetection()
{
testcase("P256 key type requires 0xF6 prefix");
using namespace test::jtx;
// Valid P-256 key from the test framework
Account const p256acct{"p256acct", KeyType::P256};
auto const keyType = publicKeyType(p256acct.pk());
BEAST_EXPECT(keyType.has_value());
BEAST_EXPECT(*keyType == KeyType::P256);
// A 65-byte buffer with 0x04 prefix (standard uncompressed EC)
// must NOT be accepted as P-256 on XRPL
std::array<uint8_t, 65> badKey{};
badKey[0] = 0x04;
auto const badType = publicKeyType(makeSlice(badKey));
BEAST_EXPECT(!badType.has_value());
// A 65-byte buffer with 0xF6 prefix should be accepted
std::array<uint8_t, 65> goodKey{};
goodKey[0] = 0xF6;
auto const goodType = publicKeyType(makeSlice(goodKey));
BEAST_EXPECT(goodType.has_value());
BEAST_EXPECT(*goodType == KeyType::P256);
// Wrong size keys should not be detected as P-256
std::array<uint8_t, 33> shortKey{};
shortKey[0] = 0xF6;
auto const shortType = publicKeyType(makeSlice(shortKey));
BEAST_EXPECT(!shortType.has_value() || *shortType != KeyType::P256);
std::array<uint8_t, 66> longKey{};
longKey[0] = 0xF6;
auto const longType = publicKeyType(makeSlice(longKey));
BEAST_EXPECT(!longType.has_value());
}
void
testP256SingleSign(FeatureBitset features)
{
using namespace test::jtx;
testcase("P256 single sign");
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::P256};
Account const bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
env(pay(alice, bob, XRP(100)));
env.close();
// Verify the payment went through
BEAST_EXPECT(env.balance(bob) == XRP(1100));
}
void
testP256WithOtherKeyTypes(FeatureBitset features)
{
using namespace test::jtx;
testcase("P256 alongside other key types");
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::P256};
Account const bob{"bob"}; // secp256k1
Account const carol{"carol", KeyType::Ed25519};
env.fund(XRP(1000), alice, bob, carol);
env.close();
// All key types should work for payments
env(pay(alice, bob, XRP(10)));
env(pay(bob, carol, XRP(10)));
env(pay(carol, alice, XRP(10)));
env.close();
}
void
testWithFeats(FeatureBitset features)
{
testP256SingleSign(features);
testP256WithOtherKeyTypes(features);
}
public:
void
run() override
{
using namespace test::jtx;
auto const sa = testableAmendments();
// Protocol-level tests (no env needed)
testP256KeyTypeDetection();
// Integration tests with env
testWithFeats(sa);
}
};
BEAST_DEFINE_TESTSUITE(PassKey, protocol, xrpl);
} // namespace xrpl

View File

@@ -0,0 +1,203 @@
// Auto-generated unit tests for ledger entry PasskeyList
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol_autogen/ledger_entries/PasskeyList.h>
#include <xrpl/protocol_autogen/ledger_entries/Ticket.h>
#include <string>
namespace xrpl::ledger_entries {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed for both the
// builder's STObject and the wrapper's SLE.
TEST(PasskeyListTests, BuilderSettersRoundTrip)
{
uint256 const index{1u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const ownerValue = canonical_ACCOUNT();
auto const passkeysValue = canonical_ARRAY();
PasskeyListBuilder builder{
previousTxnIDValue,
previousTxnLgrSeqValue,
ownerNodeValue,
ownerValue,
passkeysValue
};
builder.setLedgerIndex(index);
builder.setFlags(0x1u);
EXPECT_TRUE(builder.validate());
auto const entry = builder.build(index);
EXPECT_TRUE(entry.validate());
{
auto const& expected = previousTxnIDValue;
auto const actual = entry.getPreviousTxnID();
expectEqualField(expected, actual, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const actual = entry.getPreviousTxnLgrSeq();
expectEqualField(expected, actual, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = ownerNodeValue;
auto const actual = entry.getOwnerNode();
expectEqualField(expected, actual, "sfOwnerNode");
}
{
auto const& expected = ownerValue;
auto const actual = entry.getOwner();
expectEqualField(expected, actual, "sfOwner");
}
{
auto const& expected = passkeysValue;
auto const actual = entry.getPasskeys();
expectEqualField(expected, actual, "sfPasskeys");
}
EXPECT_TRUE(entry.hasLedgerIndex());
auto const ledgerIndex = entry.getLedgerIndex();
ASSERT_TRUE(ledgerIndex.has_value());
EXPECT_EQ(*ledgerIndex, index);
EXPECT_EQ(entry.getKey(), index);
}
// 2 & 4) Start from an SLE, set fields directly on it, construct a builder
// from that SLE, build a new wrapper, and verify all fields (and validate()).
TEST(PasskeyListTests, BuilderFromSleRoundTrip)
{
uint256 const index{2u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const ownerValue = canonical_ACCOUNT();
auto const passkeysValue = canonical_ARRAY();
auto sle = std::make_shared<SLE>(PasskeyList::entryType, index);
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
sle->at(sfOwnerNode) = ownerNodeValue;
sle->at(sfOwner) = ownerValue;
sle->setFieldArray(sfPasskeys, passkeysValue);
PasskeyListBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
auto const entryFromBuilder = builderFromSle.build(index);
PasskeyList entryFromSle{sle};
EXPECT_TRUE(entryFromBuilder.validate());
EXPECT_TRUE(entryFromSle.validate());
{
auto const& expected = previousTxnIDValue;
auto const fromSle = entryFromSle.getPreviousTxnID();
auto const fromBuilder = entryFromBuilder.getPreviousTxnID();
expectEqualField(expected, fromSle, "sfPreviousTxnID");
expectEqualField(expected, fromBuilder, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const fromSle = entryFromSle.getPreviousTxnLgrSeq();
auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq();
expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq");
expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = ownerNodeValue;
auto const fromSle = entryFromSle.getOwnerNode();
auto const fromBuilder = entryFromBuilder.getOwnerNode();
expectEqualField(expected, fromSle, "sfOwnerNode");
expectEqualField(expected, fromBuilder, "sfOwnerNode");
}
{
auto const& expected = ownerValue;
auto const fromSle = entryFromSle.getOwner();
auto const fromBuilder = entryFromBuilder.getOwner();
expectEqualField(expected, fromSle, "sfOwner");
expectEqualField(expected, fromBuilder, "sfOwner");
}
{
auto const& expected = passkeysValue;
auto const fromSle = entryFromSle.getPasskeys();
auto const fromBuilder = entryFromBuilder.getPasskeys();
expectEqualField(expected, fromSle, "sfPasskeys");
expectEqualField(expected, fromBuilder, "sfPasskeys");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
TEST(PasskeyListTests, WrapperThrowsOnWrongEntryType)
{
uint256 const index{3u};
// Build a valid ledger entry of a different type
// Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq
// Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(PasskeyList{wrongEntry.getSle()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong ledger entry type.
TEST(PasskeyListTests, BuilderThrowsOnWrongEntryType)
{
uint256 const index{4u};
// Build a valid ledger entry of a different type
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(PasskeyListBuilder{wrongEntry.getSle()}, std::runtime_error);
}
}

View File

@@ -0,0 +1,146 @@
// Auto-generated unit tests for transaction PasskeyListSet
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/PasskeyListSet.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsPasskeyListSetTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testPasskeyListSet"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const passkeysValue = canonical_ARRAY();
PasskeyListSetBuilder builder{
accountValue,
passkeysValue,
sequenceValue,
feeValue
};
// Set optional fields
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = passkeysValue;
auto const actual = tx.getPasskeys();
expectEqualField(expected, actual, "sfPasskeys");
}
// Verify optional fields
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsPasskeyListSetTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testPasskeyListSetFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const passkeysValue = canonical_ARRAY();
// Build an initial transaction
PasskeyListSetBuilder initialBuilder{
accountValue,
passkeysValue,
sequenceValue,
feeValue
};
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
PasskeyListSetBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = passkeysValue;
auto const actual = rebuiltTx.getPasskeys();
expectEqualField(expected, actual, "sfPasskeys");
}
// Verify optional fields
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsPasskeyListSetTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(PasskeyListSet{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsPasskeyListSetTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(PasskeyListSetBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
}

View File

@@ -720,6 +720,15 @@ parseRippleState(
return keylet::trustLine(*id1, *id2, uCurrency).key;
}
static std::expected<uint256, json::Value>
parsePasskeyList(
json::Value const& params,
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
}
static std::expected<uint256, json::Value>
parseSignerList(
json::Value const& params,