Skip to content

Transaction input encryption — simple validator-key scheme (Phase 1) #2319

Description

@sergerad

Title: Transaction input encryption — simple validator-key scheme (Phase 1)

Background

From the design discussion: today, private transaction data (TransactionInputs) is visible in plaintext to both the RPC operator and the validators. The long-term design (Bobbin's 3-step sequencing) is:

  1. Simple scheme (this issue): a single shared submission (encryption) key across the validator set, signed-attested by each validator's own signing key, no TEE.
  2. Threshold encryption in miden-crypto to separate submission keys from archival keys (separate issue).
  3. TEE integration, including a proper distributed setup protocol so no party outside the TEEs ever learns the shared key (separate issue).

This issue covers only step 1.

Scope

In scope:

  • Each validator continues to manage its own individual signing keypair (as today — one signing key per validator, committed in the block header of blocks it signs).
  • In addition, every validator in the set is configured with the same shared X25519 encryption keypair (the "submission key"). This is provisioned identically to each validator instance rather than derived independently.
  • New GetTransactionEncryptionKey endpoint, served by each RPC (forwarded to a validator), returning the shared public key attested by the corresponding validator's own signing key.
  • Any validator in the set can decrypt TransactionInputs on receipt, since they all hold the same shared secret.

Out of scope (future issues):

  • The distributed setup protocol that lets multiple validators arrive at the same encryption key without any party (including the validators themselves) knowing it outside a TEE — that's step 3. For this phase, the shared secret is provisioned to each validator out-of-band by the operator.
  • Threshold encryption / threshold key-derivation scheme (step 2).
  • TEE-based validator execution (step 3).
  • Client-side changes (periodic key fetch, encrypting TransactionInputs before submission) — these belong in miden-client/miden-base, not this repo.

Design

Key management
Each validator keeps its existing per-validator ValidatorSigner (bin/validator/src/signers/mod.rs) unchanged. Alongside it, add an encryption-key counterpart built on miden-crypto's existing IES machinery (ies::{SealingKey, UnsealingKey}, ecdh::x25519, dsa::eddsa_25519_sha512::KeyExchangeKey). Unlike the signing key, this key's secret material must be identical across every validator in the set.

No KMS-backed key
Unlike the signing key (--key.kms-id, backed by AWS KMS Sign), the encryption key has no KMS equivalent: AWS KMS's DeriveSharedSecret (ECDH) only supports NIST P-256/P-384/P-521, and both curves miden-crypto's IES module actually offers — secp256k1 (K256XChaCha20Poly1305) and X25519 (X25519XChaCha20Poly1305/X25519AeadPoseidon2) — are excluded from KMS key agreement entirely (they're sign/verify-only key specs in KMS). Adding a NIST-curve IES scheme, plus a miden-crypto API that accepts an externally-computed shared secret (so a KMS-derived secret could feed the existing KDF/AEAD pipeline), would be required to change this (which will not be done). This is also a dead end strategically, not just technically: step 3 replaces this key with TEE-managed material derived via a distributed, attested setup protocol, a trust model KMS has no place in — so this key was never going to live in KMS long-term regardless of AWS's curve support.

Secrets Manager
The shared secret will be provisioned to every validator via a secrets manager (e.g. AWS Secrets Manager) rather than KMS: each validator fetches the same secret value at startup and constructs the local UnsealingKey/SealingKey from it directly. Config should mirror the signing key's CLI/env pattern in bin/validator/src/commands/mod.rs (--key.hex/MIDEN_VALIDATOR_KEY): add --encryption-key.hex/MIDEN_VALIDATOR_ENCRYPTION_KEY, sourced from Secrets Manager in production deployments. Actually protecting the key from the validator's own operator is out of scope for this phase — that's what step 3's TEE-based setup protocol is for.

Attestation
Each validator signs the shared encryption public-key bytes with its own individual ECDSA signing key (the same key already committed in the block header as validator_key for blocks it produces) — so the signature differs per validator even though the underlying encryption public key bytes are identical across the set. That is, for the same encryption_pubkey_bytes, validator A's response carries sign(encryption_pubkey_bytes, A's signing key) while validator B's carries sign(encryption_pubkey_bytes, B's signing key): same data, different (validator-specific) signature. "Attested" here means the signature is proof that a specific, chain-recognized validator identity is vouching for the key — it's what lets a client trust the key came from a legitimate validator rather than an impostor RPC operator. No block header / consensus format changes needed: the encryption key is delivered out-of-band, and the caller verifies the signature against whichever validator's signing key it already trusts from the chain.

