From a5fed9943273f139b6ee8d2650ff88294c4f3e77 Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Thu, 14 Sep 2023 04:40:32 -0400 Subject: [PATCH 01/10] v0.1.0 From 4d88dbaf36b1d2677cf19541811b62e5d729ba3c Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Fri, 15 Sep 2023 00:10:36 -0400 Subject: [PATCH 02/10] v0.1.1 From 9723ec5127965c0d42c44d17869ab237c3e3ae5a Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Tue, 12 Dec 2023 16:41:42 -0500 Subject: [PATCH 03/10] v1.0.0 From 6b90a9574c62ea7ff26945d351073a42b736c0d2 Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Wed, 17 Jan 2024 03:31:59 -0500 Subject: [PATCH 04/10] feat: draft combine signatures into gateway batch --- .gitignore | 2 + evm/combine-signatures.js | 221 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 evm/combine-signatures.js diff --git a/.gitignore b/.gitignore index 2c00110db3..0f574a112c 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,5 @@ sui/move .idea .stellar/ + +signatures/ diff --git a/evm/combine-signatures.js b/evm/combine-signatures.js new file mode 100644 index 0000000000..c9bb18ba83 --- /dev/null +++ b/evm/combine-signatures.js @@ -0,0 +1,221 @@ +'use strict'; + +const { ethers } = require('hardhat'); +const fs = require('fs'); +const path = require('path'); +const axios = require('axios'); +const { + getDefaultProvider, + utils: { computePublicKey, keccak256, getAddress, verifyMessage, arrayify, concat, toUtf8Bytes, defaultAbiCoder }, + Contract, +} = ethers; +const { Command, Option } = require('commander'); +const { mainProcessor, printInfo, printWalletInfo, getGasOptions, printError, validateParameters, getContractJSON } = require('./utils'); +const { handleTx } = require('./its'); +const { getWallet } = require('./sign-utils'); +const { addBaseOptions } = require('./cli-utils'); +const IAxelarGateway = getContractJSON('IAxelarGateway'); + +function readSignatures() { + const signaturesDir = path.join(__dirname, '../signatures'); + const signatureFiles = fs.readdirSync(signaturesDir); + const signatures = []; + + signatureFiles.forEach((file) => { + const filePath = path.join(signaturesDir, file); + const fileContent = fs.readFileSync(filePath, 'utf8'); + + try { + const signature = JSON.parse(fileContent); + signatures.push(signature); + } catch (error) { + printError(`Error parsing JSON in file ${file}`, error.message); + } + }); + + return signatures; +} + +function getAddressFromPublicKey(publicKey) { + const uncompressedPublicKey = computePublicKey(publicKey, false); + const addressHash = keccak256(uncompressedPublicKey.slice(4)); + + return getAddress('0x' + addressHash.slice(-40)); +} + +async function getValidatorsAndThreshold(chain) { + const url = `https://lcd-axelar.imperator.co/axelar/evm/v1beta1/key_address/${chain.id}`; + + try { + const response = await axios.get(url); + const data = response.data; + + return [data.key_id, data.threshold, data.addresses]; + } catch (error) { + printError('Error fetching data', error); + } +} + +function getEthSignedMessageHash(message) { + const messageHash = keccak256(arrayify(message)); + + const prefix = '\x19Ethereum Signed Message:\n32'; + + const ethSignedMessageHash = keccak256(concat([toUtf8Bytes(prefix), arrayify(messageHash)])); + + return ethSignedMessageHash; +} + +async function processCommand(_, chain, options) { + const { address, action, privateKey } = options; + + const contracts = chain.contracts; + + const contractName = 'AxelarGateway'; + + const gatewayAddress = address || contracts.AxelarGateway?.address; + + validateParameters({ isValidAddress: { gatewayAddress } }); + + const rpc = chain.rpc; + const provider = getDefaultProvider(rpc); + + const wallet = await getWallet(privateKey, provider, options); + await printWalletInfo(wallet); + + const gasOptions = await getGasOptions(chain, options, contractName); + + printInfo('Batch Action', action); + + switch (action) { + case 'constructBatch': { + const { message } = options; + + validateParameters({ isValidCalldata: { message } }); + + const [chainKeyId, threshold, validatorAddresses] = await getValidatorsAndThreshold(chain); + + const signatures = readSignatures(); + + const batchSignatures = []; + const batchValidators = []; + const batchWeights = []; + + let totalWeight = 0; + + const expectedMessageHash = getEthSignedMessageHash(message); + + for (const signatureJSON of signatures) { + const keyId = signatureJSON.key_id; + const validatorAddress = signatureJSON.validator; + const msgHash = signatureJSON.msg_hash; + const pubKey = signatureJSON.pub_key; + const signature = signatureJSON.signature; + + validateParameters({ + isNonEmptyString: { keyId }, + isValidAddress: { validatorAddress }, + isKeccak256Hash: { msgHash }, + isValidCalldata: { pubKey, signature }, + }); + + if (chainKeyId !== keyId) { + printError('Signature contains invalid key_id', keyId); + return; + } + + if (msgHash.toLowerCase() !== expectedMessageHash.toLowerCase()) { + printError('Message hash does not equal expected message hash', msgHash); + return; + } + + const expectedAddress = getAddressFromPublicKey(pubKey); + + if (expectedAddress.toLowerCase() !== validatorAddress.toLowerCase()) { + printError('Public key does not match validator address', validatorAddress); + return; + } + + const signer = verifyMessage(msgHash, signature); + + if (signer.toLowerCase() !== validatorAddress.toLowerCase()) { + printError('Signature is invalid for the given validator address', validatorAddress); + return; + } + + const addressInfo = validatorAddresses.find( + (addressObj) => addressObj.address.toLowerCase() === validatorAddress.toLowerCase(), + ); + const validatorWeight = addressInfo.weight; + + totalWeight += validatorWeight; + + batchValidators.push(validatorAddress); + batchSignatures.push(signature); + batchWeights.push(validatorWeight); + + if (totalWeight >= threshold) { + break; + } + } + + if (totalWeight < threshold) { + printError('Total signer weight less than threshold', totalWeight); + return; + } + + const proof = defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'uint256', 'bytes[]'], + [batchValidators, batchWeights, threshold, batchSignatures], + ); + + const input = defaultAbiCoder.encode(['bytes', 'bytes'], [message, proof]); + + printInfo('Batch input data', input); + + break; + } + + case 'executeBatch': { + const { input } = options; + + validateParameters({ isValidCalldata: { input } }); + + const gateway = new Contract(gatewayAddress, IAxelarGateway.abi, wallet); + + const tx = await gateway.execute(input, gasOptions); + + await handleTx(tx, chain, gateway, action, 'Executed'); + + break; + } + + default: { + throw new Error(`Unknown batch action ${action}`); + } + } +} + +async function main(options) { + await mainProcessor(options, processCommand); +} + +if (require.main === module) { + const program = new Command(); + + program.name('combine-signatures').description('script to combine manually created signatures and construct gateway batch'); + + addBaseOptions(program, { address: true }); + + program.addOption( + new Option('--action ', 'signature action').choices(['computeMessageHash', 'constructBatch', 'executeBatch']), + ); + program.addOption(new Option('-m, --message ', 'bytes message for validators to sign').env('MESSAGE')); + program.addOption(new Option('-i, --input ', 'batch input consisting of bytes message (data) and bytes proof').env('INPUT')); + + program.action((options) => { + main(options); + }); + + program.parse(); +} From edca227dc909513ef655b992a6e10ac15c0cd2a6 Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Wed, 24 Jan 2024 00:22:10 -0500 Subject: [PATCH 05/10] feat: validate proof with axelar auth weighted --- evm/combine-signatures.js | 182 ++++++++++++++++++++++++-------------- 1 file changed, 117 insertions(+), 65 deletions(-) diff --git a/evm/combine-signatures.js b/evm/combine-signatures.js index c9bb18ba83..f4e2dbfc44 100644 --- a/evm/combine-signatures.js +++ b/evm/combine-signatures.js @@ -3,18 +3,28 @@ const { ethers } = require('hardhat'); const fs = require('fs'); const path = require('path'); -const axios = require('axios'); const { getDefaultProvider, - utils: { computePublicKey, keccak256, getAddress, verifyMessage, arrayify, concat, toUtf8Bytes, defaultAbiCoder }, + utils: { computePublicKey, keccak256, getAddress, arrayify, defaultAbiCoder, hashMessage }, Contract, } = ethers; const { Command, Option } = require('commander'); -const { mainProcessor, printInfo, printWalletInfo, getGasOptions, printError, validateParameters, getContractJSON } = require('./utils'); +const { + mainProcessor, + printInfo, + printWalletInfo, + getGasOptions, + printError, + validateParameters, + getContractJSON, + getRandomBytes32, + getEVMAddresses, +} = require('./utils'); const { handleTx } = require('./its'); const { getWallet } = require('./sign-utils'); const { addBaseOptions } = require('./cli-utils'); const IAxelarGateway = getContractJSON('IAxelarGateway'); +const IAxelarAuth = getContractJSON('IAxelarAuth'); function readSignatures() { const signaturesDir = path.join(__dirname, '../signatures'); @@ -38,44 +48,19 @@ function readSignatures() { function getAddressFromPublicKey(publicKey) { const uncompressedPublicKey = computePublicKey(publicKey, false); - const addressHash = keccak256(uncompressedPublicKey.slice(4)); + const addressHash = keccak256(`0x${uncompressedPublicKey.slice(4)}`); return getAddress('0x' + addressHash.slice(-40)); } -async function getValidatorsAndThreshold(chain) { - const url = `https://lcd-axelar.imperator.co/axelar/evm/v1beta1/key_address/${chain.id}`; - - try { - const response = await axios.get(url); - const data = response.data; - - return [data.key_id, data.threshold, data.addresses]; - } catch (error) { - printError('Error fetching data', error); - } -} - -function getEthSignedMessageHash(message) { - const messageHash = keccak256(arrayify(message)); - - const prefix = '\x19Ethereum Signed Message:\n32'; - - const ethSignedMessageHash = keccak256(concat([toUtf8Bytes(prefix), arrayify(messageHash)])); - - return ethSignedMessageHash; -} - -async function processCommand(_, chain, options) { +async function processCommand(config, chain, options) { const { address, action, privateKey } = options; const contracts = chain.contracts; - const contractName = 'AxelarGateway'; - - const gatewayAddress = address || contracts.AxelarGateway?.address; - - validateParameters({ isValidAddress: { gatewayAddress } }); + if (address) { + validateParameters({ isValidAddress: { address } }); + } const rpc = chain.rpc; const provider = getDefaultProvider(rpc); @@ -83,43 +68,85 @@ async function processCommand(_, chain, options) { const wallet = await getWallet(privateKey, provider, options); await printWalletInfo(wallet); - const gasOptions = await getGasOptions(chain, options, contractName); - printInfo('Batch Action', action); switch (action) { + case 'computeMessageHash': { + const { commandId, sourceChain, sourceAddress, contractAddress, payloadHash, sourceTxHash, sourceEventIndex } = options; + + validateParameters({ + isNonEmptyString: { sourceChain, sourceAddress }, + isValidAddress: { contractAddress }, + isKeccak256Hash: { commandId, payloadHash, sourceTxHash }, + isValidNumber: { sourceEventIndex }, + }); + + const chainId = chain.chainId; + const commandID = commandId || getRandomBytes32(); + const command = 'approveContractCall'; + const params = defaultAbiCoder.encode( + ['string', 'string', 'address', 'bytes32', 'bytes32', 'uint256'], + [sourceChain, sourceAddress, contractAddress, payloadHash, sourceTxHash, sourceEventIndex], + ); + + const data = defaultAbiCoder.encode( + ['uint256', 'bytes32[]', 'string[]', 'bytes[]'], + [chainId, [commandID], [command], [params]], + ); + + const dataHash = hashMessage(arrayify(keccak256(data))); + + printInfo('Original bytes message (pre-hash)', data); + printInfo('Message hash for validators to sign', dataHash); + + break; + } + case 'constructBatch': { const { message } = options; validateParameters({ isValidCalldata: { message } }); - const [chainKeyId, threshold, validatorAddresses] = await getValidatorsAndThreshold(chain); + const { + addresses: validatorAddresses, + weights, + threshold, + keyID: expectedKeyId, + } = await getEVMAddresses(config, chain.id, options); + + const validatorWeights = {}; + + validatorAddresses.forEach((address, index) => { + validatorWeights[address.toLowerCase()] = weights[index]; + }); const signatures = readSignatures(); + const sortedSignatures = signatures.sort((a, b) => { + const addressA = getAddressFromPublicKey(`0x${a.pub_key}`).toLowerCase(); + const addressB = getAddressFromPublicKey(`0x${b.pub_key}`).toLowerCase(); + return addressA.localeCompare(addressB); + }); + const batchSignatures = []; - const batchValidators = []; - const batchWeights = []; let totalWeight = 0; - const expectedMessageHash = getEthSignedMessageHash(message); + const expectedMessageHash = hashMessage(arrayify(keccak256(message))); - for (const signatureJSON of signatures) { + for (const signatureJSON of sortedSignatures) { const keyId = signatureJSON.key_id; - const validatorAddress = signatureJSON.validator; - const msgHash = signatureJSON.msg_hash; - const pubKey = signatureJSON.pub_key; - const signature = signatureJSON.signature; + const msgHash = `0x${signatureJSON.msg_hash}`; + const pubKey = `0x${signatureJSON.pub_key}`; + const signature = `0x${signatureJSON.signature}`; validateParameters({ isNonEmptyString: { keyId }, - isValidAddress: { validatorAddress }, isKeccak256Hash: { msgHash }, isValidCalldata: { pubKey, signature }, }); - if (chainKeyId !== keyId) { + if (expectedKeyId !== keyId) { printError('Signature contains invalid key_id', keyId); return; } @@ -129,30 +156,25 @@ async function processCommand(_, chain, options) { return; } - const expectedAddress = getAddressFromPublicKey(pubKey); + const validatorAddress = getAddressFromPublicKey(pubKey); - if (expectedAddress.toLowerCase() !== validatorAddress.toLowerCase()) { - printError('Public key does not match validator address', validatorAddress); - return; - } + // const signer = verifyMessage(msgHash, signature); + + // if (signer.toLowerCase() !== validatorAddress.toLowerCase()) { + // printError('Signature is invalid for the given validator address', validatorAddress); + // return; + // } - const signer = verifyMessage(msgHash, signature); + const validatorWeight = validatorWeights[validatorAddress.toLowerCase()]; - if (signer.toLowerCase() !== validatorAddress.toLowerCase()) { - printError('Signature is invalid for the given validator address', validatorAddress); + if (!validatorWeight) { + printError('Validator does not belong to current epoch', validatorAddress); return; } - const addressInfo = validatorAddresses.find( - (addressObj) => addressObj.address.toLowerCase() === validatorAddress.toLowerCase(), - ); - const validatorWeight = addressInfo.weight; - totalWeight += validatorWeight; - batchValidators.push(validatorAddress); batchSignatures.push(signature); - batchWeights.push(validatorWeight); if (totalWeight >= threshold) { break; @@ -166,12 +188,29 @@ async function processCommand(_, chain, options) { const proof = defaultAbiCoder.encode( ['address[]', 'uint256[]', 'uint256', 'bytes[]'], - [batchValidators, batchWeights, threshold, batchSignatures], + [validatorAddresses, weights, threshold, batchSignatures], ); + const authAddress = address || contracts.AxelarGateway?.authModule; + const auth = new Contract(authAddress, IAxelarAuth.abi, wallet); + + let isValidProof; + + try { + isValidProof = await auth.validateProof(expectedMessageHash, proof); + } catch (error) { + printError('Invalid batch proof', error); + return; + } + + if (!isValidProof) { + printError('Invalid batch proof'); + return; + } + const input = defaultAbiCoder.encode(['bytes', 'bytes'], [message, proof]); - printInfo('Batch input data', input); + printInfo('Batch input data for gateway execute function', input); break; } @@ -181,8 +220,13 @@ async function processCommand(_, chain, options) { validateParameters({ isValidCalldata: { input } }); + const contractName = 'AxelarGateway'; + + const gatewayAddress = address || contracts.AxelarGateway?.address; const gateway = new Contract(gatewayAddress, IAxelarGateway.abi, wallet); + const gasOptions = await getGasOptions(chain, options, contractName); + const tx = await gateway.execute(input, gasOptions); await handleTx(tx, chain, gateway, action, 'Executed'); @@ -191,7 +235,7 @@ async function processCommand(_, chain, options) { } default: { - throw new Error(`Unknown batch action ${action}`); + throw new Error(`Unknown signature action ${action}`); } } } @@ -210,9 +254,17 @@ if (require.main === module) { program.addOption( new Option('--action ', 'signature action').choices(['computeMessageHash', 'constructBatch', 'executeBatch']), ); - program.addOption(new Option('-m, --message ', 'bytes message for validators to sign').env('MESSAGE')); + program.addOption(new Option('-m, --message ', 'bytes message (validators sign the hash of this message)').env('MESSAGE')); program.addOption(new Option('-i, --input ', 'batch input consisting of bytes message (data) and bytes proof').env('INPUT')); + program.addOption(new Option('--commandId ', 'gateway command id').env('COMMAND_ID')); + program.addOption(new Option('--sourceChain ', 'source chain for contract call').env('SOURCE_CHAIN')); + program.addOption(new Option('--sourceAddress ', 'source address for contract call').env('SOURCE_ADDRESS')); + program.addOption(new Option('--contractAddress ', 'contract address on current chain').env('CONTRACT_ADDRESS')); + program.addOption(new Option('--payloadHash ', 'payload hash').env('PAYLOAD_HASH')); + program.addOption(new Option('--sourceTxHash ', 'source transaction hash').env('SOURCE_TX_HASH')); + program.addOption(new Option('--sourceEventIndex ', 'source event index').env('SOURCE_EVENT_INDEX')); + program.action((options) => { main(options); }); From 1a5baffb351641509e2d039ada9d383be8fd395f Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Wed, 24 Jan 2024 11:32:25 -0500 Subject: [PATCH 06/10] feat: ensure recovered address matches validator address --- evm/combine-signatures.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/evm/combine-signatures.js b/evm/combine-signatures.js index f4e2dbfc44..486c120b1a 100644 --- a/evm/combine-signatures.js +++ b/evm/combine-signatures.js @@ -5,7 +5,7 @@ const fs = require('fs'); const path = require('path'); const { getDefaultProvider, - utils: { computePublicKey, keccak256, getAddress, arrayify, defaultAbiCoder, hashMessage }, + utils: { computePublicKey, keccak256, getAddress, arrayify, defaultAbiCoder, hashMessage, recoverAddress }, Contract, } = ethers; const { Command, Option } = require('commander'); @@ -158,12 +158,12 @@ async function processCommand(config, chain, options) { const validatorAddress = getAddressFromPublicKey(pubKey); - // const signer = verifyMessage(msgHash, signature); + const signer = recoverAddress(msgHash, signature); - // if (signer.toLowerCase() !== validatorAddress.toLowerCase()) { - // printError('Signature is invalid for the given validator address', validatorAddress); - // return; - // } + if (signer.toLowerCase() !== validatorAddress.toLowerCase()) { + printError('Signature is invalid for the given validator address', validatorAddress); + return; + } const validatorWeight = validatorWeights[validatorAddress.toLowerCase()]; From abcf0a7d6fef96a25c1baf233cc42ef92ddebe01 Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Fri, 2 Feb 2024 04:19:22 -0500 Subject: [PATCH 07/10] feat: address comments --- evm/README.md | 71 +++++++++++++++++++++++++---- evm/combine-signatures.js | 94 +++++++++++++++++++++------------------ evm/utils.js | 22 ++++++++- 3 files changed, 133 insertions(+), 54 deletions(-) diff --git a/evm/README.md b/evm/README.md index 34297279c6..f62f06f2ad 100644 --- a/evm/README.md +++ b/evm/README.md @@ -258,16 +258,71 @@ To get details of options provided in the command run: node evm/verify-contract.js --help ``` -## Interchain Token Service +## Combine Validator Signatures -### Link Token +The following script is for emergency situations where we need to ask validators to manually sign gateway approvals. These validator signatures must be collected and stored within separate files in the signatures folder in the root directory. The script will then combine these signatures into a gateway batch and optionally execute this batch on the Axelar gateway contract on the specified chain. -#### Legacy custom ITS tokens +Below are instructions on how to utilize this script: -Custom tokens that have already registered with ITS (via `deployTokenManager`) prior to ITS v2.1.0 release can continue being linked to new chains via the following approach. However, we do recommend registering them. Token manager type should be passed in via `--type` flag (e.g. `MINT_BURN`). +1. Construct the gateway batch data by passing in the payload hash gotten from running the `governance.js` script. -```bash -node evm/its.js link-token --salt [deploy-salt] [token-id] [destination-chain] [token-address] [type] [operator] -``` + ```bash + node evm/combine-signatures.js -e [ENV] -n [CHAIN_NAME] --action createBatchData --payloadHash [PAYLOAD_HASH] + ``` + + Note: Other gateway approval variables have default values but can be overriden via CLI options + + - sourceChain: 'Axelarnet' + - sourceAddress: 'axelar10d07y265gmmuvt4z0w9aw880jnsr700j7v9daj' + - contractAddress = interchain governance address + - commandId = random bytes32 value + +2. Running the command above will output the raw batch data, the data hash, and the vald sign command that should be used to generate validator signatures. Here is an example output: + + ```bash + axelard vald-sign evm-ethereum-2-3799882 [validator-addr] 0x2716d6d6c37ccfb1ea1db014b175ba23d0f522d7789f3676b7e5e64e4322731 + ``` + + The validator address should be entered dynamically based on which validator is signing. + +3. Connect to AWS and switch to the correct context depending on which environment validators will be signing in. + +4. Get all the namespaces within this context: + + ```bash + kubectl get ns + ``` + +5. List pods for each validator namespace. Example for stagenet validator 1: + + ```bash + kubectl get pods -n stagenet-validator1 + ``` + +6. Exec into the validator pod. Example for stagenet validator 1: + + ```bash + kubectl exec -ti axelar-core-node-validator1-5b68db5c49-mf5q6 -n stagenet-validator1 -c vald -- sh + ``` + +7. Within the validator container, run the vald command from step 2 with the correct validator address. Validator addresses can be found by appending this endpoint `/axelar/multisig/v1beta1/key?key_id=[KEY_ID]` to the Axelar lcd url in the chains config file. Running this command should result in a JSON output containing the signature, similar to the example below: + + ```json + { + "key_id": "evm-ethereum-2-3799882", + "msg_hash": "9dd126c2fc8b8a29f92894eb8830e552b7caf1a3c25e89cc0d74b99bc1165039", + "pub_key": "030d0b5bff4bff1adb9ab47df8c70defe27bfae84b12b77fac64cdcc2263e8e51b", + "signature": "634cb96060d13c083572d5c57ec9f6441103532434824a046dec4e8dfa0645544ea155230656ee37329572e1c52df1c85e48f0ef5f283bc7e036b9fd51ade6661b", + "validator": "axelarvaloper1yfrz78wthyzykzf08h7z6pr0et0zlzf0dnehwx" + } + ``` + +8. Create a signatures folder in the root directory of this repository. Repeat step 7 until enough signatures have been generated to surpass the threshold weight for the current epoch (threshold weight can be found at the same endpoint as validator addresses). Store each signature JSON in a separate file within the signatures folder, filenames do not have to follow any specific convention. + +9. Run the construct batch command passing in the raw batch data logged in step 1. This will generate the input bytes that can be passed directly into the execute command on Axelar gateway. You can also optionally pass the `--execute` command which will automatically execute the batch on the Axelar gateway contract. + + ```bash + node evm/combine-signatures.js -e [ENV] -n [CHAIN_NAME] --action constructBatch --batchData [RAW_BATCH_DATA] + ``` -The raw `bytes32` salt can be provided via `--rawSalt [raw-salt]` instead of hashing the provided salt string. + Note: This step will generate the proof and validate it against the Axelar auth module before printing out the input bytes. diff --git a/evm/combine-signatures.js b/evm/combine-signatures.js index 486c120b1a..c6c2b5a6e9 100644 --- a/evm/combine-signatures.js +++ b/evm/combine-signatures.js @@ -6,6 +6,7 @@ const path = require('path'); const { getDefaultProvider, utils: { computePublicKey, keccak256, getAddress, arrayify, defaultAbiCoder, hashMessage, recoverAddress }, + constants: { HashZero }, Contract, } = ethers; const { Command, Option } = require('commander'); @@ -23,8 +24,6 @@ const { const { handleTx } = require('./its'); const { getWallet } = require('./sign-utils'); const { addBaseOptions } = require('./cli-utils'); -const IAxelarGateway = getContractJSON('IAxelarGateway'); -const IAxelarAuth = getContractJSON('IAxelarAuth'); function readSignatures() { const signaturesDir = path.join(__dirname, '../signatures'); @@ -71,22 +70,25 @@ async function processCommand(config, chain, options) { printInfo('Batch Action', action); switch (action) { - case 'computeMessageHash': { - const { commandId, sourceChain, sourceAddress, contractAddress, payloadHash, sourceTxHash, sourceEventIndex } = options; + case 'createBatchData': { + const { commandId, payloadHash } = options; + + const sourceChain = options.sourceChain || 'Axelarnet'; + const sourceAddress = options.sourceAddress || 'axelar10d07y265gmmuvt4z0w9aw880jnsr700j7v9daj'; + const contractAddress = options.contractAddress || contracts.InterchainGovernance?.address; + const commandID = commandId || getRandomBytes32(); validateParameters({ isNonEmptyString: { sourceChain, sourceAddress }, isValidAddress: { contractAddress }, - isKeccak256Hash: { commandId, payloadHash, sourceTxHash }, - isValidNumber: { sourceEventIndex }, + isKeccak256Hash: { commandID, payloadHash }, }); const chainId = chain.chainId; - const commandID = commandId || getRandomBytes32(); const command = 'approveContractCall'; const params = defaultAbiCoder.encode( ['string', 'string', 'address', 'bytes32', 'bytes32', 'uint256'], - [sourceChain, sourceAddress, contractAddress, payloadHash, sourceTxHash, sourceEventIndex], + [sourceChain, sourceAddress, contractAddress, payloadHash, HashZero, 0], ); const data = defaultAbiCoder.encode( @@ -96,23 +98,21 @@ async function processCommand(config, chain, options) { const dataHash = hashMessage(arrayify(keccak256(data))); + const { keyID } = await getEVMAddresses(config, chain.id, options); + printInfo('Original bytes message (pre-hash)', data); printInfo('Message hash for validators to sign', dataHash); + printInfo('Vald sign command', `axelard vald-sign ${keyID} [validator-addr] ${dataHash}`); break; } case 'constructBatch': { - const { message } = options; + const { batchData, execute } = options; - validateParameters({ isValidCalldata: { message } }); + validateParameters({ isValidCalldata: { batchData } }); - const { - addresses: validatorAddresses, - weights, - threshold, - keyID: expectedKeyId, - } = await getEVMAddresses(config, chain.id, options); + const { addresses: validatorAddresses, weights, threshold } = await getEVMAddresses(config, chain.id, options); const validatorWeights = {}; @@ -129,10 +129,12 @@ async function processCommand(config, chain, options) { }); const batchSignatures = []; + const checkedAddresses = []; - let totalWeight = 0; + const expectedMessageHash = hashMessage(arrayify(keccak256(batchData))); - const expectedMessageHash = hashMessage(arrayify(keccak256(message))); + let prevKeyId = sortedSignatures[0].key_id; + let totalWeight = 0; for (const signatureJSON of sortedSignatures) { const keyId = signatureJSON.key_id; @@ -146,26 +148,35 @@ async function processCommand(config, chain, options) { isValidCalldata: { pubKey, signature }, }); - if (expectedKeyId !== keyId) { - printError('Signature contains invalid key_id', keyId); + if (prevKeyId !== keyId) { + printError('Signatures do not contain consistent key IDs', keyId); return; } + prevKeyId = keyId; + if (msgHash.toLowerCase() !== expectedMessageHash.toLowerCase()) { printError('Message hash does not equal expected message hash', msgHash); return; } - const validatorAddress = getAddressFromPublicKey(pubKey); + const validatorAddress = getAddressFromPublicKey(pubKey).toLowerCase(); + + if (checkedAddresses.includes(validatorAddress)) { + printError('Duplicate validator address', validatorAddress); + return; + } + + checkedAddresses.push(validatorAddress); const signer = recoverAddress(msgHash, signature); - if (signer.toLowerCase() !== validatorAddress.toLowerCase()) { + if (signer.toLowerCase() !== validatorAddress) { printError('Signature is invalid for the given validator address', validatorAddress); return; } - const validatorWeight = validatorWeights[validatorAddress.toLowerCase()]; + const validatorWeight = validatorWeights[validatorAddress]; if (!validatorWeight) { printError('Validator does not belong to current epoch', validatorAddress); @@ -191,6 +202,7 @@ async function processCommand(config, chain, options) { [validatorAddresses, weights, threshold, batchSignatures], ); + const IAxelarAuth = getContractJSON('IAxelarAuth'); const authAddress = address || contracts.AxelarGateway?.authModule; const auth = new Contract(authAddress, IAxelarAuth.abi, wallet); @@ -208,28 +220,25 @@ async function processCommand(config, chain, options) { return; } - const input = defaultAbiCoder.encode(['bytes', 'bytes'], [message, proof]); - - printInfo('Batch input data for gateway execute function', input); - - break; - } + const input = defaultAbiCoder.encode(['bytes', 'bytes'], [batchData, proof]); - case 'executeBatch': { - const { input } = options; + printInfo('Batch input (data and proof) for gateway execute function', input); - validateParameters({ isValidCalldata: { input } }); + if (execute) { + printInfo('Executing gateway batch on chain', chain.name); - const contractName = 'AxelarGateway'; + const contractName = 'AxelarGateway'; - const gatewayAddress = address || contracts.AxelarGateway?.address; - const gateway = new Contract(gatewayAddress, IAxelarGateway.abi, wallet); + const IAxelarGateway = getContractJSON('IAxelarGateway'); + const gatewayAddress = address || contracts.AxelarGateway?.address; + const gateway = new Contract(gatewayAddress, IAxelarGateway.abi, wallet); - const gasOptions = await getGasOptions(chain, options, contractName); + const gasOptions = await getGasOptions(chain, options, contractName); - const tx = await gateway.execute(input, gasOptions); + const tx = await gateway.execute(input, gasOptions); - await handleTx(tx, chain, gateway, action, 'Executed'); + await handleTx(tx, chain, gateway, action, 'Executed'); + } break; } @@ -251,19 +260,16 @@ if (require.main === module) { addBaseOptions(program, { address: true }); + program.addOption(new Option('--action ', 'signature action').choices(['createBatchData', 'constructBatch'])); program.addOption( - new Option('--action ', 'signature action').choices(['computeMessageHash', 'constructBatch', 'executeBatch']), + new Option('-i, --batchData ', 'batch data to be combined with proof for gateway execute command').env('BATCH_DATA'), ); - program.addOption(new Option('-m, --message ', 'bytes message (validators sign the hash of this message)').env('MESSAGE')); - program.addOption(new Option('-i, --input ', 'batch input consisting of bytes message (data) and bytes proof').env('INPUT')); - program.addOption(new Option('--commandId ', 'gateway command id').env('COMMAND_ID')); program.addOption(new Option('--sourceChain ', 'source chain for contract call').env('SOURCE_CHAIN')); program.addOption(new Option('--sourceAddress ', 'source address for contract call').env('SOURCE_ADDRESS')); program.addOption(new Option('--contractAddress ', 'contract address on current chain').env('CONTRACT_ADDRESS')); program.addOption(new Option('--payloadHash ', 'payload hash').env('PAYLOAD_HASH')); - program.addOption(new Option('--sourceTxHash ', 'source transaction hash').env('SOURCE_TX_HASH')); - program.addOption(new Option('--sourceEventIndex ', 'source event index').env('SOURCE_EVENT_INDEX')); + program.addOption(new Option('--execute', 'whether or not to immediately execute the batch').env('EXECUTE')); program.action((options) => { main(options); diff --git a/evm/utils.js b/evm/utils.js index 859938d151..310e095f09 100644 --- a/evm/utils.js +++ b/evm/utils.js @@ -5,8 +5,18 @@ const { ethers } = require('hardhat'); const { ContractFactory, Contract, - utils: { computeAddress, getContractAddress, keccak256, isAddress, getCreate2Address, defaultAbiCoder, isHexString, hexZeroPad }, - constants: { AddressZero, HashZero }, + utils: { + computeAddress, + getContractAddress, + keccak256, + isAddress, + getCreate2Address, + defaultAbiCoder, + isHexString, + hexlify, + randomBytes, + }, + constants: { AddressZero }, getDefaultProvider, BigNumber, } = ethers; @@ -49,6 +59,14 @@ const IDeployer = require('@axelar-network/axelar-gmp-sdk-solidity/interfaces/ID const { exec } = require('child_process'); const { verifyContract } = require(`${__dirname}/../axelar-chains-config`); +const getSaltFromKey = (key) => { + return keccak256(defaultAbiCoder.encode(['string'], [key.toString()])); +}; + +const getRandomBytes32 = () => { + return hexlify(randomBytes(32)); +}; + const deployCreate = async (wallet, contractJson, args = [], options = {}, verifyOptions = null, chain = {}) => { const factory = new ContractFactory(contractJson.abi, contractJson.bytecode, wallet); From 24351f2c0ded1401659e2e5119e456a88d39b527 Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Fri, 2 Feb 2024 11:38:51 -0500 Subject: [PATCH 08/10] feat: address commandId comment --- evm/combine-signatures.js | 31 +++++++++++++++++++++++-------- evm/utils.js | 22 +--------------------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/evm/combine-signatures.js b/evm/combine-signatures.js index c6c2b5a6e9..a10fb254ed 100644 --- a/evm/combine-signatures.js +++ b/evm/combine-signatures.js @@ -5,8 +5,8 @@ const fs = require('fs'); const path = require('path'); const { getDefaultProvider, - utils: { computePublicKey, keccak256, getAddress, arrayify, defaultAbiCoder, hashMessage, recoverAddress }, - constants: { HashZero }, + utils: { computePublicKey, keccak256, getAddress, arrayify, defaultAbiCoder, hashMessage, recoverAddress, hexZeroPad, hexlify }, + constants: { HashZero, MaxUint256 }, Contract, } = ethers; const { Command, Option } = require('commander'); @@ -18,12 +18,12 @@ const { printError, validateParameters, getContractJSON, - getRandomBytes32, getEVMAddresses, } = require('./utils'); const { handleTx } = require('./its'); const { getWallet } = require('./sign-utils'); const { addBaseOptions } = require('./cli-utils'); +const IAxelarGateway = getContractJSON('IAxelarGateway'); function readSignatures() { const signaturesDir = path.join(__dirname, '../signatures'); @@ -52,6 +52,22 @@ function getAddressFromPublicKey(publicKey) { return getAddress('0x' + addressHash.slice(-40)); } +async function getCommandId(gateway) { + let currentValue = MaxUint256; + + while (true) { + const isCommandIdExecuted = await gateway.isCommandExecuted(hexZeroPad(hexlify(currentValue), 32)); + + if (!isCommandIdExecuted) { + break; + } + + currentValue = currentValue.sub(1); + } + + return hexZeroPad(hexlify(currentValue), 32); +} + async function processCommand(config, chain, options) { const { address, action, privateKey } = options; @@ -67,6 +83,9 @@ async function processCommand(config, chain, options) { const wallet = await getWallet(privateKey, provider, options); await printWalletInfo(wallet); + const gatewayAddress = address || contracts.AxelarGateway?.address; + const gateway = new Contract(gatewayAddress, IAxelarGateway.abi, wallet); + printInfo('Batch Action', action); switch (action) { @@ -76,7 +95,7 @@ async function processCommand(config, chain, options) { const sourceChain = options.sourceChain || 'Axelarnet'; const sourceAddress = options.sourceAddress || 'axelar10d07y265gmmuvt4z0w9aw880jnsr700j7v9daj'; const contractAddress = options.contractAddress || contracts.InterchainGovernance?.address; - const commandID = commandId || getRandomBytes32(); + const commandID = commandId || (await getCommandId(gateway)); validateParameters({ isNonEmptyString: { sourceChain, sourceAddress }, @@ -229,10 +248,6 @@ async function processCommand(config, chain, options) { const contractName = 'AxelarGateway'; - const IAxelarGateway = getContractJSON('IAxelarGateway'); - const gatewayAddress = address || contracts.AxelarGateway?.address; - const gateway = new Contract(gatewayAddress, IAxelarGateway.abi, wallet); - const gasOptions = await getGasOptions(chain, options, contractName); const tx = await gateway.execute(input, gasOptions); diff --git a/evm/utils.js b/evm/utils.js index 310e095f09..e9981001e8 100644 --- a/evm/utils.js +++ b/evm/utils.js @@ -5,17 +5,7 @@ const { ethers } = require('hardhat'); const { ContractFactory, Contract, - utils: { - computeAddress, - getContractAddress, - keccak256, - isAddress, - getCreate2Address, - defaultAbiCoder, - isHexString, - hexlify, - randomBytes, - }, + utils: { computeAddress, getContractAddress, keccak256, isAddress, getCreate2Address, defaultAbiCoder, isHexString }, constants: { AddressZero }, getDefaultProvider, BigNumber, @@ -63,10 +53,6 @@ const getSaltFromKey = (key) => { return keccak256(defaultAbiCoder.encode(['string'], [key.toString()])); }; -const getRandomBytes32 = () => { - return hexlify(randomBytes(32)); -}; - const deployCreate = async (wallet, contractJson, args = [], options = {}, verifyOptions = null, chain = {}) => { const factory = new ContractFactory(contractJson.abi, contractJson.bytecode, wallet); @@ -1095,10 +1081,4 @@ module.exports = { getSaltFromKey, getDeployOptions, isValidChain, - relayTransaction, - getDeploymentTx, - getWeightedSigners, - getQualifiedContractName, - verifyContractByName, - isConsensusChain, }; From 67329457becc96d775b4877c1d1ad431bd423ad4 Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Wed, 7 Feb 2024 09:23:19 -0500 Subject: [PATCH 09/10] feat: address comments --- evm/README.md | 25 +++++++++++++++++++++++++ evm/combine-signatures.js | 4 +++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/evm/README.md b/evm/README.md index f62f06f2ad..f729dac051 100644 --- a/evm/README.md +++ b/evm/README.md @@ -262,6 +262,19 @@ node evm/verify-contract.js --help The following script is for emergency situations where we need to ask validators to manually sign gateway approvals. These validator signatures must be collected and stored within separate files in the signatures folder in the root directory. The script will then combine these signatures into a gateway batch and optionally execute this batch on the Axelar gateway contract on the specified chain. +Ensure that the lcd endpoint has been provided for the specified environment in the chains config file under `axelar`: + +```json +"axelar": { + "id": "Axelarnet", + "axelarId": "Axelarnet", + "rpc": "", + "lcd": "", + "grpc": "", + "tokenSymbol": "AXL" + } +``` + Below are instructions on how to utilize this script: 1. Construct the gateway batch data by passing in the payload hash gotten from running the `governance.js` script. @@ -277,6 +290,18 @@ Below are instructions on how to utilize this script: - contractAddress = interchain governance address - commandId = random bytes32 value + Example payload hash generation via governance script for Axelar gateway upgrade. For more information see [here](https://github.com/axelarnetwork/axelar-contract-deployments/tree/main/evm#governance): + + ```bash + node evm/governance.js --targetContractName AxelarGateway --action upgrade --proposalAction schedule --date 2023-12-13T16:50:00 --file proposal.json + ``` + + Example output: + + ```bash + Axelar Proposal payload hash: 0x485cc20898a043994a38827c04230ed642db5362e7d412b84b909b0504862024 + ``` + 2. Running the command above will output the raw batch data, the data hash, and the vald sign command that should be used to generate validator signatures. Here is an example output: ```bash diff --git a/evm/combine-signatures.js b/evm/combine-signatures.js index a10fb254ed..1969e6520c 100644 --- a/evm/combine-signatures.js +++ b/evm/combine-signatures.js @@ -97,6 +97,8 @@ async function processCommand(config, chain, options) { const contractAddress = options.contractAddress || contracts.InterchainGovernance?.address; const commandID = commandId || (await getCommandId(gateway)); + printInfo('Command ID', commandID); + validateParameters({ isNonEmptyString: { sourceChain, sourceAddress }, isValidAddress: { contractAddress }, @@ -212,7 +214,7 @@ async function processCommand(config, chain, options) { } if (totalWeight < threshold) { - printError('Total signer weight less than threshold', totalWeight); + printError(`Total signer weight ${totalWeight} less than threshold`, threshold); return; } From 56fb574ad3ad92a12db3dbd24c919f4baf028eea Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Tue, 13 Feb 2024 02:18:28 -0500 Subject: [PATCH 10/10] feat: address comments --- evm/combine-signatures.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/evm/combine-signatures.js b/evm/combine-signatures.js index 1969e6520c..130f7bf047 100644 --- a/evm/combine-signatures.js +++ b/evm/combine-signatures.js @@ -154,14 +154,14 @@ async function processCommand(config, chain, options) { const expectedMessageHash = hashMessage(arrayify(keccak256(batchData))); - let prevKeyId = sortedSignatures[0].key_id; + const prevKeyId = sortedSignatures[0].key_id; let totalWeight = 0; for (const signatureJSON of sortedSignatures) { const keyId = signatureJSON.key_id; - const msgHash = `0x${signatureJSON.msg_hash}`; - const pubKey = `0x${signatureJSON.pub_key}`; - const signature = `0x${signatureJSON.signature}`; + const msgHash = signatureJSON.msg_hash.startsWith('0x') ? signatureJSON.msg_hash : `0x${signatureJSON.msg_hash}`; + const pubKey = signatureJSON.pub_key.startsWith('0x') ? signatureJSON.pub_key : `0x${signatureJSON.pub_key}`; + const signature = signatureJSON.signature.startsWith('0x') ? signatureJSON.signature : `0x${signatureJSON.signature}`; validateParameters({ isNonEmptyString: { keyId }, @@ -174,8 +174,6 @@ async function processCommand(config, chain, options) { return; } - prevKeyId = keyId; - if (msgHash.toLowerCase() !== expectedMessageHash.toLowerCase()) { printError('Message hash does not equal expected message hash', msgHash); return;