-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1 parent
d073303
commit 6b81078
Showing
11 changed files
with
2,006 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[package] | ||
name = "AxelarGateway" | ||
version = "0.1.0" | ||
edition = "2024.beta" | ||
|
||
[dependencies] | ||
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } | ||
|
||
[addresses] | ||
axelar_gateway = "0x0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"singletons": [ | ||
"gateway::Gateway", | ||
"discovery::RelayerDiscovery" | ||
] | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,324 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
module axelar_gateway::channel { | ||
use std::ascii::String; | ||
use sui::table::{Self, Table}; | ||
use sui::event; | ||
|
||
/// Generic target for the messaging system. | ||
/// | ||
/// This struct is required on the Sui side to be the destination for the | ||
/// messages sent from other chains. Even though it has a UID field, it does | ||
/// not have a `key` ability to force wrapping. | ||
/// | ||
/// Notes: | ||
/// | ||
/// - Does not have key to prevent 99% of the mistakes related to access management. | ||
/// Also prevents arbitrary Message destruction if the object is shared. Lastly, | ||
/// when shared, `Channel` cannot be destroyed, and its contents will remain locked | ||
/// forever. | ||
/// | ||
/// - Allows asset or capability-locking inside. Some applications might | ||
/// authorize admin actions through the bridge (eg by locking some `AdminCap` | ||
/// inside and getting a `&mut AdminCap` in the `consume_message`); | ||
/// | ||
/// - Can be destroyed freely as the `UID` is guaranteed to be unique across | ||
/// the system. Destroying a channel would mean the end of the Channel cycle | ||
/// and all further messages will have to target a new Channel if there is one. | ||
/// | ||
/// - Does not contain direct link to the state in Sui, as some functions | ||
/// might not take any specific data (eg allow users to create new objects). | ||
/// If specific object on Sui is targeted by this `Channel`, its reference | ||
/// should be implemented using the `data` field. | ||
/// | ||
/// - The funniest and extremely simple implementation would be a `Channel<ID>` | ||
/// since it actually contains the data required to point at the object in Sui. | ||
|
||
/// For when trying to consume the wrong object. | ||
const EWrongDestination: u64 = 0; | ||
/// For when message has already been processed and submitted twice. | ||
const EDuplicateMessage: u64 = 2; | ||
|
||
/// The Channel object. Acts as a destination for the messages sent through | ||
/// the bridge. The `target_id` is compared against the `id` of the `Channel` | ||
/// during the message consumption. | ||
/// | ||
/// The `T` parameter allows wrapping a Capability or a piece of data into | ||
/// the channel to be used when the message is consumed (eg authorize a | ||
/// `mint` call using a stored `AdminCap`). | ||
public struct Channel has key, store { | ||
/// Unique ID of the target object which allows message targeting | ||
/// by comparing against `id_bytes`. | ||
id: UID, | ||
/// Messages processed by this object for the current axelar epoch. To make system less | ||
/// centralized, and spread the storage + io costs across multiple | ||
/// destinations, we can track every `Channel`'s messages. | ||
processed_call_approvals: Table<address, bool>, | ||
} | ||
|
||
/// A HotPotato - call received from the Gateway. Must be delivered to the | ||
/// matching Channel, otherwise the TX fails. | ||
public struct ApprovedCall { | ||
/// ID of the call approval, guaranteed to be unique by Axelar. | ||
cmd_id: address, | ||
/// The target Channel's UID. | ||
target_id: address, | ||
/// Name of the chain where this approval came from. | ||
source_chain: String, | ||
/// Address of the source chain (vector used for compatibility). | ||
/// UTF8 / ASCII encoded string (for 0x0... eth address gonna be 42 bytes with 0x) | ||
source_address: String, | ||
/// Payload of the command. | ||
payload: vector<u8>, | ||
} | ||
|
||
// ====== Events ====== | ||
|
||
public struct ChannelCreated has copy, drop { | ||
id: address, | ||
} | ||
|
||
public struct ChannelDestroyed has copy, drop { | ||
id: address, | ||
} | ||
|
||
/// Create new `Channel` object. Anyone can create their own `Channel` to target | ||
/// from the outside and there's no limitation to the data stored inside it. | ||
/// | ||
/// `copy` ability is required to disallow asset locking inside the `Channel`. | ||
public fun new(ctx: &mut TxContext): Channel { | ||
let id = object::new(ctx); | ||
event::emit(ChannelCreated { id: id.uid_to_address() }); | ||
|
||
Channel { | ||
id, | ||
processed_call_approvals: table::new(ctx), | ||
} | ||
} | ||
|
||
/// Destroy a `Channen` releasing the T. Not constrained and can be performed | ||
/// by any party as long as they own a Channel. | ||
public fun destroy(self: Channel) { | ||
let Channel { id, processed_call_approvals } = self; | ||
|
||
processed_call_approvals.drop(); | ||
event::emit(ChannelDestroyed { id: id.uid_to_address() }); | ||
id.delete(); | ||
} | ||
|
||
public fun id(self: &Channel): ID { | ||
object::id(self) | ||
} | ||
|
||
public fun to_address(self: &Channel): address { | ||
object::id_address(self) | ||
} | ||
|
||
/// Create a new `ApprovedCall` object to be sent to another chain. Is called | ||
/// by the gateway when a message is "picked up" by the relayer. | ||
public(package) fun create_approved_call( | ||
cmd_id: address, | ||
source_chain: String, | ||
source_address: String, | ||
target_id: address, | ||
payload: vector<u8>, | ||
): ApprovedCall { | ||
ApprovedCall { | ||
cmd_id, | ||
source_chain, | ||
source_address, | ||
target_id, | ||
payload | ||
} | ||
} | ||
|
||
/// Consume a approved call hot potato object sent to this `Channel` from another chain. | ||
/// For Capability-locking, a mutable reference to the `Channel.data` field is returned. | ||
/// | ||
/// Returns a mutable reference to the locked T, the `source_chain`, the `source_address` | ||
/// and the `payload` to be used by the consuming application. | ||
public fun consume_approved_call( | ||
channel: &mut Channel, | ||
approved_call: ApprovedCall | ||
): (String, String, vector<u8>) { | ||
let ApprovedCall { | ||
cmd_id, | ||
target_id, | ||
source_chain, | ||
source_address, | ||
payload, | ||
} = approved_call; | ||
|
||
// Check if the message has already been processed. | ||
assert!(!channel.processed_call_approvals.contains(cmd_id), EDuplicateMessage); | ||
// Check if the message is sent to the correct destination. | ||
assert!(target_id == object::id_address(channel), EWrongDestination); | ||
|
||
channel.processed_call_approvals.add(cmd_id, true); | ||
|
||
( | ||
source_chain, | ||
source_address, | ||
payload, | ||
) | ||
} | ||
|
||
#[test_only] | ||
public fun create_test_approved_call( | ||
cmd_id: address, | ||
source_chain: String, | ||
source_address: String, | ||
target_id: address, | ||
payload: vector<u8>, | ||
): ApprovedCall { | ||
create_approved_call( | ||
cmd_id, | ||
source_chain, | ||
source_address, | ||
target_id, | ||
payload, | ||
) | ||
} | ||
|
||
#[test] | ||
fun test_new_and_destroy() { | ||
let ctx = &mut sui::tx_context::dummy(); | ||
let channel: Channel = new(ctx); | ||
channel.destroy() | ||
} | ||
|
||
#[test] | ||
fun test_id() { | ||
let ctx = &mut sui::tx_context::dummy(); | ||
let channel: Channel = new(ctx); | ||
assert!(channel.id() == object::id(&channel), 0); | ||
channel.destroy() | ||
} | ||
|
||
|
||
#[test] | ||
fun test_to_address() { | ||
let ctx = &mut sui::tx_context::dummy(); | ||
let channel: Channel = new(ctx); | ||
assert!(channel.to_address() == object::id_address(&channel), 0); | ||
channel.destroy() | ||
} | ||
|
||
#[test] | ||
fun test_create_approved_call() { | ||
let input_cmd_id = @0x1234; | ||
let input_source_chain = std::ascii::string(b"Source Chain"); | ||
let input_source_address = std::ascii::string(b"Source Address"); | ||
let input_target_id = @0x5678; | ||
let input_payload = b"payload"; | ||
let approved_call: ApprovedCall = create_approved_call( | ||
input_cmd_id, | ||
input_source_chain, | ||
input_source_address, | ||
input_target_id, | ||
input_payload | ||
); | ||
|
||
let ApprovedCall { | ||
cmd_id, | ||
source_chain, | ||
source_address, | ||
target_id, | ||
payload | ||
} = approved_call; | ||
assert!(cmd_id == input_cmd_id, 0); | ||
assert!(source_chain == input_source_chain, 1); | ||
assert!(source_address == input_source_address, 2); | ||
assert!(target_id == input_target_id, 3); | ||
assert!(payload == input_payload, 4); | ||
} | ||
|
||
#[test] | ||
fun test_consume_approved_call() { | ||
let ctx = &mut sui::tx_context::dummy(); | ||
let mut channel: Channel = new(ctx); | ||
|
||
let input_cmd_id = @0x1234; | ||
let input_source_chain = std::ascii::string(b"Source Chain"); | ||
let input_source_address = std::ascii::string(b"Source Address"); | ||
let input_target_id = channel.to_address(); | ||
let input_payload = b"payload"; | ||
let approved_call: ApprovedCall = create_approved_call( | ||
input_cmd_id, | ||
input_source_chain, | ||
input_source_address, | ||
input_target_id, | ||
input_payload, | ||
); | ||
|
||
let (source_chain, source_address, payload) = consume_approved_call(&mut channel, approved_call); | ||
|
||
|
||
assert!(source_chain == input_source_chain, 1); | ||
assert!(source_address == input_source_address, 2); | ||
assert!(payload == input_payload, 4); | ||
|
||
channel.destroy(); | ||
} | ||
|
||
#[test] | ||
#[expected_failure(abort_code = EDuplicateMessage)] | ||
fun test_consume_approved_call_duplicate_message() { | ||
let ctx = &mut sui::tx_context::dummy(); | ||
let mut channel: Channel = new(ctx); | ||
|
||
let cmd_id = @0x1234; | ||
let source_chain1 = std::ascii::string(b"Source Chain 1"); | ||
let source_address1 = std::ascii::string(b"Source Address 1"); | ||
let target_id1 = channel.to_address(); | ||
let payload1 = b"payload 1"; | ||
let source_chain2 = std::ascii::string(b"Source Chain"); | ||
let source_address2 = std::ascii::string(b"Source Address"); | ||
let target_id2 = channel.to_address(); | ||
let payload2 = b"payload 2"; | ||
|
||
consume_approved_call(&mut channel, create_approved_call( | ||
cmd_id, | ||
source_chain1, | ||
source_address1, | ||
target_id1, | ||
payload1, | ||
)); | ||
|
||
consume_approved_call(&mut channel, create_approved_call( | ||
cmd_id, | ||
source_chain2, | ||
source_address2, | ||
target_id2, | ||
payload2, | ||
)); | ||
|
||
channel.destroy(); | ||
} | ||
|
||
#[test] | ||
#[expected_failure(abort_code = EWrongDestination)] | ||
fun test_consume_approved_call_wrong_destination() { | ||
let ctx = &mut sui::tx_context::dummy(); | ||
let mut channel: Channel = new(ctx); | ||
|
||
let cmd_id = @0x1234; | ||
let source_chain = std::ascii::string(b"Source Chain"); | ||
let source_address = std::ascii::string(b"Source Address"); | ||
let target_id = @0x5678; | ||
let payload = b"payload"; | ||
|
||
let approved_call = create_approved_call( | ||
cmd_id, | ||
source_chain, | ||
source_address, | ||
target_id, | ||
payload, | ||
); | ||
|
||
consume_approved_call(&mut channel, approved_call); | ||
|
||
channel.destroy(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,317 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
/// This module implements a discovery mechanic for the Relayer to be able to | ||
/// call some (!) transactions automatically. | ||
/// | ||
/// Warning: this solution does allow for any transaction to be executed and | ||
/// should be treated as a reference and a temporary solution until there's a | ||
/// proper discovery / execution mechanism in place. | ||
module axelar_gateway::discovery { | ||
use std::ascii::{Self, String}; | ||
use std::type_name; | ||
|
||
use sui::table::{Self, Table}; | ||
use sui::bcs::{Self, BCS}; | ||
use sui::hex; | ||
use sui::address; | ||
|
||
use axelar_gateway::channel::Channel; | ||
|
||
/// TypeArgument is not a valid string. | ||
const EInvalidString: u64 = 0; | ||
|
||
/// Channel not found. | ||
const EChannelNotFound: u64 = 1; | ||
|
||
/// A central shared object that stores discovery configuration for the | ||
/// Relayer. The Relayer will use this object to discover and execute the | ||
/// transactions when a message is targeted at specific channel. | ||
public struct RelayerDiscovery has key { | ||
id: UID, | ||
/// A map of channel IDs to the target that needs to be executed by the | ||
/// relayer. There can be only one configuration per channel. | ||
configurations: Table<ID, Transaction>, | ||
} | ||
|
||
public struct Function has store, copy, drop { | ||
package_id: address, | ||
module_name: String, | ||
name: String, | ||
} | ||
|
||
/// Arguments are prefixed with: | ||
/// - 0 for objects followed by exactly 32 bytes that cointain the object id | ||
/// - 1 for pures followed by the bcs encoded form of the pure | ||
/// - 2 for the call contract object, followed by nothing (to be passed into the target function) | ||
/// - 3 for the payload of the contract call (to be passed into the intermediate function) | ||
/// - 4 for an argument returned from a previous move call, followed by a u8 specified which call to get the return of (0 for the first transaction AFTER the one that gets ApprovedCall out), and then another u8 specifying which argument to input. | ||
public struct MoveCall has store, copy, drop { | ||
function: Function, | ||
arguments: vector<vector<u8>>, | ||
type_arguments: vector<String>, | ||
} | ||
|
||
public struct Transaction has store, copy, drop { | ||
is_final: bool, | ||
move_calls: vector<MoveCall>, | ||
} | ||
|
||
|
||
|
||
fun init(ctx: &mut TxContext) { | ||
transfer::share_object(RelayerDiscovery { | ||
id: object::new(ctx), | ||
configurations: table::new(ctx), | ||
}); | ||
} | ||
|
||
/// During the creation of the object, the UID should be passed here to | ||
/// receive the Channel and emit an event which will be handled by the | ||
/// Relayer. | ||
/// | ||
/// Example: | ||
/// ``` | ||
/// let id = object::new(ctx); | ||
/// let channel = discovery::create_configuration( | ||
/// relayer_discovery, &id, contents, ctx | ||
/// ); | ||
/// let wrapper = ExampleWrapper { id, channel }; | ||
/// transfer::share_object(wrapper); | ||
/// ``` | ||
/// | ||
/// Note: Wrapper must be a shared object so that Relayer can access it. | ||
public fun register_transaction( | ||
self: &mut RelayerDiscovery, | ||
channel: &Channel, | ||
tx: Transaction, | ||
) { | ||
let channel_id = channel.id(); | ||
if (self.configurations.contains(channel_id)) { | ||
self.configurations.remove(channel_id); | ||
}; | ||
self.configurations.add(channel_id, tx); | ||
} | ||
|
||
/// Get a transaction for a specific channel by the channel `ID`. | ||
public fun get_transaction( | ||
self: &mut RelayerDiscovery, | ||
channel_id: ID, | ||
): Transaction { | ||
assert!(self.configurations.contains(channel_id), EChannelNotFound); | ||
*self.configurations.borrow(channel_id) | ||
} | ||
|
||
// === Tx Building === | ||
|
||
public fun new_function( | ||
package_id: address, module_name: String, name: String | ||
): Function { | ||
Function { | ||
package_id, | ||
module_name, | ||
name, | ||
} | ||
} | ||
|
||
public fun new_function_from_bcs(bcs: &mut BCS): Function { | ||
Function { | ||
package_id: bcs::peel_address(bcs), | ||
module_name: ascii::string(bcs::peel_vec_u8(bcs)), | ||
name: ascii::string(bcs::peel_vec_u8(bcs)), | ||
} | ||
} | ||
|
||
public fun new_move_call( | ||
function: Function, | ||
arguments: vector<vector<u8>>, | ||
type_arguments: vector<String> | ||
): MoveCall { | ||
MoveCall { | ||
function, | ||
arguments, | ||
type_arguments, | ||
} | ||
} | ||
|
||
public fun new_move_call_from_bcs(bcs: &mut BCS): MoveCall { | ||
let function = new_function_from_bcs(bcs); | ||
let arguments = bcs.peel_vec_vec_u8(); | ||
let length = bcs.peel_vec_length(); | ||
let mut type_arguments = vector[]; | ||
let mut i = 0; | ||
|
||
while (i < length) { | ||
let mut type_argument = ascii::try_string(bcs.peel_vec_u8()); | ||
assert!(type_argument.is_some(), EInvalidString); | ||
vector::push_back(&mut type_arguments, type_argument.extract()); | ||
i = i + 1; | ||
}; | ||
|
||
MoveCall { | ||
function, | ||
arguments, | ||
type_arguments, | ||
} | ||
} | ||
|
||
public fun new_transaction(is_final: bool, move_calls: vector<MoveCall>): Transaction { | ||
Transaction { | ||
is_final, | ||
move_calls, | ||
} | ||
} | ||
|
||
public fun new_transaction_from_bcs(bcs: &mut BCS): Transaction { | ||
let is_final = bcs.peel_bool(); | ||
let length = bcs.peel_vec_length(); | ||
let mut move_calls = vector[]; | ||
let mut i = 0; | ||
|
||
while (i < length) { | ||
vector::push_back( | ||
&mut move_calls, | ||
new_move_call_from_bcs(bcs), | ||
); | ||
i = i + 1; | ||
}; | ||
|
||
Transaction { | ||
is_final, | ||
move_calls, | ||
} | ||
} | ||
|
||
/// Helper function which returns the package id of from a type. | ||
public fun package_id<T>(): address { | ||
address::from_bytes( | ||
hex::decode( | ||
*ascii::as_bytes( | ||
&type_name::get_address(&type_name::get<T>()) | ||
) | ||
) | ||
) | ||
} | ||
|
||
#[test_only] | ||
public fun package_id_from_function(self: &Function): address { | ||
self.package_id | ||
} | ||
|
||
#[test_only] | ||
public fun module_name(self: &Function): ascii::String { | ||
self.module_name | ||
} | ||
|
||
#[test_only] | ||
public fun name(self: &Function): ascii::String { | ||
self.name | ||
} | ||
|
||
#[test_only] | ||
public fun function(self: &MoveCall): Function { | ||
self.function | ||
} | ||
|
||
#[test_only] | ||
public fun arguments(self: &MoveCall): vector<vector<u8>> { | ||
self.arguments | ||
} | ||
|
||
#[test_only] | ||
public fun type_arguments(self: &MoveCall): vector<ascii::String> { | ||
self.type_arguments | ||
} | ||
|
||
#[test_only] | ||
public fun is_final(self: &Transaction): bool { | ||
self.is_final | ||
} | ||
|
||
#[test_only] | ||
public fun move_calls(self: &Transaction): vector<MoveCall> { | ||
self.move_calls | ||
} | ||
|
||
|
||
#[test_only] | ||
public fun new(ctx: &mut TxContext): RelayerDiscovery { | ||
RelayerDiscovery { | ||
id: object::new(ctx), | ||
configurations: table::new(ctx), | ||
} | ||
} | ||
|
||
#[test] | ||
fun tx_builder() { | ||
let function = new_function( | ||
@0x1, | ||
ascii::string(b"ascii"), | ||
ascii::string(b"string"), | ||
); | ||
|
||
let _tx = function.new_move_call( | ||
vector[ bcs::to_bytes(&b"some_string") ], // arguments | ||
vector[ ], // type_arguments | ||
); | ||
} | ||
|
||
#[test] | ||
fun test_new_function_from_bcs() { | ||
let package_id = @0x5f7809eb09754577387a816582ece609511d0262b2c52aa15306083ca3c85962; | ||
let module_name = std::ascii::string(b"module"); | ||
let name = std::ascii::string(b"function"); | ||
let input = x"5f7809eb09754577387a816582ece609511d0262b2c52aa15306083ca3c85962066d6f64756c650866756e6374696f6e"; | ||
|
||
let function = new_function_from_bcs(&mut bcs::new(input)); | ||
assert!(function.package_id == package_id, 0); | ||
assert!(function.module_name == module_name, 1); | ||
assert!(function.name == name, 2); | ||
} | ||
|
||
#[test] | ||
fun test_new_transaction_from_bcs() { | ||
let package_id = @0x5f7809eb09754577387a816582ece609511d0262b2c52aa15306083ca3c85962; | ||
let module_name = std::ascii::string(b"module"); | ||
let name = std::ascii::string(b"function"); | ||
let arguments = vector[x"1234", x"5678"]; | ||
let type_arguments = vector[ascii::string(b"type1"), ascii::string(b"type2")]; | ||
let input = x"5f7809eb09754577387a816582ece609511d0262b2c52aa15306083ca3c85962066d6f64756c650866756e6374696f6e0202123402567802057479706531057479706532"; | ||
|
||
let transaction = new_move_call_from_bcs(&mut bcs::new(input)); | ||
assert!(transaction.function.package_id == package_id, 0); | ||
assert!(transaction.function.module_name == module_name, 1); | ||
assert!(transaction.function.name == name, 2); | ||
assert!(transaction.arguments == arguments, 3); | ||
assert!(transaction.type_arguments == type_arguments, 4); | ||
} | ||
|
||
#[test] | ||
fun test_register_and_get() { | ||
let ctx = &mut sui::tx_context::dummy(); | ||
let mut self = new(ctx); | ||
let channel = axelar_gateway::channel::new(ctx); | ||
|
||
let move_call = MoveCall { | ||
function: Function { | ||
package_id: @0x1234, | ||
module_name: std::ascii::string(b"module"), | ||
name: std::ascii::string(b"function"), | ||
}, | ||
arguments: vector::empty<vector<u8>>(), | ||
type_arguments: vector::empty<ascii::String>(), | ||
}; | ||
let input_transaction = Transaction { | ||
is_final: true, | ||
move_calls: vector[move_call], | ||
}; | ||
|
||
self.register_transaction(&channel, input_transaction); | ||
|
||
let transaction = self.get_transaction(channel.id()); | ||
assert!(transaction == input_transaction, 0); | ||
|
||
sui::test_utils::destroy(self); | ||
sui::test_utils::destroy(channel); | ||
} | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
module axelar_gateway::bytes32 { | ||
use sui::bcs::BCS; | ||
|
||
/// ----- | ||
/// Types | ||
/// ----- | ||
public struct Bytes32 has copy, drop, store { | ||
bytes: vector<u8>, | ||
} | ||
|
||
/// --------- | ||
/// Constants | ||
/// --------- | ||
const LEN: u64 = 32; | ||
|
||
/// ------ | ||
/// Errors | ||
/// ------ | ||
/// Invalid length for bytes32 cast | ||
const EInvalidLength: u64 = 0; | ||
|
||
/// ---------------- | ||
/// Public Functions | ||
/// ---------------- | ||
/// Casts a vector of bytes to a bytes32 | ||
public fun new(bytes: vector<u8>): Bytes32 { | ||
assert!(bytes.length() == LEN, EInvalidLength); | ||
|
||
Bytes32{bytes: bytes} | ||
} | ||
|
||
public fun default(): Bytes32 { | ||
let mut bytes: vector<u8> = vector[]; | ||
let mut i: u64 = 0; | ||
|
||
while (i < LEN) { | ||
vector::push_back(&mut bytes, 0); | ||
i = i + 1; | ||
}; | ||
|
||
Bytes32{bytes: bytes} | ||
} | ||
|
||
public fun from_bytes(bytes: vector<u8>): Bytes32 { | ||
new(bytes) | ||
} | ||
|
||
public fun to_bytes(self: Bytes32): vector<u8> { | ||
self.bytes | ||
} | ||
|
||
public fun validate(self: &Bytes32) { | ||
assert!(self.bytes.length() == 32, EInvalidLength); | ||
} | ||
|
||
public fun length(_self: &Bytes32): u64 { | ||
LEN | ||
} | ||
|
||
public(package) fun peel(bcs: &mut BCS): Bytes32 { | ||
let bytes = bcs.peel_vec_u8(); | ||
new(bytes) | ||
} | ||
} | ||
|
||
#[test_only] | ||
module axelar_gateway::bytes32_tests { | ||
use axelar_gateway::bytes32::{Self}; | ||
|
||
#[test] | ||
public fun new() { | ||
let bytes = | ||
x"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; | ||
let actual = bytes32::new(bytes); | ||
|
||
assert!(actual.to_bytes() == bytes, 0); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
module axelar_gateway::message { | ||
use std::ascii::String; | ||
use sui::bcs::BCS; | ||
use sui::address; | ||
|
||
use axelar_gateway::bytes32::{Self, Bytes32}; | ||
|
||
/// ----- | ||
/// Types | ||
/// ----- | ||
/// Cross chain message type | ||
public struct Message has copy, drop, store { | ||
source_chain: String, | ||
message_id: String, | ||
source_address: String, | ||
destination_id: address, | ||
payload_hash: Bytes32, | ||
} | ||
|
||
/// ------ | ||
/// Errors | ||
/// ------ | ||
/// Invalid bytes length | ||
const EInvalidLength: u64 = 0; | ||
|
||
/// ----------------- | ||
/// Package Functions | ||
/// ----------------- | ||
public(package) fun peel(bcs: &mut BCS): Message { | ||
// TODO: allow UTF-8 strings? Or keep it as more generic bytes? | ||
let source_chain = bcs.peel_vec_u8().to_ascii_string(); | ||
let message_id = bcs.peel_vec_u8().to_ascii_string(); | ||
let source_address = bcs.peel_vec_u8().to_ascii_string(); | ||
|
||
let destination_id_bytes = bcs.peel_vec_u8(); | ||
assert!(destination_id_bytes.length() == 32, EInvalidLength); | ||
|
||
let payload_hash = bytes32::peel(bcs); | ||
|
||
Message { | ||
source_chain, | ||
message_id, | ||
source_address, | ||
destination_id: address::from_bytes(destination_id_bytes), | ||
payload_hash, | ||
} | ||
} | ||
|
||
public(package) fun id(self: &Message): vector<u8> { | ||
let mut id = self.source_chain.into_bytes(); | ||
id.append(b"_"); | ||
id.append(self.message_id.into_bytes()); | ||
|
||
id | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
module axelar_gateway::weighted_signer { | ||
use sui::bcs::BCS; | ||
|
||
/// ----- | ||
/// Types | ||
/// ----- | ||
public struct WeightedSigner has copy, drop, store { | ||
signer: vector<u8>, | ||
weight: u128, | ||
} | ||
|
||
/// ----------------- | ||
/// Package Functions | ||
/// ----------------- | ||
public(package) fun peel(bcs: &mut BCS): WeightedSigner { | ||
let signer = bcs.peel_vec_u8(); | ||
let weight = bcs.peel_u128(); | ||
|
||
WeightedSigner { signer, weight } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
module axelar_gateway::weighted_signers { | ||
use sui::bcs::BCS; | ||
|
||
use axelar_gateway::bytes32::{Self, Bytes32}; | ||
use axelar_gateway::weighted_signer::{Self}; | ||
|
||
public struct WeightedSigners has copy, drop, store { | ||
signers: vector<weighted_signer::WeightedSigner>, | ||
threshold: u128, | ||
nonce: Bytes32, | ||
} | ||
|
||
/// ------ | ||
/// Errors | ||
/// ------ | ||
/// Invalid length of the bytes | ||
const EInvalidLength: u64 = 0; | ||
|
||
/// ----------------- | ||
/// Package Functions | ||
/// ----------------- | ||
public(package) fun from_bcs(bcs: &mut BCS): WeightedSigners { | ||
let mut signers = vector::empty<weighted_signer::WeightedSigner>(); | ||
|
||
let mut length = bcs.peel_vec_length(); | ||
assert!(length > 0, EInvalidLength); | ||
|
||
while (length > 0) { | ||
signers.push_back(weighted_signer::peel(bcs)); | ||
|
||
length = length - 1; | ||
}; | ||
|
||
let threshold = bcs.peel_u128(); | ||
let nonce = bytes32::peel(bcs); | ||
|
||
WeightedSigners { | ||
signers, | ||
threshold, | ||
nonce, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
module axelar_gateway::utils { | ||
use sui::bcs; | ||
use sui::hash; | ||
|
||
const EInvalidSignatureLength: u64 = 0; | ||
const EVectorLengthMismatch: u64 = 1; | ||
|
||
/// Prefix for Sui Messages. | ||
const PREFIX: vector<u8> = b"\x19Sui Signed Message:\n"; | ||
|
||
/// Normalize last byte of the signature. Have it 1 or 0. | ||
/// See https://tech.mystenlabs.com/cryptography-in-sui-cross-chain-signature-verification/ | ||
public fun normalize_signature(signature: &mut vector<u8>) { | ||
// Compute v = 0 or 1. | ||
assert!(vector::length<u8>(signature) == 65, EInvalidSignatureLength); | ||
let v = vector::borrow_mut(signature, 64); | ||
if (*v == 27) { | ||
*v = 0; | ||
} else if (*v == 28) { | ||
*v = 1; | ||
} else if (*v > 35) { | ||
*v = (*v - 1) % 2; | ||
}; | ||
} | ||
|
||
/// Add a prefix to the bytes. | ||
public fun to_sui_signed(bytes: vector<u8>): vector<u8> { | ||
let mut res = vector[]; | ||
vector::append(&mut res, PREFIX); | ||
vector::append(&mut res, bytes); | ||
res | ||
} | ||
|
||
/// Compute operators hash from the list of `operators` (public keys). | ||
/// This hash is used in `Axelar.epoch_by_signer_hash`. | ||
public fun operators_hash(operators: &vector<vector<u8>>, weights: &vector<u128>, threshold: u128): vector<u8> { | ||
let mut data = bcs::to_bytes(operators); | ||
vector::append(&mut data, bcs::to_bytes(weights)); | ||
vector::append(&mut data, bcs::to_bytes(&threshold)); | ||
hash::keccak256(&data) | ||
} | ||
|
||
public fun is_address_vector_zero(v: &vector<u8>): bool { | ||
let length = vector::length(v); | ||
let mut i = 0; | ||
while(i < length) { | ||
if(*vector::borrow(v, i) != 0) return false; | ||
i = i + 1; | ||
}; | ||
true | ||
} | ||
|
||
public fun compare_address_vectors(v1: &vector<u8>, v2: &vector<u8>): bool { | ||
let length = vector::length(v1); | ||
assert!(length == vector::length(v2), EVectorLengthMismatch); | ||
let mut i = 0; | ||
while(i < length) { | ||
let b1 = *vector::borrow(v1, i); | ||
let b2 = *vector::borrow(v2, i); | ||
if(b1 < b2) { | ||
return true | ||
} else if(b1 > b2) { | ||
return false | ||
}; | ||
i = i + 1; | ||
}; | ||
false | ||
} | ||
|
||
#[test] | ||
#[expected_failure(abort_code = EInvalidSignatureLength)] | ||
fun test_normalize_signature() { | ||
let prefix = x"5f7809eb09754577387a816582ece609511d0262b2c52aa15306083ca3c85962066d6f64756c650866756e6374696f6e02021234025678020574797065310574"; | ||
let mut signature = x"5f7809eb09754577387a816582ece609511d0262b2c52aa15306083ca3c85962066d6f64756c650866756e6374696f6e02021234025678020574797065310574"; | ||
let inputs = vector[0, 1, 10, 11, 27, 28, 30, 38, 39]; | ||
let outputs = vector[0, 1, 10, 11, 0, 1, 30, 1, 0]; | ||
|
||
let length = vector::length(&inputs); | ||
let mut i = 0; | ||
while(i < length) { | ||
vector::push_back(&mut signature, *vector::borrow(&inputs, i)); | ||
normalize_signature(&mut signature); | ||
assert!(vector::pop_back(&mut signature) == *vector::borrow(&outputs, i), i); | ||
assert!(signature == prefix, i); | ||
i = i + 1; | ||
}; | ||
|
||
normalize_signature(&mut signature); | ||
} | ||
|
||
#[test] | ||
fun test_to_sui_signed() { | ||
let input = b"012345"; | ||
let output = b"\x19Sui Signed Message:\n012345"; | ||
assert!(to_sui_signed(input) == output, 0); | ||
} | ||
|
||
#[test] | ||
fun test_operators_hash() { | ||
let operators = vector[x"0123", x"4567", x"890a"]; | ||
let weights = vector[1, 3, 6]; | ||
let threshold = 4; | ||
let output = x"dd5d3f9c1017e8356ea1858db7b89800b6cd43775c5c1b7c633f6ef933583cfd"; | ||
|
||
assert!(operators_hash(&operators, &weights, threshold) == output, 0); | ||
} | ||
|
||
#[test] | ||
fun test_is_address_vector_zero() { | ||
assert!(is_address_vector_zero(&x"01") == false, 0); | ||
assert!(is_address_vector_zero(&x"") == true, 0); | ||
assert!(is_address_vector_zero(&x"00") == true, 0); | ||
assert!(is_address_vector_zero(&x"00000000000001") == false, 0); | ||
assert!(is_address_vector_zero(&x"00000000000000") == true, 0); | ||
} | ||
|
||
#[test] | ||
#[expected_failure(abort_code = EVectorLengthMismatch)] | ||
fun test_compare_address_vectors() { | ||
let v1 = &x"000000"; | ||
let v2 = &x"000001"; | ||
let v2copy = &x"000001"; | ||
let v3 = &x"010000"; | ||
|
||
assert!(compare_address_vectors(v1, v2) == true, 0); | ||
assert!(compare_address_vectors(v1, v3) == true, 1); | ||
assert!(compare_address_vectors(v2, v3) == true, 2); | ||
|
||
assert!(compare_address_vectors(v2, v1) == false, 3); | ||
assert!(compare_address_vectors(v3, v2) == false, 4); | ||
assert!(compare_address_vectors(v3, v1) == false, 5); | ||
|
||
assert!(compare_address_vectors(v2, v2copy) == false, 6); | ||
|
||
compare_address_vectors(&x"", &x"01"); | ||
} | ||
} |