Skip to content

Commit

Permalink
Implement ethereum lock
Browse files Browse the repository at this point in the history
  • Loading branch information
mohanson committed Aug 1, 2024
1 parent dfeb2c5 commit 909ac18
Show file tree
Hide file tree
Showing 17 changed files with 625 additions and 98 deletions.
30 changes: 30 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
Expand Down
1 change: 1 addition & 0 deletions checksums.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
3d659b15f2aad5f9350f55ce471806c6d6ad4f51a555a82b7918e9d88f84f04a build/release/ccc-btc-lock
4ae08bd7ed954997dcbca5ff88700bf7f949b1080c2bd1cb024f15c8b0436396 build/release/ccc-eth-lock
2 changes: 2 additions & 0 deletions contracts/ccc-eth-lock/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/build
/target
11 changes: 11 additions & 0 deletions contracts/ccc-eth-lock/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
77 changes: 77 additions & 0 deletions contracts/ccc-eth-lock/Makefile
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions contracts/ccc-eth-lock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ccc-eth-lock

CCC ETH lock implementation. See [specification](../../docs/eth.md) for more information.
87 changes: 87 additions & 0 deletions contracts/ccc-eth-lock/src/entry.rs
Original file line number Diff line number Diff line change
@@ -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<u8> = 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(())
}
43 changes: 43 additions & 0 deletions contracts/ccc-eth-lock/src/error.rs
Original file line number Diff line number Diff line change
@@ -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<HelperError> 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<SysError> 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,
}
}
}
22 changes: 22 additions & 0 deletions contracts/ccc-eth-lock/src/main.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
}
29 changes: 28 additions & 1 deletion tests/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -21,3 +22,4 @@ k256 = "0.13.1"
ripemd = "0.1.3"
serde_json = "1.0"
sha2 = "0.10.8"
sha3 = "0.10.8"
Loading

0 comments on commit 909ac18

Please sign in to comment.