From c291850ce332b7ca34a1cfe726c3804b7a954822 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Sat, 2 Nov 2024 15:54:15 +0000 Subject: [PATCH] feat: Blockchain WASM integration (#899) --- crates/runtime/src/logic.rs | 43 ++++++++++ crates/runtime/src/logic/imports.rs | 2 + crates/sdk/macros/src/state.rs | 6 ++ crates/sdk/src/env/ext.rs | 129 +++++++++++++++++++++++++++- crates/sdk/src/sys.rs | 2 + 5 files changed, 180 insertions(+), 2 deletions(-) diff --git a/crates/runtime/src/logic.rs b/crates/runtime/src/logic.rs index 4cab55e34..1049296a2 100644 --- a/crates/runtime/src/logic.rs +++ b/crates/runtime/src/logic.rs @@ -2,6 +2,7 @@ #![allow(clippy::mem_forget, reason = "Safe for now")] use core::num::NonZeroU64; +use std::collections::BTreeMap; use std::time::{SystemTime, UNIX_EPOCH}; use borsh::from_slice as from_borsh_slice; @@ -108,6 +109,7 @@ pub struct VMLogic<'a> { events: Vec, actions: Vec>, root_hash: Option<[u8; 32]>, + proposals: BTreeMap<[u8; 32], Vec>, } impl<'a> VMLogic<'a> { @@ -123,6 +125,7 @@ impl<'a> VMLogic<'a> { events: vec![], actions: vec![], root_hash: None, + proposals: BTreeMap::new(), } } @@ -152,6 +155,7 @@ pub struct Outcome { pub events: Vec, pub actions: Vec>, pub root_hash: Option<[u8; 32]>, + pub proposals: BTreeMap<[u8; 32], Vec>, // execution runtime // current storage usage of the app } @@ -181,6 +185,7 @@ impl VMLogic<'_> { events: self.events, actions: self.actions, root_hash: self.root_hash, + proposals: self.proposals, } } } @@ -556,4 +561,42 @@ impl VMHostFunctions<'_> { Ok(()) } + + /// Call the contract's `send_proposal()` function through the bridge. + /// + /// The proposal actions are obtained as raw data and pushed onto a list of + /// proposals to be sent to the host. + /// + /// Note that multiple actions are received, and the entire batch is pushed + /// onto the proposal list to represent one proposal. + /// + /// # Parameters + /// + /// * `actions_ptr` - Pointer to the start of the action data in WASM + /// memory. + /// * `actions_len` - Length of the action data. + /// * `id_ptr` - Pointer to the start of the id data in WASM memory. + /// * `id_len` - Length of the action data. This should be 32 bytes. + /// + pub fn send_proposal( + &mut self, + actions_ptr: u64, + actions_len: u64, + id_ptr: u64, + id_len: u64, + ) -> VMLogicResult<()> { + if id_len != 32 { + return Err(HostError::InvalidMemoryAccess.into()); + } + + let actions_bytes: Vec = self.read_guest_memory(actions_ptr, actions_len)?; + let mut proposal_id = [0; 32]; + + rand::thread_rng().fill_bytes(&mut proposal_id); + drop(self.with_logic_mut(|logic| logic.proposals.insert(proposal_id, actions_bytes))); + + self.borrow_memory().write(id_ptr, &proposal_id)?; + + Ok(()) + } } diff --git a/crates/runtime/src/logic/imports.rs b/crates/runtime/src/logic/imports.rs index f74ed53cf..531c51f50 100644 --- a/crates/runtime/src/logic/imports.rs +++ b/crates/runtime/src/logic/imports.rs @@ -69,6 +69,8 @@ impl VMLogic<'_> { fn random_bytes(ptr: u64, len: u64); fn time_now(ptr: u64, len: u64); + + fn send_proposal(actions_ptr: u64, actions_len: u64, id_ptr: u64, id_len: u64); } } } diff --git a/crates/sdk/macros/src/state.rs b/crates/sdk/macros/src/state.rs index d9ccd4548..f2c56619d 100644 --- a/crates/sdk/macros/src/state.rs +++ b/crates/sdk/macros/src/state.rs @@ -50,6 +50,12 @@ impl ToTokens for StateImpl<'_> { impl #impl_generics ::calimero_sdk::state::AppState for #ident #ty_generics #where_clause { type Event<#lifetime> = #event; } + + impl #impl_generics #ident #ty_generics #where_clause { + fn external() -> ::calimero_sdk::env::ext::External { + ::calimero_sdk::env::ext::External {} + } + } } .to_tokens(tokens); } diff --git a/crates/sdk/src/env/ext.rs b/crates/sdk/src/env/ext.rs index 1879e7543..032f7b826 100644 --- a/crates/sdk/src/env/ext.rs +++ b/crates/sdk/src/env/ext.rs @@ -1,8 +1,133 @@ -use borsh::to_vec as to_borsh_vec; +use borsh::{to_vec as to_borsh_vec, to_vec, BorshDeserialize, BorshSerialize}; use super::{expected_boolean, expected_register, panic_str, read_register, DATA_REGISTER}; use crate::sys; -use crate::sys::Buffer; +use crate::sys::{Buffer, BufferMut}; + +/// A blockchain proposal action. +/// +/// This enum represents the different actions that can be executed against a +/// blockchain, and combined into a proposal. +/// +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, Eq, PartialEq)] +pub enum ProposalAction { + /// Call a method on a contract. + ExternalFunctionCall { + /// The account ID of the contract to call. + receiver_id: AccountId, + + /// The method name to call. + method_name: String, + + /// The arguments to pass to the method. + args: String, + + /// The amount of tokens to attach to the call. + deposit: u64, + + /// The maximum amount of gas to use for the call. + gas: u64, + }, + + /// Transfer tokens to an account. + Transfer { + /// The account ID of the receiver. + receiver_id: AccountId, + + /// The amount of tokens to transfer. + amount: u64, + }, + + /// Set the number of approvals required for a proposal to be executed. + SetNumApprovals { + /// The number of approvals required. + num_approvals: u32, + }, + + /// Set the number of active proposals allowed at once. + SetActiveProposalsLimit { + /// The number of active proposals allowed. + active_proposals_limit: u32, + }, + + /// Set a value in the contract's context. + SetContextValue { + /// The key to set. + key: Box<[u8]>, + + /// The value to set. + value: Box<[u8]>, + }, +} + +/// Unique identifier for an account. +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct AccountId(String); + +/// A draft proposal. +/// +/// This struct is used to build a proposal before sending it to the blockchain. +/// It is distinct from a proposal that has been prepared and needs signing. +/// +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, Default, Eq, PartialEq)] +pub struct DraftProposal { + /// The actions to be executed by the proposal. One proposal can contain + /// multiple actions to execute. + actions: Vec, +} + +impl DraftProposal { + /// Create a new draft proposal. + #[must_use] + pub const fn new() -> Self { + Self { + actions: Vec::new(), + } + } + + /// Add an action to transfer tokens to an account. + #[must_use] + pub fn transfer(mut self, receiver: AccountId, amount: u64) -> Self { + self.actions.push(ProposalAction::Transfer { + receiver_id: receiver, + amount, + }); + self + } + + /// Finalise the proposal and send it to the blockchain. + #[must_use] + pub fn send(self) -> ProposalId { + let mut buf = [0; 32]; + let actions = to_vec(&self.actions).unwrap(); + + #[expect( + clippy::needless_borrows_for_generic_args, + reason = "We don't want to copy the buffer, but write to the same one that's returned" + )] + unsafe { + sys::send_proposal(Buffer::from(&*actions), BufferMut::new(&mut buf)) + } + + ProposalId(buf) + } +} + +/// Interface for interacting with external proposals for blockchain actions. +#[derive(Clone, Copy, Debug)] +pub struct External; + +impl External { + /// Create a new proposal. This will initially be a draft, until sent. + #[must_use] + pub const fn propose(self) -> DraftProposal { + DraftProposal::new() + } +} + +/// Unique identifier for a proposal. +#[derive(BorshDeserialize, BorshSerialize, Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct ProposalId(pub [u8; 32]); #[doc(hidden)] pub unsafe fn fetch( diff --git a/crates/sdk/src/sys.rs b/crates/sdk/src/sys.rs index 121555e20..44a5bcc08 100644 --- a/crates/sdk/src/sys.rs +++ b/crates/sdk/src/sys.rs @@ -37,6 +37,8 @@ wasm_imports! { // -- fn random_bytes(buf: BufferMut<'_>); fn time_now(buf: BufferMut<'_>); + // -- + fn send_proposal(value: Buffer<'_>, buf: BufferMut<'_>); } }