Skip to content

feat: add input encryption for validators#2342

Merged
bobbinth merged 10 commits into
nextfrom
jmunoz-tx-input-encryption-p1
Jul 22, 2026
Merged

feat: add input encryption for validators#2342
bobbinth merged 10 commits into
nextfrom
jmunoz-tx-input-encryption-p1

Conversation

@juan518munoz

@juan518munoz juan518munoz commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Closes #2319

Summary

Transaction inputs submitted to the network are currently visible to anyone in plaintext. First step to solve this is to provide validators with a shared encryption keypair, and make clients discover and use that key. Actual encryption of the submission path lands in a follow-up PR.

How:

  • Every validator is configured with the same shared transaction encryption key (Ed25519). It is passed via --encryption-key.hex / MIDEN_VALIDATOR_ENCRYPTION_KEY. If ommited, an insecure development default is used (that logs a loud warning at startup).
  • A new ValidatorEncryptor wraps the key and produces the attestation. Each validator signs a Poseidon2 commitment over domain_tag || scheme || key_id || genesis_commitment || public_key. The domain tag separates attestations from block header signatures, and the genesis commitment prevents cross-network replay.
  • There's a new GetTransactionEncryptionKey endpoint on the validator's internal API returns the public key, IES scheme id, key id, and attesting signature (TransactionEncryptionKey proto message). The public RPC exposes the same endpoint by forwarding to a validator and passing the response through unchanged, so clients can verify the attestation against a chain-recognized validator key without trusting the RPC.
  • ValidatorSigner::sign was generalized to sign_commitment so both block headers and key attestations go through the same local/KMS signing path.

Changelog

[[entry]]
scope       = "rpc"
impact      = "added"
description = "Added the `GetTransactionEncryptionKey` endpoint, returning the shared transaction encryption public key attested by a validator's signing key."

[[entry]]
scope       = "validator"
impact      = "added"
description = "Validators are now provisioned with a shared transaction encryption key via `--encryption-key.hex` / `MIDEN_VALIDATOR_ENCRYPTION_KEY` and serve it through the new `GetTransactionEncryptionKey` endpoint."
[[entry]]
scope       = "validator"
impact      = "breaking"
description = "Renamed the validator signing key options: `--key.hex` / `MIDEN_VALIDATOR_KEY` is now `--signing-key.hex` / `MIDEN_VALIDATOR_SIGNING_KEY`, and `--key.kms-id` / `MIDEN_VALIDATOR_KEY_KMS_ID` is now `--signing-key.kms-id` / `MIDEN_VALIDATOR_SIGNING_KEY_KMS_ID`."

@sergerad
sergerad self-requested a review July 16, 2026 08:27
@SantiagoPittella
SantiagoPittella self-requested a review July 16, 2026 14:42
Comment thread bin/validator/src/server/validator_service/get_transaction_encryption_key.rs Outdated
Comment thread bin/validator/src/commands/mod.rs
Comment thread bin/validator/src/signers/mod.rs Outdated
Comment thread bin/validator/src/server/validator_service/tests.rs Outdated
Comment thread CHANGELOG.md Outdated
@juan518munoz
juan518munoz marked this pull request as ready for review July 17, 2026 12:13
@bobbinth
bobbinth self-requested a review July 17, 2026 14:50
@bobbinth

Copy link
Copy Markdown
Contributor

I'm planning to review this today.

@huitseeker huitseeker left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good for a Phase 1, thanks!

One key question is whether future validator code may assume that the decryption secret is available as bytes: a TEE stack may only expose operations. I made suggestions inline in the direction of providing a path that doesn't involve more implementation but is a bit more future-proof.

Another item of note is the public key response should not assume a fixed public key length or a fixed key id length. Phase 1 uses one X25519 key. Later we may use a DStack backed key, an HPKE KEM public key or a key whose identity is tied to an epoch. The key id may need to name a bunch of things. The wire type should therefore be opaque bytes, even if the Phase 1 implementation derives those bytes from the X25519 public key.

