Skip to content

Commit

Permalink
[framework] Refactor the auth payload encoding and implement bitcoin …
Browse files Browse the repository at this point in the history
…multisign validator (#2422)

* [framework] Refactor the auth payload encoding

* fix compatibility issue

* [framework] Implement bitcoin_multisign_validator

* [test] Add tests to bitcoin multisign validator

* [framework] Define BitcoinMultisignValidator id

* [multisign_account] Execute l2 tx via multisign account

* [multisign_account] Split multisign_account and multisign_wallet

* [auth_validator] Migrate bitcoin_multisign_validator AuthPayload to auth_payload

* fixup
  • Loading branch information
jolestar authored Aug 16, 2024
1 parent a557d31 commit d984259
Show file tree
Hide file tree
Showing 26 changed files with 1,237 additions and 246 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) RoochNetwork
// SPDX-License-Identifier: Apache-2.0

use crate::binding_test;
use move_core_types::u256::U256;
use moveos_types::module_binding::MoveFunctionCaller;
use moveos_types::state::MoveStructType;
use moveos_types::transaction::{MoveAction, MoveOSTransaction};
use rooch_types::crypto::{RoochKeyPair, RoochSignature};
use rooch_types::framework::auth_payload::{MultisignAuthPayload, SignData};
use rooch_types::framework::auth_validator::BuiltinAuthValidator;
use rooch_types::framework::empty::Empty;
use rooch_types::framework::gas_coin::GasCoin;
use rooch_types::framework::transfer::TransferModule;
use rooch_types::nursery::bitcoin_multisign_validator::BitcoinMultisignValidatorModule;
use rooch_types::nursery::multisign_account::{self, MultisignAccountModule};
use rooch_types::transaction::rooch::RoochTransactionData;
use rooch_types::transaction::{Authenticator, RoochTransaction};

#[tokio::test]
async fn test_validate() {
let mut binding_test = binding_test::RustBindingTest::new().unwrap();
let root = binding_test.root().clone();

let kp1 = RoochKeyPair::generate_secp256k1();
let kp2 = RoochKeyPair::generate_secp256k1();
let kp3 = RoochKeyPair::generate_secp256k1();

let u1 = kp1.public().bitcoin_address().unwrap().to_rooch_address();
//let u2 = kp2.public().bitcoin_address().unwrap().to_rooch_address();
//let u3 = kp3.public().bitcoin_address().unwrap().to_rooch_address();

let pubkeys = vec![
kp1.bitcoin_public_key().unwrap(),
kp2.bitcoin_public_key().unwrap(),
kp3.bitcoin_public_key().unwrap(),
];

let pubkeys = pubkeys
.into_iter()
.map(|pk| pk.to_bytes())
.collect::<Vec<_>>();

let bitcoin_address_from_rust =
multisign_account::generate_multisign_address(2, pubkeys.clone()).unwrap();
//println!("bitcoin_address_from_rust: {}", bitcoin_address_from_rust);

//Initialize the multisign account
let action = MultisignAccountModule::initialize_multisig_account_action(2, pubkeys.to_vec());
let tx_data = RoochTransactionData::new_for_test(u1, 0, action);
let tx = tx_data.sign(&kp1);
binding_test.execute(tx).unwrap();

let multisign_address = bitcoin_address_from_rust.to_rooch_address();

//transfer gas free to multisign account

let gas_action = TransferModule::create_transfer_coin_action(
GasCoin::struct_tag(),
multisign_address.into(),
U256::from(1000000000u128),
);

let gas_tx_data = RoochTransactionData::new_for_test(u1, 1, gas_action);
let gas_tx = gas_tx_data.sign(&kp1);
binding_test.execute(gas_tx).unwrap();

let sender = multisign_address;
let sequence_number = 0;
let action = MoveAction::new_function_call(Empty::empty_function_id(), vec![], vec![]);
let tx_data = RoochTransactionData::new_for_test(sender, sequence_number, action);

let sign_data = SignData::new_with_default(&tx_data);
let data_hash = sign_data.data_hash();

let signature1 = kp1.sign(data_hash.as_bytes());
let signature2 = kp2.sign(data_hash.as_bytes());

let message_info = sign_data.message_info_without_tx_hash();
let payload = MultisignAuthPayload {
signatures: vec![
signature1.signature_bytes().to_vec(),
signature2.signature_bytes().to_vec(),
],
message_prefix: sign_data.message_prefix,
message_info,
public_keys: pubkeys[0..2].to_vec(),
};

let authenticator = Authenticator::new(
BuiltinAuthValidator::BitcoinMultisign.flag().into(),
bcs::to_bytes(&payload).unwrap(),
);

let tx = RoochTransaction::new(tx_data, authenticator);

let auth_info = tx.authenticator_info();

//Test the validate function
{
let move_tx: MoveOSTransaction = tx.clone().into_moveos_transaction(root);

let validator_caller = binding_test.as_module_binding::<BitcoinMultisignValidatorModule>();
let result = validator_caller.validate(&move_tx.ctx, auth_info.authenticator.payload);
assert!(result.is_ok());
}

//Execute the multisign transaction
binding_test.execute(tx).unwrap();
}
1 change: 1 addition & 0 deletions crates/rooch-framework-tests/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) RoochNetwork
// SPDX-License-Identifier: Apache-2.0

