Skip to content

feat: Validator block signatures#1426

Merged
sergerad merged 34 commits intonextfrom
sergerad-validator-sig
Dec 15, 2025
Merged

feat: Validator block signatures#1426
sergerad merged 34 commits intonextfrom
sergerad-validator-sig

Conversation

@sergerad
Copy link
Collaborator

@sergerad sergerad commented Dec 3, 2025

Context

The Validator provides an endpoint for signing blocks. This endpoint has so far been stubbed out and needs to be implemented.

The Validator and bootstrap commands need to be configured with ECDSA signers to sign blocks. For now we will use insecure, locally stored secret keys for development and testing while providing an abstraction to facilitate a secure backend for signing in the future.

Relates to #1316.

Changes

  • Update bootstrap command and genesis state to allow genesis block to be signed by validator key.
    • Genesis config TOML is no longer optional (must provide key that is used in subsequent start command).
  • Add signer configuration to Validator.
  • Add block signing logic to Validator.
  • Update Validator SignBlock RPC endpoint to return ECDSA Signature.
  • Add validator key field to block header proto.
  • Refactor Dockerfile to not bootstrap at build time.

@sergerad sergerad force-pushed the sergerad-validator-sig branch from fefe7e4 to 126d992 Compare December 4, 2025 02:43
@sergerad sergerad force-pushed the sergerad-validator-sig branch from 5064109 to 659e14e Compare December 9, 2025 21:44
@sergerad sergerad marked this pull request as ready for review December 11, 2025 03:06
Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you! Not a full review, but I left some comments/questions inline.

Comment on lines 32 to 33
[signer]
Insecure = "/tmp/insecure_2"
Copy link
Contributor

Choose a reason for hiding this comment

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

This is for specifying validator private key, right? If so, I'd change the section to validator and allow specifying either private key or public key - e.g.,

[validator]
private_key = "hex-encoded-private-key"

# or

[validator]
public_key = "hex-encoded-public-key"

Then, for dev/test scenarios, the users would be able to specify the private key, and for production scenarios, we could set the public key. Setting both at the same time would be an error.

Also, we should document this (I think we have a section that describes how the genesis file is to be used.

Lastly, I'd move this section up to be before native_faucet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Providing the public key alone would be insufficient for performing bootstrap - an entire signer needs to be provided in order to sign the genesis block.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yes - good point! I think we can do something like:

[validator]
private_key = "hex-encoded-private-key"

# or, for production settings:

[validator]
public_key = "hex-encoded-public-key"
block_signature = "hex-encoded-block-signature"

But we can start with just the private_key option for now, and add the production-specific option later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This approach also means you can no longer do a default bootstrap - you need to wire together the secret key from toml for bootstrap command and then into the subsequent start command via CLI flags

Copy link
Contributor

Choose a reason for hiding this comment

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

I see - so, we can use the private key file for both - genesis block construction and for node startup, right? In this case, probably fine to keep it as a file.

Another option is to output the private key file as a part of genesis construction procedure (similar to how we output the account files).

Copy link
Collaborator

@Mirko-von-Leipzig Mirko-von-Leipzig Dec 12, 2025

Choose a reason for hiding this comment

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

I think we've gotten lost by focusing on the toml config file. What about keeping the current definition of the toml file - as an (optional) way to configure the contents of the genesis block.

We then take in the private key via CLI separately. I would prefer this be in-line aka no key file - the caller can always use a file and echo it in via an arg. imo this is maximally flexible and alleviates us having to manage a file since the key can just be passed in via env var.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yea that makes sense. Will update

long = "validator-secret-key-filepath",
value_name = "VALIDATOR_SECRET_KEY_FILEPATH"
)]
validator_secret_key_filepath: Option<PathBuf>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Related to the previous comment, I'm not sure this needs to be a filepath. A secret key is just a 32-byte string that could be hex encoded. But maybe keeping it as a file simplifies things?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Given that this is only intended for devnet/local testing I think taking it inline is good enough.

What we can do is give it a more specific name e.g.

validator.development.secret_key

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Have done this. Note that genesis TOML is no longer optional because you need to use the bootstrap key in subsequent node start command.

Comment on lines +130 to +133
pub fn into_state<S>(
self,
signer: S,
) -> Result<(GenesisState<S>, AccountSecrets), GenesisConfigError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to parametrize this with the signer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

