From 8af26415d1066db5b5bf4da322b22f5d3dfa6b10 Mon Sep 17 00:00:00 2001 From: driftluo Date: Wed, 17 Apr 2024 16:50:31 +0800 Subject: [PATCH] feat: omnilock transaction support pubkeyhash/multisign/eth --- src/tests/transaction/omnilock.rs | 189 +++++++++++++- src/transaction/builder/mod.rs | 17 +- src/transaction/handler/omnilock.rs | 43 +++- src/transaction/signer/mod.rs | 14 +- src/transaction/signer/multisig.rs | 5 +- src/transaction/signer/omnilock.rs | 66 ++++- src/transaction/signer/sighash.rs | 5 +- src/types/transaction_with_groups.rs | 14 +- src/unlock/omni_lock.rs | 48 +++- src/unlock/signer.rs | 365 ++++++++++++++++----------- src/unlock/unlocker.rs | 52 ++-- 11 files changed, 624 insertions(+), 194 deletions(-) diff --git a/src/tests/transaction/omnilock.rs b/src/tests/transaction/omnilock.rs index 4dac5ef9..2e070905 100644 --- a/src/tests/transaction/omnilock.rs +++ b/src/tests/transaction/omnilock.rs @@ -10,8 +10,8 @@ use ckb_types::{ use crate::{ constants::ONE_CKB, tests::{ - build_omnilock_script, build_sighash_script, init_context, ACCOUNT0_KEY, ACCOUNT2_ARG, - FEE_RATE, OMNILOCK_BIN, + build_omnilock_script, build_sighash_script, init_context, ACCOUNT0_ARG, ACCOUNT0_KEY, + ACCOUNT1_ARG, ACCOUNT1_KEY, ACCOUNT2_ARG, FEE_RATE, OMNILOCK_BIN, }, transaction::{ builder::{CkbTransactionBuilder, SimpleTransactionBuilder}, @@ -26,8 +26,8 @@ use crate::{ signer::{SignContexts, TransactionSigner}, TransactionBuilderConfiguration, }, - unlock::OmniLockConfig, - util::keccak160, + unlock::{MultisigConfig, OmniLockConfig}, + util::{blake160, keccak160}, NetworkInfo, }; @@ -65,11 +65,17 @@ fn test_omnilock_config(omnilock_outpoint: OutPoint) -> TransactionBuilderConfig } #[test] -fn test_transfer_from_omnilock_ethereum() { +fn test_omnilock_ethereum() { + omnilock_ethereum(false); + omnilock_ethereum(true) +} + +fn omnilock_ethereum(cobuild: bool) { let network_info = NetworkInfo::testnet(); let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap(); let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &account0_key); - let cfg = OmniLockConfig::new_ethereum(keccak160(Pubkey::from(pubkey).as_ref())); + let mut cfg = OmniLockConfig::new_ethereum(keccak160(Pubkey::from(pubkey).as_ref())); + cfg.enable_cobuild(cobuild); let sender = build_omnilock_script(&cfg); let receiver = build_sighash_script(ACCOUNT2_ARG); @@ -104,8 +110,8 @@ fn test_transfer_from_omnilock_ethereum() { let mut tx_with_groups = builder.build(&contexts).expect("build failed"); - let json_tx = ckb_jsonrpc_types::TransactionView::from(tx_with_groups.get_tx_view().clone()); - println!("tx: {}", serde_json::to_string_pretty(&json_tx).unwrap()); + // let json_tx = ckb_jsonrpc_types::TransactionView::from(tx_with_groups.get_tx_view().clone()); + // println!("tx: {}", serde_json::to_string_pretty(&json_tx).unwrap()); TransactionSigner::new(&network_info) // use unitest lock to verify @@ -119,8 +125,171 @@ fn test_transfer_from_omnilock_ethereum() { ) .unwrap(); - let json_tx = ckb_jsonrpc_types::TransactionView::from(tx_with_groups.get_tx_view().clone()); - println!("tx: {}", serde_json::to_string_pretty(&json_tx).unwrap()); + // let json_tx = ckb_jsonrpc_types::TransactionView::from(tx_with_groups.get_tx_view().clone()); + // println!("tx: {}", serde_json::to_string_pretty(&json_tx).unwrap()); + + let tx = tx_with_groups.get_tx_view().clone(); + let script_groups = tx_with_groups.script_groups.clone(); + assert_eq!(script_groups.len(), 1); + assert_eq!(tx.header_deps().len(), 0); + assert_eq!(tx.cell_deps().len(), 2); + assert_eq!(tx.inputs().len(), 2); + for out_point in tx.input_pts_iter() { + assert_eq!(ctx.get_input(&out_point).unwrap().0.lock(), sender); + } + assert_eq!(tx.outputs().len(), 2); + assert_eq!(tx.output(0).unwrap(), output); + assert_eq!(tx.output(1).unwrap().lock(), sender); + let change_capacity: u64 = tx.output(1).unwrap().capacity().unpack(); + let fee = (100 + 200 - 120) * ONE_CKB - change_capacity; + assert_eq!(tx.data().as_reader().serialized_size_in_block() as u64, fee); + + ctx.verify(tx, FEE_RATE).unwrap(); +} + +#[test] +fn test_omnilock_pubkeyhash() { + omnilock_pubkeyhash(false); + omnilock_pubkeyhash(true) +} + +fn omnilock_pubkeyhash(cobuild: bool) { + let network_info = NetworkInfo::testnet(); + let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap(); + let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &account0_key); + let mut cfg = OmniLockConfig::new_pubkey_hash(blake160(&pubkey.serialize())); + cfg.enable_cobuild(cobuild); + + let sender = build_omnilock_script(&cfg); + let receiver = build_sighash_script(ACCOUNT2_ARG); + let (ctx, mut outpoints) = init_context( + vec![(OMNILOCK_BIN, true)], + vec![ + (sender.clone(), Some(100 * ONE_CKB)), + (sender.clone(), Some(200 * ONE_CKB)), + (sender.clone(), Some(300 * ONE_CKB)), + ], + ); + + let configuration = test_omnilock_config(outpoints.pop().unwrap()); + + let iterator = InputIterator::new_with_cell_collector( + vec![sender.clone()], + Box::new(ctx.to_live_cells_context()) as Box<_>, + ); + let mut builder = SimpleTransactionBuilder::new(configuration, iterator); + + let output = CellOutput::new_builder() + .capacity((120 * ONE_CKB).pack()) + .lock(receiver) + .build(); + builder.add_output_and_data(output.clone(), ckb_types::packed::Bytes::default()); + builder.set_change_lock(sender.clone()); + + let context = OmnilockScriptContext::new(cfg.clone(), network_info.url.clone()); + let mut contexts = HandlerContexts::default(); + contexts.add_context(Box::new(context) as Box<_>); + + let mut tx_with_groups = builder.build(&contexts).expect("build failed"); + + // let json_tx = ckb_jsonrpc_types::TransactionView::from(tx_with_groups.get_tx_view().clone()); + + TransactionSigner::new(&network_info) + // use unitest lock to verify + .insert_unlocker( + crate::ScriptId::new_data1(H256::from(blake2b_256(OMNILOCK_BIN))), + crate::transaction::signer::omnilock::OmnilockSigner {}, + ) + .sign_transaction( + &mut tx_with_groups, + &SignContexts::new_omnilock(vec![account0_key], cfg), + ) + .unwrap(); + + let tx = tx_with_groups.get_tx_view().clone(); + let script_groups = tx_with_groups.script_groups.clone(); + assert_eq!(script_groups.len(), 1); + assert_eq!(tx.header_deps().len(), 0); + assert_eq!(tx.cell_deps().len(), 2); + assert_eq!(tx.inputs().len(), 2); + for out_point in tx.input_pts_iter() { + assert_eq!(ctx.get_input(&out_point).unwrap().0.lock(), sender); + } + assert_eq!(tx.outputs().len(), 2); + assert_eq!(tx.output(0).unwrap(), output); + assert_eq!(tx.output(1).unwrap().lock(), sender); + let change_capacity: u64 = tx.output(1).unwrap().capacity().unpack(); + let fee = (100 + 200 - 120) * ONE_CKB - change_capacity; + assert_eq!(tx.data().as_reader().serialized_size_in_block() as u64, fee); + + ctx.verify(tx, FEE_RATE).unwrap(); +} + +#[test] +fn test_omnilock_multisign() { + omnilock_multisign(false); + omnilock_multisign(true) +} + +fn omnilock_multisign(cobuild: bool) { + let network_info = NetworkInfo::testnet(); + let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap(); + let account1_key = secp256k1::SecretKey::from_slice(ACCOUNT1_KEY.as_bytes()).unwrap(); + let lock_args = vec![ + ACCOUNT0_ARG.clone(), + ACCOUNT1_ARG.clone(), + ACCOUNT2_ARG.clone(), + ]; + let multi_cfg = MultisigConfig::new_with(lock_args, 0, 2).unwrap(); + let mut cfg = OmniLockConfig::new_multisig(multi_cfg); + cfg.enable_cobuild(cobuild); + + let sender = build_omnilock_script(&cfg); + let receiver = build_sighash_script(ACCOUNT2_ARG); + + let (ctx, mut outpoints) = init_context( + vec![(OMNILOCK_BIN, true)], + vec![ + (sender.clone(), Some(100 * ONE_CKB)), + (sender.clone(), Some(200 * ONE_CKB)), + (sender.clone(), Some(300 * ONE_CKB)), + ], + ); + + let configuration = test_omnilock_config(outpoints.pop().unwrap()); + + let iterator = InputIterator::new_with_cell_collector( + vec![sender.clone()], + Box::new(ctx.to_live_cells_context()) as Box<_>, + ); + let mut builder = SimpleTransactionBuilder::new(configuration, iterator); + + let output = CellOutput::new_builder() + .capacity((120 * ONE_CKB).pack()) + .lock(receiver) + .build(); + builder.add_output_and_data(output.clone(), ckb_types::packed::Bytes::default()); + builder.set_change_lock(sender.clone()); + + let context = OmnilockScriptContext::new(cfg.clone(), network_info.url.clone()); + let mut contexts = HandlerContexts::default(); + contexts.add_context(Box::new(context) as Box<_>); + + let mut tx_with_groups = builder.build(&contexts).expect("build failed"); + + // let json_tx = ckb_jsonrpc_types::TransactionView::from(tx_with_groups.get_tx_view().clone()); + + TransactionSigner::new(&network_info) + // use unitest lock to verify + .insert_unlocker( + crate::ScriptId::new_data1(H256::from(blake2b_256(OMNILOCK_BIN))), + crate::transaction::signer::omnilock::OmnilockSigner {}, + ) + .sign_transaction( + &mut tx_with_groups, + &SignContexts::new_omnilock(vec![account0_key, account1_key], cfg), + ) + .unwrap(); let tx = tx_with_groups.get_tx_view().clone(); let script_groups = tx_with_groups.script_groups.clone(); diff --git a/src/transaction/builder/mod.rs b/src/transaction/builder/mod.rs index f6edd0f6..d1d5973e 100644 --- a/src/transaction/builder/mod.rs +++ b/src/transaction/builder/mod.rs @@ -10,7 +10,7 @@ use crate::{ }; use ckb_types::{ core::{Capacity, TransactionView}, - packed::{self, Byte32, CellOutput, Script}, + packed::{self, Byte32, CellOutput, OutPoint, Script}, prelude::{Builder, Entity, Pack, Unpack}, }; pub mod fee_calculator; @@ -146,6 +146,7 @@ fn inner_build< ) -> Result { let mut lock_groups: HashMap = HashMap::default(); let mut type_groups: HashMap = HashMap::default(); + let mut inputs: HashMap = HashMap::default(); // setup outputs' type script group for (output_idx, output) in tx.get_outputs().clone().iter().enumerate() { @@ -167,6 +168,14 @@ fn inner_build< tx.input(input.cell_input()); tx.witness(packed::Bytes::default()); + inputs.insert( + input.live_cell.out_point.clone(), + ( + input.live_cell.output.clone(), + input.live_cell.output_data.clone(), + ), + ); + let previous_output = input.previous_output(); let lock_script = previous_output.lock(); lock_groups @@ -203,7 +212,11 @@ fn inner_build< let tx_view = change_builder.finalize(tx); - return Ok(TransactionWithScriptGroups::new(tx_view, script_groups)); + return Ok(TransactionWithScriptGroups::new( + tx_view, + script_groups, + inputs, + )); } } diff --git a/src/transaction/handler/omnilock.rs b/src/transaction/handler/omnilock.rs index 42efe918..1040f551 100644 --- a/src/transaction/handler/omnilock.rs +++ b/src/transaction/handler/omnilock.rs @@ -9,6 +9,10 @@ use super::{cell_dep, HandlerContext, ScriptHandler}; use crate::{ core::TransactionBuilder, tx_builder::TxBuilderError, + types::cobuild::{ + basic::{Message, SighashAll, SighashAllOnly}, + top_level::WitnessLayout, + }, unlock::{OmniLockConfig, OmniUnlockMode}, NetworkInfo, NetworkType, ScriptGroup, ScriptId, }; @@ -79,10 +83,43 @@ impl ScriptHandler for OmnilockScriptHandler { if let Some(args) = context.as_any().downcast_ref::() { tx_builder.dedup_cell_deps(self.cell_deps.clone()); let index = script_group.input_indices.first().unwrap(); - let placeholder_witness = args.cfg.placeholder_witness(args.unlock_mode)?; - if let Some(lock) = placeholder_witness.lock().to_opt() { - tx_builder.set_witness_lock(*index, Some(lock.raw_data())); + if args.cfg.enable_cobuild { + let lock_field = args.cfg.placeholder_witness_lock(args.unlock_mode)?; + + let witness = match &args.cfg.cobuild_message { + None => { + let sighash_all_only = SighashAllOnly::new_builder() + .seal( + [bytes::Bytes::copy_from_slice(&[0x00u8]), lock_field] + .concat() + .pack(), + ) + .build(); + let sighash_all_only = + WitnessLayout::new_builder().set(sighash_all_only).build(); + sighash_all_only.as_bytes().pack() + } + Some(msg) => { + let sighash_all = SighashAll::new_builder() + .message(Message::new_unchecked(msg.clone())) + .seal( + [bytes::Bytes::copy_from_slice(&[0x00u8]), lock_field] + .concat() + .pack(), + ) + .build(); + let sighash_all = WitnessLayout::new_builder().set(sighash_all).build(); + sighash_all.as_bytes().pack() + } + }; + tx_builder.set_witness(*index, witness); + } else { + let placeholder_witness = args.cfg.placeholder_witness(args.unlock_mode)?; + if let Some(lock) = placeholder_witness.lock().to_opt() { + tx_builder.set_witness_lock(*index, Some(lock.raw_data())); + } } + Ok(true) } else { Ok(false) diff --git a/src/transaction/signer/mod.rs b/src/transaction/signer/mod.rs index 8e662701..ecae3db9 100644 --- a/src/transaction/signer/mod.rs +++ b/src/transaction/signer/mod.rs @@ -1,4 +1,8 @@ -use ckb_types::{core, H256}; +use ckb_types::{ + core, + packed::{CellOutput, OutPoint}, + H256, +}; use std::collections::HashMap; use crate::{ @@ -22,6 +26,7 @@ pub trait CKBScriptSigner { tx_view: &core::TransactionView, script_group: &ScriptGroup, context: &dyn SignContext, + inputs: &HashMap, ) -> Result; } @@ -144,7 +149,12 @@ impl TransactionSigner { if !unlocker.match_context(context.as_ref()) { continue; } - tx = unlocker.sign_transaction(&tx, script_group, context.as_ref())?; + tx = unlocker.sign_transaction( + &tx, + script_group, + context.as_ref(), + &transaction.inputs, + )?; signed_groups_indices.push(idx); break; } diff --git a/src/transaction/signer/multisig.rs b/src/transaction/signer/multisig.rs index 282ae70f..c9e6ac2d 100644 --- a/src/transaction/signer/multisig.rs +++ b/src/transaction/signer/multisig.rs @@ -1,4 +1,6 @@ -use ckb_types::core; +use std::collections::HashMap; + +use ckb_types::{core, packed}; use crate::{ traits::{dummy_impls::DummyTransactionDependencyProvider, SecpCkbRawKeySigner}, @@ -45,6 +47,7 @@ impl CKBScriptSigner for Secp256k1Blake160MultisigAllSigner { transaction: &core::TransactionView, script_group: &crate::ScriptGroup, context: &dyn super::SignContext, + _inputs: &HashMap, ) -> Result { if let Some(args) = context .as_any() diff --git a/src/transaction/signer/omnilock.rs b/src/transaction/signer/omnilock.rs index 1367cd4f..8c5ed80b 100644 --- a/src/transaction/signer/omnilock.rs +++ b/src/transaction/signer/omnilock.rs @@ -1,7 +1,9 @@ -use ckb_types::core; +use std::collections::HashMap; + +use ckb_types::{core, packed}; use crate::{ - traits::{dummy_impls::DummyTransactionDependencyProvider, SecpCkbRawKeySigner}, + traits::SecpCkbRawKeySigner, unlock::{ OmniLockConfig, OmniLockScriptSigner, OmniLockUnlocker, OmniUnlockMode, ScriptUnlocker, UnlockError, @@ -50,17 +52,67 @@ impl CKBScriptSigner for OmnilockSigner { transaction: &core::TransactionView, script_group: &crate::ScriptGroup, context: &dyn super::SignContext, + inputs: &HashMap, ) -> Result { if let Some(args) = context.as_any().downcast_ref::() { let unlocker = args.build_omnilock_unlocker(); - let tx = unlocker.unlock( - transaction, - script_group, - &DummyTransactionDependencyProvider {}, - )?; + let tx = unlocker.unlock(transaction, script_group, &InputsProvider { inputs } as _)?; Ok(tx) } else { Err(UnlockError::SignContextTypeIncorrect) } } } + +struct InputsProvider<'a> { + inputs: &'a HashMap, +} + +impl<'a> crate::traits::TransactionDependencyProvider for InputsProvider<'a> { + /// For verify certain cell belong to certain transaction + fn get_transaction( + &self, + _tx_hash: &packed::Byte32, + ) -> Result { + Err(crate::traits::TransactionDependencyError::NotFound( + "not support".to_string(), + )) + } + /// For get the output information of inputs or cell_deps, those cell should be live cell + fn get_cell( + &self, + out_point: &packed::OutPoint, + ) -> Result { + self.inputs.get(out_point).map(|a| a.0.clone()).ok_or( + crate::traits::TransactionDependencyError::NotFound("not found".to_string()), + ) + } + /// For get the output data information of inputs or cell_deps + fn get_cell_data( + &self, + out_point: &packed::OutPoint, + ) -> Result { + self.inputs.get(out_point).map(|a| a.1.clone()).ok_or( + crate::traits::TransactionDependencyError::NotFound("not found".to_string()), + ) + } + /// For get the header information of header_deps + fn get_header( + &self, + _block_hash: &packed::Byte32, + ) -> Result { + Err(crate::traits::TransactionDependencyError::NotFound( + "not support".to_string(), + )) + } + + /// For get_block_extension + fn get_block_extension( + &self, + _block_hash: &packed::Byte32, + ) -> Result, crate::traits::TransactionDependencyError> { + Err(crate::traits::TransactionDependencyError::NotFound( + "not support".to_string(), + )) + } +} diff --git a/src/transaction/signer/sighash.rs b/src/transaction/signer/sighash.rs index 38d4e095..7180142f 100644 --- a/src/transaction/signer/sighash.rs +++ b/src/transaction/signer/sighash.rs @@ -1,4 +1,6 @@ -use ckb_types::core; +use std::collections::HashMap; + +use ckb_types::{core, packed}; use crate::{ traits::{dummy_impls::DummyTransactionDependencyProvider, SecpCkbRawKeySigner}, @@ -32,6 +34,7 @@ impl CKBScriptSigner for Secp256k1Blake160SighashAllSigner { transaction: &core::TransactionView, script_group: &crate::ScriptGroup, context: &dyn super::SignContext, + _inputs: &HashMap, ) -> Result { if let Some(args) = context .as_any() diff --git a/src/types/transaction_with_groups.rs b/src/types/transaction_with_groups.rs index 4f03dafb..c71646de 100644 --- a/src/types/transaction_with_groups.rs +++ b/src/types/transaction_with_groups.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; + use ckb_types::{ core::{ScriptHashType, TransactionView}, - packed::Script, + packed::{CellOutput, OutPoint, Script}, prelude::*, }; @@ -9,13 +11,19 @@ use crate::ScriptGroup; pub struct TransactionWithScriptGroups { pub(crate) tx_view: TransactionView, pub(crate) script_groups: Vec, + pub(crate) inputs: HashMap, } impl TransactionWithScriptGroups { - pub fn new(tx_view: TransactionView, script_groups: Vec) -> Self { + pub fn new( + tx_view: TransactionView, + script_groups: Vec, + inputs: HashMap, + ) -> Self { Self { tx_view, script_groups, + inputs, } } pub fn get_tx_view(&self) -> &TransactionView { @@ -39,6 +47,7 @@ impl TransactionWithScriptGroups { pub struct TransactionWithScriptGroupsBuilder { tx_view: Option, script_groups: Vec, + inputs: HashMap, } impl TransactionWithScriptGroupsBuilder { @@ -82,6 +91,7 @@ impl TransactionWithScriptGroupsBuilder { TransactionWithScriptGroups { tx_view: self.tx_view.unwrap(), script_groups: self.script_groups, + inputs: self.inputs, } } } diff --git a/src/unlock/omni_lock.rs b/src/unlock/omni_lock.rs index c2e5e408..c1150917 100644 --- a/src/unlock/omni_lock.rs +++ b/src/unlock/omni_lock.rs @@ -6,6 +6,7 @@ use super::{MultisigConfig, OmniUnlockMode}; use crate::{ tx_builder::SinceSource, types::{ + cobuild::basic::Message, omni_lock::{Auth, Identity as IdentityType, IdentityOpt, OmniLockWitnessLock}, xudt_rce_mol::SmtProofEntryVec, }, @@ -16,7 +17,7 @@ use bitflags::bitflags; pub use ckb_types::prelude::Pack; use ckb_types::{ bytes::{BufMut, Bytes, BytesMut}, - packed::WitnessArgs, + packed::{Byte, WitnessArgs}, prelude::*, H160, H256, }; @@ -139,6 +140,16 @@ impl Identity { let auth_content = H160::from_slice(&slice[1..21]).map_err(|e| e.to_string())?; Ok(Identity { flag, auth_content }) } + + pub fn to_auth(&self) -> Auth { + let raw: [u8; 21] = self.clone().into(); + let builder = Auth::new_builder(); + let mut m_raw = [Byte::new(0); 21]; + for (i, v) in IntoIterator::into_iter(raw).enumerate() { + m_raw[i] = Byte::new(v); + } + builder.set(m_raw).build() + } } impl From for [u8; 21] { @@ -437,6 +448,13 @@ pub struct OmniLockConfig { time_lock_config: Option, // 32 bytes type script hash for supply mode info_cell: Option, + // Whether to cobuild + pub(crate) enable_cobuild: bool, + // cobuild message + pub(crate) cobuild_message: Option, + + pub(crate) enable_rc: bool, + pub(crate) use_rc_identify: bool, } impl OmniLockConfig { @@ -490,6 +508,10 @@ impl OmniLockConfig { acp_config, time_lock_config, info_cell, + enable_cobuild: false, + cobuild_message: Some(Message::default().as_bytes()), + enable_rc: false, + use_rc_identify: false, }) } @@ -510,6 +532,10 @@ impl OmniLockConfig { acp_config: None, time_lock_config: None, info_cell: None, + enable_cobuild: false, + cobuild_message: Some(Message::default().as_bytes()), + enable_rc: false, + use_rc_identify: false, } } @@ -551,9 +577,17 @@ impl OmniLockConfig { acp_config: None, time_lock_config: None, info_cell: None, + enable_cobuild: false, + cobuild_message: Some(Message::default().as_bytes()), + enable_rc: false, + use_rc_identify: false, } } + pub fn enable_cobuild(&mut self, enable: bool) { + self.enable_cobuild = enable + } + /// Set the admin cofiguration, and set the OmniLockFlags::ADMIN flag. /// # Arguments /// * `admin_config` The new admin config. @@ -791,6 +825,18 @@ impl OmniLockConfig { _ => todo!("to support other placeholder_witness implementions"), } } + + pub fn build_proofs(&self) -> SmtProofEntryVec { + if let Some(ref admin) = self.admin_config { + admin.proofs.clone() + } else { + Default::default() + } + } + + pub fn build_auth(&self) -> Auth { + self.id.to_auth() + } } #[cfg(test)] diff --git a/src/unlock/signer.rs b/src/unlock/signer.rs index 708154a2..5b50cb3b 100644 --- a/src/unlock/signer.rs +++ b/src/unlock/signer.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use anyhow::anyhow; -use ckb_hash::{blake2b_256, new_blake2b}; +use ckb_hash::{blake2b_256, new_blake2b, Blake2b, Blake2bBuilder}; use ckb_types::{ bytes::{Bytes, BytesMut}, core::{ScriptHashType, TransactionView}, @@ -13,20 +13,28 @@ use ckb_types::{ use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{constants::MULTISIG_TYPE_HASH, types::omni_lock::OmniLockWitnessLock}; use crate::{ - traits::{Signer, SignerError}, + constants::MULTISIG_TYPE_HASH, + types::{cobuild::top_level::WitnessLayoutUnion, omni_lock::OmniLockWitnessLock}, +}; +use crate::{ + traits::{Signer, SignerError, TransactionDependencyProvider}, util::convert_keccak256_hash, }; use crate::{ - types::{AddressPayload, CodeHashIndex, ScriptGroup, Since}, + types::{ + cobuild::{ + basic::{Message, SighashAll, SighashAllOnly}, + top_level::WitnessLayout, + }, + omni_lock, + xudt_rce_mol::SmtProofEntryVec, + AddressPayload, CodeHashIndex, ScriptGroup, Since, + }, Address, NetworkType, }; -use super::{ - omni_lock::{ConfigError, Identity}, - IdentityFlag, OmniLockConfig, -}; +use super::{omni_lock::ConfigError, IdentityFlag, OmniLockConfig}; #[derive(Error, Debug)] pub enum ScriptSignError { @@ -67,6 +75,7 @@ pub trait ScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result; } @@ -131,6 +140,7 @@ impl ScriptSigner for SecpSighashScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let args = script_group.script.args().raw_data(); self.sign_tx_with_owner_id(args.as_ref(), tx, script_group) @@ -296,6 +306,7 @@ impl ScriptSigner for SecpMultisigScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let witness_idx = script_group.input_indices[0]; let mut witnesses: Vec = tx.witnesses().into_iter().collect(); @@ -388,6 +399,7 @@ impl ScriptSigner for AcpScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let args = script_group.script.args().raw_data(); let id = &args[0..20]; @@ -437,6 +449,7 @@ impl ScriptSigner for ChequeScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let args = script_group.script.args().raw_data(); let id = self.owner_id(args.as_ref()); @@ -445,6 +458,74 @@ impl ScriptSigner for ChequeScriptSigner { } } +pub const PERSONALIZATION_SIGHASH_ALL: &[u8] = b"ckb-tcob-sighash"; +pub const PERSONALIZATION_SIGHASH_ALL_ONLY: &[u8] = b"ckb-tcob-sgohash"; +pub const PERSONALIZATION_OTX: &[u8] = b"ckb-tcob-otxhash"; + +/// return a blake2b instance with personalization for SighashAll +pub fn new_sighash_all_blake2b() -> Blake2b { + Blake2bBuilder::new(32) + .personal(PERSONALIZATION_SIGHASH_ALL) + .build() +} + +/// return a blake2b instance with personalization for SighashAllOnly +pub fn new_sighash_all_only_blake2b() -> Blake2b { + Blake2bBuilder::new(32) + .personal(PERSONALIZATION_SIGHASH_ALL_ONLY) + .build() +} + +/// return a blake2b instance with personalization for OTX +pub fn new_otx_blake2b() -> Blake2b { + Blake2bBuilder::new(32) + .personal(PERSONALIZATION_OTX) + .build() +} + +pub fn cobuild_generate_signing_message_hash( + message: &Option, + tx_dep_provider: &dyn TransactionDependencyProvider, + tx: &TransactionView, +) -> Bytes { + // message + let mut hasher = match message { + Some(m) => { + let mut hasher = new_sighash_all_blake2b(); + hasher.update(&m); + hasher + } + None => new_sighash_all_only_blake2b(), + }; + // tx hash + hasher.update(tx.hash().as_slice()); + + // inputs cell and data + let inputs_len = tx.inputs().len(); + for i in 0..inputs_len { + let input_cell = tx.inputs().get(i).unwrap(); + let input_cell_out_point = input_cell.previous_output(); + let (input_cell, input_cell_data) = ( + tx_dep_provider.get_cell(&input_cell_out_point).unwrap(), + tx_dep_provider + .get_cell_data(&input_cell_out_point) + .unwrap(), + ); + hasher.update(input_cell.as_slice()); + hasher.update(&(input_cell_data.len() as u32).to_le_bytes()); + hasher.update(&input_cell_data); + } + // extra witnesses + for witness in tx.witnesses().into_iter().skip(inputs_len) { + hasher.update(&(witness.len() as u32).to_le_bytes()); + hasher.update(&witness.raw_data()); + } + + let mut result = vec![0u8; 32]; + hasher.finalize(&mut result); + Bytes::from(result) +} + /// Common logic of generate message for certain script group. Overwrite /// this method to support special use case. pub fn generate_message( @@ -554,22 +635,8 @@ impl OmniLockScriptSigner { fn sign_multisig_tx( &self, 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 = self.config.zero_lock(self.unlock_mode)?; - let zero_lock_len = zero_lock.len(); - let message = generate_message(&tx_new, script_group, zero_lock)?; - + message: Bytes, + ) -> Result { let multisig_config = match self.unlock_mode { OmniUnlockMode::Admin => self .config @@ -585,38 +652,13 @@ impl OmniLockScriptSigner { .filter(|id| self.signer.match_id(id.as_bytes())) .map(|id| self.signer.sign(id.as_bytes(), message.as_ref(), true, tx)) .collect::, SignerError>>()?; - // Put signature into witness - let witness_idx = script_group.input_indices[0]; - 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())? - }; - let lock_field = current_witness.lock().to_opt().map(|data| data.raw_data()); - let omnilock_witnesslock = if let Some(lock_field) = lock_field { - if lock_field.len() != zero_lock_len { - return Err(ScriptSignError::Other(anyhow!( - "invalid witness lock field length: {}, expected: {}", - lock_field.len(), - zero_lock_len, - ))); - } - OmniLockWitnessLock::from_slice(lock_field.as_ref())? - } else { - OmniLockWitnessLock::default() - }; let config_data = multisig_config.to_witness_data(); - let mut omni_sig = omnilock_witnesslock - .signature() - .to_opt() - .map(|data| data.raw_data().as_ref().to_vec()) - .unwrap_or_else(|| { - let mut omni_sig = - vec![0u8; config_data.len() + multisig_config.threshold() as usize * 65]; - omni_sig[..config_data.len()].copy_from_slice(&config_data); - omni_sig - }); + let mut omni_sig = { + let mut omni_sig = + vec![0u8; config_data.len() + multisig_config.threshold() as usize * 65]; + omni_sig[..config_data.len()].copy_from_slice(&config_data); + omni_sig + }; for signature in signatures { let mut idx = config_data.len(); while idx < omni_sig.len() { @@ -633,59 +675,18 @@ impl OmniLockScriptSigner { return Err(ScriptSignError::TooManySignatures); } } - let lock = omnilock_witnesslock - .as_builder() - .signature(Some(Bytes::from(omni_sig)).pack()) - .build() - .as_bytes(); - - current_witness = current_witness.as_builder().lock(Some(lock).pack()).build(); - witnesses[witness_idx] = current_witness.as_bytes().pack(); - Ok(tx.as_advanced_builder().set_witnesses(witnesses).build()) - } - - fn sign_ethereum_tx( - &self, - tx: &TransactionView, - script_group: &ScriptGroup, - id: &Identity, - ) -> 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 = self.config.zero_lock(self.unlock_mode())?; - let message = generate_message(&tx_new, script_group, zero_lock)?; - let message = convert_keccak256_hash(message.as_ref()); - - let signature = self - .signer - .sign(id.auth_content().as_ref(), 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())? - }; - - let lock = Self::build_witness_lock(current_witness.lock(), signature)?; - current_witness = current_witness.as_builder().lock(Some(lock).pack()).build(); - witnesses[witness_idx] = current_witness.as_bytes().pack(); - Ok(tx.as_advanced_builder().set_witnesses(witnesses).build()) + Ok(Bytes::from(omni_sig)) } /// Build proper witness lock pub fn build_witness_lock( orig_lock: BytesOpt, signature: Bytes, + use_rc: bool, + use_rc_identity: bool, + proofs: &SmtProofEntryVec, + identity: &omni_lock::Auth, + preimage: Option, ) -> Result { let lock_field = orig_lock.to_opt().map(|data| data.raw_data()); let omnilock_witnesslock = if let Some(lock_field) = lock_field { @@ -694,11 +695,24 @@ impl OmniLockScriptSigner { OmniLockWitnessLock::default() }; - Ok(omnilock_witnesslock - .as_builder() - .signature(Some(signature).pack()) - .build() - .as_bytes()) + let builder = omnilock_witnesslock.as_builder(); + + let mut builder = builder.signature(Some(signature).pack()); + + if builder.preimage.is_none() { + builder = builder.preimage(preimage.pack()); + } + + if use_rc && use_rc_identity && builder.omni_identity.is_none() { + let rc_identity = omni_lock::IdentityBuilder::default() + .identity(identity.clone()) + .proofs(proofs.clone()) + .build(); + let opt = omni_lock::IdentityOpt::new_unchecked(rc_identity.as_bytes()); + builder = builder.omni_identity(opt); + } + + Ok(builder.build().as_bytes()) } } @@ -760,6 +774,7 @@ impl ScriptSigner for OmniLockScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let id = match self.unlock_mode { OmniUnlockMode::Admin => self @@ -770,49 +785,115 @@ impl ScriptSigner for OmniLockScriptSigner { .clone(), OmniUnlockMode::Normal => self.config.id().clone(), }; - match id.flag() { - IdentityFlag::PubkeyHash => { - 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 = self.config.zero_lock(self.unlock_mode)?; - let message = generate_message(&tx_new, script_group, zero_lock)?; - - let signature = - self.signer - .sign(id.auth_content().as_ref(), 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())? - }; - let lock = Self::build_witness_lock(current_witness.lock(), signature)?; + 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(); - current_witness = current_witness.as_builder().lock(Some(lock).pack()).build(); - witnesses[witness_idx] = current_witness.as_bytes().pack(); - Ok(tx.as_advanced_builder().set_witnesses(witnesses).build()) + let message = if self.config.enable_cobuild { + cobuild_generate_signing_message_hash(&self.config.cobuild_message, tx_dep_provider, tx) + } else { + let zero_lock = self.config.zero_lock(self.unlock_mode)?; + generate_message(&tx_new, script_group, zero_lock)? + }; + let signature = match id.flag() { + IdentityFlag::PubkeyHash => { + self.signer + .sign(id.auth_content().as_ref(), message.as_ref(), true, tx)? } - IdentityFlag::Ethereum => self.sign_ethereum_tx(tx, script_group, &id), - IdentityFlag::Multisig => self.sign_multisig_tx(tx, script_group), - IdentityFlag::OwnerLock => { - // should not reach here, just return a clone for compatible reason. - Ok(tx.clone()) + IdentityFlag::Ethereum => { + let message = convert_keccak256_hash(message.as_ref()); + + self.signer + .sign(id.auth_content().as_ref(), message.as_ref(), true, tx)? } + IdentityFlag::Multisig => self.sign_multisig_tx(tx, message)?, + // IdentityFlag::OwnerLock => { + // // should not reach here, just return a clone for compatible reason. + // Ok(tx.clone()) + // } _ => { todo!("not supported yet"); } + }; + if self.config.enable_cobuild { + let witness_data = witnesses[witness_idx].raw_data(); + let current_witness: WitnessLayout = if witness_data.is_empty() { + WitnessLayout::default() + } else { + WitnessLayout::from_slice(witness_data.as_ref())? + }; + + let lock_field = match current_witness.to_enum() { + WitnessLayoutUnion::SighashAll(s) => { + OmniLockWitnessLock::from_slice(&s.as_reader().seal().raw_data()[1..]) + .unwrap() + .as_bytes() + } + WitnessLayoutUnion::SighashAllOnly(s) => { + OmniLockWitnessLock::from_slice(&s.as_reader().seal().raw_data()[1..]) + .unwrap() + .as_bytes() + } + _ => panic!("not support yet"), + }; + let lock = Self::build_witness_lock( + Some(lock_field).pack(), + signature, + self.config.use_rc(), + matches!(self.unlock_mode, OmniUnlockMode::Admin), + &self.config.build_proofs(), + &id.to_auth(), + None, + )?; + match &self.config.cobuild_message { + Some(msg) => { + let sighash_all = SighashAll::new_builder() + .message(Message::new_unchecked(msg.clone())) + .seal([Bytes::copy_from_slice(&[0x00u8]), lock].concat().pack()) + .build(); + let sighash_all = WitnessLayout::new_builder().set(sighash_all).build(); + let sighash_all = sighash_all.as_bytes(); + witnesses[witness_idx] = sighash_all.pack(); + } + None => { + let sighash_all_only = SighashAllOnly::new_builder() + .seal([Bytes::copy_from_slice(&[0x00u8]), lock].concat().pack()) + .build(); + let sighash_all_only = + WitnessLayout::new_builder().set(sighash_all_only).build(); + let sighash_all_only = sighash_all_only.as_bytes(); + witnesses[witness_idx] = sighash_all_only.pack(); + } + } + } else { + // 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())? + }; + + let lock = Self::build_witness_lock( + current_witness.lock(), + signature, + self.config.use_rc(), + matches!(self.unlock_mode, OmniUnlockMode::Admin), + &self.config.build_proofs(), + &id.to_auth(), + None, + )?; + current_witness = current_witness.as_builder().lock(Some(lock).pack()).build(); + witnesses[witness_idx] = current_witness.as_bytes().pack(); } + Ok(tx.as_advanced_builder().set_witnesses(witnesses).build()) } } diff --git a/src/unlock/unlocker.rs b/src/unlock/unlocker.rs index 9ff74160..3cb88ad6 100644 --- a/src/unlock/unlocker.rs +++ b/src/unlock/unlocker.rs @@ -91,24 +91,30 @@ pub fn fill_witness_lock( tx: &TransactionView, script_group: &ScriptGroup, lock_field: Bytes, + enable_cobuild: bool, ) -> 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 witness_data = witnesses[witness_idx].raw_data(); - let mut witness = if witness_data.is_empty() { - WitnessArgs::default() + + if enable_cobuild { + Ok(tx.clone()) } else { - WitnessArgs::from_slice(witness_data.as_ref()) - .map_err(|_| UnlockError::InvalidWitnessArgs(witness_idx))? - }; - if witness.lock().is_none() { - witness = witness.as_builder().lock(Some(lock_field).pack()).build(); + let witness_data = witnesses[witness_idx].raw_data(); + let mut witness = if witness_data.is_empty() { + WitnessArgs::default() + } else { + WitnessArgs::from_slice(witness_data.as_ref()) + .map_err(|_| UnlockError::InvalidWitnessArgs(witness_idx))? + }; + if witness.lock().is_none() { + witness = witness.as_builder().lock(Some(lock_field).pack()).build(); + witnesses[witness_idx] = witness.as_bytes().pack(); + } + Ok(tx.as_advanced_builder().set_witnesses(witnesses).build()) } - witnesses[witness_idx] = witness.as_bytes().pack(); - Ok(tx.as_advanced_builder().set_witnesses(witnesses).build()) } pub fn reset_witness_lock( @@ -160,9 +166,9 @@ impl ScriptUnlocker for SecpSighashUnlocker { &self, tx: &TransactionView, script_group: &ScriptGroup, - _tx_dep_provider: &dyn TransactionDependencyProvider, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { - Ok(self.signer.sign_tx(tx, script_group)?) + Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?) } fn fill_placeholder_witness( @@ -171,7 +177,7 @@ impl ScriptUnlocker for SecpSighashUnlocker { script_group: &ScriptGroup, _tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { - fill_witness_lock(tx, script_group, Bytes::from(vec![0u8; 65])) + fill_witness_lock(tx, script_group, Bytes::from(vec![0u8; 65]), false) } } @@ -197,9 +203,9 @@ impl ScriptUnlocker for SecpMultisigUnlocker { &self, tx: &TransactionView, script_group: &ScriptGroup, - _tx_dep_provider: &dyn TransactionDependencyProvider, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { - Ok(self.signer.sign_tx(tx, script_group)?) + Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?) } fn fill_placeholder_witness( @@ -212,7 +218,7 @@ impl ScriptUnlocker for SecpMultisigUnlocker { let config_data = config.to_witness_data(); let mut zero_lock = vec![0u8; config_data.len() + 65 * (config.threshold() as usize)]; zero_lock[0..config_data.len()].copy_from_slice(&config_data); - fill_witness_lock(tx, script_group, Bytes::from(zero_lock)) + fill_witness_lock(tx, script_group, Bytes::from(zero_lock), false) } } @@ -434,7 +440,7 @@ impl ScriptUnlocker for AcpUnlocker { if self.is_unlocked(tx, script_group, tx_dep_provider)? { self.clear_placeholder_witness(tx, script_group) } else { - Ok(self.signer.sign_tx(tx, script_group)?) + Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?) } } @@ -456,7 +462,7 @@ impl ScriptUnlocker for AcpUnlocker { if self.is_unlocked(tx, script_group, tx_dep_provider)? { Ok(tx.clone()) } else { - fill_witness_lock(tx, script_group, Bytes::from(vec![0u8; 65])) + fill_witness_lock(tx, script_group, Bytes::from(vec![0u8; 65]), false) } } } @@ -580,7 +586,7 @@ impl ScriptUnlocker for ChequeUnlocker { if self.is_unlocked(tx, script_group, tx_dep_provider)? { self.clear_placeholder_witness(tx, script_group) } else { - Ok(self.signer.sign_tx(tx, script_group)?) + Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?) } } @@ -602,7 +608,7 @@ impl ScriptUnlocker for ChequeUnlocker { if self.is_unlocked(tx, script_group, tx_dep_provider)? { Ok(tx.clone()) } else { - fill_witness_lock(tx, script_group, Bytes::from(vec![0u8; 65])) + fill_witness_lock(tx, script_group, Bytes::from(vec![0u8; 65]), false) } } } @@ -709,9 +715,9 @@ impl ScriptUnlocker for OmniLockUnlocker { &self, tx: &TransactionView, script_group: &ScriptGroup, - _tx_dep_provider: &dyn TransactionDependencyProvider, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { - Ok(self.signer.sign_tx(tx, script_group)?) + Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?) } fn fill_placeholder_witness( @@ -722,7 +728,7 @@ impl ScriptUnlocker for OmniLockUnlocker { ) -> Result { let config = self.signer.config(); let lock_field = config.placeholder_witness_lock(self.signer.unlock_mode())?; - fill_witness_lock(tx, script_group, lock_field) + fill_witness_lock(tx, script_group, lock_field, config.enable_cobuild) } } #[cfg(test)]