diff --git a/crates/snot-common/src/state.rs b/crates/snot-common/src/state.rs index 330d27bf..754d0150 100644 --- a/crates/snot-common/src/state.rs +++ b/crates/snot-common/src/state.rs @@ -47,55 +47,6 @@ impl Display for PortConfig { } } -// // agent code -// impl AgentState { -// async fn reconcile(&self, target: AgentState) { -// // assert that we can actually move from self to target -// // if not, return ReconcileFailed - -// if self.peers != target.peers { -// if self.online { -// self.turn_offline(); -// } - -// // make change to peers -// self.peers = target.peers; -// // make the change in snarkos - -// // restore online state -// } - -// // and do the rest of these fields - -// // return StateReconciled(self) -// } -// } - -// #[derive(Debug, Default, Clone, Serialize, Deserialize)] -// pub enum AgentState { -// Inventory, -// Node(ContextRequest, ConfigRequest), -// Cannon(/* config */), -// } - -// /// Desired state for an agent's node. -// #[derive(Debug, Default, Clone, Serialize, Deserialize)] -// pub struct ContextRequest { -// pub id: usize, -// pub ty: NodeType, -// pub storage: StorageId, -// pub starting_height: Option, -// } - -// #[derive(Debug, Default, Clone, Serialize, Deserialize)] -// pub struct ConfigRequest { -// pub id: usize, -// pub online: bool, -// pub peers: Vec, -// pub validators: Vec, -// pub next_height: Option, -// } - #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub enum HeightRequest { #[default] @@ -130,42 +81,6 @@ impl AgentPeer { } } -// /// The state reported by an agent. -// #[derive(Debug, Default, Clone, Serialize, Deserialize, Hash)] -// pub struct ResolvedState { -// /// The timestamp of the last update. -// pub timestamp: i64, // TODO: chrono - -// // pub online: bool, -// // pub config_ty: Option, - -// pub current_state: State, - -// pub genesis_hash: Option, -// pub config_peers: Option>, -// pub config_validators: Option>, -// pub snarkos_peers: Option>, -// pub snarkos_validators: Option>, -// pub block_height: Option, -// pub block_timestamp: Option, -// } - -// impl ConfigRequest { -// pub fn new() -> Self { -// Self::default() -// } - -// pub fn with_online(mut self, online: bool) -> Self { -// self.online = online; -// self -// } - -// pub fn with_type(mut self, ty: Option) -> Self { -// self.ty = ty; -// self -// } -// } - #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct NodeKey { pub ty: NodeType, diff --git a/crates/snot/src/schema/nodes.rs b/crates/snot/src/schema/nodes.rs index 52b2f609..ceda118c 100644 --- a/crates/snot/src/schema/nodes.rs +++ b/crates/snot/src/schema/nodes.rs @@ -1,7 +1,8 @@ use std::net::SocketAddr; use indexmap::IndexMap; -use serde::Deserialize; +use lazy_static::lazy_static; +use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; use snot_common::state::{HeightRequest, NodeState, NodeType}; use super::{NodeKey, NodeTargets}; @@ -43,11 +44,8 @@ pub struct Node { /// When specified, creates a group of nodes, all with the same /// configuration. pub replicas: Option, - /// The private key to start the node with. When unspecified, a random - /// private key is generated at runtime. - // TODO: turn this into an enum with options like `None`, `Additional(usize)`, - // `Committee(usize)`, `Named(String)`, `Literal(String)` - pub key: Option, + /// The private key to start the node with. + pub key: Option, /// Height of ledger to inherit. /// /// * When null, a ledger is created when the node is started. @@ -65,7 +63,7 @@ impl Node { pub fn into_state(&self, ty: NodeType) -> NodeState { NodeState { ty, - private_key: self.key.clone(), + private_key: None, // TODO height: (0, HeightRequest::Top), @@ -78,3 +76,145 @@ impl Node { } } } + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum KeySource { + /// APrivateKey1zkp... + Literal(String), + /// committee.0 or committee.$ (for replicas) + Committee(Option), + /// accounts.0 or accounts.$ (for replicas) + Named(String, Option), +} + +struct KeySourceVisitor; + +impl<'de> Visitor<'de> for KeySourceVisitor { + type Value = KeySource; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string that represents an aleo private key, or a file from storage") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + // use KeySource::Literal(String) when the string is 59 characters long and starts with "APrivateKey1zkp" + // use KeySource::Commitee(Option) when the string is "committee.0" or "committee.$" + // use KeySource::Named(String, Option) when the string is "\w+.0" or "\w+.$" + + // aleo private key + if v.len() == 59 && v.starts_with("APrivateKey1") { + return Ok(KeySource::Literal(v.to_string())); + + // committee key + } else if let Some(index) = v.strip_prefix("committee.") { + if index == "$" { + return Ok(KeySource::Committee(None)); + } + let replica = index + .parse() + .map_err(|_e| E::custom("committee index must be a positive number"))?; + return Ok(KeySource::Committee(Some(replica))); + } + + // named key (using regex with capture groups) + lazy_static! { + static ref NAMED_KEYSOURCE_REGEX: regex::Regex = + regex::Regex::new(r"^(?P\w+)\.(?P\d+|\$)$").unwrap(); + } + let groups = NAMED_KEYSOURCE_REGEX + .captures(v) + .ok_or_else(|| E::custom("invalid key source"))?; + let name = groups.name("name").unwrap().as_str().to_string(); + let idx = match groups.name("idx").unwrap().as_str() { + "$" => None, + idx => Some( + idx.parse() + .map_err(|_e| E::custom("index must be a positive number"))?, + ), + }; + Ok(KeySource::Named(name, idx)) + } +} + +impl<'de> Deserialize<'de> for KeySource { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(KeySourceVisitor) + } +} + +impl Serialize for KeySource { + fn serialize(&self, serializer: S) -> Result { + match self { + KeySource::Literal(key) => serializer.serialize_str(key), + KeySource::Committee(None) => serializer.serialize_str("committee.$"), + KeySource::Committee(Some(idx)) => { + serializer.serialize_str(&format!("committee.{}", idx)) + } + KeySource::Named(name, None) => serializer.serialize_str(&format!("{}.{}", name, "$")), + KeySource::Named(name, Some(idx)) => { + serializer.serialize_str(&format!("{}.{}", name, idx)) + } + } + } +} + +impl KeySource { + pub fn with_index(&self, idx: usize) -> Self { + match self { + KeySource::Committee(_) => KeySource::Committee(Some(idx)), + KeySource::Named(name, _) => KeySource::Named(name.clone(), Some(idx)), + _ => self.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_key_source_deserialization() { + assert_eq!( + serde_yaml::from_str::("committee.0").expect("foo"), + KeySource::Committee(Some(0)) + ); + assert_eq!( + serde_yaml::from_str::("committee.100").expect("foo"), + KeySource::Committee(Some(100)) + ); + assert_eq!( + serde_yaml::from_str::("committee.$").expect("foo"), + KeySource::Committee(None) + ); + + assert_eq!( + serde_yaml::from_str::("accounts.0").expect("foo"), + KeySource::Named("accounts".to_string(), Some(0)) + ); + assert_eq!( + serde_yaml::from_str::("accounts.$").expect("foo"), + KeySource::Named("accounts".to_string(), None) + ); + + assert_eq!( + serde_yaml::from_str::( + "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" + ) + .expect("foo"), + KeySource::Literal( + "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH".to_string() + ) + ); + + assert!(serde_yaml::from_str::("committee.-100").is_err(),); + assert!(serde_yaml::from_str::("accounts.-100").is_err(),); + assert!(serde_yaml::from_str::("accounts._").is_err(),); + assert!(serde_yaml::from_str::("accounts.*").is_err(),); + } +} diff --git a/crates/snot/src/schema/storage.rs b/crates/snot/src/schema/storage.rs index 85470bb1..7f184b54 100644 --- a/crates/snot/src/schema/storage.rs +++ b/crates/snot/src/schema/storage.rs @@ -51,14 +51,23 @@ pub struct Transaction { } #[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] pub struct GenesisGeneration { pub output: PathBuf, + pub committee: usize, + pub committee_balances: usize, + pub additional_accounts: usize, + pub additional_balances: usize, } impl Default for GenesisGeneration { fn default() -> Self { Self { output: PathBuf::from("genesis.block"), + committee: 5, + committee_balances: 10_000_000_000_000, + additional_accounts: 5, + additional_balances: 100_000_000_000, } } } @@ -158,15 +167,9 @@ impl Document { } generation.genesis = GenesisGeneration { - output: base.join("genesis.block"), + output: base.join(generation.genesis.output), + ..generation.genesis }; - // generation.genesis = snarkos_aot::genesis::Genesis { - // output: base.join("genesis.block"), - // ledger: None, - // additional_accounts_output: Some(base.join("accounts.json")), - // committee_output: Some(base.join("committee.json")), - // ..generation.genesis - // }; // generate the genesis block using the aot cli let bin = std::env::var("AOT_BIN").map(PathBuf::from).unwrap_or( @@ -180,11 +183,11 @@ impl Document { .arg("--output") .arg(&generation.genesis.output) .arg("--committee-size") - .arg("5") + .arg(generation.genesis.committee.to_string()) .arg("--committee-output") .arg(base.join("committee.json")) .arg("--additional-accounts") - .arg("5") + .arg(generation.genesis.additional_accounts.to_string()) .arg("--additional-accounts-output") .arg(base.join("accounts.json")) .arg("--ledger") diff --git a/crates/snot/src/server/mod.rs b/crates/snot/src/server/mod.rs index f5cb6999..b81c9b20 100644 --- a/crates/snot/src/server/mod.rs +++ b/crates/snot/src/server/mod.rs @@ -49,6 +49,7 @@ pub async fn start(cli: Cli) -> Result<()> { let app = Router::new() .route("/agent", get(agent_ws_handler)) .nest("/api/v1", api::routes()) + // /env//ledger/* - ledger query service reverse proxying /mainnet/latest/stateRoot .nest("/content", content::init_routes(&state).await) .with_state(Arc::new(state)) .layer( diff --git a/crates/snot/src/testing.rs b/crates/snot/src/testing.rs index f0a1b769..740a2805 100644 --- a/crates/snot/src/testing.rs +++ b/crates/snot/src/testing.rs @@ -8,7 +8,7 @@ use tracing::{info, warn}; use crate::{ schema::{ - nodes::{ExternalNode, Node}, + nodes::{ExternalNode, KeySource, Node}, storage::FilenameString, ItemDocument, NodeTargets, }, @@ -100,7 +100,7 @@ impl Test { // replace the key with a new one let mut node = doc_node.to_owned(); if let Some(key) = node.key.take() { - node.key = Some(key.replace('$', &i.to_string())) + node.key = Some(key.with_index(i)) } ent.insert(TestNode::Internal(node)) } @@ -313,6 +313,11 @@ pub async fn initial_reconcile(state: &GlobalState) -> anyhow::Result<()> { // resolve the peers and validators let mut node_state = node.into_state(key.ty); + node_state.private_key = node.key.as_ref().map(|key| match key { + KeySource::Literal(pk) => pk.to_owned(), + KeySource::Committee(_i) => todo!(), + KeySource::Named(_, _) => todo!(), + }); node_state.peers = matching_nodes(key, &node.peers, false)?; node_state.validators = matching_nodes(key, &node.validators, true)?;