diff --git a/Cargo.lock b/Cargo.lock index 1e2831f7..0a531b64 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", @@ -2569,6 +2570,24 @@ dependencies = [ "risc0-build", ] +[[package]] +name = "hyle-loadtest" +version = "0.3.0" +dependencies = [ + "amm", + "anyhow", + "bincode 2.0.0-rc.3", + "clap", + "hydentity", + "hyle", + "hyle-contract-sdk", + "hyle-contracts", + "hyllar", + "reqwest 0.12.9", + "serde_json", + "tokio", +] + [[package]] name = "hyllar" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 2d801e2e..fd809dc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crates/bonsai-runner", "crates/hyrun", + "crates/hyle-loadtest", ] resolver = "2" @@ -33,7 +34,9 @@ hyle-contract-sdk = { path = "./contract-sdk", features = ["tracing"] } hyle-contracts = { path = "./contracts", package = "hyle-contracts" } hydentity = { path = "./contracts/hydentity" } hyllar = { path = "./contracts/hyllar" } +amm = { path = "./contracts/amm" } 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/erc20.rs b/contract-sdk/src/erc20.rs index e3711d34..d2c3d500 100644 --- a/contract-sdk/src/erc20.rs +++ b/contract-sdk/src/erc20.rs @@ -7,7 +7,7 @@ use bincode::{Decode, Encode}; use crate::{ caller::{CallerCallee, CheckCalleeBlobs}, - Blob, BlobData, BlobIndex, ContractName, RunResult, StructuredBlobData, + Blob, BlobData, BlobIndex, ContractAction, ContractName, RunResult, StructuredBlobData, }; /// Trait representing the ERC-20 token standard interface. @@ -106,8 +106,8 @@ pub enum ERC20Action { }, } -impl ERC20Action { - pub fn as_blob( +impl ContractAction for ERC20Action { + fn as_blob( &self, contract_name: ContractName, caller: Option, diff --git a/contract-sdk/src/identity_provider.rs b/contract-sdk/src/identity_provider.rs index 128d6b4a..535b9b31 100644 --- a/contract-sdk/src/identity_provider.rs +++ b/contract-sdk/src/identity_provider.rs @@ -1,7 +1,7 @@ -use alloc::{format, string::String}; +use alloc::{format, string::String, vec::Vec}; use bincode::{Decode, Encode}; -use crate::{Blob, BlobData, ContractName, RunResult}; +use crate::{Blob, BlobData, BlobIndex, ContractAction, ContractName, RunResult}; /// Trait representing an identity verification contract. pub trait IdentityVerification { @@ -56,8 +56,13 @@ pub enum IdentityAction { GetIdentityInfo { account: String }, } -impl IdentityAction { - pub fn as_blob(self, contract_name: ContractName) -> Blob { +impl ContractAction for IdentityAction { + fn as_blob( + &self, + contract_name: ContractName, + _caller: Option, + _callees: Option>, + ) -> Blob { Blob { contract_name, data: BlobData( diff --git a/contract-sdk/src/lib.rs b/contract-sdk/src/lib.rs index 8721d005..3fad4bbb 100644 --- a/contract-sdk/src/lib.rs +++ b/contract-sdk/src/lib.rs @@ -173,6 +173,15 @@ impl TryFrom for StructuredBlob { } } +pub trait ContractAction: Send { + fn as_blob( + &self, + contract_name: ContractName, + caller: Option, + callees: Option>, + ) -> Blob; +} + pub fn flatten_blobs(blobs: &[Blob]) -> Vec { blobs .iter() diff --git a/contracts/amm/src/lib.rs b/contracts/amm/src/lib.rs index 52829900..e0bdc4ba 100644 --- a/contracts/amm/src/lib.rs +++ b/contracts/amm/src/lib.rs @@ -5,7 +5,7 @@ use bincode::{Decode, Encode}; use sdk::caller::{CalleeBlobs, CallerCallee, CheckCalleeBlobs, ExecutionContext, MutCalleeBlobs}; use sdk::erc20::{ERC20BlobChecker, ERC20}; use sdk::{erc20::ERC20Action, Identity}; -use sdk::{Blob, BlobIndex, Digestable, RunResult}; +use sdk::{Blob, BlobIndex, ContractAction, Digestable, RunResult}; use sdk::{BlobData, ContractName, StructuredBlobData}; use serde::{Deserialize, Serialize}; @@ -90,6 +90,14 @@ impl AmmState { } None } + + pub fn create_new_pair(&mut self, pair: UnorderedTokenPair, amounts: TokenPairAmount) { + self.pairs.insert(pair, amounts); + } + + pub fn update_pair(&mut self, pair: UnorderedTokenPair, amounts: TokenPairAmount) { + self.pairs.insert(pair, amounts); + } } impl AmmContract { @@ -144,7 +152,7 @@ impl AmmContract { let program_outputs = format!("Pair {:?} created", normalized_pair); - self.state.pairs.insert(normalized_pair, amounts); + self.state.create_new_pair(normalized_pair, amounts); Ok(program_outputs) } @@ -173,20 +181,21 @@ impl AmmContract { // Compute x,y and check swap is legit (x*y=k) let normalized_pair = UnorderedTokenPair::new(pair.0.clone(), pair.1.clone()); let is_normalized_order = pair.0 <= pair.1; - let Some((prev_x, prev_y)) = self.state.pairs.get_mut(&normalized_pair) else { + let Some((prev_x, prev_y)) = self.state.pairs.get(&normalized_pair) else { return Err(format!("Pair {:?} not found in AMM state", pair)); }; - let expected_to_amount = if is_normalized_order { + let (expected_to_amount, new_x, new_y) = if is_normalized_order { let amount = *prev_y - (*prev_x * *prev_y / (*prev_x + from_amount)); - *prev_x += from_amount; - *prev_y -= amount; // we need to remove the full amount to avoid slipping - amount + let new_x = prev_x + from_amount; + let new_y = prev_y - amount; // we need to remove the full amount to avoid slipping + (amount, new_x, new_y) } else { let amount = *prev_x - (*prev_y * *prev_x / (*prev_y + from_amount)); - *prev_y += from_amount; - *prev_x -= amount; // we need to remove the full amount to avoid slipping - amount + let new_y = prev_y + from_amount; + let new_x = prev_x - amount; // we need to remove the full amount to avoid slipping + (amount, new_x, new_y) }; + self.state.update_pair(normalized_pair, (new_x, new_y)); // Assert that we transferred less than that, within 2% if to_amount > expected_to_amount || to_amount < expected_to_amount * 98 / 100 { @@ -250,9 +259,9 @@ pub enum AmmAction { }, } -impl AmmAction { - pub fn as_blob( - self, +impl ContractAction for AmmAction { + fn as_blob( + &self, contract_name: ContractName, caller: Option, callees: Option>, diff --git a/contracts/hyllar/src/lib.rs b/contracts/hyllar/src/lib.rs index 9b630b5a..67664ff1 100644 --- a/contracts/hyllar/src/lib.rs +++ b/contracts/hyllar/src/lib.rs @@ -32,6 +32,18 @@ pub struct HyllarTokenContract { } impl HyllarToken { + pub fn init( + total_supply: u128, + balances: BTreeMap, + allowances: BTreeMap<(String, String), u128>, + ) -> Self { + HyllarToken { + total_supply, + balances, + allowances, + } + } + /// Creates a new Hyllar token with the specified initial supply. /// /// # Arguments diff --git a/contracts/hyllar/tests/hyllar_r0.rs b/contracts/hyllar/tests/hyllar_r0.rs index 9ab8ba43..108b97f9 100644 --- a/contracts/hyllar/tests/hyllar_r0.rs +++ b/contracts/hyllar/tests/hyllar_r0.rs @@ -1,7 +1,10 @@ use core::str; use hyllar::HyllarToken; -use sdk::{erc20::ERC20Action, BlobData, BlobIndex, ContractInput, ContractName, HyleOutput}; +use sdk::{ + erc20::ERC20Action, BlobData, BlobIndex, ContractAction, ContractInput, ContractName, + HyleOutput, +}; fn execute(inputs: ContractInput) -> HyleOutput { let env = risc0_zkvm::ExecutorEnv::builder() diff --git a/crates/hyle-loadtest/Cargo.toml b/crates/hyle-loadtest/Cargo.toml new file mode 100644 index 00000000..9baaf084 --- /dev/null +++ b/crates/hyle-loadtest/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "hyle-loadtest" +version.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + + +[dependencies] +serde_json = "1.0.133" +tokio = "^1.12" +hyle = { path = "../../" } +hyllar = { path = "../../contracts/hyllar" } +amm = { path = "../../contracts/amm" } +hydentity = { path = "../../contracts/hydentity" } +hyle-contracts = { path = "../../contracts", package = "hyle-contracts" } +hyle-contract-sdk = { path = "../../contract-sdk", features = ["tracing"] } +reqwest = "0.12.9" +anyhow = "1.0.94" +bincode = { version = "2.0.0-rc.3" } +clap = "4.5.23" diff --git a/crates/hyle-loadtest/report.html b/crates/hyle-loadtest/report.html new file mode 100644 index 00000000..a2b52f8a --- /dev/null +++ b/crates/hyle-loadtest/report.html @@ -0,0 +1,902 @@ + + + + Goose Attack Report + + + + +
+

