From 8364666bfc7771fb00b7ed20871bd27ed92c7071 Mon Sep 17 00:00:00 2001 From: ian Date: Thu, 1 Feb 2024 19:43:45 +0800 Subject: [PATCH] :sparkles: Verify withdraw operation --- Makefile | 4 +- contracts/__tests__/src/lib.rs | 114 +++- contracts/__tests__/src/tests.rs | 604 +++++++++++++++++- .../src/derived_dao_action_data.rs | 335 +++++++++- .../dao-action-verifier/src/error_code.rs | 2 + 5 files changed, 1037 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index 283fed4..5b767b7 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +CARGO_TEST_ARGS := -- --nocapture + all: web contract web: @@ -6,7 +8,7 @@ web: contract: schemas make -f contracts.mk build - make CARGO_ARGS="-- --nocapture" -f contracts.mk test + make CARGO_ARGS="${CARGO_TEST_ARGS}" -f contracts.mk test SCHEMA_MOL_FILES := $(wildcard schemas/*.mol) SCHEMA_RUST_FILES := $(patsubst %.mol,crates/ckb-dao-cobuild-schemas/src/%.rs,$(SCHEMA_MOL_FILES)) diff --git a/contracts/__tests__/src/lib.rs b/contracts/__tests__/src/lib.rs index 670c2ad..d1cc8be 100644 --- a/contracts/__tests__/src/lib.rs +++ b/contracts/__tests__/src/lib.rs @@ -1,5 +1,6 @@ use ckb_dao_cobuild_schemas::{ - Capacity, Claim, ClaimVec, DaoActionData, Deposit, DepositVec, Withdraw, WithdrawVec, + Capacity, Claim, ClaimVec, DaoActionData, Deposit, DepositVec, Uint64 as DaoActionUint64, + Withdraw, WithdrawVec, }; use ckb_testtool::{ builtin::ALWAYS_SUCCESS, @@ -7,11 +8,14 @@ use ckb_testtool::{ ckb_hash::blake2b_256, ckb_types::{ bytes::Bytes, - core::{Cycle, TransactionBuilder, TransactionView}, + core::{ + Cycle, EpochNumberWithFraction, HeaderBuilder, HeaderView, TransactionBuilder, + TransactionView, + }, packed, prelude::*, }, - context::Context, + context::{random_out_point, Context}, }; use ckb_transaction_cobuild::schemas::{ basic::{Action, ActionVec, Message, SighashAll}, @@ -173,6 +177,7 @@ pub fn build_tx(spec: &mut T) -> TransactionView { pub struct DefaultTxSpec { context: Context, + dao_input_out_point: packed::OutPoint, alice_lock_script: packed::Script, @@ -183,6 +188,7 @@ pub struct DefaultTxSpec { impl DefaultTxSpec { fn new() -> Self { let mut context = Context::default(); + let dao_input_out_point = random_out_point(); let loader = Loader::default(); // use always success as lock @@ -233,6 +239,7 @@ impl DefaultTxSpec { Self { context, + dao_input_out_point, alice_lock_script, verifier_type_script, dao_type_script, @@ -290,13 +297,30 @@ impl Default for DefaultTxSpec { } } -pub const DEFAULT_CAPACITY: u64 = 1000; +pub const DEFAULT_CAPACITY: u64 = 1000_0000_0000; pub const DAO_DEPOSIT_DATA: [u8; 8] = hex!("0000000000000000"); +pub const DAO_CYCLE: u64 = 180; +pub const ESITMATED_EPOCH_DURATION: u64 = 4 * 60 * 60 * 1000; +// data: 8, the block number +// capacity: 8 +// lock: 32+1+1, always success + arg(1) +// type: 32+1, dao + arg(0) +pub const DAO_INPUT_OCCUPIED_CAPACITY: u64 = 83_0000_0000; pub fn pack_capacity(shannons: u64) -> Capacity { Capacity::new_unchecked(Bytes::from(shannons.to_le_bytes().to_vec())) } +pub fn pack_uint64(number: u64) -> DaoActionUint64 { + DaoActionUint64::new_unchecked(Bytes::from(number.to_le_bytes().to_vec())) +} + +pub fn pack_ar(ar: u64) -> packed::Byte32 { + let mut dao_buf = vec![0u8; 32]; + dao_buf[8..16].copy_from_slice(&ar.to_le_bytes()); + packed::Byte32::new_unchecked(Bytes::from(dao_buf)) +} + impl TxSpec for DefaultTxSpec { fn new_dao_input_spec(&mut self) -> CellSpec { CellSpec { @@ -324,11 +348,13 @@ impl TxSpec for DefaultTxSpec { dao_input_spec: CellSpec, dao_output_spec: CellSpec, ) -> TransactionBuilder { - let dao_cell = self - .context - .create_cell(dao_input_spec.output.build(), dao_input_spec.data); + self.context.create_cell_with_out_point( + self.dao_input_out_point.clone(), + dao_input_spec.output.build(), + dao_input_spec.data, + ); let dao_input = packed::CellInput::new_builder() - .previous_output(dao_cell) + .previous_output(self.dao_input_out_point.clone()) .since(dao_input_spec.since) .build(); let dao_output = dao_output_spec.output.build(); @@ -427,3 +453,75 @@ impl CustomTxSpec { self } } + +// Create an out point that does not exist in the tx. +pub fn non_existing_out_point() -> packed::OutPoint { + packed::OutPoint::new_builder() + .index(u32::MAX.pack()) + .build() +} + +pub fn create_withdraw_spec( + deposit_header: HeaderView, + estimated_withdraw_header: Option, + withdraw_builder: F, +) -> CustomTxSpec +where + F: Fn(&mut CustomTxSpec) -> Withdraw, +{ + let mut spec = CustomTxSpec::default(); + + let withdraw = withdraw_builder(&mut spec); + let mut witnesses = vec![ + Bytes::new(), + Bytes::new(), + spec.inner + .pack_dao_operations(vec![], vec![withdraw], vec![]), + ]; + + let deposit_header_hash = deposit_header.hash(); + let deposit_block_number = pack_uint64(deposit_header.number()); + spec.inner.context.insert_header(deposit_header); + spec.inner.context.link_cell_with_block( + spec.inner.dao_input_out_point.clone(), + deposit_header_hash.clone(), + 0, + ); + let mut header_deps = vec![deposit_header_hash]; + if let Some(withdraw_header) = estimated_withdraw_header { + header_deps.push(withdraw_header.hash()); + spec.inner.context.insert_header(withdraw_header); + witnesses[0] = packed::WitnessArgs::new_builder() + .input_type(Some(Bytes::from(1u64.to_le_bytes().to_vec())).pack()) + .build() + .as_bytes(); + } + + 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: Bytes::from(DAO_DEPOSIT_DATA.to_vec()), + ..cell + }); + spec.on_new_output_spec(move |cell| CellSpec { + data: deposit_block_number.as_bytes(), + ..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; + + HeaderBuilder::default() + .number(number.pack()) + .epoch(EpochNumberWithFraction::new(epoch_number, index, epoch_length).pack()) +} diff --git a/contracts/__tests__/src/tests.rs b/contracts/__tests__/src/tests.rs index 2e03f0d..5196e65 100644 --- a/contracts/__tests__/src/tests.rs +++ b/contracts/__tests__/src/tests.rs @@ -2,7 +2,11 @@ // See https://github.com/xxuejie/ckb-native-build-sample/blob/main/tests/src/tests.rs for examples use crate::*; -use ckb_dao_cobuild_schemas::{Address, DaoActionData}; +use ckb_dao_cobuild_schemas::{ + Address, DaoActionData, DepositInfo, EstimatedWithdrawInfo, EstimatedWithdrawInfoOpt, OutPoint, + WithdrawInfo, +}; +use ckb_testtool::ckb_types::core::{EpochNumberWithFraction, HeaderBuilder}; const MAX_CYCLES: u64 = 10_000_000; @@ -116,3 +120,601 @@ fn test_dao_deposit_not_coverred() { let tx = build_tx(&mut spec); assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotCoverred, MAX_CYCLES); } + +#[test] +fn test_dao_withdraw_not_found() { + 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 tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotFound, MAX_CYCLES); +} + +#[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 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_withdraw_from_not_match() { + let mut spec = create_withdraw_spec(new_header_builder(1, 100).build(), None, |spec| { + Withdraw::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_withdraw_amount_not_match() { + let mut spec = create_withdraw_spec(new_header_builder(1, 100).build(), None, |spec| { + Withdraw::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_withdraw_to_not_match() { + let mut spec = create_withdraw_spec(new_header_builder(1, 100).build(), None, |spec| { + Withdraw::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_withdraw_deposit_block_number_not_match() { + let mut spec = create_withdraw_spec(new_header_builder(1, 100).build(), None, |spec| { + Withdraw::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() + .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_withdraw_deposit_timestamp_not_match() { + let mut spec = create_withdraw_spec( + new_header_builder(1, 100).timestamp(1.pack()).build(), + None, + |spec| { + Withdraw::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_withdraw_estimated_withdraw_header_not_found() { + let mut spec = create_withdraw_spec(new_header_builder(1, 100).build(), None, |spec| { + Withdraw::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(), + ) + .estimated_withdraw_info( + EstimatedWithdrawInfoOpt::new_builder() + .set(Some(EstimatedWithdrawInfo::new_builder().build())) + .build(), + ) + .build() + }); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_withdraw_estimated_withdraw_header_not_coverred() { + let mut spec = create_withdraw_spec( + new_header_builder(1, 100).build(), + Some(new_header_builder(2, 100).build()), + |spec| { + Withdraw::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_withdraw_without_estimated_withdraw_info() { + let mut spec = create_withdraw_spec(new_header_builder(1, 100).build(), None, |spec| { + Withdraw::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); + verify_and_dump_failed_tx(&spec.inner.context, &tx, MAX_CYCLES).expect("pass"); +} + +#[test] +fn test_dao_withdraw_larger_estimated_withdraw_block_number() { + let mut spec = create_withdraw_spec( + new_header_builder(1, 100).build(), + Some(new_header_builder(1, 100).build()), + |spec| { + Withdraw::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(), + ) + .estimated_withdraw_info( + EstimatedWithdrawInfoOpt::new_builder() + .set(Some(EstimatedWithdrawInfo::new_builder().build())) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_withdraw_estimated_withdraw_block_number_not_matched() { + let mut spec = create_withdraw_spec( + new_header_builder(1, 100).build(), + Some(new_header_builder(2, 100).build()), + |spec| { + Withdraw::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(), + ) + .estimated_withdraw_info( + EstimatedWithdrawInfoOpt::new_builder() + .set(Some(EstimatedWithdrawInfo::new_builder().build())) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_withdraw_estimated_withdraw_timestamp_not_matched() { + let mut spec = create_withdraw_spec( + new_header_builder(1, 100).build(), + Some(new_header_builder(2, 100).timestamp(1.pack()).build()), + |spec| { + Withdraw::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(), + ) + .estimated_withdraw_info( + EstimatedWithdrawInfoOpt::new_builder() + .set(Some( + EstimatedWithdrawInfo::new_builder() + .withdraw_info( + WithdrawInfo::new_builder() + .withdraw_block_number(pack_uint64(2)) + .build(), + ) + .build(), + )) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_withdraw_estimated_waiting_milliseconds_not_matched() { + let mut spec = create_withdraw_spec( + new_header_builder(1, 100).build(), + Some(new_header_builder(2, 100).build()), + |spec| { + Withdraw::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(), + ) + .estimated_withdraw_info( + EstimatedWithdrawInfoOpt::new_builder() + .set(Some( + EstimatedWithdrawInfo::new_builder() + .withdraw_info( + WithdrawInfo::new_builder() + .withdraw_block_number(pack_uint64(2)) + .build(), + ) + .build(), + )) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_withdraw_estimated_componsation_amount_not_matched() { + let mut spec = create_withdraw_spec( + new_header_builder(1, 100).dao(pack_ar(100)).build(), + Some(new_header_builder(2, 100).dao(pack_ar(110)).build()), + |spec| { + Withdraw::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(), + ) + .estimated_withdraw_info( + EstimatedWithdrawInfoOpt::new_builder() + .set(Some( + EstimatedWithdrawInfo::new_builder() + .waiting_milliseconds(pack_uint64( + DAO_CYCLE * ESITMATED_EPOCH_DURATION + - ESITMATED_EPOCH_DURATION / 100, + )) + .withdraw_info( + WithdrawInfo::new_builder() + .withdraw_block_number(pack_uint64(2)) + .build(), + ) + .build(), + )) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotMatched, MAX_CYCLES); +} + +#[test] +fn test_dao_withdraw_with_estimated_withdraw_info() { + let mut spec = create_withdraw_spec( + new_header_builder(1, 100).dao(pack_ar(100)).build(), + Some(new_header_builder(2, 100).dao(pack_ar(110)).build()), + |spec| { + Withdraw::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(), + ) + .estimated_withdraw_info( + EstimatedWithdrawInfoOpt::new_builder() + .set(Some( + EstimatedWithdrawInfo::new_builder() + .waiting_milliseconds(pack_uint64( + DAO_CYCLE * ESITMATED_EPOCH_DURATION + - ESITMATED_EPOCH_DURATION / 100, + )) + .withdraw_info( + WithdrawInfo::new_builder() + .withdraw_block_number(pack_uint64(2)) + .componsation_amount(pack_capacity( + (DEFAULT_CAPACITY - DAO_INPUT_OCCUPIED_CAPACITY) / 10, + )) + .build(), + ) + .build(), + )) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + verify_and_dump_failed_tx(&spec.inner.context, &tx, MAX_CYCLES).expect("pass"); +} + +fn case_withdraw_waiting_milliseconds( + from: EpochNumberWithFraction, + to: EpochNumberWithFraction, + expected: u64, +) { + let from_block_number = from.number() * from.length() + from.index(); + let to_block_number = to.number() * to.length() + to.index(); + + let mut spec = create_withdraw_spec( + HeaderBuilder::default() + .number(from_block_number.pack()) + .epoch(from.pack()) + .dao(pack_ar(100)) + .build(), + Some( + HeaderBuilder::default() + .number(to_block_number.pack()) + .epoch(to.pack()) + .dao(pack_ar(110)) + .build(), + ), + |spec| { + Withdraw::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(from_block_number)) + .amount(pack_capacity(DEFAULT_CAPACITY)) + .build(), + ) + .estimated_withdraw_info( + EstimatedWithdrawInfoOpt::new_builder() + .set(Some( + EstimatedWithdrawInfo::new_builder() + .waiting_milliseconds(pack_uint64(expected)) + .withdraw_info( + WithdrawInfo::new_builder() + .withdraw_block_number(pack_uint64(to_block_number)) + .componsation_amount(pack_capacity( + (DEFAULT_CAPACITY - DAO_INPUT_OCCUPIED_CAPACITY) / 10, + )) + .build(), + ) + .build(), + )) + .build(), + ) + .build() + }, + ); + + let tx = build_tx(&mut spec); + verify_and_dump_failed_tx(&spec.inner.context, &tx, MAX_CYCLES).expect("pass"); +} + +#[test] +fn test_withdraw_waiting_milliseconds_zero() { + case_withdraw_waiting_milliseconds( + EpochNumberWithFraction::new(0, 5, 10), + EpochNumberWithFraction::new(180, 5, 10), + 0, + ); +} + +#[test] +fn test_withdraw_waiting_milliseconds_boundary_cases() { + case_withdraw_waiting_milliseconds( + EpochNumberWithFraction::new(0, 5, 10), + EpochNumberWithFraction::new(179, 5, 10), + ESITMATED_EPOCH_DURATION, + ); + case_withdraw_waiting_milliseconds( + EpochNumberWithFraction::new(0, 5, 10), + EpochNumberWithFraction::new(179, 9, 10), + ESITMATED_EPOCH_DURATION * 6 / 10, + ); + case_withdraw_waiting_milliseconds( + EpochNumberWithFraction::new(0, 5, 10), + EpochNumberWithFraction::new(180, 4, 10), + ESITMATED_EPOCH_DURATION / 10, + ); + case_withdraw_waiting_milliseconds( + EpochNumberWithFraction::new(0, 5, 10), + EpochNumberWithFraction::new(180, 6, 10), + DAO_CYCLE * ESITMATED_EPOCH_DURATION - ESITMATED_EPOCH_DURATION / 10, + ); +} 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 6d96eee..ec5f267 100644 --- a/contracts/dao-action-verifier/src/derived_dao_action_data.rs +++ b/contracts/dao-action-verifier/src/derived_dao_action_data.rs @@ -2,11 +2,16 @@ use core::cmp; use alloc::collections::BTreeMap; -use ckb_dao_cobuild_schemas::{DaoActionData, Deposit}; +use ckb_dao_cobuild_schemas::{DaoActionData, Deposit, OutPoint, Withdraw}; use ckb_std::{ ckb_constants::Source, ckb_types::{bytes::Bytes, packed, prelude::*}, - high_level::{load_cell, load_cell_data, QueryIter}, + error::SysError, + high_level::{ + load_cell, load_cell_data, load_cell_occupied_capacity, load_header, load_input_out_point, + load_witness_args, QueryIter, + }, + since::EpochNumberWithFraction, }; use crate::{ @@ -22,9 +27,21 @@ pub struct DepositKey { amount: Bytes, } +#[derive(Debug, Eq, PartialEq)] +pub struct WithdrawKey { + cell_pointer: Bytes, +} + +#[derive(Debug)] +pub struct WithdrawValue { + index: usize, + input_cell_output: packed::CellOutput, +} + #[derive(Default, Debug)] pub struct DerivedDaoActionData { deposits: BTreeMap, + withdraws: BTreeMap, } fn is_deposit_cell(index: usize, source: Source) -> bool { @@ -33,6 +50,81 @@ fn is_deposit_cell(index: usize, source: Source) -> bool { .unwrap_or(false) } +fn load_header_from_witness(witness: &[u8]) -> Result { + if witness.len() != 8 { + return Err(trace_error!( + ErrorCode::InvalidHeaderDepIndex, + "expect witness len 8, got {}", + witness.len() + )); + } + + let mut index_buf = [0u8; 8]; + index_buf.copy_from_slice(witness); + let index = u64::from_le_bytes(index_buf); + + load_header(index as usize, Source::HeaderDep) + .map(|h| h.raw()) + .map_err(|err| { + trace_error!( + ErrorCode::InvalidHeaderDepIndex, + "failed to load header dep at {}: {:?}", + index, + err + ) + }) +} + +fn decode_ar(dao: &[u8]) -> u64 { + let mut ar_buf = [0u8; 8]; + ar_buf.copy_from_slice(&dao[8..16]); + u64::from_le_bytes(ar_buf) +} + +const DAO_CYCLE: u64 = 180; +// 4 hours +const ESITMATED_EPOCH_DURATION: u64 = 4 * 60 * 60 * 1000; + +// Assume that one epoch lasts 4 hours. +// +// The fraction part are converted to milliseconds by truncating the decimal digits. +fn compute_waiting_milliseconds( + deposit_header: &packed::RawHeader, + withdraw_header: &packed::RawHeader, +) -> packed::Uint64 { + let from_epoch = EpochNumberWithFraction::from_full_value(deposit_header.epoch().unpack()); + let to_epoch = EpochNumberWithFraction::from_full_value(withdraw_header.epoch().unpack()); + + let from_epoch_passed_duration = + from_epoch.index() * ESITMATED_EPOCH_DURATION / from_epoch.length(); + let to_epoch_passed_duration = to_epoch.index() * ESITMATED_EPOCH_DURATION / to_epoch.length(); + + // find next cycle + let remaining_epochs_draft = DAO_CYCLE - (to_epoch.number() - from_epoch.number()) % DAO_CYCLE; + let remaining_epochs = if remaining_epochs_draft == DAO_CYCLE + && from_epoch_passed_duration >= to_epoch_passed_duration + { + 0 + } else { + remaining_epochs_draft + }; + + (remaining_epochs * ESITMATED_EPOCH_DURATION + from_epoch_passed_duration + - to_epoch_passed_duration) + .pack() +} + +fn compute_componsation_amount( + counted_capacity: u64, + deposit_header: &packed::RawHeader, + withdraw_header: &packed::RawHeader, +) -> packed::Uint64 { + let deposit_ar = decode_ar(deposit_header.dao().as_slice()) as u128; + let withdraw_ar = decode_ar(withdraw_header.dao().as_slice()) as u128; + + (((counted_capacity as u128) * withdraw_ar / deposit_ar) as u64 - counted_capacity).pack() +} + impl DerivedDaoActionData { /// Derive dao action from the tx. pub fn derive() -> Self { @@ -46,25 +138,59 @@ impl DerivedDaoActionData { .map(|(index, cell_output)| ((&cell_output).into(), index)) .collect(); - Self { deposits } + #[allow(clippy::mutable_key_type)] + let withdraws = QueryIter::new(load_cell, Source::Input) + .enumerate() + .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)| { + ( + load_input_out_point(index, Source::Input) + .expect("load input out_point") + .into(), + WithdrawValue { + index, + input_cell_output, + }, + ) + }) + .collect(); + + Self { + deposits, + withdraws, + } } /// Ensure all derived operations have been found in tx. pub fn complete(self) -> Result<(), Error> { - match self.deposits.into_values().next() { - Some(index) => Err(trace_error!( + if let Some(index) = self.deposits.into_values().next() { + return Err(trace_error!( ErrorCode::NotCoverred, - "tx output {} not coverred by dao action", + "deposit at output {} not coverred by dao action", index - )), - None => Ok(()), + )); } + if let Some(value) = self.withdraws.into_values().next() { + return Err(trace_error!( + ErrorCode::NotCoverred, + "withdraw at input {} not coverred by dao action", + value.index + )); + } + + Ok(()) } pub fn verify(&mut self, dao_action_data: DaoActionData) -> Result<(), Error> { for deposit in dao_action_data.deposits().into_iter() { self.verify_deposit(deposit)?; } + for withdraw in dao_action_data.withdraws().into_iter() { + self.verify_withdraw(withdraw)?; + } Ok(()) } @@ -79,6 +205,166 @@ impl DerivedDaoActionData { )), } } + + fn verify_withdraw(&mut self, withdraw: Withdraw) -> Result<(), Error> { + match self.withdraws.remove(&((&withdraw.cell_pointer()).into())) { + Some(WithdrawValue { + index, + input_cell_output, + }) => { + if input_cell_output.lock().as_slice() != withdraw.from().as_slice() { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect withdraw from {}, got {}", + withdraw.from(), + input_cell_output.lock() + )); + } + + let deposit_info = withdraw.deposit_info(); + if input_cell_output.capacity().as_slice() != deposit_info.amount().as_slice() { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect withdraw amount {}, got {}", + deposit_info.amount(), + input_cell_output.capacity() + )); + } + + let output_cell_output = load_cell(index, Source::Output)?; + if output_cell_output.lock().as_slice() != withdraw.to().as_slice() { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect withdraw to {}, got {}", + withdraw.to(), + output_cell_output.lock() + )); + } + + let deposit_header = load_header(index, Source::Input)?.raw(); + 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 witness_opt = match load_witness_args(index, Source::Input) { + Ok(witness_args) => witness_args.input_type().to_opt(), + Err(SysError::Encoding) => None, + Err(err) => { + return Err(trace_error!( + err, + "failed to load witness args at {}", + index + )) + } + }; + match (withdraw.estimated_withdraw_info().to_opt(), witness_opt) { + (Some(estimated_withdraw_info), Some(witness)) => { + let estimated_withdraw_header = + load_header_from_witness(&witness.raw_data())?; + + let withdraw_info = estimated_withdraw_info.withdraw_info(); + if estimated_withdraw_header.number().unpack() + <= deposit_header.number().unpack() + { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect estimated withdraw block number larger than {}, got {}", + deposit_header.number(), + estimated_withdraw_header.number() + )); + } + if estimated_withdraw_header.number().as_slice() + != withdraw_info.withdraw_block_number().as_slice() + { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect estimated withdraw block number {}, got {}", + withdraw_info.withdraw_block_number(), + estimated_withdraw_header.number() + )); + } + if estimated_withdraw_header.timestamp().as_slice() + != withdraw_info.withdraw_timestamp().as_slice() + { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect estimated withdraw block timestamp {}, got {}", + withdraw_info.withdraw_timestamp(), + estimated_withdraw_header.timestamp() + )); + } + + let actual_waiting_milliseconds = compute_waiting_milliseconds( + &deposit_header, + &estimated_withdraw_header, + ); + if actual_waiting_milliseconds.as_slice() + != estimated_withdraw_info.waiting_milliseconds().as_slice() + { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect estimated withdraw waiting milliseconds {}, got {}", + estimated_withdraw_info.waiting_milliseconds(), + actual_waiting_milliseconds + )); + } + let counted_capacity = output_cell_output.capacity().unpack() + - load_cell_occupied_capacity(index, Source::Output)?; + let actual_componsation_amount = compute_componsation_amount( + counted_capacity, + &deposit_header, + &estimated_withdraw_header, + ); + if actual_componsation_amount.as_slice() + != withdraw_info.componsation_amount().as_slice() + { + return Err(trace_error!( + ErrorCode::NotMatched, + "expect estimated withdraw componsation amount {}, got {}", + withdraw_info.componsation_amount(), + actual_componsation_amount + )); + } + + Ok(()) + } + (None, None) => Ok(()), + (Some(withdraw_info), None) => Err(trace_error!( + ErrorCode::NotMatched, + "expect estimated withdraw info {}, got no witness", + withdraw_info + )), + (None, Some(witness)) => Err(trace_error!( + ErrorCode::NotMatched, + "expect no estimated withdraw info, got witness {}", + witness + )), + } + } + None => Err(trace_error!( + ErrorCode::NotFound, + "withdraw not found in tx: {}", + withdraw + )), + } + } } impl From<&packed::CellOutput> for DepositKey { @@ -101,10 +387,7 @@ impl From<&Deposit> for DepositKey { impl PartialOrd for DepositKey { fn partial_cmp(&self, other: &Self) -> Option { - match self.to.partial_cmp(&other.to) { - Some(cmp::Ordering::Equal) => self.amount.partial_cmp(&other.amount), - other => other, - } + Some(self.cmp(other)) } } @@ -116,3 +399,31 @@ impl Ord for DepositKey { } } } + +impl From for WithdrawKey { + fn from(value: packed::OutPoint) -> Self { + Self { + cell_pointer: value.as_bytes(), + } + } +} + +impl From<&OutPoint> for WithdrawKey { + fn from(value: &OutPoint) -> Self { + Self { + cell_pointer: value.as_bytes(), + } + } +} + +impl PartialOrd for WithdrawKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for WithdrawKey { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.cell_pointer.cmp(&other.cell_pointer) + } +} diff --git a/contracts/dao-action-verifier/src/error_code.rs b/contracts/dao-action-verifier/src/error_code.rs index 8c4fb1a..f80f7b7 100644 --- a/contracts/dao-action-verifier/src/error_code.rs +++ b/contracts/dao-action-verifier/src/error_code.rs @@ -13,4 +13,6 @@ pub enum ErrorCode { InvalidActionDataSchema = 65, NotCoverred = 66, NotFound = 67, + NotMatched = 68, + InvalidHeaderDepIndex = 69, }