From 523dc7f21a3c1bf074e0ffb706a821eca96c038c Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Fri, 16 Feb 2024 13:37:58 +0000 Subject: [PATCH] [native bridge move code 1/n] - native bridge move package (#16259) ## Description Describe the changes or additions included in this PR. ## Test Plan How did you test the new or updated feature? --- If your changes are not user-facing and do not break anything, you can skip the following section. Otherwise, please briefly describe what has changed under the Release Notes section. ### Type of Change (Check all that apply) - [ ] protocol change - [ ] user-visible impact - [ ] breaking change for a client SDKs - [ ] breaking change for FNs (FN binary must upgrade) - [ ] breaking change for validators or node operators (must upgrade binaries) - [ ] breaking change for on-chain data layout - [ ] necessitate either a data wipe or data migration ### Release notes --- crates/sui-framework-snapshot/src/lib.rs | 4 +- crates/sui-framework/build.rs | 36 +- .../sui-framework/packages/bridge/Move.toml | 11 + .../packages/bridge/sources/bridge.move | 349 +++++++++++ .../packages/bridge/sources/btc.move | 29 + .../packages/bridge/sources/chain_ids.move | 55 ++ .../packages/bridge/sources/committee.move | 216 +++++++ .../packages/bridge/sources/crypto.move | 43 ++ .../packages/bridge/sources/eth.move | 30 + .../packages/bridge/sources/message.move | 573 ++++++++++++++++++ .../bridge/sources/message_types.move | 31 + .../packages/bridge/sources/treasury.move | 80 +++ .../packages/bridge/sources/usdc.move | 29 + .../packages/bridge/sources/usdt.move | 29 + .../sui-framework/sources/object.move | 12 + crates/sui-framework/src/lib.rs | 7 +- crates/sui-move-build/src/lib.rs | 9 +- crates/sui-protocol-config/src/lib.rs | 24 +- .../v1/sui-verifier/src/id_leak_verifier.rs | 6 +- .../src/one_time_witness_verifier.rs | 14 +- 20 files changed, 1579 insertions(+), 8 deletions(-) create mode 100644 crates/sui-framework/packages/bridge/Move.toml create mode 100644 crates/sui-framework/packages/bridge/sources/bridge.move create mode 100644 crates/sui-framework/packages/bridge/sources/btc.move create mode 100644 crates/sui-framework/packages/bridge/sources/chain_ids.move create mode 100644 crates/sui-framework/packages/bridge/sources/committee.move create mode 100644 crates/sui-framework/packages/bridge/sources/crypto.move create mode 100644 crates/sui-framework/packages/bridge/sources/eth.move create mode 100644 crates/sui-framework/packages/bridge/sources/message.move create mode 100644 crates/sui-framework/packages/bridge/sources/message_types.move create mode 100644 crates/sui-framework/packages/bridge/sources/treasury.move create mode 100644 crates/sui-framework/packages/bridge/sources/usdc.move create mode 100644 crates/sui-framework/packages/bridge/sources/usdt.move diff --git a/crates/sui-framework-snapshot/src/lib.rs b/crates/sui-framework-snapshot/src/lib.rs index c468a36d8de1f..346be2c6dfeae 100644 --- a/crates/sui-framework-snapshot/src/lib.rs +++ b/crates/sui-framework-snapshot/src/lib.rs @@ -7,7 +7,8 @@ use std::{fs, io::Read, path::PathBuf}; use sui_framework::SystemPackage; use sui_types::base_types::ObjectID; use sui_types::{ - DEEPBOOK_PACKAGE_ID, MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID, SUI_SYSTEM_PACKAGE_ID, + BRIDGE_PACKAGE_ID, DEEPBOOK_PACKAGE_ID, MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID, + SUI_SYSTEM_PACKAGE_ID, }; pub type SnapshotManifest = BTreeMap; @@ -25,6 +26,7 @@ const SYSTEM_PACKAGE_PUBLISH_ORDER: &[ObjectID] = &[ SUI_FRAMEWORK_PACKAGE_ID, SUI_SYSTEM_PACKAGE_ID, DEEPBOOK_PACKAGE_ID, + BRIDGE_PACKAGE_ID, ]; pub fn load_bytecode_snapshot_manifest() -> SnapshotManifest { diff --git a/crates/sui-framework/build.rs b/crates/sui-framework/build.rs index 57f768b3ff325..878b8dc1efee8 100644 --- a/crates/sui-framework/build.rs +++ b/crates/sui-framework/build.rs @@ -19,15 +19,18 @@ fn main() { let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let packages_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("packages"); + let bridge_path = packages_path.join("bridge"); let deepbook_path = packages_path.join("deepbook"); let sui_system_path = packages_path.join("sui-system"); let sui_framework_path = packages_path.join("sui-framework"); + let bridge_path_clone = bridge_path.clone(); let deepbook_path_clone = deepbook_path.clone(); let sui_system_path_clone = sui_system_path.clone(); let sui_framework_path_clone = sui_framework_path.clone(); let move_stdlib_path = packages_path.join("move-stdlib"); build_packages( + bridge_path_clone, deepbook_path_clone, sui_system_path_clone, sui_framework_path_clone, @@ -43,6 +46,14 @@ fn main() { "cargo:rerun-if-changed={}", deepbook_path.join("sources").display() ); + println!( + "cargo:rerun-if-changed={}", + bridge_path.join("Move.toml").display() + ); + println!( + "cargo:rerun-if-changed={}", + bridge_path.join("sources").display() + ); println!( "cargo:rerun-if-changed={}", sui_system_path.join("Move.toml").display() @@ -70,6 +81,7 @@ fn main() { } fn build_packages( + bridge_path: PathBuf, deepbook_path: PathBuf, sui_system_path: PathBuf, sui_framework_path: PathBuf, @@ -84,10 +96,12 @@ fn build_packages( }; debug_assert!(!config.test_mode); build_packages_with_move_config( + bridge_path.clone(), deepbook_path.clone(), sui_system_path.clone(), sui_framework_path.clone(), out_dir.clone(), + "bridge", "deepbook", "sui-system", "sui-framework", @@ -104,10 +118,12 @@ fn build_packages( ..Default::default() }; build_packages_with_move_config( + bridge_path, deepbook_path, sui_system_path, sui_framework_path, out_dir, + "bridge-test", "deepbook-test", "sui-system-test", "sui-framework-test", @@ -118,10 +134,12 @@ fn build_packages( } fn build_packages_with_move_config( + bridge_path: PathBuf, deepbook_path: PathBuf, sui_system_path: PathBuf, sui_framework_path: PathBuf, out_dir: PathBuf, + bridge_dir: &str, deepbook_dir: &str, system_dir: &str, framework_dir: &str, @@ -144,21 +162,30 @@ fn build_packages_with_move_config( .build(sui_system_path) .unwrap(); let deepbook_pkg = BuildConfig { - config, + config: config.clone(), run_bytecode_verifier: true, print_diags_to_stderr: false, } .build(deepbook_path) .unwrap(); + let bridge_pkg = BuildConfig { + config, + run_bytecode_verifier: true, + print_diags_to_stderr: false, + } + .build(bridge_path) + .unwrap(); let sui_system = system_pkg.get_sui_system_modules(); let sui_framework = framework_pkg.get_sui_framework_modules(); let deepbook = deepbook_pkg.get_deepbook_modules(); + let bridge = bridge_pkg.get_bridge_modules(); let move_stdlib = framework_pkg.get_stdlib_modules(); serialize_modules_to_file(sui_system, &out_dir.join(system_dir)).unwrap(); serialize_modules_to_file(sui_framework, &out_dir.join(framework_dir)).unwrap(); serialize_modules_to_file(deepbook, &out_dir.join(deepbook_dir)).unwrap(); + serialize_modules_to_file(bridge, &out_dir.join(bridge_dir)).unwrap(); serialize_modules_to_file(move_stdlib, &out_dir.join(stdlib_dir)).unwrap(); // write out generated docs // TODO: remove docs of deleted files @@ -170,6 +197,13 @@ fn build_packages_with_move_config( fs::create_dir_all(dst_path.parent().unwrap()).unwrap(); fs::write(dst_path, doc).unwrap(); } + for (fname, doc) in bridge_pkg.package.compiled_docs.unwrap() { + let mut dst_path = PathBuf::from(DOCS_DIR); + dst_path.push(bridge_dir); + dst_path.push(fname); + fs::create_dir_all(dst_path.parent().unwrap()).unwrap(); + fs::write(dst_path, doc).unwrap(); + } for (fname, doc) in system_pkg.package.compiled_docs.unwrap() { let mut dst_path = PathBuf::from(DOCS_DIR); dst_path.push(system_dir); diff --git a/crates/sui-framework/packages/bridge/Move.toml b/crates/sui-framework/packages/bridge/Move.toml new file mode 100644 index 0000000000000..5bf6eb3935522 --- /dev/null +++ b/crates/sui-framework/packages/bridge/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "Bridge" +version = "0.0.1" +published-at = "0xb" + +[dependencies] +MoveStdlib = { local = "../move-stdlib" } +Sui = { local = "../sui-framework" } + +[addresses] +bridge = "0xb" diff --git a/crates/sui-framework/packages/bridge/sources/bridge.move b/crates/sui-framework/packages/bridge/sources/bridge.move new file mode 100644 index 0000000000000..001f259af6603 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/bridge.move @@ -0,0 +1,349 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::bridge { + use std::option; + use std::option::{none, Option, some}; + + use sui::address; + use sui::balance; + use sui::coin::{Self, Coin}; + use sui::event::emit; + use sui::linked_table::{Self, LinkedTable}; + use sui::object::UID; + use sui::transfer; + use sui::tx_context::{Self, TxContext}; + use sui::vec_map::{Self, VecMap}; + use sui::versioned::{Self, Versioned}; + + use bridge::chain_ids; + use bridge::committee::{Self, BridgeCommittee}; + use bridge::message::{Self, BridgeMessage, BridgeMessageKey}; + use bridge::message_types; + use bridge::treasury::{Self, BridgeTreasury}; + + struct Bridge has key { + id: UID, + inner: Versioned + } + + struct BridgeInner has store { + bridge_version: u64, + chain_id: u8, + // nonce for replay protection + sequence_nums: VecMap, + // committee + committee: BridgeCommittee, + // Bridge treasury for mint/burn bridged tokens + treasury: BridgeTreasury, + bridge_records: LinkedTable, + frozen: bool, + } + + // Emergency Op types + const FREEZE: u8 = 0; + const UNFREEZE: u8 = 1; + + struct TokenBridgeEvent has copy, drop { + message_type: u8, + seq_num: u64, + source_chain: u8, + sender_address: vector, + target_chain: u8, + target_address: vector, + token_type: u8, + amount: u64 + } + + struct BridgeRecord has store, drop { + message: BridgeMessage, + verified_signatures: Option>>, + claimed: bool + } + + const EUnexpectedMessageType: u64 = 0; + const EUnauthorisedClaim: u64 = 1; + const EMalformedMessageError: u64 = 2; + const EUnexpectedTokenType: u64 = 3; + const EUnexpectedChainID: u64 = 4; + const ENotSystemAddress: u64 = 5; + const EUnexpectedSeqNum: u64 = 6; + const EWrongInnerVersion: u64 = 7; + const EBridgeUnavailable: u64 = 8; + const EUnexpectedOperation: u64 = 9; + const EInvalidBridgeRoute: u64 = 10; + const EInvariantSuiInitializedTokenTransferShouldNotBeClaimed: u64 = 11; + const EMessageNotFoundInRecords: u64 = 12; + const ETokenAlreadyClaimed: u64 = 12; + + const CURRENT_VERSION: u64 = 1; + + struct TokenTransferApproved has copy, drop { + message_key: BridgeMessageKey, + } + + struct TokenTransferClaimed has copy, drop { + message_key: BridgeMessageKey, + } + + struct TokenTransferAlreadyApproved has copy, drop { + message_key: BridgeMessageKey, + } + + struct TokenTransferAlreadyClaimed has copy, drop { + message_key: BridgeMessageKey, + } + + // this method is called once in end of epoch tx to create the bridge + #[allow(unused_function)] + fun create(id: UID, chain_id: u8, ctx: &mut TxContext) { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + let bridge_inner = BridgeInner { + bridge_version: CURRENT_VERSION, + chain_id, + sequence_nums: vec_map::empty(), + committee: committee::create(ctx), + treasury: treasury::create(ctx), + bridge_records: linked_table::new(ctx), + frozen: false, + }; + let bridge = Bridge { + id, + inner: versioned::create(CURRENT_VERSION, bridge_inner, ctx) + }; + transfer::share_object(bridge); + } + + // Create bridge request to send token to other chain, the request will be in pending state until approved + public fun send_token( + self: &mut Bridge, + target_chain: u8, + target_address: vector, + token: Coin, + ctx: &mut TxContext + ) { + let inner = load_inner_mut(self); + assert!(chain_ids::is_valid_route(inner.chain_id, target_chain), EInvalidBridgeRoute); + assert!(!inner.frozen, EBridgeUnavailable); + let bridge_seq_num = next_seq_num(inner, message_types::token()); + let token_id = treasury::token_id(); + let token_amount = balance::value(coin::balance(&token)); + + // create bridge message + let message = message::create_token_bridge_message( + inner.chain_id, + bridge_seq_num, + address::to_bytes(tx_context::sender(ctx)), + target_chain, + target_address, + token_id, + token_amount, + ); + + // burn / escrow token, unsupported coins will fail in this step + treasury::burn(&mut inner.treasury, token, ctx); + + // Store pending bridge request + let key = message::key(&message); + linked_table::push_back(&mut inner.bridge_records, key, BridgeRecord { + message, + verified_signatures: none(), + claimed: false, + }); + + // emit event + emit(TokenBridgeEvent { + message_type: message_types::token(), + seq_num: bridge_seq_num, + source_chain: inner.chain_id, + sender_address: address::to_bytes(tx_context::sender(ctx)), + target_chain, + target_address, + token_type: token_id, + amount: token_amount, + }); + } + + // Record bridge message approvals in Sui, called by the bridge client + // If already approved, return early instead of aborting. + public fun approve_bridge_message( + self: &mut Bridge, + message: BridgeMessage, + signatures: vector>, + ) { + let inner = load_inner_mut(self); + let key = message::key(&message); + + // retrieve pending message if source chain is Sui, the initial message must exist on chain. + if (message::message_type(&message) == message_types::token() && message::source_chain(&message) == inner.chain_id) { + let record = linked_table::borrow_mut(&mut inner.bridge_records, key); + assert!(record.message == message, EMalformedMessageError); + assert!(!record.claimed, EInvariantSuiInitializedTokenTransferShouldNotBeClaimed); + + // If record already has verified signatures, it means the message has been approved. + // Then we exit early. + if (option::is_some(&record.verified_signatures)) { + emit(TokenTransferAlreadyApproved { message_key: key }); + return + }; + // verify signatures + committee::verify_signatures(&inner.committee, message, signatures); + // Store approval + record.verified_signatures = some(signatures) + } else { + // At this point, if this message is in bridge_records, we know it's already approved + // because we only add a message to bridge_records after verifying the signatures. + if (linked_table::contains(&inner.bridge_records, key)) { + emit(TokenTransferAlreadyApproved { message_key: key }); + return + }; + // verify signatures + committee::verify_signatures(&inner.committee, message, signatures); + // Store message and approval + linked_table::push_back(&mut inner.bridge_records, key, BridgeRecord { + message, + verified_signatures: some(signatures), + claimed: false + }); + }; + emit(TokenTransferApproved { message_key: key }); + } + + + // This function can only be called by the token recipient + // Abort if the token has already been claimed. + public fun claim_token(self: &mut Bridge, source_chain: u8, bridge_seq_num: u64, ctx: &mut TxContext): Coin { + let (maybe_token, owner) = claim_token_internal(self, source_chain, bridge_seq_num, ctx); + // Only token owner can claim the token + assert!(tx_context::sender(ctx) == owner, EUnauthorisedClaim); + assert!(option::is_some(&maybe_token), ETokenAlreadyClaimed); + option::destroy_some(maybe_token) + } + + // This function can be called by anyone to claim and transfer the token to the recipient + // If the token has already been claimed, it will return instead of aborting. + public fun claim_and_transfer_token( + self: &mut Bridge, + source_chain: u8, + bridge_seq_num: u64, + ctx: &mut TxContext + ) { + let (token, owner) = claim_token_internal(self, source_chain, bridge_seq_num, ctx); + if (option::is_none(&token)) { + option::destroy_none(token); + let key = message::create_key(source_chain, message_types::token(), bridge_seq_num); + emit(TokenTransferAlreadyClaimed { message_key: key }); + return + }; + transfer::public_transfer(option::destroy_some(token), owner) + } + + public fun execute_emergency_op( + self: &mut Bridge, + message: BridgeMessage, + signatures: vector>, + ) { + assert!(message::message_type(&message) == message_types::emergency_op(), EUnexpectedMessageType); + let inner = load_inner_mut(self); + // check emergency ops seq number, emergency ops can only be executed in sequence order. + let emergency_op_seq_num = next_seq_num(inner, message_types::emergency_op()); + assert!(message::seq_num(&message) == emergency_op_seq_num, EUnexpectedSeqNum); + committee::verify_signatures(&inner.committee, message, signatures); + let payload = message::extract_emergency_op_payload(&message); + + if (message::emergency_op_type(&payload) == FREEZE) { + inner.frozen == true; + } else if (message::emergency_op_type(&payload) == UNFREEZE) { + inner.frozen == false; + } else { + abort EUnexpectedOperation + }; + } + + fun load_inner_mut( + self: &mut Bridge, + ): &mut BridgeInner { + let version = versioned::version(&self.inner); + + // TODO: Replace this with a lazy update function when we add a new version of the inner object. + assert!(version == CURRENT_VERSION, EWrongInnerVersion); + let inner: &mut BridgeInner = versioned::load_value_mut(&mut self.inner); + assert!(inner.bridge_version == version, EWrongInnerVersion); + inner + } + + #[allow(unused_function)] // TODO: remove annotation after implementing user-facing API + fun load_inner( + self: &Bridge, + ): &BridgeInner { + let version = versioned::version(&self.inner); + + // TODO: Replace this with a lazy update function when we add a new version of the inner object. + assert!(version == CURRENT_VERSION, EWrongInnerVersion); + let inner: &BridgeInner = versioned::load_value(&self.inner); + assert!(inner.bridge_version == version, EWrongInnerVersion); + inner + } + + // Claim token from approved bridge message + // Returns Some(Coin) if coin can be claimed. If already claimed, return None + fun claim_token_internal( + self: &mut Bridge, + source_chain: u8, + bridge_seq_num: u64, + ctx: &mut TxContext + ): (Option>, address) { + let inner = load_inner_mut(self); + assert!(!inner.frozen, EBridgeUnavailable); + + let key = message::create_key(source_chain, message_types::token(), bridge_seq_num); + assert!(linked_table::contains(&inner.bridge_records, key), EMessageNotFoundInRecords); + + // retrieve approved bridge message + let record = linked_table::borrow_mut(&mut inner.bridge_records, key); + // ensure this is a token bridge message + assert!(message::message_type(&record.message) == message_types::token(), EUnexpectedMessageType); + // Ensure it's signed + assert!(option::is_some(&record.verified_signatures), EUnauthorisedClaim); + + // extract token message + let token_payload = message::extract_token_bridge_payload(&record.message); + // get owner address + let owner = address::from_bytes(message::token_target_address(&token_payload)); + + // If already claimed, exit early + if (record.claimed) { + return (option::none(), owner) + }; + + let target_chain = message::token_target_chain(&token_payload); + // ensure target chain matches self.chain_id + assert!(target_chain == inner.chain_id, EUnexpectedChainID); + + // TODO: why do we check validity of the route here? what if inconsistency? + // Ensure route is valid + // TODO: add unit tests + assert!(chain_ids::is_valid_route(source_chain, target_chain), EInvalidBridgeRoute); + + // get owner address + let owner = address::from_bytes(message::token_target_address(&token_payload)); + // check token type + assert!(treasury::token_id() == message::token_type(&token_payload), EUnexpectedTokenType); + // claim from treasury + let token = treasury::mint(&mut inner.treasury, message::token_amount(&token_payload), ctx); + // Record changes + record.claimed = true; + emit(TokenTransferClaimed { message_key: key }); + (option::some(token), owner) + } + + fun next_seq_num(self: &mut BridgeInner, msg_type: u8): u64 { + if (!vec_map::contains(&self.sequence_nums, &msg_type)) { + vec_map::insert(&mut self.sequence_nums, msg_type, 1); + return 0 + }; + let (key, seq_num) = vec_map::remove(&mut self.sequence_nums, &msg_type); + vec_map::insert(&mut self.sequence_nums, key, seq_num + 1); + seq_num + } +} diff --git a/crates/sui-framework/packages/bridge/sources/btc.move b/crates/sui-framework/packages/bridge/sources/btc.move new file mode 100644 index 0000000000000..0bcff068af7d7 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/btc.move @@ -0,0 +1,29 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::btc { + use std::option; + + use sui::coin; + use sui::coin::TreasuryCap; + use sui::transfer; + use sui::tx_context::TxContext; + + friend bridge::treasury; + + struct BTC has drop {} + + public(friend) fun create(ctx: &mut TxContext): TreasuryCap { + let (treasury_cap, metadata) = coin::create_currency( + BTC {}, + 8, + b"BTC", + b"Bitcoin", + b"Bridged Bitcoin token", + option::none(), + ctx + ); + transfer::public_freeze_object(metadata); + treasury_cap + } +} diff --git a/crates/sui-framework/packages/bridge/sources/chain_ids.move b/crates/sui-framework/packages/bridge/sources/chain_ids.move new file mode 100644 index 0000000000000..2e6305a2a83dc --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/chain_ids.move @@ -0,0 +1,55 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::chain_ids { + + use std::vector; + + // Chain IDs + const SuiMainnet: u8 = 0; + const SuiTestnet: u8 = 1; + const SuiDevnet: u8 = 2; + + const EthMainnet: u8 = 10; + const EthSepolia: u8 = 11; + + struct BridgeRoute has drop { + source: u8, + destination: u8, + } + + public fun sui_mainnet(): u8 { + SuiMainnet + } + + public fun sui_testnet(): u8 { + SuiTestnet + } + + public fun sui_devnet(): u8 { + SuiDevnet + } + + public fun eth_mainnet(): u8 { + EthMainnet + } + + public fun eth_sepolia(): u8 { + EthSepolia + } + + public fun valid_routes(): vector { + vector[ + BridgeRoute { source: SuiMainnet, destination: EthMainnet }, + BridgeRoute { source: SuiDevnet, destination: EthSepolia }, + BridgeRoute { source: SuiTestnet, destination: EthSepolia }, + BridgeRoute { source: EthMainnet, destination: SuiMainnet }, + BridgeRoute { source: EthSepolia, destination: SuiDevnet }, + BridgeRoute { source: EthSepolia, destination: SuiTestnet }] + } + + public fun is_valid_route(source: u8, destination: u8): bool { + let route = BridgeRoute { source, destination }; + return vector::contains(&valid_routes(), &route) + } +} diff --git a/crates/sui-framework/packages/bridge/sources/committee.move b/crates/sui-framework/packages/bridge/sources/committee.move new file mode 100644 index 0000000000000..503182da49a9f --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/committee.move @@ -0,0 +1,216 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_use)] +module bridge::committee { + use std::vector; + + use sui::address; + use sui::ecdsa_k1; + use sui::hex; + use sui::tx_context::{Self, TxContext}; + use sui::vec_map::{Self, VecMap}; + use sui::vec_set; + + use bridge::message::{Self, BridgeMessage}; + use bridge::message_types; + + friend bridge::bridge; + + const ESignatureBelowThreshold: u64 = 0; + const EDuplicatedSignature: u64 = 1; + const EInvalidSignature: u64 = 2; + const ENotSystemAddress: u64 = 3; + + const SUI_MESSAGE_PREFIX: vector = b"SUI_BRIDGE_MESSAGE"; + + struct BridgeCommittee has store { + // commitee pub key and weight + members: VecMap, CommitteeMember>, + // threshold for each message type + thresholds: VecMap + } + + struct CommitteeMember has drop, store { + /// The Sui Address of the validator + sui_address: address, + /// The public key bytes of the bridge key + bridge_pubkey_bytes: vector, + /// Voting power + voting_power: u64, + /// The HTTP REST URL the member's node listens to + /// it looks like b'https://127.0.0.1:9191' + http_rest_url: vector, + /// If this member is blocklisted + blocklisted: bool, + } + + public(friend) fun create(ctx: &TxContext): BridgeCommittee { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + // Hardcoded genesis committee + // TODO: change this to real committe members + let members = vec_map::empty, CommitteeMember>(); + + let bridge_pubkey_bytes = hex::decode(b"029bef8d556d80e43ae7e0becb3a7e6838b95defe45896ed6075bb9035d06c9964"); + vec_map::insert(&mut members, bridge_pubkey_bytes, CommitteeMember { + sui_address: address::from_u256(1), + bridge_pubkey_bytes, + voting_power: 10, + http_rest_url: b"https://127.0.0.1:9191", + blocklisted: false + }); + + let bridge_pubkey_bytes = hex::decode(b"033e99a541db69bd32040dfe5037fbf5210dafa8151a71e21c5204b05d95ce0a62"); + vec_map::insert(&mut members, bridge_pubkey_bytes, CommitteeMember { + sui_address: address::from_u256(2), + bridge_pubkey_bytes, + voting_power: 10, + http_rest_url: b"https://127.0.0.1:9192", + blocklisted: false + }); + + let thresholds = vec_map::empty(); + vec_map::insert(&mut thresholds, message_types::token(), 10); + BridgeCommittee { members, thresholds } + } + + public fun verify_signatures( + self: &BridgeCommittee, + message: BridgeMessage, + signatures: vector>, + ) { + let (i, signature_counts) = (0, vector::length(&signatures)); + let seen_pub_key = vec_set::empty>(); + let required_threshold = *vec_map::get(&self.thresholds, &message::message_type(&message)); + + // add prefix to the message bytes + let message_bytes = SUI_MESSAGE_PREFIX; + vector::append(&mut message_bytes, message::serialize_message(message)); + + let threshold = 0; + while (i < signature_counts) { + let signature = vector::borrow(&signatures, i); + let pubkey = ecdsa_k1::secp256k1_ecrecover(signature, &message_bytes, 0); + // check duplicate + assert!(!vec_set::contains(&seen_pub_key, &pubkey), EDuplicatedSignature); + // make sure pub key is part of the committee + assert!(vec_map::contains(&self.members, &pubkey), EInvalidSignature); + // get committee signature weight and check pubkey is part of the committee + let member = vec_map::get(&self.members, &pubkey); + if (!member.blocklisted) { + threshold = threshold + member.voting_power; + }; + i = i + 1; + vec_set::insert(&mut seen_pub_key, pubkey); + }; + assert!(threshold >= required_threshold, ESignatureBelowThreshold); + } + + #[test_only] + const TEST_MSG: vector = + b"00010a0000000000000000200000000000000000000000000000000000000000000000000000000000000064012000000000000000000000000000000000000000000000000000000000000000c8033930000000000000"; + + #[test] + fun test_verify_signatures_good_path() { + let committee = setup_test(); + let msg = message::deserialize_message(hex::decode(TEST_MSG)); + // good path + verify_signatures( + &committee, + msg, + vector[hex::decode( + b"8ba030a450cb1e36f61e572645fc9da1dea5f79b6db663a21ab63286d7fc29af447433abdd0c0b35ab751154ac5b612ae64d3be810f0d9e10ff68e764514ced300" + ), hex::decode( + b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801" + )], + ); + + // Clean up + let BridgeCommittee { + members: _, + thresholds: _ + } = committee; + } + + #[test] + #[expected_failure(abort_code = EDuplicatedSignature)] + fun test_verify_signatures_duplicated_sig() { + let committee = setup_test(); + let msg = message::deserialize_message(hex::decode(TEST_MSG)); + // good path + verify_signatures( + &committee, + msg, + vector[hex::decode( + b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801" + ), hex::decode( + b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801" + )], + ); + abort 0 + } + + #[test] + #[expected_failure(abort_code = EInvalidSignature)] + fun test_verify_signatures_invalid_signature() { + let committee = setup_test(); + let msg = message::deserialize_message(hex::decode(TEST_MSG)); + // good path + verify_signatures( + &committee, + msg, + vector[hex::decode( + b"6ffb3e5ce04dd138611c49520fddfbd6778879c2db4696139f53a487043409536c369c6ffaca165ce3886723cfa8b74f3e043e226e206ea25e313ea2215e6caf01" + )], + ); + abort 0 + } + + #[test] + #[expected_failure(abort_code = ESignatureBelowThreshold)] + fun test_verify_signatures_below_threshold() { + let committee = setup_test(); + let msg = message::deserialize_message(hex::decode(TEST_MSG)); + // good path + verify_signatures( + &committee, + msg, + vector[hex::decode( + b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801" + )], + ); + abort 0 + } + + #[test_only] + fun setup_test(): BridgeCommittee { + let members = vec_map::empty, CommitteeMember>(); + + let bridge_pubkey_bytes = hex::decode(b"029bef8d556d80e43ae7e0becb3a7e6838b95defe45896ed6075bb9035d06c9964"); + vec_map::insert(&mut members, bridge_pubkey_bytes, CommitteeMember { + sui_address: address::from_u256(1), + bridge_pubkey_bytes, + voting_power: 100, + http_rest_url: b"https://127.0.0.1:9191", + blocklisted: false + }); + + let bridge_pubkey_bytes = hex::decode(b"033e99a541db69bd32040dfe5037fbf5210dafa8151a71e21c5204b05d95ce0a62"); + vec_map::insert(&mut members, bridge_pubkey_bytes, CommitteeMember { + sui_address: address::from_u256(2), + bridge_pubkey_bytes, + voting_power: 100, + http_rest_url: b"https://127.0.0.1:9192", + blocklisted: false + }); + + let thresholds = vec_map::empty(); + vec_map::insert(&mut thresholds, message_types::token(), 200); + + let committee = BridgeCommittee { + members, + thresholds + }; + committee + } +} diff --git a/crates/sui-framework/packages/bridge/sources/crypto.move b/crates/sui-framework/packages/bridge/sources/crypto.move new file mode 100644 index 0000000000000..bf87feffab555 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/crypto.move @@ -0,0 +1,43 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::crypto { + + use std::vector; + use sui::ecdsa_k1; + use sui::hash::keccak256; + #[test_only] + use sui::hex; + + public fun ecdsa_pub_key_to_eth_address(compressed_pub_key: vector): vector { + // Decompress pub key + let decompressed = ecdsa_k1::decompress_pubkey(&compressed_pub_key); + + // Remove first byte + let (i, decompressed_64) = (1, vector[]); + while (i < 65) { + let value = vector::borrow(&decompressed, i); + vector::push_back(&mut decompressed_64, *value); + i = i + 1; + }; + + // Hash + let hash = keccak256(&decompressed_64); + + // Take last 20 bytes + let address = vector[]; + let i = 12; + while (i < 32) { + vector::push_back(&mut address, *vector::borrow(&hash, i)); + i = i + 1; + }; + address + } + + #[test] + fun test_pub_key_to_eth_address() { + let validator_pub_key = hex::decode(b"029bef8d556d80e43ae7e0becb3a7e6838b95defe45896ed6075bb9035d06c9964"); + let expected_address = hex::decode(b"b14d3c4f5fbfbcfb98af2d330000d49c95b93aa7"); + assert!(ecdsa_pub_key_to_eth_address(validator_pub_key) == expected_address, 0); + } +} diff --git a/crates/sui-framework/packages/bridge/sources/eth.move b/crates/sui-framework/packages/bridge/sources/eth.move new file mode 100644 index 0000000000000..411443d8ae275 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/eth.move @@ -0,0 +1,30 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::eth { + use std::option; + + use sui::coin; + use sui::coin::TreasuryCap; + use sui::transfer; + use sui::tx_context::TxContext; + + friend bridge::treasury; + + struct ETH has drop {} + + public(friend) fun create(ctx: &mut TxContext): TreasuryCap { + let (treasury_cap, metadata) = coin::create_currency( + ETH {}, + // ETC DP limited to 8 on Sui + 8, + b"ETH", + b"Ethereum", + b"Bridged Ethereum token", + option::none(), + ctx + ); + transfer::public_freeze_object(metadata); + treasury_cap + } +} diff --git a/crates/sui-framework/packages/bridge/sources/message.move b/crates/sui-framework/packages/bridge/sources/message.move new file mode 100644 index 0000000000000..fbffc528d2204 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/message.move @@ -0,0 +1,573 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::message { + use std::vector; + + use sui::bcs; + use sui::bcs::{BCS}; + use bridge::treasury; + + use bridge::message_types; + + #[test_only] + use bridge::chain_ids; + #[test_only] + use bridge::usdc::USDC; + #[test_only] + use sui::address; + #[test_only] + use sui::balance; + #[test_only] + use sui::coin; + #[test_only] + use sui::hex; + #[test_only] + use sui::test_scenario; + #[test_only] + use bridge::eth::ETH; + #[test_only] + use bridge::treasury::token_id; + + const CURRENT_MESSAGE_VERSION: u8 = 1; + const ECDSA_ADDRESS_LENGTH: u64 = 20; + + const ETrailingBytes: u64 = 0; + const EInvalidAddressLength: u64 = 1; + + struct BridgeMessage has copy, drop, store { + message_type: u8, + message_version: u8, + seq_num: u64, + source_chain: u8, + payload: vector + } + + struct BridgeMessageKey has copy, drop, store { + source_chain: u8, + message_type: u8, + bridge_seq_num: u64 + } + + struct TokenPayload has drop { + sender_address: vector, + target_chain: u8, + target_address: vector, + token_type: u8, + amount: u64 + } + + struct EmergencyOp has drop { + op_type: u8 + } + + struct Blocklist has drop { + blocklist_type: u8, + validator_addresses: vector> + } + + struct UpdateBridgeLimit has drop { + source_chain: u8, + target_chain: u8, + limit: u64 + } + + struct UpdateAssetPrice has drop { + token_id: u8, + new_price: u64 + } + + // Note: `bcs::peel_vec_u8` *happens* to work here because + // `sender_address` and `target_address` are no longer than 255 bytes. + // Therefore their length can be represented by a single byte. + // See `create_token_bridge_message` for the actual encoding rule. + public fun extract_token_bridge_payload(message: &BridgeMessage): TokenPayload { + let bcs = bcs::new(message.payload); + let sender_address = bcs::peel_vec_u8(&mut bcs); + let target_chain = bcs::peel_u8(&mut bcs); + let target_address = bcs::peel_vec_u8(&mut bcs); + let token_type = bcs::peel_u8(&mut bcs); + let amount = peel_u64_be(&mut bcs); + assert!(vector::is_empty(&bcs::into_remainder_bytes(bcs)), ETrailingBytes); + TokenPayload { + sender_address, + target_chain, + target_address, + token_type, + amount + } + } + + public fun extract_emergency_op_payload(message: &BridgeMessage): EmergencyOp { + // emergency op payload is just a single byte + assert!(vector::length(&message.payload) == 1, ETrailingBytes); + EmergencyOp { + op_type: *vector::borrow(&message.payload, 0) + } + } + + public fun extract_blocklist_payload(message: &BridgeMessage): Blocklist { + // blocklist payload should consist of one byte blocklist type, and list of 33 bytes ecdsa pub keys + let bcs = bcs::new(message.payload); + let blocklist_type = bcs::peel_u8(&mut bcs); + let address_count = bcs::peel_u8(&mut bcs); + let validator_addresses = vector[]; + while (address_count > 0) { + let (address, i) = (vector[], 0); + while (i < ECDSA_ADDRESS_LENGTH) { + vector::push_back(&mut address, bcs::peel_u8(&mut bcs)); + i = i + 1; + }; + vector::push_back(&mut validator_addresses, address); + address_count = address_count - 1; + }; + assert!(vector::is_empty(&bcs::into_remainder_bytes(bcs)), ETrailingBytes); + Blocklist { + blocklist_type, + validator_addresses + } + } + + public fun extract_update_bridge_limit(message: &BridgeMessage): UpdateBridgeLimit { + let bcs = bcs::new(message.payload); + let target_chain = bcs::peel_u8(&mut bcs); + let limit = peel_u64_be(&mut bcs); + assert!(vector::is_empty(&bcs::into_remainder_bytes(bcs)), ETrailingBytes); + UpdateBridgeLimit { + source_chain: message.source_chain, + target_chain, + limit + } + } + + public fun extract_update_asset_price(message: &BridgeMessage): UpdateAssetPrice { + let bcs = bcs::new(message.payload); + let token_id = bcs::peel_u8(&mut bcs); + let new_price = peel_u64_be(&mut bcs); + assert!(vector::is_empty(&bcs::into_remainder_bytes(bcs)), ETrailingBytes); + UpdateAssetPrice { + token_id, + new_price + } + } + + public fun serialize_message(message: BridgeMessage): vector { + let BridgeMessage { + message_type, + message_version, + seq_num, + source_chain, + payload + } = message; + + let message = vector[]; + vector::push_back(&mut message, message_type); + vector::push_back(&mut message, message_version); + // bcs serializes u64 as 8 bytes + vector::append(&mut message, reverse_bytes(bcs::to_bytes(&seq_num))); + vector::push_back(&mut message, source_chain); + vector::append(&mut message, payload); + message + } + + /// Token Transfer Message Format: + /// [message_type: u8] + /// [version:u8] + /// [nonce:u64] + /// [source_chain: u8] + /// [sender_address_length:u8] + /// [sender_address: byte[]] + /// [target_chain:u8] + /// [target_address_length:u8] + /// [target_address: byte[]] + /// [token_type:u8] + /// [amount:u64] + public fun create_token_bridge_message( + source_chain: u8, + seq_num: u64, + sender_address: vector, + target_chain: u8, + target_address: vector, + token_type: u8, + amount: u64 + ): BridgeMessage { + let payload = vector[]; + // sender address should be less than 255 bytes so can fit into u8 + vector::push_back(&mut payload, (vector::length(&sender_address) as u8)); + vector::append(&mut payload, sender_address); + vector::push_back(&mut payload, target_chain); + // target address should be less than 255 bytes so can fit into u8 + vector::push_back(&mut payload, (vector::length(&target_address) as u8)); + vector::append(&mut payload, target_address); + vector::push_back(&mut payload, token_type); + // bcs serialzies u64 as 8 bytes + vector::append(&mut payload, reverse_bytes(bcs::to_bytes(&amount))); + + BridgeMessage { + message_type: message_types::token(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain, + payload, + } + } + + /// Emergency Op Message Format: + /// [message_type: u8] + /// [version:u8] + /// [nonce:u64] + /// [chain_id: u8] + /// [op_type: u8] + public fun create_emergency_op_message( + source_chain: u8, + seq_num: u64, + op_type: u8, + ): BridgeMessage { + BridgeMessage { + message_type: message_types::emergency_op(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain, + payload: vector[op_type], + } + } + + /// Block list Message Format: + /// [message_type: u8] + /// [version:u8] + /// [nonce:u64] + /// [chain_id: u8] + /// [blocklist_type: u8] + /// [validator_length: u8] + /// [validator_ecdsa_addresses: byte[][]] + public fun create_block_list_message( + source_chain: u8, + seq_num: u64, + // 0: block, 1: unblock + blocklist_type: u8, + validator_ecdsa_addresses: vector>, + ): BridgeMessage { + let address_length = (vector::length(&validator_ecdsa_addresses) as u8); + let payload = vector[blocklist_type, address_length]; + let i = 0; + + while (i < address_length) { + let address = vector::borrow(&validator_ecdsa_addresses, (i as u64)); + assert!(vector::length(address) == ECDSA_ADDRESS_LENGTH, EInvalidAddressLength); + vector::append(&mut payload, *address); + i = i + 1; + }; + + BridgeMessage { + message_type: message_types::committee_blocklist(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain, + payload, + } + } + + /// Update bridge limit Message Format: + /// [message_type: u8] + /// [version:u8] + /// [nonce:u64] + /// [chain_id: u8] + /// [new_limit: u64] + public fun create_update_bridge_limit_message( + source_chain: u8, + seq_num: u64, + target_chain: u8, + new_limit: u64, + ): BridgeMessage { + let payload = vector[target_chain]; + vector::append(&mut payload, reverse_bytes(bcs::to_bytes(&new_limit))); + BridgeMessage { + message_type: message_types::update_bridge_limit(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain, + payload, + } + } + + /// Update asset price message + /// [message_type: u8] + /// [version:u8] + /// [nonce:u64] + /// [chain_id: u8] + /// [token_id: u8] + /// [new_price:u64] + public fun create_update_asset_price_message( + source_chain: u8, + seq_num: u64, + new_price: u64, + ): BridgeMessage { + let payload = vector[treasury::token_id()]; + vector::append(&mut payload, reverse_bytes(bcs::to_bytes(&new_price))); + BridgeMessage { + message_type: message_types::update_asset_price(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain, + payload, + } + } + + public fun create_key(source_chain: u8, message_type: u8, bridge_seq_num: u64): BridgeMessageKey { + BridgeMessageKey { source_chain, message_type, bridge_seq_num } + } + + public fun key(self: &BridgeMessage): BridgeMessageKey { + create_key(self.source_chain, self.message_type, self.seq_num) + } + + // BridgeMessage getters + public fun message_type(self: &BridgeMessage): u8 { + self.message_type + } + + public fun seq_num(self: &BridgeMessage): u64 { + self.seq_num + } + + // TokenBridgePayload getters + public fun source_chain(self: &BridgeMessage): u8 { + self.source_chain + } + + public fun token_target_chain(self: &TokenPayload): u8 { + self.target_chain + } + + public fun token_target_address(self: &TokenPayload): vector { + self.target_address + } + + public fun token_type(self: &TokenPayload): u8 { + self.token_type + } + + public fun token_amount(self: &TokenPayload): u64 { + self.amount + } + + // EmergencyOpPayload getters + public fun emergency_op_type(self: &EmergencyOp): u8 { + self.op_type + } + + fun reverse_bytes(bytes: vector): vector { + vector::reverse(&mut bytes); + bytes + } + + fun peel_u64_be(bcs: &mut BCS): u64 { + let (value, i) = (0u64, 64u8); + while (i > 0) { + i = i - 8; + let byte = (bcs::peel_u8(bcs) as u64); + value = value + (byte << i); + }; + value + } + + #[test_only] + public fun deserialize_message(message: vector): BridgeMessage { + let bcs = bcs::new(message); + BridgeMessage { + message_type: bcs::peel_u8(&mut bcs), + message_version: bcs::peel_u8(&mut bcs), + seq_num: peel_u64_be(&mut bcs), + source_chain: bcs::peel_u8(&mut bcs), + payload: bcs::into_remainder_bytes(bcs) + } + } + + #[test] + fun test_message_serialization_sui_to_eth() { + let sender_address = address::from_u256(100); + let scenario = test_scenario::begin(sender_address); + let ctx = test_scenario::ctx(&mut scenario); + + let coin = coin::mint_for_testing(12345, ctx); + + let token_bridge_message = create_token_bridge_message( + chain_ids::sui_testnet(), // source chain + 10, // seq_num + address::to_bytes(sender_address), // sender address + chain_ids::eth_sepolia(), // target_chain + // Eth address is 20 bytes long + hex::decode(b"00000000000000000000000000000000000000c8"), // target_address + 3u8, // token_type + balance::value(coin::balance(&coin)) // amount: u64 + ); + + // Test payload extraction + let token_payload = TokenPayload { + sender_address: address::to_bytes(sender_address), + target_chain: chain_ids::eth_sepolia(), + target_address: hex::decode(b"00000000000000000000000000000000000000c8"), + token_type: 3u8, + amount: balance::value(coin::balance(&coin)) + }; + assert!(extract_token_bridge_payload(&token_bridge_message) == token_payload, 0); + + // Test message serialization + let message = serialize_message(token_bridge_message); + let expected_msg = hex::decode( + b"0001000000000000000a012000000000000000000000000000000000000000000000000000000000000000640b1400000000000000000000000000000000000000c8030000000000003039", + ); + + assert!(message == expected_msg, 0); + assert!(token_bridge_message == deserialize_message(message), 0); + + coin::burn_for_testing(coin); + test_scenario::end(scenario); + } + + #[test] + fun test_message_serialization_eth_to_sui() { + let address_1 = address::from_u256(100); + let scenario = test_scenario::begin(address_1); + let ctx = test_scenario::ctx(&mut scenario); + + let coin = coin::mint_for_testing(12345, ctx); + + let token_bridge_message = create_token_bridge_message( + chain_ids::eth_sepolia(), // source chain + 10, // seq_num + // Eth address is 20 bytes long + hex::decode(b"00000000000000000000000000000000000000c8"), // eth sender address + chain_ids::sui_testnet(), // target_chain + address::to_bytes(address_1), // target address + 3u8, // token_type + balance::value(coin::balance(&coin)) // amount: u64 + ); + + // Test payload extraction + let token_payload = TokenPayload { + sender_address: hex::decode(b"00000000000000000000000000000000000000c8"), + target_chain: chain_ids::sui_testnet(), + target_address: address::to_bytes(address_1), + token_type: 3u8, + amount: balance::value(coin::balance(&coin)) + }; + assert!(extract_token_bridge_payload(&token_bridge_message) == token_payload, 0); + + + // Test message serialization + let message = serialize_message(token_bridge_message); + let expected_msg = hex::decode( + b"0001000000000000000a0b1400000000000000000000000000000000000000c801200000000000000000000000000000000000000000000000000000000000000064030000000000003039", + ); + assert!(message == expected_msg, 0); + assert!(token_bridge_message == deserialize_message(message), 0); + + coin::burn_for_testing(coin); + test_scenario::end(scenario); + } + + #[test] + fun test_emergency_op_message_serialization() { + let emergency_op_message = create_emergency_op_message( + chain_ids::sui_testnet(), // source chain + 10, // seq_num + 0, + ); + + // Test message serialization + let message = serialize_message(emergency_op_message); + let expected_msg = hex::decode( + b"0201000000000000000a0100", + ); + + assert!(message == expected_msg, 0); + assert!(emergency_op_message == deserialize_message(message), 0); + } + + #[test] + fun test_blocklist_message_serialization() { + let validator_pub_key1 = hex::decode(b"b14d3c4f5fbfbcfb98af2d330000d49c95b93aa7"); + let validator_pub_key2 = hex::decode(b"f7e93cc543d97af6632c9b8864417379dba4bf15"); + + let validator_addresses = vector[validator_pub_key1, validator_pub_key2]; + let blocklist_message = create_block_list_message( + chain_ids::sui_testnet(), // source chain + 10, // seq_num + 0, + validator_addresses + ); + // Test message serialization + let message = serialize_message(blocklist_message); + + let expected_msg = hex::decode( + b"0101000000000000000a010002b14d3c4f5fbfbcfb98af2d330000d49c95b93aa7f7e93cc543d97af6632c9b8864417379dba4bf15", + ); + + assert!(message == expected_msg, 0); + assert!(blocklist_message == deserialize_message(message), 0); + + let blocklist = extract_blocklist_payload(&blocklist_message); + assert!(blocklist.validator_addresses == validator_addresses, 0) + } + + #[test] + fun test_update_bridge_limit_message_serialization() { + let update_bridge_limit = create_update_bridge_limit_message( + chain_ids::sui_testnet(), // source chain + 10, // seq_num + chain_ids::eth_sepolia(), + 1000000000 + ); + + // Test message serialization + let message = serialize_message(update_bridge_limit); + let expected_msg = hex::decode( + b"0301000000000000000a010b000000003b9aca00", + ); + + assert!(message == expected_msg, 0); + assert!(update_bridge_limit == deserialize_message(message), 0); + + let bridge_limit = extract_update_bridge_limit(&update_bridge_limit); + assert!(bridge_limit.source_chain == chain_ids::sui_testnet(), 0); + assert!(bridge_limit.target_chain == chain_ids::eth_sepolia(), 0); + assert!(bridge_limit.limit == 1000000000, 0); + } + + #[test] + fun test_update_asset_price_message_serialization() { + let asset_price_message = create_update_asset_price_message( + chain_ids::sui_testnet(), // source chain + 10, // seq_num + 12345 + ); + + // Test message serialization + let message = serialize_message(asset_price_message); + let expected_msg = hex::decode( + b"0401000000000000000a01020000000000003039", + ); + assert!(message == expected_msg, 0); + assert!(asset_price_message == deserialize_message(message), 0); + + let asset_price = extract_update_asset_price(&asset_price_message); + assert!(asset_price.token_id == token_id(), 0); + assert!(asset_price.new_price == 12345, 0); + } + + #[test] + fun test_be_to_le_conversion() { + let input = hex::decode(b"78563412"); + let expected = hex::decode(b"12345678"); + assert!(reverse_bytes(input) == expected, 0) + } + + #[test] + fun test_peel_u64_be() { + let input = hex::decode(b"0000000000003039"); + let expected = 12345u64; + let bcs = bcs::new(input); + assert!(peel_u64_be(&mut bcs) == expected, 0) + } +} diff --git a/crates/sui-framework/packages/bridge/sources/message_types.move b/crates/sui-framework/packages/bridge/sources/message_types.move new file mode 100644 index 0000000000000..a74b3d072814c --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/message_types.move @@ -0,0 +1,31 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::message_types { + // message types + const TOKEN: u8 = 0; + const COMMITTEE_BLOCKLIST: u8 = 1; + const EMERGENCY_OP: u8 = 2; + const UPDATE_BRIDGE_LIMIT: u8 = 3; + const UPDATE_ASSET_PRICE: u8 = 4; + + public fun token():u8{ + TOKEN + } + + public fun committee_blocklist():u8{ + COMMITTEE_BLOCKLIST + } + + public fun emergency_op():u8{ + EMERGENCY_OP + } + + public fun update_bridge_limit():u8{ + UPDATE_BRIDGE_LIMIT + } + + public fun update_asset_price():u8{ + UPDATE_ASSET_PRICE + } +} diff --git a/crates/sui-framework/packages/bridge/sources/treasury.move b/crates/sui-framework/packages/bridge/sources/treasury.move new file mode 100644 index 0000000000000..ad3ebcf7e4851 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/treasury.move @@ -0,0 +1,80 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::treasury { + use std::type_name; + + use sui::coin::{Self, Coin}; + use sui::object_bag::{Self, ObjectBag}; + use sui::tx_context::{Self, TxContext}; + + use bridge::btc; + use bridge::btc::BTC; + use bridge::eth; + use bridge::eth::ETH; + use bridge::usdc; + use bridge::usdc::USDC; + use bridge::usdt; + use bridge::usdt::USDT; + + friend bridge::bridge; + + const EUnsupportedTokenType: u64 = 0; + const ENotSystemAddress: u64 = 1; + + struct BridgeTreasury has store { + treasuries: ObjectBag + } + + public fun token_id(): u8 { + let coin_type = type_name::get(); + if (coin_type == type_name::get()) { + 1 + } else if (coin_type == type_name::get()) { + 2 + } else if (coin_type == type_name::get()) { + 3 + } else if (coin_type == type_name::get()) { + 4 + } else { + abort EUnsupportedTokenType + } + } + + public(friend) fun create(ctx: &mut TxContext): BridgeTreasury { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + BridgeTreasury { + treasuries: object_bag::new(ctx) + } + } + + public(friend) fun burn(self: &mut BridgeTreasury, token: Coin, ctx: &mut TxContext) { + create_treasury_if_not_exist(self, ctx); + let treasury = object_bag::borrow_mut(&mut self.treasuries, type_name::get()); + coin::burn(treasury, token); + } + + public(friend) fun mint(self: &mut BridgeTreasury, amount: u64, ctx: &mut TxContext): Coin { + create_treasury_if_not_exist(self, ctx); + let treasury = object_bag::borrow_mut(&mut self.treasuries, type_name::get()); + coin::mint(treasury, amount, ctx) + } + + fun create_treasury_if_not_exist(self: &mut BridgeTreasury, ctx: &mut TxContext) { + let type = type_name::get(); + if (!object_bag::contains(&self.treasuries, type)) { + // Lazily create currency if not exists + if (type == type_name::get()) { + object_bag::add(&mut self.treasuries, type, btc::create(ctx)); + } else if (type == type_name::get()) { + object_bag::add(&mut self.treasuries, type, eth::create(ctx)); + } else if (type == type_name::get()) { + object_bag::add(&mut self.treasuries, type, usdc::create(ctx)); + } else if (type == type_name::get()) { + object_bag::add(&mut self.treasuries, type, usdt::create(ctx)); + } else { + abort EUnsupportedTokenType + }; + }; + } +} diff --git a/crates/sui-framework/packages/bridge/sources/usdc.move b/crates/sui-framework/packages/bridge/sources/usdc.move new file mode 100644 index 0000000000000..535db12bf5a64 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/usdc.move @@ -0,0 +1,29 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::usdc { + use std::option; + + use sui::coin; + use sui::coin::TreasuryCap; + use sui::transfer; + use sui::tx_context::TxContext; + + friend bridge::treasury; + + struct USDC has drop {} + + public(friend) fun create(ctx: &mut TxContext): TreasuryCap { + let (treasury_cap, metadata) = coin::create_currency( + USDC {}, + 6, + b"USDC", + b"USD Coin", + b"Bridged USD Coin token", + option::none(), + ctx + ); + transfer::public_freeze_object(metadata); + treasury_cap + } +} diff --git a/crates/sui-framework/packages/bridge/sources/usdt.move b/crates/sui-framework/packages/bridge/sources/usdt.move new file mode 100644 index 0000000000000..8fc0bc0d709f1 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/usdt.move @@ -0,0 +1,29 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::usdt { + use std::option; + + use sui::coin; + use sui::coin::TreasuryCap; + use sui::transfer; + use sui::tx_context::TxContext; + + friend bridge::treasury; + + struct USDT has drop {} + + public(friend) fun create(ctx: &mut TxContext): TreasuryCap { + let (treasury_cap, metadata) = coin::create_currency( + USDT {}, + 6, + b"USDT", + b"Tether", + b"Bridged Tether token", + option::none(), + ctx + ); + transfer::public_freeze_object(metadata); + treasury_cap + } +} diff --git a/crates/sui-framework/packages/sui-framework/sources/object.move b/crates/sui-framework/packages/sui-framework/sources/object.move index 8c9bbf88f00e6..a8a68eb8ae440 100644 --- a/crates/sui-framework/packages/sui-framework/sources/object.move +++ b/crates/sui-framework/packages/sui-framework/sources/object.move @@ -34,6 +34,9 @@ module sui::object { /// The hardcoded ID for the singleton DenyList. const SUI_DENY_LIST_OBJECT_ID: address = @0x403; + /// The hardcoded ID for the Bridge Object. + const SUI_BRIDGE_ID: address = @0x9; + /// Sender is not @0x0 the system address. const ENotSystemAddress: u64 = 0; @@ -127,6 +130,15 @@ module sui::object { } } + #[allow(unused_function)] + /// Create the `UID` for the singleton `Bridge` object. + /// This should only be called once from `bridge`. + fun bridge(): UID { + UID { + id: ID { bytes: SUI_BRIDGE_ID } + } + } + /// Get the inner `ID` of `uid` public fun uid_as_inner(uid: &UID): &ID { &uid.id diff --git a/crates/sui-framework/src/lib.rs b/crates/sui-framework/src/lib.rs index e5dcc2e176783..a7352fe305239 100644 --- a/crates/sui-framework/src/lib.rs +++ b/crates/sui-framework/src/lib.rs @@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize}; use std::fmt::Formatter; use sui_types::base_types::ObjectRef; use sui_types::storage::ObjectStore; -use sui_types::DEEPBOOK_PACKAGE_ID; use sui_types::{ base_types::ObjectID, digests::TransactionDigest, @@ -18,6 +17,7 @@ use sui_types::{ object::{Object, OBJECT_START_VERSION}, MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID, SUI_SYSTEM_PACKAGE_ID, }; +use sui_types::{BRIDGE_PACKAGE_ID, DEEPBOOK_PACKAGE_ID}; use tracing::error; /// Represents a system package in the framework, that's built from the source code inside @@ -122,6 +122,11 @@ impl BuiltInFramework { DEEPBOOK_PACKAGE_ID, "deepbook", [MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID] + ), + ( + BRIDGE_PACKAGE_ID, + "bridge", + [MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID] ) ]) .iter() diff --git a/crates/sui-move-build/src/lib.rs b/crates/sui-move-build/src/lib.rs index 29c3dc91d7f65..7179f4be31ef8 100644 --- a/crates/sui-move-build/src/lib.rs +++ b/crates/sui-move-build/src/lib.rs @@ -48,7 +48,8 @@ use sui_types::{ error::{SuiError, SuiResult}, is_system_package, move_package::{FnInfo, FnInfoKey, FnInfoMap, MovePackage}, - DEEPBOOK_ADDRESS, MOVE_STDLIB_ADDRESS, SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS, + BRIDGE_ADDRESS, DEEPBOOK_ADDRESS, MOVE_STDLIB_ADDRESS, SUI_FRAMEWORK_ADDRESS, + SUI_SYSTEM_ADDRESS, }; use sui_verifier::{default_verifier_config, verifier as sui_bytecode_verifier}; @@ -409,6 +410,12 @@ impl CompiledPackage { .filter(|m| *m.self_id().address() == DEEPBOOK_ADDRESS) } + /// Get bytecode modules from DeepBook that are used by this package + pub fn get_bridge_modules(&self) -> impl Iterator { + self.get_modules_and_deps() + .filter(|m| *m.self_id().address() == BRIDGE_ADDRESS) + } + /// Get bytecode modules from the Sui System that are used by this package pub fn get_sui_system_modules(&self) -> impl Iterator { self.get_modules_and_deps() diff --git a/crates/sui-protocol-config/src/lib.rs b/crates/sui-protocol-config/src/lib.rs index c2caf411c3ce5..0b44b719fcf2b 100644 --- a/crates/sui-protocol-config/src/lib.rs +++ b/crates/sui-protocol-config/src/lib.rs @@ -12,7 +12,7 @@ use tracing::{info, warn}; /// The minimum and maximum protocol versions supported by this build. const MIN_PROTOCOL_VERSION: u64 = 1; -const MAX_PROTOCOL_VERSION: u64 = 36; +const MAX_PROTOCOL_VERSION: u64 = 37; // Record history of protocol version allocations here: // @@ -108,6 +108,9 @@ const MAX_PROTOCOL_VERSION: u64 = 36; // Version 36: Enable group operations native functions in devnet. // Enable shared object deletion in mainnet. // Set the consensus accepted transaction size and the included transactions size in the proposed block. +// Version 37: Add native bridge. +// Enable native bridge in devnet + #[derive(Copy, Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct ProtocolVersion(u64); @@ -335,6 +338,10 @@ struct FeatureFlags { #[serde(skip_serializing_if = "is_false")] random_beacon: bool, + // Enable bridge protocol + #[serde(skip_serializing_if = "is_false")] + bridge: bool, + #[serde(skip_serializing_if = "is_false")] enable_effects_v2: bool, @@ -1111,6 +1118,15 @@ impl ProtocolConfig { ret } + pub fn enable_bridge(&self) -> bool { + let ret = self.feature_flags.bridge; + if ret { + // bridge required end-of-epoch transactions + assert!(self.feature_flags.end_of_epoch_transaction_supported); + } + ret + } + pub fn enable_effects_v2(&self) -> bool { self.feature_flags.enable_effects_v2 } @@ -1886,6 +1902,12 @@ impl ProtocolConfig { cfg.consensus_max_transactions_in_block_bytes = Some(6 * 1_024 * 1024); // 6 MB } + 37 => { + // enable bridge in devnet + if chain != Chain::Mainnet && chain != Chain::Testnet { + cfg.feature_flags.bridge = true; + } + } // Use this template when making changes: // // // modify an existing constant. diff --git a/sui-execution/v1/sui-verifier/src/id_leak_verifier.rs b/sui-execution/v1/sui-verifier/src/id_leak_verifier.rs index bd2e7c4065559..8c28e0516ad91 100644 --- a/sui-execution/v1/sui-verifier/src/id_leak_verifier.rs +++ b/sui-execution/v1/sui-verifier/src/id_leak_verifier.rs @@ -29,13 +29,14 @@ use move_core_types::{ account_address::AccountAddress, ident_str, identifier::IdentStr, vm_status::StatusCode, }; use std::{collections::BTreeMap, error::Error, num::NonZeroU64}; +use sui_types::bridge::BRIDGE_MODULE_NAME; use sui_types::{ authenticator_state::AUTHENTICATOR_STATE_MODULE_NAME, clock::CLOCK_MODULE_NAME, error::{ExecutionError, VMMVerifierErrorSubStatusCode}, id::OBJECT_MODULE_NAME, sui_system_state::SUI_SYSTEM_MODULE_NAME, - SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS, + BRIDGE_ADDRESS, SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS, }; use crate::{ @@ -83,11 +84,14 @@ const SUI_AUTHENTICATOR_STATE_CREATE: FunctionIdent = ( AUTHENTICATOR_STATE_MODULE_NAME, ident_str!("create"), ); +const SUI_BRIDGE_CREATE: FunctionIdent = + (&BRIDGE_ADDRESS, BRIDGE_MODULE_NAME, ident_str!("create")); const FRESH_ID_FUNCTIONS: &[FunctionIdent] = &[OBJECT_NEW, OBJECT_NEW_UID_FROM_HASH, TS_NEW_OBJECT]; const FUNCTIONS_TO_SKIP: &[FunctionIdent] = &[ SUI_SYSTEM_CREATE, SUI_CLOCK_CREATE, SUI_AUTHENTICATOR_STATE_CREATE, + SUI_BRIDGE_CREATE, ]; impl AbstractValue { diff --git a/sui-execution/v1/sui-verifier/src/one_time_witness_verifier.rs b/sui-execution/v1/sui-verifier/src/one_time_witness_verifier.rs index 59909f1bd454b..70664b5d0e2b2 100644 --- a/sui-execution/v1/sui-verifier/src/one_time_witness_verifier.rs +++ b/sui-execution/v1/sui-verifier/src/one_time_witness_verifier.rs @@ -24,11 +24,12 @@ use move_binary_format::{ }, }; use move_core_types::{ident_str, language_storage::ModuleId}; +use sui_types::bridge::BRIDGE_SUPPORTED_ASSET; use sui_types::{ base_types::{TX_CONTEXT_MODULE_NAME, TX_CONTEXT_STRUCT_NAME}, error::ExecutionError, move_package::{is_test_fun, FnInfoMap}, - SUI_FRAMEWORK_ADDRESS, + BRIDGE_ADDRESS, SUI_FRAMEWORK_ADDRESS, }; use crate::{verification_failure, INIT_FN_NAME}; @@ -45,7 +46,16 @@ pub fn verify_module( // the module has no initializer). The reason for it is that the SUI coin is only instantiated // during genesis. It is easiest to simply special-case this module particularly that this is // framework code and thus deemed correct. - if ModuleId::new(SUI_FRAMEWORK_ADDRESS, ident_str!("sui").to_owned()) == module.self_id() { + let self_id = module.self_id(); + + if ModuleId::new(SUI_FRAMEWORK_ADDRESS, ident_str!("sui").to_owned()) == self_id { + return Ok(()); + } + + if BRIDGE_SUPPORTED_ASSET + .iter() + .any(|token| ModuleId::new(BRIDGE_ADDRESS, ident_str!(token).to_owned()) == self_id) + { return Ok(()); }