Finally, the current validator signature is useful, in that it proves that a chain recognized validator vouched for the key response. It does not prove that the key lives in a specific TEE image, which will come later. The response should have a place for that evidence, with field numbers reserved now to fit the future plan.

Comment thread docs/external/src/rpc/public-api.md Outdated
Comment thread bin/validator/src/signers/mod.rs Outdated
Comment thread bin/validator/src/server/validator_service/mod.rs Outdated
Comment thread bin/validator/src/server/validator_service/mod.rs
Comment thread crates/rpc/src/server/api/get_transaction_encryption_key.rs
Comment thread bin/validator/src/server/validator_service/get_transaction_encryption_key.rs Outdated
Comment thread bin/validator/src/signers/mod.rs Outdated
Comment thread proto/proto/types/transaction.proto Outdated
Comment thread bin/validator/src/signers/mod.rs
Comment thread bin/validator/src/signers/mod.rs Outdated
@huitseeker
huitseeker requested a review from adr1anh July 17, 2026 20:41
Comment thread bin/validator/src/commands/start.rs Outdated
Comment thread docs/internal/src/validator.md Outdated

@bobbinth bobbinth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Thank you! I left some comments inline. Overall, I think this lays a good foundation, but there are two things that we'll need to add:

  1. Make provisions for key rotations. Specifically, if key rotation is upcoming, the endpoint should send the next encryption key - this way, the clients would have the next key ahead of time.
  2. In the RPC, instead of forwarding the request to the validator, we should be caching the encryption key info, and periodically refresh it.

Both of these could be done in follow-up PRs though, the first point could probably be done here as well (I wouldn't try to do the second point in this PR because it would be quite a bit of extra work that has no impact on the user-facing interface).

Comment thread CHANGELOG.md
Comment thread proto/proto/rpc.proto Outdated
Comment thread proto/proto/types/transaction.proto Outdated
Comment thread proto/proto/types/transaction.proto Outdated
Comment thread proto/proto/types/transaction.proto
Comment thread docs/external/src/rpc/public-api.md Outdated
Comment thread bin/validator/src/signers/mod.rs
Comment thread bin/validator/src/commands/mod.rs Outdated
Comment on lines +139 to +145
#[arg(
long = "encryption-key.hex",
env = ENV_ENCRYPTION_KEY,
value_name = "VALIDATOR_ENCRYPTION_KEY",
default_value = INSECURE_ENCRYPTION_KEY_HEX
)]
encryption_key: String,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but should we also provide an option to get the key from KMS?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We just need to ensure KMS supports this key type, otherwise we cannot decrypt.

Comment thread bin/validator/src/signers/mod.rs
Comment thread crates/rpc/src/server/api/get_transaction_encryption_key.rs
Comment thread bin/validator/src/signers/mod.rs

@sergerad sergerad left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM just wondering about whether its worth adding a type for associated_data in the trait. Also the decryptor->decrypter rename 🙏

@huitseeker huitseeker left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with the known follow-ups discussed in thread.

Comment thread bin/validator/src/commands/mod.rs

@bobbinth bobbinth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Thank you! I left a couple of comments inline - but these can be addresses in follow-ups (if we decide to address them).