Goose Attack Report

+ +
+

Users: 1

+

Target Host: http://127.0.0.1:4321/

+

goose v0.17.2

+

Plan overview

+ + + + + + + + + + + + + +
ActionStartedStoppedElapsedUsers
Increasing24-12-19 12:06:3624-12-19 12:06:3600:00:000 → 1
Canceling24-12-19 12:06:3624-12-19 12:06:3600:00:000 ← 1
+
+ +
+

Request Metrics

+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName# Requests# FailsAverage (ms)Min (ms)Max (ms)RPSFailures/s
POST/v1/tx/send/blob102.00221.000.00
Aggregated102.00221.000.00
+
+ + + +
+

Response Time Metrics

+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodName50%ile (ms)60%ile (ms)70%ile (ms)80%ile (ms)90%ile (ms)95%ile (ms)99%ile (ms)100%ile (ms)
POST/v1/tx/send/blob22222222
Aggregated22222222
+
+ + + +
+

Status Code Metrics

+ + + + + + + + + + + + + + + + + + + + +
MethodNameStatus Codes
POST/v1/tx/send/blob1 [200]
Aggregated1 [200]
+
+ +
+

Transaction Metrics

+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Transaction# Times Run# FailsAverage (ms)Min (ms)Max (ms)RPSFailures/s
HyleBenchmark
0.0 102.00221.000.00
Aggregated102.00221.000.00
+
+ +
+