GenesisState<S>::into_block() performs signing

Copy link
Collaborator

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

Choose a reason for hiding this comment

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

Thanks, looks good in general. The main contention is whether we truly need to be generic over the signer and whether we need a secret key file at all.

Comment on lines +54 to +55
pub fn bootstrap<S: BlockSigner>(
genesis: GenesisState<S>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we not have a concrete implementation for this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How would we support secure + insecure signers without this abstraction?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we flash out different scenarios/steps we are trying to cover? For example, we have the dev/testing flow, this flow requires us to have the private key in 2 places:

  • When generating the genesis block. Here we need the private key for two reasons:
    • To generate the validator's public key that goes into the block header.
    • To sign the resulting block.
  • When starting up the bundled node. Here, we pass the private key to the validator so that it can sign the blocks produced by the block producer.

The production flow will probably be different from this:

  • The genesis config file would be pre-populated with the public key of the validator and the signature over the block. Or maybe, we'd have an external component that generates the genesis block out of band (from the genesis config file), and then, the node bootstraps from the pre-built genesis block.
  • When we start up the validator component, we'll need to provide the private key to it. There are probably a couple of options to do it:
    • The private key could be provided via an environment variable (probably not very secure).
    • There could be a separate signing services (though, the benefit of such a services is somewhat questionable because it would be doing just "dumb signing" and so, whoever controls the validator can get it to sign anything).
    • Are there other options?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When we start up the validator component, we'll need to provide the private key to it. There are probably a couple of options to do it:

  • The private key could be provided via an environment variable (probably not very secure).
  • There could be a separate signing services (though, the benefit of such a services is somewhat questionable because it would be doing just "dumb signing" and so, whoever controls the validator can get it to sign anything).

For the second point, the main thing is that the corresponding key would be retrieved securely from some remote backend.

The genesis config file would be pre-populated with the public key of the validator and the signature over the block. Or maybe, we'd have an external component that generates the genesis block out of band (from the genesis config file), and then, the node bootstraps from the pre-built genesis block.

The out-of-band process should be as secure as the validator's process of signing blocks. Which would be an argument for using the same/similar approach that the validator uses above for creating the genesis block.

Having said that, I'm happy to remove the Signer trait usage from the bootstrap stack. I don't think we should remove it from the validator/bundled stack, however.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You can remove it either by using an enum to represent signer options, or taking in a dyn T. At some point in the node you'll need to pass in a concrete implementation so we will know what that is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

LMK if we want to go with enum or dyn T. If we go with the former then I will update base to remove the pub trait BlockSigner { altogether.

Copy link
Contributor

Choose a reason for hiding this comment

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

If it doesn't introduce more complexity, I'd probably go with dyn BockSinger approach. But keeping as is is probably fine too.

long = "validator-secret-key-filepath",
value_name = "VALIDATOR_SECRET_KEY_FILEPATH"
)]
validator_secret_key_filepath: Option<PathBuf>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given that this is only intended for devnet/local testing I think taking it inline is good enough.

What we can do is give it a more specific name e.g.

validator.development.secret_key

@sergerad sergerad force-pushed the sergerad-validator-sig branch from f0db6e8 to 5c481e6 Compare December 11, 2025 21:07
Copy link
Contributor

@bobbinth bobbinth left a comment

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 - once these are addressed, we can merge.

One set of comments is about making insecure_secret_key a required parameter. But I wonder if instead, if the key is not provided, we can assume some default value (e.g., 32 bytes of alternating ones and zeros). This way, in dev/test environments, users won't need to bother about this key at all.

pub fee_parameters: FeeParameters,
pub version: u32,
pub timestamp: u32,
pub block_signer: S,
Copy link
Contributor

Choose a reason for hiding this comment

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

It is still not clear to me if the genesis state needs to be generic over BlocSigner - but I guess it doesn't create too much complexity.

Comment on lines +54 to +55
pub fn bootstrap<S: BlockSigner>(
genesis: GenesisState<S>,
Copy link
Contributor

Choose a reason for hiding this comment

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

If it doesn't introduce more complexity, I'd probably go with dyn BockSinger approach. But keeping as is is probably fine too.

@sergerad sergerad merged commit 53660be into next Dec 15, 2025
6 checks passed
@sergerad sergerad deleted the sergerad-validator-sig branch December 15, 2025 19:50
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.

3 participants