Skip to content

feat: add private key file configuration for sequencer signing #161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,11 @@ To run a sequencer node you should build the binary in release mode using the in
Then, you can run the sequencer node with the following command:

```sh
./target/release/rollup-node node --chain dev -d --scroll-sequencer-enabled --http --http.api admin,debug,eth,net,trace,txpool,web3,rpc,reth,ots,flashbots,miner,mev
./target/release/rollup-node node --chain dev --sequencer.enabled --signer.key-file /path/to/your/private.key --http --http.api admin,debug,eth,net,trace,txpool,web3,rpc,reth,ots,flashbots,miner,mev
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should still include the -d argument, which specifies that we should disable discovery of other nodes. What do you think?

```

**Note**: When running a sequencer, a signer key file is required unless the `--test` flag is specified. Use the `--signer.key-file` option to specify the path to your private key file. Keep your private key file secure and never commit it to version control.

This will start a dev node in sequencer mode with all rpc apis enabled. You can adjust the `--http.api` flag to include or exclude specific APIs as needed.

The chain will be configured with a genesis that funds 20 addresses derived from the mnemonic:
Expand All @@ -147,6 +149,8 @@ A list of sequencer specific configuration options can be seen below:
The max L1 messages per block for the sequencer [default: 4]
--fee-recipient <FEE_RECIPIENT>
The fee recipient for the sequencer [default: 0x5300000000000000000000000000000000000005]
--signer.key-file <FILE_PATH>
Path to the signer's private key file (required when sequencer is enabled)
```
## Contributing
Expand Down
1 change: 1 addition & 0 deletions crates/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ reth-tracing.workspace = true
rollup-node = { workspace = true, features = ["test-utils"] }
scroll-alloy-rpc-types-engine.workspace = true
serde_json = { version = "1.0.94", default-features = false, features = ["alloc"] }
tempfile = "3.0"

[features]
test-utils = [
Expand Down
73 changes: 71 additions & 2 deletions crates/node/src/add_ons/rollup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use scroll_engine::{EngineDriver, ForkchoiceState};
use scroll_migration::traits::ScrollMigrator;
use scroll_network::ScrollNetworkManager;
use scroll_wire::{ScrollWireConfig, ScrollWireProtocolHandler};
use std::{sync::Arc, time::Duration};
use std::{fs, sync::Arc, time::Duration};
use tokio::sync::mpsc::Sender;

/// Implementing the trait allows the type to return whether it is configured for dev chain.
Expand Down Expand Up @@ -70,6 +70,11 @@ impl RollupManagerAddOn {
<<N as FullNodeTypes>::Types as NodeTypes>::ChainSpec: ScrollHardforks + IsDevChain,
N::Network: NetworkProtocols + FullNetwork<Primitives = ScrollNetworkPrimitives>,
{
// Validate configuration before starting any components
self.config
.validate()
.map_err(|e| eyre::eyre!("Configuration validation failed: {}", e))?;

// Instantiate the network manager
let (scroll_wire_handler, events) =
ScrollWireProtocolHandler::new(ScrollWireConfig::new(true));
Expand Down Expand Up @@ -211,7 +216,20 @@ impl RollupManagerAddOn {
.then_some(ctx.node.network().eth_wire_block_listener().await?);

// Instantiate the signer
let signer = self.config.test.then_some(Signer::spawn(PrivateKeySigner::random()));
let signer = if self.config.test {
Some(Signer::spawn(PrivateKeySigner::random()))
} else if let Some(key_file_path) = &self.config.signer_args.key_file {
let key_bytes = fs::read(key_file_path).map_err(|e| {
eyre::eyre!("Failed to read signer key file {}: {}", key_file_path.display(), e)
})?;
Comment on lines +222 to +224
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of expecting the file to contain raw bytes should we expect the private key to be hex encoded? How is this managed in geth/reth? What do you think?


let private_key_signer = PrivateKeySigner::from_slice(&key_bytes)
.map_err(|e| eyre::eyre!("Failed to create signer from key file: {}", e))?;

Some(Signer::spawn(private_key_signer))
} else {
None
};

// Spawn the rollup node manager
let rnm = RollupNodeManager::new(
Expand All @@ -230,3 +248,54 @@ impl RollupManagerAddOn {
Ok((rnm, l1_notification_tx))
}
}

#[cfg(test)]
mod tests {
use alloy_signer_local::PrivateKeySigner;
use std::io::Write;
use tempfile::NamedTempFile;

#[test]
fn test_signer_initialization_with_valid_key_file() {
// Test valid key file
let mut temp_file = NamedTempFile::new().unwrap();
let private_key = [1u8; 32];
temp_file.write_all(&private_key).unwrap();
temp_file.flush().unwrap();

let key_bytes = std::fs::read(temp_file.path()).unwrap();
let result = PrivateKeySigner::from_slice(&key_bytes);

assert!(result.is_ok());
}

#[test]
fn test_signer_initialization_with_invalid_key_file() {
// Test invalid key file
let mut temp_file = NamedTempFile::new().unwrap();
let invalid_key = b"invalid key data";
temp_file.write_all(invalid_key).unwrap();
temp_file.flush().unwrap();

let key_bytes = std::fs::read(temp_file.path()).unwrap();
let result = PrivateKeySigner::from_slice(&key_bytes);

assert!(result.is_err());

let error = result.unwrap_err();
assert!(error.to_string().contains("signature error"));
}

#[test]
fn test_signer_initialization_with_nonexistent_file() {
// Test nonexistent file
let nonexistent_path = "/nonexistent/path/to/key";
let result = std::fs::read(nonexistent_path);

assert!(result.is_err());

let error = result.unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
assert!(error.to_string().contains("No such file or directory (os error 2)"));
}
}
Comment on lines +252 to +301
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should move these tests to the signer crate.

102 changes: 102 additions & 0 deletions crates/node/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ pub struct ScrollRollupNodeConfig {
/// The network arguments
#[command(flatten)]
pub network_args: NetworkArgs,
/// The signer arguments
#[command(flatten)]
pub signer_args: SignerArgs,
}

impl ScrollRollupNodeConfig {
/// Validate that signer key file is provided when sequencer is enabled
pub fn validate(&self) -> Result<(), String> {
if !self.test &&
self.sequencer_args.sequencer_enabled &&
self.signer_args.key_file.is_none()
{
return Err("Signer key file is required when sequencer is enabled".to_string());
}
Ok(())
}
}

/// The database arguments.
Expand Down Expand Up @@ -122,3 +138,89 @@ pub struct SequencerArgs {
)]
pub l1_message_inclusion_mode: L1MessageInclusionMode,
}

/// The arguments for the signer.
#[derive(Debug, Default, Clone, clap::Args)]
pub struct SignerArgs {
/// Path to the file containing the signer's private key
#[arg(
long = "signer.key-file",
value_name = "FILE_PATH",
help = "Path to the signer's private key file (required when sequencer is enabled)"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's migrate to hex-encoded key files and note that we expect hex encoding in help here.

)]
pub key_file: Option<PathBuf>,
}

#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;

#[test]
fn test_validate_sequencer_enabled_without_key_file_fails() {
let config = ScrollRollupNodeConfig {
test: false,
sequencer_args: SequencerArgs { sequencer_enabled: true, ..Default::default() },
signer_args: SignerArgs { key_file: None },
database_args: DatabaseArgs::default(),
engine_driver_args: EngineDriverArgs::default(),
l1_provider_args: L1ProviderArgs::default(),
beacon_provider_args: BeaconProviderArgs::default(),
network_args: NetworkArgs::default(),
};

let result = config.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Signer key file is required when sequencer is enabled"));
}

#[test]
fn test_validate_sequencer_enabled_with_key_file_succeeds() {
let config = ScrollRollupNodeConfig {
test: false,
sequencer_args: SequencerArgs { sequencer_enabled: true, ..Default::default() },
signer_args: SignerArgs { key_file: Some(PathBuf::from("/path/to/key")) },
database_args: DatabaseArgs::default(),
engine_driver_args: EngineDriverArgs::default(),
l1_provider_args: L1ProviderArgs::default(),
beacon_provider_args: BeaconProviderArgs::default(),
network_args: NetworkArgs::default(),
};

assert!(config.validate().is_ok());
}

#[test]
fn test_validate_test_mode_without_key_file_succeeds() {
let config = ScrollRollupNodeConfig {
test: true,
sequencer_args: SequencerArgs { sequencer_enabled: true, ..Default::default() },
signer_args: SignerArgs { key_file: None },
database_args: DatabaseArgs::default(),
engine_driver_args: EngineDriverArgs::default(),
l1_provider_args: L1ProviderArgs::default(),
beacon_provider_args: BeaconProviderArgs::default(),
network_args: NetworkArgs::default(),
};

assert!(config.validate().is_ok());
}

#[test]
fn test_validate_sequencer_disabled_without_key_file_succeeds() {
let config = ScrollRollupNodeConfig {
test: false,
sequencer_args: SequencerArgs { sequencer_enabled: false, ..Default::default() },
signer_args: SignerArgs { key_file: None },
database_args: DatabaseArgs::default(),
engine_driver_args: EngineDriverArgs::default(),
l1_provider_args: L1ProviderArgs::default(),
beacon_provider_args: BeaconProviderArgs::default(),
network_args: NetworkArgs::default(),
};

assert!(config.validate().is_ok());
}
}
2 changes: 2 additions & 0 deletions crates/node/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ pub fn default_test_scroll_rollup_node_config() -> ScrollRollupNodeConfig {
engine_driver_args: EngineDriverArgs { en_sync_trigger: 100 },
sequencer_args: SequencerArgs::default(),
beacon_provider_args: BeaconProviderArgs::default(),
signer_args: Default::default(),
}
}

Expand All @@ -162,5 +163,6 @@ pub fn default_sequencer_test_scroll_rollup_node_config() -> ScrollRollupNodeCon
l1_message_inclusion_mode: L1MessageInclusionMode::BlockDepth(0),
},
beacon_provider_args: BeaconProviderArgs::default(),
signer_args: Default::default(),
}
}
2 changes: 2 additions & 0 deletions crates/node/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ async fn can_bridge_l1_messages() -> eyre::Result<()> {
..SequencerArgs::default()
},
beacon_provider_args: BeaconProviderArgs::default(),
signer_args: Default::default(),
};
let (mut nodes, _tasks, _wallet) = setup_engine(node_args, 1, chain_spec, false).await?;
let node = nodes.pop().unwrap();
Expand Down Expand Up @@ -106,6 +107,7 @@ async fn can_sequence_and_gossip_blocks() {
..SequencerArgs::default()
},
beacon_provider_args: BeaconProviderArgs::default(),
signer_args: Default::default(),
};

let (nodes, _tasks, wallet) =
Expand Down
2 changes: 1 addition & 1 deletion docker-compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ services:
--log.stdout.format log-fmt -vvv --l1.url http://l1reth-rpc.sepolia.scroll.tech:8545 --beacon.url http://l1reth-cl.sepolia.scroll.tech:5052 --network.scroll-wire --network.bridge
--trusted-peers "enode://29cee709c400533ae038a875b9ca975c8abef9eade956dcf3585e940acd5c0ae916968f514bd37d1278775aad1b7db30f7032a70202a87fd7365bd8de3c9f5fc@44.242.39.33:30303,enode://ceb1636bac5cbb262e5ad5b2cd22014bdb35ffe7f58b3506970d337a63099481814a338dbcd15f2d28757151e3ecd40ba38b41350b793cd0d910ff0436654f8c@35.85.84.250:30303,enode://dd1ac5433c5c2b04ca3166f4cb726f8ff6d2da83dbc16d9b68b1ea83b7079b371eb16ef41c00441b6e85e32e33087f3b7753ea9e8b1e3f26d3e4df9208625e7f@54.148.111.168:30303"
elif [ "$${ENV:-}" = "mainnet" ]; then
exec rollup-node node --chain scroll --datadir=/l2reth --metrics=0.0.0.0:6060 --disable-discovery \
exec rollup-node node --chain scroll-mainnet --datadir=/l2reth --metrics=0.0.0.0:6060 --disable-discovery \
--http --http.addr=0.0.0.0 --http.port=8545 --http.corsdomain "*" --http.api admin,debug,eth,net,trace,txpool,web3,rpc,reth,ots,flashbots,miner,mev
--ws --ws.addr 0.0.0.0 --ws.port 8546 --ws.api admin,debug,eth,net,trace,txpool,web3,rpc,reth,ots,flashbots,miner,mev \
--log.stdout.format log-fmt -vvv --l1.url http://l1geth-rpc.mainnet.scroll.tech:8545/l1 --beacon.url http://l1reth-cl.mainnet.scroll.tech:5052 --network.scroll-wire --network.bridge \
Expand Down