From 9d3666204a6aa8c1f1f2c269844ec85bf2beb047 Mon Sep 17 00:00:00 2001 From: npty <78221556+npty@users.noreply.github.com> Date: Fri, 18 Oct 2024 18:37:27 +0700 Subject: [PATCH] feat: export approve and execute function (#186) --- .changeset/olive-masks-exercise.md | 5 + package-lock.json | 14 +- package.json | 1 + src/execute.ts | 201 +++++++++++++++++++++++++++++ src/index.ts | 1 + src/types.ts | 76 ++++++++++- src/utils.ts | 21 +++ test/axelar-gateway.js | 19 +-- test/its.js | 17 ++- 9 files changed, 332 insertions(+), 23 deletions(-) create mode 100644 .changeset/olive-masks-exercise.md create mode 100644 src/execute.ts diff --git a/.changeset/olive-masks-exercise.md b/.changeset/olive-masks-exercise.md new file mode 100644 index 00000000..57c587a4 --- /dev/null +++ b/.changeset/olive-masks-exercise.md @@ -0,0 +1,5 @@ +--- +'@axelar-network/axelar-cgp-sui': patch +--- + +feat: export approve and execute functions diff --git a/package-lock.json b/package-lock.json index 6407604e..ce33764a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@axelar-network/axelar-cgp-sui", - "version": "0.6.4", + "version": "0.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@axelar-network/axelar-cgp-sui", - "version": "0.6.4", + "version": "0.7.1", "license": "MIT", "dependencies": { "@cosmjs/cosmwasm-stargate": "^0.32.2", @@ -20,6 +20,7 @@ "@changesets/cli": "^2.27.6", "@ianvs/prettier-plugin-sort-imports": "^4.2.1", "@types/node": "^20.14.11", + "@types/secp256k1": "^4.0.6", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", "chai": "^4.3.7", @@ -2060,6 +2061,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/secp256k1": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.6.tgz", + "integrity": "sha512-hHxJU6PAEUn0TP4S/ZOzuTUvJWuZ6eIKeNKb5RBpODvSl6hp1Wrw4s7ATY50rklRCScUDpHzVA/DQdSjJ3UoYQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", diff --git a/package.json b/package.json index f147f3fb..47cc93cd 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@changesets/cli": "^2.27.6", "@ianvs/prettier-plugin-sort-imports": "^4.2.1", "@types/node": "^20.14.11", + "@types/secp256k1": "^4.0.6", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", "chai": "^4.3.7", diff --git a/src/execute.ts b/src/execute.ts new file mode 100644 index 00000000..95e218bd --- /dev/null +++ b/src/execute.ts @@ -0,0 +1,201 @@ +import { fromHEX } from '@mysten/bcs'; +import { bcs } from '@mysten/sui/bcs'; +import { SuiClient, SuiTransactionBlockResponse, SuiTransactionBlockResponseOptions } from '@mysten/sui/client'; +import { Keypair } from '@mysten/sui/cryptography'; +import { Transaction as SuiTransaction } from '@mysten/sui/transactions'; +import { arrayify, hexlify, keccak256 } from 'ethers/lib/utils'; +import { bcsStructs } from './bcs'; +import { TxBuilder } from './tx-builder'; +import { + ApprovedMessage, + DiscoveryInfo, + GatewayApprovalInfo, + GatewayInfo, + GatewayMessageType, + MessageInfo, + MoveCall, + MoveCallArgument, + MoveCallType, + RawMoveCall, +} from './types'; +import { hashMessage, signMessage } from './utils'; + +const { + gateway: { WeightedSigners, MessageToSign, Proof, Message, Transaction }, +} = bcsStructs; + +export async function approve( + client: SuiClient, + keypair: Keypair, + gatewayApprovalInfo: GatewayApprovalInfo, + messageInfo: MessageInfo, + options: SuiTransactionBlockResponseOptions, +) { + const { packageId, gateway, signers, signerKeys, domainSeparator } = gatewayApprovalInfo; + + const messageData = bcs.vector(Message).serialize([messageInfo]).toBytes(); + const hashed = hashMessage(messageData, GatewayMessageType.ApproveMessages); + + const message = MessageToSign.serialize({ + domain_separator: fromHEX(domainSeparator), + signers_hash: keccak256(WeightedSigners.serialize(signers).toBytes()), + data_hash: hashed, + }).toBytes(); + + let minSigners = 0; + let totalWeight = 0; + + for (let i = 0; i < signers.signers.length; i++) { + totalWeight += signers.signers[i].weight; + + if (totalWeight >= signers.threshold) { + minSigners = i + 1; + break; + } + } + + const signatures = signMessage(signerKeys.slice(0, minSigners), message); + const encodedProof = Proof.serialize({ + signers, + signatures, + }).toBytes(); + + const txBuilder = new TxBuilder(client); + + await txBuilder.moveCall({ + target: `${packageId}::gateway::approve_messages`, + arguments: [gateway, hexlify(messageData), hexlify(encodedProof)], + }); + + await txBuilder.signAndExecute(keypair, options); +} + +export async function execute( + client: SuiClient, + keypair: Keypair, + discoveryInfo: DiscoveryInfo, + gatewayInfo: GatewayInfo, + messageInfo: MessageInfo, + options: SuiTransactionBlockResponseOptions, +): Promise { + let moveCalls = [createInitialMoveCall(discoveryInfo, messageInfo.destination_id)]; + let isFinal = false; + + while (!isFinal) { + const builder = new TxBuilder(client); + + doMoveCalls(builder.tx, moveCalls, messageInfo.payload); + + const nextTx = await inspectTransaction(builder, keypair); + + isFinal = nextTx.is_final; + moveCalls = nextTx.move_calls; + } + + const txBuilder = new TxBuilder(client); + + const ApprovedMessage = await createApprovedMessageCall(txBuilder, gatewayInfo, messageInfo); + + doMoveCalls(txBuilder.tx, moveCalls, messageInfo.payload, ApprovedMessage); + + return txBuilder.signAndExecute(keypair, options); +} + +export async function approveAndExecute( + client: SuiClient, + keypair: Keypair, + gatewayApprovalInfo: GatewayApprovalInfo, + discoveryInfo: DiscoveryInfo, + messageInfo: MessageInfo, + options: SuiTransactionBlockResponseOptions = { + showEvents: true, + }, +): Promise { + await approve(client, keypair, gatewayApprovalInfo, messageInfo, options); + return execute(client, keypair, discoveryInfo, gatewayApprovalInfo, messageInfo, options); +} + +function createInitialMoveCall(discoveryInfo: DiscoveryInfo, destinationId: string): RawMoveCall { + const { packageId, discovery } = discoveryInfo; + const discoveryArg = [MoveCallType.Object, ...arrayify(discovery)]; + const targetIdArg = [MoveCallType.Pure, ...arrayify(destinationId)]; + + return { + function: { + package_id: packageId, + module_name: 'discovery', + name: 'get_transaction', + }, + arguments: [discoveryArg, targetIdArg], + type_arguments: [], + }; +} + +async function inspectTransaction(builder: TxBuilder, keypair: Keypair) { + const resp = await builder.devInspect(keypair.toSuiAddress()); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const txData: any = resp.results?.[0]?.returnValues?.[0]?.[0]; + + return Transaction.parse(new Uint8Array(txData)); +} + +function createApprovedMessageCall(builder: TxBuilder, gatewayInfo: GatewayInfo, messageInfo: MessageInfo) { + return builder.moveCall({ + target: `${gatewayInfo.packageId}::gateway::take_approved_message`, + arguments: [ + gatewayInfo.gateway, + messageInfo.source_chain, + messageInfo.message_id, + messageInfo.source_address, + messageInfo.destination_id, + messageInfo.payload, + ], + }); +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +function doMoveCalls(tx: SuiTransaction, moveCalls: RawMoveCall[], payload: string, ApprovedMessage?: ApprovedMessage) { + const txResults: any[][] = []; + + for (const call of moveCalls) { + const moveCall = buildMoveCall(tx, call, payload, txResults, ApprovedMessage); + const txResult = tx.moveCall(moveCall); + txResults.push(Array.isArray(txResult) ? txResult : [txResult]); + } +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +function buildMoveCall( + tx: SuiTransaction, + moveCallInfo: RawMoveCall, + payload: string, + previousReturns: any[][], + ApprovedMessage?: ApprovedMessage, +): MoveCall { + const decodeArgs = (args: any[]): MoveCallArgument[] => + args.map(([argType, ...arg]) => { + switch (argType) { + case MoveCallType.Object: + return tx.object(hexlify(arg)); + case MoveCallType.Pure: + return tx.pure(arrayify(arg)); + case MoveCallType.ApproveMessage: + return ApprovedMessage; + case MoveCallType.Payload: + return tx.pure(bcs.vector(bcs.U8).serialize(arrayify(payload))); + case MoveCallType.HotPotato: + return previousReturns[arg[1]][arg[2]]; + default: + throw new Error(`Invalid argument prefix: ${argType}`); + } + }); + + const { package_id: packageId, module_name: moduleName, name } = moveCallInfo.function; + + return { + target: `${packageId}::${moduleName}::${name}`, + arguments: decodeArgs(moveCallInfo.arguments), + typeArguments: moveCallInfo.type_arguments, + }; +} diff --git a/src/index.ts b/src/index.ts index b2409a5c..ede02c5a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export * from './bcs'; export * from './tx-builder'; export * from './utils'; export * from './types'; +export * from './execute'; diff --git a/src/types.ts b/src/types.ts index b4ae443a..57da06be 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,7 @@ -const { bcs } = require('@mysten/sui/bcs'); -const { fromHEX, toHEX } = require('@mysten/bcs'); +import { fromHEX, toHEX } from '@mysten/bcs'; +import type { SerializedBcs } from '@mysten/bcs'; +import { bcs } from '@mysten/sui/bcs'; +import type { TransactionArgument } from '@mysten/sui/transactions'; export const SUI_PACKAGE_ID = '0x2'; export const STD_PACKAGE_ID = '0x1'; @@ -23,6 +25,11 @@ export enum ITSMessageType { InterchainTokenDeployment = 1, } +export enum GatewayMessageType { + ApproveMessages = 0, + RotateSigners = 1, +} + export interface DependencyNode extends Dependency { dependencies: string[]; } @@ -31,3 +38,68 @@ export const UID = bcs.fixedArray(32, bcs.u8()).transform({ input: (id: string) => fromHEX(id), output: (id: number[]) => toHEX(Uint8Array.from(id)), }); + +export type Signer = { + pub_key: Uint8Array; + weight: number; +}; + +export type MessageInfo = { + source_chain: string; + message_id: string; + source_address: string; + destination_id: string; + payload_hash: string; + payload: string; +}; + +export type GatewayInfo = { + packageId: string; + gateway: string; +}; + +export type GatewayApprovalInfo = GatewayInfo & { + signers: { + signers: Signer[]; + threshold: number; + nonce: string; + }; + signerKeys: string[]; + domainSeparator: string; +}; + +export type DiscoveryInfo = { + packageId: string; + discovery: string; +}; + +export type RawMoveCall = { + function: { + package_id: string; + module_name: string; + name: string; + }; + arguments: any[]; // eslint-disable-line @typescript-eslint/no-explicit-any + type_arguments: string[]; +}; + +export type MoveCallArgument = TransactionArgument | SerializedBcs; // eslint-disable-line @typescript-eslint/no-explicit-any + +export type MoveCall = { + arguments?: MoveCallArgument[]; + typeArguments?: string[]; + target: string; +}; + +export type ApprovedMessage = { + $kind: string; + Result: number; +}; + +export enum MoveCallType { + Object = 0, + Pure = 1, + ApproveMessage = 2, + Payload = 3, + HotPotato = 4, +} diff --git a/src/utils.ts b/src/utils.ts index 2433eeff..6bdf9d86 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,8 @@ import fs from 'fs'; import path from 'path'; import { getFullnodeUrl } from '@mysten/sui/client'; import { getFaucetHost, requestSuiFromFaucetV0 } from '@mysten/sui/faucet'; +import { arrayify, keccak256 } from 'ethers/lib/utils'; +import secp256k1 from 'secp256k1'; import toml from 'smol-toml'; import { Dependency, DependencyNode, InterchainTokenOptions } from './types'; @@ -214,3 +216,22 @@ export function parseEnv(arg: string) { return JSON.parse(arg); } } + +export function hashMessage(data: Uint8Array, commandType: number) { + const toHash = new Uint8Array(data.length + 1); + toHash[0] = commandType; + toHash.set(data, 1); + + return keccak256(toHash); +} + +export function signMessage(privKeys: string[], messageToSign: Uint8Array) { + const signatures = []; + + for (const privKey of privKeys) { + const { signature, recid } = secp256k1.ecdsaSign(arrayify(keccak256(messageToSign)), arrayify(privKey)); + signatures.push(new Uint8Array([...signature, recid])); + } + + return signatures; +} diff --git a/test/axelar-gateway.js b/test/axelar-gateway.js index de51c4c1..6b603096 100644 --- a/test/axelar-gateway.js +++ b/test/axelar-gateway.js @@ -2,16 +2,8 @@ const { SuiClient, getFullnodeUrl } = require('@mysten/sui/client'); const { Ed25519Keypair } = require('@mysten/sui/keypairs/ed25519'); const { Secp256k1Keypair } = require('@mysten/sui/keypairs/secp256k1'); const { requestSuiFromFaucetV0, getFaucetHost } = require('@mysten/sui/faucet'); -const { - publishPackage, - getRandomBytes32, - expectRevert, - expectEvent, - approveMessage, - hashMessage, - signMessage, - approveAndExecuteMessage, -} = require('./testutils'); +const { publishPackage, getRandomBytes32, expectRevert, expectEvent, approveMessage, hashMessage, signMessage } = require('./testutils'); +const { approveAndExecute } = require('../dist/execute'); const { TxBuilder } = require('../dist/tx-builder'); const { bcsStructs: { @@ -37,6 +29,7 @@ describe('Axelar Gateway', () => { let gateway; let discovery; const gatewayInfo = {}; + const discoveryInfo = {}; function calculateNextSigners() { const signerKeys = [getRandomBytes32(), getRandomBytes32(), getRandomBytes32()]; @@ -119,8 +112,8 @@ describe('Axelar Gateway', () => { gatewayInfo.gateway = gateway; gatewayInfo.domainSeparator = domainSeparator; gatewayInfo.packageId = packageId; - gatewayInfo.discovery = discovery; - gatewayInfo.discoveryPackageId = discoveryPackageId; + discoveryInfo.packageId = discoveryPackageId; + discoveryInfo.discovery = discovery; }); describe('Signer Rotation', () => { @@ -316,7 +309,7 @@ describe('Axelar Gateway', () => { payload_hash: keccak256(payload), }; - let resp = await approveAndExecuteMessage(client, keypair, gatewayInfo, message, { showEvents: true }); + let resp = await approveAndExecute(client, keypair, gatewayInfo, discoveryInfo, message, { showEvents: true }); const event = resp.events.find((event) => event.type === `${testId}::gmp::Executed`); diff --git a/test/its.js b/test/its.js index f931c719..65f71863 100644 --- a/test/its.js +++ b/test/its.js @@ -18,11 +18,11 @@ const { getSingletonChannelId, getITSChannelId, setupTrustedAddresses, - approveAndExecuteMessage, } = require('./testutils'); const { expect } = require('chai'); const { CLOCK_PACKAGE_ID } = require('../dist/types'); const { getDeploymentOrder, fundAccountsFromFaucet } = require('../dist/utils'); +const { approveAndExecute } = require('../dist/execute'); const { bcsStructs } = require('../dist/bcs'); const { ITSMessageType } = require('../dist/types'); const { TxBuilder } = require('../dist/tx-builder'); @@ -45,6 +45,7 @@ describe('ITS', () => { // Parameters for Gateway Setup const gatewayInfo = {}; + const discoveryInfo = {}; const domainSeparator = getRandomBytes32(); const [operator, deployer, keypair] = generateEd25519Keypairs(3); const minimumRotationDelay = 1000; @@ -81,9 +82,8 @@ describe('ITS', () => { gatewayInfo.gateway = objectIds.gateway; gatewayInfo.domainSeparator = domainSeparator; gatewayInfo.packageId = deployments.axelar_gateway.packageId; - gatewayInfo.discoveryPackageId; - gatewayInfo.discoveryPackageId = deployments.relayer_discovery.packageId; - gatewayInfo.discovery = objectIds.relayerDiscovery; + discoveryInfo.packageId = deployments.relayer_discovery.packageId; + discoveryInfo.discovery = objectIds.relayerDiscovery; } async function registerItsTransaction() { @@ -231,6 +231,11 @@ describe('ITS', () => { const amount = 1e9; const data = '0x1234'; + const discoveryInfo = { + packageId: deployments.relayer_discovery.packageId, + discovery: objectIds.relayerDiscovery, + }; + // Channel ID for the ITS example. This will be encoded in the payload const itsExampleChannelId = await getSingletonChannelId(client, objectIds.singleton); // ITS transfer payload from Ethereum to Sui @@ -248,7 +253,7 @@ describe('ITS', () => { payload_hash: keccak256(payload), }; - await approveAndExecuteMessage(client, keypair, gatewayInfo, message); + await approveAndExecute(client, keypair, gatewayInfo, discoveryInfo, message); }); }); @@ -333,7 +338,7 @@ describe('ITS', () => { payload_hash: keccak256(payload), }; - await approveAndExecuteMessage(client, keypair, gatewayInfo, message); + await approveAndExecute(client, keypair, gatewayInfo, discoveryInfo, message); }); }); });