diff --git a/crates/client-sdk/src/helpers.rs b/crates/client-sdk/src/helpers.rs index 151f875ad..3df767019 100644 --- a/crates/client-sdk/src/helpers.rs +++ b/crates/client-sdk/src/helpers.rs @@ -173,7 +173,7 @@ pub mod test { success: true, tx_hash: contract_input.tx_hash.clone(), tx_ctx: None, - registered_contracts: vec![], + onchain_effects: vec![], program_outputs: vec![], }; Ok(hyle_output) diff --git a/crates/contract-sdk/src/guest.rs b/crates/contract-sdk/src/guest.rs index 26442abc3..a449fae6b 100644 --- a/crates/contract-sdk/src/guest.rs +++ b/crates/contract-sdk/src/guest.rs @@ -64,7 +64,7 @@ pub fn fail(input: ContractInput, initial_state_digest: StateDigest, message: &s success: false, tx_hash: input.tx_hash, tx_ctx: input.tx_ctx, - registered_contracts: vec![], + onchain_effects: vec![], program_outputs: message.to_string().into_bytes(), } } diff --git a/crates/contract-sdk/src/lib.rs b/crates/contract-sdk/src/lib.rs index 0dd8daa41..68744d527 100644 --- a/crates/contract-sdk/src/lib.rs +++ b/crates/contract-sdk/src/lib.rs @@ -44,7 +44,7 @@ macro_rules! info { } } -pub type RunResult = Result<(String, ExecutionContext, Vec), String>; +pub type RunResult = Result<(String, ExecutionContext, Vec), String>; pub trait HyleContract { fn execute(&mut self, contract_input: &ContractInput) -> RunResult; diff --git a/crates/contract-sdk/src/utils.rs b/crates/contract-sdk/src/utils.rs index 1491c5da7..26ab1cf79 100644 --- a/crates/contract-sdk/src/utils.rs +++ b/crates/contract-sdk/src/utils.rs @@ -98,7 +98,7 @@ pub fn as_hyle_output( res: &mut crate::RunResult, ) -> HyleOutput { match res { - Ok((ref mut program_output, execution_context, ref mut registered_contracts)) => { + Ok((ref mut program_output, execution_context, ref mut onchain_effects)) => { if !execution_context.callees_blobs.is_empty() { return fail( contract_input, @@ -119,7 +119,7 @@ pub fn as_hyle_output( success: true, tx_hash: contract_input.tx_hash, tx_ctx: contract_input.tx_ctx, - registered_contracts: core::mem::take(registered_contracts), + onchain_effects: core::mem::take(onchain_effects), program_outputs: core::mem::take(program_output).into_bytes(), } } diff --git a/crates/contracts/amm/amm.img b/crates/contracts/amm/amm.img index 7349cf264..ff9c13d8f 100644 Binary files a/crates/contracts/amm/amm.img and b/crates/contracts/amm/amm.img differ diff --git a/crates/contracts/amm/amm.txt b/crates/contracts/amm/amm.txt index eced442fc..5bca3efdc 100644 --- a/crates/contracts/amm/amm.txt +++ b/crates/contracts/amm/amm.txt @@ -1 +1 @@ -22ac110f3f834c8280eb889e6cdfc42603e1e02d365e6d3d6c6c599d0ea3c9f3 \ No newline at end of file +a7e2b0a643d4b9bcaf6150b6150727c99e252770a22c99489addd4c2ede9a1ac \ No newline at end of file diff --git a/crates/contracts/hydentity/hydentity.img b/crates/contracts/hydentity/hydentity.img index 992028459..767834537 100644 Binary files a/crates/contracts/hydentity/hydentity.img and b/crates/contracts/hydentity/hydentity.img differ diff --git a/crates/contracts/hydentity/hydentity.txt b/crates/contracts/hydentity/hydentity.txt index a3dba116a..26000e270 100644 --- a/crates/contracts/hydentity/hydentity.txt +++ b/crates/contracts/hydentity/hydentity.txt @@ -1 +1 @@ -0eebe0bf4ec1f41e04657ce85ebada2db7b794b58cae8ede5d4f5cbb0ee7ea52 \ No newline at end of file +fb7fcd711f9daa9d38f7009c3dcc8a872fd65d4255252be404a94a47aa58c3c6 \ No newline at end of file diff --git a/crates/contracts/hyllar/hyllar.img b/crates/contracts/hyllar/hyllar.img index ef2d37e84..b04bbe28e 100644 Binary files a/crates/contracts/hyllar/hyllar.img and b/crates/contracts/hyllar/hyllar.img differ diff --git a/crates/contracts/hyllar/hyllar.txt b/crates/contracts/hyllar/hyllar.txt index ba387a73b..45b86f187 100644 --- a/crates/contracts/hyllar/hyllar.txt +++ b/crates/contracts/hyllar/hyllar.txt @@ -1 +1 @@ -a9a8e1c923319fd8294d588a8292cae29d64bc803a3e4154f6178ee49b1c187f \ No newline at end of file +88d35bcce3c7b8bdef7093f6c24182a377b9491a82578ea336c26ea42f4b1f18 \ No newline at end of file diff --git a/crates/contracts/risc0-recursion/risc0-recursion.img b/crates/contracts/risc0-recursion/risc0-recursion.img index 80fea72e4..4a10d6d37 100644 Binary files a/crates/contracts/risc0-recursion/risc0-recursion.img and b/crates/contracts/risc0-recursion/risc0-recursion.img differ diff --git a/crates/contracts/risc0-recursion/risc0-recursion.txt b/crates/contracts/risc0-recursion/risc0-recursion.txt index f51932766..0cb399eac 100644 --- a/crates/contracts/risc0-recursion/risc0-recursion.txt +++ b/crates/contracts/risc0-recursion/risc0-recursion.txt @@ -1 +1 @@ -e5e79abef98ee0f3efacd9442f1c20890cced17a25ecf210f052ffcb065dca36 \ No newline at end of file +55ebfdcda965221c5d576841b6fa38d8582e8fcad4c5183ee31ed4874a32ef9b \ No newline at end of file diff --git a/crates/contracts/staking/staking.img b/crates/contracts/staking/staking.img index d4ab109cf..b3cd63e78 100644 Binary files a/crates/contracts/staking/staking.img and b/crates/contracts/staking/staking.img differ diff --git a/crates/contracts/staking/staking.txt b/crates/contracts/staking/staking.txt index 204c5d66e..677a33ae4 100644 --- a/crates/contracts/staking/staking.txt +++ b/crates/contracts/staking/staking.txt @@ -1 +1 @@ -9151ca0f22f0513e1f2f0c5bbe4b81b72fbc7252dcdbb060c820fcff8c55c521 \ No newline at end of file +1ec82aa7092fc5be42561dbdc4a71f1708bca82fa414e77fef2a42e1608b1ea0 \ No newline at end of file diff --git a/crates/contracts/uuid-tld/src/lib.rs b/crates/contracts/uuid-tld/src/lib.rs index 13e14f4a0..9bacd33b5 100644 --- a/crates/contracts/uuid-tld/src/lib.rs +++ b/crates/contracts/uuid-tld/src/lib.rs @@ -5,8 +5,8 @@ use rand::Rng; use rand_seeder::SipHasher; use sdk::{ info, utils::parse_raw_contract_input, Blob, BlobData, BlobIndex, ContractAction, - ContractInput, ContractName, Digestable, HyleContract, ProgramId, RegisterContractEffect, - RunResult, StateDigest, Verifier, + ContractInput, ContractName, Digestable, HyleContract, OnchainEffect, ProgramId, + RegisterContractEffect, RunResult, StateDigest, Verifier, }; use uuid::Uuid; @@ -57,7 +57,7 @@ impl HyleContract for UuidTld { Ok(( format!("registered {}", id.clone()), exec_ctx, - vec![RegisterContractEffect { + vec![OnchainEffect::RegisterContract(RegisterContractEffect { contract_name: format!( "{}.{}", id, contract_input.blobs[contract_input.index.0].contract_name.0 @@ -66,7 +66,7 @@ impl HyleContract for UuidTld { verifier: action.verifier, program_id: action.program_id, state_digest: action.state_digest, - }], + })], )) } } @@ -158,10 +158,11 @@ mod test { let contract_input = make_contract_input(action.clone(), borsh::to_vec(&state).unwrap()); - let (_, _, registered_contracts) = state.execute(&contract_input).unwrap(); - - let effect: &RegisterContractEffect = registered_contracts.first().unwrap(); + let (_, _, onchain_effects) = state.execute(&contract_input).unwrap(); + let OnchainEffect::RegisterContract(effect) = onchain_effects.first().unwrap() else { + panic!("Expected RegisterContract effect"); + }; assert_eq!( effect.contract_name.0, "7de07efe-e91d-45f7-a5d2-0b813c1d3e10.uuid" @@ -169,9 +170,11 @@ mod test { let contract_input = make_contract_input(action.clone(), borsh::to_vec(&state).unwrap()); - let (_, _, registered_contracts) = state.execute(&contract_input).unwrap(); + let (_, _, onchain_effects) = state.execute(&contract_input).unwrap(); - let effect = registered_contracts.first().unwrap(); + let OnchainEffect::RegisterContract(effect) = onchain_effects.first().unwrap() else { + panic!("Expected RegisterContract effect"); + }; assert_eq!( effect.contract_name.0, diff --git a/crates/contracts/uuid-tld/uuid-tld.img b/crates/contracts/uuid-tld/uuid-tld.img index 07025737e..af38c03f5 100644 Binary files a/crates/contracts/uuid-tld/uuid-tld.img and b/crates/contracts/uuid-tld/uuid-tld.img differ diff --git a/crates/contracts/uuid-tld/uuid-tld.txt b/crates/contracts/uuid-tld/uuid-tld.txt index efcb64bba..98837eb00 100644 --- a/crates/contracts/uuid-tld/uuid-tld.txt +++ b/crates/contracts/uuid-tld/uuid-tld.txt @@ -1 +1 @@ -13e4c348917b5d83138b71aee0eef86a182284245b9cf0b764fba29eba552ce6 \ No newline at end of file +8cfdb6ed48c6c61883ea31c5e1f22745f7575e35985a188edc8284d9f2590af7 \ No newline at end of file diff --git a/crates/hyle-model/src/block.rs b/crates/hyle-model/src/block.rs index faf1ce007..72a5fc9d8 100644 --- a/crates/hyle-model/src/block.rs +++ b/crates/hyle-model/src/block.rs @@ -26,6 +26,7 @@ pub struct Block { pub new_bounded_validators: Vec, pub staking_actions: Vec<(Identity, StakingAction)>, pub registered_contracts: Vec<(TxHash, RegisterContractEffect)>, + pub deleted_contracts: Vec<(TxHash, ContractName)>, pub updated_states: BTreeMap, pub transactions_events: BTreeMap>, } diff --git a/crates/hyle-model/src/contract.rs b/crates/hyle-model/src/contract.rs index 229e4278d..0071ba4c6 100644 --- a/crates/hyle-model/src/contract.rs +++ b/crates/hyle-model/src/contract.rs @@ -316,6 +316,17 @@ pub struct Verifier(pub String); #[cfg_attr(feature = "full", derive(utoipa::ToSchema))] pub struct ProgramId(pub Vec); +#[derive( + Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize, +)] +#[cfg_attr(feature = "full", derive(utoipa::ToSchema))] +/// Enum for various side-effects blobs can have on the chain. +/// This is implemented as an enum for easier forward compatibility. +pub enum OnchainEffect { + RegisterContract(RegisterContractEffect), + DeleteContract(ContractName), +} + #[derive( Default, Serialize, @@ -342,7 +353,7 @@ pub struct HyleOutput { // Optional - if empty, these won't be checked, but also can't be used inside the program. pub tx_ctx: Option, - pub registered_contracts: Vec, + pub onchain_effects: Vec, pub program_outputs: Vec, } @@ -507,6 +518,7 @@ impl Add for BlockHeight { #[derive( Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, )] +/// Used as a blob action to register a contract in the 'hyle' TLD. pub struct RegisterContractAction { pub verifier: Verifier, pub program_id: ProgramId, @@ -547,6 +559,32 @@ impl ContractAction for RegisterContractAction { } } +#[derive( + Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, +)] +/// Used as a blob action to delete a contract in the 'hyle' TLD. +pub struct DeleteContractAction { + pub contract_name: ContractName, +} + +impl ContractAction for DeleteContractAction { + fn as_blob( + &self, + contract_name: ContractName, + caller: Option, + callees: Option>, + ) -> Blob { + Blob { + contract_name, + data: BlobData::from(StructuredBlobData { + caller, + callees, + parameters: self.clone(), + }), + } + } +} + /// Used by the Hylé node to recognize contract registration. /// Simply output this struct in your HyleOutput registered_contracts. /// See uuid-tld for examples. diff --git a/crates/hyle-model/src/node/data_availability.rs b/crates/hyle-model/src/node/data_availability.rs index ffcc62e20..1895046fe 100644 --- a/crates/hyle-model/src/node/data_availability.rs +++ b/crates/hyle-model/src/node/data_availability.rs @@ -118,10 +118,11 @@ impl Hashed for HyleOutput { hasher.update(self.index.0.to_le_bytes()); hasher.update(&self.blobs); hasher.update([self.success as u8]); - hasher.update(self.registered_contracts.len().to_le_bytes()); - self.registered_contracts - .iter() - .for_each(|c| hasher.update(contract::Hashed::hashed(c).0)); + hasher.update(self.onchain_effects.len().to_le_bytes()); + self.onchain_effects.iter().for_each(|c| match c { + OnchainEffect::RegisterContract(c) => hasher.update(contract::Hashed::hashed(c).0), + OnchainEffect::DeleteContract(cn) => hasher.update(cn.0.as_bytes()), + }); hasher.update(&self.program_outputs); HyleOutputHash(hasher.finalize().to_vec()) } diff --git a/crates/hyle-verifiers/src/lib.rs b/crates/hyle-verifiers/src/lib.rs index f6bdd7fb4..7649c4f20 100644 --- a/crates/hyle-verifiers/src/lib.rs +++ b/crates/hyle-verifiers/src/lib.rs @@ -225,7 +225,7 @@ mod tests { success: true, tx_hash: TxHash::default(), // TODO tx_ctx: None, - registered_contracts: vec![], + onchain_effects: vec![], program_outputs: vec![] }] ); diff --git a/crates/hyle-verifiers/src/noir_utils.rs b/crates/hyle-verifiers/src/noir_utils.rs index a548b14f0..fed821050 100644 --- a/crates/hyle-verifiers/src/noir_utils.rs +++ b/crates/hyle-verifiers/src/noir_utils.rs @@ -33,7 +33,7 @@ pub fn parse_noir_output(vector: &mut Vec) -> Result index: BlobIndex(index as usize), blobs, success, - registered_contracts: vec![], + onchain_effects: vec![], program_outputs: vec![], }) } diff --git a/src/indexer.rs b/src/indexer.rs index 1d70feb5c..839f491b1 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -922,7 +922,7 @@ mod test { index: blob_index, blobs, success: true, - registered_contracts: vec![], + onchain_effects: vec![], program_outputs: vec![], }, }], diff --git a/src/mempool/verifiers.rs b/src/mempool/verifiers.rs index db54de2bd..9e02cef94 100644 --- a/src/mempool/verifiers.rs +++ b/src/mempool/verifiers.rs @@ -137,7 +137,7 @@ pub fn verify_native( success, tx_hash, tx_ctx: None, - registered_contracts: vec![], + onchain_effects: vec![], program_outputs: vec![], } } diff --git a/src/node_state.rs b/src/node_state.rs index 796b05255..5d9dd74b3 100644 --- a/src/node_state.rs +++ b/src/node_state.rs @@ -1,6 +1,8 @@ //! State required for participation in consensus by the node. -use crate::model::contract_registration::validate_state_digest_size; +use crate::model::contract_registration::{ + validate_contract_name_registration, validate_state_digest_size, +}; use crate::model::verifiers::NativeVerifiers; use crate::model::*; use crate::{ @@ -9,6 +11,7 @@ use crate::{ use anyhow::{bail, Context, Error, Result}; use borsh::{BorshDeserialize, BorshSerialize}; use hyle_contract_sdk::{utils::parse_structured_blob, BlobIndex, HyleOutput, TxHash}; +use hyle_tld::handle_blob_for_hyle_tld; use ordered_tx_map::OrderedTxMap; use std::{ collections::{BTreeMap, BTreeSet, HashMap}, @@ -18,19 +21,42 @@ use timeouts::Timeouts; use tracing::{debug, error, info, trace}; mod api; +mod hyle_tld; pub mod module; mod ordered_tx_map; mod timeouts; -pub struct SettledTxOutput { +struct SettledTxOutput { // Original blob transaction, now settled. pub tx: UnsettledBlobTransaction, - /// This is the index of the blob proof output used in the blob settlement, for each blob. - pub blob_proof_output_indices: Vec, - /// New data for contracts modified by the settled TX. - pub updated_contracts: BTreeMap, - /// Whether the transaction is settled as a success or a failure. - pub success: bool, + /// Result of the settlement + pub result: Result, +} + +#[derive(Debug, Clone)] +// Similar to OnchainEffect but slightly more adapted to nodestate settlement +enum SideEffect { + Register(Contract), + // Similar to register but the contract already existed + Update(Contract), + Delete(ContractName), +} + +impl SideEffect { + fn apply(&mut self, other_effect: SideEffect) { + tracing::trace!("Applying side effect: {:?} -> {:?}", self, other_effect); + match (self, other_effect) { + (SideEffect::Register(reg), SideEffect::Update(contract)) => reg.state = contract.state, + (SideEffect::Delete(_), SideEffect::Update(_)) => {} + (me, other) => *me = other, + } + } +} + +#[derive(Debug, Clone)] +struct SettlementResult { + contract_changes: BTreeMap, + blob_proof_output_indices: Vec, } /// NodeState manages the flattened, up-to-date state of the chain. @@ -94,6 +120,7 @@ impl NodeState { .collect(), timed_out_txs: vec![], // Added below as it needs the block registered_contracts: vec![], + deleted_contracts: vec![], updated_states: BTreeMap::new(), transactions_events: BTreeMap::new(), dp_hashes: BTreeMap::new(), @@ -243,6 +270,7 @@ impl NodeState { .iter() .enumerate() .map(|(index, blob)| { + tracing::trace!("Handling blob - {:?}", blob); if let Some(Ok(verifier)) = self .contracts .get(&blob.contract_name) @@ -254,31 +282,13 @@ impl NodeState { &tx.blobs, verifier, ); + tracing::trace!("Nativer verifier in blob tx - {:?}", hyle_output); return UnsettledBlobMetadata { blob: blob.clone(), possible_proofs: vec![(verifier.into(), hyle_output)], }; } else if blob.contract_name.0 == "hyle" { - // Special case for 'hyle' - we generate a fake proof like for native verifiers - // but this proof will need to be checked at settling time as it's stateful. - if let Ok(reg) = - StructuredBlobData::::try_from(blob.data.clone()) - { - let synthetic_output = HyleOutput { - success: true, - registered_contracts: vec![RegisterContractEffect { - contract_name: reg.parameters.contract_name, - verifier: reg.parameters.verifier, - program_id: reg.parameters.program_id, - state_digest: reg.parameters.state_digest, - }], - ..HyleOutput::default() - }; - return UnsettledBlobMetadata { - blob: blob.clone(), - possible_proofs: vec![(ProgramId(vec![]), synthetic_output)], - }; - } + // 'hyle' is a special case -> See settlement logic. } else { should_try_and_settle = false; } @@ -410,19 +420,14 @@ impl NodeState { match self.try_to_settle_blob_tx(&bth, events) { Ok(SettledTxOutput { tx: settled_tx, - blob_proof_output_indices, - updated_contracts: tx_updated_contracts, - success, + result, }) => { // Settle the TX and add any new TXs to try and settle next. - blob_tx_to_try_and_settle.append(&mut self.on_settled_blob_tx( - block_under_construction, - bth, - settled_tx, - blob_proof_output_indices, - tx_updated_contracts, - success, - )); + if let Some(mut txs) = + self.on_settled_blob_tx(block_under_construction, bth, settled_tx, result) + { + blob_tx_to_try_and_settle.append(&mut txs) + } } Err(e) => { unsettlable_txs.insert(bth.clone()); @@ -452,6 +457,7 @@ impl NodeState { // Sanity check: if some of the blob contracts are not registered, we can't proceed if !unsettled_tx.blobs.iter().all(|blob_metadata| { + tracing::trace!("Checking contract: {:?}", blob_metadata.blob.contract_name); self.contracts .contains_key(&blob_metadata.blob.contract_name) }) { @@ -460,19 +466,18 @@ impl NodeState { let updated_contracts = BTreeMap::new(); - let (updated_contracts, blob_proof_output_indices, success) = - match Self::settle_blobs_recursively( - &self.contracts, - updated_contracts, - unsettled_tx.blobs.iter(), - vec![], - events, - ) { - Some(res) => res, - None => { - bail!("Tx: {} is not ready to settle.", unsettled_tx.hash); - } - }; + let result = match Self::settle_blobs_recursively( + &self.contracts, + updated_contracts, + unsettled_tx.blobs.iter(), + vec![], + events, + ) { + Some(res) => res, + None => { + bail!("Tx: {} is not ready to settle.", unsettled_tx.hash); + } + }; // We are OK to settle now. @@ -484,50 +489,48 @@ impl NodeState { Ok(SettledTxOutput { tx: unsettled_tx, - blob_proof_output_indices, - updated_contracts, - success, + result, }) } fn settle_blobs_recursively<'a>( contracts: &HashMap, - current_contracts: BTreeMap, + mut contract_changes: BTreeMap, mut blob_iter: impl Iterator + Clone, mut blob_proof_output_indices: Vec, events: &mut Vec, - ) -> Option<(BTreeMap, Vec, bool)> { + ) -> Option> { // Recursion end-case: we succesfully settled all prior blobs, so success. let Some(current_blob) = blob_iter.next() else { - return Some((current_contracts, blob_proof_output_indices, true)); + tracing::trace!("Settlement - Done"); + return Some(Ok(SettlementResult { + contract_changes, + blob_proof_output_indices, + })); }; let contract_name = ¤t_blob.blob.contract_name; + blob_proof_output_indices.push(0); + #[allow( clippy::unwrap_used, reason = "all contract names are validated to exist above" )] - let known_contract_state = current_contracts - .get(contract_name) - .unwrap_or(contracts.get(contract_name).unwrap()); - - blob_proof_output_indices.push(0); - // Super special case - the hyle contract has "synthetic proofs". // We need to check the current state of 'current_contracts' to check validity, // so we really can't do this before we've settled the earlier blobs. if contract_name.0 == "hyle" { - return match Self::handle_blob_for_hyle_tld( + tracing::trace!("Settlement - processing for Hyle"); + return match handle_blob_for_hyle_tld( contracts, - ¤t_contracts, + &mut contract_changes, ¤t_blob.blob, ) { - Ok(contract) => { - let mut us = current_contracts.clone(); - us.insert(contract.name.clone(), contract); + Ok(()) => { + tracing::trace!("Settlement - OK side effect"); Self::settle_blobs_recursively( contracts, - us, + contract_changes, blob_iter.clone(), blob_proof_output_indices.clone(), events, @@ -538,53 +541,63 @@ impl NodeState { let msg = format!("Could not settle blob proof output for 'hyle': {:?}", err); debug!("{msg}"); events.push(TransactionStateEvent::SettleEvent(msg)); - Some((current_contracts, blob_proof_output_indices, false)) + Some(Err(())) } }; } + let Some(known_contract_state) = contract_changes + .get(contract_name) + .and_then(|c| match c { + SideEffect::Register(c) => Some(c), + SideEffect::Update(c) => Some(c), + _ => None, + }) + .or(contracts.get(contract_name)) + else { + // Contract not found (presumably no longer exists), we can't settle this TX. + let msg = format!( + "Cannot settle blob, contract '{}' no longer exists", + contract_name + ); + debug!("{msg}"); + events.push(TransactionStateEvent::SettleEvent(msg)); + return Some(Err(())); + }; + // Regular case: go through each proof for this blob. If they settle, carry on recursively. for (i, proof_metadata) in current_blob.possible_proofs.iter().enumerate() { #[allow(clippy::unwrap_used, reason = "pushed above so last must exist")] let blob_index = blob_proof_output_indices.last_mut().unwrap(); *blob_index = i; - if !Self::validate_proof_metadata(proof_metadata, known_contract_state) { + // TODO: ideally make this CoW + let mut current_contracts = contract_changes.clone(); + if let Err(msg) = + Self::process_proof(&mut current_contracts, proof_metadata, known_contract_state) + { // Not a valid proof, log it and try the next one. let msg = format!( - "Could not settle blob proof output #{} for contract '{}'. Expected initial state: {:?}, got: {:?}, expected program ID: {:?}, got: {:?}", - i, - contract_name, - known_contract_state.state, - proof_metadata.1.initial_state, - known_contract_state.program_id, - proof_metadata.0 + "Could not settle blob proof output #{} for contract '{}': {}", + i, contract_name, msg ); debug!("{msg}"); events.push(TransactionStateEvent::SettleEvent(msg)); continue; } + if !proof_metadata.1.success { // We have a valid proof of failure, we short-circuit. let msg = format!("Proven failure for blob {}", i); debug!("{msg}"); events.push(TransactionStateEvent::SettleEvent(msg)); - return Some((current_contracts, blob_proof_output_indices, false)); + return Some(Err(())); } - // TODO: ideally make this CoW - let mut us = current_contracts.clone(); - us.insert( - contract_name.clone(), - Contract { - name: contract_name.clone(), - program_id: proof_metadata.0.clone(), - state: proof_metadata.1.next_state.clone(), - verifier: known_contract_state.verifier.clone(), - }, - ); + + tracing::trace!("Settlement - OK blob"); match Self::settle_blobs_recursively( contracts, - us, + current_contracts, blob_iter.clone(), blob_proof_output_indices.clone(), events, @@ -607,27 +620,42 @@ impl NodeState { block_under_construction: &mut Block, bth: TxHash, settled_tx: UnsettledBlobTransaction, - blob_proof_output_indices: Vec, - tx_updated_contracts: BTreeMap, - success: bool, - ) -> BTreeSet { + tx_result: Result, + ) -> Option> { // Transaction was settled, update our state. - if success { - block_under_construction - .transactions_events - .entry(bth.clone()) - .or_default() - .push(TransactionStateEvent::Settled); - info!("✨ Settled tx {}", &bth); - } else { + + // Insert dp hash of the tx, whether its a success or not + block_under_construction + .dp_hashes + .insert(settled_tx.hash, settled_tx.parent_dp_hash); + + // If it's a failed settlement, mark it so and move on. + if tx_result.is_err() { block_under_construction .transactions_events .entry(bth.clone()) .or_default() .push(TransactionStateEvent::SettledAsFailed); info!("⛈️ Settled tx {} has failed", &bth); + + block_under_construction.failed_txs.push(bth); + return None; } + // Otherwise process the side-effects. + #[allow(clippy::unwrap_used, reason = "must exist because of above checks")] + let SettlementResult { + contract_changes: contracts_changes, + blob_proof_output_indices, + } = tx_result.unwrap(); + + block_under_construction + .transactions_events + .entry(bth.clone()) + .or_default() + .push(TransactionStateEvent::Settled); + info!("✨ Settled tx {}", &bth); + // Keep track of which blob proof output we used to settle the TX for each blob. // Also note all the TXs that we might want to try and settle next let next_txs_to_try_and_settle = settled_tx @@ -647,106 +675,94 @@ impl NodeState { }) .collect::>(); - // Insert dp hash of the tx, whether its a success or not - block_under_construction - .dp_hashes - .insert(settled_tx.hash, settled_tx.parent_dp_hash); + // Take note of staking + for blob_metadata in settled_tx.blobs.into_iter() { + let blob = blob_metadata.blob; + // Keep track of all stakers + if blob.contract_name.0 == "staking" { + if let Some(structured_blob) = parse_structured_blob(&[blob], &BlobIndex(0)) { + let staking_action: StakingAction = structured_blob.data.parameters; - // Handle side-effect of each blobs on the node. - if !success { - block_under_construction.failed_txs.push(bth); - } else { - // Take note of staking and contract registration - for (i, mut blob_metadata) in settled_tx.blobs.into_iter().enumerate() { - #[allow(clippy::indexing_slicing, reason = "all exist by construction")] - let settled_proof = blob_metadata - .possible_proofs - .remove(blob_proof_output_indices[i]); - - for rce in settled_proof.1.registered_contracts { - self.handle_register_contract_effect(&rce); block_under_construction - .registered_contracts - .push((bth.clone(), rce)); + .staking_actions + .push((settled_tx.identity.clone(), staking_action)); + } else { + error!("Failed to parse StakingAction"); } + } + } - let blob = blob_metadata.blob; - // Keep track of all stakers - if blob.contract_name.0 == "staking" { - if let Some(structured_blob) = parse_structured_blob(&[blob], &BlobIndex(0)) { - let staking_action: StakingAction = structured_blob.data.parameters; + // Update contract states + for (_, side_effect) in contracts_changes.into_iter() { + match side_effect { + SideEffect::Delete(contract_name) => { + debug!("✏️ Delete {} contract", contract_name); + self.contracts.remove(&contract_name); - block_under_construction - .staking_actions - .push((settled_tx.identity.clone(), staking_action)); + block_under_construction + .deleted_contracts + .push((bth.clone(), contract_name)); + } + SideEffect::Register(contract) => { + let has_contract = self.contracts.contains_key(&contract.name); + if has_contract { + debug!( + "✍️ Modify '{}', contract state: {:?}", + &contract.name, contract.state + ); } else { - error!("Failed to parse StakingAction"); + debug!( + "📝 Register '{}', contract state: {:?}", + &contract.name, contract.state + ); } - } - } + self.contracts + .insert(contract.name.clone(), contract.clone()); + + block_under_construction.registered_contracts.push(( + bth.clone(), + RegisterContractEffect { + contract_name: contract.name.clone(), + program_id: contract.program_id.clone(), + state_digest: contract.state.clone(), + verifier: contract.verifier.clone(), + }, + )); - // Keep track of settled txs - block_under_construction.successful_txs.push(bth); - - // Update contract states - // Have to put the clippy here because it's experimental on expressions - #[allow( - clippy::unwrap_used, - reason = "all contract names are validated to exist above" - )] - for (contract_name, next_state) in tx_updated_contracts.iter() { - debug!( - "Update {} contract state: {:?}", - contract_name, next_state.state - ); - self.contracts.get_mut(contract_name).unwrap().state = next_state.state.clone(); // unwrap, see above ^ + // TODO: would be nice to have a drain-like API here. + block_under_construction + .updated_states + .insert(contract.name, contract.state); + } + // clippy lint set here because setting it on expressions is experimental + #[allow(clippy::unwrap_used, reason = "we check existence before get_mut")] + SideEffect::Update(contract) => { + if !self.contracts.contains_key(&contract.name) { + // We presume this was because it's been deleted so everything is OK. + debug!( + "🪦 Updating contract {} cannot happen - no longer exists", + &contract.name + ); + continue; + } + debug!( + "✍️ Update {} contract state: {:?}", + &contract.name, contract.state + ); + self.contracts.get_mut(&contract.name).unwrap().state = contract.state.clone(); - // TODO: would be nice to have a drain-like API here. - block_under_construction - .updated_states - .insert(contract_name.clone(), next_state.state.clone()); + // TODO: would be nice to have a drain-like API here. + block_under_construction + .updated_states + .insert(contract.name, contract.state); + } } } - next_txs_to_try_and_settle - } + // Keep track of settled txs + block_under_construction.successful_txs.push(bth); - fn handle_blob_for_hyle_tld( - contracts: &HashMap, - current_contracts: &BTreeMap, - current_blob: &Blob, - ) -> Result { - let Ok(reg) = - StructuredBlobData::::try_from(current_blob.data.clone()) - else { - bail!("Blob is not a RegisterContractAction"); - }; - - // Check name, it's either a direct subdomain or a TLD - validate_contract_registration_metadata( - &"hyle".into(), - ®.parameters.contract_name, - ®.parameters.verifier, - ®.parameters.program_id, - ®.parameters.state_digest, - )?; - - // Check it's not already registered - if contracts.contains_key(®.parameters.contract_name) - || current_contracts.contains_key(®.parameters.contract_name) - { - bail!( - "Contract {} is already registered", - reg.parameters.contract_name.0 - ); - } - - Ok(Contract { - name: reg.parameters.contract_name.clone(), - program_id: reg.parameters.program_id.clone(), - state: reg.parameters.state_digest.clone(), - verifier: reg.parameters.verifier.clone(), - }) + Some(next_txs_to_try_and_settle) } // Called when processing a verified proof TX - checks the proof is potentially valid for settlement. @@ -797,30 +813,75 @@ impl NodeState { Ok(()) } - // Called when trying to actually settle a blob TX - asserts a proof validly settles a blob. + // Called when trying to actually settle a blob TX - processes a proof for settlement. // verify_hyle_output has already been called at this point. - fn validate_proof_metadata( + fn process_proof( + contract_changes: &mut BTreeMap, proof_metadata: &(ProgramId, HyleOutput), contract: &Contract, - ) -> bool { - if proof_metadata.1.registered_contracts.iter().any(|effect| { - validate_contract_registration_metadata( - &contract.name, - &effect.contract_name, - &effect.verifier, - &effect.program_id, - &effect.state_digest, + ) -> Result<()> { + validate_state_digest_size(&proof_metadata.1.next_state)?; + + tracing::trace!( + "Processing proof for contract {} with state {:?}", + contract.name, + contract.state + ); + if proof_metadata.1.initial_state != contract.state { + bail!( + "Initial state mismatch: {:?}, expected {:?}", + proof_metadata.1.initial_state, + contract.state ) - .is_err() - }) { - return false; } - if validate_state_digest_size(&proof_metadata.1.next_state).is_err() { - return false; + if proof_metadata.0 != contract.program_id { + bail!( + "Program ID mismatch: {:?}, expected {:?}", + proof_metadata.0, + contract.program_id + ) } - proof_metadata.1.initial_state == contract.state && proof_metadata.0 == contract.program_id + for effect in &proof_metadata.1.onchain_effects { + match effect { + OnchainEffect::RegisterContract(effect) => { + validate_contract_registration_metadata( + &contract.name, + &effect.contract_name, + &effect.verifier, + &effect.program_id, + &effect.state_digest, + )?; + contract_changes.insert( + effect.contract_name.clone(), + SideEffect::Register(Contract { + name: effect.contract_name.clone(), + program_id: effect.program_id.clone(), + state: effect.state_digest.clone(), + verifier: effect.verifier.clone(), + }), + ); + } + OnchainEffect::DeleteContract(cn) => { + // TODO - check the contract exists ? + validate_contract_name_registration(&contract.name, cn)?; + contract_changes.insert(cn.clone(), SideEffect::Delete(cn.clone())); + } + } + } + + // Apply the generic state updates + let update = SideEffect::Update(Contract { + state: proof_metadata.1.next_state.clone(), + ..contract.clone() + }); + contract_changes + .entry(contract.name.clone()) + .and_modify(|c| c.apply(update.clone())) + .or_insert(update); + + Ok(()) } /// Clear timeouts for transactions that have timed out. @@ -954,7 +1015,7 @@ pub mod test { success: true, tx_hash: blob_tx.hashed(), tx_ctx: None, - registered_contracts: vec![], + onchain_effects: vec![], program_outputs: vec![], } } @@ -975,8 +1036,8 @@ pub mod test { success: true, tx_hash: blob_tx.hashed(), tx_ctx: None, + onchain_effects: vec![], program_outputs: vec![], - registered_contracts: vec![], } } @@ -994,43 +1055,6 @@ pub mod test { } } - // Small wrapper for the general case until we get a larger refactoring? - fn handle_verify_proof_transaction( - state: &mut NodeState, - proof: &VerifiedProofTransaction, - ) -> Result<(), Error> { - let mut bhpo = vec![]; - let blob_tx_to_try_and_settle = proof - .proven_blobs - .iter() - .filter_map(|blob_proof_data| { - state - .handle_blob_proof( - TxHash::new(""), - &mut BTreeMap::new(), - &mut bhpo, - &mut BTreeMap::new(), - blob_proof_data, - ) - .unwrap_or_default() - }) - .collect::>(); - if blob_tx_to_try_and_settle.len() != 1 { - return Err(anyhow::anyhow!( - "Test can only handle exactly one TX to settle" - )); - } - let SettledTxOutput { - updated_contracts, .. - } = state.try_to_settle_blob_tx(blob_tx_to_try_and_settle.first().unwrap(), &mut vec![])?; - for (contract_name, contract) in updated_contracts.iter() { - state - .contracts - .insert(contract_name.clone(), contract.clone()); - } - Ok(()) - } - fn bogus_tx_context() -> Arc { Arc::new(TxContext { block_hash: ConsensusProposalHash("0xfedbeef".to_owned()), @@ -1135,8 +1159,10 @@ pub mod test { let verified_proof_c2 = new_proof_tx(&c2, &hyle_output_c2, &blob_tx_hash); - let _ = handle_verify_proof_transaction(&mut state, &verified_proof_c1); - let _ = handle_verify_proof_transaction(&mut state, &verified_proof_c2); + state.handle_signed_block(&craft_signed_block( + 10, + vec![verified_proof_c1.into(), verified_proof_c2.into()], + )); assert_eq!(state.contracts.get(&c1).unwrap().state.0, vec![4, 5, 6]); assert_eq!(state.contracts.get(&c2).unwrap().state.0, vec![4, 5, 6]); @@ -1167,12 +1193,9 @@ pub mod test { let verified_proof_c1 = new_proof_tx(&c1, &hyle_output_c1, &blob_tx_hash_1); - assert_err!(handle_verify_proof_transaction( - &mut state, - &verified_proof_c1 - )); + state.handle_signed_block(&craft_signed_block(10, vec![verified_proof_c1.into()])); - // Check that we did not settled + // Check that we did not settle assert_eq!(state.contracts.get(&c1).unwrap().state.0, vec![0, 1, 2, 3]); assert_eq!(state.contracts.get(&c2).unwrap().state.0, vec![0, 1, 2, 3]); } @@ -1202,8 +1225,10 @@ pub mod test { let verified_proof_c1 = new_proof_tx(&c1, &hyle_output_c1, &blob_tx_hash); - let _ = handle_verify_proof_transaction(&mut state, &verified_proof_c1); - let _ = handle_verify_proof_transaction(&mut state, &verified_proof_c1); + state.handle_signed_block(&craft_signed_block( + 10, + vec![verified_proof_c1.clone().into(), verified_proof_c1.into()], + )); assert_eq!( state @@ -1297,9 +1322,14 @@ pub mod test { let verified_third_proof = new_proof_tx(&c1, &third_hyle_output, &blob_tx_hash); - let _ = handle_verify_proof_transaction(&mut state, &verified_first_proof); - let _ = handle_verify_proof_transaction(&mut state, &verified_second_proof); - handle_verify_proof_transaction(&mut state, &verified_third_proof).unwrap(); + state.handle_signed_block(&craft_signed_block( + 10, + vec![ + verified_first_proof.into(), + verified_second_proof.into(), + verified_third_proof.into(), + ], + )); // Check that we did settled with the last state assert_eq!(state.contracts.get(&c1).unwrap().state.0, vec![10, 11, 12]); @@ -1414,17 +1444,13 @@ pub mod test { let verified_second_proof = new_proof_tx(&c1, &second_hyle_output, &blob_tx_hash); - assert_err!(handle_verify_proof_transaction( - &mut state, - &verified_first_proof - )); - assert_err!(handle_verify_proof_transaction( - &mut state, - &another_verified_first_proof - )); - assert_err!(handle_verify_proof_transaction( - &mut state, - &verified_second_proof + state.handle_signed_block(&craft_signed_block( + 10, + vec![ + verified_first_proof.into(), + another_verified_first_proof.into(), + verified_second_proof.into(), + ], )); // Check that we did not settled @@ -1470,17 +1496,13 @@ pub mod test { let verified_third_proof = new_proof_tx(&c1, &third_hyle_output, &blob_tx_hash); - assert_err!(handle_verify_proof_transaction( - &mut state, - &verified_first_proof - )); - assert_err!(handle_verify_proof_transaction( - &mut state, - &verified_second_proof - )); - assert_err!(handle_verify_proof_transaction( - &mut state, - &verified_third_proof + state.handle_signed_block(&craft_signed_block( + 10, + vec![ + verified_first_proof.into(), + verified_second_proof.into(), + verified_third_proof.into(), + ], )); // Check that we did not settled @@ -1822,9 +1844,15 @@ pub mod test { mod contract_registration { use std::collections::HashSet; + use utoipa::openapi::info; + use super::*; - pub fn make_tx(sender: Identity, tld: ContractName, name: ContractName) -> BlobTransaction { + pub fn make_register_tx( + sender: Identity, + tld: ContractName, + name: ContractName, + ) -> BlobTransaction { BlobTransaction::new( sender, vec![RegisterContractAction { @@ -1841,9 +1869,9 @@ pub mod test { async fn test_register_contract_simple_hyle() { let mut state = new_node_state().await; - let register_c1 = make_tx("hyle.hyle".into(), "hyle".into(), "c1".into()); - let register_c2 = make_tx("hyle.hyle".into(), "hyle".into(), "c2.hyle".into()); - let register_c3 = make_tx("hyle.hyle".into(), "hyle".into(), "c3".into()); + let register_c1 = make_register_tx("hyle.hyle".into(), "hyle".into(), "c1".into()); + let register_c2 = make_register_tx("hyle.hyle".into(), "hyle".into(), "c2.hyle".into()); + let register_c3 = make_register_tx("hyle.hyle".into(), "hyle".into(), "c3".into()); let block_1 = craft_signed_block(1, vec![register_c1.clone().into()]); state.handle_signed_block(&block_1); @@ -1874,10 +1902,12 @@ pub mod test { async fn test_register_contract_failure() { let mut state = new_node_state().await; - let register_1 = make_tx("hyle.hyle".into(), "hyle".into(), "c1.hyle.lol".into()); - let register_2 = make_tx("other.hyle".into(), "hyle".into(), "c2.hyle.hyle".into()); - let register_3 = make_tx("hyle.hyle".into(), "hyle".into(), "c3.other".into()); - let register_4 = make_tx("hyle.hyle".into(), "hyle".into(), ".hyle".into()); + let register_1 = + make_register_tx("hyle.hyle".into(), "hyle".into(), "c1.hyle.lol".into()); + let register_2 = + make_register_tx("other.hyle".into(), "hyle".into(), "c2.hyle.hyle".into()); + let register_3 = make_register_tx("hyle.hyle".into(), "hyle".into(), "c3.other".into()); + let register_4 = make_register_tx("hyle.hyle".into(), "hyle".into(), ".hyle".into()); let register_5 = BlobTransaction::new( "hyle.hyle", vec![Blob { @@ -1885,7 +1915,8 @@ pub mod test { data: BlobData(vec![0, 1, 2, 3]), }], ); - let register_good = make_tx("hyle.hyle".into(), "hyle".into(), "c1.hyle".into()); + let register_good = + make_register_tx("hyle.hyle".into(), "hyle".into(), "c1.hyle".into()); let signed_block = craft_signed_block( 1, @@ -1918,7 +1949,7 @@ pub mod test { #[test_log::test(tokio::test)] async fn test_register_contract_composition() { let mut state = new_node_state().await; - let register = make_tx("hyle.hyle".into(), "hyle".into(), "hydentity".into()); + let register = make_register_tx("hyle.hyle".into(), "hyle".into(), "hydentity".into()); let block = state.handle_signed_block(&craft_signed_block(1, vec![register.clone().into()])); @@ -2029,5 +2060,225 @@ pub mod test { assert!(dp_hashes.contains(tx_hash)); } } + + pub fn make_delete_tx( + sender: Identity, + tld: ContractName, + contract_name: ContractName, + ) -> BlobTransaction { + BlobTransaction::new( + sender, + vec![DeleteContractAction { contract_name }.as_blob(tld, None, None)], + ) + } + + #[test_log::test(tokio::test)] + async fn test_register_contract_and_delete_hyle() { + let mut state = new_node_state().await; + + let register_c1 = make_register_tx("hyle.hyle".into(), "hyle".into(), "c1".into()); + let register_c2 = make_register_tx("hyle.hyle".into(), "hyle".into(), "c2.hyle".into()); + // This technically doesn't matter as it's actually the proof that does the work + let register_sub_c2 = make_register_tx( + "toto.c2.hyle".into(), + "c2.hyle".into(), + "sub.c2.hyle".into(), + ); + + let mut output = make_hyle_output(register_sub_c2.clone(), BlobIndex(0)); + output + .onchain_effects + .push(OnchainEffect::RegisterContract(RegisterContractEffect { + verifier: "test".into(), + program_id: ProgramId(vec![1]), + state_digest: StateDigest(vec![0, 1, 2, 3]), + contract_name: "sub.c2.hyle".into(), + })); + let sub_c2_proof = new_proof_tx(&"c2.hyle".into(), &output, ®ister_sub_c2.hashed()); + + let block = state.handle_signed_block(&craft_signed_block( + 1, + vec![ + register_c1.into(), + register_c2.into(), + register_sub_c2.into(), + sub_c2_proof.into(), + ], + )); + assert_eq!( + block + .registered_contracts + .iter() + .map(|(_, rce)| rce.contract_name.0.clone()) + .collect::>(), + vec!["c1", "c2.hyle", "sub.c2.hyle"] + ); + assert_eq!(state.contracts.len(), 4); + + // Now delete them. + let self_delete_tx = make_delete_tx("c1.c1".into(), "c1".into(), "c1".into()); + let delete_sub_tx = make_delete_tx( + "toto.c2.hyle".into(), + "c2.hyle".into(), + "sub.c2.hyle".into(), + ); + let delete_tx = make_delete_tx("hyle.hyle".into(), "hyle".into(), "c2.hyle".into()); + + let mut output = make_hyle_output(self_delete_tx.clone(), BlobIndex(0)); + output + .onchain_effects + .push(OnchainEffect::DeleteContract("c1".into())); + let delete_self_proof = + new_proof_tx(&"c1.hyle".into(), &output, &self_delete_tx.hashed()); + + let mut output = + make_hyle_output_with_state(delete_sub_tx.clone(), BlobIndex(0), &[4, 5, 6], &[1]); + output + .onchain_effects + .push(OnchainEffect::DeleteContract("sub.c2.hyle".into())); + let delete_sub_proof = + new_proof_tx(&"c2.hyle".into(), &output, &delete_sub_tx.hashed()); + + let block = state.handle_signed_block(&craft_signed_block( + 2, + vec![ + self_delete_tx.into(), + delete_sub_tx.into(), + delete_self_proof.into(), + delete_sub_proof.into(), + delete_tx.into(), + ], + )); + + assert_eq!( + block + .deleted_contracts + .iter() + .map(|(_, dce)| dce.0.clone()) + .collect::>(), + vec!["c1", "sub.c2.hyle", "c2.hyle"] + ); + assert_eq!(state.contracts.len(), 1); + } + + #[test_log::test(tokio::test)] + async fn test_hyle_sub_delete() { + let mut state = new_node_state().await; + + let register_c2 = make_register_tx("hyle.hyle".into(), "hyle".into(), "c2.hyle".into()); + // This technically doesn't matter as it's actually the proof that does the work + let register_sub_c2 = make_register_tx( + "toto.c2.hyle".into(), + "c2.hyle".into(), + "sub.c2.hyle".into(), + ); + + let mut output = make_hyle_output(register_sub_c2.clone(), BlobIndex(0)); + output + .onchain_effects + .push(OnchainEffect::RegisterContract(RegisterContractEffect { + verifier: "test".into(), + program_id: ProgramId(vec![1]), + state_digest: StateDigest(vec![0, 1, 2, 3]), + contract_name: "sub.c2.hyle".into(), + })); + let sub_c2_proof = new_proof_tx(&"c2.hyle".into(), &output, ®ister_sub_c2.hashed()); + + state.handle_signed_block(&craft_signed_block( + 1, + vec![ + register_c2.into(), + register_sub_c2.into(), + sub_c2_proof.into(), + ], + )); + assert_eq!(state.contracts.len(), 3); + + // Now delete the intermediate contract first, then delete the sub-contract via hyle + let delete_tx = make_delete_tx("hyle.hyle".into(), "hyle".into(), "c2.hyle".into()); + let delete_sub_tx = + make_delete_tx("hyle.hyle".into(), "hyle".into(), "sub.c2.hyle".into()); + + let block = state.handle_signed_block(&craft_signed_block( + 2, + vec![delete_tx.into(), delete_sub_tx.into()], + )); + + assert_eq!( + block + .deleted_contracts + .iter() + .map(|(_, dce)| dce.0.clone()) + .collect::>(), + vec!["c2.hyle", "sub.c2.hyle"] + ); + assert_eq!(state.contracts.len(), 1); + } + + #[test_log::test(tokio::test)] + async fn test_register_update_delete_combinations_hyle() { + let register_tx = make_register_tx("hyle.hyle".into(), "hyle".into(), "c.hyle".into()); + let delete_tx = make_delete_tx("hyle.hyle".into(), "hyle".into(), "c.hyle".into()); + let delete_self_tx = + make_delete_tx("hyle.c.hyle".into(), "c.hyle".into(), "c.hyle".into()); + let update_tx = + make_register_tx("test.c.hyle".into(), "c.hyle".into(), "c.hyle".into()); + + let proof_update = new_proof_tx( + &"c.hyle".into(), + &make_hyle_output(update_tx.clone(), BlobIndex(0)), + &update_tx.hashed(), + ); + + let mut output = + make_hyle_output_with_state(delete_self_tx.clone(), BlobIndex(0), &[4, 5, 6], &[1]); + output + .onchain_effects + .push(OnchainEffect::DeleteContract("c.hyle".into())); + let proof_delete = new_proof_tx(&"c.hyle".into(), &output, &delete_self_tx.hashed()); + + async fn test_combination( + proofs: Option<&[&VerifiedProofTransaction]>, + txs: &[&BlobTransaction], + expected_ct: usize, + expected_txs: usize, + ) { + let mut state = new_node_state().await; + let mut txs = txs + .iter() + .map(|tx| (*tx).clone().into()) + .collect::>(); + if let Some(proofs) = proofs { + txs.extend(proofs.iter().map(|p| (*p).clone().into())); + } + let block = state.handle_signed_block(&craft_signed_block(1, txs)); + + assert_eq!(state.contracts.len(), expected_ct); + assert_eq!(block.successful_txs.len(), expected_txs); + info!("done"); + } + + // Test all combinations + test_combination(None, &[®ister_tx], 2, 1).await; + test_combination(None, &[&delete_tx], 1, 0).await; + test_combination(None, &[®ister_tx, &delete_tx], 1, 2).await; + test_combination(Some(&[&proof_update]), &[®ister_tx, &update_tx], 2, 2).await; + // TODO: This actually deletes right away before update is settled, as it isn't blocked. + // This is arguably a bug and should be fixed. + test_combination( + Some(&[&proof_update]), + &[®ister_tx, &update_tx, &delete_tx], + 1, + 2, + ) + .await; + test_combination( + Some(&[&proof_update, &proof_delete]), + &[®ister_tx, &update_tx, &delete_self_tx], + 1, + 3, + ) + .await; + } } } diff --git a/src/node_state/hyle_tld.rs b/src/node_state/hyle_tld.rs new file mode 100644 index 000000000..08bd49d11 --- /dev/null +++ b/src/node_state/hyle_tld.rs @@ -0,0 +1,87 @@ +use crate::model::contract_registration::{ + validate_contract_name_registration, validate_contract_registration_metadata, +}; +use crate::model::*; +use anyhow::{bail, Result}; +use std::collections::{BTreeMap, HashMap}; + +use super::SideEffect; + +pub fn handle_blob_for_hyle_tld( + contracts: &HashMap, + contract_changes: &mut BTreeMap, + current_blob: &Blob, +) -> Result<()> { + // TODO: check the identity of the caller here. + + // TODO: support unstructured blobs as well ? + if let Ok(reg) = + StructuredBlobData::::try_from(current_blob.data.clone()) + { + handle_register_blob(contracts, contract_changes, ®.parameters)?; + } else if let Ok(reg) = + StructuredBlobData::::try_from(current_blob.data.clone()) + { + handle_delete_blob(contracts, contract_changes, ®.parameters)?; + } else { + bail!("Invalid blob data for TLD"); + } + Ok(()) +} + +fn handle_register_blob( + contracts: &HashMap, + contract_changes: &mut BTreeMap, + reg: &RegisterContractAction, +) -> Result<()> { + // Check name, it's either a direct subdomain or a TLD + validate_contract_registration_metadata( + &"hyle".into(), + ®.contract_name, + ®.verifier, + ®.program_id, + ®.state_digest, + )?; + + // Check it's not already registered + if contracts.contains_key(®.contract_name) + || contract_changes.contains_key(®.contract_name) + { + bail!("Contract {} is already registered", reg.contract_name.0); + } + + contract_changes.insert( + reg.contract_name.clone(), + SideEffect::Register(Contract { + name: reg.contract_name.clone(), + program_id: reg.program_id.clone(), + state: reg.state_digest.clone(), + verifier: reg.verifier.clone(), + }), + ); + Ok(()) +} + +fn handle_delete_blob( + contracts: &HashMap, + contract_changes: &mut BTreeMap, + delete: &DeleteContractAction, +) -> Result<()> { + // For now, Hylé is allowed to delete all contracts but itself + if delete.contract_name.0 == "hyle" { + bail!("Cannot delete Hylé contract"); + } + + // Check it's registered + if contracts.contains_key(&delete.contract_name) + || contract_changes.contains_key(&delete.contract_name) + { + contract_changes.insert( + delete.contract_name.clone(), + SideEffect::Delete(delete.contract_name.clone()), + ); + Ok(()) + } else { + bail!("Contract {} is already registered", delete.contract_name.0); + } +} diff --git a/src/tests/tx_settlement.rs b/src/tests/tx_settlement.rs index 9aaa8c222..4e516628d 100644 --- a/src/tests/tx_settlement.rs +++ b/src/tests/tx_settlement.rs @@ -1,7 +1,8 @@ use client_sdk::rest_client::{IndexerApiHttpClient, NodeApiHttpClient}; use hyle_model::{ - api::APIRegisterContract, BlobTransaction, ContractAction, ContractName, Hashed, ProgramId, - ProofData, ProofTransaction, RegisterContractAction, RegisterContractEffect, StateDigest, + api::APIRegisterContract, BlobTransaction, ContractAction, ContractName, Hashed, OnchainEffect, + ProgramId, ProofData, ProofTransaction, RegisterContractAction, RegisterContractEffect, + StateDigest, }; use testcontainers_modules::{ postgres::Postgres, @@ -374,14 +375,14 @@ async fn test_contract_upgrade() -> Result<()> { make_hyle_output_with_state(b2.clone(), BlobIndex(0), &[1, 2, 3], &[8, 8, 8]); hyle_output - .registered_contracts - .push(RegisterContractEffect { + .onchain_effects + .push(OnchainEffect::RegisterContract(RegisterContractEffect { verifier: "test".into(), program_id: ProgramId(vec![7, 7, 7]), // The state digest is ignored during the update phase. state_digest: StateDigest(vec![3, 3, 3]), contract_name: "c1.hyle".into(), - }); + })); let proof_update = ProofTransaction { contract_name: "c1.hyle".into(), diff --git a/tests/uuid_test.rs b/tests/uuid_test.rs index e4cb4f8d8..f44c4d485 100644 --- a/tests/uuid_test.rs +++ b/tests/uuid_test.rs @@ -13,6 +13,7 @@ use hyle_contract_sdk::{ StateDigest, Verifier, }; use hyle_contracts::{HYDENTITY_ELF, UUID_TLD_ELF, UUID_TLD_ID}; +use hyle_model::OnchainEffect; use uuid_tld::{UuidTld, UuidTldAction}; contract_states!( @@ -111,14 +112,10 @@ async fn test_uuid_registration() { let contract = loop { if let Ok(c) = ctx - .get_contract( - &expected_output - .registered_contracts - .first() - .unwrap() - .contract_name - .0, - ) + .get_contract(match expected_output.onchain_effects.first() { + Some(OnchainEffect::RegisterContract(e)) => &e.contract_name.0, + _ => panic!("Expected RegisterContractEffect"), + }) .await { break c;