从 omnilock 讲起(四)
上两篇讲了 omnilock 的通用签名 message 构造和 args 构造,这一篇讲解签名过程和 witness 构造。
签名部分
不同的算法签名方式也不相同,下面分别介绍
Pubkeyhash
拿到第二篇的 message ,正常签名即可
pub fn serialize_signature(signature: &secp256k1::ecdsa::RecoverableSignature) -> [u8; 65] {
let (recov_id, data) = signature.serialize_compact();
let mut signature_bytes = [0u8; 65];
signature_bytes[0..64].copy_from_slice(&data[0..64]);
signature_bytes[64] = recov_id.to_i32() as u8;
signature_bytes
}
fn sign(privkey: secp256k1::Privkey, message: Vec<u8>) -> Vec<u8> {
let sign = secp256k1::Secp256k1::new().sign_ecdsa_recoverable(&message, privkey);
serialize_signature(&sig).to_vec()
}
Ethereum
需要先将 message 转换为 ethereum style message,然后签名
pub fn convert_keccak256_hash(message: &[u8]) -> H256 {
let eth_prefix: &[u8; 28] = b"\x19Ethereum Signed Message:\n32";
let mut hasher = Keccak256::new();
hasher.update(eth_prefix);
hasher.update(message);
let r = hasher.finalize();
H256::from_slice(r.as_slice()).expect("convert_keccak256_hash")
}
fn sign(privkey: secp256k1::Privkey, message: Vec<u8>) -> Vec<u8> {
let eth_msg = convert_keccak256_hash(&message);
let sign = secp256k1::Secp256k1::new().sign_ecdsa_recoverable(eth_msg.as_ref(), privkey);
serialize_signature(&sig).to_vec()
}
EthereumDisplay
需要先将 message 转换为 ethereum display style message,然后签名
pub const COMMON_PREFIX: &str = "CKB transaction: 0x";
pub fn convert_eth_dispaly_hash(message: &[u8]) -> H256 {
let eth_prefix = b"\x19Ethereum Signed Message:\n";
let mut hasher = Keccak256::new();
hasher.update(eth_prefix);
hasher.update(Bytes::from(format!(
"{}",
COMMON_PREFIX.len() + message.len() * 2
)));
hasher.update(Bytes::from(COMMON_PREFIX));
hasher.update(hex_encode(&message));
let r = hasher.finalize();
H256::from_slice(r.as_slice()).expect("convert_keccak256_hash")
}
fn sign(privkey: secp256k1::Privkey, message: Vec<u8>) -> Vec<u8> {
let eth_display_msg = convert_eth_dispaly_hash(&message);
let sign = secp256k1::Secp256k1::new().sign_ecdsa_recoverable(eth_display_msg.as_ref(), privkey);
serialize_signature(&sig).to_vec()
}
Bitcoin
需要先将 message 转换为 btc style message,然后签名,签名结果还需要根据 vtype 进行一些变换
pub const BTC_PREFIX: &str = "CKB (Bitcoin Layer) transaction: 0x";
pub fn btc_convert_message(msg: &[u8]) -> [u8; 32] {
let message_magic = b"Bitcoin Signed Message:\n";
let msg_hex = hex_encode(msg);
assert_eq!(msg_hex.len(), 64);
let mut temp2 =
Vec::with_capacity(1 + message_magic.len() + 1 + BTC_PREFIX.len() + msg_hex.len());
temp2.put_u8(message_magic.len() as u8);
temp2.put(message_magic.as_slice());
temp2.put_u8(0x40 + BTC_PREFIX.len() as u8);
temp2.put(format!("{}{}", BTC_PREFIX, msg_hex).as_bytes());
let msg = calculate_sha256(&temp2);
calculate_sha256(&msg)
}
pub fn btc_convert_sign(sign: Vec<u8>, vtype: BTCSignVtype) -> Vec<u8> {
let recid = sign[64];
let mark = recid + vtype as u8;
let mut ret = Vec::with_capacity(65);
ret.put_u8(mark);
ret.put(&sign[0..64]);
ret
}
fn sign(privkey: secp256k1::Privkey, message: Vec<u8>, vtype: BTCSignVtype) -> Vec<u8> {
let btc_msg = btc_convert_message(&message);
let sign = secp256k1::Secp256k1::new().sign_ecdsa_recoverable(btc_msg.as_ref(), privkey);
let serde_sign = serialize_signature(&sig).to_vec();
btc_convert_sign(serde_sign, vtype)
}
Eos
直接签名,签名结果还需要根据 vtype 进行一些变换
fn sign(privkey: secp256k1::Privkey, message: Vec<u8>, vtype: BTCSignVtype) -> Vec<u8> {
let sign = secp256k1::Secp256k1::new().sign_ecdsa_recoverable(&message, privkey);
let serde_sign = serialize_signature(&sig).to_vec();
btc_convert_sign(serde_sign, vtype)
}
Dogecoin
需要先将 message 转换为 dogecoin style message,然后签名,签名结果还需要根据 vtype 进行一些变换
pub fn dog_convert_message(msg: &[u8]) -> [u8; 32] {
let message_magic = b"\x19Dogecoin Signed Message:\n\x40";
let msg_hex = hex_encode(&msg);
assert_eq!(msg_hex.len(), 64);
let mut temp2 = Vec::with_capacity(message_magic.len() + msg_hex.len());
temp2.put(message_magic.as_slice());
temp2.put(msg_hex.as_bytes());
let msg = calculate_sha256(&temp2);
calculate_sha256(&msg)
}
fn sign(privkey: secp256k1::Privkey, message: Vec<u8>, vtype: BTCSignVtype) -> Vec<u8> {
let dog_msg = dog_convert_message(&message);
let sign = secp256k1::Secp256k1::new().sign_ecdsa_recoverable(dog_msg.as_ref(), privkey);
let serde_sign = serialize_signature(&sig).to_vec();
btc_convert_sign(serde_sign, vtype)
}
Tron
需要先将 message 转换为 tron style message,然后签名
pub fn tron_convert_message(msg: &[u8]) -> H256 {
let tron_prefix: &[u8; 24] = b"\x19TRON Signed Message:\n32";
let mut hasher = Keccak256::new();
hasher.update(tron_prefix);
hasher.update(msg);
H256::from_slice(r.as_slice()).expect("convert_keccak256_hash")
}
fn sign(privkey: secp256k1::Privkey, message: Vec<u8>) -> Vec<u8> {
let tron_msg = tron_convert_message(&message);
let sign = secp256k1::Secp256k1::new().sign_ecdsa_recoverable(tron_msg.as_ref(), privkey);
serialize_signature(&sig).to_vec()
}
Solana
需要先将 message 转换为 solana style message,然后签名
pub fn solana_convert_message(msg: &[u8]) -> Vec<u8> {
let mut preifx = b"CKB transaction: 0x".to_vec();
preifx.extend(hex_encode(&message).as_bytes());
prefix
}
fn sign(mut privkey: ed25519_dalek::SigningKey, message: Vec<u8>) -> Vec<u8> {
let solana_msg = solana_convert_message(&message);
let sign = privkey.sign(&solana_msg);
let verifying_key = key.verifying_key();
let mut sig_plus_pubkey = sig.to_vec();
sig_plus_pubkey.extend(verifying_key.as_bytes());
}
Multisign
分为 admin 模式和 normal 模式,两种状态下,是两套私钥(RCE 配置的私钥与一般的私钥)
签名过程构造过程:
- 首先,依次用参与签名的私钥对 message 进行签名
- 获取 auth hash 之前的数据
- 将 auth 数据和签名拼接在一起
fn multisign_auth(require_first_n: u8, threshold: u8, sign_addresses: &[H160]) -> Vec<u8> {
let mut res = vec![
0, // reserved_byte
require_first_n,
threshold,
sign_addresses.len() as u8,
];
for sighash_address in sighash_addresses {
res.extend_from_slice(sighash_address.as_bytes());
}
res
}
fn sign(pks: Vec<Secp256k1::Privkey>, message: &[u8], require_first_n: u8, threshold: u8, sign_addresses: &[H160]) -> Vec<u8> {
let context = secp256k1::Secp256k1::new();
let signatures = pks.iter().map(|pk| {
let sig= context.sign_ecdsa_recoverable(dog_msg.as_ref(), privkey);
let serde_sign = serialize_signature(&sig).to_vec()
}).collect();
let config_data = multisign_auth(require_first_n, threshold, sign_addresses);
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);
for sig in signatures {
let mut idx = config_data.len();
while idx < omni_sig.len() {
if omni_sig[idx..idx + 65] == signature {
break;
} else if omni_sig[idx..idx + 65] == [0u8; 65] {
omni_sig[idx..idx + 65].copy_from_slice(signature.as_ref());
break;
}
idx += 65;
}
}
omni_sig
}
DL and Exec
这两个 flag 并没有固定的签名方式,全看 script 指向,根据 script 自行签名即可
Ownerlock
不需要签名,只看包含指向的 script 的 input cell 是否解锁成功
Witness 部分
omnilock 的 witnesslock 是一个 molecule 结构:
import xudt_rce;
array Auth[byte; 21];
table Identity {
identity: Auth,
// Regulation Compliance Extension (RCE)
proofs: SmtProofEntryVec,
}
option IdentityOpt (Identity);
// the data structure used in lock field of witness
table OmniLockWitnessLock {
signature: BytesOpt,
omni_identity: IdentityOpt,
preimage: BytesOpt,
}
普通模式
从 tx 中加载出原始的 lock 之后,将 witness_lock 中的 signature 替换掉,如果存在是 dl 或者 exec 模式,需要加上对应的 preimage(第二篇有提及如何计算),如果需要使用 rce 并开启 admin 模式,则需要将 proof 和 admin 的 identity 放入 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<Bytes>,
) -> Result<Bytes, ScriptSignError> {
let lock_field = orig_lock.to_opt().map(|data| data.raw_data());
let omnilock_witnesslock = if let Some(lock_field) = lock_field {
OmniLockWitnessLock::from_slice(lock_field.as_ref())?
} else {
OmniLockWitnessLock::default()
};
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())
}
fn build_witness(tx: TransactionView, witness_idx: usize, use_rc: bool, use_rc_identity: bool, proofs: SmtProofEntryVec, admin_identity: &omni_lock::Auth, preimage: Option<Bytes>, signature: Bytes) -> WitnessArgs {
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 = build_witness_lock(current_witness.lock(), signature, use_rc, use_rc_identity, proofs, admin_identity, preimage);
current_witness.as_builder().lock(Some(lock).pack()).build()
}
cobuild 模式
cobuild 不仅改变了 message 的构造方式,还将 witness 的结构也改了,它根据 cobuild message 存在与否,用 WitnessLayout 替换掉了 WitnessArgs。
table Action {
script_info_hash: Byte32, // script info
script_hash: Byte32, // script
data: Bytes, // action data
}
vector ActionVec <Action>;
table Message {
actions: ActionVec,
}
table SighashAll {
message: Message,
seal: Bytes,
}
table SighashAllOnly {
seal: Bytes,
}
union WitnessLayout {
SighashAll: 4278190081,
SighashAllOnly: 4278190082,
Otx: 4278190083,
OtxStart: 4278190084,
}
fn build_witness(tx: TransactionView, witness_idx: usize, use_rc: bool, use_rc_identity: bool, proofs: SmtProofEntryVec, admin_identity: &omni_lock::Auth, preimage: Option<Bytes>, signature: Bytes, cobuild_message: Option<Bytes>) -> Bytes {
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 = build_witness_lock(Some(lock_field).pack(), signature, use_rc, use_rc_identity, proofs, admin_identity, preimage);
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();
sighash_all.as_bytes().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();
sighash_all_only.as_bytes().pack()
}
}
}
看代码仔细的同学可能注意到,构建好的 lock 在放入 seal 字段的时候,前面多放了 0x00u8
这样一个东西,它是保留位,目前无作用,写死就行。
小结
以上就是交易构造的签名和 witness 构建,然后统一将 witness 替换成已签名的 witness,这样一个交易可验证的交易就构建完成了,之后就是测试交易是否能正常验证了