Skip to content
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
4 changes: 4 additions & 0 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions bin/benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ nohup miden-validator start \
--encryption-key.hex "<encryption-key-hex>" \
> logs/validator.log 2>&1 &

miden-node fee-collector create --data-directory "$DATA/node"
miden-node fee-collector deploy \
--data-directory "$DATA/node" \
--validator.url http://127.0.0.1:50101

# The ntx-builder needs a transaction prover, so start one regardless.
nohup miden-remote-prover \
--port 50051 \
Expand Down
2 changes: 2 additions & 0 deletions bin/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ miden-node-store = { workspace = true }
miden-node-tracing = { workspace = true }
miden-node-utils = { workspace = true }
miden-protocol = { workspace = true }
miden-standards = { workspace = true }
rand = { workspace = true }
serde = { features = ["derive"], workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
Expand Down
187 changes: 187 additions & 0 deletions bin/node/src/commands/fee_collector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Duration;

use anyhow::Context;
use miden_node_block_producer::{DEFAULT_VALIDATOR_TIMEOUT, deploy_fee_collector};
use miden_node_store::{DataDirectory, State};
use miden_node_tracing::info;
use miden_node_utils::clap::duration_to_human_readable_string;
use miden_node_utils::shutdown::CancellationToken;
use miden_protocol::account::auth::AuthSecretKey;
use miden_protocol::account::{AccountBuilder, AccountFile, AccountType};
use miden_protocol::utils::serde::Serializable;
use miden_standards::account::auth::AuthTxFeeCollector;
use miden_standards::account::wallets::BasicWallet;
use url::Url;

use super::ENV_DATA_DIRECTORY;
use super::store::StoreOptions;

#[cfg(test)]
mod tests;

#[derive(clap::Subcommand, Debug)]
pub enum FeeCollectorCommand {
/// Create a fee collector account and save its signing key.
///
/// Writes fee-collector.mac in the existing data directory. Refuses to overwrite an existing
/// file. Creation is offline. Keep the file private because it contains the signing key.
///
/// Use `miden-node fee-collector deploy` to deploy this account before collecting batch fees.
Create(CreateCommand),

/// Deploy a fee collector account in a dedicated block.
///
/// Loads fee-collector.mac from the data directory, or the file specified by
/// --fee-collector-account.
///
/// Stop any node process that uses the data directory. All validators must be running to
/// validate the transaction and sign the deployment block.
///
/// Generates the transaction, batch, and block proofs locally. Deployment requires no funds
/// and pays no transaction fee. If the matching account is already deployed, the command
/// succeeds without creating another block.
///
/// Keep the account file and its signing key for fee collection.
Deploy(Box<DeployCommand>),
}

impl FeeCollectorCommand {
pub async fn handle(self, shutdown: CancellationToken) -> anyhow::Result<()> {
match self {
Self::Create(command) => command.handle(),
Self::Deploy(command) => command.handle(shutdown).await,
}
}
}

#[derive(clap::Args, Debug)]
pub struct CreateCommand {
/// Existing directory in which to create fee-collector.mac. The file must not exist.
#[arg(long, env = ENV_DATA_DIRECTORY, value_name = "DIR")]
data_directory: PathBuf,
}

impl CreateCommand {
fn handle(self) -> anyhow::Result<()> {
let output = DataDirectory::load(self.data_directory)?.fee_collector_account_path();
let secret_key = AuthSecretKey::new_ecdsa_k256_keccak();
let account = AccountBuilder::new(rand::random())
.account_type(AccountType::Public)
.with_component(AuthTxFeeCollector::from_public_key(secret_key.public_key()))
.with_component(BasicWallet)
.build()?;
let account_file = AccountFile::new(account, vec![secret_key]);
let mut options = fs_err::OpenOptions::new();
options.create_new(true).write(true);
#[cfg(unix)]
{
use fs_err::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file =
options.open(&output).context("failed to create fee collector account file")?;
file.write_all(&account_file.to_bytes())?;
file.sync_all()?;
info!(
target: crate::LOG_TARGET,
"Saved new fee collector account",
account.id = account_file.account.id(),
account.file = output.as_path()
);
Ok(())
}
}

#[derive(clap::Args, Clone, Debug)]
pub struct FeeCollectorAccountOptions {
/// Fee collector account file, including its signing key. Defaults to fee-collector.mac in the
/// data directory.
#[arg(
long = "fee-collector-account",
env = "MIDEN_NODE_FEE_COLLECTOR_ACCOUNT",
value_name = "FILE"
)]
account: Option<PathBuf>,
}

