From d42dd40c3fd0fd2ce11b185ef0049cc7bfeb0fc9 Mon Sep 17 00:00:00 2001 From: ZenGround0 <5515260+ZenGround0@users.noreply.github.com> Date: Sun, 1 May 2022 15:42:48 -0400 Subject: [PATCH] Multisig vesting unit tests (#295) * First vesting test * add blake2b to test vm Co-authored-by: zenground0 --- Cargo.lock | 1 + actors/multisig/tests/multisig_actor_test.rs | 286 ++++++++++++++++++- actors/multisig/tests/util.rs | 28 +- test_vm/Cargo.toml | 1 + test_vm/src/lib.rs | 17 +- 5 files changed, 315 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2b5d446e..855c46c64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1743,6 +1743,7 @@ name = "test_vm" version = "8.0.0-alpha.1" dependencies = [ "anyhow", + "blake2b_simd", "cid", "fil_actor_account", "fil_actor_cron", diff --git a/actors/multisig/tests/multisig_actor_test.rs b/actors/multisig/tests/multisig_actor_test.rs index d9c86eb22..82158ff9d 100644 --- a/actors/multisig/tests/multisig_actor_test.rs +++ b/actors/multisig/tests/multisig_actor_test.rs @@ -5,6 +5,8 @@ use fil_actors_runtime::test_utils::*; use fil_actors_runtime::{INIT_ACTOR_ADDR, SYSTEM_ACTOR_ADDR}; use fvm_ipld_encoding::RawBytes; use fvm_shared::address::{Address, BLS_PUB_LEN}; +use fvm_shared::bigint::Zero; +use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; use fvm_shared::error::ExitCode; use fvm_shared::METHOD_SEND; @@ -113,7 +115,7 @@ mod constructor_tests { let st: State = rt.get_state(); assert_eq!(params.signers, st.signers); assert_eq!(params.num_approvals_threshold, st.num_approvals_threshold); - assert_eq!(TokenAmount::from(0u8), st.initial_balance); + assert_eq!(TokenAmount::zero(), st.initial_balance); assert_eq!(100, st.unlock_duration); assert_eq!(1234, st.start_epoch); h.assert_transactions(&rt, vec![]); @@ -205,7 +207,7 @@ mod constructor_tests { anne_non_id, METHOD_SEND, RawBytes::default(), - TokenAmount::from(0u8), + TokenAmount::zero(), RawBytes::default(), ExitCode::OK, ); @@ -265,6 +267,284 @@ mod constructor_tests { } } +#[cfg(test)] +mod vesting_tests { + use super::*; + + const MSIG: Address = Address::new_id(1000); + const ANNE: Address = Address::new_id(101); + const BOB: Address = Address::new_id(102); + const CHARLIE: Address = Address::new_id(103); + const DARLENE: Address = Address::new_id(104); + + const UNLOCK_DURATION: ChainEpoch = 10; + const START_EPOCH: ChainEpoch = 0; + const MSIG_INITIAL_BALANCE: u8 = 100; + + #[test] + fn happy_path_full_vesting() { + let mut rt = construct_runtime(MSIG); + let h = util::ActorHarness::new(); + + rt.set_balance(TokenAmount::from(MSIG_INITIAL_BALANCE)); + rt.set_received(TokenAmount::from(MSIG_INITIAL_BALANCE)); + h.construct_and_verify(&mut rt, 2, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]); + rt.set_received(TokenAmount::zero()); + + // anne proposes that darlene receive inital balance + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE); + let proposal_hash = h.propose_ok( + &mut rt, + DARLENE, + TokenAmount::from(MSIG_INITIAL_BALANCE), + METHOD_SEND, + RawBytes::default(), + ); + + // bob approves anne's tx too soon + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, BOB); + expect_abort(ExitCode::USR_INSUFFICIENT_FUNDS, h.approve(&mut rt, TxnID(0), proposal_hash)); + rt.reset(); + + // advance the epoch s.t. all funds are unlocked + rt.set_epoch(START_EPOCH + UNLOCK_DURATION); + rt.expect_send( + DARLENE, + METHOD_SEND, + RawBytes::default(), + TokenAmount::from(MSIG_INITIAL_BALANCE), + RawBytes::default(), + ExitCode::OK, + ); + assert_eq!(RawBytes::default(), h.approve_ok(&mut rt, TxnID(0), proposal_hash)) + + // h.check_state() + } + + #[test] + fn partial_vesting_propose_to_send_half_the_actor_balance_when_the_epoch_is_half_the_unlock_duration( + ) { + let mut rt = construct_runtime(MSIG); + let h = util::ActorHarness::new(); + + rt.set_balance(TokenAmount::from(MSIG_INITIAL_BALANCE)); + rt.set_received(TokenAmount::from(MSIG_INITIAL_BALANCE)); + h.construct_and_verify(&mut rt, 2, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]); + rt.set_received(TokenAmount::zero()); + + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE); + let proposal_hash = h.propose_ok( + &mut rt, + DARLENE, + TokenAmount::from(MSIG_INITIAL_BALANCE / 2), + METHOD_SEND, + RawBytes::default(), + ); + rt.set_epoch(START_EPOCH + UNLOCK_DURATION / 2); + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, BOB); + rt.expect_send( + DARLENE, + METHOD_SEND, + RawBytes::default(), + TokenAmount::from(MSIG_INITIAL_BALANCE / 2), + RawBytes::default(), + ExitCode::OK, + ); + h.approve_ok(&mut rt, TxnID(0), proposal_hash); + + // h.check_state() + } + + #[test] + fn propose_and_autoapprove_tx_above_locked_amount_fails() { + let mut rt = construct_runtime(MSIG); + let h = util::ActorHarness::new(); + + rt.set_balance(TokenAmount::from(MSIG_INITIAL_BALANCE)); + rt.set_received(TokenAmount::from(MSIG_INITIAL_BALANCE)); + h.construct_and_verify(&mut rt, 1, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]); + rt.set_received(TokenAmount::zero()); + + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE); + expect_abort( + ExitCode::USR_INSUFFICIENT_FUNDS, + h.propose( + &mut rt, + DARLENE, + TokenAmount::from(MSIG_INITIAL_BALANCE), + METHOD_SEND, + RawBytes::default(), + ), + ); + rt.reset(); + rt.set_epoch(START_EPOCH + UNLOCK_DURATION / 10); + let amount_out = TokenAmount::from(MSIG_INITIAL_BALANCE / 10); + rt.expect_send( + DARLENE, + METHOD_SEND, + RawBytes::default(), + amount_out.clone(), + RawBytes::default(), + ExitCode::OK, + ); + h.propose_ok(&mut rt, DARLENE, amount_out, METHOD_SEND, RawBytes::default()); + + // h.check_state() + } + + #[test] + fn fail_to_vest_more_than_locked_amount() { + let mut rt = construct_runtime(MSIG); + let h = util::ActorHarness::new(); + + rt.set_balance(TokenAmount::from(MSIG_INITIAL_BALANCE)); + rt.set_received(TokenAmount::from(MSIG_INITIAL_BALANCE)); + h.construct_and_verify(&mut rt, 2, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]); + rt.set_received(TokenAmount::zero()); + + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE); + let proposal_hash = h.propose_ok( + &mut rt, + DARLENE, + TokenAmount::from(MSIG_INITIAL_BALANCE / 2), + METHOD_SEND, + RawBytes::default(), + ); + rt.set_epoch(START_EPOCH + UNLOCK_DURATION / 10); + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, BOB); + expect_abort(ExitCode::USR_INSUFFICIENT_FUNDS, h.approve(&mut rt, TxnID(0), proposal_hash)); + } + + #[test] + fn avoid_truncating_division() { + let mut rt = construct_runtime(MSIG); + let h = util::ActorHarness::new(); + + let locked_balance = TokenAmount::from(UNLOCK_DURATION - 1); // balance < duration + let one = TokenAmount::from(1u8); + rt.set_balance(locked_balance.clone()); + rt.set_received(locked_balance.clone()); + h.construct_and_verify(&mut rt, 1, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]); + rt.set_received(TokenAmount::zero()); + + // expect nothing vested yet + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE); + expect_abort( + ExitCode::USR_INSUFFICIENT_FUNDS, + h.propose(&mut rt, ANNE, one.clone(), METHOD_SEND, RawBytes::default()), + ); + rt.reset(); + + // expect nothing ( (x-1/x) <1 unit) vested after 1 epoch + rt.set_epoch(START_EPOCH + 1); + expect_abort( + ExitCode::USR_INSUFFICIENT_FUNDS, + h.propose(&mut rt, ANNE, one.clone(), METHOD_SEND, RawBytes::default()), + ); + rt.reset(); + + // expect 1 unit available after 2 epochs + rt.set_epoch(START_EPOCH + 2); + rt.expect_send( + ANNE, + METHOD_SEND, + RawBytes::default(), + one.clone(), + RawBytes::default(), + ExitCode::OK, + ); + h.propose_ok(&mut rt, ANNE, one.clone(), METHOD_SEND, RawBytes::default()); + rt.set_balance(locked_balance.clone()); + + // do not expect full vesting before full duration elapsed + rt.set_epoch(START_EPOCH + UNLOCK_DURATION - 1); + expect_abort( + ExitCode::USR_INSUFFICIENT_FUNDS, + h.propose(&mut rt, ANNE, locked_balance.clone(), METHOD_SEND, RawBytes::default()), + ); + rt.reset(); + + // expect all but one unit available after all but one epochs + rt.expect_send( + ANNE, + METHOD_SEND, + RawBytes::default(), + locked_balance.clone() - one.clone(), + RawBytes::default(), + ExitCode::OK, + ); + h.propose_ok(&mut rt, ANNE, locked_balance.clone() - one, METHOD_SEND, RawBytes::default()); + rt.set_balance(locked_balance.clone()); + + // expect everything after exactly lock duration + rt.set_epoch(START_EPOCH + UNLOCK_DURATION); + rt.expect_send( + ANNE, + METHOD_SEND, + RawBytes::default(), + locked_balance.clone(), + RawBytes::default(), + ExitCode::OK, + ); + h.propose_ok(&mut rt, ANNE, locked_balance, METHOD_SEND, RawBytes::default()); + } + + #[test] + fn sending_zero_ok_when_nothing_vests() { + let mut rt = construct_runtime(MSIG); + let h = util::ActorHarness::new(); + + rt.set_balance(TokenAmount::from(MSIG_INITIAL_BALANCE)); + rt.set_received(TokenAmount::from(MSIG_INITIAL_BALANCE)); + h.construct_and_verify(&mut rt, 2, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]); + rt.set_received(TokenAmount::zero()); + + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE); + rt.expect_send( + BOB, + METHOD_SEND, + RawBytes::default(), + TokenAmount::zero(), + RawBytes::default(), + ExitCode::OK, + ); + } + + #[test] + fn sending_zero_when_lockup_exceeds_balance() { + let mut rt = construct_runtime(MSIG); + let h = util::ActorHarness::new(); + + h.construct_and_verify(&mut rt, 1, 0, START_EPOCH, vec![ANNE]); + rt.set_caller(*MULTISIG_ACTOR_CODE_ID, MSIG); + rt.set_balance(TokenAmount::from(10u8)); + rt.set_received(TokenAmount::from(10u8)); + + // lock up funds the actor doesn't have yet + h.lock_balance(&mut rt, START_EPOCH, UNLOCK_DURATION, TokenAmount::from(10u8)).unwrap(); + + // make a tx that transfers no value + let send_amount = TokenAmount::zero(); + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE); + rt.expect_send( + BOB, + METHOD_SEND, + RawBytes::default(), + send_amount.clone(), + RawBytes::default(), + ExitCode::OK, + ); + h.propose_ok(&mut rt, BOB, send_amount, METHOD_SEND, RawBytes::default()); + + // verify that sending any value is prevented + let send_amount = TokenAmount::from(1u8); + expect_abort( + ExitCode::USR_INSUFFICIENT_FUNDS, + h.propose(&mut rt, BOB, send_amount, METHOD_SEND, RawBytes::default()), + ) + } +} + // Propose #[test] @@ -676,7 +956,7 @@ fn test_approve_simple_propose_and_approval() { let fake_params = RawBytes::from(vec![1, 2, 3, 4]); let fake_method = 42; let fake_ret = RawBytes::from(vec![4, 3, 2, 1]); - let send_value = TokenAmount::from(10u8); + let send_value = TokenAmount::zero(); rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, anne); let proposal_hash = h.propose_ok(&mut rt, chuck, send_value.clone(), fake_method, fake_params.clone()); diff --git a/actors/multisig/tests/util.rs b/actors/multisig/tests/util.rs index a68ad47fe..cb564ae93 100644 --- a/actors/multisig/tests/util.rs +++ b/actors/multisig/tests/util.rs @@ -1,6 +1,7 @@ use fil_actor_multisig::{ compute_proposal_hash, Actor, AddSignerParams, ApproveReturn, ConstructorParams, Method, - ProposeParams, RemoveSignerParams, State, SwapSignerParams, Transaction, TxnID, TxnIDParams, + ProposeParams, ProposeReturn, RemoveSignerParams, State, SwapSignerParams, Transaction, TxnID, + TxnIDParams, }; use fil_actor_multisig::{ChangeNumApprovalsThresholdParams, LockBalanceParams}; use fil_actors_runtime::test_utils::*; @@ -94,12 +95,8 @@ impl ActorHarness { method: MethodNum, params: RawBytes, ) -> [u8; 32] { - rt.expect_validate_caller_type(vec![*ACCOUNT_ACTOR_CODE_ID, *MULTISIG_ACTOR_CODE_ID]); - let propose_params = - ProposeParams { to, value: value.clone(), method, params: params.clone() }; - rt.call::(Method::Propose as u64, &RawBytes::serialize(propose_params).unwrap()) - .unwrap(); - rt.verify(); + let ret = self.propose(rt, to, value.clone(), method, params.clone()); + ret.unwrap().deserialize::().unwrap(); // compute proposal hash let txn = Transaction { to, value, method, params, approved: vec![rt.caller] }; compute_proposal_hash(&txn, rt).unwrap() @@ -119,6 +116,23 @@ impl ActorHarness { approve_ret.ret } + pub fn propose( + &self, + rt: &mut MockRuntime, + to: Address, + value: TokenAmount, + method: MethodNum, + params: RawBytes, + ) -> Result { + rt.expect_validate_caller_type(vec![*ACCOUNT_ACTOR_CODE_ID, *MULTISIG_ACTOR_CODE_ID]); + let propose_params = + ProposeParams { to, value: value.clone(), method, params: params.clone() }; + let ret = + rt.call::(Method::Propose as u64, &RawBytes::serialize(propose_params).unwrap()); + rt.verify(); + ret + } + pub fn approve( &self, rt: &mut MockRuntime, diff --git a/test_vm/Cargo.toml b/test_vm/Cargo.toml index becd7fa13..badf661ee 100644 --- a/test_vm/Cargo.toml +++ b/test_vm/Cargo.toml @@ -35,5 +35,6 @@ cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] serde = { version = "1.0.136", features = ["derive"] } thiserror = "1.0.30" anyhow = "1.0.56" +blake2b_simd = "1.0" diff --git a/test_vm/src/lib.rs b/test_vm/src/lib.rs index 21cb70c83..689b09a18 100644 --- a/test_vm/src/lib.rs +++ b/test_vm/src/lib.rs @@ -634,9 +634,7 @@ impl<'invocation, 'bs> Runtime for InvocationCtx<'invocation, policy: self.policy, subinvocations: RefCell::new(vec![]), }; - println!("starting send invoc [{}:{}]", to, method); let res = new_ctx.invoke(); - println!("finished send invoc [{}:{}]", to, method); let invoc = new_ctx.gather_trace(res.clone()); RefMut::map(self.subinvocations.borrow_mut(), |subinvocs| { @@ -751,12 +749,15 @@ impl Primitives for InvocationCtx<'_, '_> { Ok(()) } - fn hash_blake2b(&self, _data: &[u8]) -> [u8; 32] { - // TODO: actual blake 2b - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, - ] + fn hash_blake2b(&self, data: &[u8]) -> [u8; 32] { + blake2b_simd::Params::new() + .hash_length(32) + .to_state() + .update(data) + .finalize() + .as_bytes() + .try_into() + .unwrap() } fn compute_unsealed_sector_cid(