From c4bcbab325b2b7af283229d6d490511ede7c20ea Mon Sep 17 00:00:00 2001 From: ian Date: Fri, 2 Feb 2024 14:23:27 +0800 Subject: [PATCH] :sparkles: Verify deposit from and claim to --- Makefile | 7 +- contracts/dao-action-verifier/src/dao.rs | 9 ++ .../src/derived_ckb_accounting.rs | 119 ++++++++++++++++++ .../src/derived_dao_action_data.rs | 93 ++------------ .../dao-action-verifier/src/error_code.rs | 2 + contracts/dao-action-verifier/src/keys.rs | 110 ++++++++++++++++ contracts/dao-action-verifier/src/main.rs | 17 ++- 7 files changed, 265 insertions(+), 92 deletions(-) create mode 100644 contracts/dao-action-verifier/src/dao.rs create mode 100644 contracts/dao-action-verifier/src/derived_ckb_accounting.rs create mode 100644 contracts/dao-action-verifier/src/keys.rs diff --git a/Makefile b/Makefile index 5b767b7..ccb8f10 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,11 @@ contract: schemas make -f contracts.mk build make CARGO_ARGS="${CARGO_TEST_ARGS}" -f contracts.mk test +contract-linters: + make -f contracts.mk fmt + make -f contracts.mk check + make -f contracts.mk clippy + SCHEMA_MOL_FILES := $(wildcard schemas/*.mol) SCHEMA_RUST_FILES := $(patsubst %.mol,crates/ckb-dao-cobuild-schemas/src/%.rs,$(SCHEMA_MOL_FILES)) crates/ckb-dao-cobuild-schemas/src/%.rs: %.mol @@ -20,4 +25,4 @@ schemas: $(SCHEMA_RUST_FILES) clean-schemas: rm -f $(SCHEMA_RUST_FILES) -.PHONY: all web contract schemas clean-schemas +.PHONY: all web contract contract-linters schemas clean-schemas diff --git a/contracts/dao-action-verifier/src/dao.rs b/contracts/dao-action-verifier/src/dao.rs new file mode 100644 index 0000000..3ba506a --- /dev/null +++ b/contracts/dao-action-verifier/src/dao.rs @@ -0,0 +1,9 @@ +use ckb_std::{ckb_constants::Source, high_level::load_cell_data}; + +use crate::constants::DAO_DEPOSIT_DATA; + +pub fn is_deposit_cell(index: usize, source: Source) -> bool { + load_cell_data(index, source) + .map(|data| data.as_ref() == DAO_DEPOSIT_DATA) + .unwrap_or(false) +} diff --git a/contracts/dao-action-verifier/src/derived_ckb_accounting.rs b/contracts/dao-action-verifier/src/derived_ckb_accounting.rs new file mode 100644 index 0000000..9c30682 --- /dev/null +++ b/contracts/dao-action-verifier/src/derived_ckb_accounting.rs @@ -0,0 +1,119 @@ +use alloc::collections::BTreeMap; +use ckb_dao_cobuild_schemas::{Capacity, DaoActionData}; +use ckb_std::{ + ckb_constants::Source, + ckb_types::prelude::*, + high_level::{load_cell, load_input_out_point, QueryIter}, +}; + +use crate::{ + constants::DAO_TYPE_SCRIPT, + dao::is_deposit_cell, + error::Error, + error_code::ErrorCode, + keys::{AccountingKey, CellPointerKey}, + trace_error, +}; + +#[derive(Default, Debug)] +pub struct DerivedCkbAccounting { + inputs: BTreeMap, + outputs: BTreeMap, + // cache componsations for deposit from + componsations: BTreeMap, +} + +fn unpack_capacity(capacity: Capacity) -> u64 { + let mut buf = [0u8; 8]; + buf.copy_from_slice(capacity.as_slice()); + u64::from_le_bytes(buf) +} + +#[allow(clippy::mutable_key_type)] +fn tally_input(key: &AccountingKey, componsations: &BTreeMap) -> u128 { + // Exclude deposit cells, which must go to the corresponding withdraw cell. + QueryIter::new(load_cell, Source::Input) + .enumerate() + .filter(|(index, cell_output)| { + cell_output.lock().as_slice() == key.as_slice() + && !(cell_output.type_().as_slice() == DAO_TYPE_SCRIPT + && is_deposit_cell(*index, Source::Input)) + }) + .fold(0u128, |acc, (index, cell_output)| { + let componsation = if cell_output.type_().as_slice() == DAO_TYPE_SCRIPT { + let key = load_input_out_point(index, Source::Input) + .expect("load input") + .into(); + componsations + .get(&key) + .copied() + .expect("componsation exists") + } else { + 0 + }; + acc + cell_output.capacity().unpack() as u128 + componsation as u128 + }) +} + +fn tally_output(key: &AccountingKey) -> u128 { + // Exclude withdraw cells, which must come from the corresponding deposit cell. + QueryIter::new(load_cell, Source::Output) + .enumerate() + .filter(|(index, cell_output)| { + cell_output.lock().as_slice() == key.as_slice() + && (cell_output.type_().as_slice() != DAO_TYPE_SCRIPT + || is_deposit_cell(*index, Source::Output)) + }) + .fold(0u128, |acc, (_index, cell_output)| { + acc + cell_output.capacity().unpack() as u128 + }) +} + +impl DerivedCkbAccounting { + pub fn derive(&mut self, data: &DaoActionData) { + for deposit in data.deposits().into_iter() { + let total = self.inputs.entry(deposit.from().into()).or_default(); + *total += unpack_capacity(deposit.amount()) as u128; + } + for claim in data.claims().into_iter() { + let componsation = unpack_capacity(claim.withdraw_info().componsation_amount()); + + let total = self.outputs.entry(claim.to().into()).or_default(); + *total += unpack_capacity(claim.deposit_info().amount()) as u128 + componsation as u128; + + self.componsations + .insert((&claim.cell_pointer()).into(), componsation); + } + } + + pub fn verify(self) -> Result<(), Error> { + #[allow(clippy::mutable_key_type)] + let componsations = self.componsations; + for (key, expected) in self.inputs.into_iter() { + let actual = tally_input(&key, &componsations); + if actual < expected { + return Err(trace_error!( + ErrorCode::InsufficientDepositFrom, + "expect deposit {} from {:?}, got {}", + expected, + key, + actual + )); + } + } + for (key, expected) in self.outputs.into_iter() { + let actual = tally_output(&key); + if actual < expected { + return Err(trace_error!( + ErrorCode::InsufficientClaimTo, + "expect claim {} to {:?}, got {}", + expected, + key, + actual + )); + } + } + + Ok(()) + } +} 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 784f091..2c33745 100644 --- a/contracts/dao-action-verifier/src/derived_dao_action_data.rs +++ b/contracts/dao-action-verifier/src/derived_dao_action_data.rs @@ -1,44 +1,32 @@ //! Derive dao action from tx. -use core::cmp; - use alloc::collections::BTreeMap; -use ckb_dao_cobuild_schemas::{Claim, DaoActionData, Deposit, OutPoint, Withdraw}; +use ckb_dao_cobuild_schemas::{Claim, DaoActionData, Deposit, Withdraw}; use ckb_std::{ ckb_constants::Source, - ckb_types::{bytes::Bytes, packed, prelude::*}, + ckb_types::{packed, prelude::*}, error::SysError, high_level::{ - load_cell, load_cell_data, load_cell_occupied_capacity, load_header, load_input_out_point, + load_cell, load_cell_occupied_capacity, load_header, load_input_out_point, load_witness_args, QueryIter, }, since::EpochNumberWithFraction, }; use crate::{ - constants::{DAO_DEPOSIT_DATA, DAO_TYPE_SCRIPT}, + constants::DAO_TYPE_SCRIPT, + dao::is_deposit_cell, error::Error, error_code::ErrorCode, + keys::{ClaimKey, DepositKey, WithdrawKey}, trace_error, }; -#[derive(Debug, Eq, PartialEq)] -pub struct DepositKey { - to: Bytes, - amount: Bytes, -} - -#[derive(Debug, Eq, PartialEq)] -pub struct WithdrawKey { - cell_pointer: Bytes, -} - #[derive(Debug)] pub struct WithdrawValue { index: usize, input_cell_output: packed::CellOutput, } -pub type ClaimKey = WithdrawKey; pub type ClaimValue = WithdrawValue; #[derive(Default, Debug)] @@ -48,12 +36,6 @@ pub struct DerivedDaoActionData { claims: BTreeMap, } -fn is_deposit_cell(index: usize, source: Source) -> bool { - load_cell_data(index, source) - .map(|data| data.as_ref() == DAO_DEPOSIT_DATA) - .unwrap_or(false) -} - fn load_header_from_witness(witness: &[u8]) -> Result { if witness.len() != 8 { return Err(trace_error!( @@ -195,7 +177,7 @@ impl DerivedDaoActionData { Ok(()) } - pub fn verify(&mut self, dao_action_data: DaoActionData) -> Result<(), Error> { + pub fn verify(&mut self, dao_action_data: &DaoActionData) -> Result<(), Error> { for deposit in dao_action_data.deposits().into_iter() { self.verify_deposit(deposit)?; } @@ -502,64 +484,3 @@ impl DerivedDaoActionData { } } } - -impl From<&packed::CellOutput> for DepositKey { - fn from(value: &packed::CellOutput) -> Self { - Self { - to: value.lock().as_bytes(), - amount: value.capacity().as_bytes(), - } - } -} - -impl From<&Deposit> for DepositKey { - fn from(value: &Deposit) -> Self { - Self { - to: value.to().as_bytes(), - amount: value.amount().as_bytes(), - } - } -} - -impl PartialOrd for DepositKey { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for DepositKey { - fn cmp(&self, other: &Self) -> cmp::Ordering { - match self.to.cmp(&other.to) { - cmp::Ordering::Equal => self.amount.cmp(&other.amount), - other => other, - } - } -} - -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 f80f7b7..a6f3487 100644 --- a/contracts/dao-action-verifier/src/error_code.rs +++ b/contracts/dao-action-verifier/src/error_code.rs @@ -15,4 +15,6 @@ pub enum ErrorCode { NotFound = 67, NotMatched = 68, InvalidHeaderDepIndex = 69, + InsufficientDepositFrom = 70, + InsufficientClaimTo = 71, } diff --git a/contracts/dao-action-verifier/src/keys.rs b/contracts/dao-action-verifier/src/keys.rs new file mode 100644 index 0000000..683036b --- /dev/null +++ b/contracts/dao-action-verifier/src/keys.rs @@ -0,0 +1,110 @@ +use core::cmp; + +use ckb_dao_cobuild_schemas::{Address, Deposit, OutPoint as DaoActionOutPoint}; +use ckb_std::ckb_types::{bytes::Bytes, packed, prelude::*}; + +#[derive(Debug, Eq, PartialEq)] +pub struct DepositKey { + to: Bytes, + amount: Bytes, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct CellPointerKey { + cell_pointer: Bytes, +} + +pub type WithdrawKey = CellPointerKey; +pub type ClaimKey = CellPointerKey; + +#[derive(Debug, Eq, PartialEq)] +pub struct AccountingKey { + address: Bytes, +} + +impl AccountingKey { + pub fn as_slice(&self) -> &[u8] { + self.address.as_ref() + } +} + +impl From<&packed::CellOutput> for DepositKey { + fn from(value: &packed::CellOutput) -> Self { + Self { + to: value.lock().as_bytes(), + amount: value.capacity().as_bytes(), + } + } +} + +impl From<&Deposit> for DepositKey { + fn from(value: &Deposit) -> Self { + Self { + to: value.to().as_bytes(), + amount: value.amount().as_bytes(), + } + } +} + +impl PartialOrd for DepositKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for DepositKey { + fn cmp(&self, other: &Self) -> cmp::Ordering { + match self.to.cmp(&other.to) { + cmp::Ordering::Equal => self.amount.cmp(&other.amount), + other => other, + } + } +} + +impl From for CellPointerKey { + fn from(value: packed::OutPoint) -> Self { + Self { + cell_pointer: value.as_bytes(), + } + } +} + +impl From<&DaoActionOutPoint> for CellPointerKey { + fn from(value: &DaoActionOutPoint) -> Self { + Self { + cell_pointer: value.as_bytes(), + } + } +} + +impl PartialOrd for CellPointerKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for CellPointerKey { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.cell_pointer.cmp(&other.cell_pointer) + } +} + +impl From
for AccountingKey { + fn from(value: Address) -> Self { + Self { + address: value.as_bytes(), + } + } +} + +impl PartialOrd for AccountingKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for AccountingKey { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.address.cmp(&other.address) + } +} diff --git a/contracts/dao-action-verifier/src/main.rs b/contracts/dao-action-verifier/src/main.rs index fc5e897..4207c09 100644 --- a/contracts/dao-action-verifier/src/main.rs +++ b/contracts/dao-action-verifier/src/main.rs @@ -16,12 +16,16 @@ use ckb_std::ckb_types::{bytes::Bytes, prelude::*}; use ckb_transaction_cobuild::fetch_message; mod constants; +mod dao; +mod derived_ckb_accounting; mod derived_dao_action_data; mod error; mod error_code; +mod keys; use crate::{ - constants::DAO_SCRIPT_HASH, derived_dao_action_data::DerivedDaoActionData, error::Error, + constants::DAO_SCRIPT_HASH, derived_ckb_accounting::DerivedCkbAccounting, + derived_dao_action_data::DerivedDaoActionData, error::Error, }; pub fn program_entry() -> i8 { @@ -34,18 +38,21 @@ pub fn program_entry() -> i8 { fn verify() -> Result<(), Error> { if let Ok(Some(message)) = fetch_message() { let mut derived_dao_action_data = DerivedDaoActionData::derive(); + let mut derived_ckb_accounting = DerivedCkbAccounting::default(); for action in message.actions().into_iter() { if action.script_hash().as_slice() == DAO_SCRIPT_HASH { let dao_action_data = decode_dao_action_data(action.data().raw_data())?; - derived_dao_action_data.verify(dao_action_data)?; + derived_dao_action_data.verify(&dao_action_data)?; + derived_ckb_accounting.derive(&dao_action_data); } } - derived_dao_action_data.complete() - } else { - Ok(()) + derived_dao_action_data.complete()?; + derived_ckb_accounting.verify()?; } + + Ok(()) } fn decode_dao_action_data(data: Bytes) -> Result {