Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the deposit check and withdrawals in the contract #144

Merged
merged 4 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions demo-contract-tests/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod test_fixture;
#[cfg(test)]
mod tests {
use crate::test_fixture::TestContext;
use casper_types::U512;
use casper_types::{PublicKey, SecretKey, U512};
use kairos_verifier_risc0_lib::verifier::verify_execution;

#[test]
Expand Down Expand Up @@ -89,14 +89,39 @@ mod tests {
verify_execution(&serde_json_wasm::from_slice(receipt1).unwrap()).unwrap();

let mut fixture = TestContext::new(None);

// must match the key in the receipt simple_batches_0
let alice_secret_key =
SecretKey::from_pem(include_str!("../../testdata/users/user-2/secret_key.pem"))
.unwrap();

let alice_public_key = fixture.create_funded_account_for_secret_key(alice_secret_key);
let alice_account_hash = alice_public_key.to_account_hash();
let alice_pre_bal = fixture.get_user_balance(alice_account_hash);

let bob_secret_key =
SecretKey::from_pem(include_str!("../../testdata/users/user-3/secret_key.pem"))
.unwrap();
let bob_public_key = PublicKey::from(&bob_secret_key);
let bob_account_hash = bob_public_key.to_account_hash();

// must match the amount in the receipt simple_batches_0
fixture.deposit_succeeds(alice_public_key, U512::from(10u64));

// submit proofs to contract
fixture.submit_proof_to_contract(fixture.admin, receipt0.to_vec());
fixture.submit_proof_to_contract(fixture.admin, receipt1.to_vec());
fixture.submit_proof_to_contract_expect_success(fixture.admin, receipt0.to_vec());
fixture.submit_proof_to_contract_expect_success(fixture.admin, receipt1.to_vec());

// must match the logic in the kairos_prover simple_batches test.
let bob_post_bal = fixture.get_user_balance(bob_account_hash);
assert_eq!(bob_post_bal, U512::from(2u64));

let alice_post_bal = fixture.get_user_balance(alice_account_hash);
assert_eq!(alice_post_bal, alice_pre_bal - U512::from(3u64));
}

// TODO some more real larger batches fail with code unreachable in the contract.
// They verify fine outside the contract, so I suspect they use too much gas.
#[allow(dead_code)]
fn submit_batch_to_contract(receipt: &[u8]) {
// precheck proofs before contract tests that are hard to debug
let proof_outputs =
Expand All @@ -105,7 +130,17 @@ mod tests {
eprintln!("{:?}", proof_outputs);

let mut fixture = TestContext::new(proof_outputs.pre_batch_trie_root);
fixture.submit_proof_to_contract(fixture.admin, receipt.to_vec())
let api_err =
fixture.submit_proof_to_contract_expect_api_err(fixture.admin, receipt.to_vec());

// We expect error 201 which occurs after proof verification
// when the proof outputs deposits are checked against the contract's deposits.
//
// Since we have not made any deposits on the l1 an error is expected.
//
// In the future it would be nice to make these prop test batches use real public keys so
// we could make this test pass all the way through.
assert_eq!(api_err, casper_types::ApiError::User(201));
}

#[test]
Expand Down
64 changes: 53 additions & 11 deletions demo-contract-tests/tests/test_fixture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ use casper_engine_test_support::{
DeployItemBuilder, ExecuteRequestBuilder, WasmTestBuilder, ARG_AMOUNT, DEFAULT_ACCOUNT_ADDR,
DEFAULT_ACCOUNT_INITIAL_BALANCE,
};
use casper_execution_engine::storage::global_state::in_memory::InMemoryGlobalState;
use casper_execution_engine::{
core::{engine_state, execution},
storage::global_state::in_memory::InMemoryGlobalState,
};
use casper_types::{
account::AccountHash,
bytesrepr::Bytes,
crypto::{PublicKey, SecretKey},
runtime_args,
system::{handle_payment::ARG_TARGET, mint::ARG_ID},
RuntimeArgs, U512,
ApiError, RuntimeArgs, U512,
};
use rand::Rng;
use std::path::Path;
Expand All @@ -36,7 +39,8 @@ impl TestContext {
let mut builder = InMemoryWasmTestBuilder::default();
builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST);

let admin = create_funded_account_for_secret_key_bytes(&mut builder, ADMIN_SECRET_KEY)
let admin_secret_key = SecretKey::ed25519_from_bytes(ADMIN_SECRET_KEY).unwrap();
let admin = create_funded_account_for_secret_key_bytes(&mut builder, admin_secret_key)
.to_account_hash();
let contract_path = get_wasm_directory().0.join("demo-contract-optimized.wasm");
run_session_with_args(
Expand Down Expand Up @@ -78,7 +82,15 @@ impl TestContext {
while random_secret_key == ADMIN_SECRET_KEY {
random_secret_key = rand::random();
}
create_funded_account_for_secret_key_bytes(&mut self.builder, random_secret_key)

create_funded_account_for_secret_key_bytes(
&mut self.builder,
SecretKey::ed25519_from_bytes(random_secret_key).unwrap(),
)
}

pub fn create_funded_account_for_secret_key(&mut self, secret_key: SecretKey) -> PublicKey {
create_funded_account_for_secret_key_bytes(&mut self.builder, secret_key)
}

pub fn get_user_balance(&mut self, user: AccountHash) -> U512 {
Expand Down Expand Up @@ -151,7 +163,8 @@ impl TestContext {
);
self.builder.expect_failure();
}
pub fn submit_proof_to_contract(&mut self, sender: AccountHash, proof_serialized: Vec<u8>) {

fn submit_proof_to_contract_commit(&mut self, sender: AccountHash, proof_serialized: Vec<u8>) {
let session_args = runtime_args! {
"risc0_receipt" => Bytes::from(proof_serialized),
};
Expand All @@ -164,10 +177,40 @@ impl TestContext {
payment,
)
.build();
self.builder
.exec(submit_batch_request)
.commit()
.expect_success();
self.builder.exec(submit_batch_request).commit();
}

pub fn submit_proof_to_contract_expect_success(
&mut self,
sender: AccountHash,
proof_serialized: Vec<u8>,
) {
self.submit_proof_to_contract_commit(sender, proof_serialized);
self.builder.expect_success();
}

pub fn submit_proof_to_contract_expect_api_err(
&mut self,
sender: AccountHash,
proof_serialized: Vec<u8>,
) -> ApiError {
self.submit_proof_to_contract_commit(sender, proof_serialized);

let exec_results = self
.builder
.get_last_exec_results()
.expect("Expected to be called after run()");

// not sure about first here it's what the upstream code does
let exec_result = exec_results
.first()
.expect("Unable to get first deploy result");

match exec_result.as_error() {
Some(engine_state::Error::Exec(execution::Error::Revert(err))) => *err,
Some(err) => panic!("Expected revert ApiError, got {:?}", err),
None => panic!("Expected error"),
}
}
}

Expand All @@ -187,9 +230,8 @@ pub fn run_session_with_args(
/// It panics if the passed secret key bytes cannot be read
pub fn create_funded_account_for_secret_key_bytes(
builder: &mut WasmTestBuilder<InMemoryGlobalState>,
account_secret_key_bytes: [u8; 32],
account_secret_key: SecretKey,
) -> PublicKey {
let account_secret_key = SecretKey::ed25519_from_bytes(account_secret_key_bytes).unwrap();
let account_public_key = PublicKey::from(&account_secret_key);
let account_hash = account_public_key.to_account_hash();
let transfer = ExecuteRequestBuilder::transfer(
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
./kairos-prover/kairos-circuit-logic
./kairos-prover/kairos-verifier-risc0-lib
./kairos-contracts/demo-contract/contract-utils
./testdata
];
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ pub const KAIROS_CONTRACT_HASH: &str = "kairos_contract_hash";
pub const KAIROS_CONTRACT_PACKAGE_HASH: &str = "kairos_contract_package_hash";
pub const KAIROS_CONTRACT_UREF: &str = "kairos_contract_uref";

pub const KAIROS_LAST_PROCESSED_DEPOSIT_COUNTER: &str = "last_processed_deposit_counter";
/// The casper event standard length key of the last processed deposit.
pub const KAIROS_UNPROCESSED_DEPOSIT_INDEX: &str = "kairos_unprocessed_deposit_index";
pub const KAIROS_DEPOSIT_PURSE: &str = "kairos_deposit_purse";
pub const KAIROS_TRIE_ROOT: &str = "kairos_trie_root";

Expand Down
132 changes: 124 additions & 8 deletions kairos-contracts/demo-contract/contract/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
#![no_std]
#![no_main]
extern crate alloc;
use alloc::string::ToString;
use alloc::vec;
use alloc::{string::ToString, vec::Vec};
use casper_contract::{
contract_api::{runtime, storage, system},
unwrap_or_revert::UnwrapOrRevert,
};
use casper_event_standard::Schemas;
use casper_types::bytesrepr::{Bytes, ToBytes};
use casper_types::bytesrepr::{Bytes, FromBytes, ToBytes};
use casper_types::PublicKey;
use casper_types::{
contracts::NamedKeys, runtime_args, AccessRights, ApiError, CLValue, EntryPoints, Key,
RuntimeArgs, URef, U512,
};
use contract_utils::constants::{
KAIROS_CONTRACT_HASH, KAIROS_CONTRACT_PACKAGE_HASH, KAIROS_CONTRACT_UREF, KAIROS_DEPOSIT_PURSE,
KAIROS_LAST_PROCESSED_DEPOSIT_COUNTER, KAIROS_TRIE_ROOT, RUNTIME_ARG_AMOUNT,
KAIROS_TRIE_ROOT, KAIROS_UNPROCESSED_DEPOSIT_INDEX, RUNTIME_ARG_AMOUNT,
RUNTIME_ARG_INITIAL_TRIE_ROOT, RUNTIME_ARG_RECEIPT, RUNTIME_ARG_RECIPIENT,
RUNTIME_ARG_TEMP_PURSE,
};
mod entry_points;
mod utils;
use kairos_circuit_logic::transactions::{Signed, Withdraw};
use kairos_verifier_risc0_lib::verifier::Receipt;
use utils::errors::DepositError;
use utils::get_immediate_caller;
Expand Down Expand Up @@ -103,15 +105,13 @@ pub extern "C" fn submit_batch() {
let Ok(ProofOutputs {
pre_batch_trie_root,
post_batch_trie_root,
deposits: _, // TODO: implement deposits
withdrawals: _, // TODO: implement withdrawals
deposits,
withdrawals,
}) = kairos_verifier_risc0_lib::verifier::verify_execution(&receipt)
else {
runtime::revert(ApiError::User(1u16));
};

// todo: check that the deposits are unique

// get the current root from contract storage
let trie_root_uref: URef = runtime::get_key(KAIROS_TRIE_ROOT)
.unwrap_or_revert()
Expand All @@ -125,11 +125,127 @@ pub extern "C" fn submit_batch() {
if trie_root != pre_batch_trie_root {
runtime::revert(ApiError::User(4u16))
};

check_batch_deposits_against_unprocessed(&deposits);
execute_withdrawals(&withdrawals);

// store the new root under the contract URef
storage::write(trie_root_uref, post_batch_trie_root);
// todo: update sliding window
}

/// Retrive all deposits that have not appeared in a batch yet.
/// Returns the value of `KAIROS_UNPROCESSED_DEPOSIT_INDEX`
/// and an event_index ordered vector of `(event_index, L1Deposit)` tuples.
///
/// This functions error codes are in the range of 101-199.
fn get_unprocessed_deposits() -> (u32, Vec<(u32, L1Deposit)>) {
let unprocessed_deposits_uref: URef = runtime::get_key(KAIROS_UNPROCESSED_DEPOSIT_INDEX)
.unwrap_or_revert()
.into_uref()
.unwrap_or_revert_with(ApiError::User(101u16));
let unprocessed_deposits_index: u32 = storage::read(unprocessed_deposits_uref)
.unwrap_or_revert()
.unwrap_or_revert_with(ApiError::User(102u16));

let events_length_uref: URef = runtime::get_key(casper_event_standard::EVENTS_LENGTH)
.unwrap_or_revert()
.into_uref()
.unwrap_or_revert_with(ApiError::User(103u16));
let events_length: u32 = storage::read(events_length_uref)
.unwrap_or_revert()
.unwrap_or_revert_with(ApiError::User(104u16));

let events_dict_uref: URef = runtime::get_key(casper_event_standard::EVENTS_DICT)
.unwrap_or_revert()
.into_uref()
.unwrap_or_revert_with(ApiError::User(105u16));

let mut unprocessed_deposits: Vec<(u32, L1Deposit)> =
Vec::with_capacity(events_length as usize);

for i in unprocessed_deposits_index..events_length {
let event_key = storage::dictionary_get(events_dict_uref, &i.to_string())
.unwrap_or_revert()
.unwrap_or_revert_with(ApiError::User(106u16));

let event_bytes: Bytes = storage::read(event_key)
.unwrap_or_revert()
.unwrap_or_revert_with(ApiError::User(107u16));

match L1Deposit::from_bytes(&event_bytes) {
Ok((deposit, [])) => unprocessed_deposits.push((i, deposit)),
// There should be no trailing bytes
Ok((_, [_, ..])) => {
runtime::revert(ApiError::User(108u16));
}
Err(_) => continue,
}
}

(unprocessed_deposits_index, unprocessed_deposits)
}

/// Check that the deposits in the batch match the deposits in the unprocessed deposits list.
/// The batch deposits must an ordered subset of the unprocessed deposits.
///
/// Returns the event index of the first unprocessed deposit that is not present in the batch.
/// If the batch contains all unprocessed deposits,
/// the returned index will point to the next event emitted by the contract.
///
/// Panics: This functions error codes are in the range of 201-299.
fn check_batch_deposits_against_unprocessed(batch_deposits: &[L1Deposit]) -> u32 {
let (unprocessed_deposits_idx, unprocessed_deposits) = get_unprocessed_deposits();

// This check ensures that zip does not smuggle fake deposits.
// Without this check, an attacker could submit a batch with deposits that are not in the unprocessed list.
if batch_deposits.len() > unprocessed_deposits.len() {
runtime::revert(ApiError::User(201u16));
};

batch_deposits.iter().zip(unprocessed_deposits.iter()).fold(
unprocessed_deposits_idx,
|unprocessed_deposits_idx, (batch_deposit, (event_idx, unprocessed_deposit))| {
assert!(unprocessed_deposits_idx <= *event_idx);

if batch_deposit != unprocessed_deposit {
runtime::revert(ApiError::User(202u16));
}
*event_idx
},
)
}

/// Execute the withdrawals from the batch.
/// Errors are in the range of 301-399.
///
/// TODO guard against tiny withdrawals that could be used to spam the contract.
fn execute_withdrawals(withdrawals: &[Signed<Withdraw>]) {
for withdraw in withdrawals {
let (recipient, trailing_bytes) = PublicKey::from_bytes(&withdraw.public_key)
.unwrap_or_revert_with(ApiError::User(301u16));

if !trailing_bytes.is_empty() {
runtime::revert(ApiError::User(302u16));
}

let amount = U512::from(withdraw.transaction.amount);

let deposit_purse: URef = runtime::get_key(KAIROS_DEPOSIT_PURSE)
.unwrap_or_revert_with(ApiError::User(303u16))
.into_uref()
.unwrap_or_revert_with(ApiError::User(304u16));

system::transfer_from_purse_to_account(
deposit_purse,
recipient.to_account_hash(),
amount,
None,
)
.unwrap_or_revert();
}
}

#[no_mangle]
pub extern "C" fn call() {
let entry_points = EntryPoints::from(vec![
Expand All @@ -147,7 +263,7 @@ pub extern "C" fn call() {
let trie_root_uref: URef = storage::new_uref(initial_trie_root);
let named_keys = NamedKeys::from([
(
KAIROS_LAST_PROCESSED_DEPOSIT_COUNTER.to_string(),
KAIROS_UNPROCESSED_DEPOSIT_INDEX.to_string(),
last_processed_deposit_counter_uref.into(),
),
(KAIROS_TRIE_ROOT.to_string(), trie_root_uref.into()),
Expand Down
1 change: 1 addition & 0 deletions kairos-prover/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading