diff --git a/Cargo.lock b/Cargo.lock index 1e2831f7..0668c9c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2498,6 +2498,7 @@ dependencies = [ "base64 0.22.1", "bincode 2.0.0-rc.3", "blst", + "bonsai-runner", "borsh", "bytes", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 2d801e2e..57bf6bc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ hyle-contracts = { path = "./contracts", package = "hyle-contracts" } hydentity = { path = "./contracts/hydentity" } hyllar = { path = "./contracts/hyllar" } staking = { path = "./contracts/staking" } +bonsai-runner = { path = "./crates/bonsai-runner" } config = "0.15.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["json"] } diff --git a/contract-sdk/src/guest.rs b/contract-sdk/src/guest.rs index b6d090fd..020ce5bf 100644 --- a/contract-sdk/src/guest.rs +++ b/contract-sdk/src/guest.rs @@ -4,8 +4,9 @@ use bincode::{Decode, Encode}; use serde::de::DeserializeOwned; use crate::{ - flatten_blobs, Blob, BlobIndex, ContractInput, Digestable, HyleOutput, Identity, - StructuredBlob, StructuredBlobData, + flatten_blobs, + utils::{parse_blob, parse_structured_blob}, + ContractInput, Digestable, HyleOutput, Identity, StructuredBlob, StructuredBlobData, }; #[cfg(feature = "risc0")] @@ -95,44 +96,6 @@ where Ok((input, parsed_blob, caller)) } -pub fn parse_blob(blobs: &[Blob], index: &BlobIndex) -> Parameters -where - Parameters: Decode, -{ - let blob = match blobs.get(index.0) { - Some(v) => v, - None => { - panic!("unable to find the payload"); - } - }; - - let (parameters, _) = - bincode::decode_from_slice(blob.data.0.as_slice(), bincode::config::standard()) - .expect("Failed to decode payload"); - parameters -} - -pub fn parse_structured_blob( - blobs: &[Blob], - index: &BlobIndex, -) -> StructuredBlob -where - Parameters: Decode, -{ - let blob = match blobs.get(index.0) { - Some(v) => v, - None => { - panic!("unable to find the payload"); - } - }; - - let parsed_blob: StructuredBlob = StructuredBlob::try_from(blob.clone()) - .unwrap_or_else(|e| { - panic!("Failed to decode blob: {:?}", e); - }); - parsed_blob -} - pub fn commit(input: ContractInput, new_state: State, res: crate::RunResult) where State: Digestable, diff --git a/contract-sdk/src/lib.rs b/contract-sdk/src/lib.rs index 8721d005..be27a4a0 100644 --- a/contract-sdk/src/lib.rs +++ b/contract-sdk/src/lib.rs @@ -14,6 +14,7 @@ pub mod erc20; #[cfg(any(feature = "risc0", feature = "sp1"))] pub mod guest; pub mod identity_provider; +pub mod utils; #[cfg(feature = "tracing")] pub use tracing; diff --git a/contract-sdk/src/utils.rs b/contract-sdk/src/utils.rs new file mode 100644 index 00000000..e9d7432e --- /dev/null +++ b/contract-sdk/src/utils.rs @@ -0,0 +1,41 @@ +use bincode::Decode; + +use crate::{Blob, BlobIndex, StructuredBlob}; + +pub fn parse_blob(blobs: &[Blob], index: &BlobIndex) -> Parameters +where + Parameters: Decode, +{ + let blob = match blobs.get(index.0) { + Some(v) => v, + None => { + panic!("unable to find the payload"); + } + }; + + let (parameters, _) = + bincode::decode_from_slice(blob.data.0.as_slice(), bincode::config::standard()) + .expect("Failed to decode payload"); + parameters +} + +pub fn parse_structured_blob( + blobs: &[Blob], + index: &BlobIndex, +) -> StructuredBlob +where + Parameters: Decode, +{ + let blob = match blobs.get(index.0) { + Some(v) => v, + None => { + panic!("unable to find the payload"); + } + }; + + let parsed_blob: StructuredBlob = StructuredBlob::try_from(blob.clone()) + .unwrap_or_else(|e| { + panic!("Failed to decode blob: {:?}", e); + }); + parsed_blob +} diff --git a/contracts/amm/amm.img b/contracts/amm/amm.img index 6742e915..0f8cd59e 100644 Binary files a/contracts/amm/amm.img and b/contracts/amm/amm.img differ diff --git a/contracts/amm/amm.txt b/contracts/amm/amm.txt index 822f70d5..977fac1a 100644 --- a/contracts/amm/amm.txt +++ b/contracts/amm/amm.txt @@ -1 +1 @@ -d10937cad6d5d0b18bd99a22f62c0f52fd591e4c18fd6ba7ba98a5e1088a219b \ No newline at end of file +5dcae35e603495a426fa821a0d15fe43268a99addead70b87057078c925578e7 \ No newline at end of file diff --git a/contracts/amm/src/main.rs b/contracts/amm/src/main.rs index e4bff162..e6d48dbd 100644 --- a/contracts/amm/src/main.rs +++ b/contracts/amm/src/main.rs @@ -29,12 +29,12 @@ fn main() { }; } - let execution_state = ExecutionContext { + let execution_ctx = ExecutionContext { callees_blobs: callees_blobs.into(), caller, }; let amm_state = input.initial_state.clone(); - let mut amm_contract = AmmContract::new(execution_state, parsed_blob.contract_name, amm_state); + let mut amm_contract = AmmContract::new(execution_ctx, parsed_blob.contract_name, amm_state); let amm_action = parsed_blob.data.parameters; diff --git a/contracts/hydentity/hydentity.img b/contracts/hydentity/hydentity.img index 23f7714b..3da58653 100644 Binary files a/contracts/hydentity/hydentity.img and b/contracts/hydentity/hydentity.img differ diff --git a/contracts/hydentity/hydentity.txt b/contracts/hydentity/hydentity.txt index 45069f7e..98ec7fa7 100644 --- a/contracts/hydentity/hydentity.txt +++ b/contracts/hydentity/hydentity.txt @@ -1 +1 @@ -a97a8642215dafd0402deaa87de330cbeb603301e3b328a76a66526b83fc7ede \ No newline at end of file +8b71902dc20d7e2693645365169506e11cb4eab19e1f8bbbad262cbd60ecd22d \ No newline at end of file diff --git a/contracts/hyllar/hyllar.img b/contracts/hyllar/hyllar.img index 7cfd87e5..d217a57f 100644 Binary files a/contracts/hyllar/hyllar.img and b/contracts/hyllar/hyllar.img differ diff --git a/contracts/hyllar/hyllar.txt b/contracts/hyllar/hyllar.txt index 1a5d7c11..adfb07fb 100644 --- a/contracts/hyllar/hyllar.txt +++ b/contracts/hyllar/hyllar.txt @@ -1 +1 @@ -e085fa46f2e62d69897fc77f379c0ba1d252d7285f84dbcc017957567d1e812f \ No newline at end of file +9cdb9039b39c5799c41629d736d69062eea31d06586c64e097d69714a908575b \ No newline at end of file diff --git a/contracts/staking/src/lib.rs b/contracts/staking/src/lib.rs index e05529a6..e2782bcf 100644 --- a/contracts/staking/src/lib.rs +++ b/contracts/staking/src/lib.rs @@ -1,207 +1,90 @@ -use core::fmt; - use anyhow::Result; use bincode::{Decode, Encode}; -use model::ValidatorPublicKey; -use sdk::{info, Blob, BlobData, ContractName, Digestable}; +use model::{BlockHeight, ValidatorPublicKey}; +use sdk::{ + caller::{CalleeBlobs, CallerCallee, ExecutionContext, MutCalleeBlobs}, + Blob, BlobData, BlobIndex, ContractName, Identity, StructuredBlobData, +}; use serde::{Deserialize, Serialize}; +use state::OnChainState; pub mod model; +pub mod state; + #[cfg(feature = "metadata")] pub mod metadata { pub const STAKING_ELF: &[u8] = include_bytes!("../staking.img"); pub const PROGRAM_ID: [u8; 32] = sdk::str_to_u8(include_str!("../staking.txt")); } +#[derive(Encode, Decode, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct RewardsClaim { + block_heights: Vec, +} + /// Enum representing the actions that can be performed by the IdentityVerification contract. -#[derive(Encode, Decode, Debug, Clone)] +#[derive(Encode, Decode, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub enum StakingAction { Stake { amount: u128 }, Delegate { validator: ValidatorPublicKey }, + Distribute { claim: RewardsClaim }, } impl StakingAction { - pub fn as_blob(self, contract_name: ContractName) -> Blob { + pub fn as_blob( + self, + contract_name: ContractName, + caller: Option, + callees: Option>, + ) -> Blob { Blob { contract_name, - data: BlobData( - bincode::encode_to_vec(self, bincode::config::standard()) - .expect("failed to encode program inputs"), - ), + data: BlobData::from(StructuredBlobData { + caller, + callees, + parameters: self.clone(), + }), } } } -#[derive(Debug, Encode, Decode, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct Staker { - pub pubkey: ValidatorPublicKey, - pub stake: Stake, +pub struct StakingContract { + exec_ctx: ExecutionContext, + state: state::Staking, } -impl fmt::Display for Staker { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?} with stake {:?}", self.pubkey, self.stake.amount) +impl CallerCallee for StakingContract { + fn caller(&self) -> &Identity { + &self.exec_ctx.caller } -} - -#[derive(Debug, Encode, Decode, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct Stake { - pub amount: u64, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Encode, Decode, PartialEq, Eq, Hash)] -pub struct Staking { - stakers: Vec, - bonded: Vec, - total_bond: u64, -} - -/// Minimal stake necessary to be part of consensus -pub const MIN_STAKE: u64 = 32; - -impl Staking { - pub fn new() -> Self { - Staking { - stakers: Default::default(), - bonded: Default::default(), - total_bond: 0, - } - } - - /// Add a staking validator - pub fn add_staker(&mut self, staker: Staker) -> Result<(), String> { - if self.stakers.iter().any(|s| s.pubkey == staker.pubkey) { - return Err("Validator already staking".to_string()); - } - info!("πŸ’° New staker {}", staker); - self.stakers.push(staker); - Ok(()) - } - - /// list bonded validators - pub fn bonded(&self) -> &Vec { - &self.bonded + fn callee_blobs(&self) -> CalleeBlobs { + CalleeBlobs(self.exec_ctx.callees_blobs.borrow()) } - - /// Unbond a staking validator - pub fn unbond(&mut self, validator: &ValidatorPublicKey) -> Result { - if let Some(staker) = self.stakers.iter().find(|s| &s.pubkey == validator) { - if !self.bonded.contains(validator) { - return Err("Validator already unbonded".to_string()); - } - self.bonded.retain(|v| v != validator); - self.total_bond -= staker.stake.amount; - Ok(staker.stake) - } else { - Err("Validator not staking".to_string()) - } - } - - /// Get the total bonded amount - pub fn total_bond(&self) -> u64 { - self.total_bond - } - - /// Compute f value - pub fn compute_f(&self) -> u64 { - self.total_bond().div_ceil(3) - } - - pub fn compute_voting_power(&self, validators: &[ValidatorPublicKey]) -> u64 { - validators - .iter() - .flat_map(|v| self.get_stake(v).map(|s| s.amount)) - .sum::() + fn mut_callee_blobs(&self) -> MutCalleeBlobs { + MutCalleeBlobs(self.exec_ctx.callees_blobs.borrow_mut()) } +} - /// Get the stake for a validator - pub fn get_stake(&self, validator: &ValidatorPublicKey) -> Option { - self.stakers - .iter() - .find(|s| &s.pubkey == validator) - .map(|s| s.stake) +impl StakingContract { + pub fn new(exec_ctx: ExecutionContext, state: state::Staking) -> Self { + StakingContract { exec_ctx, state } } - /// Bond a staking validator - pub fn bond(&mut self, validator: ValidatorPublicKey) -> Result { - info!("πŸ” Bonded validator {}", validator); - if let Some(staker) = self.stakers.iter().find(|s| s.pubkey == validator) { - if self.bonded.contains(&validator) { - return Err("Validator already bonded".to_string()); + pub fn execute_action(&mut self, action: StakingAction) -> Result { + match action { + StakingAction::Stake { amount } => self.state.stake(self.caller().clone(), amount), + StakingAction::Delegate { validator } => { + self.state.delegate_to(self.caller().clone(), validator) } - self.bonded.push(validator); - self.bonded.sort(); // TODO insert in order? - self.total_bond += staker.stake.amount; - Ok(staker.stake) - } else { - Err("Validator not staking".to_string()) + StakingAction::Distribute { claim: _ } => todo!(), } } - pub fn is_bonded(&self, pubkey: &ValidatorPublicKey) -> bool { - self.bonded.iter().any(|v| v == pubkey) - } -} - -impl Default for Staking { - fn default() -> Self { - Self::new() - } -} - -impl Digestable for Staking { - fn as_digest(&self) -> sdk::StateDigest { - sdk::StateDigest( - bincode::encode_to_vec(self, bincode::config::standard()) - .expect("Failed to encode Balances"), - ) - } -} - -impl TryFrom for Staking { - type Error = anyhow::Error; - - fn try_from(state: sdk::StateDigest) -> Result { - let (balances, _) = bincode::decode_from_slice(&state.0, bincode::config::standard()) - .map_err(|_| anyhow::anyhow!("Could not decode start height"))?; - Ok(balances) - } -} - -#[cfg(test)] -mod test { - - use super::*; - #[test] - fn test_staking() { - let mut staking = Staking::new(); - let staker = Staker { - pubkey: ValidatorPublicKey::default(), - stake: Stake { amount: 100 }, - }; - assert!(staking.add_staker(staker.clone()).is_ok()); - assert_eq!(staking.total_bond(), 0); - let stake = staking.bond(staker.pubkey.clone()).unwrap(); - assert_eq!(staking.total_bond(), 100); - assert_eq!(stake.amount, 100); - let stake = staking.unbond(&staker.pubkey).unwrap(); - assert_eq!(staking.total_bond(), 0); - assert_eq!(stake.amount, 100); + pub fn on_chain_state(&self) -> OnChainState { + self.state.on_chain_state() } - #[test] - fn test_errors() { - let mut staking = Staking::new(); - let staker = Staker { - pubkey: ValidatorPublicKey::default(), - stake: Stake { amount: 100 }, - }; - assert!(staking.bond(staker.pubkey.clone()).is_err()); - assert!(staking.unbond(&staker.pubkey).is_err()); - assert!(staking.add_staker(staker.clone()).is_ok()); - assert!(staking.bond(staker.pubkey.clone()).is_ok()); - assert!(staking.bond(staker.pubkey.clone()).is_err()); - assert!(staking.unbond(&staker.pubkey).is_ok()); - assert!(staking.unbond(&staker.pubkey).is_err()); + pub fn state(self) -> state::Staking { + self.state } } diff --git a/contracts/staking/src/main.rs b/contracts/staking/src/main.rs index b20515ef..5a53e9ce 100644 --- a/contracts/staking/src/main.rs +++ b/contracts/staking/src/main.rs @@ -3,6 +3,57 @@ extern crate alloc; +use alloc::format; +use alloc::vec::Vec; +use sdk::caller::{CallerCallee, ExecutionContext}; +use sdk::{info, StructuredBlobData}; +use staking::state::OnChainState; +use staking::{state::Staking, StakingAction, StakingContract}; risc0_zkvm::guest::entry!(main); -fn main() {} +fn main() { + let (input, parsed_blob, caller) = + match sdk::guest::init_with_caller::() { + Ok(res) => res, + Err(err) => { + panic!("Staking contract initialization failed {}", err); + } + }; + + // TODO: refactor this into ExecutionContext + let mut callees_blobs = Vec::new(); + for blob in input.blobs.clone().into_iter() { + if let Ok(structured_blob) = blob.data.clone().try_into() { + let structured_blob: StructuredBlobData> = structured_blob; // for type inference + if structured_blob.caller == Some(input.index.clone()) { + callees_blobs.push(blob); + } + }; + } + + let (state, _): (Staking, _) = + bincode::decode_from_slice(input.private_blob.0.as_slice(), bincode::config::standard()) + .expect("Failed to decode payload"); + + info!("state: {:?}", state); + info!("computed:: {:?}", state.on_chain_state()); + info!("given: {:?}", input.initial_state); + if state.on_chain_state() != input.initial_state { + panic!("State mismatch"); + } + + let ctx = ExecutionContext { + callees_blobs: callees_blobs.into(), + caller, + }; + let mut contract = StakingContract::new(ctx, state); + + let action = parsed_blob.data.parameters; + + let res = contract.execute_action(action); + + assert!(contract.callee_blobs().is_empty()); + + sdk::guest::commit(input, contract.on_chain_state(), res); + info!("state: {:?}", contract.state()); +} diff --git a/contracts/staking/src/state.rs b/contracts/staking/src/state.rs new file mode 100644 index 00000000..50c881c9 --- /dev/null +++ b/contracts/staking/src/state.rs @@ -0,0 +1,175 @@ +use std::collections::BTreeMap; + +use super::model::{BlockHeight, ValidatorPublicKey}; +use anyhow::Result; +use bincode::{Decode, Encode}; +use sdk::{info, Digestable, Identity}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +#[derive(Debug, Serialize, Deserialize, Clone, Encode, Decode, PartialEq, Eq)] +pub struct OnChainState(pub String); + +#[derive(Debug, Serialize, Deserialize, Clone, Encode, Decode, PartialEq, Eq)] +pub struct Staking { + stakes: BTreeMap, + delegations: BTreeMap>, + /// When a validator distribute rewards, it is added in this list to + /// avoid distributing twice the rewards for a same block + rewarded: BTreeMap>, + + /// List of validators that are part of consensus + bonded: Vec, + total_bond: u128, +} + +/// Minimal stake necessary to be part of consensus +pub const MIN_STAKE: u128 = 32; + +impl Staking { + pub fn new() -> Self { + Staking { + stakes: BTreeMap::new(), + delegations: BTreeMap::new(), + rewarded: BTreeMap::new(), + bonded: Vec::new(), + total_bond: 0, + } + } + /// On-chain state is a hash of parts of the state that are altered only + /// by BlobTransactions + /// Other parts of the states (handled by consensus) are not part of on-chain state + pub fn on_chain_state(&self) -> OnChainState { + let mut hasher = Sha256::new(); + for s in self.stakes.iter() { + hasher.update(&s.0 .0); + hasher.update(s.1.to_le_bytes()); + } + for d in self.delegations.iter() { + hasher.update(&d.0 .0); + for i in d.1 { + hasher.update(&i.0); + } + } + for r in self.rewarded.iter() { + hasher.update(&r.0 .0); + for i in r.1 { + hasher.update(i.0.to_le_bytes()); + } + } + OnChainState(format!("{:x}", hasher.finalize())) + } + + pub fn bonded(&self) -> &Vec { + &self.bonded + } + /// Get the total bonded amount + pub fn total_bond(&self) -> u128 { + self.total_bond + } + pub fn is_bonded(&self, pubkey: &ValidatorPublicKey) -> bool { + self.bonded.iter().any(|v| v == pubkey) + } + + /// Bond a staking validator + pub fn bond(&mut self, validator: ValidatorPublicKey) -> Result<(), String> { + if self.is_bonded(&validator) { + return Err("Validator already bonded".to_string()); + } + + info!("πŸ” Bonded validator {}", validator); + if let Some(stake) = self.get_stake(&validator) { + if stake < MIN_STAKE { + return Err("Validator does not have enough stake".to_string()); + } + self.bonded.push(validator); + self.bonded.sort(); // TODO insert in order? + self.total_bond += stake; + Ok(()) + } else { + Err("Validator does not have enough stake".to_string()) + } + } + + /// Compute f value + pub fn compute_f(&self) -> u128 { + self.total_bond().div_ceil(3) + } + + pub fn compute_voting_power(&self, validators: &[ValidatorPublicKey]) -> u128 { + validators + .iter() + .flat_map(|v| self.get_stake(v)) + .sum::() + } + + pub fn get_stake(&self, validator: &ValidatorPublicKey) -> Option { + self.delegations.get(validator).map(|delegations| { + delegations + .iter() + .map(|delegator| self.stakes.get(delegator).unwrap_or(&0)) + .sum() + }) + } + + pub fn stake(&mut self, staker: Identity, amount: u128) -> Result { + info!("πŸ’° Adding {} to stake for {}", amount, staker); + self.stakes + .entry(staker) + .and_modify(|e| *e += amount) + .or_insert(amount); + Ok("Staked".to_string()) + } + + /// Delegate to a validator, or fail if already delegated to another validator + pub fn delegate_to( + &mut self, + staker: Identity, + validator: ValidatorPublicKey, + ) -> Result { + info!("🀝 New delegation from {} to {}", staker, validator); + if self.delegations.values().flatten().any(|v| v == &staker) { + return Err("Already delegated".to_string()); + } + + self.delegations + .entry(validator) + .and_modify(|e| e.push(staker.clone())) + .or_insert_with(|| vec![staker]); + Ok("Delegated".to_string()) + } +} + +impl Default for Staking { + fn default() -> Self { + Self::new() + } +} + +impl Digestable for OnChainState { + fn as_digest(&self) -> sdk::StateDigest { + sdk::StateDigest( + bincode::encode_to_vec(self, bincode::config::standard()) + .expect("Failed to encode Balances"), + ) + } +} + +impl Digestable for Staking { + fn as_digest(&self) -> sdk::StateDigest { + sdk::StateDigest( + bincode::encode_to_vec(self, bincode::config::standard()) + .expect("Failed to encode Balances"), + ) + } +} + +impl TryFrom for OnChainState { + type Error = anyhow::Error; + + fn try_from(state: sdk::StateDigest) -> Result { + let (state, _) = bincode::decode_from_slice(&state.0, bincode::config::standard()) + .map_err(|_| anyhow::anyhow!("Could not decode start height"))?; + Ok(state) + } +} diff --git a/contracts/staking/staking.img b/contracts/staking/staking.img index 7c0598e2..d42f1635 100644 Binary files a/contracts/staking/staking.img and b/contracts/staking/staking.img differ diff --git a/contracts/staking/staking.txt b/contracts/staking/staking.txt index 224af1cf..01278b46 100644 --- a/contracts/staking/staking.txt +++ b/contracts/staking/staking.txt @@ -1 +1 @@ -e3b4e07a8518e45fcf3c8c95d9c994343fcc3fad07ef028db1aa29136d3cdd64 \ No newline at end of file +ead121c652495c0f21f51cb50acc5ff992d3e6b256a5a1eb4905ae67ac37bd81 \ No newline at end of file diff --git a/src/bin/indexer.rs b/src/bin/indexer.rs index 7c079930..346b1f5e 100644 --- a/src/bin/indexer.rs +++ b/src/bin/indexer.rs @@ -56,7 +56,7 @@ async fn main() -> Result<()> { "node" => TracingMode::NodeName, _ => TracingMode::Full, }, - config.id.clone(), + format!("{}(nopkey)", config.id.clone(),), )?; info!("Starting indexer with config: {:?}", &config); diff --git a/src/bin/node.rs b/src/bin/node.rs index b7a1da94..b868ca52 100644 --- a/src/bin/node.rs +++ b/src/bin/node.rs @@ -25,7 +25,7 @@ use std::{ sync::{Arc, Mutex}, time::Duration, }; -use tracing::{debug, error, info}; +use tracing::{error, info}; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -81,8 +81,6 @@ async fn main() -> Result<()> { info!("Starting node with config: {:?}", &config); - debug!("server mode"); - // Init global metrics meter we expose as an endpoint let metrics_layer = HttpMetricsLayerBuilder::new() .with_service_name(config.id.clone()) diff --git a/src/consensus.rs b/src/consensus.rs index 3e831738..565ab3ea 100644 --- a/src/consensus.rs +++ b/src/consensus.rs @@ -29,7 +29,8 @@ use metrics::ConsensusMetrics; use role_follower::{FollowerRole, FollowerState, TimeoutState}; use role_leader::{LeaderRole, LeaderState}; use serde::{Deserialize, Serialize}; -use staking::{Staking, MIN_STAKE}; +use staking::state::{Staking, MIN_STAKE}; +use staking::StakingAction; use std::time::Duration; use std::{collections::HashMap, default::Default, path::PathBuf}; use tokio::time::interval; @@ -71,6 +72,9 @@ pub enum ConsensusEvent { #[derive(Clone)] pub struct QueryConsensusInfo {} +#[derive(Clone)] +pub struct QueryConsensusStakingState {} + #[derive(Clone, Serialize, Deserialize, Debug)] pub struct ConsensusInfo { pub slot: Slot, @@ -96,6 +100,7 @@ struct ConsensusBusClient { receiver(SignedByValidator), receiver(PeerEvent), receiver(Query), + receiver(Query), } } @@ -342,7 +347,7 @@ impl Consensus { .staking .get_stake(&new_validator.pubkey) { - if stake.amount < staking::MIN_STAKE { + if stake < staking::state::MIN_STAKE { bail!("New bonded validator has not enough stake to be bonded"); } } else { @@ -416,14 +421,14 @@ impl Consensus { } } - fn get_own_voting_power(&self) -> u64 { + fn get_own_voting_power(&self) -> u128 { if self.is_part_of_consensus(self.crypto.validator_pubkey()) { - if let Some(my_sake) = self + if let Some(my_stake) = self .bft_round_state .staking .get_stake(self.crypto.validator_pubkey()) { - my_sake.amount + my_stake } else { panic!("I'm not in my own staking registry !") } @@ -657,7 +662,6 @@ impl Consensus { .bft_round_state .staking .get_stake(self.crypto.validator_pubkey()) - .map(|s| s.amount) .unwrap_or(0) > MIN_STAKE { @@ -730,7 +734,7 @@ impl Consensus { // Verify that the candidate has enough stake if let Some(stake) = self.bft_round_state.staking.get_stake(&candidacy.pubkey) { - if stake.amount < staking::MIN_STAKE { + if stake < staking::state::MIN_STAKE { bail!("πŸ›‘ Candidate validator does not have enough stake to be part of consensus"); } } else { @@ -749,19 +753,31 @@ impl Consensus { match msg { DataEvent::NewBlock(block) => { let block_total_tx = block.total_txs(); - for staker in block.stakers { - self.store - .bft_round_state - .staking - .add_staker(staker) - .map_err(|e| anyhow!(e))?; + for action in block.staking_actions { + match action { + (identity, StakingAction::Stake { amount }) => { + self.store + .bft_round_state + .staking + .stake(identity, amount) + .map_err(|e| anyhow!(e))?; + } + (identity, StakingAction::Delegate { validator }) => { + self.store + .bft_round_state + .staking + .delegate_to(identity, validator) + .map_err(|e| anyhow!(e))?; + } + (_identity, StakingAction::Distribute { claim: _ }) => todo!(), + } } for validator in block.new_bounded_validators.iter() { self.store .bft_round_state .staking .bond(validator.clone()) - .map_err(|e| anyhow!(e))?; + .ok(); } if let StateTag::Joining = self.bft_round_state.state_tag { @@ -922,6 +938,9 @@ impl Consensus { let validators = self.bft_round_state.staking.bonded().clone(); Ok(ConsensusInfo { slot, view, round_leader, validators }) } + command_response _ => { + Ok(self.bft_round_state.staking.clone()) + } _ = timeout_ticker.tick() => { self.bus.send(ConsensusCommand::TimeoutTick) .log_error("Cannot send message over channel")?; @@ -977,8 +996,6 @@ pub mod test { utils::{conf::Conf, crypto}, }; use assertables::assert_contains; - use staking::Stake; - use staking::Staker; use tokio::sync::broadcast::Receiver; use tracing::error; @@ -1147,11 +1164,15 @@ pub mod test { self.consensus .bft_round_state .staking - .add_staker(Staker { - pubkey: pubkey.clone(), - stake: Stake { amount: 100 }, - }) - .expect("cannot add trusted staker"); + .stake(hex::encode(pubkey.0.clone()).into(), 100) + .unwrap(); + + self.consensus + .bft_round_state + .staking + .delegate_to(hex::encode(pubkey.0.clone()).into(), pubkey.clone()) + .unwrap(); + self.consensus .bft_round_state .staking @@ -1190,21 +1211,26 @@ pub mod test { err } - async fn add_staker(&mut self, staker: &Self, amount: u64, err: &str) { + async fn add_staker(&mut self, staker: &Self, amount: u128, err: &str) { info!("βž• {} Add staker: {:?}", self.name, staker.name); self.consensus .handle_data_event(DataEvent::NewBlock(Box::new(Block { - stakers: vec![Staker { - pubkey: staker.pubkey(), - stake: Stake { amount }, - }], + staking_actions: vec![ + (staker.name.clone().into(), StakingAction::Stake { amount }), + ( + staker.name.clone().into(), + StakingAction::Delegate { + validator: staker.pubkey(), + }, + ), + ], ..Default::default() }))) .await - .expect(err) + .expect(err); } - async fn add_bonded_staker(&mut self, staker: &Self, amount: u64, err: &str) { + async fn add_bonded_staker(&mut self, staker: &Self, amount: u128, err: &str) { self.add_staker(staker, amount, err).await; self.consensus .handle_data_event(DataEvent::NewBlock(Box::new(Block { @@ -1215,13 +1241,18 @@ pub mod test { .expect(err) } - async fn with_stake(&mut self, amount: u64, err: &str) { + async fn with_stake(&mut self, amount: u128, err: &str) { self.consensus .handle_data_event(DataEvent::NewBlock(Box::new(Block { - stakers: vec![Staker { - pubkey: self.consensus.crypto.validator_pubkey().clone(), - stake: Stake { amount }, - }], + staking_actions: vec![ + (self.name.clone().into(), StakingAction::Stake { amount }), + ( + self.name.clone().into(), + StakingAction::Delegate { + validator: self.consensus.crypto.validator_pubkey().clone(), + }, + ), + ], ..Default::default() }))) .await diff --git a/src/consensus/api.rs b/src/consensus/api.rs index 694ffd29..cb901464 100644 --- a/src/consensus/api.rs +++ b/src/consensus/api.rs @@ -3,6 +3,7 @@ use axum::{ debug_handler, extract::State, http::StatusCode, response::IntoResponse, routing::get, Json, Router, }; +use staking::state::Staking; use tracing::error; use crate::{ @@ -15,11 +16,12 @@ use crate::{ rest::AppError, }; -use super::{ConsensusInfo, QueryConsensusInfo}; +use super::{ConsensusInfo, QueryConsensusInfo, QueryConsensusStakingState}; bus_client! { struct RestBusClient { sender(Query), + sender(Query), } } @@ -34,6 +36,7 @@ pub async fn api(ctx: &CommonRunContext) -> Router<()> { Router::new() .route("/info", get(get_consensus_state)) + .route("/staking_state", get(get_consensus_staking_state)) .with_state(state) } @@ -54,6 +57,23 @@ pub async fn get_consensus_state( } } +#[debug_handler] +pub async fn get_consensus_staking_state( + State(mut state): State, +) -> Result { + match state.bus.request(QueryConsensusStakingState {}).await { + Ok(staking_state) => Ok(Json(staking_state)), + Err(err) => { + error!("{:?}", err); + + Err(AppError( + StatusCode::INTERNAL_SERVER_ERROR, + anyhow!("Error while getting staking state: {err}"), + )) + } + } +} + impl Clone for RouterState { fn clone(&self) -> Self { use crate::utils::static_type_map::Pick; @@ -64,6 +84,10 @@ impl Clone for RouterState { &self.bus, ) .clone(), + Pick::>>::get( + &self.bus, + ) + .clone(), ) } } diff --git a/src/consensus/role_follower.rs b/src/consensus/role_follower.rs index a1419d2d..d510f73e 100644 --- a/src/consensus/role_follower.rs +++ b/src/consensus/role_follower.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use bincode::{Decode, Encode}; -use tracing::{debug, info, warn}; +use tracing::{debug, info, trace, warn}; use super::{ Consensus, ConsensusNetMessage, ConsensusProposal, ConsensusProposalHash, QuorumCertificate, @@ -481,6 +481,11 @@ impl FollowerRole for Consensus { fn verify_poda(&mut self, consensus_proposal: &ConsensusProposal) -> Result<()> { let f = self.bft_round_state.staking.compute_f(); + trace!( + "verify poda with staking: {:#?}", + self.bft_round_state.staking + ); + let accepted_validators = self.bft_round_state.staking.bonded(); for (validator, data_proposal_hash, poda_sig) in &consensus_proposal.cut { let voting_power = self @@ -496,6 +501,9 @@ impl FollowerRole for Consensus { ); } + trace!("consensus_proposal: {:#?}", consensus_proposal); + trace!("voting_power: {voting_power} < {f} + 1"); + // Verify that DataProposal received enough votes if voting_power < f + 1 { bail!("PoDA for validator {validator} does not have enough validators that signed his DataProposal"); diff --git a/src/consensus/role_leader.rs b/src/consensus/role_leader.rs index aa41a816..117dbe02 100644 --- a/src/consensus/role_leader.rs +++ b/src/consensus/role_leader.rs @@ -9,8 +9,8 @@ use crate::{ }; use anyhow::{anyhow, bail, Result}; use bincode::{Decode, Encode}; -use staking::MIN_STAKE; -use tracing::{debug, error, info, warn}; +use staking::state::MIN_STAKE; +use tracing::{debug, error, info, trace, warn}; use super::{Consensus, ConsensusNetMessage, ConsensusProposalHash, Ticket}; @@ -71,7 +71,6 @@ impl LeaderRole for Consensus { self.bft_round_state .staking .get_stake(&v.pubkey) - .map(|s| s.amount) .unwrap_or(0) > MIN_STAKE && !self.bft_round_state.staking.is_bonded(&v.pubkey) @@ -86,6 +85,11 @@ impl LeaderRole for Consensus { // Creates ConsensusProposal // Query new cut to Mempool + trace!( + "Querying Mempool for a new cut with Staking: {:#?}", + self.bft_round_state.staking + ); + match self .bus .request(QueryNewCut(self.bft_round_state.staking.clone())) @@ -174,7 +178,7 @@ impl LeaderRole for Consensus { .compute_voting_power(&validated_votes); let voting_power = votes_power + self.get_own_voting_power(); - self.metrics.prepare_votes_gauge(voting_power); + self.metrics.prepare_votes_gauge(voting_power as u64); // TODO risky cast // Waits for at least n-f = 2f+1 matching PrepareVote messages let f = self.bft_round_state.staking.compute_f(); @@ -284,7 +288,7 @@ impl LeaderRole for Consensus { self.bft_round_state.staking.total_bond() ); - self.metrics.confirmed_ack_gauge(voting_power); + self.metrics.confirmed_ack_gauge(voting_power as u64); // TODO risky cast if voting_power > 2 * f { // Get all signatures received and change ValidatorPublicKey for ValidatorPubKey diff --git a/src/data_availability.rs b/src/data_availability.rs index 2dee0091..8ea1d6b4 100644 --- a/src/data_availability.rs +++ b/src/data_availability.rs @@ -276,7 +276,7 @@ impl DataAvailability { match msg { PeerEvent::NewPeer { pubkey, .. } => { if self.asked_last_processed_block.is_none() { - info!("πŸ“‘ Asking for last block from new peer"); + info!("πŸ“‘ Asking for last block from {pubkey}"); self.asked_last_processed_block = Some(pubkey); self.query_last_block(); } @@ -466,7 +466,7 @@ impl DataAvailability { async fn handle_signed_block(&mut self, block: SignedBlock) { // if new block is already handled, ignore it if self.blocks.contains(&block) { - warn!("Block {:?} already exists !", block); + warn!("Block {} {} already exists !", block.height(), block.hash()); return; } // if new block is not the next block in the chain, buffer @@ -510,7 +510,10 @@ impl DataAvailability { // Iterative loop to avoid stack overflows while let Some(first_buffered) = self.buffered_signed_blocks.first() { if first_buffered.parent_hash != last_block_hash { - error!("Buffered block parent hash does not match last block hash"); + error!( + "Buffered block parent hash {} does not match last block hash {}", + first_buffered.parent_hash, last_block_hash + ); break; } @@ -539,15 +542,14 @@ impl DataAvailability { error!("storing block: {}", e); return; } - - debug!("{:?}", block.clone()); - - debug!("txs: {:?}", block.txs()); + trace!("Block {} {}: {:#?}", block.height(), block.hash(), block); info!( - "new block {} with {} txs, last hash = {}", + "new block {} {} with {} txs: {:?}, last hash = {}", block.height(), + block.hash(), block.txs().len(), + block.txs().iter().map(|tx| tx.hash().0).collect::>(), self.blocks .last_block_hash() .unwrap_or(BlockHash("".to_string())) diff --git a/src/data_availability/node_state.rs b/src/data_availability/node_state.rs index 75d6db1c..4c216958 100644 --- a/src/data_availability/node_state.rs +++ b/src/data_availability/node_state.rs @@ -7,10 +7,12 @@ use crate::model::{ }; use anyhow::{bail, Context, Error, Result}; use bincode::{Decode, Encode}; -use hyle_contract_sdk::{HyleOutput, StateDigest, TxHash}; +use hyle_contract_sdk::{ + utils::parse_structured_blob, BlobIndex, HyleOutput, Identity, StateDigest, TxHash, +}; use model::{Contract, Timeouts, UnsettledBlobMetadata, UnsettledBlobTransaction}; use ordered_tx_map::OrderedTxMap; -use staking::Staker; +use staking::StakingAction; use std::collections::{BTreeMap, HashMap}; use tracing::{debug, error, info}; @@ -25,12 +27,14 @@ pub struct NodeState { // This field is public for testing purposes pub contracts: HashMap, unsettled_transactions: OrderedTxMap, + unsettled_staking_actions: HashMap>, } #[derive(Debug)] pub struct HandledProofTxOutput { pub settled_blob_tx_hashes: Vec, pub updated_states: BTreeMap, + pub staking_actions: Vec<(Identity, StakingAction)>, } impl NodeState { @@ -43,16 +47,13 @@ impl NodeState { let mut new_verified_proof_txs: Vec = vec![]; let mut verified_blobs: Vec<(TxHash, hyle_contract_sdk::BlobIndex)> = vec![]; let mut failed_txs: Vec = vec![]; - let mut stakers: Vec = vec![]; + let mut staking_actions: Vec<(Identity, StakingAction)> = vec![]; let mut settled_blob_tx_hashes: Vec = vec![]; let mut updated_states: BTreeMap = BTreeMap::new(); + // Handle all transactions for tx in signed_block.txs().iter() { match &tx.transaction_data { - // FIXME: to remove when we have a real staking smart contract - TransactionData::Stake(staker) => { - stakers.push(staker.clone()); - } TransactionData::Blob(blob_transaction) => { match self.handle_blob_tx(blob_transaction) { Ok(_) => { @@ -90,6 +91,8 @@ impl NodeState { updated_states.extend(proof_tx_output.updated_states); // Keep track of all verified proof txs new_verified_proof_txs.push(tx.clone()); + // Keep track of all stakers + staking_actions.extend(proof_tx_output.staking_actions); } Err(e) => { error!("Failed to handle proof transaction: {:?}", e); @@ -152,7 +155,7 @@ impl NodeState { new_verified_proof_txs, verified_blobs, failed_txs, - stakers, + staking_actions, new_bounded_validators: signed_block .consensus_proposal .new_validators_to_bond @@ -211,7 +214,7 @@ impl NodeState { }) .collect(); - debug!("Add blob transaction to state {:?}", tx); + debug!("Add blob transaction {} to state {:?}", tx.hash(), tx); self.unsettled_transactions.add(UnsettledBlobTransaction { identity: tx.identity.clone(), hash: blob_tx_hash.clone(), @@ -219,6 +222,20 @@ impl NodeState { blobs, }); + for blob in tx.blobs.clone() { + if blob.contract_name.0 != "staking" { + continue; + } + + let staking_action: StakingAction = parse_structured_blob(&[blob], &BlobIndex(0)) + .data + .parameters; + self.unsettled_staking_actions + .entry(blob_tx_hash.clone()) + .or_default() + .push((tx.identity.clone(), staking_action)); + } + // Update timeouts self.timeouts.set(blob_tx_hash, self.current_height + 100); // TODO: Timeout after 100 blocks, make it configurable ! @@ -274,6 +291,7 @@ impl NodeState { return Ok(HandledProofTxOutput { settled_blob_tx_hashes, updated_states, + staking_actions: vec![], }); } @@ -290,6 +308,7 @@ impl NodeState { return Ok(HandledProofTxOutput { settled_blob_tx_hashes, updated_states, + staking_actions: vec![], }); } @@ -305,11 +324,17 @@ impl NodeState { // Clean the unsettled tx from the state self.unsettled_transactions.remove(&settled_blob_tx_hash); - settled_blob_tx_hashes.push(settled_blob_tx_hash); + settled_blob_tx_hashes.push(settled_blob_tx_hash.clone()); + + let staking_actions = self + .unsettled_staking_actions + .remove(&settled_blob_tx_hash) + .unwrap_or_default(); Ok(HandledProofTxOutput { settled_blob_tx_hashes, updated_states, + staking_actions, }) } @@ -335,6 +360,11 @@ impl NodeState { { return (updated_states, true); } + } else { + debug!( + "Initial state mismatch for contract '{}'. Expected: {:?}, got: {:?}", + contract_name, known_initial_state, proof_metadata.initial_state + ); } } (current_states, false) @@ -407,6 +437,7 @@ impl NodeState { let dropped = self.timeouts.drop(height); for tx in dropped.iter() { self.unsettled_transactions.remove(tx); + self.unsettled_staking_actions.remove(tx); } dropped } diff --git a/src/data_availability/node_state/model.rs b/src/data_availability/node_state/model.rs index 47eb1b9b..3d8b3d93 100644 --- a/src/data_availability/node_state/model.rs +++ b/src/data_availability/node_state/model.rs @@ -23,7 +23,6 @@ pub struct UnsettledBlobTransaction { #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Encode, Decode)] pub struct UnsettledBlobMetadata { - // FIXME: Investigate what happens if there is multiple blob for same contract pub contract_name: ContractName, // Each time we receive a proof, we add it to this list pub metadata: Vec, diff --git a/src/genesis.rs b/src/genesis.rs index 6ad92bc3..64216655 100644 --- a/src/genesis.rs +++ b/src/genesis.rs @@ -4,18 +4,18 @@ use crate::{ bus::{bus_client, BusClientSender, BusMessage}, handle_messages, model::{ - RegisterContractTransaction, SharedRunContext, Transaction, TransactionData, - ValidatorPublicKey, + BlobTransaction, Hashable, ProofData, RegisterContractTransaction, SharedRunContext, + Transaction, TransactionData, ValidatorPublicKey, VerifiedProofTransaction, }, p2p::network::PeerEvent, + tools::transactions_builder::{BuildResult, States, TransactionBuilder}, utils::{conf::SharedConf, crypto::SharedBlstCrypto, modules::Module}, }; use anyhow::{bail, Error, Result}; -use hyle_contract_sdk::identity_provider::IdentityVerification; use hyle_contract_sdk::Digestable; +use hyle_contract_sdk::{identity_provider::IdentityVerification, Identity}; use serde::{Deserialize, Serialize}; -use staking::{Stake, Staker}; -use tracing::info; +use tracing::{error, info}; #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] pub enum GenesisEvent { @@ -107,27 +107,13 @@ impl Genesis { let mut initial_validators = self.peer_pubkey.values().cloned().collect::>(); initial_validators.sort(); - info!("Genesis validators {:?}", initial_validators.clone()); - - let stake_txs = self - .peer_pubkey - .iter() - .map(|(k, v)| { - Transaction::wrap(TransactionData::Stake(Staker { - pubkey: v.clone(), - stake: Stake { - amount: *self.config.consensus.genesis_stakers.get(k).unwrap_or(&100), - }, - })) - }) - .collect::>(); - - let contracts_txs = Self::genesis_contracts_txs(); - - let genesis_txs = stake_txs - .into_iter() - .chain(contracts_txs.into_iter()) - .collect(); + let genesis_txs = match self.generate_genesis_txs().await { + Ok(t) => t, + Err(e) => { + error!("🌱 Genesis block generation failed: {:?}", e); + return Err(e); + } + }; // At this point, we can setup the genesis block. _ = self.bus.send(GenesisEvent::GenesisBlock { @@ -138,7 +124,115 @@ impl Genesis { Ok(()) } - pub fn genesis_contracts_txs() -> Vec { + async fn generate_genesis_txs(&self) -> Result> { + let (mut genesis_txs, mut states) = Self::genesis_contracts_txs(); + + let register_txs = self.generate_register_txs(&mut states).await?; + let faucet_txs = self.generate_faucet_txs(&mut states).await?; + let stake_txs = self.generate_stake_txs(&mut states).await?; + + let builders = register_txs + .into_iter() + .chain(faucet_txs.into_iter()) + .chain(stake_txs.into_iter()) + .collect::>(); + + for BuildResult { + identity, + blobs, + outputs, + } in builders + { + // On genesis we don't need an actual zkproof as the txs are not going through data + // dissemnitation. We can create the same VerifiedProofTransaction on each genesis + // validator, and assume it's the same. + + let tx = BlobTransaction { identity, blobs }; + let blob_tx_hash = tx.hash(); + + genesis_txs.push(Transaction::wrap(TransactionData::Blob(tx))); + + for (contract_name, out) in outputs { + genesis_txs.push(Transaction::wrap(TransactionData::VerifiedProof( + VerifiedProofTransaction { + blob_tx_hash: blob_tx_hash.clone(), + contract_name, + proof_hash: ProofData::default().hash(), + hyle_output: out, + proof: None, + }, + ))); + } + } + + Ok(genesis_txs) + } + + pub async fn generate_register_txs(&self, states: &mut States) -> Result> { + // TODO: use an identity provider that checks BLST signature on a pubkey instead of + // hydentity that checks password + // The validator will send the signature for the register transaction in the handshake + // in order to let all genesis validators to create the genesis register + + let mut txs = vec![]; + for peer in self.peer_pubkey.values() { + info!("🌱 Registering identity {peer}"); + + let identity = Identity(format!("{peer}.hydentity")); + let mut transaction = TransactionBuilder::new(identity.clone()); + + transaction.register_identity("password".to_string()); + txs.push(transaction.build(states).await?); + } + + Ok(txs) + } + + pub async fn generate_faucet_txs(&self, states: &mut States) -> Result> { + let genesis_faucet = 100; + + let mut txs = vec![]; + for peer in self.peer_pubkey.values() { + info!("🌱 Fauceting {genesis_faucet} hyllar to {peer}"); + + let identity = Identity("faucet.hydentity".to_string()); + let mut transaction = TransactionBuilder::new(identity.clone()); + + transaction + .verify_identity(&states.hydentity, "password".to_string()) + .await?; + transaction.transfer("hyllar".into(), format!("{peer}.hydentity"), genesis_faucet); + + txs.push(transaction.build(states).await?); + } + + Ok(txs) + } + + pub async fn generate_stake_txs(&self, states: &mut States) -> Result> { + let genesis_stake = 100; + + let mut txs = vec![]; + for peer in self.peer_pubkey.values().cloned() { + info!("🌱 Staking {genesis_stake} hyllar from {peer}"); + + let identity = Identity(format!("{peer}.hydentity").to_string()); + let mut transaction = TransactionBuilder::new(identity.clone()); + + transaction + .verify_identity(&states.hydentity, "password".to_string()) + .await?; + transaction.stake("hyllar".into(), "staking".into(), genesis_stake)?; + transaction.delegate(peer)?; + + txs.push(transaction.build(states).await?); + } + + Ok(txs) + } + + pub fn genesis_contracts_txs() -> (Vec, States) { + let staking_program_id = hyle_contracts::STAKING_ID.to_vec(); let hyllar_program_id = hyle_contracts::HYLLAR_ID.to_vec(); let hydentity_program_id = hyle_contracts::HYDENTITY_ID.to_vec(); @@ -147,39 +241,55 @@ impl Genesis { .register_identity("faucet.hydentity", "password") .unwrap(); - vec![ - Transaction::wrap(TransactionData::RegisterContract( - RegisterContractTransaction { - owner: "hyle".into(), - verifier: "risc0".into(), - program_id: hyllar_program_id.clone().into(), - state_digest: hyllar::HyllarToken::new( - 100_000_000_000, - "faucet.hydentity".to_string(), - ) - .as_digest(), - contract_name: "hyllar".into(), - }, - )), - Transaction::wrap(TransactionData::RegisterContract( - RegisterContractTransaction { - owner: "hyle".into(), - verifier: "risc0".into(), - program_id: hydentity_program_id.into(), - state_digest: hydentity_state.as_digest(), - contract_name: "hydentity".into(), - }, - )), - Transaction::wrap(TransactionData::RegisterContract( - RegisterContractTransaction { - owner: "hyle".into(), - verifier: "risc0".into(), - program_id: hyle_contracts::RISC0_RECURSION_ID.to_vec().into(), - state_digest: hyle_contract_sdk::StateDigest(vec![]), - contract_name: "risc0-recursion".into(), - }, - )), - ] + let staking_state = staking::state::Staking::new(); + + let states = States { + hyllar: hyllar::HyllarToken::new(100_000_000_000, "faucet.hydentity".to_string()), + hydentity: hydentity_state, + staking: staking_state, + }; + + ( + vec![ + Transaction::wrap(TransactionData::RegisterContract( + RegisterContractTransaction { + owner: "hyle".into(), + verifier: "risc0".into(), + program_id: staking_program_id.into(), + state_digest: states.staking.on_chain_state().as_digest(), + contract_name: "staking".into(), + }, + )), + Transaction::wrap(TransactionData::RegisterContract( + RegisterContractTransaction { + owner: "hyle".into(), + verifier: "risc0".into(), + program_id: hyllar_program_id.into(), + state_digest: states.hyllar.as_digest(), + contract_name: "hyllar".into(), + }, + )), + Transaction::wrap(TransactionData::RegisterContract( + RegisterContractTransaction { + owner: "hyle".into(), + verifier: "risc0".into(), + program_id: hydentity_program_id.into(), + state_digest: states.hydentity.as_digest(), + contract_name: "hydentity".into(), + }, + )), + Transaction::wrap(TransactionData::RegisterContract( + RegisterContractTransaction { + owner: "hyle".into(), + verifier: "risc0".into(), + program_id: hyle_contracts::RISC0_RECURSION_ID.to_vec().into(), + state_digest: hyle_contract_sdk::StateDigest(vec![]), + contract_name: "risc0-recursion".into(), + }, + )), + ], + states, + ) } } @@ -353,17 +463,17 @@ mod tests { let (mut genesis, mut bus) = new(config.clone()).await; bus.send(PeerEvent::NewPeer { name: "node-2".into(), - pubkey: ValidatorPublicKey("node-2".into()).clone(), + pubkey: BlstCrypto::new("node-2".into()).validator_pubkey().clone(), }) .expect("send"); bus.send(PeerEvent::NewPeer { name: "node-3".into(), - pubkey: ValidatorPublicKey("node-3".into()).clone(), + pubkey: BlstCrypto::new("node-3".into()).validator_pubkey().clone(), }) .expect("send"); bus.send(PeerEvent::NewPeer { name: "node-4".into(), - pubkey: ValidatorPublicKey("node-4".into()).clone(), + pubkey: BlstCrypto::new("node-4".into()).validator_pubkey().clone(), }) .expect("send"); let _ = genesis.start().await; @@ -373,17 +483,17 @@ mod tests { let (mut genesis, mut bus) = new(config).await; bus.send(PeerEvent::NewPeer { name: "node-4".into(), - pubkey: ValidatorPublicKey("node-4".into()).clone(), + pubkey: BlstCrypto::new("node-4".into()).validator_pubkey().clone(), }) .expect("send"); bus.send(PeerEvent::NewPeer { name: "node-2".into(), - pubkey: ValidatorPublicKey("node-2".into()).clone(), + pubkey: BlstCrypto::new("node-2".into()).validator_pubkey().clone(), }) .expect("send"); bus.send(PeerEvent::NewPeer { name: "node-3".into(), - pubkey: ValidatorPublicKey("node-3".into()).clone(), + pubkey: BlstCrypto::new("node-3".into()).validator_pubkey().clone(), }) .expect("send"); let _ = genesis.start().await; diff --git a/src/indexer.rs b/src/indexer.rs index b38721b2..0d877fdc 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -409,7 +409,7 @@ impl Indexer { } // Handling new stakers - for _staker in block.stakers { + for _staker in block.staking_actions { // TODO: add new table with stakers at a given height } diff --git a/src/indexer/model.rs b/src/indexer/model.rs index 657b8ea9..8f57d47d 100644 --- a/src/indexer/model.rs +++ b/src/indexer/model.rs @@ -34,7 +34,6 @@ impl TransactionType { TransactionData::VerifiedProof(_) => TransactionType::ProofTransaction, TransactionData::VerifiedRecursiveProof(_) => TransactionType::ProofTransaction, TransactionData::RegisterContract(_) => TransactionType::RegisterContractTransaction, - TransactionData::Stake(_) => TransactionType::Stake, } } } diff --git a/src/mempool.rs b/src/mempool.rs index 49fcacf6..ea2a5aba 100644 --- a/src/mempool.rs +++ b/src/mempool.rs @@ -3,7 +3,10 @@ use crate::{ bus::{command_response::Query, BusClientSender, BusMessage}, consensus::{CommittedConsensusProposal, ConsensusEvent}, - data_availability::node_state::verifiers::{verify_proof, verify_recursive_proof}, + data_availability::{ + node_state::verifiers::{verify_proof, verify_recursive_proof}, + DataEvent, + }, genesis::GenesisEvent, mempool::storage::{DataProposal, Storage}, model::{ @@ -25,7 +28,7 @@ use bincode::{Decode, Encode}; use hyle_contract_sdk::{ContractName, ProgramId, Verifier}; use metrics::MempoolMetrics; use serde::{Deserialize, Serialize}; -use staking::Staking; +use staking::state::Staking; use std::{ collections::{HashMap, HashSet, VecDeque}, fmt::Display, @@ -74,6 +77,7 @@ struct MempoolBusClient { receiver(MempoolCommand), receiver(ConsensusEvent), receiver(GenesisEvent), + receiver(DataEvent), receiver(Query), } } @@ -203,6 +207,20 @@ impl Mempool { } } } + listen cmd => { + if let DataEvent::NewBlock(block) = cmd { + for contract in block.new_contract_txs { + let TransactionData::RegisterContract(register_contract_transaction) = contract.transaction_data else { + continue; + }; + self.known_contracts.register_contract( + ®ister_contract_transaction.contract_name, + ®ister_contract_transaction.verifier, + ®ister_contract_transaction.program_id, + ).ok(); + } + } + } command_response validators => { Ok(self.handle_querynewcut(validators)) } @@ -614,20 +632,22 @@ impl Mempool { } fn on_new_tx(&mut self, mut tx: Transaction) -> Result<()> { - debug!("Got new tx {}", tx.hash()); // TODO: Verify fees ? // TODO: Verify identity ? match tx.transaction_data { TransactionData::RegisterContract(ref register_contract_transaction) => { + debug!("Got new register contract tx {}", tx.hash()); + self.known_contracts.register_contract( ®ister_contract_transaction.contract_name, ®ister_contract_transaction.verifier, ®ister_contract_transaction.program_id, )?; } - TransactionData::Stake(ref _staker) => {} - TransactionData::Blob(ref _blob_transaction) => {} + TransactionData::Blob(ref _blob_transaction) => { + debug!("Got new blob tx {}", tx.hash()); + } TransactionData::Proof(proof_transaction) => { // Verify and extract proof let (verifier, program_id) = self @@ -639,11 +659,17 @@ impl Mempool { verify_proof(&proof_transaction.proof.to_bytes()?, verifier, program_id)?; tx.transaction_data = TransactionData::VerifiedProof(VerifiedProofTransaction { proof_hash: proof_transaction.proof.hash(), - contract_name: proof_transaction.contract_name, - blob_tx_hash: proof_transaction.blob_tx_hash, + contract_name: proof_transaction.contract_name.clone(), + blob_tx_hash: proof_transaction.blob_tx_hash.clone(), proof: Some(proof_transaction.proof), hyle_output, }); + debug!( + "Got new proof tx {} for blob tx {}:{}", + tx.hash(), + proof_transaction.blob_tx_hash, + proof_transaction.contract_name + ); } TransactionData::RecursiveProof(proof_transaction) => { // Verify and extract proof @@ -683,6 +709,7 @@ impl Mempool { }) .collect(), }); + debug!("Got new recursive proof tx {}", tx.hash()); } TransactionData::VerifiedProof(_) => { bail!("Already verified ProofTransaction are not allowed to be received in the mempool"); diff --git a/src/mempool/api.rs b/src/mempool/api.rs index 23c1bc9d..3656ec06 100644 --- a/src/mempool/api.rs +++ b/src/mempool/api.rs @@ -12,7 +12,6 @@ use crate::{ }, rest::AppError, }; -use staking::Staker; #[derive(Debug, Serialize, Deserialize, Clone, Encode, Decode)] pub enum RestApiMessage { @@ -37,7 +36,6 @@ pub async fn api(ctx: &CommonRunContext) -> Router<()> { Router::new() .route("/contract/register", post(send_contract_transaction)) - .route("/tx/send/stake", post(send_staking_transaction)) .route("/tx/send/blob", post(send_blob_transaction)) .route("/tx/send/proof", post(send_proof_transaction)) .route( @@ -99,13 +97,6 @@ pub async fn send_recursive_proof_transaction( handle_send(state, TransactionData::RecursiveProof(payload)).await } -pub async fn send_staking_transaction( - State(state): State, - Json(payload): Json, -) -> Result { - handle_send(state, TransactionData::Stake(payload)).await -} - impl Clone for RouterState { fn clone(&self) -> Self { use crate::utils::static_type_map::Pick; diff --git a/src/mempool/storage.rs b/src/mempool/storage.rs index 46772873..88fac6ec 100644 --- a/src/mempool/storage.rs +++ b/src/mempool/storage.rs @@ -3,7 +3,7 @@ use bincode::{Decode, Encode}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use sha3::{Digest, Sha3_256}; -use staking::Staking; +use staking::state::Staking; use std::{collections::HashMap, fmt::Display, hash::Hash, vec}; use tracing::{debug, error, warn}; @@ -612,9 +612,7 @@ impl DataProposal { // A DataProposal that has been processed has turned all TransactionData::Proof into TransactionData::VerifiedProof unreachable!(); } - TransactionData::Blob(_) - | TransactionData::Stake(_) - | TransactionData::RegisterContract(_) => {} + TransactionData::Blob(_) | TransactionData::RegisterContract(_) => {} } }); } @@ -801,7 +799,7 @@ mod tests { utils::crypto, }; use hyle_contract_sdk::{BlobIndex, HyleOutput, Identity, ProgramId, StateDigest, TxHash}; - use staking::{Stake, Staker, Staking}; + use staking::state::Staking; use super::{DataProposal, Lane}; @@ -1586,18 +1584,14 @@ mod tests { let known_contracts1 = KnownContracts::default(); let known_contracts2 = KnownContracts::default(); let mut staking = Staking::default(); + staking.stake("pk1".into(), 100).expect("could not stake"); staking - .add_staker(Staker { - pubkey: pubkey1.clone(), - stake: Stake { amount: 100 }, - }) - .expect("could not stake"); + .delegate_to("pk1".into(), pubkey1.clone()) + .expect("could not delegate"); + staking.stake("pk2".into(), 100).expect("could not stake"); staking - .add_staker(Staker { - pubkey: pubkey2.clone(), - stake: Stake { amount: 100 }, - }) - .expect("could not stake"); + .delegate_to("pk2".into(), pubkey2.clone()) + .expect("could not delegate"); staking .bond(pubkey1.clone()) .expect("Could not bond pubkey1"); @@ -1677,18 +1671,15 @@ mod tests { let mut store1 = Storage::new(pubkey1.clone()); let mut staking = Staking::default(); + + staking.stake("pk1".into(), 100).expect("Staking failed"); staking - .add_staker(Staker { - pubkey: pubkey1.clone(), - stake: Stake { amount: 100 }, - }) - .expect("could not stake"); + .delegate_to("pk1".into(), pubkey1.clone()) + .expect("Delegation failed"); + staking.stake("pk2".into(), 100).expect("Staking failed"); staking - .add_staker(Staker { - pubkey: pubkey2.clone(), - stake: Stake { amount: 100 }, - }) - .expect("could not stake"); + .delegate_to("pk2".into(), pubkey2.clone()) + .expect("Delegation failed"); staking .bond(pubkey1.clone()) diff --git a/src/model.rs b/src/model.rs index 79583c10..258a4f89 100644 --- a/src/model.rs +++ b/src/model.rs @@ -12,6 +12,7 @@ pub use hyle_contract_sdk::{Blob, BlobData, ContractName}; use serde::{Deserialize, Serialize}; use sha3::{Digest, Sha3_256}; use sqlx::{prelude::Type, Postgres}; +use staking::StakingAction; use std::{ cmp::Ordering, collections::BTreeMap, @@ -34,8 +35,6 @@ use crate::{ }, }; -use staking::Staker; - // Re-export pub use staking::model::ValidatorPublicKey; @@ -87,7 +86,6 @@ pub struct Transaction { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Encode, Decode, IntoStaticStr)] pub enum TransactionData { - Stake(Staker), // FIXME: to remove, this is temporary waiting for real staking contract !! Blob(BlobTransaction), Proof(ProofTransaction), VerifiedProof(VerifiedProofTransaction), @@ -264,8 +262,8 @@ pub struct Block { pub new_verified_proof_txs: Vec, pub verified_blobs: Vec<(TxHash, BlobIndex)>, pub failed_txs: Vec, - pub stakers: Vec, pub new_bounded_validators: Vec, + pub staking_actions: Vec<(Identity, StakingAction)>, pub timed_out_tx_hashes: Vec, pub settled_blob_tx_hashes: Vec, pub updated_states: BTreeMap, @@ -277,7 +275,6 @@ impl Block { + self.new_blob_txs.len() + self.new_verified_proof_txs.len() + self.failed_txs.len() - + self.stakers.len() } } @@ -315,9 +312,6 @@ impl Hashable for Block { for tx_f in self.failed_txs.iter() { hasher.update(tx_f.hash().0); } - for staker in self.stakers.iter() { - hasher.update(staker.hash().0); - } for new_bounded_validator in self.new_bounded_validators.iter() { hasher.update(new_bounded_validator.0.as_slice()); } @@ -453,7 +447,6 @@ impl Hashable for SignedBlock { impl Hashable for Transaction { fn hash(&self) -> TxHash { match &self.transaction_data { - TransactionData::Stake(staker) => staker.hash(), TransactionData::Blob(tx) => tx.hash(), TransactionData::Proof(tx) => tx.hash(), TransactionData::RecursiveProof(tx) => tx.hash(), @@ -463,15 +456,6 @@ impl Hashable for Transaction { } } } -impl Hashable for Staker { - fn hash(&self) -> TxHash { - let mut hasher = Sha3_256::new(); - _ = write!(hasher, "{:?}", self.pubkey.0); - _ = write!(hasher, "{}", self.stake.amount); - let hash_bytes = hasher.finalize(); - TxHash(hex::encode(hash_bytes)) - } -} impl Hashable for BlobTransaction { fn hash(&self) -> TxHash { diff --git a/src/rest/client.rs b/src/rest/client.rs index c43ae3f2..907bd581 100644 --- a/src/rest/client.rs +++ b/src/rest/client.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use hyle_contract_sdk::TxHash; use reqwest::{Response, Url}; -use staking::Staker; +use staking::state::Staking; use crate::{ consensus::ConsensusInfo, @@ -71,26 +71,28 @@ impl ApiHttpClient { .context("Sending tx register contract") } - pub async fn send_stake_tx(&self, tx: &Staker) -> Result { + pub async fn get_consensus_info(&self) -> Result { self.reqwest_client - .post(format!("{}v1/tx/send/stake", self.url)) - .body(serde_json::to_string(&tx)?) + .get(format!("{}v1/consensus/info", self.url)) .header("Content-Type", "application/json") .send() .await - .context("Sending tx stake") + .context("getting consensus info")? + .json::() + .await + .context("reading consensus info response") } - pub async fn get_consensus_info(&self) -> Result { + pub async fn get_consensus_staking_state(&self) -> Result { self.reqwest_client - .get(format!("{}v1/consensus/info", self.url)) + .get(format!("{}v1/consensus/staking_state", self.url)) .header("Content-Type", "application/json") .send() .await - .context("getting consensus info")? - .json::() + .context("getting consensus staking state")? + .json::() .await - .context("reading consensus info response") + .context("reading consensus staking state response") } pub async fn get_node_info(&self) -> Result { diff --git a/src/single_node_consensus.rs b/src/single_node_consensus.rs index 90fbff2d..71a66414 100644 --- a/src/single_node_consensus.rs +++ b/src/single_node_consensus.rs @@ -17,7 +17,7 @@ use crate::utils::modules::module_bus_client; use crate::{model::SharedRunContext, utils::modules::Module}; use anyhow::Result; use bincode::{Decode, Encode}; -use staking::{Stake, Staker, Staking}; +use staking::state::Staking; use tracing::warn; module_bus_client! { @@ -91,16 +91,21 @@ impl SingleNodeConsensus { async fn start(&mut self) -> Result<()> { let pubkey = self.crypto.validator_pubkey(); if !self.store.staking.is_bonded(pubkey) { - let _ = self.store.staking.add_staker(Staker { - pubkey: pubkey.clone(), - stake: Stake { amount: 100 }, - }); + self.store + .staking + .stake("single".into(), 100) + .expect("Staking failed"); + self.store + .staking + .delegate_to("single".into(), pubkey.clone()) + .expect("Delegation failed"); + let _ = self.store.staking.bond(pubkey.clone()); } // On peut Query DA pour rΓ©cuperer le dernier block/cut ? if !self.store.has_done_genesis { // This is the genesis - let genesis_txs = Genesis::genesis_contracts_txs(); + let (genesis_txs, _) = Genesis::genesis_contracts_txs(); tracing::info!("Doing genesis"); _ = self.bus.send(GenesisEvent::GenesisBlock { diff --git a/src/tools/contract_runner.rs b/src/tools/contract_runner.rs new file mode 100644 index 00000000..93104240 --- /dev/null +++ b/src/tools/contract_runner.rs @@ -0,0 +1,124 @@ +use crate::{ + indexer::model::ContractDb, + model::{ContractName, ProofData}, + rest::client::ApiHttpClient, +}; +use anyhow::{bail, Error, Result}; +use hyle_contract_sdk::{ + Blob, BlobData, BlobIndex, ContractInput, Digestable, HyleOutput, Identity, StateDigest, +}; +use serde::Serialize; +use tracing::info; + +pub struct ContractRunner { + pub contract_name: ContractName, + binary: &'static [u8], + contract_input: Vec, +} + +impl ContractRunner { + pub async fn new( + contract_name: ContractName, + binary: &'static [u8], + identity: Identity, + private_blob: BlobData, + blobs: Vec, + index: BlobIndex, + initial_state: State, + ) -> Result + where + State: Digestable + Serialize, + { + let contract_input = ContractInput:: { + initial_state, + identity, + tx_hash: "".into(), + private_blob, + blobs, + index, + }; + let contract_input = bonsai_runner::as_input_data(&contract_input)?; + + Ok(Self { + contract_name, + binary, + contract_input, + }) + } + + pub fn execute(&self) -> Result { + info!("Checking transition for {}...", self.contract_name); + + let execute_info = execute(self.binary, &self.contract_input)?; + let output = execute_info.journal.decode::().unwrap(); + if !output.success { + let program_error = std::str::from_utf8(&output.program_outputs).unwrap(); + bail!( + "\x1b[91mExecution failed ! Program output: {}\x1b[0m", + program_error + ); + } + Ok(output) + } + + pub async fn prove(&self) -> Result { + info!("Proving transition for {}...", self.contract_name); + + let explicit = std::env::var("RISC0_PROVER").unwrap_or_default(); + let receipt = match explicit.to_lowercase().as_str() { + "bonsai" => bonsai_runner::run_bonsai(self.binary, self.contract_input.clone()).await?, + _ => { + let env = risc0_zkvm::ExecutorEnv::builder() + .write_slice(&self.contract_input) + .build() + .unwrap(); + + let prover = risc0_zkvm::default_prover(); + let prove_info = prover.prove(env, self.binary)?; + prove_info.receipt + } + }; + + let hyle_output = receipt + .journal + .decode::() + .expect("Failed to decode journal"); + + if !hyle_output.success { + let program_error = std::str::from_utf8(&hyle_output.program_outputs).unwrap(); + bail!( + "\x1b[91mExecution failed ! Program output: {}\x1b[0m", + program_error + ); + } + + let encoded_receipt = borsh::to_vec(&receipt).expect("Unable to encode receipt"); + Ok(ProofData::Bytes(encoded_receipt)) + } +} + +pub async fn fetch_current_state( + indexer_client: &ApiHttpClient, + contract_name: &ContractName, +) -> Result +where + State: TryFrom, +{ + let resp = indexer_client + .get_indexer_contract(contract_name) + .await? + .json::() + .await?; + + StateDigest(resp.state_digest).try_into() +} + +fn execute(binary: &'static [u8], contract_input: &[u8]) -> Result { + let env = risc0_zkvm::ExecutorEnv::builder() + .write_slice(contract_input) + .build() + .unwrap(); + + let prover = risc0_zkvm::default_executor(); + prover.execute(env, binary) +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 74507804..95617a61 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,3 +1,5 @@ //! Various tools for e.g. profiling and observability. +pub mod contract_runner; pub mod mock_workflow; +pub mod transactions_builder; diff --git a/src/tools/transactions_builder.rs b/src/tools/transactions_builder.rs new file mode 100644 index 00000000..cddc7070 --- /dev/null +++ b/src/tools/transactions_builder.rs @@ -0,0 +1,291 @@ +use std::pin::Pin; + +use anyhow::{anyhow, bail, Error, Result}; +use hydentity::{AccountInfo, Hydentity}; +use hyle_contract_sdk::{ + caller::ExecutionContext, + erc20::ERC20Action, + identity_provider::{IdentityAction, IdentityVerification}, + Blob, BlobData, BlobIndex, ContractName, Digestable, HyleOutput, Identity, +}; +use hyllar::HyllarToken; +use staking::{model::ValidatorPublicKey, state::Staking, StakingAction, StakingContract}; +use tracing::info; + +use crate::model::ProofData; + +use super::contract_runner::ContractRunner; + +pub struct Password(BlobData); + +pub static HYLLAR_BIN: &[u8] = hyle_contracts::HYLLAR_ELF; +pub static HYDENTITY_BIN: &[u8] = hyle_contracts::HYDENTITY_ELF; +pub static STAKING_BIN: &[u8] = hyle_contracts::STAKING_ELF; + +pub fn get_binary(contract_name: ContractName) -> Result<&'static [u8]> { + match contract_name.0.as_str() { + "hyllar" => Ok(HYLLAR_BIN), + "hydentity" => Ok(HYDENTITY_BIN), + "staking" => Ok(STAKING_BIN), + _ => bail!("contract {} not supported", contract_name), + } +} + +#[derive(Debug, Clone)] +pub struct States { + pub hyllar: HyllarToken, + pub hydentity: Hydentity, + pub staking: Staking, +} + +impl States { + pub fn for_token<'a>(&'a self, token: &ContractName) -> Result<&'a HyllarToken> { + match token.0.as_str() { + "hyllar" => Ok(&self.hyllar), + _ => bail!("Invalid token"), + } + } + + pub fn update_for_token(&mut self, token: &ContractName, new_state: HyllarToken) -> Result<()> { + match token.0.as_str() { + "hyllar" => self.hyllar = new_state, + _ => bail!("Invalid token"), + } + Ok(()) + } +} + +pub struct TransactionBuilder { + pub identity: Identity, + hydentity_cf: Vec<(IdentityAction, Password, BlobIndex)>, + hyllar_cf: Vec<(ERC20Action, ContractName, BlobIndex)>, + staking_cf: Vec<(StakingAction, BlobIndex)>, + blobs: Vec, + runners: Vec, +} + +pub struct BuildResult { + pub identity: Identity, + pub blobs: Vec, + pub outputs: Vec<(ContractName, HyleOutput)>, +} + +impl TransactionBuilder { + pub fn new(identity: Identity) -> Self { + Self { + identity, + hydentity_cf: vec![], + hyllar_cf: vec![], + staking_cf: vec![], + blobs: vec![], + runners: vec![], + } + } + + fn add_hydentity_cf(&mut self, action: IdentityAction, password: Password) { + self.hydentity_cf + .push((action.clone(), password, BlobIndex(self.blobs.len()))); + self.blobs.push(action.as_blob("hydentity".into())); + } + fn add_hyllar_cf( + &mut self, + token: ContractName, + action: ERC20Action, + caller: Option, + ) { + self.hyllar_cf + .push((action.clone(), token.clone(), BlobIndex(self.blobs.len()))); + self.blobs.push(action.as_blob(token, caller, None)); + } + fn add_stake_cf(&mut self, action: StakingAction) { + self.staking_cf + .push((action.clone(), BlobIndex(self.blobs.len()))); + self.blobs + .push(action.as_blob("staking".into(), None, None)); + } + + pub async fn verify_identity( + &mut self, + state: &Hydentity, + password: String, + ) -> Result<(), Error> { + let nonce = get_nonce(state, &self.identity.0).await?; + let password = Password(BlobData(password.into_bytes().to_vec())); + + self.add_hydentity_cf( + IdentityAction::VerifyIdentity { + account: self.identity.0.clone(), + nonce, + }, + password, + ); + + Ok(()) + } + + pub fn register_identity(&mut self, password: String) { + let password = Password(BlobData(password.into_bytes().to_vec())); + + self.add_hydentity_cf( + IdentityAction::RegisterIdentity { + account: self.identity.0.clone(), + }, + password, + ); + } + + pub fn approve(&mut self, token: ContractName, spender: String, amount: u128) { + self.add_hyllar_cf(token, ERC20Action::Approve { spender, amount }, None); + } + + pub fn transfer(&mut self, token: ContractName, recipient: String, amount: u128) { + self.add_hyllar_cf(token, ERC20Action::Transfer { recipient, amount }, None); + } + + pub fn stake( + &mut self, + token: ContractName, + staking_contract: ContractName, + amount: u128, + ) -> Result<(), Error> { + self.add_stake_cf(StakingAction::Stake { amount }); + self.add_hyllar_cf( + token, + ERC20Action::Transfer { + recipient: staking_contract.0.clone(), + amount, + }, + None, + ); + + Ok(()) + } + + pub fn delegate(&mut self, validator: ValidatorPublicKey) -> Result<(), Error> { + self.add_stake_cf(StakingAction::Delegate { validator }); + + Ok(()) + } + + pub async fn build(&mut self, states: &mut States) -> Result { + let mut new_states = states.clone(); + let mut outputs = vec![]; + for id in self.hydentity_cf.iter() { + let runner = ContractRunner::new( + "hydentity".into(), + get_binary("hydentity".into())?, + self.identity.clone(), + id.1 .0.clone(), + self.blobs.clone(), + id.2.clone(), + new_states.hydentity.clone(), + ) + .await?; + let out = runner.execute()?; + new_states.hydentity = out.next_state.clone().try_into()?; + outputs.push(("hydentity".into(), out)); + self.runners.push(runner); + } + + for cf in self.hyllar_cf.iter() { + let runner = ContractRunner::new( + cf.1.clone(), + get_binary(cf.1.clone())?, + self.identity.clone(), + BlobData(vec![]), + self.blobs.clone(), + cf.2.clone(), + new_states.for_token(&cf.1)?.clone(), + ) + .await?; + let out = runner.execute()?; + new_states.update_for_token(&cf.1, out.next_state.clone().try_into()?)?; + outputs.push(("hyllar".into(), out)); + self.runners.push(runner); + } + + for cf in self.staking_cf.iter() { + info!("State before runner: {:?}", new_states.staking); + info!("on chain state: {:?}", new_states.staking.on_chain_state()); + let runner = ContractRunner::new::( + "staking".into(), + get_binary("staking".into())?, + self.identity.clone(), + BlobData(new_states.staking.as_digest().0), + self.blobs.clone(), + cf.1.clone(), + new_states.staking.on_chain_state(), + ) + .await?; + let out = runner.execute()?; + let mut contract = StakingContract::new( + ExecutionContext { + callees_blobs: vec![].into(), + caller: self.identity.clone(), + }, + new_states.staking.clone(), + ); + + contract + .execute_action(cf.0.clone()) + .map_err(|e| anyhow!("Error in staking execution: {e}"))?; + + new_states.staking = contract.state(); + assert_eq!( + out.next_state, + new_states.staking.on_chain_state().as_digest() + ); + outputs.push(("staking".into(), out)); + self.runners.push(runner); + } + + *states = new_states; + + Ok(BuildResult { + identity: self.identity.clone(), + blobs: self.blobs.clone(), + outputs, + }) + } + + /// Returns an iterator over the proofs of the transactions + /// In order to send proofs when they are ready, without waiting for all of them to be ready + /// Example usage: + /// for (proof, contract_name) in transaction.iter_prove() { + /// let proof: ProofData = proof.await.unwrap(); + /// ctx.client() + /// .send_tx_proof(&hyle::model::ProofTransaction { + /// blob_tx_hash: blob_tx_hash.clone(), + /// proof, + /// contract_name, + /// }) + /// .await + /// .unwrap(); + ///} + pub fn iter_prove<'a>( + &'a self, + ) -> impl Iterator< + Item = ( + Pin> + Send + 'a>>, + ContractName, + ), + > + 'a { + self.runners.iter().map(|runner| { + let future = runner.prove(); + ( + Box::pin(future) + as Pin> + Send + 'a>>, + runner.contract_name.clone(), + ) + }) + } +} + +async fn get_nonce(state: &Hydentity, username: &str) -> Result { + let info = state + .get_identity_info(username) + .map_err(|err| anyhow::anyhow!(err))?; + let state: AccountInfo = serde_json::from_str(&info) + .map_err(|_| anyhow::anyhow!("Failed to parse identity info"))?; + Ok(state.nonce) +} diff --git a/tests/amm_test.rs b/tests/amm_test.rs index 8d04b901..46b71c7a 100644 --- a/tests/amm_test.rs +++ b/tests/amm_test.rs @@ -148,16 +148,16 @@ mod e2e_amm { let contract_hydentity = ctx.get_contract("hydentity").await?; let state: hydentity::Hydentity = contract_hydentity.state.try_into()?; - let expected_info = serde_json::to_string(&AccountInfo { - hash: "b6baa13a27c933bb9f7df812108407efdff1ec3c3ef8d803e20eed7d4177d596".to_string(), - nonce: 0, - }); - assert_eq!( + // faucet_start_nonce = 0 in single-mode, N in multi-node(N) mode + let faucet_start_nonce = serde_json::from_str::( state .get_identity_info("faucet.hydentity") - .expect("faucet identity not found"), - expected_info.unwrap() // hash for "faucet.hydentity::password" - ); + .expect("faucet identity not found") + .as_str(), + ) + .expect("Failed to parse faucet identity info") + .nonce; + ///////////////////////////////////////////////////////////////////// ///////////////// sending hyllar from faucet to bob ///////////////// @@ -168,7 +168,7 @@ mod e2e_amm { vec![ IdentityAction::VerifyIdentity { account: "faucet.hydentity".to_string(), - nonce: 0, + nonce: faucet_start_nonce, } .as_blob(ContractName("hydentity".to_owned())), ERC20Action::Transfer { @@ -192,7 +192,7 @@ mod e2e_amm { }, "faucet.hydentity", "password", - Some(0), + Some(faucet_start_nonce), ) .await; @@ -246,7 +246,7 @@ mod e2e_amm { vec![ IdentityAction::VerifyIdentity { account: "faucet.hydentity".to_string(), - nonce: 1, + nonce: faucet_start_nonce + 1, } .as_blob(ContractName("hydentity".to_owned())), ERC20Action::Transfer { @@ -270,7 +270,7 @@ mod e2e_amm { }, "faucet.hydentity", "password", - Some(1), + Some(faucet_start_nonce + 1), ) .await; diff --git a/tests/consensus_tests.rs b/tests/consensus_tests.rs index b42f3036..3d49ff5c 100644 --- a/tests/consensus_tests.rs +++ b/tests/consensus_tests.rs @@ -4,7 +4,17 @@ use fixtures::ctx::E2ECtx; mod fixtures; mod e2e_consensus { - use staking::{Stake, Staker}; + + use fixtures::test_helpers::send_transaction; + use hyle::{ + tools::{ + contract_runner::fetch_current_state, + transactions_builder::{States, TransactionBuilder}, + }, + utils::logger::LogMe, + }; + use hyle_contract_sdk::Identity; + use staking::state::OnChainState; use super::*; @@ -25,9 +35,10 @@ mod e2e_consensus { Ok(()) } + #[ignore = "flakky due to bug in data dissemination"] #[test_log::test(tokio::test)] async fn can_rejoin() -> Result<()> { - let mut ctx = E2ECtx::new_multi(2, 500).await?; + let mut ctx = E2ECtx::new_multi_with_indexer(2, 500).await?; ctx.wait_height(4).await?; @@ -37,12 +48,56 @@ mod e2e_consensus { assert!(node_info.pubkey.is_some()); - ctx.client() - .send_stake_tx(&Staker { - pubkey: node_info.pubkey.clone().unwrap(), - stake: Stake { amount: 100 }, - }) - .await?; + let hyllar = fetch_current_state(ctx.indexer_client(), &"hyllar".into()) + .await + .log_error("fetch state failed") + .unwrap(); + let hydentity = fetch_current_state(ctx.indexer_client(), &"hydentity".into()) + .await + .unwrap(); + + let staking_state: OnChainState = + fetch_current_state(ctx.indexer_client(), &"staking".into()) + .await + .unwrap(); + + let staking = ctx.client().get_consensus_staking_state().await.unwrap(); + + assert_eq!(staking_state, staking.on_chain_state()); + let mut states = States { + hyllar, + hydentity, + staking, + }; + + let node_identity = Identity(format!("{}.hydentity", node_info.id)); + { + let mut transaction = TransactionBuilder::new(node_identity.clone()); + transaction.register_identity("password".to_string()); + + send_transaction(ctx.client(), transaction, &mut states).await; + } + { + let mut transaction = TransactionBuilder::new("faucet.hydentity".into()); + + transaction + .verify_identity(&states.hydentity, "password".to_string()) + .await?; + transaction.transfer("hyllar".into(), node_identity.0.clone(), 100); + + send_transaction(ctx.client(), transaction, &mut states).await; + } + { + let mut transaction = TransactionBuilder::new(node_identity.clone()); + + transaction + .verify_identity(&states.hydentity, "password".to_string()) + .await?; + transaction.stake("hyllar".into(), "staking".into(), 100)?; + transaction.delegate(node_info.pubkey.clone().unwrap())?; + + send_transaction(ctx.client(), transaction, &mut states).await; + } // 2 slots to get the tx in a blocks // 1 slot to send the candidacy diff --git a/tests/fixtures/ctx.rs b/tests/fixtures/ctx.rs index df140bc6..5c1aea83 100644 --- a/tests/fixtures/ctx.rs +++ b/tests/fixtures/ctx.rs @@ -84,6 +84,8 @@ impl E2ECtx { } pub async fn new_single(slot_duration: u64) -> Result { + std::env::set_var("RISC0_DEV_MODE", "1"); + let mut conf_maker = ConfMaker::default(); conf_maker.default.consensus.slot_duration = slot_duration; conf_maker.default.single_node = Some(true); @@ -115,6 +117,8 @@ impl E2ECtx { } pub async fn new_multi(count: usize, slot_duration: u64) -> Result { + std::env::set_var("RISC0_DEV_MODE", "1"); + let mut conf_maker = ConfMaker::default(); conf_maker.default.consensus.slot_duration = slot_duration; @@ -158,6 +162,8 @@ impl E2ECtx { } pub async fn new_multi_with_indexer(count: usize, slot_duration: u64) -> Result { + std::env::set_var("RISC0_DEV_MODE", "1"); + let pg = Self::init().await; let mut conf_maker = ConfMaker::default(); @@ -202,6 +208,10 @@ impl E2ECtx { &self.clients[self.client_index] } + pub fn indexer_client(&self) -> &ApiHttpClient { + &self.clients[self.indexer_client_index] + } + pub fn has_indexer(&self) -> bool { self.pg.is_some() } @@ -315,7 +325,8 @@ impl E2ECtx { } pub async fn get_indexer_contract(&self, name: &str) -> Result { - let indexer_contract_response = self.clients[self.indexer_client_index] + let indexer_contract_response = self + .indexer_client() .get_indexer_contract(&name.into()) .await .and_then(|response| response.error_for_status().context("Getting contract")); diff --git a/tests/fixtures/test_helpers.rs b/tests/fixtures/test_helpers.rs index d9846746..7e16d3ab 100644 --- a/tests/fixtures/test_helpers.rs +++ b/tests/fixtures/test_helpers.rs @@ -1,6 +1,8 @@ use assert_cmd::prelude::*; use hyle::{ + model::{BlobTransaction, ProofData}, rest::client::ApiHttpClient, + tools::transactions_builder::{BuildResult, States, TransactionBuilder}, utils::conf::{Conf, Consensus}, }; use rand::Rng; @@ -165,3 +167,31 @@ pub async fn wait_height(client: &ApiHttpClient, heights: u64) -> anyhow::Result //result.map_err(|_| anyhow::anyhow!("Timeout reached while waiting for height")) } + +#[allow(dead_code)] +pub async fn send_transaction( + client: &ApiHttpClient, + mut transaction: TransactionBuilder, + states: &mut States, +) { + let BuildResult { + identity, blobs, .. + } = transaction.build(states).await.unwrap(); + + let blob_tx_hash = client + .send_tx_blob(&BlobTransaction { identity, blobs }) + .await + .unwrap(); + + for (proof, contract_name) in transaction.iter_prove() { + let proof: ProofData = proof.await.unwrap(); + client + .send_tx_proof(&hyle::model::ProofTransaction { + blob_tx_hash: blob_tx_hash.clone(), + proof, + contract_name, + }) + .await + .unwrap(); + } +} diff --git a/tests/hyllar_test.rs b/tests/hyllar_test.rs index a7eddfa1..f27e1bff 100644 --- a/tests/hyllar_test.rs +++ b/tests/hyllar_test.rs @@ -63,16 +63,15 @@ mod e2e_hyllar { let contract = ctx.get_contract("hydentity").await?; let state: hydentity::Hydentity = contract.state.try_into()?; - let expected_info = serde_json::to_string(&AccountInfo { - hash: "b6baa13a27c933bb9f7df812108407efdff1ec3c3ef8d803e20eed7d4177d596".to_string(), - nonce: 0, - }); - assert_eq!( + // faucet_start_nonce = 0 in single-mode, N in multi-node(N) mode + let faucet_start_nonce = serde_json::from_str::( state .get_identity_info("faucet.hydentity") - .expect("faucet identity not found"), - expected_info.unwrap() // hash for "faucet.hydentity::password" - ); + .expect("faucet identity not found") + .as_str(), + ) + .expect("Failed to parse faucet identity info") + .nonce; info!("➑️ Sending blob to transfer 25 tokens from faucet to bob"); let blob_tx_hash = ctx @@ -81,7 +80,7 @@ mod e2e_hyllar { vec![ IdentityAction::VerifyIdentity { account: "faucet.hydentity".to_string(), - nonce: 0, + nonce: faucet_start_nonce, } .as_blob(ContractName("hydentity".to_owned())), ERC20Action::Transfer { @@ -105,7 +104,7 @@ mod e2e_hyllar { }, "faucet.hydentity", "password", - Some(0), + Some(faucet_start_nonce), ) .await; @@ -140,12 +139,6 @@ mod e2e_hyllar { .expect("bob identity not found"), 25 ); - assert_eq!( - state - .balance_of("faucet.hydentity") - .expect("faucet identity not found"), - 99_999_999_975 - ); Ok(()) }