mod bitcoin_multisign_validator_tests;
mod bitcoin_test;
mod bitcoin_validator_tests;
mod brc20_test;
Expand Down
2 changes: 1 addition & 1 deletion crates/rooch-genesis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ mod tests {

#[tokio::test]
async fn test_custom_genesis_init() {
let network = RoochNetwork::new(100.into(), BuiltinChainID::Local.genesis_config().clone());
let network = RoochNetwork::new(100.into(), BuiltinChainID::Test.genesis_config().clone());
let genesis = RoochGenesis::build(network.clone()).unwrap();
genesis_init_test_case(network, genesis);
}
Expand Down
101 changes: 74 additions & 27 deletions crates/rooch-types/src/framework/auth_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{
crypto::{RoochSignature, Signature, SignatureScheme},
transaction::RoochTransactionData,
};
use anyhow::Result;
use fastcrypto::{
hash::Sha256,
secp256k1::{Secp256k1PublicKey, Secp256k1Signature},
Expand All @@ -20,38 +21,48 @@ use serde::{Deserialize, Serialize};

pub const MODULE_NAME: &IdentStr = ident_str!("auth_payload");

const MESSAGE_INFO_PREFIX: &[u8] = b"\x18Bitcoin Signed Message:\n";
/// The original message prefix of the Bitcoin wallet includes the length of the message `x18`
/// We remove the length because the bcs serialization format already contains the length information
const MESSAGE_INFO_PREFIX: &[u8] = b"Bitcoin Signed Message:\n";
const MESSAGE_INFO: &[u8] = b"Rooch Transaction:\n";

const TX_HASH_HEX_LENGTH: usize = 64;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignData {
pub message_prefix: Vec<u8>,
pub message_info: Vec<u8>,
pub tx_hash_hex: Vec<u8>,
}

impl SignData {
pub fn new(tx_data: &RoochTransactionData) -> Self {
let tx_hash_hex = hex::encode(tx_data.tx_hash().as_bytes()).into_bytes();
let message_info = MESSAGE_INFO.to_vec();

// We simulate the format of the Bitcoin wallet, append the length of message info and tx hash to the prefix
let mut encode_message_prefix = MESSAGE_INFO_PREFIX.to_vec();
encode_message_prefix.push((message_info.len() + tx_hash_hex.len()) as u8);

pub fn new(
message_prefix: Vec<u8>,
message_info_without_tx_hash: Vec<u8>,
tx_data: &RoochTransactionData,
) -> Self {
let message_info = {
let tx_hash_hex = hex::encode(tx_data.tx_hash().as_bytes()).into_bytes();
let mut message_info = message_info_without_tx_hash;
message_info.extend_from_slice(&tx_hash_hex);
message_info
};
SignData {
message_prefix: encode_message_prefix,
message_prefix,
message_info,
tx_hash_hex,
}
}

pub fn new_with_default(tx_data: &RoochTransactionData) -> Self {
Self::new(MESSAGE_INFO_PREFIX.to_vec(), MESSAGE_INFO.to_vec(), tx_data)
}

pub fn encode(&self) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&self.message_prefix);
data.extend_from_slice(&self.message_info);
data.extend_from_slice(&self.tx_hash_hex);
data
bcs::to_bytes(self).expect("Serialize SignData should success")
}

/// The message info without tx hash, the verifier should append the tx hash to the message info
pub fn message_info_without_tx_hash(&self) -> Vec<u8> {
self.message_info[..self.message_info.len() - TX_HASH_HEX_LENGTH].to_vec()
}

pub fn data_hash(&self) -> H256 {
Expand All @@ -66,7 +77,7 @@ pub struct AuthPayload {
pub signature: Vec<u8>,
// Some wallets add magic prefixes, such as unisat adding 'Bitcoin Signed Message:\n'
pub message_prefix: Vec<u8>,
// Description of a user-defined signature
// Description of a user-defined signature, the message info does not include the tx hash
pub message_info: Vec<u8>,
// Public key of address
pub public_key: Vec<u8>,
Expand Down Expand Up @@ -105,24 +116,23 @@ impl MoveStructState for AuthPayload {
impl AuthPayload {
pub fn new(sign_data: SignData, signature: Signature, bitcoin_address: String) -> Self {
debug_assert_eq!(signature.scheme(), SignatureScheme::Secp256k1);

let message_info = sign_data.message_info_without_tx_hash();
AuthPayload {
signature: signature.signature_bytes().to_vec(),
message_prefix: sign_data.message_prefix,
message_info: sign_data.message_info,
message_info,
public_key: signature.public_key_bytes().to_vec(),
from_address: bitcoin_address.into_bytes(),
}
}

pub fn verify(&self, tx_data: &RoochTransactionData) -> Result<(), anyhow::Error> {
pub fn verify(&self, tx_data: &RoochTransactionData) -> Result<()> {
let pk = Secp256k1PublicKey::from_bytes(&self.public_key)?;
let tx_hash_hex = hex::encode(tx_data.tx_hash().as_bytes()).into_bytes();
let sign_data = SignData {
message_prefix: self.message_prefix.clone(),
message_info: self.message_info.clone(),
tx_hash_hex,
};
let sign_data = SignData::new(
self.message_prefix.clone(),
self.message_info.clone(),
tx_data,
);
let message = sign_data.encode();
let message_hash = sha2_256_of(&message).0.to_vec();
let signature = Secp256k1Signature::from_bytes(&self.signature)?;
Expand All @@ -131,6 +141,43 @@ impl AuthPayload {
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MultisignAuthPayload {
pub signatures: Vec<Vec<u8>>,
pub message_prefix: Vec<u8>,
pub message_info: Vec<u8>,
pub public_keys: Vec<Vec<u8>>,
}

impl MoveStructType for MultisignAuthPayload {
const ADDRESS: AccountAddress = ROOCH_FRAMEWORK_ADDRESS;
const MODULE_NAME: &'static IdentStr = MODULE_NAME;
const STRUCT_NAME: &'static IdentStr = ident_str!("MultisignAuthPayload");
}

impl MoveStructState for MultisignAuthPayload {
fn struct_layout() -> move_core_types::value::MoveStructLayout {
move_core_types::value::MoveStructLayout::new(vec![
move_core_types::value::MoveTypeLayout::Vector(Box::new(
move_core_types::value::MoveTypeLayout::Vector(Box::new(
move_core_types::value::MoveTypeLayout::U8,
)),
)),
move_core_types::value::MoveTypeLayout::Vector(Box::new(
move_core_types::value::MoveTypeLayout::U8,
)),
move_core_types::value::MoveTypeLayout::Vector(Box::new(
move_core_types::value::MoveTypeLayout::U8,
)),
move_core_types::value::MoveTypeLayout::Vector(Box::new(
move_core_types::value::MoveTypeLayout::Vector(Box::new(
move_core_types::value::MoveTypeLayout::U8,
)),
)),
])
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
24 changes: 14 additions & 10 deletions crates/rooch-types/src/framework/auth_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use crate::address::{BitcoinAddress, RoochSupportedAddress};
use crate::addresses::ROOCH_FRAMEWORK_ADDRESS;
use crate::error::RoochError;
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::ValueEnum;
use framework_types::addresses::ROOCH_NURSERY_ADDRESS;
use move_core_types::value::MoveValue;
Expand Down Expand Up @@ -47,19 +47,22 @@ pub const MODULE_NAME: &IdentStr = ident_str!("auth_validator");
pub enum BuiltinAuthValidator {
Rooch,
Bitcoin,
BitcoinMultisign,
Ethereum,
}

impl BuiltinAuthValidator {
const ROOCH_FLAG: u8 = 0x00;
const BITCOIN_FLAG: u8 = 0x01;
const ETHEREUM_FLAG: u8 = 0x02;
const BITCOIN_MULTISIGN: u8 = 0x02;
const ETHEREUM_FLAG: u8 = 0x03;

pub fn flag(&self) -> u8 {
match self {
BuiltinAuthValidator::Rooch => Self::ROOCH_FLAG,
BuiltinAuthValidator::Ethereum => Self::ETHEREUM_FLAG,
BuiltinAuthValidator::Bitcoin => Self::BITCOIN_FLAG,
BuiltinAuthValidator::BitcoinMultisign => Self::BITCOIN_MULTISIGN,
BuiltinAuthValidator::Ethereum => Self::ETHEREUM_FLAG,
}
}

Expand All @@ -74,6 +77,7 @@ impl BuiltinAuthValidator {
match byte_int {
Self::ROOCH_FLAG => Ok(BuiltinAuthValidator::Rooch),
Self::BITCOIN_FLAG => Ok(BuiltinAuthValidator::Bitcoin),
Self::BITCOIN_MULTISIGN => Ok(BuiltinAuthValidator::BitcoinMultisign),
Self::ETHEREUM_FLAG => Ok(BuiltinAuthValidator::Ethereum),
_ => Err(RoochError::KeyConversionError(
"Invalid key auth validator".to_owned(),
Expand All @@ -93,6 +97,12 @@ impl BuiltinAuthValidator {
module_address: ROOCH_FRAMEWORK_ADDRESS,
module_name: MoveString::from_str("bitcoin_validator").expect("Should be valid"),
},
BuiltinAuthValidator::BitcoinMultisign => AuthValidator {
id: self.flag().into(),
module_address: ROOCH_NURSERY_ADDRESS,
module_name: MoveString::from_str("bitcoin_multisign_validator")
.expect("Should be valid"),
},
BuiltinAuthValidator::Ethereum => AuthValidator {
id: self.flag().into(),
module_address: ROOCH_NURSERY_ADDRESS,
Expand Down Expand Up @@ -221,12 +231,6 @@ impl<'a> AuthValidatorCaller<'a> {
);
self.caller
.call_function(ctx, auth_validator_call)?
.decode(|values| {
ensure!(
!values.is_empty(),
"Unexpect validate function return values"
);
Ok(())
})
.decode(|_values| Ok(()))
}
}
Loading

0 comments on commit d984259

Please sign in to comment.