Signatures
The signature schemes Umbra relies on, where each is checked, and why the attesters' signatures combine into one.
Umbra's programs and proofs rely on four signature schemes, each where its properties fit: BLS on BN254 for the combined attester signature, EdDSA-Poseidon on BabyJubJub for zone approvals checked inside the proof, Ed25519 for Solana wallets, and ECDSA P-384 because that is what AWS signs Nitro attestations with. A fifth, a composite of ML-DSA-87 and Ed25519, is checked only off chain, between guardian nodes and the services that talk to them.
BLS on BN254: the combined attester signature
BLS signatures have a property the other schemes lack: signatures by different keys on the same message add up. The sum of the signatures verifies against the sum of the public keys, in one check. Umbra uses that to collapse six attesters into one 32-byte signature.
In Umbra's BLS, a public key is a point in BN254's G2 group and a signature is a point in G1, 32 bytes compressed. A message is hashed onto G1 by trying SHA-256 of a 16-byte domain tag, the message and an incrementing counter byte until the result is the x-coordinate of a curve point. A value transaction spares the program that search: the last byte of its payload, outside the digest the attesters sign, is the counter at which the search lands, and the program makes that one try. Any other counter either finds no point, and the transaction is refused, or finds a different point that no attester signed, and the signature fails. Every signature is hashed under the tag umbra/bls/sig/v1, and every proof of possession (described below) under umbra/bls/pop/v1. Verification checks one product of two pairings: that the message hash paired with the public key equals the signature paired with the group generator. process_3x3x2x2 folds that check into the same pairing call as its two proofs, weighting it by a random scalar drawn from a hash of the signature, the message hash, the key and both proofs, so the combined check still fails whenever the signature would.
Every attester signs the same message, the transaction's 32-byte context digest (see hash functions). Your device collects the six signatures and adds them. The shielded pool program then builds the matching key:
- the guardian node registry keeps each cluster's aggregate key, the sum of its members' keys, adding a key when a member joins and subtracting it when one leaves;
- the program adds the keys of the active screener, the fee enclave and the data endorser, read from their registrations, or takes that sum from the attesters' cached committee key, as described below;
- it checks the one signature against that sum.
A registration is signed the same way by four parties: the three guardian nodes and the data endorser.
The cached committee key
Adding the four keys costs every value transaction three G2 additions. A committee-key account holds that work done once: for one guardian cluster, one screener and one data endorser, it stores the four keys (the cluster's aggregate key, the screener's, the fee enclave's and the data endorser's) and their sum. Its address is derived from the cluster identifier and the two node indexes, so each combination of attesters has exactly one account. The fee enclave has a single record, so it is not part of the address.
Anyone may create one, paying its rent, and anyone may refresh one at any time, with no signature and no delay. Both read the four records afresh and accept them only as active members: the cluster must be the active cluster the account names, and each registration must be the node index the account names, of the right kind. The program computes the sum itself and refuses a sum that is the point at infinity or carries a flag bit. So the account can only ever hold the current keys of its own attesters and their true sum; a stale account, after a key rotation, is put right by anyone who refreshes it.
A value transaction names the committee-key account in the account slot the code calls transaction_verifying_key. The transaction verifying key itself is built into the program, so that slot carries no key. The program uses the cached sum only when that account is a committee-key account the program wrote: owned by the pool, at the address its stored cluster and node indexes derive, and initialized. Its four stored keys must also equal, byte for byte, the four keys the transaction has just read from the records. Any other account in that slot, or a cache whose keys differ, and the program adds the four keys itself, so a missing or stale cache only costs the additions. Since the cached sum is always the program's own sum of the keys stored beside it, using it never changes the key the signature is checked under. Naming the cache saves about two thousand compute units.
The key is a plain sum with no threshold. If any one attester does not sign, the sum of the other signatures does not verify, and the transaction is refused. That gives each attester a veto over every value transaction, which liveness and the escape hatch deal with.
Rogue keys and proof of possession
Summing keys has a known weakness. Someone registering after the others could choose their public key as a chosen point minus the sum of everyone else's keys. The sum would then be a key only they control, and they could sign alone for the whole group.
Every registry that accepts a BLS key for the pool refuses it without a proof of possession: a signature by that key over the key's own 128 bytes, hashed under the proof-of-possession tag, which only the holder of the private key can make. The guardian node registry, the screener registry (which registers both the screener and the data endorser) and the fee enclave program all check it. They also check that the key lies in the correct prime-order subgroup, is not the point at infinity and carries no stray flag bits in its encoding. A key that passes cannot have been computed from the others.
The two tags are what keep a proof apart from an ordinary signature. If both were hashed the same way, a proof would just be a signature over 128 particular bytes. An attester that could be led to sign 128 bytes of someone else's choosing would then hand them the one piece they cannot compute: the attester's share of a proof for a key built to cancel the attester's own. Because signatures and proofs are hashed under different tags, no signature over any message, of any length, can be part of a proof, and no published proof verifies as a signature.
EdDSA-Poseidon on BabyJubJub: zone approvals
A zone authority approves notes leaving or entering its zone by signing. If the chain checked that signature, it would learn which zone was involved. Umbra checks it inside the transaction proof instead.
BabyJubJub is a twisted Edwards curve defined over the BN254 scalar field, the field the circuits already work in, so its point arithmetic costs little to prove. Zone authorities sign with EdDSA using Poseidon as the hash, the scheme circomlib provides a verifier for.
What a zone authority signs is bound to one transaction: the Poseidon hash of domain 63 (for the source zone) or 64 (for the destination zone) and a hash of the transaction's context digest. Inside the proof, the circuit checks that the key and the signature's point lie on the curve, that the signature verifies, and that the pair (key, zone number) is a leaf of the zone signer-set tree. Only the root of that tree enters the proof's public input. A chain observer therefore cannot see which zone signed, which key signed, or whether any zone signature was needed at all. See zones.
Ed25519: wallets
Ed25519 is Solana's own signature scheme, and the runtime checks it on every transaction. Umbra meets it in two places.
- Your main wallet pays Solana fees for what you submit yourself, and signs the fixed message whose signature becomes your account signature.
- Your platform wallet, a keypair Umbra's clients derive from that account signature, signs the binding to your main wallet, signs registration as the bound wallet and signs deposits as the depositor.
The account signature depends on a property of Ed25519 as RFC 8032 specifies it: signing is deterministic, so the same wallet signing the same message always produces the same 64 bytes. Deriving keys from that signature is the standard Umbra's SDKs and client interfaces follow, not a protocol rule. See the account signature.
ECDSA P-384: attestation certificates
AWS Nitro signs each attestation document, and each certificate in the chain behind it, with ECDSA on the P-384 curve and SHA-384. The Solana runtime has no syscall for P-384, so the P-384 signature verifier program does the arithmetic itself, spread across several transactions:
Open a session. A requester submits the public key, the signature and the message length. The program refuses a key that is not on the curve and a signature component that is zero or out of range.
Hash the message. The requester uploads the message in chunks, and the program finishes the SHA-384 hash.
Verify in steps. Anyone may crank the verification: each call advances the double scalar multiplication by a bounded number of steps.
Publish the result. Once the verdict is recorded and accepted, the requester publishes a result marker, an account whose address commits to the key and the message.
The Nitro attestation verifier then requires one accepted result marker for each certificate link and one for the document itself before it accepts the attestation. See trusted execution.
Composite signatures off chain
Guardian nodes sign their responses, and verify requests forwarded by their relays, with a composite of ML-DSA-87 and Ed25519. Both halves sign the same digest, and a signature is valid only if both verify, so forging one requires breaking both schemes. These signatures are checked by the services that receive them, not by any Solana program.
Related
Last updated
Zero-knowledge proofs
What a statement, a witness and a public input are, how Groth16 proofs are built in Circom and verified by Solana programs, and what the verifier learns.
Encryption
How new notes are encrypted to their recipients, how viewing-key shares reach the enclaves, and how the sender record is encrypted inside the proof.