diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3361a4..d182873 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,4 +21,4 @@ jobs: - name: Push run: | cargo login ${{ secrets.CARGO_REGISTRY_TOKEN }} - cargo publish + make publish-crate diff --git a/Cargo.toml b/Cargo.toml index 2a94f70..66c90e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,12 +26,17 @@ libc = [] # work with `target-feature=-a` Cargo flag dummy-atomic = [] log = ["dep:log", "dummy-atomic"] +# require `ckb-hash` +type-id = ["ckb-hash", "ckb-types"] + [build-dependencies] cc = "1.0" [dependencies] ckb-types = { package = "ckb-gen-types", version = "0.118", default-features = false, optional = true } +ckb-hash = { version = "0.118", default-features = false, features = ["ckb-contract"], optional = true } + buddy-alloc = { version = "0.5", optional = true } ckb-x64-simulator = { version = "0.9", optional = true } gcd = "2.3" diff --git a/Makefile b/Makefile index 8e67093..9b43297 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,10 @@ CC := riscv64-unknown-elf-gcc default: integration publish-crate: - cargo publish -p ckb-std + cargo publish --features build-with-clang --target ${TARGET} -p ckb-std + +publish-crate-dryrun: + cargo publish --dry-run --features build-with-clang --target ${TARGET} -p ckb-std --allow-dirty publish: publish-crate @@ -16,10 +19,10 @@ test-shared-lib: integration: check -test: +test: publish-crate-dryrun make -C test test check: - cargo check --target ${TARGET} --examples + cargo check --target ${TARGET} --examples --features type-id,build-with-clang .PHONY: test check diff --git a/README.md b/README.md index 4aa4632..23efa04 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ckb-std -[![Crates.io](https://img.shields.io/crates/v/ckb-std.svg)](https://crates.io/crates/ckb-std) +[![Crates.io](https://img.shields.io/crates/v/ckb-std.svg)](https://crates.io/crates/ckb-std) This library contains several modules that help you write CKB contract with Rust. @@ -17,6 +17,7 @@ This library contains several modules that help you write CKB contract with Rust * `default_alloc!` macro: defines global allocator for no-std rust * `dummy_atomic` module: dummy atomic operations * `logger` module: colored logger implementation +* `type_id` module: Type ID implementation (feature `type-id`) ### Memory allocator Default allocator uses a mixed allocation strategy: diff --git a/contracts/ckb-std-tests/src/entry.rs b/contracts/ckb-std-tests/src/entry.rs index 410f6a3..3715873 100644 --- a/contracts/ckb-std-tests/src/entry.rs +++ b/contracts/ckb-std-tests/src/entry.rs @@ -263,7 +263,8 @@ fn test_dynamic_loading_c_impl(context: &mut ContextType) { fn test_vm_version() { let version = syscalls::vm_version().unwrap(); debug!("vm version: {}", version); - assert_eq!(version, 1); + // currently, version 1(before hardfork) and 2(after hardfork) are both ok + assert!(version == 1 || version == 2); } fn test_current_cycles() { diff --git a/examples/type_id.rs b/examples/type_id.rs new file mode 100644 index 0000000..57dae08 --- /dev/null +++ b/examples/type_id.rs @@ -0,0 +1,15 @@ +#![no_std] +#![no_main] + +use ckb_std::type_id::check_type_id; +use ckb_std::{default_alloc, entry}; + +entry!(main); +default_alloc!(); + +fn main() -> i8 { + match check_type_id(0) { + Ok(_) => 0, + Err(_) => -10, + } +} diff --git a/src/debug.rs b/src/debug.rs index e15e0d8..f78de62 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -47,11 +47,9 @@ macro_rules! debug { macro_rules! debug { ($fmt:literal) => { - #[cfg(std)] println!("{}", format!($fmt)); }; ($fmt:literal, $($args:expr),+) => { - #[cfg(std)] println!("{}", format!($fmt, $($args), +)); }; } diff --git a/src/error.rs b/src/error.rs index 65ccd6c..862383c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -20,7 +20,9 @@ pub enum SysError { MaxVmsSpawned, /// Max fds has been spawned. Its value is 9. MaxFdsCreated, - + /// Type ID Error + #[cfg(feature = "type-id")] + TypeIDError, /// Unknown syscall error number Unknown(u64), } diff --git a/src/lib.rs b/src/lib.rs index 20eb594..bf87577 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,3 +41,5 @@ pub mod dummy_atomic; pub mod logger; #[cfg(feature = "log")] pub use log; +#[cfg(feature = "type-id")] +pub mod type_id; diff --git a/src/type_id.rs b/src/type_id.rs new file mode 100644 index 0000000..aa1daf3 --- /dev/null +++ b/src/type_id.rs @@ -0,0 +1,141 @@ +//! Implementation of Type ID +//! +//! This module provides functionality for validating and checking Type IDs in +//! CKB transactions. It requires "type-id" feature in ckb-std enabled. +//! +//! For more details, see the [Type ID +//! RFC](https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0022-transaction-structure/0022-transaction-structure.md#type-id). +//! +//! Note: Type ID cells are allowed to be burned. +//! +use crate::{ + ckb_constants::Source, + error::SysError, + high_level::{load_cell_type_hash, load_input, load_script, load_script_hash, QueryIter}, + syscalls::load_cell, +}; +use ckb_hash::new_blake2b; +use ckb_types::prelude::Entity; + +fn is_cell_present(index: usize, source: Source) -> bool { + let buf = &mut []; + matches!( + load_cell(buf, 0, index, source), + Ok(_) | Err(SysError::LengthNotEnough(_)) + ) +} + +fn locate_index() -> Result { + let hash = load_script_hash()?; + + let index = QueryIter::new(load_cell_type_hash, Source::Output) + .position(|type_hash| type_hash == Some(hash)) + .ok_or(SysError::TypeIDError)?; + + Ok(index) +} + +/// +/// Validates the Type ID in a flexible manner. +/// +/// This function performs a low-level validation of the Type ID. It checks for the +/// presence of cells in the transaction and validates the Type ID based on whether +/// it's a minting operation or a transfer. +/// +/// # Arguments +/// +/// * `type_id` - A 32-byte array representing the Type ID to validate. +/// +/// # Returns +/// +/// * `Ok(())` if the Type ID is valid. +/// * `Err(SysError::TypeIDError)` if the validation fails. +/// +/// # Note +/// +/// For most use cases, it's recommended to use the `check_type_id` function instead, +/// which expects the Type ID to be included in the script `args`. +/// +/// # Examples +/// +/// ```no_run +/// use ckb_std::type_id::validate_type_id; +/// +/// let type_id = [0u8; 32]; +/// validate_type_id(type_id)?; +/// ``` +pub fn validate_type_id(type_id: [u8; 32]) -> Result<(), SysError> { + // after this checking, there are 3 cases: + // 1. 0 input cell and 1 output cell, it's minting operation + // 2. 1 input cell and 1 output cell, it's transfer operation + // 3. 1 input cell and 0 output cell, it's burning operation(allowed) + if is_cell_present(1, Source::GroupInput) || is_cell_present(1, Source::GroupOutput) { + return Err(SysError::TypeIDError); + } + + // case 1: minting operation + if !is_cell_present(0, Source::GroupInput) { + let index = locate_index()? as u64; + let input = load_input(0, Source::Input)?; + let mut blake2b = new_blake2b(); + blake2b.update(input.as_slice()); + blake2b.update(&index.to_le_bytes()); + let mut ret = [0; 32]; + blake2b.finalize(&mut ret); + + if ret != type_id { + return Err(SysError::TypeIDError); + } + } + // case 2 & 3: for the `else` part, it's transfer operation or burning operation + Ok(()) +} + +fn load_id_from_args(offset: usize) -> Result<[u8; 32], SysError> { + let script = load_script()?; + let args = script.as_reader().args(); + let args_data = args.raw_data(); + + args_data + .get(offset..offset + 32) + .ok_or(SysError::TypeIDError)? + .try_into() + .map_err(|_| SysError::TypeIDError) +} + +/// +/// Validates that the script follows the Type ID rule. +/// +/// This function checks if the Type ID (a 32-byte value) stored in the script's `args` +/// at the specified offset is valid according to the Type ID rules. +/// +/// # Arguments +/// +/// * `offset` - The byte offset in the script's `args` where the Type ID starts. +/// +/// # Returns +/// +/// * `Ok(())` if the Type ID is valid. +/// * `Err(SysError::TypeIDError)` if the Type ID is invalid or cannot be retrieved. +/// +/// # Examples +/// +/// ```no_run +/// use ckb_std::type_id::check_type_id; +/// +/// fn main() -> Result<(), ckb_std::error::SysError> { +/// // Check the Type ID stored at the beginning of the script args +/// check_type_id(0)?; +/// Ok(()) +/// } +/// ``` +/// +/// # Note +/// +/// This function internally calls `load_id_from_args` to retrieve the Type ID +/// and then `validate_type_id` to perform the actual validation. +pub fn check_type_id(offset: usize) -> Result<(), SysError> { + let type_id = load_id_from_args(offset)?; + validate_type_id(type_id)?; + Ok(()) +} diff --git a/test/Cargo.toml b/test/Cargo.toml index f3b1a66..9e3a13a 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -7,9 +7,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ckb-x64-simulator = "0.7" -ckb-testtool = "0.8.0" +ckb-x64-simulator = "0.9.2" +ckb-testtool = "0.13.1" serde_json = "1.0" -ckb-mock-tx-types = "0.4.0" +ckb-mock-tx-types = "0.118.0" blake2b-rs = "0.1.5" faster-hex = "0.6" +ckb-hash = "0.118.0" diff --git a/test/Makefile b/test/Makefile index 6d98db8..55a2180 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,4 +1,4 @@ -test: build +test: build build-examples RUST_LOG=debug cargo test -- --nocapture make -C simulator build make -C simulator run @@ -8,6 +8,10 @@ build: make -C shared-lib all-via-docker cd ../contracts && RUSTFLAGS="-C target-feature=-a" cargo build --target riscv64imac-unknown-none-elf +build-examples: + cd ../examples && RUSTFLAGS="-C target-feature=-a" cargo build --features build-with-clang --example type_id --target riscv64imac-unknown-none-elf --features "type-id" + cd ../examples && RUSTFLAGS="-C target-feature=-a" cargo build --features build-with-clang --example main --target riscv64imac-unknown-none-elf + clean: rm -rf ../build cargo clean diff --git a/test/src/contract.rs b/test/src/contract.rs index a7bbd2c..4df5393 100644 --- a/test/src/contract.rs +++ b/test/src/contract.rs @@ -1,7 +1,7 @@ use super::util::dump_mock_tx; use ckb_testtool::ckb_types::{bytes::Bytes, core::TransactionBuilder, packed::*, prelude::*}; use ckb_testtool::context::Context; -use ckb_x64_simulator::RunningSetup; +use ckb_x64_simulator::{RunningSetup, RunningType}; use std::collections::HashMap; use std::fs::File; use std::io::Read; @@ -86,6 +86,7 @@ fn it_works() { script_index: 0, vm_version: 1, native_binaries: HashMap::default(), + run_type: Some(RunningType::Executable), }; dump_mock_tx(test_case_name, &tx, &context, &setup); diff --git a/test/src/exec.rs b/test/src/exec.rs index 3844d19..f130ea9 100644 --- a/test/src/exec.rs +++ b/test/src/exec.rs @@ -9,7 +9,7 @@ use ckb_testtool::{ }, context::Context, }; -use ckb_x64_simulator::RunningSetup; +use ckb_x64_simulator::{RunningSetup, RunningType}; use std::collections::HashMap; use std::fs::File; use std::io::Read; @@ -84,10 +84,12 @@ fn test_exec_by_code_hash() { let mut context = Context::default(); let caller_bin = { let mut buf = Vec::new(); - File::open("../contracts/target/riscv64imac-unknown-none-elf/debug/exec-caller-by-code-hash") - .unwrap() - .read_to_end(&mut buf) - .expect("read code"); + File::open( + "../contracts/target/riscv64imac-unknown-none-elf/debug/exec-caller-by-code-hash", + ) + .unwrap() + .read_to_end(&mut buf) + .expect("read code"); Bytes::from(buf) }; let caller_out_point = context.deploy_cell(caller_bin); @@ -159,6 +161,7 @@ fn test_exec_by_code_hash() { script_index: 0, vm_version: 1, native_binaries, + run_type: Some(RunningType::Executable), }; dump_mock_tx(test_case_name, &tx, &context, &setup); diff --git a/test/src/lib.rs b/test/src/lib.rs index 300fa74..81fad63 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -3,4 +3,6 @@ mod contract; #[cfg(test)] mod exec; #[cfg(test)] +mod type_id; +#[cfg(test)] mod util; diff --git a/test/src/type_id.rs b/test/src/type_id.rs new file mode 100644 index 0000000..11abb18 --- /dev/null +++ b/test/src/type_id.rs @@ -0,0 +1,183 @@ +use std::fs::File; +use std::io::Read; + +use ckb_hash::new_blake2b; +use ckb_testtool::ckb_types::{bytes::Bytes, core::TransactionBuilder, packed::*, prelude::*}; +use ckb_testtool::context::Context; +const MAX_CYCLES: u64 = 1000_0000; + +fn build_bins() -> (Bytes, Bytes) { + let always_success_bin = { + let mut buf = Vec::new(); + File::open("../target/riscv64imac-unknown-none-elf/debug/examples/main") + .unwrap() + .read_to_end(&mut buf) + .expect("read code"); + Bytes::from(buf) + }; + let type_id_bin = { + let mut buf = Vec::new(); + File::open("../target/riscv64imac-unknown-none-elf/debug/examples/type_id") + .unwrap() + .read_to_end(&mut buf) + .expect("read code"); + Bytes::from(buf) + }; + (always_success_bin, type_id_bin) +} + +fn type_id_mint(wrong_type_id: bool) { + let mut context = Context::default(); + let (always_success_bin, type_id_bin) = build_bins(); + let always_success_out_point = context.deploy_cell(always_success_bin); + let type_id_out_point = context.deploy_cell(type_id_bin); + let type_script_dep = CellDep::new_builder() + .out_point(type_id_out_point.clone()) + .build(); + + let lock_script = context + .build_script(&always_success_out_point, Default::default()) + .expect("script"); + let lock_script_dep = CellDep::new_builder() + .out_point(always_success_out_point) + .build(); + + let input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity(1000u64.pack()) + .lock(lock_script.clone()) + .build(), + Bytes::new(), + ); + let input = CellInput::new_builder() + .previous_output(input_out_point) + .build(); + + let index: usize = 0; + let mut type_id = vec![0u8; 32]; + let mut blake2b = new_blake2b(); + blake2b.update(input.as_slice()); + blake2b.update(&index.to_be_bytes()); + blake2b.finalize(&mut type_id); + if wrong_type_id { + type_id[0] ^= 1; + } + let type_script = context + .build_script(&type_id_out_point, type_id.into()) + .expect("script"); + + let outputs = vec![CellOutput::new_builder() + .capacity(500u64.pack()) + .lock(lock_script.clone()) + .type_(Some(type_script.clone()).pack()) + .build()]; + + let mut outputs_data: Vec = Vec::new(); + outputs_data.push(vec![42u8; 1000].into()); + + // build transaction + let tx = TransactionBuilder::default() + .input(input) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .cell_dep(lock_script_dep) + .cell_dep(type_script_dep) + .build(); + let tx = context.complete_tx(tx); + + // run + let result = context.verify_tx(&tx, MAX_CYCLES); + if wrong_type_id { + result.expect_err("should verify failed"); + } else { + result.expect("should verify success"); + } +} + +fn type_id_tx(wrong_type_id: bool) { + let mut context = Context::default(); + let (always_success_bin, type_id_bin) = build_bins(); + let always_success_out_point = context.deploy_cell(always_success_bin); + let type_id_out_point = context.deploy_cell(type_id_bin); + let type_script_dep = CellDep::new_builder() + .out_point(type_id_out_point.clone()) + .build(); + + let lock_script = context + .build_script(&always_success_out_point, Default::default()) + .expect("script"); + let lock_script_dep = CellDep::new_builder() + .out_point(always_success_out_point) + .build(); + let type_id = vec![1u8; 32]; + let type_script = context + .build_script(&type_id_out_point, type_id.into()) + .expect("script"); + + let input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity(1000u64.pack()) + .lock(lock_script.clone()) + .type_(Some(type_script.clone()).pack()) + .build(), + Bytes::new(), + ); + let input = CellInput::new_builder() + .previous_output(input_out_point) + .build(); + + let type_id2 = if wrong_type_id { + vec![2u8; 32] + } else { + vec![1u8; 32] + }; + let type_script2 = context + .build_script(&type_id_out_point, type_id2.into()) + .expect("script"); + + let outputs = vec![CellOutput::new_builder() + .capacity(500u64.pack()) + .lock(lock_script.clone()) + .type_(Some(type_script2.clone()).pack()) + .build()]; + + let mut outputs_data: Vec = Vec::new(); + outputs_data.push(vec![42u8; 1000].into()); + + // build transaction + let tx = TransactionBuilder::default() + .input(input) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .cell_dep(lock_script_dep) + .cell_dep(type_script_dep) + .build(); + let tx = context.complete_tx(tx); + + // run + let result = context.verify_tx(&tx, MAX_CYCLES); + if wrong_type_id { + result.expect_err("should verify failed"); + } else { + result.expect("should verify success"); + } +} +#[test] +fn test_type_id_mint() { + type_id_mint(false); +} + +#[test] +fn test_type_id_mint_failed() { + type_id_mint(true); +} + +#[test] +fn test_type_id_tx() { + type_id_tx(false); +} + +#[test] +fn test_type_id_tx_failed() { + type_id_tx(true); +} diff --git a/test/src/util.rs b/test/src/util.rs index 3d8067b..40c5203 100644 --- a/test/src/util.rs +++ b/test/src/util.rs @@ -64,6 +64,7 @@ fn build_mock_tx(tx: &core::TransactionView, context: &Context) -> MockTransacti inputs: mock_inputs, cell_deps: mock_cell_deps, header_deps: vec![], + extensions: Default::default(), }; MockTransaction { mock_info,