// The public key is shared across the whole validator set, while each attesting signature is
// specific to one validator. Verifying an attestation against a chain-recognized validator
// signing key proves the encryption key was vouched for by a legitimate validator.
message TransactionEncryptionKey {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have maybe arranged things a bit differently:

// Same as now
message ValidatorKeyAttestation {
  bytes validator_public_key = 1;
  bytes signature = 2;
}

message TransactionEncryptionKey {
  IesScheme scheme = 1;
  bytes key_id = 2;
  bytes public_key = 3;
  repeated ValidatorKeyAttestation attestations = 4;
}

message NextTransactionEncryptionKey {
    TransactionEncryptionKey key = 1;
    fixed32 rotation_block_num = 2;
}

message TransactionEncryptionKeyResponse {
    TransactionEncryptionKey current_key = 1;
    optional NextTransactionEncryptionKey = 2;
}

This way, the signature doesn't need to cover next_key_transcript.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I'll apply it in a follow-up PR that adds rotation to the validator keys.

Comment on lines 79 to +82
signer: ValidatorSigner,
/// Decrypter for transaction inputs sealed against the shared encryption key.
#[expect(dead_code, reason = "used by the submit path in a follow-up PR")]
decrypter: Arc<dyn TransactionInputDecrypter>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question (not a change request): why did we decide to go with a dyn trait for the decrypter but with an enum for the signer? Is there something different between these two such that we can't use the same approach for both?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We went with dyn trait follows @huitseeker suggestion that we should expose a minimal api for the decrypter to better acommodate later implementations.

Left the signer as an enum, as it was already like that from before this PR. We can modify it to better align the two types.

@Mirko-von-Leipzig Mirko-von-Leipzig left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nits.

Comment thread CHANGELOG.md
Comment thread docs/external/src/rpc/public-api.md
Comment thread bin/validator/src/commands/bootstrap.rs
Comment thread crates/rpc/src/server/api/get_transaction_encryption_key.rs
Comment on lines +139 to +145
#[arg(
long = "encryption-key.hex",
env = ENV_ENCRYPTION_KEY,
value_name = "VALIDATOR_ENCRYPTION_KEY",
default_value = INSECURE_ENCRYPTION_KEY_HEX
)]
encryption_key: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We just need to ensure KMS supports this key type, otherwise we cannot decrypt.

Comment on lines +73 to +75
/// Unlike the signing key, the key material behind an implementation must be identical across
/// every validator in the set. This lets any validator unseal an encrypted submission, regardless
/// of which validator attested the encryption key to the client.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@huitseeker for my edification - does this hold true under multisig conditions?

A single global public encryption key, but decryption requires N of M different keys? Or does encryption require M public keys?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decryption also currently requires just one key - but this is just the first step. After this PR, there are two more steps:

  1. The validators will perform threshold re-encryption. That is, they will decrypt the private inputs with the single decryption key (same as now), but then will re-encrypt it with threshold encryption so that decryption requires collaboration with others.
  2. The decryption and re-encryption moves into a TEE.

The end result would be that a validator never sees plaintext data but gets a threshold-encrypted version of it from the TEE. After this, a subset of validators meeting the required threshold requirement can decrypt a given transaction.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iiuc this means there will be a single TEE shared amongst all validators? And that the user-facing encryption key will be the TEE's pub key?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There would be multiple TEEs (one per validator) but the attestations for the encryption key will probably be a bit more complicated so that the user can be convinced that the encryption key comes from the validators' TEEs (I don't have the exact scheme in mind yet).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay yeah I didn't understand how one could attest that, nor how one could share such a key amongst TEEs without exposing the key to the TEE operator.

Comment thread bin/validator/src/signers/mod.rs Outdated
@juan518munoz

Copy link
Copy Markdown
Collaborator Author

We just need to ensure KMS supports this key type, otherwise we cannot decrypt.

Pushed COMMIT that addressed this. Added a new argument option --encryption-key.kms-ciphertext / MIDEN_VALIDATOR_ENCRYPTION_KEY_KMS_CIPHERTEXT, which takes a base64 encoded KMS ciphertext blob. At startup the validator calls kms:Decrypt to recover the shared secret, then feeds it to the existing local decrypter.

24aa5f9

@bobbinth

Copy link
Copy Markdown
Contributor

Should we merge this now? Or does anyone want to talk one last look?

Comment thread bin/validator/src/signers/mod.rs
@Mirko-von-Leipzig

Copy link
Copy Markdown
Collaborator

Should we merge this now? Or does anyone want to talk one last look?

I'm okay with merging; I'm sure other things will fall out in the follow-up PRs.

@bobbinth bobbinth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All looks good! Thank you!

@bobbinth
bobbinth merged commit 732a4db into next Jul 22, 2026
25 checks passed
@bobbinth
bobbinth deleted the jmunoz-tx-input-encryption-p1 branch July 22, 2026 05:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

6 participants