impl FeeCollectorAccountOptions {
pub fn read(&self, data_directory: &Path) -> anyhow::Result<AccountFile> {
let path = match &self.account {
Some(path) => path.clone(),
None => DataDirectory::load(data_directory.to_path_buf())?.fee_collector_account_path(),
};
AccountFile::read(&path).with_context(|| {
format!("failed to read fee collector account from {}", path.display())
})
}
}

#[derive(clap::Args, Debug)]
pub struct DeployCommand {
/// Directory containing the node's local data storage.
#[arg(long, env = ENV_DATA_DIRECTORY, value_name = "DIR")]
data_directory: PathBuf,

#[command(flatten)]
fee_collector: FeeCollectorAccountOptions,

/// URLs of all validators in the current validator set. Repeat this option for each validator.
#[arg(
long = "validator.url",
env = "MIDEN_NODE_VALIDATOR_URL",
value_name = "URL",
value_delimiter = ',',
required = true
)]
validator_urls: Vec<Url>,

/// Request timeout for calls to the validator services.
#[arg(
long = "validator.timeout",
env = "MIDEN_NODE_VALIDATOR_TIMEOUT",
default_value = duration_to_human_readable_string(DEFAULT_VALIDATOR_TIMEOUT),
value_parser = humantime::parse_duration,
value_name = "DURATION"
)]
validator_timeout: Duration,

#[command(flatten)]
store: StoreOptions,
}

impl DeployCommand {
async fn handle(self, shutdown: CancellationToken) -> anyhow::Result<()> {
let account = self.fee_collector.read(&self.data_directory)?;
let loaded = State::load_with_database_options(
&self.data_directory,
self.store.storage.into(),
self.store.sqlite.database_options(),
)
.await
.context("failed to load node state")?;
let (state, mut block_writer, mut proof_writer, writer_task) =
loaded.start(CancellationToken::new());
let result = async {
anyhow::ensure!(
state.proven_tip() == state.committed_tip(),
"sync all committed block proofs before deploying a fee collector",
);
tokio::select! {
() = shutdown.cancelled() => anyhow::bail!("fee collector deployment cancelled"),
result = Box::pin(deploy_fee_collector(
&state,
&mut block_writer,
&mut proof_writer,
account,
self.validator_urls,
self.validator_timeout,
)) => result,
}
}
.await;
block_writer.stop(writer_task).await;
result
}
}
62 changes: 62 additions & 0 deletions bin/node/src/commands/fee_collector/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use super::*;

#[test]
fn saves_the_collector_signing_key_without_overwriting_existing_files() -> anyhow::Result<()> {
let directory = tempfile::tempdir()?;
let path = directory.path().join("fee-collector.mac");
CreateCommand {
data_directory: directory.path().to_path_buf(),
}
.handle()?;
let account_file = AccountFile::read(&path)?;
let loaded = FeeCollectorAccountOptions { account: None }.read(directory.path())?;
assert_eq!(loaded.to_bytes(), account_file.to_bytes());
assert!(account_file.account.is_new());
assert!(account_file.account.is_public());
assert!(account_file.account.vault().is_empty());
assert_eq!(account_file.auth_secret_keys.len(), 1);
assert_eq!(
account_file.account.storage().get_item(AuthTxFeeCollector::public_key_slot())?,
miden_protocol::Word::from(account_file.auth_secret_keys[0].public_key().to_commitment()),
);
let contents = fs_err::read(&path)?;
assert!(
CreateCommand {
data_directory: directory.path().to_path_buf()
}
.handle()
.is_err()
);
assert_eq!(fs_err::read(&path)?, contents);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(fs_err::metadata(&path)?.permissions().mode() & 0o777, 0o600);
}
Ok(())
}

