# Transaction Signing & Verification

[← Back to Cryptography I: Blockchain Security and Cryptographic Foundations](/core-dev-bootcamp/module04.md)

### Introduction

Digital signatures are the heart of XRPL's security. Every transaction must be signed with the private key corresponding to the sending account. This signature is mathematical proof that the account owner authorized the transaction. Without a valid signature, a transaction is rejected immediately.

In this chapter, we'll trace the complete signing and verification pipeline, understand the differences between secp256k1 and ed25519, and explore why canonical signatures matter.

### The Signature: Mathematical Proof of Authorization

A digital signature proves three things:

1. **Authenticity**: The signature was created by someone with the secret key
2. **Integrity**: The signed data hasn't been modified
3. **Non-repudiation**: The signer cannot deny having signed

```
Transaction Data + Secret Key  →  Signature
Transaction Data + Public Key + Signature  →  Valid/Invalid
```

### Creating a Signature

#### The High-Level Interface

```
// From src/libxrpl/protocol/SecretKey.cpp
Buffer sign(
    PublicKey const& pk,
    SecretKey const& sk,
    Slice const& m)
{
    // Automatically detect key type from public key
    auto const type = publicKeyType(pk.slice());

    switch (*type)
    {
        case KeyType::ed25519:
            return signEd25519(pk, sk, m);

        case KeyType::secp256k1:
            return signSecp256k1(pk, sk, m);
    }
}
```

**Parameters:**

* `pk`: Public key (for key type detection)
* `sk`: Secret key (the signing key)
* `m`: Message (the data to sign)

**Returns:**

* A `Buffer` containing the signature bytes

#### Ed25519 Signing: Simple and Fast

```
case KeyType::ed25519: {
    Buffer b(64);  // Ed25519 signatures are always 64 bytes

    ed25519_sign(
        m.data(), m.size(),     // Message to sign
        sk.data(),               // Secret key
        pk.data() + 1,           // Public key (skip 0xED prefix)
        b.data());               // Output buffer

    return b;
}
```

**How it works:**

1. Allocate 64-byte buffer
2. Call `ed25519_sign` with message, keys, and output buffer
3. Return the signature

**Properties:**

* Always produces exactly 64 bytes
* Deterministic: same message + key = same signature
* Fast: \~50 microseconds
* No pre-hashing needed

**Signature format:**

```
[R (32 bytes)][S (32 bytes)] = 64 bytes total
```

Where R and S are elliptic curve points/scalars (mathematical details abstracted by the library).

#### Secp256k1 Signing: More Complex

```
case KeyType::secp256k1: {
    // Step 1: Hash the message with SHA-512-Half
    sha512_half_hasher h;
    h(m.data(), m.size());
    auto const digest = sha512_half_hasher::result_type(h);

    // Step 2: Sign the digest (not the raw message)
    secp256k1_ecdsa_signature sig_imp;
    secp256k1_ecdsa_sign(
        secp256k1Context(),
        &sig_imp,
        reinterpret_cast<unsigned char const*>(digest.data()),
        reinterpret_cast<unsigned char const*>(sk.data()),
        secp256k1_nonce_function_rfc6979,  // Deterministic nonce
        nullptr);

    // Step 3: Serialize to DER format
    unsigned char sig[72];
    size_t len = sizeof(sig);
    secp256k1_ecdsa_signature_serialize_der(
        secp256k1Context(),
        sig,
        &len,
        &sig_imp);

    return Buffer{sig, len};
}
```

**How it works:**

1. **Pre-hash the message**: Compute SHA-512-Half of the message
2. **Sign the digest**: Use ECDSA to sign the 32-byte hash
3. **Serialize**: Encode signature in DER format

**Why pre-hash?**

* ECDSA works on fixed-size inputs (32 bytes)
* Messages can be any size
* Hashing first normalizes all inputs to 32 bytes
* Security proof for ECDSA assumes you're signing a hash

**Why DER encoding?**\
DER (Distinguished Encoding Rules) is a standard binary format from X.509:

