From d50b1f878307581b1b927679cd6d0279e0092083 Mon Sep 17 00:00:00 2001 From: ian Date: Thu, 1 Feb 2024 10:51:29 +0800 Subject: [PATCH] :sparkles: Verify deposit to --- contracts/__tests__/src/tests.rs | 55 +++++++- .../dao-action-verifier/src/constants.rs | 13 ++ .../src/derived_dao_action_data.rs | 118 ++++++++++++++++++ contracts/dao-action-verifier/src/error.rs | 14 +++ .../dao-action-verifier/src/error_code.rs | 5 +- contracts/dao-action-verifier/src/main.rs | 27 ++-- 6 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 contracts/dao-action-verifier/src/derived_dao_action_data.rs diff --git a/contracts/__tests__/src/tests.rs b/contracts/__tests__/src/tests.rs index 1dae3b5..2e03f0d 100644 --- a/contracts/__tests__/src/tests.rs +++ b/contracts/__tests__/src/tests.rs @@ -2,7 +2,7 @@ // See https://github.com/xxuejie/ckb-native-build-sample/blob/main/tests/src/tests.rs for examples use crate::*; -use ckb_dao_cobuild_schemas::DaoActionData; +use ckb_dao_cobuild_schemas::{Address, DaoActionData}; const MAX_CYCLES: u64 = 10_000_000; @@ -63,3 +63,56 @@ fn test_multiple_dao_actions() { let tx = build_tx(&mut spec); verify_and_dump_failed_tx(&spec.inner.context, &tx, MAX_CYCLES).expect("pass"); } + +#[test] +fn test_dao_deposit_single() { + let mut spec = CustomTxSpec::default(); + let witness = spec.inner.pack_dao_operations( + vec![Deposit::new_builder() + .from(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .to(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .amount(pack_capacity(DEFAULT_CAPACITY)) + .build()], + vec![], + vec![], + ); + spec.on_new_tx_builder(move |b| b.witness(witness.clone().pack())); + + let tx = build_tx(&mut spec); + verify_and_dump_failed_tx(&spec.inner.context, &tx, MAX_CYCLES).expect("pass"); +} + +#[test] +fn test_dao_deposit_not_found() { + let mut spec = CustomTxSpec::default(); + let witness = spec.inner.pack_dao_operations( + vec![Deposit::new_builder() + .from(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .to(Address::new_unchecked( + spec.inner.alice_lock_script.as_bytes(), + )) + .build()], + vec![], + vec![], + ); + spec.on_new_tx_builder(move |b| b.witness(witness.clone().pack())); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotFound, MAX_CYCLES); +} + +#[test] +fn test_dao_deposit_not_coverred() { + let mut spec = CustomTxSpec::default(); + let witness = spec.inner.pack_dao_operations(vec![], vec![], vec![]); + spec.on_new_tx_builder(move |b| b.witness(witness.clone().pack())); + + let tx = build_tx(&mut spec); + assert_tx_error(&spec.inner.context, &tx, ErrorCode::NotCoverred, MAX_CYCLES); +} diff --git a/contracts/dao-action-verifier/src/constants.rs b/contracts/dao-action-verifier/src/constants.rs index 1aa8b48..695e501 100644 --- a/contracts/dao-action-verifier/src/constants.rs +++ b/contracts/dao-action-verifier/src/constants.rs @@ -2,3 +2,16 @@ pub const DAO_SCRIPT_HASH: [u8; 32] = [ 204, 119, 196, 222, 172, 5, 214, 138, 181, 178, 104, 40, 240, 191, 69, 101, 168, 215, 49, 19, 215, 187, 126, 146, 184, 54, 43, 138, 116, 229, 142, 88, ]; + +// molecule encoding of the dao type script +// - code_hash: "0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e" +// - hash_type: "type" +// - args: "0x" +pub const DAO_TYPE_SCRIPT: [u8; 53] = [ + 0x35, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x82, 0xd7, 0x6d, 0x1b, 0x75, 0xfe, 0x2f, 0xd9, 0xa2, 0x7d, 0xfb, 0xaa, 0x65, 0xa0, 0x39, 0x22, + 0x1a, 0x38, 0x0d, 0x76, 0xc9, 0x26, 0xf3, 0x78, 0xd3, 0xf8, 0x1c, 0xf3, 0xe7, 0xe1, 0x3f, 0x2e, + 0x01, 0x00, 0x00, 0x00, 0x00, +]; + +pub const DAO_DEPOSIT_DATA: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 0]; diff --git a/contracts/dao-action-verifier/src/derived_dao_action_data.rs b/contracts/dao-action-verifier/src/derived_dao_action_data.rs new file mode 100644 index 0000000..6d96eee --- /dev/null +++ b/contracts/dao-action-verifier/src/derived_dao_action_data.rs @@ -0,0 +1,118 @@ +//! Derive dao action from tx. +use core::cmp; + +use alloc::collections::BTreeMap; +use ckb_dao_cobuild_schemas::{DaoActionData, Deposit}; +use ckb_std::{ + ckb_constants::Source, + ckb_types::{bytes::Bytes, packed, prelude::*}, + high_level::{load_cell, load_cell_data, QueryIter}, +}; + +use crate::{ + constants::{DAO_DEPOSIT_DATA, DAO_TYPE_SCRIPT}, + error::Error, + error_code::ErrorCode, + trace_error, +}; + +#[derive(Debug, Eq, PartialEq)] +pub struct DepositKey { + to: Bytes, + amount: Bytes, +} + +#[derive(Default, Debug)] +pub struct DerivedDaoActionData { + deposits: 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) +} + +impl DerivedDaoActionData { + /// Derive dao action from the tx. + pub fn derive() -> Self { + #[allow(clippy::mutable_key_type)] + let deposits = QueryIter::new(load_cell, Source::Output) + .enumerate() + .filter(|(index, cell_output)| { + cell_output.type_().as_slice() == DAO_TYPE_SCRIPT + && is_deposit_cell(*index, Source::Output) + }) + .map(|(index, cell_output)| ((&cell_output).into(), index)) + .collect(); + + Self { deposits } + } + + /// 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!( + ErrorCode::NotCoverred, + "tx output {} not coverred by dao action", + index + )), + None => 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)?; + } + + Ok(()) + } + + fn verify_deposit(&mut self, deposit: Deposit) -> Result<(), Error> { + match self.deposits.remove(&(&deposit).into()) { + Some(_) => Ok(()), + None => Err(trace_error!( + ErrorCode::NotFound, + "deposit not found in tx: {}", + deposit + )), + } + } +} + +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 { + match self.to.partial_cmp(&other.to) { + Some(cmp::Ordering::Equal) => self.amount.partial_cmp(&other.amount), + other => 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, + } + } +} diff --git a/contracts/dao-action-verifier/src/error.rs b/contracts/dao-action-verifier/src/error.rs index bcfd9e4..1d490f8 100644 --- a/contracts/dao-action-verifier/src/error.rs +++ b/contracts/dao-action-verifier/src/error.rs @@ -5,6 +5,7 @@ use ckb_std::error::SysError; pub enum Error { CkbStd(SysError), InvalidActionDataSchema(molecule::error::VerificationError), + Custom(ErrorCode), } impl From for ErrorCode { @@ -18,6 +19,7 @@ impl From for ErrorCode { SysError::Unknown(_) => ErrorCode::Unknown, }, Error::InvalidActionDataSchema(_) => ErrorCode::InvalidActionDataSchema, + Error::Custom(code) => code, } } } @@ -40,6 +42,12 @@ impl From for Error { } } +impl From for Error { + fn from(code: ErrorCode) -> Self { + Error::Custom(code) + } +} + #[macro_export] macro_rules! trace_error { ($err:expr) => {{ @@ -54,4 +62,10 @@ macro_rules! trace_error { ckb_std::debug!("{}:{} {:?} {}", file!(), line!(), err, $message); err }}; + ($err:expr, $format:literal, $($args:expr),+) => {{ + let err = $crate::error::Error::from($err); + #[cfg(debug_assertions)] + ckb_std::debug!("{}:{} {:?} {}", file!(), line!(), err, core::format_args!($format, $($args), +)); + err + }}; } diff --git a/contracts/dao-action-verifier/src/error_code.rs b/contracts/dao-action-verifier/src/error_code.rs index 1de787e..8c4fb1a 100644 --- a/contracts/dao-action-verifier/src/error_code.rs +++ b/contracts/dao-action-verifier/src/error_code.rs @@ -9,5 +9,8 @@ pub enum ErrorCode { Unknown = 20, // custom errors - InvalidActionDataSchema = 64, + DuplicatedActionData = 64, + InvalidActionDataSchema = 65, + NotCoverred = 66, + NotFound = 67, } diff --git a/contracts/dao-action-verifier/src/main.rs b/contracts/dao-action-verifier/src/main.rs index 2c23164..fc5e897 100644 --- a/contracts/dao-action-verifier/src/main.rs +++ b/contracts/dao-action-verifier/src/main.rs @@ -11,15 +11,18 @@ ckb_std::entry!(program_entry); #[cfg(all(target_arch = "riscv64", not(test)))] default_alloc!(); -use ckb_dao_cobuild_schemas::DaoActionDataReader; -use ckb_std::ckb_types::prelude::*; +use ckb_dao_cobuild_schemas::{DaoActionData, DaoActionDataReader}; +use ckb_std::ckb_types::{bytes::Bytes, prelude::*}; use ckb_transaction_cobuild::fetch_message; mod constants; +mod derived_dao_action_data; mod error; mod error_code; -use crate::{constants::DAO_SCRIPT_HASH, error::Error}; +use crate::{ + constants::DAO_SCRIPT_HASH, derived_dao_action_data::DerivedDaoActionData, error::Error, +}; pub fn program_entry() -> i8 { match verify() { @@ -30,18 +33,22 @@ pub fn program_entry() -> i8 { fn verify() -> Result<(), Error> { if let Ok(Some(message)) = fetch_message() { + let mut derived_dao_action_data = DerivedDaoActionData::derive(); + for action in message.actions().into_iter() { if action.script_hash().as_slice() == DAO_SCRIPT_HASH { - verify_action_data(&action.data().raw_data())?; + let dao_action_data = decode_dao_action_data(action.data().raw_data())?; + derived_dao_action_data.verify(dao_action_data)?; } } - } - // It's OK to not include DAO action data - Ok(()) + derived_dao_action_data.complete() + } else { + Ok(()) + } } -fn verify_action_data(data: &[u8]) -> Result<(), Error> { - DaoActionDataReader::from_slice(data).map_err(|err| trace_error!(err))?; - Ok(()) +fn decode_dao_action_data(data: Bytes) -> Result { + DaoActionDataReader::from_slice(&data).map_err(|err| trace_error!(err))?; + Ok(DaoActionData::new_unchecked(data)) }