diff --git a/Cargo.lock b/Cargo.lock index 319b1b6..995e498 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -608,8 +608,10 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" name = "dao-action-verifier" version = "0.1.0" dependencies = [ + "ckb-dao-cobuild-schemas", "ckb-std", "ckb-transaction-cobuild", + "molecule", ] [[package]] @@ -718,6 +720,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "includedir" version = "0.6.0" @@ -1237,7 +1245,10 @@ dependencies = [ name = "tests" version = "0.1.0" dependencies = [ + "ckb-dao-cobuild-schemas", "ckb-testtool", + "ckb-transaction-cobuild", + "hex-literal", "serde_json", ] diff --git a/contracts/__tests__/Cargo.toml b/contracts/__tests__/Cargo.toml index 58e8251..f29d235 100644 --- a/contracts/__tests__/Cargo.toml +++ b/contracts/__tests__/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] ckb-testtool = "0.10.1" serde_json = "1.0" +ckb-transaction-cobuild = { git = "https://github.com/cryptape/ckb-transaction-cobuild-poc.git" } +ckb-dao-cobuild-schemas = { path = "../../crates/ckb-dao-cobuild-schemas" } +hex-literal = "0.4.1" diff --git a/contracts/__tests__/src/lib.rs b/contracts/__tests__/src/lib.rs index 53fb2ab..e59480d 100644 --- a/contracts/__tests__/src/lib.rs +++ b/contracts/__tests__/src/lib.rs @@ -1,15 +1,26 @@ +use ckb_dao_cobuild_schemas::{ + Capacity, Claim, ClaimVec, DaoActionData, Deposit, DepositVec, Withdraw, WithdrawVec, +}; use ckb_testtool::{ + builtin::ALWAYS_SUCCESS, ckb_error::Error, + ckb_hash::blake2b_256, ckb_types::{ bytes::Bytes, - core::{Cycle, TransactionView}, + core::{Cycle, TransactionBuilder, TransactionView}, + packed, + prelude::*, }, context::Context, }; -use std::env; -use std::fs; -use std::path::PathBuf; -use std::str::FromStr; +use ckb_transaction_cobuild::schemas::{ + basic::{Action, ActionVec, Message, SighashAll}, + top_level::{WitnessLayout, WitnessLayoutUnion}, +}; +use hex_literal::hex; +use std::{env, fs, path::PathBuf, str::FromStr}; + +include!("../../dao-action-verifier/src/error_code.rs"); #[cfg(test)] mod tests; @@ -110,3 +121,309 @@ pub fn verify_and_dump_failed_tx( } result } + +pub fn assert_tx_error( + context: &Context, + tx: &TransactionView, + err_code: ErrorCode, + max_cycles: u64, +) { + match context.verify_tx(tx, max_cycles) { + Ok(_) => panic!( + "expect error code {:?}({}), got success", + err_code, err_code as i8 + ), + Err(err) => { + let error_string = err.to_string(); + assert!( + error_string.contains(format!("error code {} ", err_code as i8).as_str()), + "expect error code {:?}({}), got {}", + err_code, + err_code as i8, + error_string, + ); + } + } +} + +pub struct CellSpec { + output: packed::CellOutputBuilder, + data: Bytes, + since: packed::Uint64, +} + +pub trait TxSpec { + fn new_dao_input_spec(&mut self) -> CellSpec; + fn new_dao_output_spec(&mut self) -> CellSpec; + fn new_tx_builder( + &mut self, + dao_input_spec: CellSpec, + dao_output_spec: CellSpec, + ) -> TransactionBuilder; + fn complete_tx(&mut self, tx: TransactionView) -> TransactionView; +} + +pub fn build_tx(spec: &mut T) -> TransactionView { + let dao_input_spec = spec.new_dao_input_spec(); + let dao_output_spec = spec.new_dao_output_spec(); + let tx = spec.new_tx_builder(dao_input_spec, dao_output_spec).build(); + + spec.complete_tx(tx) +} + +pub struct DefaultTxSpec { + context: Context, + + alice_lock_script: packed::Script, + + verifier_type_script: packed::Script, + dao_type_script: packed::Script, +} + +impl DefaultTxSpec { + fn new() -> Self { + let mut context = Context::default(); + let loader = Loader::default(); + + // use always success as lock + let always_success_out_point = context.deploy_cell((*ALWAYS_SUCCESS).clone()); + let alice_lock_script = context + .build_script(&always_success_out_point, Bytes::from(vec![0xa])) + .expect("script"); + + // use always success as dao + let dao_type_script = packed::Script::new_builder() + .code_hash( + hex!("82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e").pack(), + ) + .hash_type(1.into()) + .build(); + let dao = packed::CellOutput::new_builder() + .capacity(1000u64.pack()) + .lock(alice_lock_script.clone()) + .type_( + Some( + packed::Script::new_builder() + .code_hash( + hex!( + "00000000000000000000000000000000000000000000000000545950455f4944" + ) + .pack(), + ) + .hash_type(1.into()) + .args( + hex!( + "b2a8500929d6a1294bf9bf1bf565f549fa4a5f1316a3306ad3d4783e64bcf626" + ) + .to_vec() + .pack(), + ) + .build(), + ) + .pack(), + ) + .build(); + context.create_cell(dao, (*ALWAYS_SUCCESS).clone()); + + // verifier cell + let verifier_out_point = context.deploy_cell(loader.load_binary("dao-action-verifier")); + let verifier_type_script = context + .build_script(&verifier_out_point, Default::default()) + .expect("script"); + + Self { + context, + alice_lock_script, + verifier_type_script, + dao_type_script, + } + } + + pub fn pack_dao_data(&self, action_data: Bytes) -> Bytes { + self.pack_dao_data_vec(vec![action_data]) + } + + pub fn pack_dao_operations( + &self, + deposits: Vec, + withdraws: Vec, + claims: Vec, + ) -> Bytes { + self.pack_dao_data( + DaoActionData::new_builder() + .deposits(DepositVec::new_builder().set(deposits).build()) + .withdraws(WithdrawVec::new_builder().set(withdraws).build()) + .claims(ClaimVec::new_builder().set(claims).build()) + .build() + .as_bytes(), + ) + } + + pub fn pack_dao_data_vec(&self, action_data_vec: Vec) -> Bytes { + let script_hash = blake2b_256(self.dao_type_script.as_slice()); + let actions = ActionVec::new_builder() + .set( + action_data_vec + .into_iter() + .map(|data| { + Action::new_builder() + .script_hash(script_hash.pack()) + .data(data.pack()) + .build() + }) + .collect(), + ) + .build(); + let message = Message::new_builder().actions(actions).build(); + WitnessLayout::new_builder() + .set(WitnessLayoutUnion::SighashAll( + SighashAll::new_builder().message(message).build(), + )) + .build() + .as_bytes() + } +} + +impl Default for DefaultTxSpec { + fn default() -> Self { + Self::new() + } +} + +pub const DEFAULT_CAPACITY: u64 = 1000; +pub const DAO_DEPOSIT_DATA: [u8; 8] = hex!("0000000000000000"); + +pub fn pack_capacity(shannons: u64) -> Capacity { + Capacity::new_unchecked(Bytes::from(shannons.to_le_bytes().to_vec())) +} + +impl TxSpec for DefaultTxSpec { + fn new_dao_input_spec(&mut self) -> CellSpec { + CellSpec { + output: packed::CellOutput::new_builder() + .capacity(DEFAULT_CAPACITY.pack()) + .lock(self.alice_lock_script.clone()), + data: Bytes::new(), + since: 0.pack(), + } + } + + fn new_dao_output_spec(&mut self) -> CellSpec { + CellSpec { + output: packed::CellOutput::new_builder() + .capacity(DEFAULT_CAPACITY.pack()) + .type_(Some(self.dao_type_script.clone()).pack()) + .lock(self.alice_lock_script.clone()), + data: Bytes::from(DAO_DEPOSIT_DATA.to_vec()), + since: 0.pack(), + } + } + + fn new_tx_builder( + &mut self, + 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); + let dao_input = packed::CellInput::new_builder() + .previous_output(dao_cell) + .since(dao_input_spec.since) + .build(); + let dao_output = dao_output_spec.output.build(); + + let verifier_cell = self.context.create_cell( + packed::CellOutput::new_builder() + .capacity(DEFAULT_CAPACITY.pack()) + .lock(self.alice_lock_script.clone()) + .type_(Some(self.verifier_type_script.clone()).pack()) + .build(), + Bytes::new(), + ); + let verifier_input = packed::CellInput::new_builder() + .previous_output(verifier_cell) + .build(); + let verifier_output = packed::CellOutput::new_builder() + .capacity(DEFAULT_CAPACITY.pack()) + .lock(self.alice_lock_script.clone()) + .build(); + + TransactionBuilder::default() + .inputs(vec![dao_input, verifier_input]) + .outputs(vec![dao_output, verifier_output]) + .outputs_data(vec![dao_output_spec.data, Bytes::new()].pack()) + .witnesses(vec![Bytes::new(), Bytes::new()].pack()) + } + + fn complete_tx(&mut self, tx: TransactionView) -> TransactionView { + self.context.complete_tx(tx) + } +} + +#[derive(Default)] +pub struct CustomTxSpec { + inner: DefaultTxSpec, + on_new_input_spec: Option CellSpec>>, + on_new_output_spec: Option CellSpec>>, + on_new_tx_builder: Option TransactionBuilder>>, +} + +impl TxSpec for CustomTxSpec { + fn new_dao_input_spec(&mut self) -> CellSpec { + let cell_spec = self.inner.new_dao_input_spec(); + match &self.on_new_input_spec { + Some(cb) => cb(cell_spec), + None => cell_spec, + } + } + fn new_dao_output_spec(&mut self) -> CellSpec { + let cell_spec = self.inner.new_dao_output_spec(); + match &self.on_new_output_spec { + Some(cb) => cb(cell_spec), + None => cell_spec, + } + } + + fn new_tx_builder( + &mut self, + dao_input_spec: CellSpec, + dao_output_spec: CellSpec, + ) -> TransactionBuilder { + let tx_builder = self.inner.new_tx_builder(dao_input_spec, dao_output_spec); + match &self.on_new_tx_builder { + Some(cb) => cb(tx_builder), + None => tx_builder, + } + } + + fn complete_tx(&mut self, tx: TransactionView) -> TransactionView { + self.inner.complete_tx(tx) + } +} + +impl CustomTxSpec { + pub fn on_new_tx_builder(&mut self, cb: F) -> &mut Self + where + F: Fn(TransactionBuilder) -> TransactionBuilder + 'static, + { + self.on_new_tx_builder = Some(Box::new(cb)); + self + } + + pub fn on_new_input_spec(&mut self, cb: F) -> &mut Self + where + F: Fn(CellSpec) -> CellSpec + 'static, + { + self.on_new_input_spec = Some(Box::new(cb)); + self + } + + pub fn on_new_output_spec(&mut self, cb: F) -> &mut Self + where + F: Fn(CellSpec) -> CellSpec + 'static, + { + self.on_new_output_spec = Some(Box::new(cb)); + self + } +} diff --git a/contracts/__tests__/src/tests.rs b/contracts/__tests__/src/tests.rs index cc9814b..1dae3b5 100644 --- a/contracts/__tests__/src/tests.rs +++ b/contracts/__tests__/src/tests.rs @@ -1,54 +1,65 @@ // Include your tests here // See https://github.com/xxuejie/ckb-native-build-sample/blob/main/tests/src/tests.rs for examples -use crate::Loader; -use ckb_testtool::{ - ckb_types::{bytes::Bytes, core::TransactionBuilder, packed, prelude::*}, - context::Context, -}; - -include!("../../dao-action-verifier/src/error_code.rs"); +use crate::*; +use ckb_dao_cobuild_schemas::DaoActionData; const MAX_CYCLES: u64 = 10_000_000; #[test] -fn test_fail() { - let loader = Loader::default(); - let mut context = Context::default(); - - let bin = loader.load_binary("dao-action-verifier"); - let out_point = context.deploy_cell(bin); - - let lock_script = context - .build_script(&out_point, Default::default()) - .expect("script"); - - let input_out_point = context.create_cell( - packed::CellOutput::new_builder() - .capacity(1000u64.pack()) - .lock(lock_script.clone()) - .build(), - Bytes::new(), - ); - let input = packed::CellInput::new_builder() - .previous_output(input_out_point) - .build(); +fn test_null_dao_data() { + let mut spec = DefaultTxSpec::new(); - let outputs = vec![packed::CellOutput::new_builder() - .capacity(1000u64.pack()) - .lock(lock_script.clone()) - .build()]; + let tx = build_tx(&mut spec); + verify_and_dump_failed_tx(&spec.context, &tx, MAX_CYCLES).expect("pass"); +} - let outputs_data = vec![Bytes::new()]; +#[test] +fn test_invalid_dao_data() { + let mut spec = CustomTxSpec::default(); + let witness = spec.inner.pack_dao_data(Bytes::new()); + spec.on_new_tx_builder(move |b| b.witness(witness.clone().pack())); - let tx = TransactionBuilder::default() - .input(input) - .outputs(outputs) - .outputs_data(outputs_data.pack()) - .build(); + let tx = build_tx(&mut spec); + assert_tx_error( + &spec.inner.context, + &tx, + ErrorCode::InvalidActionDataSchema, + MAX_CYCLES, + ); +} - let tx = context.complete_tx(tx); +#[test] +fn test_empty_dao_ops() { + let mut spec = CustomTxSpec::default(); + let witness = spec + .inner + .pack_dao_data_vec(vec![DaoActionData::default().as_bytes()]); + // unset deposit + spec.on_new_output_spec(|cell| CellSpec { + output: cell.output.type_(packed::ScriptOpt::default()), + ..cell + }); + 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_multiple_dao_actions() { + let mut spec = CustomTxSpec::default(); + let witness = spec.inner.pack_dao_data_vec(vec![ + DaoActionData::default().as_bytes(), + DaoActionData::default().as_bytes(), + ]); + // unset deposit + spec.on_new_output_spec(|cell| CellSpec { + output: cell.output.type_(packed::ScriptOpt::default()), + ..cell + }); + spec.on_new_tx_builder(move |b| b.witness(witness.clone().pack())); - let result = context.verify_tx(&tx, MAX_CYCLES); - assert!(result.is_err()); + 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/Cargo.toml b/contracts/dao-action-verifier/Cargo.toml index f8c1f44..dadbfbd 100644 --- a/contracts/dao-action-verifier/Cargo.toml +++ b/contracts/dao-action-verifier/Cargo.toml @@ -10,3 +10,5 @@ ckb-std = { version = "0.14.3", default-features = false, features = [ "allocator", ] } ckb-transaction-cobuild = { git = "https://github.com/cryptape/ckb-transaction-cobuild-poc.git" } +ckb-dao-cobuild-schemas = { path = "../../crates/ckb-dao-cobuild-schemas" } +molecule = { version = "0.7.5", default-features = false } diff --git a/contracts/dao-action-verifier/src/constants.rs b/contracts/dao-action-verifier/src/constants.rs new file mode 100644 index 0000000..1aa8b48 --- /dev/null +++ b/contracts/dao-action-verifier/src/constants.rs @@ -0,0 +1,4 @@ +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, +]; diff --git a/contracts/dao-action-verifier/src/error.rs b/contracts/dao-action-verifier/src/error.rs index 0653136..bcfd9e4 100644 --- a/contracts/dao-action-verifier/src/error.rs +++ b/contracts/dao-action-verifier/src/error.rs @@ -4,6 +4,7 @@ use ckb_std::error::SysError; #[derive(Debug)] pub enum Error { CkbStd(SysError), + InvalidActionDataSchema(molecule::error::VerificationError), } impl From for ErrorCode { @@ -16,6 +17,7 @@ impl From for ErrorCode { SysError::Encoding => ErrorCode::Encoding, SysError::Unknown(_) => ErrorCode::Unknown, }, + Error::InvalidActionDataSchema(_) => ErrorCode::InvalidActionDataSchema, } } } @@ -32,9 +34,21 @@ impl From for Error { } } +impl From for Error { + fn from(err: molecule::error::VerificationError) -> Self { + Error::InvalidActionDataSchema(err) + } +} + #[macro_export] macro_rules! trace_error { - ($err:expr, $message:literal) => {{ + ($err:expr) => {{ + let err = $crate::error::Error::from($err); + #[cfg(debug_assertions)] + ckb_std::debug!("{}:{} {:?}", file!(), line!(), err); + err + }}; + ($err:expr, $message:expr) => {{ let err = $crate::error::Error::from($err); #[cfg(debug_assertions)] ckb_std::debug!("{}:{} {:?} {}", file!(), line!(), err, $message); diff --git a/contracts/dao-action-verifier/src/error_code.rs b/contracts/dao-action-verifier/src/error_code.rs index 200cfb6..1de787e 100644 --- a/contracts/dao-action-verifier/src/error_code.rs +++ b/contracts/dao-action-verifier/src/error_code.rs @@ -1,4 +1,5 @@ #[cfg_attr(test, allow(dead_code))] +#[derive(Copy, Clone, Debug)] #[repr(i8)] pub enum ErrorCode { IndexOutOfBound = 16, @@ -6,4 +7,7 @@ pub enum ErrorCode { LengthNotEnough = 18, Encoding = 19, Unknown = 20, + + // custom errors + InvalidActionDataSchema = 64, } diff --git a/contracts/dao-action-verifier/src/main.rs b/contracts/dao-action-verifier/src/main.rs index fe7bca8..2c23164 100644 --- a/contracts/dao-action-verifier/src/main.rs +++ b/contracts/dao-action-verifier/src/main.rs @@ -11,11 +11,15 @@ ckb_std::entry!(program_entry); #[cfg(all(target_arch = "riscv64", not(test)))] default_alloc!(); -use ckb_std::error::SysError; +use ckb_dao_cobuild_schemas::DaoActionDataReader; +use ckb_std::ckb_types::prelude::*; +use ckb_transaction_cobuild::fetch_message; + +mod constants; mod error; mod error_code; -use error::Error; +use crate::{constants::DAO_SCRIPT_HASH, error::Error}; pub fn program_entry() -> i8 { match verify() { @@ -24,6 +28,20 @@ pub fn program_entry() -> i8 { } } -pub fn verify() -> Result<(), Error> { - Err(trace_error!(SysError::IndexOutOfBound, "error")) +fn verify() -> Result<(), Error> { + if let Ok(Some(message)) = fetch_message() { + for action in message.actions().into_iter() { + if action.script_hash().as_slice() == DAO_SCRIPT_HASH { + verify_action_data(&action.data().raw_data())?; + } + } + } + + // It's OK to not include DAO action data + Ok(()) +} + +fn verify_action_data(data: &[u8]) -> Result<(), Error> { + DaoActionDataReader::from_slice(data).map_err(|err| trace_error!(err))?; + Ok(()) } diff --git a/crates/ckb-dao-cobuild-schemas/src/lib.rs b/crates/ckb-dao-cobuild-schemas/src/lib.rs index 8cbca32..1463ba4 100644 --- a/crates/ckb-dao-cobuild-schemas/src/lib.rs +++ b/crates/ckb-dao-cobuild-schemas/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(target_arch = "riscv64", no_std)] + mod schemas; pub use schemas::dao::*;