```
DER Format:
0x30 [total length]
    0x02 [R length] [R bytes]
    0x02 [S length] [S bytes]

Example:
30 44
   02 20 [32 bytes of R]
   02 20 [32 bytes of S]
Total: ~70-72 bytes (variable length!)
```

**Deterministic Nonces (RFC 6979):**

```
secp256k1_nonce_function_rfc6979
```

This is critical for security. ECDSA requires a random "nonce" (number used once) for each signature. If:

* The same nonce is used twice with the same key → **secret key can be extracted**
* The nonce is predictable → **secret key can be extracted**

RFC 6979 derives the nonce deterministically from the message and secret key, making it:

* Different for every message
* Unpredictable to attackers
* Free from random number generation failures

### Verifying a Signature

#### The High-Level Interface

```
// From src/libxrpl/protocol/PublicKey.cpp
bool verify(
    PublicKey const& publicKey,
    Slice const& m,
    Slice const& sig,
    bool mustBeFullyCanonical) noexcept
{
    // Detect key type
    auto const type = publicKeyType(publicKey);
    if (!type)
        return false;

    if (*type == KeyType::secp256k1)
    {
        return verifySecp256k1(publicKey, m, sig, mustBeFullyCanonical);
    }
    else if (*type == KeyType::ed25519)
    {
        return verifyEd25519(publicKey, m, sig);
    }

    return false;
}
```

**Parameters:**

* `publicKey`: The public key to verify against
* `m`: The message that was signed
* `sig`: The signature to verify
* `mustBeFullyCanonical`: Whether to enforce strict canonicality (important!)

**Returns:**

* `true` if signature is valid
* `false` if signature is invalid or malformed

#### Ed25519 Verification

```
else if (*type == KeyType::ed25519)
{
    // Check signature is canonical
    if (!ed25519Canonical(sig))
        return false;

    // Verify signature
    return ed25519_sign_open(
        m.data(), m.size(),              // Message
        publicKey.data() + 1,             // Public key (skip 0xED prefix)
        sig.data()) == 0;                 // Signature
}
```

**Canonicality check:**

```
static bool ed25519Canonical(Slice const& sig)
{
    // Signature must be exactly 64 bytes
    if (sig.size() != 64)
        return false;

    // Ed25519 curve order (big-endian)
    static std::uint8_const Order[] = {
        0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x14, 0xDE, 0xF9, 0xDE, 0xA2, 0xF7, 0x9C, 0xD6,
        0x58, 0x12, 0x63, 0x1A, 0x5C, 0xF5, 0xD3, 0xED
    };

    // S component (second 32 bytes) must be < Order
    auto const le = sig.data() + 32;
    std::uint8_t S[32];
    std::reverse_copy(le, le + 32, S);  // Convert to big-endian

    return std::lexicographical_compare(S, S + 32, Order, Order + 32);
}
```

**Why check canonicality?**\
Ensures the S component is in the valid range. This prevents malformed signatures from being processed.

#### Secp256k1 Verification

```
if (*type == KeyType::secp256k1)
{
    // Hash the message first (same as signing)
    return verifyDigest(
        publicKey,
        sha512Half(m),
        sig,
        mustBeFullyCanonical);
}
```

**The digest verification function:**

```
bool verifyDigest(
    PublicKey const& publicKey,
    uint256 const& digest,
    Slice const& sig,
    bool mustBeFullyCanonical)
{
    // Check signature canonicality
    auto const canonical = ecdsaCanonicality(sig);
    if (!canonical)
        return false;

    if (mustBeFullyCanonical && *canonical != ECDSACanonicality::fullyCanonical)
        return false;

    // Parse public key
    secp256k1_pubkey pubkey_imp;
    if (secp256k1_ec_pubkey_parse(
            secp256k1Context(),
            &pubkey_imp,
            reinterpret_cast<unsigned char const*>(publicKey.data()),
            publicKey.size()) != 1)
        return false;

    // Parse signature from DER
    secp256k1_ecdsa_signature sig_imp;
    if (secp256k1_ecdsa_signature_parse_der(
            secp256k1Context(),
            &sig_imp,
            reinterpret_cast<unsigned char const*>(sig.data()),
            sig.size()) != 1)
        return false;

    // Verify!
    return secp256k1_ecdsa_verify(
        secp256k1Context(),
        &sig_imp,
        reinterpret_cast<unsigned char const*>(digest.data()),
        &pubkey_imp) == 1;
}
```

