diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..d162d3d1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "deps/quantum-resistant-lock-script"] + path = deps/quantum-resistant-lock-script + url = git@github.com:cryptape/quantum-resistant-lock-script.git + branch = main diff --git a/Cargo.toml b/Cargo.toml index bee535b0..22b458e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,3 +56,6 @@ clap = { version = "4.1.8", features = ["derive"] } httpmock = "0.6" async-global-executor = "2.3.1" hex = "0.4" + +[build-dependencies] +cc = "1.0" diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..afe56fc9 --- /dev/null +++ b/build.rs @@ -0,0 +1,65 @@ +use std::path::PathBuf; + +fn get_hash_size() -> usize { + 128 +} + +fn get_hash_option() -> &'static str { + "f" +} + +fn get_hash_info() -> (&'static str, Vec<&'static str>) { + let thash_file = "../deps/sphincsplus/ref/thash_shake_simple.c"; + + ( + "shake", + vec![ + "../deps/sphincsplus/ref/fips202.c", + "../deps/sphincsplus/ref/hash_shake.c", + thash_file, + ], + ) +} + +fn main() { + let mut source_list = vec![ + "../deps/sphincsplus/ref/address.c", + "../deps/sphincsplus/ref/merkle.c", + "../deps/sphincsplus/ref/wots.c", + "../deps/sphincsplus/ref/wotsx1.c", + "../deps/sphincsplus/ref/utils.c", + "../deps/sphincsplus/ref/utilsx1.c", + "../deps/sphincsplus/ref/fors.c", + "../deps/sphincsplus/ref/sign.c", + "../deps/sphincsplus/ref/randombytes.c", + "ckb-sphincsplus.c", + ]; + + let (hash_name, mut hash_src_files) = get_hash_info(); + + source_list.append(&mut hash_src_files); + let define_param = format!( + "sphincs-{}-{}{}", + hash_name, + get_hash_size(), + get_hash_option() + ); + + let c_src_dir = PathBuf::from("deps/quantum-resistant-lock-script/c/"); + + let mut builder = cc::Build::new(); + builder.define("PARAMS", define_param.as_str()); + builder.include(&c_src_dir); + builder.include( + &c_src_dir + .join("..") + .join("deps") + .join("sphincsplus") + .join("ref"), + ); + + for source in source_list { + builder.file(c_src_dir.join(source)); + } + builder.compile("sphincsplus"); +} diff --git a/check-cargotoml.sh b/check-cargotoml.sh index 483aa01c..a62a7f43 100755 --- a/check-cargotoml.sh +++ b/check-cargotoml.sh @@ -25,7 +25,7 @@ esac function check_package_name() { local regex_to_cut_pkgname='s/^\[\(package\)\]\nname\(\|[ ]\+\)=\(\|[ ]\+\)"\(.\+\)"/\4/p' - for cargo_toml in $(find "${SRC_ROOT}" -type f -name "Cargo.toml" -not -path '*/target/*'); do + for cargo_toml in $(find "${SRC_ROOT}" -type f -name "Cargo.toml" -not -path '*/target/*' -not -path './deps/*'); do local pkgname=$(${SED} -n -e N -e "${regex_to_cut_pkgname}" "${cargo_toml}") if [ -z "${pkgname}" ]; then printf "Error: No package name in <%s>\n" "${cargo_toml}" @@ -43,7 +43,7 @@ function check_package_name() { function check_version() { local regex_to_cut_version='s/^version = "\(.*\)"$/\1/p' local expected=$(${SED} -n "${regex_to_cut_version}" "${SRC_ROOT}/Cargo.toml") - for cargo_toml in $(find "${SRC_ROOT}" -type f -name "Cargo.toml" -not -path '*/target/*'); do + for cargo_toml in $(find "${SRC_ROOT}" -type f -name "Cargo.toml" -not -path '*/target/*' -not -path './deps/*'); do local tmp=$(${SED} -n "${regex_to_cut_version}" "${cargo_toml}") if [ "${expected}" != "${tmp}" ]; then printf "Error: Version in <%s> is not right (expect: '%s', actual: '%s')\n" \ @@ -62,7 +62,7 @@ function check_version() { function check_license() { local regex_to_cut_license='s/^license = "\(.*\)"$/\1/p' local expected=$(${SED} -n "${regex_to_cut_license}" "${SRC_ROOT}/Cargo.toml") - for cargo_toml in $(find "${SRC_ROOT}" -type f -name "Cargo.toml" -not -path '*/target/*'); do + for cargo_toml in $(find "${SRC_ROOT}" -type f -name "Cargo.toml" -not -path '*/target/*' -not -path './deps/*'); do local tmp=$(${SED} -n "${regex_to_cut_license}" "${cargo_toml}") if [ "${expected}" != "${tmp}" ]; then printf "Error: License in <%s> is not right (expect: '%s', actual: '%s')\n" \ @@ -73,7 +73,7 @@ function check_license() { } function check_cargo_publish() { - for cargo_toml in $(find "${SRC_ROOT}" -type f -name "Cargo.toml" -not -path '*/target/*'); do + for cargo_toml in $(find "${SRC_ROOT}" -type f -name "Cargo.toml" -not -path '*/target/*' -not -path './deps/*'); do if ! grep -q '^description =' "${cargo_toml}"; then echo "Error: Require description in <${cargo_toml}>" ERRCNT=$((ERRCNT + 1)) @@ -115,7 +115,7 @@ function search_crate() { function check_dependencies_for() { local deptype="$1" - for cargo_toml in $(find "${SRC_ROOT}" -type f -name "Cargo.toml" -not -path '*/target/*'); do + for cargo_toml in $(find "${SRC_ROOT}" -type f -name "Cargo.toml" -not -path '*/target/*' -not -path './deps/*'); do local pkgroot=$(dirname "${cargo_toml}") for dependency_original in $(${SED} -n "/^\[${deptype}\]/,/^\[/p" "${cargo_toml}" \ | { ${GREP} -v "^\(\[\|[ ]*$\|[ ]*#\)" || true; } \ diff --git a/deps/quantum-resistant-lock-script b/deps/quantum-resistant-lock-script new file mode 160000 index 00000000..1825f29d --- /dev/null +++ b/deps/quantum-resistant-lock-script @@ -0,0 +1 @@ +Subproject commit 1825f29d814fa27c8d7d71fd7de3aab0e4c13a9c diff --git a/examples/unlock_sphincsplucs.rs b/examples/unlock_sphincsplucs.rs new file mode 100644 index 00000000..98d9b8b2 --- /dev/null +++ b/examples/unlock_sphincsplucs.rs @@ -0,0 +1,127 @@ +use bytes::Bytes; +use ckb_sdk::{ + constants::ONE_CKB, + traits::{ + DefaultCellCollector, DefaultCellDepResolver, DefaultHeaderDepResolver, + DefaultTransactionDependencyProvider, + }, + tx_builder::{ + sphincsplus::SphincsPlusEnv, transfer::CapacityTransferBuilder, CapacityBalancer, TxBuilder, + }, + unlock::{sphincsplus::SphincsPlusPrivateKey, SphincsPlus}, + Address, CkbRpcClient, NetworkType, +}; +use ckb_types::{ + core::{BlockView, DepType, ScriptHashType, TransactionView}, + h256, + packed::{CellOutput, Script}, + prelude::*, +}; + +use std::{convert::TryFrom, error::Error as StdErr, str::FromStr}; + +pub const SK: [u8; 64] = [ + 244, 229, 172, 97, 118, 43, 186, 182, 5, 191, 38, 224, 223, 57, 251, 84, // sk.seed + 29, 7, 44, 250, 108, 236, 220, 216, 161, 162, 99, 146, 46, 4, 34, 125, // sk.prf + 152, 145, 159, 50, 118, 81, 12, 134, 27, 52, 214, 210, 91, 84, 65, 42, // pubkey seed + 252, 12, 85, 58, 222, 186, 58, 189, 25, 133, 144, 79, 103, 177, 27, 76, // pubkey root +]; + +fn build_transfer_tx( + env: &SphincsPlusEnv, + sender: Script, + sender_key: SphincsPlusPrivateKey, +) -> Result> { + // Build ScriptUnlocker + let unlockers = env.build_unlockers(vec![sender_key]); + + // Build CapacityBalancer + let placeholder_witness = SphincsPlus::placeholder_witness(); + let mut balancer = CapacityBalancer::new_simple(sender, placeholder_witness, 1000); + balancer.force_small_change_as_fee = Some(ONE_CKB); + + // Build: + // * CellDepResolver + // * HeaderDepResolver + // * CellCollector + // * TransactionDependencyProvider + let ckb_rpc = "https://testnet.ckb.dev"; + let mut ckb_client = CkbRpcClient::new(ckb_rpc); + let cell_dep_resolver = { + let genesis_block = ckb_client.get_block_by_number(0.into())?.unwrap(); + let mut cell_dep_resolver = + DefaultCellDepResolver::from_genesis(&BlockView::from(genesis_block))?; + env.add_cell_dep(&mut cell_dep_resolver); + cell_dep_resolver + }; + let header_dep_resolver = DefaultHeaderDepResolver::new(ckb_rpc); + let mut cell_collector = DefaultCellCollector::new(ckb_rpc); + let tx_dep_provider = DefaultTransactionDependencyProvider::new(ckb_rpc, 10); + + let receiver = Address::from_str("ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqv5dsed9par23x4g58seaw58j3ym5ml2hs8ztche").unwrap(); + // Build the transaction + let output = CellOutput::new_builder() + .lock(Script::from(&receiver)) + .capacity(99_9990_0000u64.pack()) + .build(); + let builder = CapacityTransferBuilder::new(vec![(output, Bytes::default())]); + let (tx, still_locked_groups) = builder.build_unlocked( + &mut cell_collector, + &cell_dep_resolver, + &header_dep_resolver, + &tx_dep_provider, + &balancer, + &unlockers, + )?; + assert!(still_locked_groups.is_empty()); + Ok(tx) +} + +fn build_env() -> SphincsPlusEnv { + SphincsPlusEnv { + tx_hash: h256!("0x35f51257673c7a7edd009fa2166e6f8645156207c9da38202f04ba4d94d9e519"), + tx_idx: 0, + dep_type: DepType::Code, + code_hash: h256!("0x989ab456455509a1c2ad1cb8116b7d209df228144445c741b101ec3e55ee8351"), + hash_type: ScriptHashType::Data1, + network_type: NetworkType::Testnet, + } +} + +/* +1. build address, + ckt1qzvf4dzkg42sngwz45wtsytt05sfmu3gz3zyt36pkyq7c0j4a6p4zqkur4fuxjyh4fzphavynhdgptuwqsyhdjns028ugqy5jgnesdy8wslj3elk +2. transfer to address: + wallet transfer --from-account 0x946c32d287a3544d5450f0cf5d43ca24dd37f55e \ + --to-address ckt1qzvf4dzkg42sngwz45wtsytt05sfmu3gz3zyt36pkyq7c0j4a6p4zqkur4fuxjyh4fzphavynhdgptuwqsyhdjns028ugqy5jgnesdy8wslj3elk \ + --capacity 100 --skip-check-to-address + 0x7fb5afa7c0bdc9cbbc2b65b523581c2bb3ed43ced114f759651ae407dee3d0c9 +3. unlock the cell, and transfer the capacity to address ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqv5dsed9par23x4g58seaw58j3ym5ml2hs8ztche +*/ +fn main() -> Result<(), Box> { + let sk = SphincsPlusPrivateKey::try_from(SK.to_vec()).unwrap(); + let pk = sk.pub_key(); + let env = build_env(); + let address = env.build_address(&pk); + let resp = serde_json::json!({ + "address": address.to_string(), + }); + println!("{}", serde_json::to_string_pretty(&resp).unwrap()); + let sender = env.script(&pk); + + let tx = build_transfer_tx(&env, sender, sk)?; + + // Send transaction + let json_tx = ckb_jsonrpc_types::TransactionView::from(tx); + println!("tx: {}", serde_json::to_string_pretty(&json_tx).unwrap()); + let outputs_validator = Some(ckb_jsonrpc_types::OutputsValidator::Passthrough); + + let ckb_rpc = "https://testnet.ckb.dev"; + let _tx_hash = CkbRpcClient::new(ckb_rpc) + .send_transaction(json_tx.inner, outputs_validator) + .expect("send transaction"); + // example tx_hash: 0xf83fd6c2fe511a9c39795624b7e0be2157e1543d9f1b1a1cbb676896e31c2b1b + println!(">>> tx sent! <<<"); + + Ok(()) +} diff --git a/src/tx_builder/mod.rs b/src/tx_builder/mod.rs index 9ac7b268..21f92347 100644 --- a/src/tx_builder/mod.rs +++ b/src/tx_builder/mod.rs @@ -2,6 +2,7 @@ pub mod acp; pub mod cheque; pub mod dao; pub mod omni_lock; +pub mod sphincsplus; pub mod transfer; pub mod udt; diff --git a/src/tx_builder/sphincsplus/mod.rs b/src/tx_builder/sphincsplus/mod.rs new file mode 100644 index 00000000..152a999a --- /dev/null +++ b/src/tx_builder/sphincsplus/mod.rs @@ -0,0 +1,89 @@ +use std::collections::HashMap; + +use bytes::Bytes; +use ckb_types::{ + core::{DepType, ScriptHashType}, + packed::{Byte32, CellDep, OutPoint, Script}, + prelude::*, + H256, +}; + +use crate::{ + traits::DefaultCellDepResolver, + unlock::{ + sphincsplus::{SphincsPlusPrivateKey, SphincsPlusPublicKey}, + ScriptUnlocker, SphincsPlusRawKeysSigner, SphincsPlusUnlocker, + }, + Address, AddressPayload, NetworkType, ScriptId, +}; + +#[derive(Debug, Clone)] +pub struct SphincsPlusEnv { + /// transaction hash where the code is deployed + pub tx_hash: H256, + /// transaction index where the code is deployed + pub tx_idx: u32, + /// cell dependency type + pub dep_type: DepType, + /// the code hash + pub code_hash: H256, + /// the code hash's hash type, + pub hash_type: ScriptHashType, + /// the network type + pub network_type: NetworkType, +} + +impl SphincsPlusEnv { + /// build script id + pub fn script_id(&self) -> ScriptId { + ScriptId { + code_hash: self.code_hash.clone(), + hash_type: self.hash_type, + } + } + + pub fn script(&self, pk: &SphincsPlusPublicKey) -> Script { + Script::new_builder() + .code_hash(self.code_hash.pack()) + .hash_type(self.hash_type.into()) + .args(Bytes::from(pk.lock_args().to_vec()).pack()) + .build() + } + /// add cell dependency to DefaultCellDepResolver + pub fn add_cell_dep(&self, cell_dep_resolver: &mut DefaultCellDepResolver) { + let out_point = OutPoint::new( + Byte32::from_slice(self.tx_hash.as_bytes()).unwrap(), + self.tx_idx, + ); + + let cell_dep = CellDep::new_builder().out_point(out_point).build(); + cell_dep_resolver.insert(self.script_id(), cell_dep, "Sphincs plus".to_string()); + } + + /// build address from public key + pub fn build_address(&self, pk: &SphincsPlusPublicKey) -> Address { + let args = Bytes::from(pk.lock_args().to_vec()); + let address_payload = AddressPayload::new_full( + self.hash_type, + Byte32::from_slice(self.code_hash.as_bytes()).unwrap(), + args, + ); + Address::new(self.network_type, address_payload, true) + } + + /// build unlockers from private keys + pub fn build_unlockers( + &self, + sks: Vec, + ) -> HashMap> { + let signer = SphincsPlusRawKeysSigner::new_with_private_keys(sks); + let sighash_unlocker = SphincsPlusUnlocker::from(Box::new(signer) as Box<_>); + let sighash_script_id = ScriptId::new_data1(self.code_hash.clone()); + let mut unlockers = HashMap::default(); + unlockers.insert( + sighash_script_id, + Box::new(sighash_unlocker) as Box, + ); + unlockers + } +} diff --git a/src/unlock/mod.rs b/src/unlock/mod.rs index edda5d0d..df7996e3 100644 --- a/src/unlock/mod.rs +++ b/src/unlock/mod.rs @@ -1,6 +1,7 @@ pub(crate) mod omni_lock; pub mod rc_data; mod signer; +pub mod sphincsplus; mod unlocker; pub use signer::{ @@ -14,3 +15,7 @@ pub use unlocker::{ }; pub use omni_lock::{IdentityFlag, InfoCellData, OmniLockAcpConfig, OmniLockConfig}; + +pub use sphincsplus::{ + signer::SphincsPlusSigner, unlocker::SphincsPlusUnlocker, SphincsPlus, SphincsPlusRawKeysSigner, +}; diff --git a/src/unlock/sphincsplus/mod.rs b/src/unlock/sphincsplus/mod.rs new file mode 100644 index 00000000..fa742193 --- /dev/null +++ b/src/unlock/sphincsplus/mod.rs @@ -0,0 +1,277 @@ +use std::collections::HashMap; + +use anyhow::anyhow; +use bytes::Bytes; +use ckb_types::{packed::WitnessArgs, prelude::*, H256}; + +use std::convert::TryFrom; + +use ckb_hash::blake2b_256; +use thiserror::Error; + +use crate::{ + traits::{Signer, SignerError}, + util::zeroize_slice, +}; + +#[link(name = "sphincsplus", kind = "static")] +extern "C" { + // uint32_t sphincs_plus_get_pk_size(); + fn sphincs_plus_get_pk_size() -> u32; + + // uint32_t sphincs_plus_get_sk_size(); + fn sphincs_plus_get_sk_size() -> u32; + + // uint32_t sphincs_plus_get_sign_size(); + fn sphincs_plus_get_sign_size() -> u32; + + // int sphincs_plus_generate_keypair(uint8_t *pk, uint8_t *sk); + fn sphincs_plus_generate_keypair(pk: *mut u8, sk: *mut u8) -> i32; + + // int sphincs_plus_sign(uint8_t *message, uint8_t *sk, uint8_t *out_sign); + fn sphincs_plus_sign(message: *const u8, sk: *const u8, out_sign: *mut u8) -> i32; + + // int sphincs_plus_verify(uint8_t *sign, uint32_t sign_size, uint8_t *message, + // uint32_t message_size, uint8_t *pubkey, + // uint32_t pubkey_size); + fn sphincs_plus_verify( + sign: *const u8, + sign_size: u32, + message: *const u8, + message_sizse: u32, + pk: *const u8, + pk_size: u32, + ) -> i32; + + // int crypto_sign_seed_keypair(unsigned char *pk, unsigned char *sk, + // const unsigned char *seed) + fn crypto_sign_seed_keypair(pk: *mut u8, sk: *mut u8, seed: *const u8) -> i32; +} + +pub struct SphincsPlus; + +impl SphincsPlus { + /// get public key length + pub fn pk_len() -> usize { + unsafe { sphincs_plus_get_pk_size() as usize } + } + /// get private key length + pub fn sk_len() -> usize { + unsafe { sphincs_plus_get_sk_size() as usize } + } + + /// get signature length + pub fn sign_len() -> usize { + unsafe { sphincs_plus_get_sign_size() as usize } + } + + /// get lock length in witness + pub fn lock_len() -> usize { + Self::sign_len() + Self::pk_len() + } + + /// build lock data for witness lock with all zero bytes + pub fn zero_lock() -> Bytes { + Bytes::from(vec![0u8; SphincsPlus::lock_len()]) + } + + /// build a placeholder witness for sphincs plus + pub fn placeholder_witness() -> WitnessArgs { + WitnessArgs::new_builder() + .lock(Some(Self::zero_lock()).pack()) + .build() + } +} + +/// Transaction builder errors +#[derive(Error, Debug)] +pub enum SphincsPlusError { + /// Generate key pair failed. + #[error("generate sphincs plus failed with return value: `{0}`")] + GenerateKeyPair(i32), + + /// Sign failed, possible error values are: + /// - 1, signature length is not equal to SphincsPlus::sign_len() + #[error("sign failed with return value: `{0}`")] + Sign(i32), + /// Signature verify failed, possible error values are: + /// - 200, parameters' length are not all correct + /// - 201, singned failed with public key + /// - 202, sined message length is not correct + /// - 203, provided signature is not equal to public key calculated signature + #[error("verify failed: `{0}`")] + Verify(i32), + /// Publick key length is not correct. + #[error("public key length is not correct")] + PublicKeyLen, + /// Private key length is not correct. + #[error("private key length is not correct")] + PrivateKeyLen, + #[error("The private key is invalid")] + InvalidPrivateKey, + /// Other errors + #[error("other error: `{0}`")] + Other(anyhow::Error), +} + +#[derive(Default, Clone)] +pub struct SphincsPlusPrivateKey(Vec); + +impl SphincsPlusPrivateKey { + /// create a new SphincsPlus, and generate a new key pair + pub fn new() -> Result { + let mut s = Self(vec![0; SphincsPlus::sk_len()]); + let mut pk = vec![0; SphincsPlus::pk_len()]; + + let ret = unsafe { sphincs_plus_generate_keypair(pk.as_mut_ptr(), s.0.as_mut_ptr()) }; + if ret != 0 { + Err(SphincsPlusError::GenerateKeyPair(ret)) + } else { + Ok(s) + } + } + + /// verify if the private key is valid, by generate the private key from the seed. + pub fn is_valid(&self) -> Result<(), SphincsPlusError> { + if self.0.len() != SphincsPlus::sk_len() { + return Err(SphincsPlusError::PrivateKeyLen); + } + let mut sk = vec![0; SphincsPlus::sk_len()]; + let mut pk = vec![0; SphincsPlus::pk_len()]; + + let ret = + unsafe { crypto_sign_seed_keypair(pk.as_mut_ptr(), sk.as_mut_ptr(), self.0.as_ptr()) }; + if ret != 0 { + return Err(SphincsPlusError::GenerateKeyPair(ret)); + } + if sk != self.0 { + return Err(SphincsPlusError::InvalidPrivateKey); + } + Ok(()) + } + + pub fn pub_key(&self) -> SphincsPlusPublicKey { + let pk = self.0[SphincsPlus::sk_len() - SphincsPlus::pk_len()..].to_vec(); + SphincsPlusPublicKey(pk) + } + + /// sign a message. + pub fn sign(&self, msg: &[u8]) -> Result, SphincsPlusError> { + let mut s = vec![0; SphincsPlus::lock_len()]; + + let ret = unsafe { sphincs_plus_sign(msg.as_ptr(), self.0.as_ptr(), s.as_mut_ptr()) }; + if ret != 0 { + Err(SphincsPlusError::Sign(ret)) + } else { + // copy public key + s[SphincsPlus::sign_len()..] + .copy_from_slice(&self.0[SphincsPlus::sk_len() - SphincsPlus::pk_len()..]); + Ok(s) + } + } +} + +impl TryFrom> for SphincsPlusPrivateKey { + type Error = SphincsPlusError; + + fn try_from(value: Vec) -> Result { + if value.len() != SphincsPlus::sk_len() { + return Err(SphincsPlusError::PrivateKeyLen); + } + let private_key = Self(value); + private_key.is_valid()?; + Ok(private_key) + } +} + +pub struct SphincsPlusPublicKey(Vec); + +impl SphincsPlusPublicKey { + /// verify if a message is correctly signed + pub fn verify(&self, msg: &[u8], sign: &[u8]) -> Result<(), SphincsPlusError> { + let ret = unsafe { + sphincs_plus_verify( + sign.as_ptr(), + sign.len() as u32, + msg.as_ptr(), + msg.len() as u32, + self.0.as_ptr(), + self.0.len() as u32, + ) + }; + if ret != 0 { + Err(SphincsPlusError::Verify(ret)) + } else { + Ok(()) + } + } + + /// Generate lock args. + pub fn lock_args(&self) -> [u8; 32] { + blake2b_256(&self.0) + } +} + +/// A signer use secp256k1 raw key, the id is `blake160(pubkey)`. +#[derive(Default, Clone)] +pub struct SphincsPlusRawKeysSigner { + keys: HashMap, +} + +impl SphincsPlusRawKeysSigner { + pub fn new(keys: HashMap) -> SphincsPlusRawKeysSigner { + SphincsPlusRawKeysSigner { keys } + } + pub fn new_with_private_keys(keys: Vec) -> SphincsPlusRawKeysSigner { + let mut signer = SphincsPlusRawKeysSigner::default(); + for key in keys { + signer.add_secret_key(key); + } + signer + } + pub fn add_secret_key(&mut self, key: SphincsPlusPrivateKey) { + let lock_args = key.pub_key().lock_args(); + let hash256 = H256::from_slice(&lock_args).expect("Generate hash(H256) from pubkey failed"); + self.keys.insert(hash256, key); + } +} + +impl Signer for SphincsPlusRawKeysSigner { + fn match_id(&self, id: &[u8]) -> bool { + id.len() == 32 && self.keys.contains_key(&H256::from_slice(id).unwrap()) + } + + fn sign( + &self, + id: &[u8], + message: &[u8], + _recoverable: bool, + _tx: &ckb_types::core::TransactionView, + ) -> Result { + if !self.match_id(id) { + return Err(SignerError::IdNotFound); + } + if message.len() != 32 { + return Err(SignerError::InvalidMessage(format!( + "expected length: 32, got: {}", + message.len() + ))); + } + let key = self.keys.get(&H256::from_slice(id).unwrap()).unwrap(); + let sig = key + .sign(message) + .map_err(|e| anyhow!("{}", e.to_string()))?; + Ok(bytes::Bytes::from(sig)) + } +} + +impl Drop for SphincsPlusRawKeysSigner { + fn drop(&mut self) { + for (_, mut secret_key) in self.keys.drain() { + zeroize_slice(&mut secret_key.0); + } + } +} + +pub(crate) mod signer; +pub(crate) mod unlocker; diff --git a/src/unlock/sphincsplus/signer.rs b/src/unlock/sphincsplus/signer.rs new file mode 100644 index 00000000..e9b20b0b --- /dev/null +++ b/src/unlock/sphincsplus/signer.rs @@ -0,0 +1,79 @@ +use ckb_types::{ + core::TransactionView, + packed::{self, WitnessArgs}, + prelude::{Builder, Entity, Pack}, +}; + +use crate::{ + traits::Signer, + unlock::{generate_message, ScriptSignError, ScriptSigner}, + ScriptGroup, +}; + +use super::SphincsPlus; + +/// Signer for spincs plus lock script +pub struct SphincsPlusSigner { + signer: Box, +} + +impl SphincsPlusSigner { + pub fn new(signer: Box) -> Self { + SphincsPlusSigner { signer } + } + + pub fn signer(&self) -> &dyn Signer { + self.signer.as_ref() + } + + fn sign_tx_with_owner_id( + &self, + owner_id: &[u8], + tx: &TransactionView, + script_group: &ScriptGroup, + ) -> Result { + let witness_idx = script_group.input_indices[0]; + let mut witnesses: Vec = tx.witnesses().into_iter().collect(); + while witnesses.len() <= witness_idx { + witnesses.push(Default::default()); + } + let tx_new = tx + .as_advanced_builder() + .set_witnesses(witnesses.clone()) + .build(); + + let zero_lock = SphincsPlus::zero_lock(); + let message = generate_message(&tx_new, script_group, zero_lock)?; + + let signature = self.signer.sign(owner_id, message.as_ref(), true, tx)?; + + // Put signature into witness + let witness_data = witnesses[witness_idx].raw_data(); + let mut current_witness: WitnessArgs = if witness_data.is_empty() { + WitnessArgs::default() + } else { + WitnessArgs::from_slice(witness_data.as_ref())? + }; + current_witness = current_witness + .as_builder() + .lock(Some(signature).pack()) + .build(); + witnesses[witness_idx] = current_witness.as_bytes().pack(); + Ok(tx.as_advanced_builder().set_witnesses(witnesses).build()) + } +} + +impl ScriptSigner for SphincsPlusSigner { + fn match_args(&self, args: &[u8]) -> bool { + args.len() == 32 && self.signer.match_id(args) + } + + fn sign_tx( + &self, + tx: &TransactionView, + script_group: &ScriptGroup, + ) -> Result { + let args = script_group.script.args().raw_data(); + self.sign_tx_with_owner_id(args.as_ref(), tx, script_group) + } +} diff --git a/src/unlock/sphincsplus/unlocker.rs b/src/unlock/sphincsplus/unlocker.rs new file mode 100644 index 00000000..4b193a5f --- /dev/null +++ b/src/unlock/sphincsplus/unlocker.rs @@ -0,0 +1,47 @@ +use ckb_types::core::TransactionView; + +use crate::{ + traits::{Signer, TransactionDependencyProvider}, + unlock::{fill_witness_lock, ScriptSigner, ScriptUnlocker, UnlockError}, + ScriptGroup, +}; + +use super::{signer::SphincsPlusSigner, SphincsPlus}; + +pub struct SphincsPlusUnlocker { + signer: SphincsPlusSigner, +} +impl SphincsPlusUnlocker { + pub fn new(signer: SphincsPlusSigner) -> Self { + Self { signer } + } +} +impl From> for SphincsPlusUnlocker { + fn from(signer: Box) -> SphincsPlusUnlocker { + SphincsPlusUnlocker::new(SphincsPlusSigner::new(signer)) + } +} + +impl ScriptUnlocker for SphincsPlusUnlocker { + fn match_args(&self, args: &[u8]) -> bool { + self.signer.match_args(args) + } + + fn unlock( + &self, + tx: &TransactionView, + script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, + ) -> Result { + Ok(self.signer.sign_tx(tx, script_group)?) + } + + fn fill_placeholder_witness( + &self, + tx: &TransactionView, + script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, + ) -> Result { + fill_witness_lock(tx, script_group, SphincsPlus::zero_lock()) + } +}