[aptos] Aptos cleanup (#994)

* Better errors

* Better variable typing

* Upgrade cli with more straightforward flow

* Remove unnecessary configs and distinguish between deployer contract and signer

* Convert the named-addresses argument to 3 separate options with default values

* Add README
This commit is contained in:
Mohammad Amin Khashkhashi Moghaddam 2023-07-31 11:43:58 +01:00 committed by GitHub
parent 07b01118d2
commit 2782c6bf64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 178 additions and 108 deletions

View File

@ -47,7 +47,7 @@ export class SubmittedWormholeMessage {
constructor(
public emitter: PublicKey,
public sequenceNumber: number,
public cluster: string
public cluster: PythCluster
) {}
/**
@ -107,8 +107,7 @@ export class SubmittedWormholeMessage {
* @param waitingSeconds how long to wait before giving up
*/
async fetchVaa(waitingSeconds: number = 1): Promise<Buffer> {
let rpcUrl =
WORMHOLE_API_ENDPOINT[this.cluster as keyof typeof WORMHOLE_API_ENDPOINT];
let rpcUrl = WORMHOLE_API_ENDPOINT[this.cluster];
let startTime = Date.now();
while (Date.now() - startTime < waitingSeconds * 1000) {
@ -176,6 +175,10 @@ export class WormholeEmitter {
this.cluster = asPythCluster(cluster);
}
public getEmitter() {
return this.wallet.publicKey;
}
async sendMessage(payload: Buffer) {
const provider = new AnchorProvider(
new Connection(getPythClusterApiUrl(this.cluster), "confirmed"),

View File

@ -0,0 +1,74 @@
# Pre-requisites
Install aptos cli with the same version specified in the ci workflows.
All the commands which submit transactions require an environment variable for the private key to be set.
Depending on the network, this can be either `APTOS_LOCALNET_KEY`, `APTOS_TESTNET_KEY` or `APTOS_MAINNET_KEY`.
# Deploying from scratch
In addition to the wormhole dependency we depend on the deployer contract that facilitates the ownership of package upgrade
capability. You can read more about it [here](https://github.com/wormhole-foundation/wormhole/blob/5255e933d68629f0643207b0f9d3fa797af5cbf7/aptos/deployer/sources/deployer.move).
Assuming the wormhole and deployer contracts are already deployed, we can deploy the pyth oracle with the following command:
```bash
npm run cli deploy-pyth ../contracts <seed> -n testnet
```
`seed` can be any random string that is used for determining a specific contract address based on the seed value and the signer address.
You can manually specify the address of wormhole and deployer contracts with `--wormhole` and `--deployer` flags.
This requires the addresses to be empty in the `Move.toml` file for the pyth package:
```toml
[addresses]
pyth = "_"
deployer = "_"
wormhole = "_"
```
### Initializing pyth
You can run the following to initialize the pyth contract, the following is a sample (testnet) config:
```bash
npm run cli init-pyth <seed> -n testnet \
--stale-price-threshold 60 \
--update-fee 1 \
--governance-emitter-chain-id 1 \
--governance-emitter-address 63278d271099bfd491951b3e648f08b1c71631e4a53674ad43e8f9f98068c385 \
--data-source-chain-ids 1 \
--data-source-chain-ids 26 \
--data-source-chain-ids 26 \
--data-source-emitter-addresses f346195ac02f37d60d4db8ffa6ef74cb1be3550047543a4a9ee9acf4d78697b0 \
--data-source-emitter-addresses a27839d641b07743c0cb5f68c51f8cd31d2c0762bec00dc6fcd25433ef1ab5b6 \
--data-source-emitter-addresses e101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71
```
Note that the `data-source-chain-ids` are paired with `data-source-emitter-addresses` and their order matters.
# Upgrade process:
The following steps are needed to upgrade our aptos contracts:
- Generate the hash for the new contract build
- Create a governance proposal, proposing the aptos package to be upgraded to this specific hash
- Approve and execute the governance proposal
- Run the upgrade transaction and publish the new package
## Generating the new contract hash:
Run the following command to generate the new hash, this will assume the default deployed addresses of deployer, wormhole, and pyth, but you can override them if necessary.
```bash
npm run cli hash-contracts ../contracts
```
## Upgrading the contract
To upgrade the contract after the governance vaa was executed run:
```bash
npm run cli upgrade ../contracts
```

View File

@ -5,6 +5,7 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"cli": "ts-node src/cli.ts",
"build": "npx tsc && pkg . --targets node14-linux-x64 --output build/pyth"
},
"author": "",

View File

@ -2,5 +2,6 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { builder } from "./commands/aptos";
yargs(hideBin(process.argv)).commandDir("commands").strict().argv;
builder(yargs(hideBin(process.argv))).argv;

View File

@ -1,6 +1,6 @@
import { Argv } from "yargs";
import { spawnSync } from "child_process";
import { BCS, AptosClient, AptosAccount, TxnBuilderTypes } from "aptos";
import type { CommandBuilder } from "yargs";
import { AptosAccount, AptosClient, BCS, TxnBuilderTypes } from "aptos";
import fs from "fs";
import sha3 from "js-sha3";
import { ethers } from "ethers";
@ -14,18 +14,29 @@ interface Network {
endpoint: string;
// Private key of the network
key: string | undefined;
// Address of the Pyth deployer contract
deployer: string;
// The Pyth deployer contract seed used to generate the derived address of the Pyth contract
pythDeployerSeed: string;
}
const network = {
const NETWORK_OPTION = {
alias: "n",
describe: "network",
type: "string",
choices: [LOCALNET, TESTNET, MAINNET],
required: true,
demandOption: true,
} as const;
const DEPLOYER_OPTION = {
describe: "deployer contract address deployed in the network",
type: "string",
default: "0xb31e712b26fd295357355f6845e77c888298636609e93bc9b05f0f604049f434",
} as const;
const WORMHOLE_OPTION = {
describe: "wormhole contract address deployed in the network",
type: "string",
default: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
} as const;
const PYTH_OPTION = {
describe: "pyth contract address deployed in the network",
type: "string",
default: "0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387",
} as const;
interface Package {
@ -45,9 +56,6 @@ const networks = new Map<string, Network>([
{
key: process.env["APTOS_LOCALNET_KEY"],
endpoint: "http://0.0.0.0:8080",
deployer:
"0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b",
pythDeployerSeed: "pyth",
},
],
[
@ -55,10 +63,6 @@ const networks = new Map<string, Network>([
{
key: process.env["APTOS_TESTNET_KEY"],
endpoint: "https://fullnode.testnet.aptoslabs.com/v1",
deployer:
"0xb31e712b26fd295357355f6845e77c888298636609e93bc9b05f0f604049f434",
// A Wormhole redeploy meant we had to use different seeds for testnet and mainnet
pythDeployerSeed: "pyth-a8d0d",
},
],
[
@ -66,16 +70,10 @@ const networks = new Map<string, Network>([
{
key: process.env["APTOS_MAINNET_KEY"],
endpoint: "https://fullnode.mainnet.aptoslabs.com/v1",
deployer:
"0xb31e712b26fd295357355f6845e77c888298636609e93bc9b05f0f604049f434",
pythDeployerSeed: "pyth-a8d0d",
},
],
]);
export const command: string = "aptos <command>";
export const builder: CommandBuilder = (yargs) =>
export const builder: (args: Argv<any>) => Argv<any> = (yargs) =>
yargs
.command(
"deploy <package-dir> <account>",
@ -85,7 +83,7 @@ export const builder: CommandBuilder = (yargs) =>
.positional("package-dir", { type: "string" })
.positional("account", { type: "string" })
.option("named-addresses", { type: "string" })
.option("network", network);
.option("network", NETWORK_OPTION);
},
async (argv) => {
const artefact = serializePackage(
@ -104,27 +102,40 @@ export const builder: CommandBuilder = (yargs) =>
}
)
.command(
"deploy-resource <package-dir> <seed>",
"Deploy a package using a resource account",
"deploy-pyth <package-dir> <seed>",
"Deploy the pyth package using a resource account.",
(yargs) => {
return yargs
.positional("package-dir", { type: "string" })
.positional("seed", { type: "string" })
.option("named-addresses", { type: "string" })
.option("network", network);
.option("deployer", DEPLOYER_OPTION)
.option("wormhole", WORMHOLE_OPTION)
.option("network", NETWORK_OPTION);
},
async (argv) => {
const artefact = serializePackage(
buildPackage(argv["package-dir"]!, argv["named-addresses"])
const sender = getSender(argv.network);
const derivedAddress = generateDerivedAddress(
sender.address().toString(),
argv.seed!
);
const namedAddresses = `wormhole=${argv.wormhole},deployer=${argv.deployer},pyth=0x${derivedAddress}`;
console.log("Building the package with the following named addresses:");
console.log(`Wormhole=${argv.wormhole}`);
console.log(`Deployer=${argv.deployer}`);
console.log(`Pyth=${derivedAddress}`);
const artifact = serializePackage(
buildPackage(argv["package-dir"]!, namedAddresses)
);
const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
TxnBuilderTypes.EntryFunction.natural(
networks.get(argv.network)!.deployer + "::deployer",
argv.deployer + "::deployer",
"deploy_derived",
[],
[
artefact.meta,
artefact.bytecodes,
artifact.meta,
artifact.bytecodes,
BCS.bcsSerializeBytes(Buffer.from(argv["seed"]!, "ascii")),
]
)
@ -134,20 +145,15 @@ export const builder: CommandBuilder = (yargs) =>
}
)
.command(
"derived-address <seed>",
"Generate the derived address for the given deployer seed",
"derived-address <seed> <signer>",
"Generate the derived address for the given seed and sender address",
(yargs) => {
return yargs
.positional("seed", { type: "string" })
.option("network", network);
.positional("seed", { type: "string", demandOption: true })
.positional("signer", { type: "string", demandOption: true });
},
async (argv) => {
console.log(
generateDerivedAddress(
networks.get(argv.network)!.deployer,
argv.seed!
)
);
console.log(generateDerivedAddress(argv.signer, argv.seed));
}
)
.command(
@ -155,29 +161,29 @@ export const builder: CommandBuilder = (yargs) =>
"Init Wormhole core contract",
(yargs) => {
return yargs
.option("network", network)
.option("network", NETWORK_OPTION)
.option("chain-id", {
describe: "Chain id",
type: "number",
default: 22,
required: false,
demandOption: false,
})
.option("governance-chain-id", {
describe: "Governance chain id",
type: "number",
default: 1,
required: false,
demandOption: false,
})
.option("governance-address", {
describe: "Governance address",
type: "string",
default:
"0x0000000000000000000000000000000000000000000000000000000000000004",
required: false,
demandOption: false,
})
.option("guardian-address", {
alias: "g",
required: true,
demandOption: true,
describe: "Initial guardian's address",
type: "string",
});
@ -202,8 +208,9 @@ export const builder: CommandBuilder = (yargs) =>
BCS.bcsSerializeBytes(Buffer.from(governance_address, "hex")),
guardian_addresses_serializer.getBytes(),
];
const sender = getSender(argv.network);
const wormholeAddress = generateDerivedAddress(
networks.get(argv.network)!.deployer!,
sender.address().hex(),
"wormhole"
);
const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
@ -219,40 +226,41 @@ export const builder: CommandBuilder = (yargs) =>
}
)
.command(
"init-pyth",
"init-pyth <seed>",
"Init Pyth contract",
(yargs) => {
return yargs
.option("network", network)
.positional("seed", { type: "string", demandOption: true })
.option("network", NETWORK_OPTION)
.option("stale-price-threshold", {
describe: "Stale price threshold",
type: "number",
required: true,
demandOption: true,
})
.option("governance-emitter-chain-id", {
describe: "Governance emitter chain id",
type: "number",
required: true,
demandOption: true,
})
.option("governance-emitter-address", {
describe: "Governance emitter address",
type: "string",
required: true,
demandOption: true,
})
.option("update-fee", {
describe: "Update fee",
type: "number",
required: true,
demandOption: true,
})
.option("data-source-chain-ids", {
describe: "Data source chain IDs",
type: "array",
required: true,
demandOption: true,
})
.option("data-source-emitter-addresses", {
describe: "Data source emitter addresses",
type: "array",
required: true,
demandOption: true,
});
},
async (argv) => {
@ -266,8 +274,8 @@ export const builder: CommandBuilder = (yargs) =>
dataSourceChainIdsSerializer.serializeU32AsUleb128(
argv["data-source-chain-ids"].length
);
argv["data-source-chain-ids"].forEach((chain_id) =>
dataSourceChainIdsSerializer.serializeU64(chain_id as number)
argv["data-source-chain-ids"].forEach((chain_id: number) =>
dataSourceChainIdsSerializer.serializeU64(chain_id)
);
const dataSourceEmitterAddressesSerializer = new BCS.Serializer();
@ -289,9 +297,10 @@ export const builder: CommandBuilder = (yargs) =>
dataSourceEmitterAddressesSerializer.getBytes(),
BCS.bcsSerializeUint64(update_fee),
];
const sender = getSender(argv.network);
const pythAddress = generateDerivedAddress(
networks.get(argv.network)!.deployer!,
networks.get(argv.network)!.pythDeployerSeed!
sender.address().hex(),
argv.seed
);
const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
TxnBuilderTypes.EntryFunction.natural(
@ -307,17 +316,20 @@ export const builder: CommandBuilder = (yargs) =>
)
.command(
"hash-contracts <package-dir>",
"Hash contract bytecodes for upgrade",
"Hash contract bytecodes for upgrade, the named addresses should be the same as the currently deployed ones",
(yargs) => {
return yargs
.positional("package-dir", {
type: "string",
required: true,
})
.option("named-addresses", { type: "string" });
.option("deployer", DEPLOYER_OPTION)
.option("wormhole", WORMHOLE_OPTION)
.option("pyth", PYTH_OPTION);
},
(argv) => {
const p = buildPackage(argv["package-dir"]!, argv["named-addresses"]);
const namedAddresses = `wormhole=${argv.wormhole},deployer=${argv.deployer},pyth=${argv.pyth}`;
const p = buildPackage(argv["package-dir"]!, namedAddresses);
const b = serializePackage(p);
console.log(Buffer.from(b.codeHash).toString("hex"));
}
@ -331,18 +343,18 @@ export const builder: CommandBuilder = (yargs) =>
type: "string",
required: true,
})
.option("network", network)
.option("named-addresses", { type: "string" });
.option("network", NETWORK_OPTION)
.option("deployer", DEPLOYER_OPTION)
.option("wormhole", WORMHOLE_OPTION)
.option("pyth", PYTH_OPTION);
},
async (argv) => {
const namedAddresses = `wormhole=${argv.wormhole},deployer=${argv.deployer},pyth=${argv.pyth}`;
const artefact = serializePackage(
buildPackage(argv["package-dir"]!, argv["named-addresses"])
buildPackage(argv["package-dir"]!, namedAddresses)
);
let pythAddress = generateDerivedAddress(
networks.get(argv.network)!.deployer!,
networks.get(argv.network)!.pythDeployerSeed!
);
let pythAddress = argv.pyth;
const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
TxnBuilderTypes.EntryFunction.natural(
`${pythAddress}::contract_upgrade`,
@ -355,42 +367,24 @@ export const builder: CommandBuilder = (yargs) =>
await executeTransaction(argv.network, txPayload);
}
)
.command(
"execute-governance <vaa-bytes>",
"Execute a governance instruction on the Pyth contract",
(_yargs) => {
return yargs
.positional("vaa-bytes", {
type: "string",
})
.option("network", network);
},
async (argv) => {
let pythAddress = generateDerivedAddress(
networks.get(argv.network)!.deployer!,
networks.get(argv.network)!.pythDeployerSeed!
);
const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
TxnBuilderTypes.EntryFunction.natural(
`${pythAddress}::governance`,
"execute_governance_instruction",
[],
[BCS.bcsSerializeBytes(Buffer.from(argv.vaaBytes!, "hex"))]
)
);
.demandCommand();
await executeTransaction(argv.network, txPayload);
}
function getSender(network: string) {
if (networks.get(network)!.key === undefined) {
throw new Error(
`No key for network ${network}. Please set the APTOS_${network.toUpperCase()}_KEY environment variable.`
);
}
return new AptosAccount(
new Uint8Array(Buffer.from(networks.get(network)!.key!, "hex"))
);
}
async function executeTransaction(
network: string,
txPayload: TxnBuilderTypes.TransactionPayloadEntryFunction
) {
const client = new AptosClient(networks.get(network)!.endpoint);
const sender = new AptosAccount(
new Uint8Array(Buffer.from(networks.get(network)!.key!, "hex"))
);
const sender = getSender(network);
console.log(
await client.generateSignSubmitWaitForTransaction(sender, txPayload, {
maxGasAmount: BigInt(30000),
@ -413,15 +407,12 @@ function hexStringToByteArray(hexString: string) {
return byteArray;
}
function generateDerivedAddress(
deployer_address: string,
seed: string
): string {
function generateDerivedAddress(signer_address: string, seed: string): string {
let derive_resource_account_scheme = Buffer.alloc(1);
derive_resource_account_scheme.writeUInt8(255);
return sha3.sha3_256(
Buffer.concat([
hexStringToByteArray(deployer_address),
hexStringToByteArray(signer_address),
Buffer.from(seed, "ascii"),
derive_resource_account_scheme,
])