diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 00000000..eceb8771 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,3 @@ +## Expected post-Upgrade Behaviour + +Nothing should work on the original package, and everything should work on the newer package. diff --git a/move/axelar_gateway/sources/gateway.move b/move/axelar_gateway/sources/gateway.move index 008ffcfd..a731fe4c 100644 --- a/move/axelar_gateway/sources/gateway.move +++ b/move/axelar_gateway/sources/gateway.move @@ -33,13 +33,14 @@ /// - CallApproval is checked to match the `Channel`.id /// /// The Gateway object uses a versioned field to support upgradability. The -/// current implementation uses Gateway_v0. +/// current implementation uses Gateway_v1. module axelar_gateway::gateway; use axelar_gateway::auth::{Self, validate_proof}; use axelar_gateway::bytes32::{Self, Bytes32}; use axelar_gateway::channel::{Channel, ApprovedMessage}; -use axelar_gateway::gateway_v0::{Self, Gateway_v0}; +use axelar_gateway::gateway_v1::{Self, Gateway_v1}; +use axelar_gateway::gateway_v0::{Gateway_v0}; use axelar_gateway::message_ticket::{Self, MessageTicket}; use axelar_gateway::owner_cap::{Self, OwnerCap}; use axelar_gateway::weighted_signers; @@ -53,7 +54,7 @@ use version_control::version_control::{Self, VersionControl}; // ------- // Version // ------- -const VERSION: u64 = 0; +const VERSION: u64 = 2; // ------- // Structs @@ -88,7 +89,7 @@ entry fun setup( ) { let inner = versioned::create( VERSION, - gateway_v0::new( + gateway_v1::new( operator, table::new(ctx), auth::setup( @@ -103,6 +104,7 @@ entry fun setup( ctx, ), version_control(), + 0, ), ctx, ); @@ -118,9 +120,9 @@ entry fun setup( // Macros // ------ /// This macro also uses version control to sinplify things a bit. -macro fun value($self: &Gateway, $function_name: vector): &Gateway_v0 { +macro fun value($self: &Gateway, $function_name: vector): &Gateway_v1 { let gateway = $self; - let value = gateway.inner.load_value(); + let value = gateway.inner.load_value(); value.version_control().check(VERSION, ascii::string($function_name)); value } @@ -129,9 +131,9 @@ macro fun value($self: &Gateway, $function_name: vector): &Gateway_v0 { macro fun value_mut( $self: &mut Gateway, $function_name: vector, -): &mut Gateway_v0 { +): &mut Gateway_v1 { let gateway = $self; - let value = gateway.inner.load_value_mut(); + let value = gateway.inner.load_value_mut(); value.version_control().check(VERSION, ascii::string($function_name)); value } @@ -174,6 +176,19 @@ entry fun rotate_signers( ) } +/// This function should only be called once +/// (checks should be made on versioned to ensure this) +/// It upgrades the version control to the new version control. +entry fun migrate(self: &mut Gateway, _: &OwnerCap, data: vector) { + let (v0, cap) = self.inner.remove_value_for_upgrade(); + let v1 = v0.migrate(version_control(), data); + self.inner.upgrade( + VERSION, + v1, + cap + ); +} + entry fun allow_function( self: &mut Gateway, _: &OwnerCap, @@ -194,6 +209,14 @@ entry fun disallow_function( .disallow_function(version, function_name); } +entry fun set_new_field(self: &mut Gateway, new_field: u64) { + self.value_mut!(b"set_new_field").set_new_field(new_field); +} + +entry fun new_field(self: &Gateway): u64 { + self.value!(b"new_field").new_field() +} + // ---------------- // Public Functions // ---------------- @@ -276,6 +299,8 @@ public fun take_approved_message( fun version_control(): VersionControl { version_control::new(vector[ + vector[], + vector[], vector[ b"approve_messages", b"rotate_signers", @@ -285,6 +310,8 @@ fun version_control(): VersionControl { b"send_message", b"allow_function", b"disallow_function", + b"set_new_field", + b"new_field", ].map!(|function_name| function_name.to_ascii_string()), ]) } @@ -311,7 +338,7 @@ public fun create_for_testing( ): Gateway { let inner = versioned::create( VERSION, - gateway_v0::new( + gateway_v1::new( operator, table::new(ctx), auth::setup( @@ -323,6 +350,7 @@ public fun create_for_testing( ctx, ), version_control(), + 0, ), ctx, ); @@ -337,11 +365,13 @@ fun dummy(ctx: &mut TxContext): Gateway { let mut rng = sui::random::new_generator_for_testing(); let inner = versioned::create( VERSION, - gateway_v0::new( + gateway_v1::new( sui::address::from_bytes(rng.generate_bytes(32)), table::new(ctx), auth::dummy(ctx), version_control::new(vector[ + vector[], + vector[], vector[ b"approve_messages", b"rotate_signers", @@ -354,6 +384,7 @@ fun dummy(ctx: &mut TxContext): Gateway { b"", ].map!(|function_name| function_name.to_ascii_string()), ]), + 0, ), ctx, ); @@ -371,7 +402,7 @@ public fun destroy_for_testing(self: Gateway) { } = self; id.delete(); - let value = inner.destroy(); + let value = inner.destroy(); let (_, messages, signers, _) = value.destroy_for_testing(); let (_, table, _, _, _, _) = signers.destroy_for_testing(); @@ -436,7 +467,7 @@ fun test_setup() { id.delete(); let (operator_result, messages, signers, _) = inner - .destroy() + .destroy() .destroy_for_testing(); assert!(operator == operator_result); @@ -510,7 +541,7 @@ fun test_setup_remaining_bytes() { id.delete(); let (operator_result, messages, signers, _) = inner - .destroy() + .destroy() .destroy_for_testing(); assert!(operator == operator_result); @@ -667,7 +698,7 @@ fun test_approve_messages() { let messages = vector[ axelar_gateway::message::dummy(), ]; - let data_hash = gateway_v0::approve_messages_data_hash(messages); + let data_hash = gateway_v1::approve_messages_data_hash(messages); let proof = generate_proof( data_hash, domain_separator, @@ -717,7 +748,7 @@ fun test_approve_messages_remaining_data() { ctx, ); let messages = vector[axelar_gateway::message::dummy()]; - let data_hash = gateway_v0::approve_messages_data_hash(messages); + let data_hash = gateway_v1::approve_messages_data_hash(messages); let proof = generate_proof( data_hash, domain_separator, @@ -781,7 +812,7 @@ fun test_rotate_signers() { utils::assert_event(); - let data_hash = gateway_v0::rotate_signers_data_hash(next_weighted_signers); + let data_hash = gateway_v1::rotate_signers_data_hash(next_weighted_signers); let proof = generate_proof( data_hash, domain_separator, @@ -853,7 +884,7 @@ fun test_rotate_signers_remaining_data_message_data() { let mut message_data = bcs::to_bytes(&next_weighted_signers); message_data.push_back(0); - let data_hash = gateway_v0::rotate_signers_data_hash(next_weighted_signers); + let data_hash = gateway_v1::rotate_signers_data_hash(next_weighted_signers); let proof = generate_proof( data_hash, domain_separator, @@ -915,7 +946,7 @@ fun test_rotate_signers_remaining_data_proof_data() { ctx, ); - let data_hash = gateway_v0::rotate_signers_data_hash(next_weighted_signers); + let data_hash = gateway_v1::rotate_signers_data_hash(next_weighted_signers); let proof = generate_proof( data_hash, domain_separator, @@ -938,7 +969,7 @@ fun test_rotate_signers_remaining_data_proof_data() { } #[test] -#[expected_failure(abort_code = axelar_gateway::gateway_v0::ENotLatestSigners)] +#[expected_failure(abort_code = axelar_gateway::gateway_v1::ENotLatestSigners)] fun test_rotate_signers_not_latest_signers() { let mut rng = sui::random::new_generator_for_testing(); let ctx = &mut sui::tx_context::dummy(); @@ -987,7 +1018,7 @@ fun test_rotate_signers_not_latest_signers() { let epoch = self.value_mut!(b"rotate_signers").signers_mut().epoch_mut(); *epoch = *epoch + 1; - let data_hash = gateway_v0::rotate_signers_data_hash(next_weighted_signers); + let data_hash = gateway_v1::rotate_signers_data_hash(next_weighted_signers); let proof = generate_proof( data_hash, domain_separator, @@ -1075,7 +1106,7 @@ fun test_allow_function() { let ctx = &mut sui::tx_context::dummy(); let mut self = dummy(ctx); let owner_cap = owner_cap::create(ctx); - let version = 0; + let version = VERSION; let function_name = b"function_name".to_ascii_string(); self.allow_function(&owner_cap, version, function_name); @@ -1089,7 +1120,7 @@ fun test_disallow_function() { let ctx = &mut sui::tx_context::dummy(); let mut self = dummy(ctx); let owner_cap = owner_cap::create(ctx); - let version = 0; + let version = VERSION; let function_name = b"approve_messages".to_ascii_string(); self.disallow_function(&owner_cap, version, function_name); diff --git a/move/axelar_gateway/sources/versioned/gateway_v0.move b/move/axelar_gateway/sources/versioned/gateway_v0.move index 813e6f41..a60f9a2a 100644 --- a/move/axelar_gateway/sources/versioned/gateway_v0.move +++ b/move/axelar_gateway/sources/versioned/gateway_v0.move @@ -1,38 +1,12 @@ module axelar_gateway::gateway_v0; use axelar_gateway::auth::AxelarSigners; -use axelar_gateway::bytes32::{Self, Bytes32}; -use axelar_gateway::channel::{Self, ApprovedMessage}; -use axelar_gateway::events; -use axelar_gateway::message::{Self, Message}; -use axelar_gateway::message_status::{Self, MessageStatus}; -use axelar_gateway::message_ticket::MessageTicket; -use axelar_gateway::proof; -use axelar_gateway::weighted_signers; -use std::ascii::String; -use sui::address; -use sui::clock::Clock; -use sui::hash; -use sui::table::{Self, Table}; -use utils::utils; +use axelar_gateway::bytes32::Bytes32; +use axelar_gateway::message_status::MessageStatus; +use sui::table::Table; use version_control::version_control::VersionControl; - -// ------ -// Errors -// ------ -#[error] -const EMessageNotApproved: vector = - b"trying to `take_approved_message` for a message that is not approved"; - -#[error] -const EZeroMessages: vector = b"no messages found"; - -#[error] -const ENotLatestSigners: vector = b"not latest signers"; - -#[error] -const ENewerMessage: vector = - b"message ticket created from newer versions cannot be sent here"; +use axelar_gateway::gateway_v1::{Self, Gateway_v1}; +use utils::utils; // ----- // Types @@ -55,513 +29,19 @@ public enum CommandType { // ----------------- // Package Functions // ----------------- -/// Init the module by giving a OwnerCap to the sender to allow a full -/// `setup`. -public(package) fun new( - operator: address, - messages: Table, - signers: AxelarSigners, - version_control: VersionControl, -): Gateway_v0 { - Gateway_v0 { +public(package) fun migrate(self: Gateway_v0, version_control: VersionControl, data: vector): Gateway_v1 { + let Gateway_v0 { operator, messages, signers, - version_control, - } -} - -public(package) fun version_control(self: &Gateway_v0): &VersionControl { - &self.version_control -} - -public(package) fun approve_messages( - self: &mut Gateway_v0, - message_data: vector, - proof_data: vector, -) { - let proof = utils::peel!(proof_data, |bcs| proof::peel(bcs)); - let messages = peel_messages(message_data); - - let _ = self - .signers - .validate_proof( - data_hash(CommandType::ApproveMessages, message_data), - proof, - ); - - messages.do!(|message| self.approve_message(message)); -} - -public(package) fun rotate_signers( - self: &mut Gateway_v0, - clock: &Clock, - new_signers_data: vector, - proof_data: vector, - ctx: &TxContext, -) { - let weighted_signers = utils::peel!( - new_signers_data, - |bcs| weighted_signers::peel(bcs), - ); - let proof = utils::peel!(proof_data, |bcs| proof::peel(bcs)); - - let enforce_rotation_delay = ctx.sender() != self.operator; - - let is_latest_signers = self - .signers - .validate_proof( - data_hash(CommandType::RotateSigners, new_signers_data), - proof, - ); - assert!(!enforce_rotation_delay || is_latest_signers, ENotLatestSigners); - - // This will fail if signers are duplicated - self - .signers - .rotate_signers(clock, weighted_signers, enforce_rotation_delay); -} - -public(package) fun is_message_approved( - self: &Gateway_v0, - source_chain: String, - message_id: String, - source_address: String, - destination_id: address, - payload_hash: Bytes32, -): bool { - let message = message::new( - source_chain, - message_id, - source_address, - destination_id, - payload_hash, - ); - let command_id = message.command_id(); - - self[command_id] == message_status::approved(message.hash()) -} - -public(package) fun is_message_executed( - self: &Gateway_v0, - source_chain: String, - message_id: String, -): bool { - let command_id = message::message_to_command_id( - source_chain, - message_id, - ); - - self[command_id] == message_status::executed() -} - -/// To execute a message, the relayer will call `take_approved_message` -/// to get the hot potato `ApprovedMessage` object, and then trigger the app's -/// package via discovery. -public(package) fun take_approved_message( - self: &mut Gateway_v0, - source_chain: String, - message_id: String, - source_address: String, - destination_id: address, - payload: vector, -): ApprovedMessage { - let command_id = message::message_to_command_id(source_chain, message_id); - - let message = message::new( - source_chain, - message_id, - source_address, - destination_id, - bytes32::from_bytes(hash::keccak256(&payload)), - ); - - assert!( - self[command_id] == message_status::approved(message.hash()), - EMessageNotApproved, - ); - - let message_status_ref = &mut self[command_id]; - *message_status_ref = message_status::executed(); - - events::message_executed( - message, - ); - - channel::create_approved_message( - source_chain, - message_id, - source_address, - destination_id, - payload, - ) -} - -public(package) fun send_message( - _self: &Gateway_v0, - message: MessageTicket, - current_version: u64, -) { - let ( - source_id, - destination_chain, - destination_address, - payload, - version, - ) = message.destroy(); - - assert!(version <= current_version, ENewerMessage); - - events::contract_call( - source_id, - destination_chain, - destination_address, - payload, - address::from_bytes(hash::keccak256(&payload)), - ); -} - -public(package) fun allow_function( - self: &mut Gateway_v0, - version: u64, - function_name: String, -) { - self.version_control.allow_function(version, function_name); -} - -public(package) fun disallow_function( - self: &mut Gateway_v0, - version: u64, - function_name: String, -) { - self.version_control.disallow_function(version, function_name); -} - -// ----------------- -// Private Functions -// ----------------- - -#[syntax(index)] -fun borrow(self: &Gateway_v0, command_id: Bytes32): &MessageStatus { - table::borrow(&self.messages, command_id) -} - -#[syntax(index)] -fun borrow_mut(self: &mut Gateway_v0, command_id: Bytes32): &mut MessageStatus { - table::borrow_mut(&mut self.messages, command_id) -} - -fun peel_messages(message_data: vector): vector { - utils::peel!(message_data, |bcs| { - let messages = vector::tabulate!( - bcs.peel_vec_length(), - |_| message::peel(bcs), - ); - assert!(messages.length() > 0, EZeroMessages); - messages - }) -} - -fun data_hash(command_type: CommandType, data: vector): Bytes32 { - let mut typed_data = vector::singleton(command_type.as_u8()); - typed_data.append(data); - - bytes32::from_bytes(hash::keccak256(&typed_data)) -} - -fun approve_message(self: &mut Gateway_v0, message: message::Message) { - let command_id = message.command_id(); - - // If the message was already approved, ignore it. - if (self.messages.contains(command_id)) { - return - }; - - self - .messages - .add( - command_id, - message_status::approved(message.hash()), - ); - - events::message_approved( - message, - ); -} - -fun as_u8(self: CommandType): u8 { - match (self) { - CommandType::ApproveMessages => 0, - CommandType::RotateSigners => 1, - } -} - -/// --------- -/// Test Only -/// --------- -#[test_only] -use axelar_gateway::weighted_signers::WeightedSigners; -#[test_only] -use sui::bcs; - -#[test_only] -public(package) fun messages_mut( - self: &mut Gateway_v0, -): &mut Table { - &mut self.messages -} - -#[test_only] -public(package) fun signers_mut(self: &mut Gateway_v0): &mut AxelarSigners { - &mut self.signers -} - -#[test_only] -public(package) fun destroy_for_testing( - self: Gateway_v0, -): (address, Table, AxelarSigners, VersionControl) { - let Gateway_v0 { + version_control: _, + } = self; + let new_field = utils::peel!(data, |bcs| bcs.peel_u64()); + gateway_v1::new( operator, messages, signers, version_control, - } = self; - (operator, messages, signers, version_control) -} - -#[test_only] -fun dummy(ctx: &mut TxContext): Gateway_v0 { - new( - @0x0, - sui::table::new(ctx), - axelar_gateway::auth::dummy(ctx), - version_control::version_control::new(vector[]), + new_field, ) } - -#[test_only] -public(package) fun approve_messages_data_hash( - messages: vector, -): Bytes32 { - data_hash(CommandType::ApproveMessages, bcs::to_bytes(&messages)) -} - -#[test_only] -public(package) fun rotate_signers_data_hash( - weighted_signers: WeightedSigners, -): Bytes32 { - data_hash(CommandType::RotateSigners, bcs::to_bytes(&weighted_signers)) -} - -#[test_only] -public(package) fun approve_message_for_testing( - self: &mut Gateway_v0, - message: Message, -) { - self.approve_message(message); -} - -/// ----- -/// Tests -/// ----- -#[test] -#[expected_failure(abort_code = EZeroMessages)] -fun test_peel_messages_no_zero_messages() { - peel_messages(sui::bcs::to_bytes(&vector[])); -} - -#[test] -fun test_approve_message() { - let mut rng = sui::random::new_generator_for_testing(); - let ctx = &mut sui::tx_context::dummy(); - - let message_id = std::ascii::string(b"Message Id"); - let channel = axelar_gateway::channel::new(ctx); - let source_chain = std::ascii::string(b"Source Chain"); - let source_address = std::ascii::string(b"Destination Address"); - let payload = rng.generate_bytes(32); - let payload_hash = axelar_gateway::bytes32::new( - sui::address::from_bytes(hash::keccak256(&payload)), - ); - - let message = message::new( - source_chain, - message_id, - source_address, - channel.to_address(), - payload_hash, - ); - - let mut data = dummy(ctx); - - data.approve_message(message); - // The second approve message should do nothing. - data.approve_message(message); - - assert!( - data.is_message_approved( - source_chain, - message_id, - source_address, - channel.to_address(), - payload_hash, - ) == - true, - EMessageNotApproved, - ); - - let approved_message = data.take_approved_message( - source_chain, - message_id, - source_address, - channel.to_address(), - payload, - ); - - channel.consume_approved_message(approved_message); - - assert!( - data.is_message_approved( - source_chain, - message_id, - source_address, - channel.to_address(), - payload_hash, - ) == - false, - EMessageNotApproved, - ); - - assert!( - data.is_message_executed( - source_chain, - message_id, - ) == - true, - EMessageNotApproved, - ); - - data.messages.remove(message.command_id()); - - sui::test_utils::destroy(data); - channel.destroy(); -} - -#[test] -fun test_peel_messages() { - let message1 = message::new( - std::ascii::string(b"Source Chain 1"), - std::ascii::string(b"Message Id 1"), - std::ascii::string(b"Source Address 1"), - @0x1, - axelar_gateway::bytes32::new(@0x2), - ); - - let message2 = message::new( - std::ascii::string(b"Source Chain 2"), - std::ascii::string(b"Message Id 2"), - std::ascii::string(b"Source Address 2"), - @0x3, - axelar_gateway::bytes32::new(@0x4), - ); - - let bytes = sui::bcs::to_bytes(&vector[message1, message2]); - - let messages = peel_messages(bytes); - - assert!(messages.length() == 2); - assert!(messages[0] == message1); - assert!(messages[1] == message2); -} - -#[test] -#[expected_failure] -fun test_peel_messages_no_remaining_data() { - let message1 = message::new( - std::ascii::string(b"Source Chain 1"), - std::ascii::string(b"Message Id 1"), - std::ascii::string(b"Source Address 1"), - @0x1, - axelar_gateway::bytes32::new(@0x2), - ); - - let mut bytes = sui::bcs::to_bytes(&vector[message1]); - bytes.push_back(0); - - peel_messages(bytes); -} - -#[test] -fun test_command_type_as_u8() { - // Note: These must not be changed to avoid breaking Amplifier integration - assert!(CommandType::ApproveMessages.as_u8() == 0); - assert!(CommandType::RotateSigners.as_u8() == 1); -} - -#[test] -fun test_data_hash() { - let mut rng = sui::random::new_generator_for_testing(); - let data = rng.generate_bytes(32); - let mut typed_data = vector::singleton(CommandType::ApproveMessages.as_u8()); - typed_data.append(data); - - assert!( - data_hash(CommandType::ApproveMessages, data) == - bytes32::from_bytes(hash::keccak256(&typed_data)), - EMessageNotApproved, - ); -} - -#[test] -#[expected_failure(abort_code = ENewerMessage)] -fun test_send_message_newer_message() { - let mut rng = sui::random::new_generator_for_testing(); - let source_id = address::from_u256(rng.generate_u256()); - let destination_chain = std::ascii::string(b"Destination Chain"); - let destination_address = std::ascii::string(b"Destination Address"); - let payload = rng.generate_bytes(32); - let version = 1; - let message = axelar_gateway::message_ticket::new( - source_id, - destination_chain, - destination_address, - payload, - version, - ); - let ctx = &mut sui::tx_context::dummy(); - let self = dummy(ctx); - self.send_message(message, 0); - sui::test_utils::destroy(self); -} - -#[test] -#[expected_failure(abort_code = EMessageNotApproved)] -fun test_take_approved_message_message_not_approved() { - let mut rng = sui::random::new_generator_for_testing(); - let destination_id = address::from_u256(rng.generate_u256()); - let source_chain = std::ascii::string(b"Source Chain"); - let source_address = std::ascii::string(b"Source Address"); - let message_id = std::ascii::string(b"Message Id"); - let payload = rng.generate_bytes(32); - let command_id = message::message_to_command_id(source_chain, message_id); - - let ctx = &mut sui::tx_context::dummy(); - let mut self = dummy(ctx); - - self - .messages - .add( - command_id, - message_status::executed(), - ); - - let approved_message = self.take_approved_message( - source_chain, - message_id, - source_address, - destination_id, - payload, - ); - sui::test_utils::destroy(self); - sui::test_utils::destroy(approved_message); -} diff --git a/move/axelar_gateway/sources/versioned/gateway_v1.move b/move/axelar_gateway/sources/versioned/gateway_v1.move new file mode 100644 index 00000000..abb87f53 --- /dev/null +++ b/move/axelar_gateway/sources/versioned/gateway_v1.move @@ -0,0 +1,580 @@ +module axelar_gateway::gateway_v1; + +use axelar_gateway::auth::AxelarSigners; +use axelar_gateway::bytes32::{Self, Bytes32}; +use axelar_gateway::channel::{Self, ApprovedMessage}; +use axelar_gateway::events; +use axelar_gateway::message::{Self, Message}; +use axelar_gateway::message_status::{Self, MessageStatus}; +use axelar_gateway::message_ticket::MessageTicket; +use axelar_gateway::proof; +use axelar_gateway::weighted_signers; +use std::ascii::String; +use sui::address; +use sui::clock::Clock; +use sui::hash; +use sui::table::{Self, Table}; +use utils::utils; +use version_control::version_control::VersionControl; + +// ------ +// Errors +// ------ +#[error] +const EMessageNotApproved: vector = + b"trying to `take_approved_message` for a message that is not approved"; + +#[error] +const EZeroMessages: vector = b"no messages found"; + +#[error] +const ENotLatestSigners: vector = b"not latest signers"; + +#[error] +const ENewerMessage: vector = + b"message ticket created from newer versions cannot be sent here"; + +// ----- +// Types +// ----- +/// An object holding the state of the Axelar bridge. +/// The central piece in managing call approval creation and signature +/// verification. +public struct Gateway_v1 has store { + operator: address, + messages: Table, + signers: AxelarSigners, + version_control: VersionControl, + new_field: u64, +} + +public enum CommandType { + ApproveMessages, + RotateSigners, +} + +// ----------------- +// Package Functions +// ----------------- +/// Init the module by giving a CreatorCap to the sender to allow a full +/// `setup`. +public(package) fun new( + operator: address, + messages: Table, + signers: AxelarSigners, + version_control: VersionControl, + new_field: u64, +): Gateway_v1 { + Gateway_v1 { + operator, + messages, + signers, + version_control, + new_field, + } +} + +public(package) fun version_control(self: &Gateway_v1): &VersionControl { + &self.version_control +} + +public(package) fun approve_messages( + self: &mut Gateway_v1, + message_data: vector, + proof_data: vector, +) { + let proof = utils::peel!(proof_data, |bcs| proof::peel(bcs)); + let messages = peel_messages(message_data); + + let _ = self + .signers + .validate_proof( + data_hash(CommandType::ApproveMessages, message_data), + proof, + ); + + messages.do!(|message| self.approve_message(message)); +} + +public(package) fun rotate_signers( + self: &mut Gateway_v1, + clock: &Clock, + new_signers_data: vector, + proof_data: vector, + ctx: &TxContext, +) { + let weighted_signers = utils::peel!( + new_signers_data, + |bcs| weighted_signers::peel(bcs), + ); + let proof = utils::peel!(proof_data, |bcs| proof::peel(bcs)); + + let enforce_rotation_delay = ctx.sender() != self.operator; + + let is_latest_signers = self + .signers + .validate_proof( + data_hash(CommandType::RotateSigners, new_signers_data), + proof, + ); + assert!(!enforce_rotation_delay || is_latest_signers, ENotLatestSigners); + + // This will fail if signers are duplicated + self + .signers + .rotate_signers(clock, weighted_signers, enforce_rotation_delay); +} + +public(package) fun is_message_approved( + self: &Gateway_v1, + source_chain: String, + message_id: String, + source_address: String, + destination_id: address, + payload_hash: Bytes32, +): bool { + let message = message::new( + source_chain, + message_id, + source_address, + destination_id, + payload_hash, + ); + let command_id = message.command_id(); + + self[command_id] == message_status::approved(message.hash()) +} + +public(package) fun is_message_executed( + self: &Gateway_v1, + source_chain: String, + message_id: String, +): bool { + let command_id = message::message_to_command_id( + source_chain, + message_id, + ); + + self[command_id] == message_status::executed() +} + +/// To execute a message, the relayer will call `take_approved_message` +/// to get the hot potato `ApprovedMessage` object, and then trigger the app's +/// package via discovery. +public(package) fun take_approved_message( + self: &mut Gateway_v1, + source_chain: String, + message_id: String, + source_address: String, + destination_id: address, + payload: vector, +): ApprovedMessage { + let command_id = message::message_to_command_id(source_chain, message_id); + + let message = message::new( + source_chain, + message_id, + source_address, + destination_id, + bytes32::from_bytes(hash::keccak256(&payload)), + ); + + assert!( + self[command_id] == message_status::approved(message.hash()), + EMessageNotApproved, + ); + + let message_status_ref = &mut self[command_id]; + *message_status_ref = message_status::executed(); + + events::message_executed( + message, + ); + + channel::create_approved_message( + source_chain, + message_id, + source_address, + destination_id, + payload, + ) +} + +public(package) fun send_message( + _self: &Gateway_v1, + message: MessageTicket, + current_version: u64, +) { + let ( + source_id, + destination_chain, + destination_address, + payload, + version, + ) = message.destroy(); + + assert!(version <= current_version, ENewerMessage); + + events::contract_call( + source_id, + destination_chain, + destination_address, + payload, + address::from_bytes(hash::keccak256(&payload)), + ); +} + +public(package) fun allow_function( + self: &mut Gateway_v1, + version: u64, + function_name: String, +) { + self.version_control.allow_function(version, function_name); +} + +public(package) fun disallow_function( + self: &mut Gateway_v1, + version: u64, + function_name: String, +) { + self.version_control.disallow_function(version, function_name); +} + +public(package) fun set_new_field(self: &mut Gateway_v1, new_field: u64) { + self.new_field = new_field; +} + +public(package) fun new_field(self: &Gateway_v1): u64 { + self.new_field +} + +// ----------------- +// Private Functions +// ----------------- + +#[syntax(index)] +fun borrow(self: &Gateway_v1, command_id: Bytes32): &MessageStatus { + table::borrow(&self.messages, command_id) +} + +#[syntax(index)] +fun borrow_mut(self: &mut Gateway_v1, command_id: Bytes32): &mut MessageStatus { + table::borrow_mut(&mut self.messages, command_id) +} + +fun peel_messages(message_data: vector): vector { + utils::peel!(message_data, |bcs| { + let messages = vector::tabulate!( + bcs.peel_vec_length(), + |_| message::peel(bcs), + ); + assert!(messages.length() > 0, EZeroMessages); + messages + }) +} + +fun data_hash(command_type: CommandType, data: vector): Bytes32 { + let mut typed_data = vector::singleton(command_type.as_u8()); + typed_data.append(data); + + bytes32::from_bytes(hash::keccak256(&typed_data)) +} + +fun approve_message(self: &mut Gateway_v1, message: message::Message) { + let command_id = message.command_id(); + + // If the message was already approved, ignore it. + if (self.messages.contains(command_id)) { + return + }; + + self + .messages + .add( + command_id, + message_status::approved(message.hash()), + ); + + events::message_approved( + message, + ); +} + +fun as_u8(self: CommandType): u8 { + match (self) { + CommandType::ApproveMessages => 0, + CommandType::RotateSigners => 1, + } +} + +/// --------- +/// Test Only +/// --------- +#[test_only] +use axelar_gateway::weighted_signers::WeightedSigners; +#[test_only] +use sui::bcs; + +#[test_only] +public(package) fun messages_mut( + self: &mut Gateway_v1, +): &mut Table { + &mut self.messages +} + +#[test_only] +public(package) fun signers_mut(self: &mut Gateway_v1): &mut AxelarSigners { + &mut self.signers +} + +#[test_only] +public(package) fun destroy_for_testing( + self: Gateway_v1, +): (address, Table, AxelarSigners, VersionControl) { + let Gateway_v1 { + operator, + messages, + signers, + version_control, + new_field: _, + } = self; + (operator, messages, signers, version_control) +} + +#[test_only] +fun dummy(ctx: &mut TxContext): Gateway_v1 { + new( + @0x0, + sui::table::new(ctx), + axelar_gateway::auth::dummy(ctx), + version_control::version_control::new(vector[]), + 0, + ) +} + +#[test_only] +public(package) fun approve_messages_data_hash( + messages: vector, +): Bytes32 { + data_hash(CommandType::ApproveMessages, bcs::to_bytes(&messages)) +} + +#[test_only] +public(package) fun rotate_signers_data_hash( + weighted_signers: WeightedSigners, +): Bytes32 { + data_hash(CommandType::RotateSigners, bcs::to_bytes(&weighted_signers)) +} + +#[test_only] +public(package) fun approve_message_for_testing( + self: &mut Gateway_v1, + message: Message, +) { + self.approve_message(message); +} + +/// ----- +/// Tests +/// ----- +#[test] +#[expected_failure(abort_code = EZeroMessages)] +fun test_peel_messages_no_zero_messages() { + peel_messages(sui::bcs::to_bytes(&vector[])); +} + +#[test] +fun test_approve_message() { + let mut rng = sui::random::new_generator_for_testing(); + let ctx = &mut sui::tx_context::dummy(); + + let message_id = std::ascii::string(b"Message Id"); + let channel = axelar_gateway::channel::new(ctx); + let source_chain = std::ascii::string(b"Source Chain"); + let source_address = std::ascii::string(b"Destination Address"); + let payload = rng.generate_bytes(32); + let payload_hash = axelar_gateway::bytes32::new( + sui::address::from_bytes(hash::keccak256(&payload)), + ); + + let message = message::new( + source_chain, + message_id, + source_address, + channel.to_address(), + payload_hash, + ); + + let mut data = dummy(ctx); + + data.approve_message(message); + // The second approve message should do nothing. + data.approve_message(message); + + assert!( + data.is_message_approved( + source_chain, + message_id, + source_address, + channel.to_address(), + payload_hash, + ) == + true, + EMessageNotApproved, + ); + + let approved_message = data.take_approved_message( + source_chain, + message_id, + source_address, + channel.to_address(), + payload, + ); + + channel.consume_approved_message(approved_message); + + assert!( + data.is_message_approved( + source_chain, + message_id, + source_address, + channel.to_address(), + payload_hash, + ) == + false, + EMessageNotApproved, + ); + + assert!( + data.is_message_executed( + source_chain, + message_id, + ) == + true, + EMessageNotApproved, + ); + + data.messages.remove(message.command_id()); + + sui::test_utils::destroy(data); + channel.destroy(); +} + +#[test] +fun test_peel_messages() { + let message1 = message::new( + std::ascii::string(b"Source Chain 1"), + std::ascii::string(b"Message Id 1"), + std::ascii::string(b"Source Address 1"), + @0x1, + axelar_gateway::bytes32::new(@0x2), + ); + + let message2 = message::new( + std::ascii::string(b"Source Chain 2"), + std::ascii::string(b"Message Id 2"), + std::ascii::string(b"Source Address 2"), + @0x3, + axelar_gateway::bytes32::new(@0x4), + ); + + let bytes = sui::bcs::to_bytes(&vector[message1, message2]); + + let messages = peel_messages(bytes); + + assert!(messages.length() == 2); + assert!(messages[0] == message1); + assert!(messages[1] == message2); +} + +#[test] +#[expected_failure] +fun test_peel_messages_no_remaining_data() { + let message1 = message::new( + std::ascii::string(b"Source Chain 1"), + std::ascii::string(b"Message Id 1"), + std::ascii::string(b"Source Address 1"), + @0x1, + axelar_gateway::bytes32::new(@0x2), + ); + + let mut bytes = sui::bcs::to_bytes(&vector[message1]); + bytes.push_back(0); + + peel_messages(bytes); +} + +#[test] +fun test_command_type_as_u8() { + // Note: These must not be changed to avoid breaking Amplifier integration + assert!(CommandType::ApproveMessages.as_u8() == 0); + assert!(CommandType::RotateSigners.as_u8() == 1); +} + +#[test] +fun test_data_hash() { + let mut rng = sui::random::new_generator_for_testing(); + let data = rng.generate_bytes(32); + let mut typed_data = vector::singleton(CommandType::ApproveMessages.as_u8()); + typed_data.append(data); + + assert!( + data_hash(CommandType::ApproveMessages, data) == + bytes32::from_bytes(hash::keccak256(&typed_data)), + EMessageNotApproved, + ); +} + +#[test] +#[expected_failure(abort_code = ENewerMessage)] +fun test_send_message_newer_message() { + let mut rng = sui::random::new_generator_for_testing(); + let source_id = address::from_u256(rng.generate_u256()); + let destination_chain = std::ascii::string(b"Destination Chain"); + let destination_address = std::ascii::string(b"Destination Address"); + let payload = rng.generate_bytes(32); + let version = 1; + let message = axelar_gateway::message_ticket::new( + source_id, + destination_chain, + destination_address, + payload, + version, + ); + let ctx = &mut sui::tx_context::dummy(); + let self = dummy(ctx); + self.send_message(message, 0); + sui::test_utils::destroy(self); +} + +#[test] +#[expected_failure(abort_code = EMessageNotApproved)] +fun test_take_approved_message_message_not_approved() { + let mut rng = sui::random::new_generator_for_testing(); + let destination_id = address::from_u256(rng.generate_u256()); + let source_chain = std::ascii::string(b"Source Chain"); + let source_address = std::ascii::string(b"Source Address"); + let message_id = std::ascii::string(b"Message Id"); + let payload = rng.generate_bytes(32); + let command_id = message::message_to_command_id(source_chain, message_id); + + let ctx = &mut sui::tx_context::dummy(); + let mut self = dummy(ctx); + + self + .messages + .add( + command_id, + message_status::executed(), + ); + + let approved_message = self.take_approved_message( + source_chain, + message_id, + source_address, + destination_id, + payload, + ); + sui::test_utils::destroy(self); + sui::test_utils::destroy(approved_message); +} diff --git a/package.json b/package.json index c1ece6ae..3537c174 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,12 @@ "move", "version.json", "tsconfig.json", - "!move/**/build" + "!move/**/build", + "src", + "scripts", + "tsconfig.*.json", + "tsconfig.json", + "UPGRADE.md" ], "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -52,7 +57,8 @@ "verify-web-build": "node scripts/verify-web-build.js", "lint": "eslint --fix './src/**/*.ts' './test/*.js'", "prettier": "prettier --write './src/**/*.ts' './test/*.js'", - "docs": "./scripts/docs.sh" + "docs": "./scripts/docs.sh", + "postinstall": "npm run build" }, "keywords": [ "axelar", diff --git a/test/axelar-gateway.js b/test/axelar-gateway.js index 645be93a..1b76ddb8 100644 --- a/test/axelar-gateway.js +++ b/test/axelar-gateway.js @@ -15,7 +15,26 @@ const { expect } = require('chai'); const COMMAND_TYPE_ROTATE_SIGNERS = 1; -describe('Axelar Gateway', () => { +const EXPECTED_FAILING_TESTS = (process.env.FAILING_TESTS || '').split(',').filter(Boolean); + +function controlledTest(testName, testFn) { + if (EXPECTED_FAILING_TESTS.includes(testName)) { + it(testName, async () => { + try { + await testFn(); + throw new Error(`Test "${testName}" was expected to fail but passed`); + } catch (error) { + if (error.message === `Test "${testName}" was expected to fail but passed`) { + throw error; + } + } + }); + } else { + it(testName, testFn); + } +} + +describe.only('Axelar Gateway', () => { let client; const operator = Ed25519Keypair.fromSecretKey(arrayify(getRandomBytes32())); const deployer = Ed25519Keypair.fromSecretKey(arrayify(getRandomBytes32())); @@ -115,7 +134,7 @@ describe('Axelar Gateway', () => { }); describe('Signer Rotation', () => { - it('should rotate signers', async () => { + controlledTest('should rotate signers', async () => { await sleep(2000); const proofSigners = gatewayInfo.signers; const proofKeys = gatewayInfo.signerKeys; @@ -147,7 +166,7 @@ describe('Axelar Gateway', () => { await builder.signAndExecute(keypair); }); - it('Should not rotate to empty signers', async () => { + controlledTest('Should not rotate to empty signers', async () => { await sleep(2000); const proofSigners = gatewayInfo.signers; const proofKeys = gatewayInfo.signerKeys; @@ -206,7 +225,7 @@ describe('Axelar Gateway', () => { channel = response.objectChanges.find((change) => change.objectType === `${packageId}::channel::Channel`).objectId; }); - it('should send a message', async () => { + controlledTest('should send a message', async () => { const destinationChain = 'Destination Chain'; const destinationAddress = 'Destination Address'; const payload = '0x1234'; @@ -236,7 +255,7 @@ describe('Axelar Gateway', () => { }); }); - it('should approve a message', async () => { + controlledTest('should approve a message', async () => { const message = { source_chain: 'Ethereum', message_id: 'Message Id', @@ -270,7 +289,7 @@ describe('Axelar Gateway', () => { expect(bcs.Bool.parse(new Uint8Array(resp.results[2].returnValues[0][0]))).to.equal(false); }); - it('should execute a message', async () => { + controlledTest('should execute a message', async () => { await publishPackage(client, keypair, 'gas_service'); await publishPackage(client, keypair, 'abi'); await publishPackage(client, keypair, 'governance');