Scenario Metrics

+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Scenario# Users# Times RunAverage (ms)Min (ms)Max (ms)Scenarios/sIterations
HyleBenchmark112.00221.001.00
Aggregated112.00221.001.00
+
+ +
+

User Metrics

+
+
+ + +
+
+ + + +
+ + \ No newline at end of file diff --git a/crates/hyle-loadtest/src/lib.rs b/crates/hyle-loadtest/src/lib.rs new file mode 100644 index 00000000..aea0c21b --- /dev/null +++ b/crates/hyle-loadtest/src/lib.rs @@ -0,0 +1,275 @@ +use std::collections::BTreeMap; + +use amm::{AmmState, UnorderedTokenPair}; +use anyhow::Error; +use hydentity::Hydentity; +use hyle::{ + model::{BlobTransaction, ContractName, Hashable, ProofTransaction}, + rest::client::ApiHttpClient, + tools::{contract_runner::ContractRunner, transactions_builder::TransactionBuilder}, +}; +use hyle_contract_sdk::erc20::ERC20; +use hyle_contract_sdk::identity_provider::IdentityVerification; +use hyle_contract_sdk::Digestable; +use hyle_contract_sdk::{flatten_blobs, BlobIndex, HyleOutput}; +use hyle_contracts::{AMM_ID, HYDENTITY_ID, HYLLAR_ID}; +use hyllar::{HyllarToken, HyllarTokenContract}; + +pub fn setup_contract_states( + number_of_users: &u32, + pair: &UnorderedTokenPair, +) -> ( + hydentity::Hydentity, + amm::AmmState, + hyllar::HyllarToken, + hyllar::HyllarToken, +) { + // Setup: create identity contract + let mut hydentity_state = Hydentity::default(); + + // Setup: creating AMM contract + let mut amm_state = AmmState::default(); + + // Setup: creating token1 contract + let mut balances_token1 = BTreeMap::new(); + let mut allowances_token1 = BTreeMap::new(); + + // Setup: creating token2 contract + let mut balances_token2 = BTreeMap::new(); + let mut allowances_token2 = BTreeMap::new(); + + for n in 0..*number_of_users { + let identity = format!("{}.hydentity-loadtest", n); + + hydentity_state + .register_identity(&identity, &identity) + .unwrap(); + + balances_token1.insert(identity.clone(), 5); + allowances_token1.insert( + (identity.clone(), "amm-loadtest".to_owned()), + 1_000_000_000_000, + ); + + balances_token2.insert(identity.clone(), 5); + allowances_token2.insert((identity, "amm-loadtest".to_owned()), 1_000_000_000_000); + } + + // Creation a new pair for these two tokens on the AMM + balances_token1.insert("amm-loadtest".to_owned(), 1_000_000_000); + balances_token2.insert("amm-loadtest".to_owned(), 1_000_000_000); + amm_state.create_new_pair(pair.clone(), (1_000_000_000, 1_000_000_000)); + + // Set token1/token2 state with 1_000_000_000_000 tokens, all users with 1000 tokens each and the AMM with allowance to swap for all of them + let token1_state = HyllarToken::init(1_000_000_000_000, balances_token1, allowances_token1); + let token2_state = HyllarToken::init(1_000_000_000_000, balances_token2, allowances_token2); + + (hydentity_state, amm_state, token1_state, token2_state) +} + +pub async fn register_contracts( + client: &ApiHttpClient, + hydentity_state: &Hydentity, + amm_state: &AmmState, + token1_state: &HyllarToken, + token2_state: &HyllarToken, +) -> Result<(), Error> { + // Create RegisterContract transactions + let register_hydentity = TransactionBuilder::register_contract( + "loadtest", + "test", + &HYDENTITY_ID, + hydentity_state.as_digest(), + "hydentity-loadtest", + ); + let register_amm = TransactionBuilder::register_contract( + "loadtest", + "test", + &AMM_ID, + amm_state.as_digest(), + "amm-loadtest", + ); + let register_token1 = TransactionBuilder::register_contract( + "loadtest", + "test", + &HYLLAR_ID, + token1_state.as_digest(), + "token1-loadtest", + ); + let register_token2 = TransactionBuilder::register_contract( + "loadtest", + "test", + &HYLLAR_ID, + token2_state.as_digest(), + "token2-loadtest", + ); + client + .send_tx_register_contract(®ister_hydentity) + .await?; + client.send_tx_register_contract(®ister_amm).await?; + client.send_tx_register_contract(®ister_token1).await?; + client.send_tx_register_contract(®ister_token2).await?; + Ok(()) +} + +pub async fn create_transactions( + pair: &UnorderedTokenPair, + mut hydentity_state: Hydentity, + mut amm_state: AmmState, + token1_state: HyllarToken, + token2_state: HyllarToken, + number_of_users: u32, +) -> Result)>, Error> { + // For each user, we create the blob_tx, we compute the transient states of each contracts in order to create the proof_txs + // We save everything to them next later + let mut txs_to_send: Vec<(BlobTransaction, Vec)> = vec![]; + + for n in 0..number_of_users { + let identity = format!("{}.hydentity-loadtest", n); + + let mut transaction = TransactionBuilder::new(identity.clone().into()); + transaction + .verify_identity( + &hydentity_state, + "hydentity-loadtest".into(), + identity.clone(), + ) + .await?; + transaction.swap( + identity.clone(), + "amm-loadtest".into(), + ("token1-loadtest".to_owned(), "token2-loadtest".to_owned()), + (5, 5), + ); + let blob_transaction = transaction.to_blob_transaction(); + let blob_tx_hash = blob_transaction.hash(); + let flatten_blobs = flatten_blobs(&blob_transaction.blobs); + + let initial_state_hydentity = hydentity_state.as_digest(); + let initial_state_amm = amm_state.as_digest(); + let initial_state_token1 = token1_state.as_digest(); + let initial_state_token2 = token2_state.as_digest(); + + // Change state of hydentity + hydentity_state + .verify_identity(&identity, 0, &identity) + .unwrap(); + + // Change state of amm + amm_state.update_pair(pair.clone(), (5, 5)); + + // Change state of token1 + let mut token1_contract = + HyllarTokenContract::init(token1_state.clone(), "amm-loadtest".into()); + token1_contract + .transfer_from(&identity, "amm-loadtest", 5) + .unwrap(); + + // Change state of token2 + let mut token2_contract = + HyllarTokenContract::init(token2_state.clone(), "amm-loadtest".into()); + token2_contract.transfer(&identity, 5).unwrap(); + + let next_state_hydentity = hydentity_state.as_digest(); + let next_state_amm = amm_state.as_digest(); + let next_state_token1 = token1_state.as_digest(); + let next_state_token2 = token2_state.as_digest(); + + // Process les hyle_output attendus + let hyle_output_hydentity = HyleOutput { + version: 1, + initial_state: initial_state_hydentity, + next_state: next_state_hydentity, + identity: identity.clone().into(), + tx_hash: blob_tx_hash.clone(), + index: BlobIndex(0), + blobs: flatten_blobs.clone(), + success: true, + program_outputs: vec![], + }; + + let hyle_output_amm = HyleOutput { + version: 1, + initial_state: initial_state_amm, + next_state: next_state_amm, + identity: identity.clone().into(), + tx_hash: blob_tx_hash.clone(), + index: BlobIndex(1), + blobs: flatten_blobs.clone(), + success: true, + program_outputs: vec![], + }; + + let hyle_output_token1 = HyleOutput { + version: 1, + initial_state: initial_state_token1, + next_state: next_state_token1, + identity: identity.clone().into(), + tx_hash: blob_tx_hash.clone(), + index: BlobIndex(2), + blobs: flatten_blobs.clone(), + success: true, + program_outputs: vec![], + }; + + let hyle_output_token2 = HyleOutput { + version: 1, + initial_state: initial_state_token2, + next_state: next_state_token2, + identity: identity.clone().into(), + tx_hash: blob_tx_hash.clone(), + index: BlobIndex(3), + blobs: flatten_blobs.clone(), + success: true, + program_outputs: vec![], + }; + + let proof_tx_hydentity = ProofTransaction { + blob_tx_hash: blob_tx_hash.clone(), + proof: ContractRunner::prove_test(hyle_output_hydentity).unwrap(), + contract_name: ContractName("hydentity-loadtest".to_owned()), + }; + let proof_tx_amm = ProofTransaction { + blob_tx_hash: blob_tx_hash.clone(), + proof: ContractRunner::prove_test(hyle_output_amm).unwrap(), + contract_name: ContractName("amm-loadtest".to_owned()), + }; + let proof_tx_token1 = ProofTransaction { + blob_tx_hash: blob_tx_hash.clone(), + proof: ContractRunner::prove_test(hyle_output_token1).unwrap(), + contract_name: ContractName("token1-loadtest".to_owned()), + }; + let proof_tx_token2 = ProofTransaction { + blob_tx_hash: blob_tx_hash.clone(), + proof: ContractRunner::prove_test(hyle_output_token2).unwrap(), + contract_name: ContractName("token2-loadtest".to_owned()), + }; + + txs_to_send.push(( + blob_transaction, + vec![ + proof_tx_hydentity, + proof_tx_amm, + proof_tx_token1, + proof_tx_token2, + ], + )); + } + + Ok(txs_to_send) +} + +pub async fn send_transactions( + client: &ApiHttpClient, + txs_to_send: Vec<(BlobTransaction, Vec)>, +) -> Result<(), Error> { + for (blob_tx, _) in txs_to_send.iter() { + client.send_tx_blob(blob_tx).await?; + } + for (_, proof_txs) in txs_to_send.iter() { + for proof_tx in proof_txs { + client.send_tx_proof(proof_tx).await?; + } + } + Ok(()) +} diff --git a/crates/hyle-loadtest/src/main.rs b/crates/hyle-loadtest/src/main.rs new file mode 100644 index 00000000..53d45870 --- /dev/null +++ b/crates/hyle-loadtest/src/main.rs @@ -0,0 +1,130 @@ +use amm::UnorderedTokenPair; +use anyhow::Error; +use clap::{Parser, Subcommand}; +use hyle::model::{BlobTransaction, ProofTransaction}; +use hyle::rest::client::ApiHttpClient; +use hyle_loadtest::{ + create_transactions, register_contracts, send_transactions, setup_contract_states, +}; +use reqwest::{Client, Url}; +use std::fs::File; +use std::io::{Read, Write}; + +/// A cli to interact with hyle node +#[derive(Debug, Parser)] // requires `derive` feature +#[command(name = "loadtest")] +#[command(about = "A CLI to loadtest hyle", long_about = None)] +struct Args { + #[command(subcommand)] + command: SendCommands, + + #[arg(long, default_value = "127.0.0.1")] + pub host: String, + + #[arg(long, default_value = "4321")] + pub port: u32, + + #[arg(long, default_value = "10")] + pub users: u32, +} + +#[derive(Debug, Subcommand)] +enum SendCommands { + /// Register Contracts + #[command(alias = "rc")] + RegisterContracts, + /// Generates Blob and Proof transactions for the load test + #[command(alias = "gt")] + GenerateTransactions, + /// Load the transactions and send them + #[command(alias = "st")] + SendTransactions, + /// Run the entire flow + #[command(alias = "l")] + LoadTest, +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + let args = Args::parse(); + + let url = format!("http://{}:{}", args.host, args.port); + let client = ApiHttpClient { + url: Url::parse(&url).unwrap(), + reqwest_client: Client::new(), + }; + + let pair = UnorderedTokenPair::new("token1-loadtest".to_owned(), "token2-loadtest".to_owned()); + + let (hydentity_state, amm_state, token1_state, token2_state) = + setup_contract_states(&args.users, &pair); + + match args.command { + SendCommands::RegisterContracts => { + register_contracts( + &client, + &hydentity_state, + &amm_state, + &token1_state, + &token2_state, + ) + .await?; + } + SendCommands::GenerateTransactions => { + let txs_to_send = create_transactions( + &pair, + hydentity_state, + amm_state, + token1_state, + token2_state, + args.users, + ) + .await?; + + let encoded: Vec = + bincode::encode_to_vec(&txs_to_send, bincode::config::standard())?; + let mut file = File::create(format!("./txs_to_send.{}-users.bin", args.users))?; + file.write_all(&encoded)?; + } + SendCommands::SendTransactions => { + let mut file = File::open(format!("./txs_to_send.{}-users.bin", args.users))?; + let mut encoded = Vec::new(); + file.read_to_end(&mut encoded)?; + + let txs_to_send: Vec<(BlobTransaction, Vec)> = + bincode::decode_from_slice(&encoded, bincode::config::standard()) + .map(|(data, _)| data)?; + + send_transactions(&client, txs_to_send).await?; + } + SendCommands::LoadTest => { + register_contracts( + &client, + &hydentity_state, + &amm_state, + &token1_state, + &token2_state, + ) + .await?; + + let txs_to_send = create_transactions( + &pair, + hydentity_state, + amm_state, + token1_state, + token2_state, + args.users, + ) + .await?; + + let encoded: Vec = + bincode::encode_to_vec(&txs_to_send, bincode::config::standard())?; + let mut file = File::create(format!("./txs_to_send.{}-users.bin", args.users))?; + file.write_all(&encoded)?; + + send_transactions(&client, txs_to_send).await?; + } + }; + + Ok(()) +} diff --git a/crates/hyrun/src/lib.rs b/crates/hyrun/src/lib.rs index 90396d07..33cfa053 100644 --- a/crates/hyrun/src/lib.rs +++ b/crates/hyrun/src/lib.rs @@ -5,8 +5,8 @@ use amm::{AmmAction, AmmState}; use hydentity::Hydentity; use hyllar::HyllarToken; use sdk::{ - erc20::ERC20Action, identity_provider::IdentityAction, BlobData, BlobIndex, ContractInput, - ContractName, StateDigest, TxHash, + erc20::ERC20Action, identity_provider::IdentityAction, BlobData, BlobIndex, ContractAction, + ContractInput, ContractName, StateDigest, TxHash, }; use serde::Deserialize; @@ -182,7 +182,7 @@ pub fn run_command(context: &Context) { .unwrap_or_else(|| panic!("Missing password argument")) .as_bytes() .to_vec(); - let blobs = vec![cf.as_blob(ContractName("hydentity".to_owned()))]; + let blobs = vec![cf.as_blob(ContractName("hydentity".to_owned()), None, None)]; contract::print_hyled_blob_tx(&identity, &blobs); contract::run( @@ -235,7 +235,7 @@ pub fn run_command(context: &Context) { }; let blobs = vec![ - identity_cf.as_blob(ContractName("hydentity".to_owned())), + identity_cf.as_blob(ContractName("hydentity".to_owned()), None, None), cf.as_blob(ContractName(hyllar_contract_name.clone()), None, None), ]; contract::print_hyled_blob_tx(&identity.clone().into(), &blobs); @@ -302,7 +302,7 @@ pub fn run_command(context: &Context) { }; let blobs = vec![ - identity_cf.as_blob(ContractName("hydentity".to_owned())), + identity_cf.as_blob(ContractName("hydentity".to_owned()), None, None), AmmAction::NewPair { pair: (token_a.clone(), token_b.clone()), amounts: (amount_a, amount_b), @@ -424,7 +424,7 @@ pub fn run_command(context: &Context) { }; let blobs = vec![ - identity_cf.as_blob(ContractName("hydentity".to_owned())), + identity_cf.as_blob(ContractName("hydentity".to_owned()), None, None), AmmAction::Swap { pair: (token_a.to_string(), token_b.to_string()), amounts: (amount_a, amount_b), diff --git a/src/data_availability.rs b/src/data_availability.rs index 2dee0091..f8ec9a43 100644 --- a/src/data_availability.rs +++ b/src/data_availability.rs @@ -444,7 +444,6 @@ impl DataAvailability { consensus_proposal, }: CommittedConsensusProposal, ) { - info!("🔒 Cut committed"); let last_block = self.blocks.last(); let parent_hash = last_block .as_ref() @@ -453,6 +452,14 @@ impl DataAvailability { "46696174206c757820657420666163746120657374206c7578", )); + info!( + "🔒 Cut committed for slot {}", + last_block + .as_ref() + .map(|b| b.consensus_proposal.slot) + .unwrap_or(0) + ); + let signed_block = SignedBlock { parent_hash, data_proposals, diff --git a/src/data_availability/node_state.rs b/src/data_availability/node_state.rs index 75d6db1c..d3c74f22 100644 --- a/src/data_availability/node_state.rs +++ b/src/data_availability/node_state.rs @@ -385,7 +385,14 @@ impl NodeState { } // Verify the contract name - let expected_contract = &unsettled_tx.blobs[hyle_output.index.0].contract_name; + let expected_contract = &unsettled_tx + .blobs + .get(hyle_output.index.0) + .context(format!( + "Can't find BlobIndex {} in BlobTx {:?}", + hyle_output.index.0, unsettled_tx + ))? + .contract_name; if expected_contract != contract_name { bail!("Blob reference from proof for {unsettled_tx_hash} does not match the BlobTx contract name {expected_contract}"); } diff --git a/src/data_availability/node_state/verifiers.rs b/src/data_availability/node_state/verifiers.rs index e64b3799..3b4bead6 100644 --- a/src/data_availability/node_state/verifiers.rs +++ b/src/data_availability/node_state/verifiers.rs @@ -22,7 +22,7 @@ pub fn verify_proof( "sp1" => sp1_proof_verifier(proof, &program_id.0), _ => bail!("{} verifier not implemented yet", verifier), }?; - tracing::info!( + tracing::debug!( "🔎 {}", std::str::from_utf8(&hyle_output.program_outputs) .map(|o| format!("Program outputs: {o}")) @@ -49,7 +49,7 @@ pub fn verify_recursive_proof( _ => bail!("{} recursive verifier not implemented yet", verifier), }?; hyle_outputs.iter().for_each(|hyle_output| { - tracing::info!( + tracing::debug!( "🔎 {}", std::str::from_utf8(&hyle_output.program_outputs) .map(|o| format!("Program outputs: {o}")) diff --git a/src/tools/contract_runner.rs b/src/tools/contract_runner.rs new file mode 100644 index 00000000..e28abd82 --- /dev/null +++ b/src/tools/contract_runner.rs @@ -0,0 +1,128 @@ +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 fn prove_test(hyle_output: HyleOutput) -> Result { + Ok(ProofData::Bytes(serde_json::to_vec(&hyle_output)?)) + } +} + +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..4d81267c --- /dev/null +++ b/src/tools/transactions_builder.rs @@ -0,0 +1,344 @@ +use std::pin::Pin; + +use amm::AmmAction; +use anyhow::{bail, Error, Result}; +use hydentity::{AccountInfo, Hydentity}; +use hyle_contract_sdk::{ + erc20::ERC20Action, + identity_provider::{IdentityAction, IdentityVerification}, + Blob, BlobData, BlobIndex, ContractAction, ContractName, HyleOutput, Identity, StateDigest, +}; +use hyllar::HyllarToken; + +use crate::model::{BlobTransaction, ProofData, RegisterContractTransaction}; + +use super::contract_runner::ContractRunner; + +pub static HYLLAR_BIN: &[u8] = include_bytes!("../../contracts/hyllar/hyllar.img"); +pub static HYDENTITY_BIN: &[u8] = include_bytes!("../../contracts/hydentity/hydentity.img"); +pub static AMM_BIN: &[u8] = include_bytes!("../../contracts/amm/amm.img"); + +pub fn get_binary(contract_name: ContractName) -> Result<&'static [u8]> { + match contract_name.0.as_str() { + "hyllar" => Ok(HYLLAR_BIN), + "hydentity" => Ok(HYDENTITY_BIN), + "amm" => Ok(AMM_BIN), + _ => bail!("contract {} not supported", contract_name), + } +} + +#[derive(Debug, Clone)] +pub struct States { + pub hyllar: HyllarToken, + pub hydentity: Hydentity, +} + +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 BlobAction { + action: Box, // action + contract_name: ContractName, // contract name + private_input: Option, // private inputs + caller: Option, // caller + callees: Option>, // callees +} + +impl BlobAction { + pub fn as_blob(&self) -> Blob { + self.action.as_blob( + self.contract_name.clone(), + self.caller.clone(), + self.callees.clone(), + ) + } +} + +pub struct TransactionBuilder { + pub identity: Identity, + actions: 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, + actions: vec![], + runners: vec![], + } + } + + pub fn register_contract( + owner: &str, + verifier: &str, + program_id: &[u8], + state_digest: StateDigest, + contract_name: &str, + ) -> RegisterContractTransaction { + RegisterContractTransaction { + owner: owner.into(), + verifier: verifier.into(), + program_id: program_id.into(), + state_digest, + contract_name: contract_name.into(), + } + } + + pub fn to_blob_transaction(&self) -> BlobTransaction { + let blobs = self + .actions + .iter() + .map(|blob_action| blob_action.as_blob()) + .collect(); + BlobTransaction { + identity: self.identity.clone(), + blobs, + } + } + + fn add_action( + &mut self, + action: Box, + contract_name: ContractName, + private_input: Option, + caller: Option, + callees: Option>, + ) { + let action = BlobAction { + action, + contract_name, + private_input, + caller, + callees, + }; + self.actions.push(action); + } + + pub fn register_identity(&mut self, contract_name: ContractName, password: String) { + let password = BlobData(password.into_bytes().to_vec()); + + self.add_action( + Box::new(IdentityAction::RegisterIdentity { + account: self.identity.0.clone(), + }), + contract_name, + Some(password), + None, + None, + ); + } + + pub async fn verify_identity( + &mut self, + state: &Hydentity, + contract_name: ContractName, + password: String, + ) -> Result<(), Error> { + let nonce = get_nonce(state, &self.identity.0).await?; + let password = BlobData(password.into_bytes().to_vec()); + + self.add_action( + Box::new(IdentityAction::VerifyIdentity { + account: self.identity.0.clone(), + nonce, + }), + contract_name, + Some(password), + None, + None, + ); + + Ok(()) + } + + pub fn approve(&mut self, token: ContractName, spender: String, amount: u128) { + self.add_action( + Box::new(ERC20Action::Approve { spender, amount }), + token, + None, + None, + None, + ); + } + + pub fn transfer( + &mut self, + token: ContractName, + recipient: String, + amount: u128, + caller: Option, + callees: Option>, + ) { + self.add_action( + Box::new(ERC20Action::Transfer { recipient, amount }), + token, + None, + caller, + callees, + ); + } + + pub fn transfer_from( + &mut self, + token: ContractName, + sender: String, + recipient: String, + amount: u128, + caller: Option, + callees: Option>, + ) { + self.add_action( + Box::new(ERC20Action::TransferFrom { + sender, + recipient, + amount, + }), + token, + None, + caller, + callees, + ); + } + + pub fn swap( + &mut self, + sender: String, + amm_contract: ContractName, + pair: (String, String), + amounts: (u128, u128), + ) { + let latest_blob_index = self.actions.len(); + let callees = Some(vec![ + BlobIndex(latest_blob_index + 2), + BlobIndex(latest_blob_index + 3), + ]); + self.add_action( + Box::new(AmmAction::Swap { + pair: pair.clone(), + amounts, + }), + amm_contract.clone(), + None, + None, + callees, + ); + + // TransferFrom to the amm + self.transfer_from( + pair.0.into(), + sender.clone(), + amm_contract.0.clone(), + amounts.0, + Some(BlobIndex(latest_blob_index + 1)), + None, + ); + + // Transfer to the sender + self.transfer( + pair.1.into(), + sender, + amounts.1, + Some(BlobIndex(latest_blob_index + 1)), + None, + ); + } + + // TODO: make it generic + pub async fn build(&mut self, states: &mut States) -> Result { + let mut new_states = states.clone(); + let mut outputs = vec![]; + + let blob_transaction = self.to_blob_transaction(); + + for (i, blob_action) in self.actions.iter().enumerate() { + let contract_name = blob_action.contract_name.clone(); + let private_input = blob_action.private_input.clone(); + let blob_index = BlobIndex(i); + + let runner = ContractRunner::new( + contract_name.clone(), + get_binary(contract_name.clone())?, + self.identity.clone(), + private_input.unwrap_or(BlobData(vec![])), + blob_transaction.blobs.clone(), + blob_index, + new_states.for_token(&contract_name)?.clone(), + ) + .await?; + let out = runner.execute()?; + new_states.hydentity = out.next_state.clone().try_into()?; + outputs.push(("hydentity".into(), out)); + self.runners.push(runner); + } + + *states = new_states; + + Ok(BuildResult { + identity: self.identity.clone(), + blobs: blob_transaction.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/src/utils/conf.rs b/src/utils/conf.rs index e22e48b8..0c4e3f76 100644 --- a/src/utils/conf.rs +++ b/src/utils/conf.rs @@ -3,11 +3,6 @@ use config::{Config, ConfigError, Environment, File}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fmt::Debug, path::PathBuf, sync::Arc}; -#[derive(Serialize, Deserialize, Clone, Debug, Default)] -pub struct Storage { - pub interval: u64, -} - #[derive(Serialize, Deserialize, Clone, Debug, Default)] pub struct Consensus { pub slot_duration: u64, @@ -26,7 +21,6 @@ pub struct Conf { pub host: String, pub p2p_listen: bool, pub peers: Vec, - pub storage: Storage, pub consensus: Consensus, pub rest: String, pub database_url: String, diff --git a/src/utils/conf_defaults.ron b/src/utils/conf_defaults.ron index e19445df..1366070e 100644 --- a/src/utils/conf_defaults.ron +++ b/src/utils/conf_defaults.ron @@ -4,15 +4,12 @@ Config( p2p_listen: true, host: "127.0.0.1:1231", peers: [], - storage: Storage( - interval: 10 - ), log_format: "full", rest: "127.0.0.1:4321", data_directory: "data_node", database_url: "postgres://postgres:postgres@localhost:5432/postgres", consensus: Consensus ( - slot_duration: 1000, + slot_duration: 10000, // Has to be empty as config is additive genesis_stakers: {} ), diff --git a/src/utils/logger.rs b/src/utils/logger.rs index 62f96ed4..2fe7a047 100644 --- a/src/utils/logger.rs +++ b/src/utils/logger.rs @@ -92,7 +92,7 @@ pub fn setup_tracing(mode: TracingMode, node_name: String) -> Result<()> { } if !var.contains("tower_http") { // API request/response debug tracing - filter = filter.add_directive("tower_http::trace=debug".parse()?); + filter = filter.add_directive("tower_http::trace=info".parse()?); } // Can't use match inline because these are different return types diff --git a/tests/amm_test.rs b/tests/amm_test.rs index 8d04b901..c815d88e 100644 --- a/tests/amm_test.rs +++ b/tests/amm_test.rs @@ -17,7 +17,7 @@ mod e2e_amm { use hyle_contract_sdk::{ erc20::{ERC20Action, ERC20}, identity_provider::{IdentityAction, IdentityVerification}, - BlobIndex, ContractName, + BlobIndex, ContractAction, ContractName, }; use hyrun::{CliCommand, HydentityArgs}; @@ -114,7 +114,7 @@ mod e2e_amm { vec![IdentityAction::RegisterIdentity { account: "bob.hydentity".to_string(), } - .as_blob(ContractName("hydentity".to_owned()))], + .as_blob(ContractName("hydentity".to_owned()), None, None)], ) .await?; @@ -170,7 +170,7 @@ mod e2e_amm { account: "faucet.hydentity".to_string(), nonce: 0, } - .as_blob(ContractName("hydentity".to_owned())), + .as_blob(ContractName("hydentity".to_owned()), None, None), ERC20Action::Transfer { recipient: "bob.hydentity".to_string(), amount: 25, @@ -248,7 +248,7 @@ mod e2e_amm { account: "faucet.hydentity".to_string(), nonce: 1, } - .as_blob(ContractName("hydentity".to_owned())), + .as_blob(ContractName("hydentity".to_owned()), None, None), ERC20Action::Transfer { recipient: "bob.hydentity".to_string(), amount: 50, @@ -324,7 +324,7 @@ mod e2e_amm { account: "bob.hydentity".to_string(), nonce: 0, } - .as_blob(ContractName("hydentity".to_owned())), + .as_blob(ContractName("hydentity".to_owned()), None, None), ERC20Action::Approve { spender: AMM_CONTRACT_NAME.to_string(), amount: 100, @@ -385,7 +385,7 @@ mod e2e_amm { account: "bob.hydentity".to_string(), nonce: 1, } - .as_blob(ContractName("hydentity".to_owned())), + .as_blob(ContractName("hydentity".to_owned()), None, None), ERC20Action::Approve { spender: AMM_CONTRACT_NAME.to_string(), amount: 100, @@ -446,7 +446,7 @@ mod e2e_amm { account: "bob.hydentity".to_string(), nonce: 2, } - .as_blob(ContractName("hydentity".to_owned())), + .as_blob(ContractName("hydentity".to_owned()), None, None), AmmAction::NewPair { pair: ("hyllar".to_string(), "hyllar2".to_string()), amounts: (20, 50), @@ -571,7 +571,7 @@ mod e2e_amm { account: "bob.hydentity".to_string(), nonce: 3, } - .as_blob(ContractName("hydentity".to_owned())), + .as_blob(ContractName("hydentity".to_owned()), None, None), AmmAction::Swap { pair: ("hyllar".to_string(), "hyllar2".to_string()), amounts: (5, 10), diff --git a/tests/hyllar_test.rs b/tests/hyllar_test.rs index a7eddfa1..87f52f05 100644 --- a/tests/hyllar_test.rs +++ b/tests/hyllar_test.rs @@ -13,7 +13,7 @@ mod e2e_hyllar { use hyle_contract_sdk::{ erc20::{ERC20Action, ERC20}, identity_provider::{IdentityAction, IdentityVerification}, - ContractName, + ContractAction, ContractName, }; use hyrun::CliCommand; @@ -29,7 +29,7 @@ mod e2e_hyllar { vec![IdentityAction::RegisterIdentity { account: "bob.hydentity".to_string(), } - .as_blob(ContractName("hydentity".to_owned()))], + .as_blob(ContractName("hydentity".to_owned()), None, None)], ) .await?; @@ -83,7 +83,7 @@ mod e2e_hyllar { account: "faucet.hydentity".to_string(), nonce: 0, } - .as_blob(ContractName("hydentity".to_owned())), + .as_blob(ContractName("hydentity".to_owned()), None, None), ERC20Action::Transfer { recipient: "bob.hydentity".to_string(), amount: 25,