From 5d260a91191c74187e5340fb9f725bd9bdc1ff07 Mon Sep 17 00:00:00 2001 From: mohanson Date: Fri, 26 Jul 2024 11:49:33 +0800 Subject: [PATCH] Implement sol lock --- Cargo.lock | 113 ++++++++++++++++++ Cargo.toml | 1 + checksums.txt | 1 + contracts/ccc-sol-lock/.gitignore | 2 + contracts/ccc-sol-lock/Cargo.toml | 10 ++ contracts/ccc-sol-lock/Makefile | 77 ++++++++++++ contracts/ccc-sol-lock/README.md | 3 + contracts/ccc-sol-lock/src/entry.rs | 44 +++++++ contracts/ccc-sol-lock/src/error.rs | 40 +++++++ contracts/ccc-sol-lock/src/main.rs | 22 ++++ docs/sol.md | 80 +++++++++++++ tests/Cargo.lock | 58 ++++++++++ tests/Cargo.toml | 1 + tests/src/lib.rs | 2 + tests/src/test_btc.rs | 8 +- tests/src/test_eth.rs | 16 +-- tests/src/test_sol.rs | 174 ++++++++++++++++++++++++++++ 17 files changed, 640 insertions(+), 12 deletions(-) create mode 100644 contracts/ccc-sol-lock/.gitignore create mode 100644 contracts/ccc-sol-lock/Cargo.toml create mode 100644 contracts/ccc-sol-lock/Makefile create mode 100644 contracts/ccc-sol-lock/README.md create mode 100644 contracts/ccc-sol-lock/src/entry.rs create mode 100644 contracts/ccc-sol-lock/src/error.rs create mode 100644 contracts/ccc-sol-lock/src/main.rs create mode 100644 docs/sol.md create mode 100644 tests/src/test_sol.rs 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..3aa67c3 100644 --- a/checksums.txt +++ b/checksums.txt @@ -1,2 +1,3 @@ 3d659b15f2aad5f9350f55ce471806c6d6ad4f51a555a82b7918e9d88f84f04a build/release/ccc-btc-lock 4ae08bd7ed954997dcbca5ff88700bf7f949b1080c2bd1cb024f15c8b0436396 build/release/ccc-eth-lock +7767835a576fc20b10f5528e42141a84650d61b779cb60aaf31b5db68b7a4dfc 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..44207f8 --- /dev/null +++ b/contracts/ccc-sol-lock/src/entry.rs @@ -0,0 +1,44 @@ +use crate::error::Error; +use alloc::string::String; +use ckb_lock_helper::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 = script.args().raw_data(); + if pubkey.len() != PUBLIC_KEY_LENGTH { + return Err(Error::WrongPubkey); + } + let pubkey: [u8; PUBLIC_KEY_LENGTH] = pubkey.to_vec().try_into().unwrap(); + let pubkey = VerifyingKey::from_bytes(&pubkey).map_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() != 64 { + return Err(Error::WrongSignatureFormat); + } + let sig = Signature::from_slice(&witness_args_lock).map_err(|_| Error::WrongSignatureFormat)?; + 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..3762c83 --- /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: +``` + +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 must be present. + +## 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. + +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/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..0be43ad --- /dev/null +++ b/tests/src/test_sol.rs @@ -0,0 +1,174 @@ +use crate::common::{assert_script_error, 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(); + // 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_byte), 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_byte), 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; 64])).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, &mut 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_wrong_pubkey_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[..31].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() { + 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; 32].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(), 34); +} + +#[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 wa = wa.as_builder().lock(Some(vec![0u8; 64].pack()).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: 0x62b23cebc0c020f433f97cb6f018883b793f5e9777db4 + // d34736a0c5df004ead5\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.copy_from_slice( + &hex::decode("df1fc2d71c4a5d4a74ea3673a0b85fc0e683e48bc8b4cc343f7656c404a5aa315c19df47fc8a414b2be8a41c315d0424a3e0f56554fd5030de3a1ed6706b7509").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(); +}