#[test]
fn explicit_account_file_overrides_the_data_directory_default() -> anyhow::Result<()> {
let directory = tempfile::tempdir()?;
let other_directory = tempfile::tempdir()?;
CreateCommand {
data_directory: directory.path().to_path_buf(),
}
.handle()?;
CreateCommand {
data_directory: other_directory.path().to_path_buf(),
}
.handle()?;
let default_path = directory.path().join("fee-collector.mac");
let custom_path = other_directory.path().join("fee-collector.mac");
let default_contents = fs_err::read(&default_path)?;
let custom_contents = fs_err::read(&custom_path)?;
let account =
FeeCollectorAccountOptions { account: Some(custom_path.clone()) }.read(directory.path())?;
assert_eq!(account.to_bytes(), custom_contents);
assert_ne!(account.account.id(), AccountFile::read(&default_path)?.account.id());
assert_eq!(fs_err::read(default_path)?, default_contents);
assert_eq!(fs_err::read(custom_path)?, custom_contents);
Ok(())
}
17 changes: 14 additions & 3 deletions bin/node/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod block_producer;
mod fee_collector;
mod lifecycle;
mod modes;
mod recover;
Expand All @@ -8,6 +9,7 @@ pub(crate) mod section;
mod store;

use clap::Subcommand;
pub use fee_collector::FeeCollectorCommand;
pub use lifecycle::{BootstrapCommand, MigrateCommand};
use miden_node_tracing::OpenTelemetry;
use miden_node_utils::shutdown::CancellationToken;
Expand Down Expand Up @@ -40,6 +42,13 @@ pub enum Command {
/// initialized before the node can be started.
Bootstrap(BootstrapCommand),

/// Create or deploy the sequencer's fee collector account.
///
/// The immutable collector combines transaction fees into P2ID notes for the batch builder's
/// wallet.
#[command(subcommand)]
FeeCollector(FeeCollectorCommand),

/// Apply pending migrations to the node's storage.
///
/// Migrates the node's data storage from its current schema version to the version required by
Expand Down Expand Up @@ -78,15 +87,17 @@ impl Command {
Command::Full(_) => OpenTelemetry::from_env()
.with_name("node")
.with_attribute("miden.node.role", "full"),
Command::Bootstrap(_) | Command::Migrate(_) | Command::Recover(_) => {
OpenTelemetry::Disabled
},
Command::Bootstrap(_)
| Command::FeeCollector(_)
| Command::Migrate(_)
| Command::Recover(_) => OpenTelemetry::Disabled,
}
}

pub(crate) async fn execute(self, shutdown: CancellationToken) -> anyhow::Result<()> {
match self {
Command::Bootstrap(bootstrap_command) => bootstrap_command.handle().await,
Command::FeeCollector(command) => command.handle(shutdown).await,
Command::Migrate(migrate_command) => migrate_command.handle(),
Command::Sequencer(sequencer_command) => sequencer_command.handle(shutdown).await,
Command::Full(full_node_command) => full_node_command.handle(shutdown).await,
Expand Down
31 changes: 31 additions & 0 deletions compose/bootstrap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,37 @@ services:

touch /data/node/.bootstrapped

deploy-fee-collector:
image: ${MIDEN_NODE_IMAGE:-miden-node}
pull_policy: missing
volumes:
- node-data:/data
depends_on:
bootstrap-node:
condition: service_completed_successfully
validator-1:
condition: service_started
validator-2:
condition: service_started
validator-3:
condition: service_started
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -e
if [ -f /data/node/.fee-collector-deployed ]; then
exit 0
fi
if [ ! -f /data/node/fee-collector.mac ]; then
miden-node fee-collector create --data-directory /data/node
fi
miden-node fee-collector deploy \
--data-directory /data/node \
--validator.url http://validator-1:50101 \
--validator.url http://validator-2:50101 \
--validator.url http://validator-3:50101
touch /data/node/.fee-collector-deployed

bootstrap-ntx-builder:
image: ${MIDEN_NTX_BUILDER_IMAGE:-miden-ntx-builder}
pull_policy: missing
Expand Down
2 changes: 1 addition & 1 deletion compose/node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ services:
volumes:
- node-data:/data
depends_on:
bootstrap-node:
deploy-fee-collector:
condition: service_completed_successfully
otel-collector:
condition: service_started
Expand Down
Loading
Loading