Skip to content

Protect validated transaction inputs with threshold encryption - #2393

Merged
huitseeker merged 26 commits into
issue-2385-store-validated-sealed-inputsfrom
node-phase2-ehtdh1
Jul 29, 2026
Merged

Protect validated transaction inputs with threshold encryption#2393
huitseeker merged 26 commits into
issue-2385-store-validated-sealed-inputsfrom
node-phase2-ehtdh1

Conversation

@huitseeker

@huitseeker huitseeker commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

After validation, the validator encrypts TransactionInputs under a threshold key. Each record uses a fresh content key. The validator stores the encrypted record and the validated transaction in one database write.

Operators provide the shared setup and one secret share per validator. The new storage schema indexes records by record id, transaction id, key epoch, setup context, and block height.

A local admin command issues a decryption share for one stored record. Operators can combine enough valid shares for the same record to recover its inputs. Shares for another record, context, or key epoch are rejected. The command has no gRPC endpoint and requires direct access to the validator files.

This requires #2389, #2390, and the first Golden crate release. The Golden dependencies use local paths until that release is available. CI failures therefore expected

Closes #2374

Changelog

[[entry]]
scope = "validator"
impact = "added"
description = "Validators can store validated private inputs under a Golden threshold key and issue local decryption shares for those records."

[[entry]]
scope = "validator"
impact = "migration"
description = "Validator databases must apply the private record migration before Golden storage is enabled."

@huitseeker
huitseeker marked this pull request as ready for review July 28, 2026 23:35
Comment thread bin/validator/src/db/migrations/003_private_records.sql Outdated
Comment thread bin/validator/src/db/migrations/003_private_records.sql Outdated
Comment on lines +17 to +28
CREATE TABLE private_records (
record_id BLOB NOT NULL,
transaction_id BLOB NOT NULL,
chain_id BLOB NOT NULL,
key_epoch BLOB NOT NULL,
setup_context_id BLOB NOT NULL,
schema_version BIGINT NOT NULL,
block_num BIGINT,
cipher_id BIGINT NOT NULL,
cipher_nonce BLOB NOT NULL,
encrypted_record BLOB NOT NULL,
wrapped_content_key BLOB NOT NULL,

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.

Would be good to add comments about what these fields are. Also, a few questions:

  • Why do we need record_id? Isn't transaction_id a primary key here?
  • What is chain_id? The validator only works on a single chain - wouldn't this be the same for all records?
  • What is setup_context_id? Would it be the same for all records? Or would it be the same for a given key_epoch?
  • wrapped_content_key is the encrypted key that can decrypt a given encrypted_record? If so, I wonder if we should rename it into something like encrypted_content_key or maybe encrypted_record_key.
  • What is the difference between schema_version and chiper_id?
  • It may be good to store the key share of the validator for a given transaction in the transaction record. This way, the share would be inserted at the same time when the transaction record is inserted, and it wouldn't need to be generated later. Or is doing this computationally intensive?

@huitseeker huitseeker Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Simplified. The transaction ID is also the record identity (edit, this is not going to work), and block height is gone. chain_id is the genesis commitment, kept so exported forensic data identifies its network (e.g. this is where we would differentiate a testnet, a mainnet, a private deployment). setup_context_id identifies the Golden DKG setup needed to combine shares. wrapped_content_key is now encrypted_record_key. One format_version fixes both the context and cipher.

Shares remain generated by the local CLI, which avoids temporary database state that we would later remove. Not to mention the decryption shares are keyed to a specific ciphertext.

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.

Shares remain generated by the local CLI, which avoids temporary database state that we would later remove. Not to mention the decryption shares are keyed to a specific ciphertext.

Why not store the decryption shares in the table together with their corresponding transaction records? The mapping is 1-to-1 (i.e., each transaction gets a unique share from a validator) - right? This would avoid the need to generate shares via local CLI.

@huitseeker huitseeker Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because the ciphertexts are distinct. A validator does not know which ciphertext it might have to generate a share for unless it happens to be its own. See commands::tests::two_validators_issue_shares_for_third_validator_record and private_record::tests::independent_writers_with_same_inputs_produce_distinct_ciphertexts

This would avoid the need to generate shares via local CLI.

Why though? Grabbing this via local CLI is simpler than introspecting inside the DB

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.

Ah - I see. So, each validator has a different ciphertext for the same transaction, and depending on which of these ciphertexts is being decrypted, the shares would be different - right? For some reason I assumed that ciphertext for the same transaction ends up being the same for all validators.

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.

Thinking more about it - this does imply that a validator cannot generate a share for a given transaction w/o knowing which validator wants to decrypt it - right? If so, the interaction would be:

  1. Validator A wants to decrypt some transaction. They send transaction info (including their cyphertext) to validator B.
  2. Validator B uses this info and their own keys to generate their own key share and sends the key share to validator A.
    a. Somehow B needs to make sure that the chipertext that A sent is for the transaction they claim?
  3. A combines the share from B with their own share, and decrypts the transaction.
    a. If I understood correctly, the same shares cannot be used by B to decrypt its ciphertext, right?

Comment thread bin/validator/src/commands/issue_private_record_share.rs Outdated
Comment thread docker-compose.yml Outdated
Comment thread CHANGELOG.md Outdated
Comment thread bin/validator/src/server/validator_service/mod.rs Outdated
@huitseeker
huitseeker force-pushed the node-phase2-ehtdh1 branch from ab7a179 to 6219bdd Compare July 29, 2026 16:58
Comment on lines +4 to +5
-- Signing public key of the validator that produced this record.
validator_id BLOB NOT NULL,

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.

nit: if this stores the public key, shouldn't we name it validator_key (or validator_pub_key)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The signing public key is the validator’s stable ID in PrivateRecordId. I would keep validator_id so the database, record bundle, and CLI use the same term, and clarify this in the SQL 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.

The signing public key is the validator’s stable ID in PrivateRecordId.

This is true until we let validators rotate their keys - right?

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.

Just fyi these are node's docs that get published to the miden docs website. Unclear if this belongs in there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The required Golden startup settings belong in the operator guide because the validator will not start without them. The temporary demo commands do not. I’ll remove the demo section and leave those details in CLI help and local demo notes.

Comment thread bin/validator/src/commands/export_private_record.rs Outdated
Comment thread bin/validator/src/commands/export_private_record.rs
@huitseeker
huitseeker merged commit b4ece8c into next Jul 29, 2026
29 checks passed
@huitseeker
huitseeker deleted the node-phase2-ehtdh1 branch July 29, 2026 18:29
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.

Add threshold encryption for stored private transaction data

5 participants