**Steps:**

1. **Check canonicality**: Ensure signature is in canonical form
2. **Parse public key**: Convert from compressed format to library format
3. **Parse signature**: Decode DER encoding
4. **Verify**: Check mathematical relationship between public key, message, and signature

### Signature Malleability and Canonicality

#### The Problem: Signature Malleability

In secp256k1, a signature is a pair of numbers (R, S). Due to the mathematics of elliptic curves:

```
If (R, S) is valid, then (R, -S mod n) is also valid
```

Where `n` is the curve order. This means **one message has two valid signatures**.

**Why this is dangerous:**

```
// Alice creates and signs a transaction
Transaction tx = Payment{ /* ... */ };
Signature sig1 = sign(alice.publicKey, alice.secretKey, tx);

// Transaction ID includes the signature
uint256 txID1 = hash(tx, sig1);

// Attacker sees tx + sig1 in network
// Attacker creates malleated signature sig2 = (R, -S mod n)
Signature sig2 = malleate(sig1);

// sig2 is also valid!
bool valid = verify(alice.publicKey, tx, sig2);  // Returns true

// But produces different transaction ID
uint256 txID2 = hash(tx, sig2);
assert(txID1 != txID2);  // Different IDs!
```

**Attack scenarios:**

1. **Transaction ID confusion**: Applications tracking txID1 won't see the transaction confirmed (it confirms as txID2)
2. **Double-spend attempts**: Submit both versions, one might get through
3. **Chain reaction**: If txID is used as input to another transaction, that transaction becomes invalid

#### The Solution: Canonical Signatures

**Require S to be in the "low" range:**

```
// Canonical if S <= order/2
if (S > order/2) {
    S = order - S;  // Flip to the low range
}
```

This makes each signature unique—only one valid signature per message.

#### Checking Canonicality

```
std::optional<ECDSACanonicality>
ecdsaCanonicality(Slice const& sig)
{
    // Parse DER-encoded signature
    auto r = sigPart(p);  // Extract R
    auto s = sigPart(p);  // Extract S

    if (!r || !s)
        return std::nullopt;  // Invalid DER encoding

    uint264 R(sliceToHex(*r));
    uint264 S(sliceToHex(*s));

    // secp256k1 curve order
    static uint264 const G(
        "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141");

    // Both R and S must be < G
    if (R >= G || S >= G)
        return std::nullopt;

    // Calculate G - S (the "flipped" value)
    auto const Sp = G - S;

    // Is S in the lower half?
    if (S > Sp)
        return ECDSACanonicality::canonical;  // Valid but not fully canonical

    return ECDSACanonicality::fullyCanonical;  // Perfect!
}
```

**Canonicality levels:**

```
enum class ECDSACanonicality {
    fullyCanonical,  // S <= order/2 (preferred)
    canonical        // S > order/2 but valid (deprecated)
};
```

**Enforcement:**

```
if (mustBeFullyCanonical && *canonical != ECDSACanonicality::fullyCanonical)
    return false;  // Reject non-canonical signatures
```

In production, XRPL always sets `mustBeFullyCanonical = true` to prevent malleability.

#### Ed25519: No Malleability

Ed25519 signatures are inherently canonical—there's only one valid signature per message. The curve mathematics don't allow the kind of malleability that exists in ECDSA.

```
// Ed25519: Each message has exactly ONE valid signature
Signature sig = sign(pk, sk, message);
// No way to create sig2 that's also valid
```

This is one of the design advantages of Ed25519 over secp256k1.

### Transaction Signing in Practice

#### Signing a Transaction

```
// From src/libxrpl/protocol/STTx.cpp
void STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
{
    // Serialize transaction for signing
    Serializer s = buildMultiSigningData(*this, publicKey);

    // Create signature
    auto const signature = ripple::sign(publicKey, secretKey, s.slice());

    // Add signature to transaction
    setFieldVL(sfTxnSignature, signature);
}
```

