diff --git a/protocol/pcp/dlu/README.md b/protocol/pcp/dlu/README.md new file mode 100644 index 00000000..ed6e812c --- /dev/null +++ b/protocol/pcp/dlu/README.md @@ -0,0 +1,5 @@ +# Deployed Logic Units (DLU) + +DLUs are units of logic that are not generally run within an independent process, but which must be deployed somewhere. Smart contracts are DLU. + +In this directory, we include DLU and the means via which to deploy them to various environments. diff --git a/protocol/pcp/manager/Cargo.toml b/protocol/pcp/manager/Cargo.toml new file mode 100644 index 00000000..3903e8e4 --- /dev/null +++ b/protocol/pcp/manager/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "postconfirmationssettlement-manager" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +publish = { workspace = true } +rust-version = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +postconfirmationsconfig = { workspace = true } +postconfirmationssettlement-client = { workspace = true } +movement-types = { workspace = true } + +anyhow = { workspace = true } +async-stream = { workspace = true } +async-trait = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +serde_json = { workspace = true } + +[dev-dependencies] +postconfirmationssettlement-client = { workspace = true, features = ["mock"] } + +[features] +default = ["stub"] +stub = [] + +[lints] +workspace = true diff --git a/protocol/pcp/manager/src/lib.rs b/protocol/pcp/manager/src/lib.rs new file mode 100644 index 00000000..914ad420 --- /dev/null +++ b/protocol/pcp/manager/src/lib.rs @@ -0,0 +1,18 @@ +use pcp_types::block_commitment::{SuperBlockCommitment, SuperBlockCommitmentEvent}; +use tokio_stream::Stream; + +mod manager; + +pub use manager::Manager as PcpSettlementManager; + +pub type CommitmentEventStream = + std::pin::Pin> + Send>>; + +#[async_trait::async_trait] +pub trait PcpSettlementManagerOperations { + /// Adds a block commitment to the manager queue. + async fn post_block_commitment( + &self, + block_commitment: SuperBlockCommitment, + ) -> Result<(), anyhow::Error>; +} diff --git a/protocol/pcp/manager/src/manager.rs b/protocol/pcp/manager/src/manager.rs new file mode 100644 index 00000000..852505ec --- /dev/null +++ b/protocol/pcp/manager/src/manager.rs @@ -0,0 +1,282 @@ +use crate::{CommitmentEventStream, PcpSettlementManagerOperations, SuperBlockCommitmentEvent}; + +use pcp_config::Config; +use postconfirmations_settlement_client::PcpSettlementClientOperations; +use pcp_types::block_commitment::{ + SuperBlockCommitment, SuperBlockCommitmentRejectionReason, +}; + +use async_stream::stream; +use async_trait::async_trait; +use futures::future::{self, Either}; +use tokio::sync::mpsc; +use tokio::time; +use tokio_stream::StreamExt; + +use std::collections::BTreeMap; +use std::mem; +use std::time::Duration; + +/// Public handle for the PCP settlement manager. +pub struct Manager { + sender: mpsc::Sender, +} + +impl Manager { + /// Creates a new PCP settlement manager. + /// + /// Returns the handle with the public API and the stream to receive commitment events. + /// The stream needs to be polled to drive the PCP settlement client and + /// process the commitments. + pub fn new( + client: C, + config: &Config, + ) -> (Self, CommitmentEventStream) { + let batch_timeout = Duration::from_millis(config.transactions.batch_timeout); + let (sender, receiver) = mpsc::channel(16); + let event_stream = process_commitments(receiver, client, batch_timeout); + (Self { sender }, event_stream) + } +} + +#[async_trait] +impl PcpSettlementManagerOperations for Manager { + async fn post_block_commitment( + &self, + block_commitment: SuperBlockCommitment, + ) -> Result<(), anyhow::Error> { + self.sender.send(block_commitment).await?; + Ok(()) + } +} + +fn process_commitments( + mut receiver: mpsc::Receiver, + client: C, + batch_timeout: Duration, +) -> CommitmentEventStream { + // Can't mix try_stream! and select!, see https://github.com/tokio-rs/async-stream/issues/63 + Box::pin(stream! { + let mut settlement_stream = client.stream_block_commitments().await?; + let mut max_height = client.get_max_tolerable_block_height().await?; + let mut ahead_of_settlement = false; + let mut commitments_to_settle = BTreeMap::new(); + let mut batch_acc = Vec::new(); + let mut batch_ready = Either::Left(future::pending::<()>()); + loop { + tokio::select! { + Some(block_commitment) = receiver.recv(), if !ahead_of_settlement => { + commitments_to_settle.insert( + block_commitment.height(), + block_commitment.commitment().clone(), + ); + if block_commitment.height() > max_height { + // Can't post this commitment to the contract yet. + // Post the previously accumulated commitments as a batch + // and pause reading from input. + ahead_of_settlement = true; + let batch = mem::replace(&mut batch_acc, Vec::new()); + if let Err(e) = client.post_block_commitment_batch(batch).await { + yield Err(e); + break; + } + } + // If this commitment starts a new batch, start the timeout + if batch_acc.is_empty() { + batch_ready = Either::Right(Box::pin(time::sleep(batch_timeout))); + } + batch_acc.push(block_commitment); + } + _ = &mut batch_ready => { + // Batch timeout has expired, post the commitments we have now + let batch = mem::replace(&mut batch_acc, Vec::new()); + if let Err(e) = client.post_block_commitment_batch(batch).await { + yield Err(e); + break; + } + // Disable the batch timeout + batch_ready = Either::Left(future::pending::<()>()); + } + Some(res) = settlement_stream.next() => { + let settled_commitment = match res { + Ok(commitment) => commitment, + Err(e) => { + yield Err(e); + break; + } + }; + + let height = settled_commitment.height(); + if let Some(commitment) = commitments_to_settle.remove(&height) { + let event = if commitment == settled_commitment.commitment() { + SuperBlockCommitmentEvent::Accepted(settled_commitment) + } else { + SuperBlockCommitmentEvent::Rejected { + height, + reason: SuperBlockCommitmentRejectionReason::InvalidCommitment, + } + }; + yield Ok(event); + } else if let Some((&lh, _)) = commitments_to_settle.last_key_value() { + if lh < height { + // Settlement has left some commitments behind, but the client could + // deliver them of order? + todo!("Handle falling behind on settlement") + } + } + // Remove back-pressure if we can proceed settling new blocks. + if ahead_of_settlement { + let new_max_height = match client.get_max_tolerable_block_height().await { + Ok(h) => h, + Err(e) => { + yield Err(e); + break; + } + }; + if new_max_height > max_height { + max_height = new_max_height; + ahead_of_settlement = false; + } + } + } + else => break + } + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use postconfirmations_settlement_client::mock::PcpSettlementClient; + use pcp_types::block_commitment::{Commitment, SuperBlockCommitment}; + + #[tokio::test] + async fn test_block_commitment_accepted() -> Result<(), anyhow::Error> { + let config = Config::default(); + let mut client = PcpSettlementClient::new(); + client.block_lead_tolerance = 1; + let (manager, mut event_stream) = Manager::new(client.clone(), &config); + let commitment = SuperBlockCommitment::new(1, Default::default(), Commitment::new([1; 32])); + manager.post_block_commitment(commitment.clone()).await?; + let commitment2 = + SuperBlockCommitment::new(2, Default::default(), Commitment::new([2; 32])); + manager.post_block_commitment(commitment2).await?; + let item = event_stream.next().await; + let res = item.unwrap(); + let event = res.unwrap(); + assert_eq!(event, SuperBlockCommitmentEvent::Accepted(commitment)); + Ok(()) + } + + #[tokio::test] + async fn test_block_commitment_rejected() -> Result<(), anyhow::Error> { + let config = Config::default(); + let mut client = PcpSettlementClient::new(); + client.block_lead_tolerance = 1; + let (manager, mut event_stream) = Manager::new(client.clone(), &config); + let commitment = SuperBlockCommitment::new(1, Default::default(), Commitment::new([1; 32])); + client + .override_block_commitment(SuperBlockCommitment::new( + 1, + Default::default(), + Commitment::new([3; 32]), + )) + .await; + manager.post_block_commitment(commitment.clone()).await?; + let commitment2 = + SuperBlockCommitment::new(2, Default::default(), Commitment::new([2; 32])); + manager.post_block_commitment(commitment2).await?; + let item = event_stream.next().await; + let res = item.unwrap(); + let event = res.unwrap(); + assert_eq!( + event, + SuperBlockCommitmentEvent::Rejected { + height: 1, + reason: SuperBlockCommitmentRejectionReason::InvalidCommitment, + } + ); + Ok(()) + } + + #[tokio::test] + async fn test_back_pressure() -> Result<(), anyhow::Error> { + let config = Config::default(); + let mut client = PcpSettlementClient::new(); + client.block_lead_tolerance = 2; + client.pause_after(2).await; + let (manager, mut event_stream) = Manager::new(client.clone(), &config); + + let commitment1 = + SuperBlockCommitment::new(1, Default::default(), Commitment::new([1; 32])); + manager.post_block_commitment(commitment1.clone()).await?; + let commitment2 = + SuperBlockCommitment::new(2, Default::default(), Commitment::new([2; 32])); + manager.post_block_commitment(commitment2.clone()).await?; + let commitment3 = + SuperBlockCommitment::new(3, Default::default(), Commitment::new([3; 32])); + manager.post_block_commitment(commitment3.clone()).await?; + + let event = event_stream.next().await.expect("stream has ended")?; + assert_eq!(event, SuperBlockCommitmentEvent::Accepted(commitment1.clone())); + let event = event_stream.next().await.expect("stream has ended")?; + assert_eq!(event, SuperBlockCommitmentEvent::Accepted(commitment2.clone())); + + // The batch of first two should have been posted, + // the third commitment is batched in the manager. + assert_eq!(client.get_commitment_at_height(1).await?, Some(commitment1.clone())); + assert_eq!(client.get_commitment_at_height(2).await?, Some(commitment2.clone())); + assert_eq!(client.get_commitment_at_height(3).await?, None); + + // Unblock the client, allowing processing of commitments to resume. + client.resume().await; + + let commitment4 = + SuperBlockCommitment::new(4, Default::default(), Commitment::new([4; 32])); + manager.post_block_commitment(commitment4).await?; + let commitment5 = + SuperBlockCommitment::new(5, Default::default(), Commitment::new([5; 32])); + manager.post_block_commitment(commitment5).await?; + + let event = event_stream.next().await.expect("stream has ended")?; + assert_eq!(event, SuperBlockCommitmentEvent::Accepted(commitment3.clone())); + + Ok(()) + } + + #[tokio::test] + async fn test_batch_timeout() -> Result<(), anyhow::Error> { + let mut config = Config::default(); + config.transactions.batch_timeout = 100; + let client = PcpSettlementClient::new(); + let (manager, mut event_stream) = Manager::new(client.clone(), &config); + + let commitment1 = + SuperBlockCommitment::new(1, Default::default(), Commitment::new([1; 32])); + manager.post_block_commitment(commitment1.clone()).await?; + let commitment2 = + SuperBlockCommitment::new(2, Default::default(), Commitment::new([2; 32])); + manager.post_block_commitment(commitment2.clone()).await?; + + let item = time::timeout(Duration::from_secs(2), event_stream.next()) + .await + .expect("no timeout"); + let event = item.expect("stream has ended")?; + assert_eq!(event, SuperBlockCommitmentEvent::Accepted(commitment1.clone())); + let event = event_stream.next().await.expect("stream has ended")?; + assert_eq!(event, SuperBlockCommitmentEvent::Accepted(commitment2.clone())); + + let commitment3 = + SuperBlockCommitment::new(3, Default::default(), Commitment::new([3; 32])); + manager.post_block_commitment(commitment3.clone()).await?; + + let item = time::timeout(Duration::from_secs(2), event_stream.next()) + .await + .expect("no timeout"); + let event = item.expect("stream has ended")?; + assert_eq!(event, SuperBlockCommitmentEvent::Accepted(commitment3)); + + Ok(()) + } +} diff --git a/protocol/pcp/util/config/Cargo.toml b/protocol/pcp/util/config/Cargo.toml new file mode 100644 index 00000000..40625c00 --- /dev/null +++ b/protocol/pcp/util/config/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "pcp-config" +description = "Configuration of the PCP settlement client" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +publish.workspace = true +rust-version.workspace = true + +[dependencies] +serde = { workspace = true , features = ["derive"] } +alloy = { workspace = true } +ffs-environment = { workspace = true } +anyhow = { workspace = true } +secure-signer-loader = { workspace = true } +secure-signer = { workspace = true} + +[lints] +workspace = true diff --git a/protocol/pcp/util/config/src/common/deploy.rs b/protocol/pcp/util/config/src/common/deploy.rs new file mode 100644 index 00000000..fc7af00d --- /dev/null +++ b/protocol/pcp/util/config/src/common/deploy.rs @@ -0,0 +1,60 @@ +use alloy::signers::local::PrivateKeySigner; +use ffs_environment::env_short_default; +use secure_signer::key::TryFromCanonicalString; +use secure_signer_loader::identifiers::{local::Local, SignerIdentifier}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + #[serde(default = "pcp_deployment_working_directory")] + pub pcp_deployment_working_directory: String, + #[serde(default = "default_signer_identifier")] + pub signer_identifier: SignerIdentifier, +} + +env_short_default!( + pcp_deployment_working_directory, + String, + "protocol-units/settlement/pcp/contracts" +); + +env_short_default!( + pcp_local_anvil_account_private_key, + String, + PrivateKeySigner::random().to_bytes().to_string() +); + +pub fn default_signer_identifier() -> SignerIdentifier { + match std::env::var("PCP_SIGNER_IDENTIFIER") { + Ok(str_value) => SignerIdentifier::try_from_canonical_string(&str_value).unwrap(), + Err(_) => SignerIdentifier::Local(Local { + // todo: validate this is a valid private key + private_key_hex_bytes: pcp_local_anvil_account_private_key(), + }), + } +} + +pub fn maybe_deploy() -> Option { + match std::env::var("MAYBE_DEPLOY_PCP") { + Ok(str_value) => { + // if it parses as true then we want to deploy under the default config + let bool_value = str_value.parse::().unwrap_or(false); + + if bool_value { + Some(Config::default()) + } else { + None + } + } + Err(_) => None, + } +} + +impl Default for Config { + fn default() -> Self { + Config { + pcp_deployment_working_directory: pcp_deployment_working_directory(), + signer_identifier: default_signer_identifier(), + } + } +} diff --git a/protocol/pcp/util/config/src/common/eth_connection.rs b/protocol/pcp/util/config/src/common/eth_connection.rs new file mode 100644 index 00000000..437bee14 --- /dev/null +++ b/protocol/pcp/util/config/src/common/eth_connection.rs @@ -0,0 +1,106 @@ +use ffs_environment::env_default; +use serde::{Deserialize, Serialize}; + +const DEFAULT_ETH_RPC_CONNECTION_HOSTNAME: &str = "ethereum-holesky-rpc.publicnode.com"; +const DEFAULT_ETH_RPC_CONNECTION_PORT: u16 = 443; +const DEFAULT_ETH_WS_CONNECTION_HOSTNAME: &str = "ethereum-holesky-rpc.publicnode.com"; +const DEFAULT_ETH_WS_CONNECTION_PORT: u16 = 443; // same as RPC + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + #[serde(default = "default_eth_rpc_connection_protocol")] + pub eth_rpc_connection_protocol: String, + #[serde(default = "default_eth_rpc_connection_hostname")] + pub eth_rpc_connection_hostname: String, + #[serde(default = "default_eth_rpc_connection_port")] + pub eth_rpc_connection_port: u16, + + #[serde(default = "default_eth_ws_connection_protocol")] + pub eth_ws_connection_protocol: String, + #[serde(default = "default_eth_ws_connection_hostname")] + pub eth_ws_connection_hostname: String, + #[serde(default = "default_eth_ws_connection_port")] + pub eth_ws_connection_port: u16, + + #[serde(default)] + pub eth_chain_id: u64, +} + +env_default!( + default_eth_rpc_connection_protocol, + "ETH_RPC_CONNECTION_PROTOCOL", + String, + "https".to_string() +); + +env_default!( + default_eth_rpc_connection_hostname, + "ETH_RPC_CONNECTION_HOSTNAME", + String, + DEFAULT_ETH_RPC_CONNECTION_HOSTNAME.to_string() +); + +env_default!( + default_eth_rpc_connection_port, + "ETH_RPC_CONNECTION_PORT", + u16, + DEFAULT_ETH_RPC_CONNECTION_PORT +); + +env_default!( + default_eth_ws_connection_protocol, + "ETH_WS_CONNECTION_PROTOCOL", + String, + "ws".to_string() +); + +env_default!( + default_eth_ws_connection_hostname, + "ETH_WS_CONNECTION_HOSTNAME", + String, + DEFAULT_ETH_WS_CONNECTION_HOSTNAME.to_string() +); + +env_default!( + default_eth_ws_connection_port, + "ETH_WS_CONNECTION_PORT", + u16, + DEFAULT_ETH_WS_CONNECTION_PORT +); + +env_default!(default_eth_chain_id, "ETH_CHAIN_ID", u64, 0); + +impl Default for Config { + fn default() -> Self { + Config { + eth_rpc_connection_protocol: default_eth_rpc_connection_protocol(), + eth_rpc_connection_hostname: default_eth_rpc_connection_hostname(), + eth_rpc_connection_port: default_eth_rpc_connection_port(), + + eth_ws_connection_protocol: default_eth_ws_connection_protocol(), + eth_ws_connection_hostname: default_eth_ws_connection_hostname(), + eth_ws_connection_port: default_eth_ws_connection_port(), + eth_chain_id: default_eth_chain_id(), + } + } +} + +impl Config { + pub fn eth_rpc_connection_url(&self) -> String { + format!( + "{}://{}:{}", + self.eth_rpc_connection_protocol, + self.eth_rpc_connection_hostname, + self.eth_rpc_connection_port + ) + } + + pub fn eth_ws_connection_url(&self) -> String { + format!( + "{}://{}:{}", + self.eth_ws_connection_protocol, + self.eth_ws_connection_hostname, + self.eth_ws_connection_port + ) + } +} diff --git a/protocol/pcp/util/config/src/common/mod.rs b/protocol/pcp/util/config/src/common/mod.rs new file mode 100644 index 00000000..3c5c62d3 --- /dev/null +++ b/protocol/pcp/util/config/src/common/mod.rs @@ -0,0 +1,6 @@ +pub mod deploy; +pub mod eth_connection; +pub mod settlement; +pub mod staking; +pub mod testing; +pub mod transactions; diff --git a/protocol/pcp/util/config/src/common/settlement.rs b/protocol/pcp/util/config/src/common/settlement.rs new file mode 100644 index 00000000..4f0c0ccf --- /dev/null +++ b/protocol/pcp/util/config/src/common/settlement.rs @@ -0,0 +1,55 @@ +use alloy::signers::local::PrivateKeySigner; +use ffs_environment::env_default; +use secure_signer_loader::identifiers::{local::Local, SignerIdentifier}; +use serde::{Deserialize, Serialize}; +use std::env; + +const DEFAULT_PCP_CONTRACT_ADDRESS: &str = "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + #[serde(default = "default_should_settle")] + pub should_settle: bool, + #[serde(default = "default_signer_identifier")] + pub signer_identifier: SignerIdentifier, + #[serde(default = "default_pcp_contract_address")] + pub pcp_contract_address: String, + #[serde(default = "default_settlement_super_block_size")] + pub settlement_super_block_size: u64, + #[serde(default = "default_settlement_admin_mode")] + pub settlement_admin_mode: bool, +} + +pub fn default_signer_identifier() -> SignerIdentifier { + let random_wallet = PrivateKeySigner::random(); + let private_key_hex_bytes = random_wallet.to_bytes().to_string(); + let signer_identifier = SignerIdentifier::Local(Local { private_key_hex_bytes }); + signer_identifier +} + +env_default!( + default_pcp_contract_address, + "PCP_CONTRACT_ADDRESS", + String, + DEFAULT_PCP_CONTRACT_ADDRESS.to_string() +); + +env_default!(default_settlement_admin_mode, "PCP_SETTLEMENT_ADMIN_MODE", bool, false); + +env_default!(default_settlement_super_block_size, "PCP_SETTLEMENT_SUPER_BLOCK_SIZE", u64, 1); + +pub fn default_should_settle() -> bool { + env::var("ETH_SIGNER_PRIVATE_KEY").is_ok() +} + +impl Default for Config { + fn default() -> Self { + Config { + should_settle: default_should_settle(), + signer_identifier: default_signer_identifier(), + pcp_contract_address: default_pcp_contract_address(), + settlement_admin_mode: default_settlement_admin_mode(), + settlement_super_block_size: default_settlement_super_block_size(), + } + } +} diff --git a/protocol/pcp/util/config/src/common/staking.rs b/protocol/pcp/util/config/src/common/staking.rs new file mode 100644 index 00000000..a872a95a --- /dev/null +++ b/protocol/pcp/util/config/src/common/staking.rs @@ -0,0 +1,14 @@ +use ffs_environment::env_short_default; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + #[serde(default = "default_move_token_contract_address")] + pub move_token_contract_address: String, + #[serde(default = "default_movement_staking_contract_address")] + pub movement_staking_contract_address: String, +} + +env_short_default!(default_move_token_contract_address, String, "0x0"); + +env_short_default!(default_movement_staking_contract_address, String, "0x0"); diff --git a/protocol/pcp/util/config/src/common/testing.rs b/protocol/pcp/util/config/src/common/testing.rs new file mode 100644 index 00000000..29df0c57 --- /dev/null +++ b/protocol/pcp/util/config/src/common/testing.rs @@ -0,0 +1,46 @@ +use ffs_environment::env_short_default; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + #[serde(default = "Vec::new")] + pub well_known_account_private_keys: Vec, + + #[serde(default = "default_pcp_testing_admin_account_private_key")] + pub pcp_testing_admin_account_private_key: String, + + #[serde(default = "default_move_token_contract_address")] + pub move_token_contract_address: String, + + #[serde(default = "default_movement_staking_contract_address")] + pub movement_staking_contract_address: String, +} + +env_short_default!(default_pcp_testing_admin_account_private_key, String, "0x0"); + +env_short_default!(default_move_token_contract_address, String, "0x0"); + +env_short_default!(default_movement_staking_contract_address, String, "0x0"); + +// env_or_none!( +// default_maybe_testing, +// Config, +// default_pcp_testing_admin_account_private_key, +// default_move_token_contract_address, +// default_movement_staking_contract_address +// ); + +pub fn maybe_testing() -> Option { + std::env::var("MAYBE_TESTING_PCP").ok().map(|_| Config::default()) +} + +impl Default for Config { + fn default() -> Self { + Config { + well_known_account_private_keys: Vec::new(), + pcp_testing_admin_account_private_key: default_pcp_testing_admin_account_private_key(), + move_token_contract_address: default_move_token_contract_address(), + movement_staking_contract_address: default_movement_staking_contract_address(), + } + } +} diff --git a/protocol/pcp/util/config/src/common/transactions.rs b/protocol/pcp/util/config/src/common/transactions.rs new file mode 100644 index 00000000..9b95ce17 --- /dev/null +++ b/protocol/pcp/util/config/src/common/transactions.rs @@ -0,0 +1,29 @@ +use ffs_environment::env_short_default; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + #[serde(default = "default_gas_limit")] + pub gas_limit: u64, + /// Timeout for batching blocks, in milliseconds + #[serde(default = "default_batch_timeout")] + pub batch_timeout: u64, + #[serde(default = "default_transaction_send_retries")] + pub transaction_send_retries: u32, +} + +env_short_default!(default_gas_limit, u64, 10_000_000_000_000_000 as u64); + +env_short_default!(default_batch_timeout, u64, 2000 as u64); + +env_short_default!(default_transaction_send_retries, u32, 10 as u32); + +impl Default for Config { + fn default() -> Self { + Config { + gas_limit: default_gas_limit(), + batch_timeout: default_batch_timeout(), + transaction_send_retries: default_transaction_send_retries(), + } + } +} diff --git a/protocol/pcp/util/config/src/lib.rs b/protocol/pcp/util/config/src/lib.rs new file mode 100644 index 00000000..fa44f5f4 --- /dev/null +++ b/protocol/pcp/util/config/src/lib.rs @@ -0,0 +1,67 @@ +//! This crate provides configuration parameters for the PCP settlement +//! component of a Movement node. +use serde::{Deserialize, Serialize}; +pub mod common; + +use common::deploy::maybe_deploy; +use common::testing::maybe_testing; +use ffs_environment::env_short_default; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + /// The ETH connection configuration. + /// This is mandatory for all possible operations. + #[serde(default)] + pub eth_connection: common::eth_connection::Config, + + #[serde(default)] + pub settle: common::settlement::Config, + + #[serde(default)] + pub transactions: common::transactions::Config, + + /// Whether or not to attempt to run locally. + #[serde(default = "maybe_run_local")] + pub maybe_run_local: bool, + + /// Optional deployment of contracts config + #[serde(default = "maybe_deploy")] + pub deploy: Option, + + /// Optional testing config + #[serde(default = "maybe_testing")] + pub testing: Option, +} + +env_short_default!(maybe_run_local, bool, false); + +impl Config { + pub fn eth_rpc_connection_url(&self) -> String { + self.eth_connection.eth_rpc_connection_url() + } + + pub fn eth_ws_connection_url(&self) -> String { + self.eth_connection.eth_ws_connection_url() + } + + pub fn should_settle(&self) -> bool { + self.settle.should_settle + } + + pub fn should_run_local(&self) -> bool { + self.maybe_run_local + } +} + +impl Default for Config { + fn default() -> Self { + Config { + eth_connection: common::eth_connection::Config::default(), + settle: common::settlement::Config::default(), + transactions: common::transactions::Config::default(), + maybe_run_local: maybe_run_local(), + deploy: maybe_deploy(), + testing: maybe_testing(), + } + } +} diff --git a/protocol/pcp/util/types/Cargo.toml b/protocol/pcp/util/types/Cargo.toml new file mode 100644 index 00000000..ff7daf17 --- /dev/null +++ b/protocol/pcp/util/types/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pcp-types" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +authors = { workspace = true } +homepage = { workspace = true } +publish = { workspace = true } +rust-version = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +# [[bin]] +# name = "test-pcp-settlement-client" +# path = "src/bin/e2e/test_client_settlement.rs" + + +[dependencies] +serde = { workspace = true } + +[features] +default = ["eth"] +e2e = ["eth"] +eth = [] +mock = [] + +[lints] +workspace = true diff --git a/protocol/pcp/util/types/src/block_commitment.rs b/protocol/pcp/util/types/src/block_commitment.rs new file mode 100644 index 00000000..05752b51 --- /dev/null +++ b/protocol/pcp/util/types/src/block_commitment.rs @@ -0,0 +1,137 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive( + Serialize, Deserialize, Clone, Copy, Default, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, +)] +pub struct Id([u8; 32]); + +impl Id { + pub fn new(data: [u8; 32]) -> Self { + Self(data) + } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub fn test() -> Self { + Self([0; 32]) + } + + pub fn to_vec(&self) -> Vec { + self.0.into() + } + + pub fn genesis_block() -> Self { + Self([0; 32]) + } +} + +impl AsRef<[u8]> for Id { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Display for Id { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +#[derive( + Serialize, Deserialize, Clone, Copy, Default, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, +)] +pub struct Commitment([u8; 32]); + +impl Commitment { + pub fn new(data: [u8; 32]) -> Self { + Self(data) + } + + pub fn test() -> Self { + Self([0; 32]) + } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +impl fmt::Display for Commitment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +impl From for [u8; 32] { + fn from(commitment: Commitment) -> [u8; 32] { + commitment.0 + } +} + +impl From for Vec { + fn from(commitment: Commitment) -> Vec { + commitment.0.into() + } +} + +#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct SuperBlockCommitment { + height: u64, + block_id: Id, + commitment: Commitment, +} + +impl SuperBlockCommitment { + pub fn new(height: u64, block_id: Id, commitment: Commitment) -> Self { + Self { height, block_id, commitment } + } + + pub fn height(&self) -> u64 { + self.height + } + + pub fn block_id(&self) -> &Id { + &self.block_id + } + + pub fn commitment(&self) -> Commitment { + self.commitment + } + + pub fn test() -> Self { + Self::new(0, Id::test(), Commitment::test()) + } +} + +impl fmt::Display for SuperBlockCommitment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SuperBlockCommitment {{ height: {}, block_id: {}, commitment: {} }}", + self.height, self.block_id, self.commitment + ) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum SuperBlockCommitmentRejectionReason { + InvalidBlockId, + InvalidCommitment, + InvalidHeight, + ContractError, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum SuperBlockCommitmentEvent { + Accepted(SuperBlockCommitment), + Rejected { height: u64, reason: SuperBlockCommitmentRejectionReason }, +} diff --git a/protocol/pcp/util/types/src/lib.rs b/protocol/pcp/util/types/src/lib.rs new file mode 100644 index 00000000..b9091a97 --- /dev/null +++ b/protocol/pcp/util/types/src/lib.rs @@ -0,0 +1 @@ +pub mod block_commitment;