Skip to content

Commit

Permalink
[gh-2295] support bitcoin multisig address (#2298)
Browse files Browse the repository at this point in the history
* [gh-2295] from verify_with_pk to verify_bitcoin_address_with_public_key.

* [gh-2295] add derive_multi_sign_address.

* [gh-2295] add derive_multisig_xonly_pubkey_from_public_keys and derive_bitcoin_taproot_address_from_multisig_xonly_pubkey.

* [gh-2295] add functions of multisig xonly pubkeys.

* [gh-2295] roolback from parse to new.

* [gh-2295] make the newly added functions optional.

* [gh-2295] fix move args.

* [gh-2295] add tests and leave function unchanged.

---------

Co-authored-by: Feliciss <10203-feliciss@users.noreply.0xacab.org>
  • Loading branch information
feliciss and Feliciss authored Jul 31, 2024
1 parent bd36352 commit c9b7597
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 18 deletions.
28 changes: 28 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ rustc-hash = { version = "2.0.0" }
xorf = { version = "0.11.0" }
vergen-git2 = { version = "1.0.0", features = ["build", "cargo", "rustc"] }
vergen-pretty = "0.3.4"
musig2 = { version = "0.0.11" }

# Note: the BEGIN and END comments below are required for external tooling. Do not remove.
# BEGIN MOVE DEPENDENCIES
Expand Down
2 changes: 1 addition & 1 deletion crates/rooch-types/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ impl BitcoinAddress {
pub fn new_witness_program(witness_program: &bitcoin::WitnessProgram) -> Self {
// First byte is BitcoinAddress Payload type
let mut bytes = vec![BitcoinAddressPayloadType::WitnessProgram.to_num()];
// Third byte represents Version 0 or PUSHNUM_1-PUSHNUM_16
// Second byte represents Version 0 or PUSHNUM_1-PUSHNUM_16
bytes.push(witness_program.version().to_num());
// Remain are Program data
bytes.extend_from_slice(witness_program.program().as_bytes());
Expand Down
1 change: 1 addition & 0 deletions frameworks/rooch-framework/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ smallvec = { workspace = true }
hex = { workspace = true }
tracing = { workspace = true }
bitcoin = { workspace = true }
musig2 = { workspace = true }

move-binary-format = { workspace = true }
move-bytecode-utils = { workspace = true }
Expand Down
85 changes: 83 additions & 2 deletions frameworks/rooch-framework/doc/bitcoin_address.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
- [Function `from_string`](#0x3_bitcoin_address_from_string)
- [Function `verify_with_public_key`](#0x3_bitcoin_address_verify_with_public_key)
- [Function `to_rooch_address`](#0x3_bitcoin_address_to_rooch_address)
- [Function `verify_bitcoin_address_with_public_key`](#0x3_bitcoin_address_verify_bitcoin_address_with_public_key)
- [Function `derive_multisig_xonly_pubkey_from_xonly_pubkeys`](#0x3_bitcoin_address_derive_multisig_xonly_pubkey_from_xonly_pubkeys)
- [Function `derive_bitcoin_taproot_address_from_multisig_xonly_pubkey`](#0x3_bitcoin_address_derive_bitcoin_taproot_address_from_multisig_xonly_pubkey)


<pre><code><b>use</b> <a href="">0x1::string</a>;
Expand Down Expand Up @@ -49,11 +52,56 @@ We just keep the raw bytes of the address and do care about the network.
## Constants


<a name="0x3_bitcoin_address_ErrorAddressBytesLen"></a>
<a name="0x3_bitcoin_address_ErrorArgNotVectorU8"></a>



<pre><code><b>const</b> <a href="bitcoin_address.md#0x3_bitcoin_address_ErrorAddressBytesLen">ErrorAddressBytesLen</a>: u64 = 1;
<pre><code><b>const</b> <a href="bitcoin_address.md#0x3_bitcoin_address_ErrorArgNotVectorU8">ErrorArgNotVectorU8</a>: u64 = 2;
</code></pre>



<a name="0x3_bitcoin_address_ErrorInvalidAddress"></a>



<pre><code><b>const</b> <a href="bitcoin_address.md#0x3_bitcoin_address_ErrorInvalidAddress">ErrorInvalidAddress</a>: u64 = 1;
</code></pre>



<a name="0x3_bitcoin_address_ErrorInvalidKeyEggContext"></a>



<pre><code><b>const</b> <a href="bitcoin_address.md#0x3_bitcoin_address_ErrorInvalidKeyEggContext">ErrorInvalidKeyEggContext</a>: u64 = 5;
</code></pre>



<a name="0x3_bitcoin_address_ErrorInvalidPublicKey"></a>



<pre><code><b>const</b> <a href="bitcoin_address.md#0x3_bitcoin_address_ErrorInvalidPublicKey">ErrorInvalidPublicKey</a>: u64 = 3;
</code></pre>



<a name="0x3_bitcoin_address_ErrorInvalidThreshold"></a>



<pre><code><b>const</b> <a href="bitcoin_address.md#0x3_bitcoin_address_ErrorInvalidThreshold">ErrorInvalidThreshold</a>: u64 = 4;
</code></pre>



<a name="0x3_bitcoin_address_ErrorInvalidXOnlyPublicKey"></a>



<pre><code><b>const</b> <a href="bitcoin_address.md#0x3_bitcoin_address_ErrorInvalidXOnlyPublicKey">ErrorInvalidXOnlyPublicKey</a>: u64 = 6;
</code></pre>


Expand Down Expand Up @@ -271,3 +319,36 @@ Empty address is a special address that is used to if we parse address failed fr

<pre><code><b>public</b> <b>fun</b> <a href="bitcoin_address.md#0x3_bitcoin_address_to_rooch_address">to_rooch_address</a>(addr: &<a href="bitcoin_address.md#0x3_bitcoin_address_BitcoinAddress">bitcoin_address::BitcoinAddress</a>): <b>address</b>
</code></pre>



<a name="0x3_bitcoin_address_verify_bitcoin_address_with_public_key"></a>

## Function `verify_bitcoin_address_with_public_key`



<pre><code><b>public</b> <b>fun</b> <a href="bitcoin_address.md#0x3_bitcoin_address_verify_bitcoin_address_with_public_key">verify_bitcoin_address_with_public_key</a>(bitcoin_addr: &<a href="bitcoin_address.md#0x3_bitcoin_address_BitcoinAddress">bitcoin_address::BitcoinAddress</a>, pk: &<a href="">vector</a>&lt;u8&gt;): bool
</code></pre>



<a name="0x3_bitcoin_address_derive_multisig_xonly_pubkey_from_xonly_pubkeys"></a>

## Function `derive_multisig_xonly_pubkey_from_xonly_pubkeys`



<pre><code><b>public</b> <b>fun</b> <a href="bitcoin_address.md#0x3_bitcoin_address_derive_multisig_xonly_pubkey_from_xonly_pubkeys">derive_multisig_xonly_pubkey_from_xonly_pubkeys</a>(public_keys: <a href="">vector</a>&lt;<a href="">vector</a>&lt;u8&gt;&gt;, threshold: u64): <a href="">vector</a>&lt;u8&gt;
</code></pre>



<a name="0x3_bitcoin_address_derive_bitcoin_taproot_address_from_multisig_xonly_pubkey"></a>

## Function `derive_bitcoin_taproot_address_from_multisig_xonly_pubkey`



<pre><code><b>public</b> <b>fun</b> <a href="bitcoin_address.md#0x3_bitcoin_address_derive_bitcoin_taproot_address_from_multisig_xonly_pubkey">derive_bitcoin_taproot_address_from_multisig_xonly_pubkey</a>(xonly_pubkey: &<a href="">vector</a>&lt;u8&gt;): <a href="bitcoin_address.md#0x3_bitcoin_address_BitcoinAddress">bitcoin_address::BitcoinAddress</a>
</code></pre>
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ module rooch_framework::bitcoin_address {
const P2SH_ADDR_BYTE_LEN: u64 = 21;

// error code
const ErrorAddressBytesLen: u64 = 1;
const ErrorInvalidAddress: u64 = 1;
const ErrorArgNotVectorU8: u64 = 2;
const ErrorInvalidPublicKey: u64 = 3;
const ErrorInvalidThreshold: u64 = 4;
const ErrorInvalidKeyEggContext: u64 = 5;
const ErrorInvalidXOnlyPublicKey: u64 = 6;

// P2PKH address decimal prefix
const P2PKH_ADDR_DECIMAL_PREFIX_MAIN: u8 = 0; // 0x00
Expand All @@ -32,7 +37,7 @@ module rooch_framework::bitcoin_address {
}

public fun new_p2pkh(pubkey_hash: vector<u8>): BitcoinAddress{
assert!(vector::length(&pubkey_hash) == PUBKEY_HASH_LEN, ErrorAddressBytesLen);
assert!(vector::length(&pubkey_hash) == PUBKEY_HASH_LEN, ErrorInvalidAddress);
//we do not distinguish between mainnet and testnet in Move
let bytes = vector::singleton<u8>(P2PKH_ADDR_DECIMAL_PREFIX_MAIN);
vector::append(&mut bytes, pubkey_hash);
Expand All @@ -42,7 +47,7 @@ module rooch_framework::bitcoin_address {
}

public fun new_p2sh(script_hash: vector<u8>): BitcoinAddress{
assert!(vector::length(&script_hash) == SCRIPT_HASH_LEN, ErrorAddressBytesLen);
assert!(vector::length(&script_hash) == SCRIPT_HASH_LEN, ErrorInvalidAddress);
let bytes = vector::singleton<u8>(P2SH_ADDR_DECIMAL_PREFIX_MAIN);
vector::append(&mut bytes, script_hash);
BitcoinAddress {
Expand Down Expand Up @@ -96,18 +101,28 @@ module rooch_framework::bitcoin_address {
}

public fun verify_with_public_key(addr: &String, pk: &vector<u8>): bool {
let raw_bytes = string::bytes(addr);
verify_with_pk(raw_bytes, pk)
let bitcoin_addr = from_string(addr);
verify_bitcoin_address_with_public_key(&bitcoin_addr, pk)
}

public fun to_rooch_address(addr: &BitcoinAddress): address{
let hash = moveos_std::hash::blake2b256(&addr.bytes);
moveos_std::bcs::to_address(hash)
}

// verify bitcoin address according to the pk bytes
public native fun verify_bitcoin_address_with_public_key(bitcoin_addr: &BitcoinAddress, pk: &vector<u8>): bool;

// derive multisig xonly public key from public keys
public native fun derive_multisig_xonly_pubkey_from_xonly_pubkeys(public_keys: vector<vector<u8>>, threshold: u64): vector<u8>;

// derive bitcoin taproot address from the multisig xonly public key
public native fun derive_bitcoin_taproot_address_from_multisig_xonly_pubkey(xonly_pubkey: &vector<u8>): BitcoinAddress;

/// Parse the Bitcoin address string bytes to Move BitcoinAddress
native fun parse(raw_addr: &vector<u8>): BitcoinAddress;
native fun verify_with_pk (addr: &vector<u8>, pk: &vector<u8>): bool;
// TODO: remove verify_with_pk after upgrade
native fun verify_with_pk(addr: &vector<u8>, pk: &vector<u8>): bool;

#[test_only]
public fun random_address_for_testing(): BitcoinAddress {
Expand All @@ -116,7 +131,7 @@ module rooch_framework::bitcoin_address {
}

#[test]
fun test_verify_with_pk_success() {
fun test_verify_with_public_key_success() {
// p2tr
let addr = string::utf8(b"bc1p8xpjpkc9uzj2dexcxjg9sw8lxje85xa4070zpcys589e3rf6k20qm6gjrt");
let pk = x"038e3d29b653e40f5b620f9443ee05222d1e40be58f544b6fed3d464edd54db883";
Expand All @@ -141,8 +156,73 @@ module rooch_framework::bitcoin_address {

#[test]
fun test_validate_signature_fail() {
let addr = string::utf8(b"ac1p8xpjpkc9uzj2dexcxjg9sw8lxje85xa4070zpcys589e3rf6k20qm6gjrt");
let pk = x"038e3d29b653e40f5b620f9443ee05222d1e40be58f544b6fed3d464edd54db883";
let addr = string::utf8(b"bc1p8xpjpkc9uzj2dexcxjg9sw8lxje85xa4070zpcys589e3rf6k20qm6gjrt");
let pk = x"038e3d29b653e40f5b620f9443ee05222d1e40be58f544b6fed3d464edd54db884";
assert!(!verify_with_public_key(&addr, &pk), 1004);
}

#[test]
fun test_derive_multisig_xonly_pubkey_from_xonly_pubkeys_success() {
let expected_xonly_pubkey = x"7b6474bd9206ad07c0bc6b0ac90d43f6f232235c9e9cbf0c47775bf47ca9c402";
let pk_list = vector::empty<vector<u8>>();

let pk_1 = x"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9";
let pk_2 = x"ffa540e2d3df158dfb202fc1a2cbb20c4920ba35e8f75bb11101bfa47d71449a";

vector::push_back(&mut pk_list, pk_1);
vector::push_back(&mut pk_list, pk_2);

let xonly_pubkey = derive_multisig_xonly_pubkey_from_xonly_pubkeys(pk_list, 2);

assert!(expected_xonly_pubkey == xonly_pubkey, ErrorInvalidKeyEggContext);
}

#[test]
#[expected_failure(location=Self, abort_code = ErrorInvalidThreshold)]
fun test_derive_multisig_xonly_pubkey_from_xonly_pubkeys_fail_invalid_threshold() {
let pk_list = vector::empty<vector<u8>>();

let pk_1 = x"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9";
let pk_2 = x"ffa540e2d3df158dfb202fc1a2cbb20c4920ba35e8f75bb11101bfa47d71449a";

vector::push_back(&mut pk_list, pk_1);
vector::push_back(&mut pk_list, pk_2);

derive_multisig_xonly_pubkey_from_xonly_pubkeys(pk_list, 3);
}

#[test]
#[expected_failure(location=Self, abort_code = ErrorInvalidPublicKey)]
fun test_derive_multisig_xonly_pubkey_from_xonly_pubkeys_fail_invalid_public_key() {
let pk_list = vector::empty<vector<u8>>();

let pk_1 = x"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9";
let pk_2 = x"02481521eb57656db4bc9ec81857e105cc7853fe8cad61be23667bb401840fc7f8";

vector::push_back(&mut pk_list, pk_1);
vector::push_back(&mut pk_list, pk_2);

derive_multisig_xonly_pubkey_from_xonly_pubkeys(pk_list, 2);
}

#[test]
fun test_derive_bitcoin_taproot_address_from_multisig_xonly_pubkey_success() {
let xonly_pubkey = x"7b6474bd9206ad07c0bc6b0ac90d43f6f232235c9e9cbf0c47775bf47ca9c402";

let bitcoin_addr = derive_bitcoin_taproot_address_from_multisig_xonly_pubkey(&xonly_pubkey);

let expected_bitcoin_addr = BitcoinAddress {
bytes: x"020102f97a0a664c8493bfa28cfcf3450628bdc0ba7b3b0af2b57d4d057f15cb41f9",
};

assert!(expected_bitcoin_addr.bytes == bitcoin_addr.bytes, ErrorInvalidXOnlyPublicKey);
}

#[test]
#[expected_failure(location=Self, abort_code = ErrorInvalidXOnlyPublicKey)]
fun test_derive_bitcoin_taproot_address_from_multisig_xonly_pubkey_fail() {
let xonly_pubkey = x"038e3d29b653e40f5b620f9443ee05222d1e40be58f544b6fed3d464edd54db883";

derive_bitcoin_taproot_address_from_multisig_xonly_pubkey(&xonly_pubkey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ crate::natives::gas_parameter::native::define_gas_parameters_for_natives!(GasPar
[.verify_with_pk.per_byte, "verify_with_pk.per_byte", 30 * MUL],
[.new.base, "parse.base", 1000 * MUL],
[.new.per_byte, "parse.per_byte", 30 * MUL],
[.verify_bitcoin_address_with_public_key.base, optional "verify_bitcoin_address_with_public_key.base", 1000 * MUL],
[.verify_bitcoin_address_with_public_key.per_byte, optional "verify_bitcoin_address_with_public_key.per_byte", 30 * MUL],
[.derive_multisig_xonly_pubkey_from_xonly_pubkeys.base, optional "derive_multisig_xonly_pubkey_from_xonly_pubkeys.base", 1000 * MUL],
[.derive_multisig_xonly_pubkey_from_xonly_pubkeys.per_byte, optional "derive_multisig_xonly_pubkey_from_xonly_pubkeys.per_byte", 30 * MUL],
[.derive_bitcoin_taproot_address_from_multisig_xonly_pubkey.base, optional "derive_bitcoin_taproot_address_from_multisig_xonly_pubkey.base", 1000 * MUL],
[.derive_bitcoin_taproot_address_from_multisig_xonly_pubkey.per_byte, optional "derive_bitcoin_taproot_address_from_multisig_xonly_pubkey.per_byte", 30 * MUL],
]);
Loading

0 comments on commit c9b7597

Please sign in to comment.