From 04a4a6a5435984194127b98592be3a317988c50f Mon Sep 17 00:00:00 2001 From: Dmytro Kozhevin Date: Tue, 11 Apr 2023 17:43:37 -0400 Subject: [PATCH] Add a very simple account contract example. (#227) * Add a very simple account contract example. This is close to minimal possible valid account contract that is aimed at demonstrating the most basic concepts. --- Cargo.lock | 10 ++++++ Cargo.toml | 1 + account/src/lib.rs | 2 +- simple_account/Cargo.toml | 21 ++++++++++++ simple_account/src/lib.rs | 67 ++++++++++++++++++++++++++++++++++++++ simple_account/src/test.rs | 63 +++++++++++++++++++++++++++++++++++ 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 simple_account/Cargo.toml create mode 100644 simple_account/src/lib.rs create mode 100644 simple_account/src/test.rs diff --git a/Cargo.lock b/Cargo.lock index eb4f1041..cd81b930 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1084,6 +1084,16 @@ dependencies = [ "syn", ] +[[package]] +name = "soroban-simple-account-contract" +version = "0.0.0" +dependencies = [ + "ed25519-dalek", + "rand 0.7.3", + "soroban-auth", + "soroban-sdk", +] + [[package]] name = "soroban-single-offer-contract" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index ce55aa14..8467de4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "atomic_multiswap", "account", "alloc", + "simple_account", ] [profile.release-with-logs] diff --git a/account/src/lib.rs b/account/src/lib.rs index 99f383f2..a8637bba 100644 --- a/account/src/lib.rs +++ b/account/src/lib.rs @@ -66,7 +66,7 @@ impl AccountContract { // This is the 'entry point' of the account contract and every account // contract has to implement it. `require_auth` calls for the Address of - // this contract will result in calling this `check_auth` function with + // this contract will result in calling this `__check_auth` function with // the appropriate arguments. // // This should return `()` if authentication and authorization checks have diff --git a/simple_account/Cargo.toml b/simple_account/Cargo.toml new file mode 100644 index 00000000..cffd88a5 --- /dev/null +++ b/simple_account/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "soroban-simple-account-contract" +version = "0.0.0" +authors = ["Stellar Development Foundation "] +license = "Apache-2.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } +soroban-auth = { workspace = true } + +[dev_dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } +soroban-auth = { workspace = true, features = ["testutils"] } +ed25519-dalek = { version = "1.0.1" } +rand = { version = "0.7.3" } \ No newline at end of file diff --git a/simple_account/src/lib.rs b/simple_account/src/lib.rs new file mode 100644 index 00000000..47106478 --- /dev/null +++ b/simple_account/src/lib.rs @@ -0,0 +1,67 @@ +//! This a minimal exapmle of an account contract. +//! +//! The account is owned by a single ed25519 public key that is also used for +//! authentication. +//! +//! For a more advanced example that demonstrates all the capabilities of the +//! Soroban account contracts see `src/account` example. +#![no_std] + +struct SimpleAccount; + +use soroban_auth::AuthorizationContext; +use soroban_sdk::{contractimpl, contracttype, BytesN, Env, Vec}; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Owner, +} + +#[contractimpl] +impl SimpleAccount { + // Initialize the contract with an owner's ed25519 public key. + pub fn init(env: Env, public_key: BytesN<32>) { + if env.storage().has(&DataKey::Owner) { + panic!("owner is already set"); + } + env.storage().set(&DataKey::Owner, &public_key); + } + + // This is the 'entry point' of the account contract and every account + // contract has to implement it. `require_auth` calls for the Address of + // this contract will result in calling this `__check_auth` function with + // the appropriate arguments. + // + // This should return `()` if authentication and authorization checks have + // been passed and return an error (or panic) otherwise. + // + // `__check_auth` takes the payload that needed to be signed, arbitrarily + // typed signatures (`BytesN<64>` type here) and authorization + // context that contains all the invocations that this call tries to verify + // (not used in this example). + // + // In this example `__check_auth` only verifies the signature. + // + // Note, that `__check_auth` function shouldn't call `require_auth` on the + // contract's own address in order to avoid infinite recursion. + #[allow(non_snake_case)] + pub fn __check_auth( + env: Env, + signature_payload: BytesN<32>, + signature_args: Vec>, + _auth_context: Vec, + ) { + if signature_args.len() != 1 { + panic!("incorrect number of signature args"); + } + let public_key: BytesN<32> = env.storage().get(&DataKey::Owner).unwrap().unwrap(); + env.crypto().ed25519_verify( + &public_key, + &signature_payload.into(), + &signature_args.get(0).unwrap().unwrap(), + ); + } +} + +mod test; diff --git a/simple_account/src/test.rs b/simple_account/src/test.rs new file mode 100644 index 00000000..ed17ce79 --- /dev/null +++ b/simple_account/src/test.rs @@ -0,0 +1,63 @@ +#![cfg(test)] +extern crate std; + +use ed25519_dalek::Keypair; +use ed25519_dalek::Signer; +use rand::thread_rng; +use soroban_auth::testutils::EnvAuthUtils; +use soroban_sdk::RawVal; +use soroban_sdk::Status; +use soroban_sdk::{testutils::BytesN as _, vec, BytesN, Env, IntoVal}; + +use crate::SimpleAccount; +use crate::SimpleAccountClient; + +fn generate_keypair() -> Keypair { + Keypair::generate(&mut thread_rng()) +} + +fn create_account_contract(e: &Env) -> SimpleAccountClient { + SimpleAccountClient::new(e, &e.register_contract(None, SimpleAccount {})) +} + +fn sign(e: &Env, signer: &Keypair, payload: &BytesN<32>) -> RawVal { + let signature: BytesN<64> = signer + .sign(payload.to_array().as_slice()) + .to_bytes() + .into_val(e); + signature.into_val(e) +} + +#[test] +fn test_account() { + let env: Env = Default::default(); + + let account_contract = create_account_contract(&env); + + let signer = generate_keypair(); + account_contract.init(&signer.public.to_bytes().into_val(&env)); + + let payload = BytesN::random(&env); + // `__check_auth` can't be called directly, hence we need to use + // `invoke_account_contract_check_auth` testing utility that emulates being + // called by the Soroban host during a `require_auth` call. + env.invoke_account_contract_check_auth::( + &account_contract.contract_id, + &payload, + &vec![&env, sign(&env, &signer, &payload)], + &vec![&env], + ) + // Unwrap the result to make sure there is no error. + .unwrap(); + + // Now pass a random bytes array instead of the signature - this should + // result in an error as this is not a valid signature. + assert!(env + .invoke_account_contract_check_auth::( + &account_contract.contract_id, + &payload, + &vec![&env, BytesN::<64>::random(&env).into()], + &vec![&env], + ) + .is_err()); +}