diff --git a/Cargo.lock b/Cargo.lock index 6dd46bb..03be9d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,17 @@ dependencies = [ "sha2", ] +[[package]] +name = "ccc-eth-lock" +version = "0.1.0" +dependencies = [ + "ckb-lock-helper", + "ckb-std", + "hex", + "k256", + "sha3", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -258,6 +269,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.155" @@ -333,6 +353,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "signature" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 72bb669..7d7f938 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ # Please don't remove the following line, we use it to automatically # detect insertion point for newly generated crates. # @@INSERTION_POINT@@ + "contracts/ccc-eth-lock", "contracts/ccc-btc-lock", "crates/ckb-lock-helper" ] diff --git a/checksums.txt b/checksums.txt index eabcec6..4bb3a3e 100644 --- a/checksums.txt +++ b/checksums.txt @@ -1 +1,2 @@ 3d659b15f2aad5f9350f55ce471806c6d6ad4f51a555a82b7918e9d88f84f04a build/release/ccc-btc-lock +4ae08bd7ed954997dcbca5ff88700bf7f949b1080c2bd1cb024f15c8b0436396 build/release/ccc-eth-lock diff --git a/contracts/ccc-eth-lock/.gitignore b/contracts/ccc-eth-lock/.gitignore new file mode 100644 index 0000000..c3dca1b --- /dev/null +++ b/contracts/ccc-eth-lock/.gitignore @@ -0,0 +1,2 @@ +/build +/target diff --git a/contracts/ccc-eth-lock/Cargo.toml b/contracts/ccc-eth-lock/Cargo.toml new file mode 100644 index 0000000..9288b88 --- /dev/null +++ b/contracts/ccc-eth-lock/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ccc-eth-lock" +version = "0.1.0" +edition = "2021" + +[dependencies] +ckb-std = "0.15" +ckb-lock-helper = { path = "../../crates/ckb-lock-helper" } +hex = { version = "0.4", default-features = false, features = ["alloc"] } +k256 = { version = "=0.13.1", default-features = false, features = ["arithmetic", "ecdsa", "alloc"] } +sha3 = { version = "0.10.8", default-features = false } diff --git a/contracts/ccc-eth-lock/Makefile b/contracts/ccc-eth-lock/Makefile new file mode 100644 index 0000000..579f431 --- /dev/null +++ b/contracts/ccc-eth-lock/Makefile @@ -0,0 +1,77 @@ +# We cannot use $(shell pwd), which will return unix path format on Windows, +# making it hard to use. +cur_dir = $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) + +TOP := $(cur_dir) +# RUSTFLAGS that are likely to be tweaked by developers. For example, +# while we enable debug logs by default here, some might want to strip them +# for minimal code size / consumed cycles. +CUSTOM_RUSTFLAGS := --cfg debug_assertions +# RUSTFLAGS that are less likely to be tweaked by developers. Most likely +# one would want to keep the default values here. +FULL_RUSTFLAGS := -C target-feature=+zba,+zbb,+zbc,+zbs $(CUSTOM_RUSTFLAGS) +# Additional cargo args to append here. For example, one can use +# make test CARGO_ARGS="-- --nocapture" so as to inspect data emitted to +# stdout in unit tests +CARGO_ARGS := +MODE := release +# Tweak this to change the clang version to use for building C code. By default +# we use a bash script with somes heuristics to find clang in current system. +CLANG := $(shell $(TOP)/scripts/find_clang) +AR := $(subst clang,llvm-ar,$(CLANG)) +# When this is set to some value, the generated binaries will be copied over +BUILD_DIR := +# Generated binaries to copy. By convention, a Rust crate's directory name will +# likely match the crate name, which is also the name of the final binary. +# However if this is not the case, you can tweak this variable. As the name hints, +# more than one binary is supported here. +BINARIES := $(notdir $(shell pwd)) + +ifeq (release,$(MODE)) + MODE_ARGS := --release +endif + +default: build test + +build: + RUSTFLAGS="$(FULL_RUSTFLAGS)" TARGET_CC="$(CLANG)" TARGET_AR="$(AR)" \ + cargo build --target=riscv64imac-unknown-none-elf $(MODE_ARGS) $(CARGO_ARGS) + @set -eu; \ + if [ "x$(BUILD_DIR)" != "x" ]; then \ + for binary in $(BINARIES); do \ + echo "Copying binary $$binary to build directory"; \ + cp $(TOP)/target/riscv64imac-unknown-none-elf/$(MODE)/$$binary $(TOP)/$(BUILD_DIR); \ + done \ + fi + +# test, check, clippy and fmt here are provided for completeness, +# there is nothing wrong invoking cargo directly instead of make. +test: + cargo test $(CARGO_ARGS) + +check: + cargo check $(CARGO_ARGS) + +clippy: + cargo clippy $(CARGO_ARGS) + +fmt: + cargo fmt $(CARGO_ARGS) + +# Arbitrary cargo command is supported here. For example: +# +# make cargo CARGO_CMD=expand CARGO_ARGS="--ugly" +# +# Invokes: +# cargo expand --ugly +CARGO_CMD := +cargo: + cargo $(CARGO_CMD) $(CARGO_ARGS) + +clean: + cargo clean + +prepare: + rustup target add riscv64imac-unknown-none-elf + +.PHONY: build test check clippy fmt cargo clean prepare diff --git a/contracts/ccc-eth-lock/README.md b/contracts/ccc-eth-lock/README.md new file mode 100644 index 0000000..5dd876f --- /dev/null +++ b/contracts/ccc-eth-lock/README.md @@ -0,0 +1,3 @@ +# ccc-btc-lock + +CCC ETH lock implementation. See [specification](../../docs/eth.md) for more information. diff --git a/contracts/ccc-eth-lock/src/entry.rs b/contracts/ccc-eth-lock/src/entry.rs new file mode 100644 index 0000000..c890751 --- /dev/null +++ b/contracts/ccc-eth-lock/src/entry.rs @@ -0,0 +1,87 @@ +use crate::error::Error; +use alloc::format; +use alloc::vec::Vec; +use ckb_lock_helper::{generate_sighash_all, println_hex, secp256k1_patch::recover_from_prehash}; +use ckb_std::{ + ckb_constants::Source, + high_level::{load_script, load_witness_args}, +}; +use k256::ecdsa::{RecoveryId, Signature}; +use sha3::Digest; + +fn keccak(msg: &[u8]) -> [u8; 32] { + let mut hasher = sha3::Keccak256::new(); + hasher.update(msg); + hasher.finalize().into() +} + +fn keccak160(msg: &[u8]) -> [u8; 20] { + let mut output = [0u8; 20]; + output.copy_from_slice(&keccak(msg)[12..]); + output +} + +fn message_hash(msg: &str) -> [u8; 32] { + // Only 32-bytes hex representation of the hash is allowed. + assert_eq!(msg.len(), 64); + // Text used to signify that a signed message follows and to prevent inadvertently signing a transaction. + const CKB_PREFIX: &str = "Signing a CKB transaction: 0x"; + const CKB_SUFFIX: &str = "\n\nIMPORTANT: Please verify the integrity and authenticity of connected Ethereum wallet before signing this message\n"; + const ETH_PREFIX: &str = "Ethereum Signed Message:\n"; + let mut data: Vec = Vec::new(); + assert_eq!(ETH_PREFIX.len(), 25); + data.push(25); + data.extend(ETH_PREFIX.as_bytes()); + data.extend( + format!( + "{}", + (CKB_PREFIX.len() + msg.len() + CKB_SUFFIX.len()) as u8 + ) + .as_bytes(), + ); + data.extend(CKB_PREFIX.as_bytes()); + data.extend(msg.as_bytes()); + data.extend(CKB_SUFFIX.as_bytes()); + keccak(&data) +} + +pub fn entry() -> Result<(), Error> { + let script = load_script()?; + let pubkey_hash_expect = script.args().raw_data(); + if pubkey_hash_expect.len() != 20 { + return Err(Error::WrongPubkeyHash); + } + let sighash_all = generate_sighash_all()?; + let sighash_all_hex = hex::encode(&sighash_all); + let digest_hash = message_hash(&sighash_all_hex); + let witness_args = load_witness_args(0, Source::GroupInput)?; + let sig_raw = witness_args + .lock() + .to_opt() + .ok_or(Error::WrongSignatureFormat)? + .raw_data(); + if sig_raw.len() != 65 { + return Err(Error::WrongSignatureFormat); + } + let rec_id = sig_raw[64].wrapping_sub(27); + if rec_id >= 2 { + return Err(Error::InvalidRecoverId); + } + let rec_id = RecoveryId::try_from(rec_id).map_err(|_| Error::InvalidRecoverId)?; + let sig = Signature::from_slice(&sig_raw[..64]).map_err(|_| Error::WrongSignatureFormat)?; + if sig.normalize_s().is_some() { + return Err(Error::SignatureIsNotLowS); + } + let pubkey_result = &recover_from_prehash(&digest_hash, &sig, rec_id) + .map_err(|_| Error::CanNotRecover)? + .to_encoded_point(false) + .to_bytes()[1..]; + assert!(pubkey_result.len() == 64); + let pubkey_hash_result = keccak160(&pubkey_result); + println_hex("pubkey_hash_result", pubkey_hash_result.as_ref()); + println_hex("pubkey_hash_expect", pubkey_hash_expect.as_ref()); + if pubkey_hash_result.as_ref() != pubkey_hash_expect.as_ref() { + return Err(Error::PubkeyHashMismatched); + } + Ok(()) +} diff --git a/contracts/ccc-eth-lock/src/error.rs b/contracts/ccc-eth-lock/src/error.rs new file mode 100644 index 0000000..27b63fa --- /dev/null +++ b/contracts/ccc-eth-lock/src/error.rs @@ -0,0 +1,43 @@ +use ckb_lock_helper::error::Error as HelperError; +use ckb_std::error::SysError; + +#[repr(i8)] +pub enum Error { + IndexOutOfBound = 1, + ItemMissing, + LengthNotEnough, + Encoding, + Unknown = 30, + WrongWitnessArgs, + WrongPubkeyHash, + PubkeyHashMismatched, + WrongSignatureFormat, + InvalidRecoverId, + CanNotRecover, + SignatureIsNotLowS, +} + +impl From for Error { + fn from(value: HelperError) -> Self { + match value { + HelperError::IndexOutOfBound => Error::IndexOutOfBound, + HelperError::ItemMissing => Error::ItemMissing, + HelperError::LengthNotEnough => Error::LengthNotEnough, + HelperError::Encoding => Error::Encoding, + HelperError::Unknown => Error::Unknown, + HelperError::WrongWitnessArgs => Error::WrongWitnessArgs, + } + } +} + +impl From for Error { + fn from(err: SysError) -> Self { + match err { + SysError::IndexOutOfBound => Self::IndexOutOfBound, + SysError::ItemMissing => Self::ItemMissing, + SysError::LengthNotEnough(_) => Self::LengthNotEnough, + SysError::Encoding => Self::Encoding, + SysError::Unknown(_) => Self::Unknown, + } + } +} diff --git a/contracts/ccc-eth-lock/src/main.rs b/contracts/ccc-eth-lock/src/main.rs new file mode 100644 index 0000000..669f724 --- /dev/null +++ b/contracts/ccc-eth-lock/src/main.rs @@ -0,0 +1,22 @@ +#![no_std] +#![no_main] + +mod entry; +mod error; + +use ckb_std::default_alloc; +ckb_std::entry!(program_entry); +default_alloc!(4 * 1024, 1400 * 1024, 64); + +use entry::entry; + +pub fn program_entry() -> i8 { + match entry() { + Ok(_) => 0, + Err(e) => { + let result = e as i8; + assert!(result != 0); + result + } + } +} diff --git a/tests/Cargo.lock b/tests/Cargo.lock index 1a2a82f..0200469 100644 --- a/tests/Cargo.lock +++ b/tests/Cargo.lock @@ -56,6 +56,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -151,6 +157,7 @@ checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" name = "ccc-lock-tests" version = "0.1.0" dependencies = [ + "base64 0.22.1", "blake2b-ref", "ckb-chain-spec", "ckb-crypto", @@ -164,6 +171,7 @@ dependencies = [ "ripemd", "serde_json", "sha2", + "sha3", ] [[package]] @@ -959,6 +967,15 @@ dependencies = [ "signature", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1473,6 +1490,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "signature" version = "2.2.0" @@ -1514,7 +1541,7 @@ version = "9.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082" dependencies = [ - "base64", + "base64 0.21.7", "digest", "hex", "miette", diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 0271213..b88074f 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -8,6 +8,7 @@ version = "0.1.0" edition = "2021" [dependencies] +base64 = "0.22" blake2b-ref = "0.3.1" ckb-chain-spec = "0.116.0" ckb-crypto = "0.116.0" @@ -21,3 +22,4 @@ k256 = "0.13.1" ripemd = "0.1.3" serde_json = "1.0" sha2 = "0.10.8" +sha3 = "0.10.8" diff --git a/tests/src/common.rs b/tests/src/common.rs index c1a3ee1..af72559 100644 --- a/tests/src/common.rs +++ b/tests/src/common.rs @@ -13,9 +13,7 @@ pub fn assert_script_error(err: ckb_error::Error, err_code: i8) { } pub fn blake2b(data: &[u8]) -> [u8; 32] { - let mut blake2b = blake2b_ref::Blake2bBuilder::new(32) - .personal(b"ckb-default-hash") - .build(); + let mut blake2b = blake2b_ref::Blake2bBuilder::new(32).personal(b"ckb-default-hash").build(); let mut hash = [0u8; 32]; blake2b.update(data); blake2b.finalize(&mut hash); @@ -23,12 +21,7 @@ pub fn blake2b(data: &[u8]) -> [u8; 32] { } pub fn println_hex(name: &str, data: &[u8]) { - println!( - "Tester(........): {}(len={}): {}", - name, - data.len(), - hex::encode(data) - ); + println!("Tester(........): {}(len={}): {}", name, data.len(), hex::encode(data)); } pub fn println_log(data: &str) { @@ -37,10 +30,7 @@ pub fn println_log(data: &str) { pub fn println_rtx(tx_resolved: &ckb_types::core::cell::ResolvedTransaction) { let tx_json = ckb_jsonrpc_types::TransactionView::from(tx_resolved.transaction.clone()); - println!( - "Tester(........): {}", - serde_json::to_string_pretty(&tx_json).unwrap() - ); + println!("Tester(........): {}", serde_json::to_string_pretty(&tx_json).unwrap()); } pub fn ripemd160(message: &[u8]) -> [u8; 20] { @@ -63,11 +53,7 @@ pub fn sha256_sha256(msg: &[u8]) -> [u8; 32] { sha256(&sha256(msg)) } -pub fn generate_sighash_all( - tx: &ckb_types::core::TransactionView, - dl: &Resource, - i: usize, -) -> [u8; 32] { +pub fn generate_sighash_all(tx: &ckb_types::core::TransactionView, dl: &Resource, i: usize) -> [u8; 32] { let mut sighash_all_data: Vec = vec![]; sighash_all_data.extend(&tx.hash().raw_data()); let input_major_outpoint = &tx.inputs().get_unchecked(i).previous_output(); @@ -94,10 +80,7 @@ pub fn generate_sighash_all( sighash_all_data.extend(&witness_len.to_le_bytes()); sighash_all_data.extend(&witness.as_bytes()); } - println_log(&format!( - "hashed {} bytes in sighash_all", - sighash_all_data.len() - )); + println_log(&format!("hashed {} bytes in sighash_all", sighash_all_data.len())); let sighash_all = blake2b(&sighash_all_data); println_hex("sighash_all", &sighash_all); sighash_all diff --git a/tests/src/core.rs b/tests/src/core.rs index 5cf93f6..5f2a9d4 100644 --- a/tests/src/core.rs +++ b/tests/src/core.rs @@ -3,27 +3,16 @@ use ckb_types::prelude::{Builder, Entity, Pack}; #[derive(Clone, Default)] pub struct Resource { - pub cell: - std::collections::HashMap, + pub cell: std::collections::HashMap, } impl ckb_traits::CellDataProvider for Resource { - fn get_cell_data( - &self, - out_point: &ckb_types::packed::OutPoint, - ) -> Option { - self.cell - .get(out_point) - .and_then(|cell_meta| cell_meta.mem_cell_data.clone()) + fn get_cell_data(&self, out_point: &ckb_types::packed::OutPoint) -> Option { + self.cell.get(out_point).and_then(|cell_meta| cell_meta.mem_cell_data.clone()) } - fn get_cell_data_hash( - &self, - out_point: &ckb_types::packed::OutPoint, - ) -> Option { - self.cell - .get(out_point) - .and_then(|cell_meta| cell_meta.mem_cell_data_hash.clone()) + fn get_cell_data_hash(&self, out_point: &ckb_types::packed::OutPoint) -> Option { + self.cell.get(out_point).and_then(|cell_meta| cell_meta.mem_cell_data_hash.clone()) } } @@ -34,20 +23,13 @@ impl ckb_traits::HeaderProvider for Resource { } impl ckb_traits::ExtensionProvider for Resource { - fn get_block_extension( - &self, - _: &ckb_types::packed::Byte32, - ) -> Option { + fn get_block_extension(&self, _: &ckb_types::packed::Byte32) -> Option { unimplemented!() } } impl ckb_types::core::cell::CellProvider for Resource { - fn cell( - &self, - out_point: &ckb_types::packed::OutPoint, - eager_load: bool, - ) -> ckb_types::core::cell::CellStatus { + fn cell(&self, out_point: &ckb_types::packed::OutPoint, eager_load: bool) -> ckb_types::core::cell::CellStatus { let _ = eager_load; if let Some(data) = self.cell.get(out_point).cloned() { ckb_types::core::cell::CellStatus::Live(data) @@ -58,10 +40,7 @@ impl ckb_types::core::cell::CellProvider for Resource { } impl ckb_types::core::cell::HeaderChecker for Resource { - fn check_valid( - &self, - _: &ckb_types::packed::Byte32, - ) -> Result<(), ckb_types::core::error::OutPointError> { + fn check_valid(&self, _: &ckb_types::packed::Byte32) -> Result<(), ckb_types::core::error::OutPointError> { Ok(()) } } @@ -70,11 +49,7 @@ impl ckb_types::core::cell::HeaderChecker for Resource { pub struct Verifier {} impl Verifier { - pub fn verify_prior( - &self, - tx_resolved: &ckb_types::core::cell::ResolvedTransaction, - _: &Resource, - ) { + pub fn verify_prior(&self, tx_resolved: &ckb_types::core::cell::ResolvedTransaction, _: &Resource) { let a = tx_resolved.transaction.outputs().item_count(); let b = tx_resolved.transaction.outputs_data().item_count(); assert_eq!(a, b); @@ -90,9 +65,7 @@ impl Verifier { ckb2021: ckb_types::core::hardfork::CKB2021::new_dev_default(), ckb2023: ckb_types::core::hardfork::CKB2023::new_dev_default(), }; - let consensus = ckb_chain_spec::consensus::ConsensusBuilder::default() - .hardfork_switch(hardfork) - .build(); + let consensus = ckb_chain_spec::consensus::ConsensusBuilder::default().hardfork_switch(hardfork).build(); let mut verifier = ckb_script::TransactionScriptsVerifier::new( std::sync::Arc::new(tx_resolved.clone()), dl.clone(), @@ -123,21 +96,21 @@ pub struct Pickaxer { } impl Pickaxer { - pub fn insert_cell_data( - &mut self, - dl: &mut Resource, - data: &[u8], - ) -> ckb_types::core::cell::CellMeta { - let cell_out_point = - ckb_types::packed::OutPoint::new(self.outpoint_hash.clone(), self.outpoint_i); + pub fn insert_cell_data(&mut self, dl: &mut Resource, data: &[u8]) -> ckb_types::core::cell::CellMeta { + let cell_out_point = ckb_types::packed::OutPoint::new(self.outpoint_hash.clone(), self.outpoint_i); + let cell_output_type = ckb_types::packed::Script::new_builder() + .args(self.outpoint_i.to_be_bytes().pack()) + .code_hash(ckb_chain_spec::consensus::TYPE_ID_CODE_HASH.pack()) + .hash_type(ckb_types::core::ScriptHashType::Type.into()) + .build(); let cell_output = ckb_types::packed::CellOutput::new_builder() .capacity(ckb_types::core::Capacity::bytes(0).unwrap().pack()) + .type_(Some(cell_output_type).pack()) .build(); let cell_data = ckb_types::bytes::Bytes::copy_from_slice(data); - let cell_meta = - ckb_types::core::cell::CellMetaBuilder::from_cell_output(cell_output, cell_data) - .out_point(cell_out_point.clone()) - .build(); + let cell_meta = ckb_types::core::cell::CellMetaBuilder::from_cell_output(cell_output, cell_data) + .out_point(cell_out_point.clone()) + .build(); dl.cell.insert(cell_out_point.clone(), cell_meta.clone()); self.outpoint_i += 1; cell_meta @@ -150,41 +123,29 @@ impl Pickaxer { kype: Option, data: &[u8], ) -> ckb_types::core::cell::CellMeta { - let cell_out_point = - ckb_types::packed::OutPoint::new(self.outpoint_hash.clone(), self.outpoint_i); + let cell_out_point = ckb_types::packed::OutPoint::new(self.outpoint_hash.clone(), self.outpoint_i); let cell_output = ckb_types::packed::CellOutput::new_builder() .capacity(ckb_types::core::Capacity::bytes(0).unwrap().pack()) .lock(lock) - .type_( - ckb_types::packed::ScriptOpt::new_builder() - .set(kype) - .build(), - ) + .type_(ckb_types::packed::ScriptOpt::new_builder().set(kype).build()) .build(); let cell_data = ckb_types::bytes::Bytes::copy_from_slice(data); - let cell_meta = - ckb_types::core::cell::CellMetaBuilder::from_cell_output(cell_output, cell_data) - .out_point(cell_out_point.clone()) - .build(); + let cell_meta = ckb_types::core::cell::CellMetaBuilder::from_cell_output(cell_output, cell_data) + .out_point(cell_out_point.clone()) + .build(); dl.cell.insert(cell_out_point.clone(), cell_meta.clone()); self.outpoint_i += 1; cell_meta } - pub fn create_cell_dep( - &self, - cell_meta: &ckb_types::core::cell::CellMeta, - ) -> ckb_types::packed::CellDep { + pub fn create_cell_dep(&self, cell_meta: &ckb_types::core::cell::CellMeta) -> ckb_types::packed::CellDep { ckb_types::packed::CellDep::new_builder() .out_point(cell_meta.out_point.clone()) .dep_type(ckb_types::core::DepType::Code.into()) .build() } - pub fn create_cell_input( - &self, - cell_meta: &ckb_types::core::cell::CellMeta, - ) -> ckb_types::packed::CellInput { + pub fn create_cell_input(&self, cell_meta: &ckb_types::core::cell::CellMeta) -> ckb_types::packed::CellInput { ckb_types::packed::CellInput::new(cell_meta.out_point.clone(), 0) } @@ -196,15 +157,11 @@ impl Pickaxer { ckb_types::packed::CellOutput::new_builder() .capacity(ckb_types::core::Capacity::bytes(0).unwrap().pack()) .lock(lock) - .type_( - ckb_types::packed::ScriptOpt::new_builder() - .set(kype) - .build(), - ) + .type_(ckb_types::packed::ScriptOpt::new_builder().set(kype).build()) .build() } - pub fn create_script( + pub fn create_script_by_data( &self, cell_meta: &ckb_types::core::cell::CellMeta, args: &[u8], @@ -215,4 +172,16 @@ impl Pickaxer { .hash_type(ckb_types::core::ScriptHashType::Data1.into()) .build() } + + pub fn create_script_by_type( + &self, + cell_meta: &ckb_types::core::cell::CellMeta, + args: &[u8], + ) -> ckb_types::packed::Script { + ckb_types::packed::Script::new_builder() + .args(args.pack()) + .code_hash(cell_meta.cell_output.type_().to_opt().unwrap().calc_script_hash()) + .hash_type(ckb_types::core::ScriptHashType::Type.into()) + .build() + } } diff --git a/tests/src/lib.rs b/tests/src/lib.rs index f4eb6e4..10dfedf 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -2,3 +2,5 @@ pub mod common; pub mod core; #[cfg(test)] mod test_btc; +#[cfg(test)] +mod test_eth; diff --git a/tests/src/test_btc.rs b/tests/src/test_btc.rs index e70dfdc..6df3b4c 100644 --- a/tests/src/test_btc.rs +++ b/tests/src/test_btc.rs @@ -1,5 +1,6 @@ use crate::common::{assert_script_error, generate_sighash_all, println_hex, ripemd160_sha256, sha256_sha256}; use crate::core::{Pickaxer, Resource, Verifier}; +use base64::Engine; use ckb_types::prelude::{Builder, Entity, Pack}; static BINARY_CCC_LOCK_BTC: &[u8] = include_bytes!("../../build/release/ccc-btc-lock"); @@ -42,14 +43,15 @@ fn default_tx(dl: &mut Resource, px: &mut Pickaxer) -> ckb_types::core::Transact println_hex("pubkey_hash_expect", &pubkey_hash); // Create cell meta let cell_meta_ccc_lock_btc = px.insert_cell_data(dl, BINARY_CCC_LOCK_BTC); - let cell_meta_i = px.insert_cell_fund(dl, px.create_script(&cell_meta_ccc_lock_btc, &pubkey_hash), None, &[]); + let cell_meta_i = + px.insert_cell_fund(dl, px.create_script_by_type(&cell_meta_ccc_lock_btc, &pubkey_hash), None, &[]); // Create cell dep let tx_builder = tx_builder.cell_dep(px.create_cell_dep(&cell_meta_ccc_lock_btc)); // Create input let tx_builder = tx_builder.input(px.create_cell_input(&cell_meta_i)); // Create output let tx_builder = - tx_builder.output(px.create_cell_output(px.create_script(&cell_meta_ccc_lock_btc, &pubkey_hash), None)); + tx_builder.output(px.create_cell_output(px.create_script_by_type(&cell_meta_ccc_lock_btc, &pubkey_hash), None)); // Create output data let tx_builder = tx_builder.output_data(ckb_types::packed::Bytes::default()); // Create witness @@ -225,3 +227,31 @@ fn test_failure_can_not_recover() { let verifier = Verifier::default(); assert_script_error(verifier.verify(&tx_resolved, &dl).unwrap_err(), 36); } + +#[test] +fn test_e2e() { + let mut dl = Resource::default(); + let mut px = Pickaxer::default(); + let tx = default_tx(&mut dl, &mut px); + + // 1. Install Unisat + // 2. Import account with private key 0x000...0001 + // 3. Open F12 + // 4. Run await unisat.signMessage('Signing a CKB transaction: 0xff934206c421310835b280fd6c9efd98be590f429c2a27a195b + // 9578bde426cd0\n\nIMPORTANT: Please verify the integrity and authenticity of connected BTC wallet before si + // gning this message\n') + let wa = ckb_types::packed::WitnessArgs::new_unchecked(tx.witnesses().get_unchecked(0).raw_data()); + let mut wa_lock = wa.lock().to_opt().unwrap().raw_data().to_vec(); + wa_lock.copy_from_slice( + &base64::prelude::BASE64_STANDARD + .decode("IJIw4RokuCqaS6TBTqJSQWvWJuRRX+0opTmhY6vL88nSOWqULiOXaeZbCtQZJ8lHj3eYoz4+5w9sXrCr5/zfxHA=") + .unwrap(), + ); + let wa = wa.as_builder().lock(Some(ckb_types::bytes::Bytes::from(wa_lock)).pack()).build(); + let tx = tx.as_advanced_builder().set_witnesses(vec![wa.as_bytes().pack()]).build(); + + let tx_resolved = + ckb_types::core::cell::resolve_transaction(tx, &mut std::collections::HashSet::new(), &dl, &dl).unwrap(); + let verifier = Verifier::default(); + verifier.verify(&tx_resolved, &dl).unwrap(); +} diff --git a/tests/src/test_eth.rs b/tests/src/test_eth.rs new file mode 100644 index 0000000..b3d1499 --- /dev/null +++ b/tests/src/test_eth.rs @@ -0,0 +1,237 @@ +use crate::common::{assert_script_error, generate_sighash_all, println_hex}; +use crate::core::{Pickaxer, Resource, Verifier}; +use ckb_types::prelude::{Builder, Entity, Pack}; +use sha3::Digest; + +static BINARY_CCC_LOCK_ETH: &[u8] = include_bytes!("../../build/release/ccc-eth-lock"); + +fn keccak(msg: &[u8]) -> [u8; 32] { + let mut hasher = sha3::Keccak256::new(); + hasher.update(msg); + hasher.finalize().into() +} + +fn keccak160(msg: &[u8]) -> [u8; 20] { + let mut output = [0u8; 20]; + output.copy_from_slice(&keccak(msg)[12..]); + output +} + +fn message_hash(msg: &str) -> [u8; 32] { + // Only 32-bytes hex representation of the hash is allowed. + assert_eq!(msg.len(), 64); + // Text used to signify that a signed message follows and to prevent inadvertently signing a transaction. + const CKB_PREFIX: &str = "Signing a CKB transaction: 0x"; + const CKB_SUFFIX: &str = "\n\nIMPORTANT: Please verify the integrity and authenticity of connected Ethereum wallet before signing this message\n"; + const ETH_PREFIX: &str = "Ethereum Signed Message:\n"; + let mut data: Vec = Vec::new(); + assert_eq!(ETH_PREFIX.len(), 25); + data.push(25); + data.extend(ETH_PREFIX.as_bytes()); + data.extend(format!("{}", (CKB_PREFIX.len() + msg.len() + CKB_SUFFIX.len()) as u8).as_bytes()); + data.extend(CKB_PREFIX.as_bytes()); + data.extend(msg.as_bytes()); + data.extend(CKB_SUFFIX.as_bytes()); + keccak(&data) +} + +fn message_sign(msg: &str, prikey: k256::ecdsa::SigningKey) -> [u8; 65] { + let m = message_hash(msg); + let sigrec = prikey.sign_prehash_recoverable(&m).unwrap(); + if sigrec.1.to_byte() > 2 { + return message_sign(msg, prikey); + } + let mut r = [0u8; 65]; + r[..64].copy_from_slice(&sigrec.0.normalize_s().unwrap_or(sigrec.0).to_bytes()); + r[64] = 27 + sigrec.1.to_byte(); + r +} + +fn default_tx(dl: &mut Resource, px: &mut Pickaxer) -> ckb_types::core::TransactionView { + let tx_builder = ckb_types::core::TransactionBuilder::default(); + // Create prior knowledge + let prikey_byte: [u8; 32] = + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; + let prikey = k256::ecdsa::SigningKey::from_slice(&prikey_byte).unwrap(); + let pubkey = prikey.verifying_key(); + let pubkey_hash = keccak160(&pubkey.to_encoded_point(false).to_bytes()[1..]); + println_hex("pubkey_hash_expect", &pubkey_hash); + // Create cell meta + let cell_meta_ccc_lock_btc = px.insert_cell_data(dl, BINARY_CCC_LOCK_ETH); + let cell_meta_i = + px.insert_cell_fund(dl, px.create_script_by_type(&cell_meta_ccc_lock_btc, &pubkey_hash), None, &[]); + // Create cell dep + let tx_builder = tx_builder.cell_dep(px.create_cell_dep(&cell_meta_ccc_lock_btc)); + // Create input + let tx_builder = tx_builder.input(px.create_cell_input(&cell_meta_i)); + // Create output + let tx_builder = + tx_builder.output(px.create_cell_output(px.create_script_by_type(&cell_meta_ccc_lock_btc, &pubkey_hash), None)); + // Create output data + let tx_builder = tx_builder.output_data(ckb_types::packed::Bytes::default()); + // Create witness + let tx_builder = tx_builder.set_witnesses(vec![ckb_types::packed::WitnessArgs::new_builder() + .lock(Some(ckb_types::bytes::Bytes::from(vec![0u8; 65])).pack()) + .build() + .as_bytes() + .pack()]); + let sighash_all = generate_sighash_all(&tx_builder.clone().build(), &dl, 0); + let sighash_all_hex = hex::encode(&sighash_all); + let sig = message_sign(&sighash_all_hex, prikey); + let tx_builder = tx_builder.set_witnesses(vec![ckb_types::packed::WitnessArgs::new_builder() + .lock(Some(ckb_types::bytes::Bytes::copy_from_slice(&sig)).pack()) + .build() + .as_bytes() + .pack()]); + tx_builder.build() +} + +#[test] +fn test_success() { + let mut dl = Resource::default(); + let mut px = Pickaxer::default(); + let tx = default_tx(&mut dl, &mut px); + let tx_resolved = + ckb_types::core::cell::resolve_transaction(tx, &mut std::collections::HashSet::new(), &dl, &dl).unwrap(); + let verifier = Verifier::default(); + verifier.verify(&tx_resolved, &dl).unwrap(); +} + +#[test] +fn test_failure_witness_args() { + let mut dl = Resource::default(); + let mut px = Pickaxer::default(); + let tx = default_tx(&mut dl, &mut px); + + let wa = ckb_types::packed::WitnessArgs::new_unchecked(tx.witnesses().get_unchecked(0).raw_data()); + let wa = wa.as_builder().lock(ckb_types::packed::BytesOpt::new_builder().set(None).build()).build(); + let tx = tx.as_advanced_builder().set_witnesses(vec![wa.as_bytes().pack()]).build(); + + let tx_resolved = + ckb_types::core::cell::resolve_transaction(tx, &mut std::collections::HashSet::new(), &dl, &dl).unwrap(); + let verifier = Verifier::default(); + assert_script_error(verifier.verify(&tx_resolved, &dl).unwrap_err(), 31); +} + +#[test] +fn test_failure_wrong_pubkey_hash() { + let mut dl = Resource::default(); + let mut px = Pickaxer::default(); + let tx = default_tx(&mut dl, &mut px); + + let input_outpoint = tx.inputs().get_unchecked(0).previous_output(); + let input_meta = dl.cell.get_mut(&input_outpoint).unwrap(); + let input_cell_output = &input_meta.cell_output; + let input_cell_output_script = input_cell_output.lock(); + let input_cell_output_script = input_cell_output_script.as_builder().args(vec![0u8; 19].pack()).build(); + let input_cell_output = input_cell_output.clone().as_builder().lock(input_cell_output_script).build(); + input_meta.cell_output = input_cell_output; + + let tx_resolved = + ckb_types::core::cell::resolve_transaction(tx, &mut std::collections::HashSet::new(), &dl, &dl).unwrap(); + let verifier = Verifier::default(); + assert_script_error(verifier.verify(&tx_resolved, &dl).unwrap_err(), 32); +} + +#[test] +fn test_failure_pubkey_hash_mismatched() { + let mut dl = Resource::default(); + let mut px = Pickaxer::default(); + let tx = default_tx(&mut dl, &mut px); + + let input_outpoint = tx.inputs().get_unchecked(0).previous_output(); + let input_meta = dl.cell.get_mut(&input_outpoint).unwrap(); + let input_cell_output = &input_meta.cell_output; + let input_cell_output_script = input_cell_output.lock(); + let input_cell_output_script = input_cell_output_script.as_builder().args(vec![0u8; 20].pack()).build(); + let input_cell_output = input_cell_output.clone().as_builder().lock(input_cell_output_script).build(); + input_meta.cell_output = input_cell_output; + + let tx_resolved = + ckb_types::core::cell::resolve_transaction(tx, &mut std::collections::HashSet::new(), &dl, &dl).unwrap(); + let verifier = Verifier::default(); + assert_script_error(verifier.verify(&tx_resolved, &dl).unwrap_err(), 33); +} + +#[test] +fn test_failure_sig_format() { + let mut dl = Resource::default(); + let mut px = Pickaxer::default(); + let tx = default_tx(&mut dl, &mut px); + + let wa = ckb_types::packed::WitnessArgs::new_unchecked(tx.witnesses().get_unchecked(0).raw_data()); + let mut wa_lock = wa.lock().to_opt().unwrap().raw_data().to_vec(); + wa_lock[0x20..0x40].copy_from_slice(&vec![0u8; 32]); + let wa = wa.as_builder().lock(Some(ckb_types::bytes::Bytes::from(wa_lock)).pack()).build(); + let tx = tx.as_advanced_builder().set_witnesses(vec![wa.as_bytes().pack()]).build(); + + let tx_resolved = + ckb_types::core::cell::resolve_transaction(tx, &mut std::collections::HashSet::new(), &dl, &dl).unwrap(); + let verifier = Verifier::default(); + assert_script_error(verifier.verify(&tx_resolved, &dl).unwrap_err(), 34); +} + +#[test] +fn test_failure_recid() { + let mut dl = Resource::default(); + let mut px = Pickaxer::default(); + let tx = default_tx(&mut dl, &mut px); + + let wa = ckb_types::packed::WitnessArgs::new_unchecked(tx.witnesses().get_unchecked(0).raw_data()); + let mut wa_lock = wa.lock().to_opt().unwrap().raw_data().to_vec(); + wa_lock[64] = 4; + let wa = wa.as_builder().lock(Some(ckb_types::bytes::Bytes::from(wa_lock)).pack()).build(); + let tx = tx.as_advanced_builder().set_witnesses(vec![wa.as_bytes().pack()]).build(); + + let tx_resolved = + ckb_types::core::cell::resolve_transaction(tx, &mut std::collections::HashSet::new(), &dl, &dl).unwrap(); + let verifier = Verifier::default(); + assert_script_error(verifier.verify(&tx_resolved, &dl).unwrap_err(), 35); +} + +#[test] +fn test_failure_sig_use_high_s() { + let mut dl = Resource::default(); + let mut px = Pickaxer::default(); + let tx = default_tx(&mut dl, &mut px); + + let wa = ckb_types::packed::WitnessArgs::new_unchecked(tx.witnesses().get_unchecked(0).raw_data()); + let mut wa_lock = wa.lock().to_opt().unwrap().raw_data().to_vec(); + let l_s = k256::NonZeroScalar::try_from(&wa_lock[32..64]).unwrap(); + let h_s = -l_s; + wa_lock[32..64].copy_from_slice(&h_s.to_bytes().as_slice()); + let wa = wa.as_builder().lock(Some(ckb_types::bytes::Bytes::from(wa_lock)).pack()).build(); + let tx = tx.as_advanced_builder().set_witnesses(vec![wa.as_bytes().pack()]).build(); + + let tx_resolved = + ckb_types::core::cell::resolve_transaction(tx, &mut std::collections::HashSet::new(), &dl, &dl).unwrap(); + let verifier = Verifier::default(); + assert_script_error(verifier.verify(&tx_resolved, &dl).unwrap_err(), 37); +} + +#[test] +fn test_e2e() { + let mut dl = Resource::default(); + let mut px = Pickaxer::default(); + let tx = default_tx(&mut dl, &mut px); + + // 1. Install Metamask + // 2. Import account with private key 0x000...0001 + // 3. Open F12 + // 4. Run await ethereum.enable() + // 5. Run await ethereum.send('personal_sign', ['5369676e696e67206120434b42207472616e73616374696f6e3a203078363665306 + // 4383366303062633332336363316665316530383336653038616234363838653036646537353164366534383133633537383738326 + // 66565363032370a0a494d504f5254414e543a20506c65617365207665726966792074686520696e7465677269747920616e6420617 + // 57468656e746963697479206f6620636f6e6e656374656420457468657265756d2077616c6c6574206265666f7265207369676e696 + // e672074686973206d6573736167650a', '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf']) + let wa = ckb_types::packed::WitnessArgs::new_unchecked(tx.witnesses().get_unchecked(0).raw_data()); + let mut wa_lock = wa.lock().to_opt().unwrap().raw_data().to_vec(); + wa_lock.copy_from_slice(&hex::decode("2291abe57fc51d83a90b3002c3b1994393a56a3cbdfd54a0fd1ece34971607b020eb1c750dbd1f159c631681e7cf1d6e97a0929299b039d6e93a9d7170b6440d1b").unwrap()); + let wa = wa.as_builder().lock(Some(ckb_types::bytes::Bytes::from(wa_lock)).pack()).build(); + let tx = tx.as_advanced_builder().set_witnesses(vec![wa.as_bytes().pack()]).build(); + + let tx_resolved = + ckb_types::core::cell::resolve_transaction(tx, &mut std::collections::HashSet::new(), &dl, &dl).unwrap(); + let verifier = Verifier::default(); + verifier.verify(&tx_resolved, &dl).unwrap(); +}