Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add key ID as optional parameter to combine signatures script #172

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,5 @@ local.json
keys.json

temp-arguments.js

signatures/
94 changes: 94 additions & 0 deletions evm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,97 @@ 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.

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.

```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

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
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.
295 changes: 295 additions & 0 deletions evm/combine-signatures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
'use strict';

const { ethers } = require('hardhat');
const fs = require('fs');
const path = require('path');
const {
getDefaultProvider,
utils: { computePublicKey, keccak256, getAddress, arrayify, defaultAbiCoder, hashMessage, recoverAddress, hexZeroPad, hexlify },
constants: { HashZero, MaxUint256 },
Contract,
} = ethers;
const { Command, Option } = require('commander');
const {
mainProcessor,
printInfo,
printWalletInfo,
getGasOptions,
printError,
validateParameters,
getContractJSON,
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');
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(`0x${uncompressedPublicKey.slice(4)}`);

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;

const contracts = chain.contracts;

if (address) {
validateParameters({ isValidAddress: { address } });
}

const rpc = chain.rpc;
const provider = getDefaultProvider(rpc);

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) {
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 || (await getCommandId(gateway));

printInfo('Command ID', commandID);

validateParameters({
isNonEmptyString: { sourceChain, sourceAddress },
isValidAddress: { contractAddress },
isKeccak256Hash: { commandID, payloadHash },
});

const chainId = chain.chainId;
const command = 'approveContractCall';
const params = defaultAbiCoder.encode(
['string', 'string', 'address', 'bytes32', 'bytes32', 'uint256'],
[sourceChain, sourceAddress, contractAddress, payloadHash, HashZero, 0],
);

const data = defaultAbiCoder.encode(
['uint256', 'bytes32[]', 'string[]', 'bytes[]'],
[chainId, [commandID], [command], [params]],
);

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 { batchData, execute } = options;

validateParameters({ isValidCalldata: { batchData } });

const { addresses: validatorAddresses, weights, threshold } = 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 checkedAddresses = [];

const expectedMessageHash = hashMessage(arrayify(keccak256(batchData)));

const prevKeyId = sortedSignatures[0].key_id;
let totalWeight = 0;

for (const signatureJSON of sortedSignatures) {
const keyId = signatureJSON.key_id;
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 },
isKeccak256Hash: { msgHash },
isValidCalldata: { pubKey, signature },
});

if (prevKeyId !== keyId) {
printError('Signatures do not contain consistent key IDs', keyId);
return;
}

if (msgHash.toLowerCase() !== expectedMessageHash.toLowerCase()) {
printError('Message hash does not equal expected message hash', msgHash);
return;
}

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) {
printError('Signature is invalid for the given validator address', validatorAddress);
return;
}

const validatorWeight = validatorWeights[validatorAddress];

if (!validatorWeight) {
printError('Validator does not belong to current epoch', validatorAddress);
return;
}

totalWeight += validatorWeight;

batchSignatures.push(signature);

if (totalWeight >= threshold) {
break;
}
}

if (totalWeight < threshold) {
printError(`Total signer weight ${totalWeight} less than threshold`, threshold);
return;
}

const proof = defaultAbiCoder.encode(
['address[]', 'uint256[]', 'uint256', 'bytes[]'],
[validatorAddresses, weights, threshold, batchSignatures],
);

const IAxelarAuth = getContractJSON('IAxelarAuth');
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'], [batchData, proof]);

printInfo('Batch input (data and proof) for gateway execute function', input);

if (execute) {
printInfo('Executing gateway batch on chain', chain.name);

const contractName = 'AxelarGateway';

const gasOptions = await getGasOptions(chain, options, contractName);

const tx = await gateway.execute(input, gasOptions);

await handleTx(tx, chain, gateway, action, 'Executed');
}

break;
}

default: {
throw new Error(`Unknown signature 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 <action>', 'signature action').choices(['createBatchData', 'constructBatch']));
program.addOption(
new Option('-i, --batchData <batchData>', 'batch data to be combined with proof for gateway execute command').env('BATCH_DATA'),
);
program.addOption(new Option('--commandId <commandId>', 'gateway command id').env('COMMAND_ID'));
program.addOption(new Option('--sourceChain <sourceChain>', 'source chain for contract call').env('SOURCE_CHAIN'));
program.addOption(new Option('--sourceAddress <sourceAddress>', 'source address for contract call').env('SOURCE_ADDRESS'));
program.addOption(new Option('--contractAddress <contractAddress>', 'contract address on current chain').env('CONTRACT_ADDRESS'));
program.addOption(new Option('--payloadHash <payloadHash>', 'payload hash').env('PAYLOAD_HASH'));
program.addOption(new Option('--execute', 'whether or not to immediately execute the batch').env('EXECUTE'));
program.addOption(new Option('--keyID <keyID>', 'key ID for operators that have signed the message').env('KEY_ID'));

program.action((options) => {
main(options);
});

program.parse();
}
Loading