Skip to content

Commit

Permalink
[CLI] Support multisign in cli (#2552)
Browse files Browse the repository at this point in the history
* [cli] Support multi sigin account sign tx

* [types] Display public key as hex and BitcoinAddress serialize support is_human_readable

* [CLI] implement account create-multisign command

* [test] Add test for multisign

* [cli] tx sign command support signer arguments

* reset stdlib v8

* [cli] Fix reroot bug

* fixup examples

---------

Co-authored-by: baichuan3 <muzixinly@gmail.com>
  • Loading branch information
jolestar and baichuan3 authored Sep 2, 2024
1 parent 626ba4a commit 4b23b70
Show file tree
Hide file tree
Showing 32 changed files with 972 additions and 196 deletions.
2 changes: 2 additions & 0 deletions crates/rooch-key/src/keystore/account_keystore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ pub trait AccountKeystore {

fn get_accounts(&self, password: Option<String>) -> Result<Vec<LocalAccount>, anyhow::Error>;

fn contains_address(&self, address: &RoochAddress) -> bool;

fn add_address_encryption_data_to_keys(
&mut self,
address: RoochAddress,
Expand Down
6 changes: 5 additions & 1 deletion crates/rooch-key/src/keystore/base_keystore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ impl AccountKeystore for BaseKeyStore {
Ok(accounts.into_values().collect())
}

fn contains_address(&self, address: &RoochAddress) -> bool {
self.keys.contains_key(address)
}

// TODO: deal with the Rooch and Nostr's get_key_pair() function. Consider Nostr scenario
fn get_key_pair(
&self,
Expand Down Expand Up @@ -229,7 +233,7 @@ impl AccountKeystore for BaseKeyStore {
.decrypt_with_type(password)
.map_err(signature::Error::from_source)?;

let auth = authenticator::Authenticator::rooch(&kp, &msg);
let auth = authenticator::Authenticator::session(&kp, &msg);
Ok(RoochTransaction::new(msg, auth))
}

Expand Down
4 changes: 4 additions & 0 deletions crates/rooch-key/src/keystore/file_keystore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ impl AccountKeystore for FileBasedKeystore {
self.keystore.get_accounts(password)
}

fn contains_address(&self, address: &RoochAddress) -> bool {
self.keystore.contains_address(address)
}

fn add_address_encryption_data_to_keys(
&mut self,
address: RoochAddress,
Expand Down
4 changes: 4 additions & 0 deletions crates/rooch-key/src/keystore/memory_keystore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ impl AccountKeystore for InMemKeystore {
self.keystore.get_accounts(password)
}

fn contains_address(&self, address: &RoochAddress) -> bool {
self.keystore.contains_address(address)
}

fn add_address_encryption_data_to_keys(
&mut self,
address: RoochAddress,
Expand Down
7 changes: 7 additions & 0 deletions crates/rooch-key/src/keystore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ impl AccountKeystore for Keystore {
}
}

fn contains_address(&self, address: &RoochAddress) -> bool {
match self {
Keystore::File(file_keystore) => file_keystore.contains_address(address),
Keystore::InMem(inmem_keystore) => inmem_keystore.contains_address(address),
}
}

fn get_accounts(
&self,
password: Option<String>,
Expand Down
16 changes: 16 additions & 0 deletions crates/rooch-rpc-client/src/wallet_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use rooch_rpc_api::jsonrpc_types::{
use rooch_types::address::ParsedAddress;
use rooch_types::address::RoochAddress;
use rooch_types::addresses;
use rooch_types::crypto::RoochKeyPair;
use rooch_types::error::{RoochError, RoochResult};
use rooch_types::transaction::rooch::{RoochTransaction, RoochTransactionData};
use std::collections::BTreeMap;
Expand Down Expand Up @@ -164,6 +165,17 @@ impl WalletContext {
Ok(tx)
}

pub async fn sign_transaction(
&self,
signer: RoochAddress,
tx_data: RoochTransactionData,
) -> RoochResult<RoochTransaction> {
let tx = self
.keystore
.sign_transaction(&signer, tx_data, self.password.clone())?;
Ok(tx)
}

pub async fn execute(
&self,
tx: RoochTransaction,
Expand Down Expand Up @@ -193,6 +205,10 @@ impl WalletContext {
self.execute(tx).await
}

pub fn get_key_pair(&self, address: &RoochAddress) -> Result<RoochKeyPair> {
self.keystore.get_key_pair(address, self.password.clone())
}

pub async fn dry_run(
&self,
tx: RoochTransactionData,
Expand Down
31 changes: 30 additions & 1 deletion crates/rooch-types/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,11 +575,40 @@ impl TryFrom<u8> for BitcoinAddressPayloadType {
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[serde_as]
pub struct BitcoinAddress {
bytes: Vec<u8>,
}

impl Serialize for BitcoinAddress {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if serializer.is_human_readable() {
self.to_string().serialize(serializer)
} else {
self.bytes.serialize(serializer)
}
}
}

impl<'de> Deserialize<'de> for BitcoinAddress {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
if deserializer.is_human_readable() {
let s = <String>::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
} else {
let bytes = Vec::<u8>::deserialize(deserializer)?;
Ok(Self { bytes })
}
}
}

impl fmt::Display for BitcoinAddress {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
// Write the Bitcoin address as a hexadecimal string
Expand Down
85 changes: 85 additions & 0 deletions crates/rooch-types/src/bitcoin/multisign_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,39 @@ use bitcoin::taproot::TaprootBuilder;
use bitcoin::{ScriptBuf, XOnlyPublicKey};
use move_core_types::{account_address::AccountAddress, ident_str, identifier::IdentStr};
use moveos_types::moveos_std::tx_context::TxContext;
use moveos_types::state::{MoveStructState, MoveStructType};
use moveos_types::{
module_binding::{ModuleBinding, MoveFunctionCaller},
state::MoveState,
transaction::MoveAction,
};
use serde::{Deserialize, Serialize};

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

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParticipantInfo {
pub participant_address: AccountAddress,
pub participant_bitcoin_address: BitcoinAddress,
pub public_key: Vec<u8>,
}

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

impl MoveStructState for ParticipantInfo {
fn struct_layout() -> move_core_types::value::MoveStructLayout {
move_core_types::value::MoveStructLayout::new(vec![
AccountAddress::type_layout(),
BitcoinAddress::type_layout(),
Vec::<u8>::type_layout(),
])
}
}

pub fn generate_multisign_address(
threshold: usize,
public_keys: Vec<Vec<u8>>,
Expand Down Expand Up @@ -87,6 +112,10 @@ impl<'a> MultisignAccountModule<'a> {
const GENERATE_MULTISIGN_ADDRESS_FUNCTION_NAME: &'static IdentStr =
ident_str!("generate_multisign_address");
const IS_PARTICIPANT_FUNCTION_NAME: &'static IdentStr = ident_str!("is_participant");
const IS_MULTISIGN_ACCOUNT_FUNCTION_NAME: &'static IdentStr =
ident_str!("is_multisign_account");
const PARTICIPANTS_FUNCTION_NAME: &'static IdentStr = ident_str!("participants");
const THRESHOLD_FUNCTION_NAME: &'static IdentStr = ident_str!("threshold");

pub fn initialize_multisig_account_action(
threshold: u64,
Expand Down Expand Up @@ -146,6 +175,62 @@ impl<'a> MultisignAccountModule<'a> {
})?;
Ok(is_participant)
}

pub fn is_multisign_account(&self, multisign_address: AccountAddress) -> Result<bool> {
let function_call = Self::create_function_call(
Self::IS_MULTISIGN_ACCOUNT_FUNCTION_NAME,
vec![],
vec![multisign_address.to_move_value()],
);
let ctx = TxContext::new_readonly_ctx(AccountAddress::ZERO);
let is_multisign_account = self
.caller
.call_function(&ctx, function_call)?
.into_result()
.map(|mut values| {
let value = values.pop().expect("should have one return value");
bcs::from_bytes::<bool>(&value.value).expect("should be a valid bool")
})?;
Ok(is_multisign_account)
}

pub fn threshold(&self, multisign_address: AccountAddress) -> Result<u64> {
let function_call = Self::create_function_call(
Self::THRESHOLD_FUNCTION_NAME,
vec![],
vec![multisign_address.to_move_value()],
);
let ctx = TxContext::new_readonly_ctx(AccountAddress::ZERO);
let threshold = self
.caller
.call_function(&ctx, function_call)?
.into_result()
.map(|mut values| {
let value = values.pop().expect("should have one return value");
bcs::from_bytes::<u64>(&value.value).expect("should be a valid u64")
})?;
Ok(threshold)
}

pub fn participants(&self, multisign_address: AccountAddress) -> Result<Vec<ParticipantInfo>> {
let function_call = Self::create_function_call(
Self::PARTICIPANTS_FUNCTION_NAME,
vec![],
vec![multisign_address.to_move_value()],
);

let ctx = TxContext::new_readonly_ctx(AccountAddress::ZERO);
let participants = self
.caller
.call_function(&ctx, function_call)?
.into_result()
.map(|mut values| {
let value = values.pop().expect("should have one return value");
bcs::from_bytes::<Vec<ParticipantInfo>>(&value.value)
.expect("should be a valid vector of ParticipantInfo")
})?;
Ok(participants)
}
}

impl<'a> ModuleBinding<'a> for MultisignAccountModule<'a> {
Expand Down
59 changes: 55 additions & 4 deletions crates/rooch-types/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
error::{RoochError, RoochResult},
rooch_key::ROOCH_SECRET_KEY_HRP,
};
use anyhow::bail;
use anyhow::{anyhow, bail};
use bech32::{encode, Bech32, EncodeError};
use derive_more::{AsMut, AsRef, From};
pub use enum_dispatch::enum_dispatch;
Expand Down Expand Up @@ -280,7 +280,7 @@ impl Serialize for PublicKey {
where
S: Serializer,
{
let s = self.encode_base64();
let s = self.to_string();
serializer.serialize_str(&s)
}
}
Expand All @@ -292,8 +292,7 @@ impl<'de> Deserialize<'de> for PublicKey {
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
<PublicKey as EncodeDecodeBase64>::decode_base64(&s)
.map_err(|e| Error::custom(e.to_string()))
Self::from_str(s.as_str()).map_err(|e| Error::custom(e.to_string()))
}
}

Expand Down Expand Up @@ -347,6 +346,58 @@ impl PublicKey {
_ => bail!("Only secp256k1 public key can be converted to nostr bech32 public key"),
}
}

pub fn to_hex(&self) -> String {
hex::encode(self.as_ref())
}

pub fn to_hex_literal(&self) -> String {
format!("0x{}", self.to_hex())
}

pub fn from_hex(hex: &str) -> Result<Self, anyhow::Error> {
let bytes = hex::decode(hex.strip_prefix("0x").unwrap_or(hex))?;
match SignatureScheme::from_flag_byte(
*bytes
.first()
.ok_or_else(|| anyhow!("Invalid public key length"))?,
) {
Ok(x) => match x {
SignatureScheme::Ed25519 => {
let pk: Ed25519PublicKey = Ed25519PublicKey::from_bytes(
bytes
.get(1..)
.ok_or_else(|| anyhow!("Invalid public key length"))?,
)?;
Ok(PublicKey::Ed25519((&pk).into()))
}
SignatureScheme::Secp256k1 => {
let pk: Secp256k1PublicKey = Secp256k1PublicKey::from_bytes(
bytes
.get(1..)
.ok_or_else(|| anyhow!("Invalid public key length"))?,
)?;
Ok(PublicKey::Secp256k1((&pk).into()))
}
},
Err(e) => Err(anyhow!("Invalid bytes :{}", e)),
}
}
}

impl std::fmt::Display for PublicKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_hex_literal())
}
}

impl FromStr for PublicKey {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let pk = Self::from_hex(s).map_err(|e| anyhow!("{}", e.to_string()))?;
Ok(pk)
}
}

pub trait RoochPublicKey: VerifyingKey {
Expand Down
34 changes: 33 additions & 1 deletion crates/rooch-types/src/framework/auth_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
crypto::{RoochSignature, Signature, SignatureScheme},
transaction::RoochTransactionData,
};
use anyhow::Result;
use anyhow::{ensure, Result};
use fastcrypto::{
hash::Sha256,
secp256k1::{Secp256k1PublicKey, Secp256k1Signature},
Expand Down Expand Up @@ -139,6 +139,10 @@ impl AuthPayload {
pk.verify_with_hash::<Sha256>(&message_hash, &signature)?;
Ok(())
}

pub fn from_address(&self) -> Result<String> {
Ok(String::from_utf8(self.from_address.to_vec())?)
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand All @@ -149,6 +153,34 @@ pub struct MultisignAuthPayload {
pub public_keys: Vec<Vec<u8>>,
}

impl MultisignAuthPayload {
pub fn build_multisig_payload(mut payloads: Vec<AuthPayload>) -> Result<Self> {
ensure!(payloads.len() > 1, "At least two signatures are required");
let first_payload = payloads.remove(0);
let message_prefix = first_payload.message_prefix.clone();
let message_info = first_payload.message_info.clone();
let mut signatures = vec![first_payload.signature];
let mut public_keys = vec![first_payload.public_key];
for payload in payloads {
ensure!(
payload.message_prefix == message_prefix,
"All signatures must have the same message prefix"
);
ensure!(
payload.message_info == message_info,
"All signatures must have the same message info"
);
signatures.push(payload.signature);
public_keys.push(payload.public_key);
}
Ok(Self {
signatures,
public_keys,
message_prefix,
message_info,
})
}
}
impl MoveStructType for MultisignAuthPayload {
const ADDRESS: AccountAddress = ROOCH_FRAMEWORK_ADDRESS;
const MODULE_NAME: &'static IdentStr = MODULE_NAME;
Expand Down
Loading

0 comments on commit 4b23b70

Please sign in to comment.