Extract private transaction processing into a service
- Dominant language
- Rust
- Stars
- 104
- Forks
- 138
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 56
Description
## Context
[Private validator mode](https://github.com/0xMiden/node/issues/2110) will place private transaction checks and threshold re-encryption inside a trusted execution environment, or TEE. [Phase 1](https://github.com/0xMiden/node/issues/2319) added encrypted submission. [Phase 2](https://github.com/0xMiden/node/issues/2374) added threshold encryption for stored private inputs. The next milestone should define the service boundary that a TEE will later implement.
Today, the sequencer and preauthenticated full node send each sealed submission to every validator after checking its public proof. The single transaction path is in [`SubmitProvenTx::handle`](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/crates/rpc/src/server/api/submit_proven_tx.rs#L42-L154), and the fanout is in [`submit_tx_to_validators`](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/crates/rpc/src/server/api.rs#L38-L57).
Each validator decrypts the same submission, runs the same private checks, creates a threshold record, and stores it in [`SubmitProvenTransaction::handle`](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/bin/validator/src/server/validator_service/submit_proven_transaction.rs#L28-L92). [`PrivateRecordSealer::seal`](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/bin/validator/src/private_record.rs#L178-L243) uses fresh randomness. The validators therefore store different ciphertexts for the same transaction.
This flow sends untrusted client data to every validator and repeats private work that a TEE result will later prove was done. It also creates several ciphertexts where the target design needs one ciphertext that all validators store.
## Proposed flow
Add a private transaction processor as a separate internal service. The first version is a mock TEE. It gives no hardware security, but it fixes the service boundary and transaction flow before TEE integration.
```text
client -> node -> private processor -> node -> all validators -> node responds
```
Add `private-processor` to the [`miden-validator` commands](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/bin/validator/src/commands/mod.rs#L89-L255). It must listen on an internal port. The node sends one proven transaction and its sealed inputs through a new `ProcessPrivateTransaction` RPC. The service decrypts the inputs and calls [`validate_transaction`](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/bin/validator/src/tx_validation/mod.rs#L39-L90). It then creates one threshold-encrypted record and returns a `SignedPrivateRecord`.
Define a versioned commitment over the transaction ID, a hash of the proven transaction bytes, and a hash of the canonical private record bytes. [`StoredPrivateRecord::write_into`](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/bin/validator/src/private_record.rs#L473-L498) defines the record encoding. Sign the commitment with the current secp256k1 ECDSA scheme exposed by [`ValidatorSigner`](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/bin/validator/src/signers/mod.rs#L18-L65). There is only one signed result and one threshold-encrypted ciphertext, so this change does not need a new signature scheme.
Replace the sealed input fanout with one processor call followed by a fanout of the signed record. Add `StoreValidatedPrivateRecord` to the existing [validator API](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/proto/proto/internal/validator.proto#L10-L37).
Each validator must verify the signature with its configured processor public key. It must also verify the transaction binding and decode the record. The decoded record must use the expected chain ID, Golden key epoch, and setup context. The validator then stores the exact record and marks the transaction as validated. It must not decrypt the client submission, run the private checks, or create another threshold-encrypted record.
Use the same processor and storage path for single submissions and for every transaction in a batch. The current batch path also sends sealed inputs to every validator in [`SubmitProvenTxBatch::handle`](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/crates/rpc/src/server/api/submit_proven_tx_batch.rs#L108-L166).
## Storage rules
An exact retry must succeed. A different record for the same transaction ID must fail. The current insert uses [`ON CONFLICT DO NOTHING`](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/bin/validator/src/db/sql/insert_transaction.sql#L1-L13), which hides that conflict. Load the stored record and compare its canonical bytes before accepting a duplicate.
The node must wait for every validator to store the record before it accepts the submission. This keeps the current block path unchanged. Block production still requires every validator signature in [`build_and_validate_block`](https://github.com/0xMiden/node/blob/1951e7894a021c333d6a449757cc6b2140687e49/crates/block-producer/src/block_builder/mod.rs#L304-L328).
## Rollout
The first release runs the processor as a normal internal service. The node and validators trust its signing key through configuration. Its signature proves only that this configured service produced the record. Do not call this signature a TEE attestation. Remove the old validator processing route in the same change. The processor is required for private submissions.
A later release runs this service inside a TEE. The submission key and processor signing key stay inside the TEE. Private checks and threshold sealing also run there. The node and validators then verify TEE evidence that binds the processor key to the approved code. The core request, signed record, validator storage RPC, and record fanout stay the same. The response adds the TEE evidence needed to verify the processor key.
## Done when
* The node calls the processor once for each transaction.
* The node never sends sealed client inputs to validators.
* Every validator stores the same canonical private record bytes.
* Validators reject a bad signature, changed transaction, wrong chain, wrong Golden setup, or conflicting record.
* An exact retry succeeds without changing the stored record.
* Single and batch submissions use the new flow.
* Two valid threshold shares can recover the inputs from the stored record.
* Existing block production behavior and its requirement for every validator remain unchanged.
## Out of scope
This issue does not add a TEE, TEE evidence verification, storage quorums, partial validator participation, submission key changes, or Golden algorithm changes.
Contributor guide
Assessment
This issue has not been assessed yet.