From bc042aa2b53753e20763ec26937d39ef88143499 Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Wed, 17 Jan 2024 03:31:59 -0500 Subject: [PATCH 1/7] 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 7d727be9..e1424f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,5 @@ local.json keys.json temp-arguments.js + +signatures/ diff --git a/evm/combine-signatures.js b/evm/combine-signatures.js new file mode 100644 index 00000000..c9bb18ba --- /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 08a5f2af318bd65a7ada7dfba6cc71e034e016da Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Wed, 24 Jan 2024 00:22:10 -0500 Subject: [PATCH 2/7] feat: validate proof with axelar auth weighted --- evm/combine-signatures.js | 182 ++++++++++++++++++++++++-------------- evm/utils.js | 5 ++ 2 files changed, 122 insertions(+), 65 deletions(-) diff --git a/evm/combine-signatures.js b/evm/combine-signatures.js index c9bb18ba..f4e2dbfc 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); }); diff --git a/evm/utils.js b/evm/utils.js index 52003cfc..5a3eff5e 100644 --- a/evm/utils.js +++ b/evm/utils.js @@ -32,6 +32,10 @@ const getSaltFromKey = (key) => { return keccak256(defaultAbiCoder.encode(['string'], [key.toString()])); }; +function getRandomBytes32() { + return keccak256(defaultAbiCoder.encode(['uint256'], [Math.floor(new Date().getTime() * Math.random())])); +} + const deployCreate = async (wallet, contractJson, args = [], options = {}, verifyOptions = null, chain = {}) => { const factory = new ContractFactory(contractJson.abi, contractJson.bytecode, wallet); @@ -1136,4 +1140,5 @@ module.exports = { getSaltFromKey, getDeployOptions, isValidChain, + getRandomBytes32, }; From 46b168b3e2c1332dbc5cf16efc68de3a15dd02b0 Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Wed, 24 Jan 2024 11:32:25 -0500 Subject: [PATCH 3/7] 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 f4e2dbfc..486c120b 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 51f5fc92fc50982226e95ccfa95ab0d8ef7a26eb Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Fri, 2 Feb 2024 04:19:22 -0500 Subject: [PATCH 4/7] feat: address comments --- evm/README.md | 69 ++++++++++++++++++++++++++++ evm/combine-signatures.js | 94 +++++++++++++++++++++------------------ evm/utils.js | 18 ++++++-- 3 files changed, 133 insertions(+), 48 deletions(-) diff --git a/evm/README.md b/evm/README.md index 1d9b2ec8..d4978743 100644 --- a/evm/README.md +++ b/evm/README.md @@ -181,3 +181,72 @@ To get details of options provided in the command run: ```bash node evm/verify-contract.js --help ``` + +## Combine Validator Signatures + +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. + +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. + + ```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] + ``` + + 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 486c120b..c6c2b5a6 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 8be26327..d5dc016b 100644 --- a/evm/utils.js +++ b/evm/utils.js @@ -4,7 +4,17 @@ const { ethers } = require('hardhat'); const { ContractFactory, Contract, - utils: { computeAddress, getContractAddress, keccak256, isAddress, getCreate2Address, defaultAbiCoder, isHexString }, + utils: { + computeAddress, + getContractAddress, + keccak256, + isAddress, + getCreate2Address, + defaultAbiCoder, + isHexString, + hexlify, + randomBytes, + }, constants: { AddressZero }, getDefaultProvider, } = ethers; @@ -32,9 +42,9 @@ const getSaltFromKey = (key) => { return keccak256(defaultAbiCoder.encode(['string'], [key.toString()])); }; -function getRandomBytes32() { - return keccak256(defaultAbiCoder.encode(['uint256'], [Math.floor(new Date().getTime() * Math.random())])); -} +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 79cadd9d95c24935e00005cba6a5cf0346cfe2ca Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Fri, 2 Feb 2024 11:38:51 -0500 Subject: [PATCH 5/7] feat: address commandId comment --- evm/combine-signatures.js | 31 +++++++++++++++++++++++-------- evm/utils.js | 17 +---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/evm/combine-signatures.js b/evm/combine-signatures.js index c6c2b5a6..a10fb254 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 d5dc016b..afb22c5f 100644 --- a/evm/utils.js +++ b/evm/utils.js @@ -4,17 +4,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, } = ethers; @@ -42,10 +32,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); @@ -1151,5 +1137,4 @@ module.exports = { getSaltFromKey, getDeployOptions, isValidChain, - getRandomBytes32, }; From 0c2c81e2c7386dcd2902c8ed8d90588d47128aa0 Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Wed, 7 Feb 2024 09:23:19 -0500 Subject: [PATCH 6/7] 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 d4978743..2daf35a3 100644 --- a/evm/README.md +++ b/evm/README.md @@ -186,6 +186,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. @@ -201,6 +214,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 a10fb254..1969e652 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 2341216bfc71eea527efe116ee2c2d5b719a9580 Mon Sep 17 00:00:00 2001 From: Dean Amiel Date: Tue, 13 Feb 2024 02:18:28 -0500 Subject: [PATCH 7/7] 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 1969e652..130f7bf0 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;