从 omnilock 讲起(四)

从 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,这样一个交易可验证的交易就构建完成了,之后就是测试交易是否能正常验证了

2 Likes