**What gets signed:**

```
Serializer buildMultiSigningData(STTx const& tx, PublicKey const& pk)
{
    Serializer s;

    // Add signing prefix
    s.add32(HashPrefix::txSign);

    // Serialize all transaction fields except signature
    tx.addWithoutSigningFields(s);

    return s;
}
```

The signature is computed over:

1. A prefix (`HashPrefix::txSign`)
2. All transaction fields (except the signature itself)

#### Verifying a Transaction

```
bool STTx::checkSign(bool mustBeFullyCanonical) const
{
    try
    {
        // Get the signing public key
        auto const publicKey = getSigningPubKey();

        // Get the signature
        auto const signature = getFieldVL(sfTxnSignature);

        // Rebuild the data that was signed
        Serializer s = buildMultiSigningData(*this, publicKey);

        // Verify!
        return verify(publicKey, s.slice(), signature, mustBeFullyCanonical);
    }
    catch (...)
    {
        return false;  // Any error = invalid
    }
}
```

### Multi-Signing

XRPL supports multi-signature transactions where multiple parties must sign:

```
struct Signer {
    AccountID account;
    PublicKey publicKey;
    Buffer signature;
    uint16_t weight;
};

bool checkMultiSign(STTx const& tx) {
    auto const signers = tx.getFieldArray(sfSigners);
    uint32_t totalWeight = 0;

    for (auto const& signer : signers) {
        // Extract signer info
        auto const account = signer.getAccountID(sfAccount);
        auto const pubKey = signer.getFieldVL(sfSigningPubKey);
        auto const sig = signer.getFieldVL(sfTxnSignature);

        // Verify this signer's signature
        Serializer s = buildMultiSigningData(tx, account, pubKey);
        if (!verify(pubKey, s.slice(), sig, true))
            return false;  // Invalid signature

        // Add weight
        totalWeight += getSignerWeight(account);
    }

    // Check if total weight meets quorum
    return totalWeight >= getRequiredQuorum(tx);
}
```

Each signer independently signs the transaction, and all signatures are verified.

### Performance Characteristics

#### Signing Speed

```
Ed25519:    ~50 microseconds
Secp256k1:  ~200 microseconds

Ed25519 is 4x faster for signing.
```

#### Verification Speed

```
Ed25519:    ~100 microseconds
Secp256k1:  ~500 microseconds

Ed25519 is 5x faster for verification.
```

**Why verification speed matters:**

Every validator must verify every transaction signature. In a high-throughput system:

```
1000 transactions/second × 500 μs/verification = 0.5 seconds of CPU time
1000 transactions/second × 100 μs/verification = 0.1 seconds of CPU time
```

Ed25519's speed advantage is significant at scale.

#### Signature Size

```
Ed25519:    64 bytes (fixed)
Secp256k1:  ~71 bytes (variable, DER encoded)

Ed25519 signatures are slightly smaller and fixed-size.
```

### Summary

Digital signatures in XRPL:

1. **Purpose**: Prove authorization, ensure integrity, enable non-repudiation
2. **Two algorithms**:
   * **secp256k1**: Hash-then-sign, DER encoding, requires canonicality checks
   * **ed25519**: Direct signing, fixed 64 bytes, inherently canonical
3. **Signing**: Secret key + message → signature
4. **Verification**: Public key + message + signature → valid/invalid
5. **Malleability**: secp256k1 requires canonical signatures to prevent attacks
6. **Performance**: ed25519 is faster for both signing and verification

**Key takeaways:**

* Always enforce canonical signatures for secp256k1
* Ed25519 is recommended for new accounts (faster, simpler)
* Verification happens for every transaction in the network
* Multi-signing allows multiple parties to authorize a transaction

In the next chapter, we'll explore hash functions and how they're used throughout XRPL for integrity and identification.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.xrpl-commons.org/core-dev-bootcamp/module04/transaction-signing-and-verification.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
