From 8735dd62a1e4662f236a277ef8abb091160e174f Mon Sep 17 00:00:00 2001 From: ian Date: Fri, 2 Feb 2024 12:44:42 +0800 Subject: [PATCH] :sparkles: Verify claim from --- contracts/__tests__/src/lib.rs | 54 +++ contracts/__tests__/src/tests.rs | 318 +++++++++++++++++- .../src/derived_dao_action_data.rs | 146 +++++++- 3 files changed, 498 insertions(+), 20 deletions(-) diff --git a/contracts/__tests__/src/lib.rs b/contracts/__tests__/src/lib.rs index d1cc8be..9e0ff17 100644 --- a/contracts/__tests__/src/lib.rs +++ b/contracts/__tests__/src/lib.rs @@ -517,6 +517,60 @@ where spec } +pub fn create_claim_spec( + deposit_header: HeaderView, + withdraw_header: HeaderView, + claim_builder: F, +) -> CustomTxSpec +where + F: Fn(&mut CustomTxSpec) -> Claim, +{ + let mut spec = CustomTxSpec::default(); + let claim = claim_builder(&mut spec); + + let deposit_header_hash = deposit_header.hash(); + let deposit_block_number = pack_uint64(deposit_header.number()); + let withdraw_header_hash = withdraw_header.hash(); + + spec.inner.context.insert_header(deposit_header); + spec.inner.context.insert_header(withdraw_header); + spec.inner.context.link_cell_with_block( + spec.inner.dao_input_out_point.clone(), + withdraw_header_hash.clone(), + 0, + ); + + let witnesses = vec![ + packed::WitnessArgs::new_builder() + .input_type(Some(Bytes::from(0u64.to_le_bytes().to_vec())).pack()) + .build() + .as_bytes(), + Bytes::new(), + spec.inner.pack_dao_operations(vec![], vec![], vec![claim]), + ]; + let header_deps = vec![deposit_header_hash, withdraw_header_hash]; + + let dao_type_script = spec.inner.dao_type_script.clone(); + spec.on_new_input_spec(move |cell| CellSpec { + output: cell.output.type_(Some(dao_type_script.clone()).pack()), + data: deposit_block_number.as_bytes(), + ..cell + }); + spec.on_new_output_spec(|cell| CellSpec { + output: cell.output.type_(packed::ScriptOpt::default()), + data: Bytes::new(), + ..cell + }); + + spec.on_new_tx_builder(move |b| { + b.set_witnesses(vec![]) + .witnesses(witnesses.clone().pack()) + .header_deps(header_deps.clone()) + }); + + spec +} + pub fn new_header_builder(number: u64, epoch_length: u64) -> HeaderBuilder { let epoch_number = number / epoch_length; let index = number % epoch_length; diff --git a/contracts/__tests__/src/tests.rs b/contracts/__tests__/src/tests.rs index 5196e65..df15a80 100644 --- a/contracts/__tests__/src/tests.rs +++ b/contracts/__tests__/src/tests.rs @@ -146,21 +146,8 @@ fn test_dao_withdraw_not_found() { #[test] fn test_dao_withdraw_not_coverred() { - let mut spec = create_withdraw_spec(new_header_builder(1, 100).build(), None, |spec| { - Withdraw::new_builder() - .cell_pointer(OutPoint::new_unchecked(non_existing_out_point().as_bytes())) - .from(Address::new_unchecked( - spec.inner.alice_lock_script.as_bytes(), - )) - .to(Address::new_unchecked( - spec.inner.alice_lock_script.as_bytes(), - )) - .deposit_info( - DepositInfo::new_builder() - .amount(pack_capacity(DEFAULT_CAPACITY)) - .build(), - ) - .build() + let mut spec = create_withdraw_spec(new_header_builder(1, 100).build(), None, |_spec| { + Withdraw::default() }); let witnesses = vec![ @@ -718,3 +705,304 @@ fn test_withdraw_waiting_milliseconds_boundary_cases() { DAO_CYCLE * ESITMATED_EPOCH_DURATION - ESITMATED_EPOCH_DURATION / 10, ); } + +#[test] +fn test_dao_claim_not_found() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).build(), + new_header_builder(2, 100).build(), + |_spec| { + Claim::new_builder() + .cell_pointer(OutPoint::new_unchecked(non_existing_out_point().as_bytes())) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotFound, MAX_CYCLES); +} + +#[test] +fn test_dao_claim_not_coverred() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).build(), + new_header_builder(2, 100).build(), + |_spec| Claim::default(), + ); + + let witnesses = vec![ + Bytes::new(), + Bytes::new(), + spec.inner + .pack_dao_data(DaoActionData::default().as_bytes()), + ]; + spec.on_new_tx_builder(move |b| b.set_witnesses(vec![]).witnesses(witnesses.clone().pack())); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotCoverred, MAX_CYCLES); +} + +#[test] +fn test_dao_claim_from_not_match() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).build(), + new_header_builder(2, 100).build(), + |spec| { + Claim::new_builder() + .cell_pointer(OutPoint::new_unchecked( + spec.inner.dao_input_out_point.as_bytes(), + )) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_claim_amount_not_match() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).build(), + new_header_builder(2, 100).build(), + |spec| { + Claim::new_builder() + .cell_pointer(OutPoint::new_unchecked( + spec.inner.dao_input_out_point.as_bytes(), + )) + .from(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_claim_deposit_block_number_not_match() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).build(), + new_header_builder(2, 100).build(), + |spec| { + Claim::new_builder() + .cell_pointer(OutPoint::new_unchecked( + spec.inner.dao_input_out_point.as_bytes(), + )) + .from(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .deposit_info( + DepositInfo::new_builder() + .amount(pack_capacity(DEFAULT_CAPACITY)) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_claim_deposit_timestamp_not_match() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).timestamp(1.pack()).build(), + new_header_builder(2, 100).build(), + |spec| { + Claim::new_builder() + .cell_pointer(OutPoint::new_unchecked( + spec.inner.dao_input_out_point.as_bytes(), + )) + .from(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .to(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .deposit_info( + DepositInfo::new_builder() + .deposit_block_number(pack_uint64(1)) + .amount(pack_capacity(DEFAULT_CAPACITY)) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_claim_larger_withdraw_block_number() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).build(), + new_header_builder(1, 100).build(), + |spec| { + Claim::new_builder() + .cell_pointer(OutPoint::new_unchecked( + spec.inner.dao_input_out_point.as_bytes(), + )) + .from(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .to(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .deposit_info( + DepositInfo::new_builder() + .deposit_block_number(pack_uint64(1)) + .amount(pack_capacity(DEFAULT_CAPACITY)) + .build(), + ) + .withdraw_info(WithdrawInfo::new_builder().build()) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_claim_withdraw_block_number_not_matched() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).build(), + new_header_builder(2, 100).build(), + |spec| { + Claim::new_builder() + .cell_pointer(OutPoint::new_unchecked( + spec.inner.dao_input_out_point.as_bytes(), + )) + .from(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .to(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .deposit_info( + DepositInfo::new_builder() + .deposit_block_number(pack_uint64(1)) + .amount(pack_capacity(DEFAULT_CAPACITY)) + .build(), + ) + .withdraw_info(WithdrawInfo::new_builder().build()) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_claim_withdraw_timestamp_not_matched() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).build(), + new_header_builder(2, 100).timestamp(1.pack()).build(), + |spec| { + Claim::new_builder() + .cell_pointer(OutPoint::new_unchecked( + spec.inner.dao_input_out_point.as_bytes(), + )) + .from(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .to(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .deposit_info( + DepositInfo::new_builder() + .deposit_block_number(pack_uint64(1)) + .amount(pack_capacity(DEFAULT_CAPACITY)) + .build(), + ) + .withdraw_info( + WithdrawInfo::new_builder() + .withdraw_block_number(pack_uint64(2)) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_claim_componsation_amount_not_matched() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).dao(pack_ar(100)).build(), + new_header_builder(2, 100).dao(pack_ar(110)).build(), + |spec| { + Claim::new_builder() + .cell_pointer(OutPoint::new_unchecked( + spec.inner.dao_input_out_point.as_bytes(), + )) + .from(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .to(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .deposit_info( + DepositInfo::new_builder() + .deposit_block_number(pack_uint64(1)) + .amount(pack_capacity(DEFAULT_CAPACITY)) + .build(), + ) + .withdraw_info( + WithdrawInfo::new_builder() + .withdraw_block_number(pack_uint64(2)) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_claim_pass() { + let mut spec = create_claim_spec( + new_header_builder(1, 100).dao(pack_ar(100)).build(), + new_header_builder(2, 100).dao(pack_ar(110)).build(), + |spec| { + Claim::new_builder() + .cell_pointer(OutPoint::new_unchecked( + spec.inner.dao_input_out_point.as_bytes(), + )) + .from(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .to(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .deposit_info( + DepositInfo::new_builder() + .deposit_block_number(pack_uint64(1)) + .amount(pack_capacity(DEFAULT_CAPACITY)) + .build(), + ) + .withdraw_info( + WithdrawInfo::new_builder() + .withdraw_block_number(pack_uint64(2)) + .componsation_amount(pack_capacity( + (DEFAULT_CAPACITY - DAO_INPUT_OCCUPIED_CAPACITY) / 10, + )) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + verify_and_dump_failed_tx(&spec.inner.context, &tx, MAX_CYCLES).expect("pass"); +} diff --git a/contracts/dao-action-verifier/src/derived_dao_action_data.rs b/contracts/dao-action-verifier/src/derived_dao_action_data.rs index 0389dc7..9ba3729 100644 --- a/contracts/dao-action-verifier/src/derived_dao_action_data.rs +++ b/contracts/dao-action-verifier/src/derived_dao_action_data.rs @@ -2,7 +2,7 @@ use core::cmp; use alloc::collections::BTreeMap; -use ckb_dao_cobuild_schemas::{DaoActionData, Deposit, OutPoint, Withdraw}; +use ckb_dao_cobuild_schemas::{Claim, DaoActionData, Deposit, OutPoint, Withdraw}; use ckb_std::{ ckb_constants::Source, ckb_types::{bytes::Bytes, packed, prelude::*}, @@ -38,10 +38,14 @@ pub struct WithdrawValue { input_cell_output: packed::CellOutput, } +pub type ClaimKey = WithdrawKey; +pub type ClaimValue = WithdrawValue; + #[derive(Default, Debug)] pub struct DerivedDaoActionData { deposits: BTreeMap, withdraws: BTreeMap, + claims: BTreeMap, } fn is_deposit_cell(index: usize, source: Source) -> bool { @@ -140,11 +144,10 @@ impl DerivedDaoActionData { .collect(); #[allow(clippy::mutable_key_type)] - let withdraws = QueryIter::new(load_cell, Source::Input) + let (withdraws, claims) = QueryIter::new(load_cell, Source::Input) .enumerate() - .filter(|(index, input_cell_output)| { + .filter(|(_index, input_cell_output)| { input_cell_output.type_().as_slice() == DAO_TYPE_SCRIPT - && is_deposit_cell(*index, Source::Input) }) .map(|(index, input_cell_output)| { ( @@ -157,11 +160,12 @@ impl DerivedDaoActionData { }, ) }) - .collect(); + .partition(|(_key, value)| is_deposit_cell(value.index, Source::Input)); Self { deposits, withdraws, + claims, } } @@ -181,6 +185,13 @@ impl DerivedDaoActionData { value.index )); } + if let Some(value) = self.claims.into_values().next() { + return Err(trace_error!( + ErrorCode::NotCoverred, + "claim at input {} not coverred by dao action", + value.index + )); + } Ok(()) } @@ -192,6 +203,9 @@ impl DerivedDaoActionData { for withdraw in dao_action_data.withdraws().into_iter() { self.verify_withdraw(withdraw)?; } + for claim in dao_action_data.claims().into_iter() { + self.verify_claim(claim)?; + } Ok(()) } @@ -366,6 +380,128 @@ impl DerivedDaoActionData { )), } } + + fn verify_claim(&mut self, claim: Claim) -> Result<(), Error> { + match self.claims.remove(&((&claim.cell_pointer()).into())) { + Some(ClaimValue { + index, + input_cell_output, + }) => { + if input_cell_output.lock().as_slice() != claim.from().as_slice() { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect claim from {}, got {}", + claim.from(), + input_cell_output.lock() + )); + } + + let deposit_info = claim.deposit_info(); + if input_cell_output.capacity().as_slice() != deposit_info.amount().as_slice() { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect claim amount {}, got {}", + deposit_info.amount(), + input_cell_output.capacity() + )); + } + + let deposit_header = load_header_from_witness( + load_witness_args(index, Source::Input) + .map_err(|err| { + trace_error!(err, "failed to load witness args at {}", index) + })? + .input_type() + .to_opt() + .ok_or_else(|| { + trace_error!( + ErrorCode::InvalidHeaderDepIndex, + "invalid witness args input type at {}", + index + ) + })? + .raw_data() + .as_ref(), + )?; + if deposit_header.number().as_slice() + != deposit_info.deposit_block_number().as_slice() + { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect deposit block number {}, got {}", + deposit_info.deposit_block_number(), + deposit_header.number() + )); + } + if deposit_header.timestamp().as_slice() + != deposit_info.deposit_timestamp().as_slice() + { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect deposit block timestamp {}, got {}", + deposit_info.deposit_timestamp(), + deposit_header.timestamp() + )); + } + + let withdraw_header = load_header(index, Source::Input)?.raw(); + let withdraw_info = claim.withdraw_info(); + if withdraw_header.number().unpack() < deposit_header.number().unpack() { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect withdraw block number >= {}, got {}", + deposit_header.number(), + withdraw_header.number() + )); + } + if withdraw_header.number().as_slice() + != withdraw_info.withdraw_block_number().as_slice() + { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect withdraw block number {}, got {}", + withdraw_info.withdraw_block_number(), + withdraw_header.number() + )); + } + if withdraw_header.timestamp().as_slice() + != withdraw_info.withdraw_timestamp().as_slice() + { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect withdraw block timestamp {}, got {}", + withdraw_info.withdraw_timestamp(), + withdraw_header.timestamp() + )); + } + + let counted_capacity = input_cell_output.capacity().unpack() + - load_cell_occupied_capacity(index, Source::Input)?; + let actual_componsation_amount = compute_componsation_amount( + counted_capacity, + &deposit_header, + &withdraw_header, + ); + if actual_componsation_amount.as_slice() + != withdraw_info.componsation_amount().as_slice() + { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect withdraw componsation amount {}, got {}", + withdraw_info.componsation_amount(), + actual_componsation_amount + )); + } + + Ok(()) + } + None => Err(trace_error!( + ErrorCode::NotFound, + "claim not found in tx: {}", + claim + )), + } + } } impl From<&packed::CellOutput> for DepositKey {