Wire format for encrypted inputs
Currently ProvenTransaction.transaction_inputs (in proto/proto/types/transaction.proto) carries raw serialized TransactionInputs bytes, decoded directly in bin/validator/src/tx_validation/data_store.rs / submit_proven_transaction.rs. This needs to become a serialized SealedMessage (miden-crypto already provides Serializable/Deserializable for it) wrapping the encrypted TransactionInputs bytes. Since the decryption secret is shared, whichever validator in the set receives the submission can decrypt it — this is what makes the scheme tolerant of routing to any validator, not just the one whose signing key the client originally verified against.

Encryption at rest
Today, after validate_transaction executes the (decrypted) TransactionInputs, the validator persists the derived plaintextaccount_delta, input_notes, output_notes — into the validated_transactions table (bin/validator/src/db/schema.rs, insert_transaction in bin/validator/src/db/mod.rs). Instead, the durable record should be the original ciphertext (SealedMessage) that arrived over the wire, not data derived from decrypting and executing it.

Implementation tasks

Proto (proto/proto/)

  • Add GetTransactionEncryptionKey rpc + request/response messages to internal/validator.proto (response: X25519 public key bytes + this validator's ECDSA signature over those bytes + scheme identifier).
  • Add the same rpc (forwarding) to rpc.proto.

Validator (bin/validator/)

  • signers/mod.rs: add an encryption-key counterpart to ValidatorSigner (e.g. ValidatorEncryptor wrapping UnsealingKey/SealingKey). Each validator process loads the same shared secret (sourced from Secrets Manager) via its own instance of this type.
  • commands/mod.rs / commands/start.rs: add CLI flags/env vars for the shared encryption key, threaded into ValidatorService::new. Document clearly (help text/docs) that this value must match across all validators in the set, unlike the per-validator signing key flags.
  • server/validator_service/mod.rs: hold the encryptor on ValidatorService; add a new get_transaction_encryption_key handler module returning the shared public key plus a signature from this validator's own signing key.
  • server/validator_service/submit_proven_transaction.rs (decode): unseal transaction_inputs via the encryptor before TransactionInputs::read_from_bytes. Works identically regardless of which validator instance receives the submission, since the secret is shared.
  • tx_validation/data_store.rs: no change expected — it already only consumes the decoded TransactionInputs.
  • db/schema.rs / db/models.rs / db/mod.rs (insert_transaction): change the validated_transactions table to persist the original ciphertext (SealedMessage bytes) received on submission instead of the derived plaintext account_delta/input_notes/output_notes.

RPC (crates/rpc/, crates/proto/)

  • crates/proto/src/clients/mod.rs: extend ValidatorClient with a method to call the validator's new endpoint (mirrors the existing submit_batch pattern used in crates/rpc/src/server/api/submit_proven_tx_batch.rs).
  • crates/rpc/src/server/api/: add get_transaction_encryption_key.rs implementing the RPC-side handler, forwarding to whichever validator the RPC is connected to and passing the response through unchanged (pure proxy, same shape as other forwarded calls).

Docs

  • Update docs/external/src/rpc/public-api.md (and docs/internal/src/rpc.md if it documents validator internals) with the new endpoint, including a note that the returned encryption key is shared across the validator set while the attesting signature is validator-specific.

Open questions to resolve during implementation

  • Key rotation policy/cadence for the shared encryption key — since it's not chain-committed, how do we keep all validators in sync if it's rotated, and how does a client detect/handle a rotation mid-flight?
  • Whether the derived plaintext fields currently stored in validated_transactions (account_delta, input_notes, output_notes) are needed in any form going forward, or can be fully replaced by the stored ciphertext blob.

Acceptance criteria

  • Every validator in the set exposes a working GetTransactionEncryptionKey endpoint, each returning the same shared X25519 public key attested by its own signing key.
  • RPC forwards the request to whichever validator it's connected to, unchanged.
  • Any validator in the set can decrypt a SealedMessage-wrapped TransactionInputs payload and validate the transaction as before, regardless of which validator originally issued the key to the client.
  • Validated transactions are persisted with the original ciphertext (SealedMessage bytes), not the derived plaintext account_delta/input_notes/output_notes.
  • Existing signing/genesis flows are unaffected (no block header format changes).

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions