diff --git a/Cargo.lock b/Cargo.lock index 03be9d7..d963999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,16 @@ dependencies = [ "sha3", ] +[[package]] +name = "ccc-sol-lock" +version = "0.1.0" +dependencies = [ + "ckb-lock-helper", + "ckb-std", + "ed25519-dalek", + "hex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -149,6 +159,32 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "der" version = "0.7.9" @@ -185,6 +221,27 @@ dependencies = [ "spki", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "subtle", +] + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -214,6 +271,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "gcd" version = "2.3.0" @@ -303,6 +366,24 @@ dependencies = [ "spki", ] +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -328,6 +409,15 @@ dependencies = [ "digest", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "sec1" version = "0.7.3" @@ -342,6 +432,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "sha2" version = "0.10.8" @@ -389,12 +485,29 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 7d7f938..a5d5ddc 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-sol-lock", "contracts/ccc-eth-lock", "contracts/ccc-btc-lock", "crates/ckb-lock-helper" diff --git a/checksums.txt b/checksums.txt index 4bb3a3e..a3e78d3 100644 --- a/checksums.txt +++ b/checksums.txt @@ -1,2 +1,3 @@ 3d659b15f2aad5f9350f55ce471806c6d6ad4f51a555a82b7918e9d88f84f04a build/release/ccc-btc-lock 4ae08bd7ed954997dcbca5ff88700bf7f949b1080c2bd1cb024f15c8b0436396 build/release/ccc-eth-lock +66bbb7041a10a0b2a2fd51ae2aa9394e9f7ee6e8b2b32dd5d3e4d37c0d4a64b8 build/release/ccc-sol-lock diff --git a/contracts/ccc-sol-lock/.gitignore b/contracts/ccc-sol-lock/.gitignore new file mode 100644 index 0000000..c3dca1b --- /dev/null +++ b/contracts/ccc-sol-lock/.gitignore @@ -0,0 +1,2 @@ +/build +/target diff --git a/contracts/ccc-sol-lock/Cargo.toml b/contracts/ccc-sol-lock/Cargo.toml new file mode 100644 index 0000000..4673f8c --- /dev/null +++ b/contracts/ccc-sol-lock/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ccc-sol-lock" +version = "0.1.0" +edition = "2021" + +[dependencies] +ckb-std = "0.15" +ckb-lock-helper = { path = "../../crates/ckb-lock-helper" } +ed25519-dalek = { version = "2.1.1", default-features = false } +hex = { version = "0.4", default-features = false, features = ["alloc"] } diff --git a/contracts/ccc-sol-lock/Makefile b/contracts/ccc-sol-lock/Makefile new file mode 100644 index 0000000..579f431 --- /dev/null +++ b/contracts/ccc-sol-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-sol-lock/README.md b/contracts/ccc-sol-lock/README.md new file mode 100644 index 0000000..bb6946c --- /dev/null +++ b/contracts/ccc-sol-lock/README.md @@ -0,0 +1,3 @@ +# ccc-sol-lock + +CCC SOL lock implementation. See [specification](../../docs/sol.md) for more information. diff --git a/contracts/ccc-sol-lock/src/entry.rs b/contracts/ccc-sol-lock/src/entry.rs new file mode 100644 index 0000000..f0f1b00 --- /dev/null +++ b/contracts/ccc-sol-lock/src/entry.rs @@ -0,0 +1,50 @@ +use crate::error::Error; +use alloc::string::String; +use ckb_lock_helper::{blake2b::blake160, generate_sighash_all}; +use ckb_std::{ + ckb_constants::Source, + high_level::{load_script, load_witness_args}, +}; +use ed25519_dalek::{Signature, Verifier, VerifyingKey, PUBLIC_KEY_LENGTH}; + +fn message_wrap(msg: &str) -> String { + // 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 Solana wallet before signing this message\n"; + [CKB_PREFIX, msg, CKB_SUFFIX].join("") +} + +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::WrongPubkey); + } + let sighash_all = generate_sighash_all()?; + let sighash_all_hex = hex::encode(&sighash_all); + let msg = message_wrap(&sighash_all_hex); + let witness_args = load_witness_args(0, Source::GroupInput)?; + let witness_args_lock = witness_args + .lock() + .to_opt() + .ok_or(Error::WrongSignatureFormat)? + .raw_data(); + if witness_args_lock.len() != 96 { + return Err(Error::WrongSignatureFormat); + } + let sig = + Signature::from_slice(&witness_args_lock[..64]).map_err(|_| Error::WrongSignatureFormat)?; + let mut pubkey = [0u8; PUBLIC_KEY_LENGTH]; + pubkey.copy_from_slice(&witness_args_lock[64..]); + let pubkey_hash_result = blake160(&pubkey); + if pubkey_hash_result.as_ref() != pubkey_hash_expect.as_ref() { + return Err(Error::WrongPubkey); + } + let pubkey = VerifyingKey::from_bytes(&pubkey).map_err(|_| Error::WrongPubkey)?; + pubkey + .verify(msg.as_bytes(), &sig) + .map_err(|_| Error::WrongSignature)?; + Ok(()) +} diff --git a/contracts/ccc-sol-lock/src/error.rs b/contracts/ccc-sol-lock/src/error.rs new file mode 100644 index 0000000..1e1e56d --- /dev/null +++ b/contracts/ccc-sol-lock/src/error.rs @@ -0,0 +1,40 @@ +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, + WrongPubkey, + WrongSignatureFormat, + WrongSignature, +} + +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-sol-lock/src/main.rs b/contracts/ccc-sol-lock/src/main.rs new file mode 100644 index 0000000..669f724 --- /dev/null +++ b/contracts/ccc-sol-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/docs/sol.md b/docs/sol.md new file mode 100644 index 0000000..d7b05fc --- /dev/null +++ b/docs/sol.md @@ -0,0 +1,80 @@ +# CCC Solana Lock Specification + +This specification describes a CCC lock script that can interoperate with the Solana blockchain. Some common designs, definitions, and conventions can be found in the [overview](./overview.md). + +## Lock Script + +A CCC Solana lock script has following structure: + +``` +Code hash: CCC Solana lock script code hash +Hash type: CCC Solana lock script hash type +Args: +``` + +Ed25519 pubkey hash is calculated via blake160 over Ed25519 pubkey (32 bytes). The ed25519 pubkey can be also decoded from an Solana address by base58 decoding. + +## Witness + +The corresponding witness must be a proper `WitnessArgs` data structure in molecule format. In the lock field of the WitnessArgs, a 64 bytes ed25519 signature and a 32 bytes ed25519 pubkey must be present, totaling 96 bytes. + +## Unlocking Process + +Ed25519 messages can be of any length and does not require hashing. Specifically, for the CCC Solana lock, the message is: + +"Signing a CKB transaction: 0x{sigh_hash}\n\nIMPORTANT: Please verify the integrity and authenticity of connected Solana wallet before signing this message\n" + +The `{sighasl_all}` is replaced by `sighash_all` in hexadecimal string, with length 64. The string in the last part can be displayed in wallet UIs. + +After verifying that the pubkey and pubkey hash are consistent, for the ed25519 message, signature, and pubkey, the ed25519 verify function is used. If the verification passes, the signature is successfully verified. + +## Examples + +```yaml +CellDeps: + CCC Solana lock script cell +Inputs: + Cell + Data: <...> + Type: <...> + Lock: + code_hash: + args: +Outputs: + Any cell +Witnesses: + WitnessArgs + Lock: + +``` + + + +## Notes + +An implementation of the lock script spec above has been deployed to CKB mainnet and testnet: + +- mainnet + +| parameter | value | +| ----------- | -------------------------------------------------------------------- | +| `code_hash` | TODO | +| `hash_type` | `type` | +| `tx_hash` | TODO | +| `index` | `0x0` | +| `dep_type` | `code` | + +- testnet + +| parameter | value | +| ----------- | -------------------------------------------------------------------- | +| `code_hash` | TODO | +| `hash_type` | `type` | +| `tx_hash` | TODO | +| `index` | `0x0` | +| `dep_type` | `code` | + +Reproducible build is supported to verify the deployed script. To build the deployed script above, one can use the following steps: + +```bash +TODO +``` diff --git a/tests/Cargo.lock b/tests/Cargo.lock index 0200469..d5302e3 100644 --- a/tests/Cargo.lock +++ b/tests/Cargo.lock @@ -166,6 +166,7 @@ dependencies = [ "ckb-script", "ckb-traits", "ckb-types", + "ed25519-dalek", "hex", "k256", "ripemd", @@ -604,6 +605,33 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + [[package]] name = "der" version = "0.7.9" @@ -665,6 +693,30 @@ dependencies = [ "spki", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.13.0" @@ -722,6 +774,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.0.30" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index b88074f..7dd5492 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -17,6 +17,7 @@ ckb-jsonrpc-types = "0.116.0" ckb-script = "0.116.0" ckb-traits = "0.116.0" ckb-types = "0.116.0" +ed25519-dalek = "2.1.1" hex = "0.4" k256 = "0.13.1" ripemd = "0.1.3" diff --git a/tests/src/common.rs b/tests/src/common.rs index af72559..9ed3d67 100644 --- a/tests/src/common.rs +++ b/tests/src/common.rs @@ -12,6 +12,12 @@ pub fn assert_script_error(err: ckb_error::Error, err_code: i8) { ); } +pub fn blake160(data: &[u8]) -> [u8; 20] { + let mut r = [0u8; 20]; + r.copy_from_slice(&blake2b(data)[..20]); + r +} + pub fn blake2b(data: &[u8]) -> [u8; 32] { let mut blake2b = blake2b_ref::Blake2bBuilder::new(32).personal(b"ckb-default-hash").build(); let mut hash = [0u8; 32]; diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 10dfedf..7d49be1 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -4,3 +4,5 @@ pub mod core; mod test_btc; #[cfg(test)] mod test_eth; +#[cfg(test)] +mod test_sol; diff --git a/tests/src/test_btc.rs b/tests/src/test_btc.rs index 6df3b4c..aa3f808 100644 --- a/tests/src/test_btc.rs +++ b/tests/src/test_btc.rs @@ -23,7 +23,7 @@ fn message_hash(msg: &str) -> [u8; 32] { sha256_sha256(&data) } -fn message_sign(msg: &str, prikey: k256::ecdsa::SigningKey) -> [u8; 65] { +fn message_sign(msg: &str, prikey: &k256::ecdsa::SigningKey) -> [u8; 65] { let m = message_hash(msg); let sigrec = prikey.sign_prehash_recoverable(&m).unwrap(); let mut r = [0u8; 65]; @@ -62,7 +62,7 @@ fn default_tx(dl: &mut Resource, px: &mut Pickaxer) -> ckb_types::core::Transact .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 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() @@ -229,13 +229,13 @@ fn test_failure_can_not_recover() { } #[test] -fn test_e2e() { +fn test_success_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 + // 2. Import account with private key 0x0000000000000000000000000000000000000000000000000000000000000001 // 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 diff --git a/tests/src/test_eth.rs b/tests/src/test_eth.rs index b3d1499..05edbff 100644 --- a/tests/src/test_eth.rs +++ b/tests/src/test_eth.rs @@ -35,7 +35,7 @@ fn message_hash(msg: &str) -> [u8; 32] { keccak(&data) } -fn message_sign(msg: &str, prikey: k256::ecdsa::SigningKey) -> [u8; 65] { +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 { @@ -57,16 +57,16 @@ fn default_tx(dl: &mut Resource, px: &mut Pickaxer) -> ckb_types::core::Transact 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_ccc_lock_eth = 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, &[]); + px.insert_cell_fund(dl, px.create_script_by_type(&cell_meta_ccc_lock_eth, &pubkey_hash), None, &[]); // Create cell dep - let tx_builder = tx_builder.cell_dep(px.create_cell_dep(&cell_meta_ccc_lock_btc)); + let tx_builder = tx_builder.cell_dep(px.create_cell_dep(&cell_meta_ccc_lock_eth)); // 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)); + tx_builder.output(px.create_cell_output(px.create_script_by_type(&cell_meta_ccc_lock_eth, &pubkey_hash), None)); // Create output data let tx_builder = tx_builder.output_data(ckb_types::packed::Bytes::default()); // Create witness @@ -77,7 +77,7 @@ fn default_tx(dl: &mut Resource, px: &mut Pickaxer) -> ckb_types::core::Transact .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 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() @@ -210,13 +210,13 @@ fn test_failure_sig_use_high_s() { } #[test] -fn test_e2e() { +fn test_success_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 + // 2. Import account with private key 0x0000000000000000000000000000000000000000000000000000000000000001 // 3. Open F12 // 4. Run await ethereum.enable() // 5. Run await ethereum.send('personal_sign', ['5369676e696e67206120434b42207472616e73616374696f6e3a203078363665306 diff --git a/tests/src/test_sol.rs b/tests/src/test_sol.rs new file mode 100644 index 0000000..a26ab7d --- /dev/null +++ b/tests/src/test_sol.rs @@ -0,0 +1,179 @@ +use crate::common::{assert_script_error, blake160, generate_sighash_all}; +use crate::core::{Pickaxer, Resource, Verifier}; +use ckb_types::prelude::{Builder, Entity, Pack}; +use k256::ecdsa::signature::SignerMut; + +static BINARY_CCC_LOCK_SOL: &[u8] = include_bytes!("../../build/release/ccc-sol-lock"); + +fn message_wrap(msg: &str) -> String { + // 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 Solana wallet before signing this message\n"; + [CKB_PREFIX, msg, CKB_SUFFIX].join("") +} + +fn message_sign(msg: &str, prikey: &mut ed25519_dalek::SigningKey) -> [u8; 64] { + let msg = message_wrap(msg); + prikey.sign(msg.as_bytes()).to_bytes() +} + +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 mut prikey = ed25519_dalek::SigningKey::from_bytes(&prikey_byte); + let pubkey = prikey.verifying_key(); + let pubkey_byte = pubkey.to_bytes(); + let pubkey_hash = blake160(&pubkey_byte); + // Create cell meta + let cell_meta_ccc_lock_sol = px.insert_cell_data(dl, BINARY_CCC_LOCK_SOL); + let cell_meta_i = + px.insert_cell_fund(dl, px.create_script_by_type(&cell_meta_ccc_lock_sol, &pubkey_hash), None, &[]); + // Create cell dep + let tx_builder = tx_builder.cell_dep(px.create_cell_dep(&cell_meta_ccc_lock_sol)); + // 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_sol, &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; 96])).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 mut sig = [0u8; 96]; + sig[..64].copy_from_slice(&message_sign(&sighash_all_hex, &mut prikey)); + sig[64..].copy_from_slice(&pubkey_byte); + 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_wrong_pubkey_hash_length() { + 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_args = input_cell_output_script.args().as_bytes(); + let input_cell_output_script = + input_cell_output_script.as_builder().args(input_cell_output_script_args[..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_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; 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(), 32); +} + +#[test] +fn test_failure_wrong_signature_length() { + 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.pop(); + 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(), 33); +} + +#[test] +fn test_failure_wrong_signature() { + 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].copy_from_slice(&[0u8; 64]); + 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_success_e2e() { + let mut dl = Resource::default(); + let mut px = Pickaxer::default(); + let tx = default_tx(&mut dl, &mut px); + + // 1. Install Phantom + // 2. Import account with private key AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFMtav2rXn79au8yvzCadhc0mUe1LiFtYafJBrt8KW6KQ== + // 3. Open F12 + // 4. Run msg = new TextEncoder().encode('Signing a CKB transaction: 0xa5505c5d5261287569fdd26f4061ba7b3ec9bf1ef1baf + // 6a43426ec115d625d37\n\nIMPORTANT: Please verify the integrity and authenticity of connected Solana wallet + // before signing this message\n'); + // 5. Run sig = await phantom.solana.signMessage(msg, 'utf8'); + // 6. Run sig.signature.toString('hex') + + 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].copy_from_slice( + &hex::decode("be179ec911d03817a14b871d5efc3b162651f644c252e16e1e97c1848ccc53784f5205d8aa2ce79774f877330e857cf78375dbdd377dfffb31405329a16dd101").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(); +}