test: accountant e2e

This commit is contained in:
Evan Gray 2023-01-27 20:48:19 +00:00 committed by Evan Gray
parent cca154baf6
commit 7f9a03254a
10 changed files with 17410 additions and 9379 deletions

View File

@ -280,7 +280,19 @@ def build_node_yaml():
"--wormchainWS", "--wormchainWS",
"ws://wormchain:26657/websocket", "ws://wormchain:26657/websocket",
"--wormchainLCD", "--wormchainLCD",
"http://wormchain:1317" "http://wormchain:1317",
"--wormchainURL",
"wormchain:9090",
"--wormchainKeyPath",
"/tmp/mounted-keys/wormchain/wormchainKey",
"--wormchainKeyPassPhrase",
"test0000",
"--accountantWS",
"http://wormchain:26657",
"--accountantContract",
"wormhole14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9srrg465",
"--accountantCheckEnabled",
"true"
] ]
return encode_yaml_stream(node_yaml_with_replicas) return encode_yaml_stream(node_yaml_with_replicas)
@ -303,7 +315,7 @@ if algorand:
if aptos: if aptos:
guardian_resource_deps = guardian_resource_deps + ["aptos"] guardian_resource_deps = guardian_resource_deps + ["aptos"]
if wormchain: if wormchain:
guardian_resource_deps = guardian_resource_deps + ["wormchain"] guardian_resource_deps = guardian_resource_deps + ["wormchain", "wormchain-deploy"]
if sui: if sui:
guardian_resource_deps = guardian_resource_deps + ["sui"] guardian_resource_deps = guardian_resource_deps + ["sui"]
@ -571,6 +583,12 @@ if ci_tests:
trigger_mode = trigger_mode, trigger_mode = trigger_mode,
resource_deps = [], # testing/spydk.sh handles waiting for spy, not having deps gets the build earlier resource_deps = [], # testing/spydk.sh handles waiting for spy, not having deps gets the build earlier
) )
k8s_resource(
"accountant-ci-tests",
labels = ["ci"],
trigger_mode = trigger_mode,
resource_deps = ["const-gen"], # uses devnet-consts.json, but wormchain/contracts/tools/test_accountant.sh handles waiting for guardian, not having deps gets the build earlier
)
if terra_classic: if terra_classic:
docker_build( docker_build(
@ -822,7 +840,7 @@ if wormchain:
k8s_resource( k8s_resource(
"wormchain-deploy", "wormchain-deploy",
resource_deps = ["wormchain"], resource_deps = ["const-gen", "wormchain"],
labels = ["wormchain"], labels = ["wormchain"],
trigger_mode = trigger_mode, trigger_mode = trigger_mode,
) )

View File

@ -47,3 +47,28 @@ spec:
- "/app/testing/success" - "/app/testing/success"
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 5 periodSeconds: 5
---
kind: Job
apiVersion: batch/v1
metadata:
name: accountant-ci-tests
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: accountant-ci-tests
image: wormchain-deploy
command:
- /bin/sh
- -c
- "bash /app/tools/test_accountant.sh && touch /app/tools/success"
readinessProbe:
exec:
command:
- test
- -e
- "/app/accountant/success"
initialDelaySeconds: 5
periodSeconds: 5

View File

@ -6,6 +6,7 @@
| Test ERC20 | ETH | 0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A | Tokens minted to Test Wallet | | Test ERC20 | ETH | 0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A | Tokens minted to Test Wallet |
| Test NFT | ETH | 0x5b9b42d6e4B2e4Bf8d42Eba32D46918e10899B66 | One minted to Test Wallet | | Test NFT | ETH | 0x5b9b42d6e4B2e4Bf8d42Eba32D46918e10899B66 | One minted to Test Wallet |
| Test WETH | ETH | 0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E | Tokens minted to Test Wallet | | Test WETH | ETH | 0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E | Tokens minted to Test Wallet |
| Test ERC20 GA | ETH | 0xf19A2A01B70519f67ADb309a994Ec8c69A967E8b | Tokens minted to Test Wallet 9 |
| Bridge Core | ETH | 0xC89Ce4735882C9F0f0FE26686c53074E09B0D550 | | | Bridge Core | ETH | 0xC89Ce4735882C9F0f0FE26686c53074E09B0D550 | |
| Token Bridge | ETH | 0x0290FB167208Af455bB137780163b7B7a9a10C16 | | | Token Bridge | ETH | 0x0290FB167208Af455bB137780163b7B7a9a10C16 | |
| NFT Bridge | ETH | 0x26b4afb60d6c903165150c6f0aa14f8016be4aec | | | NFT Bridge | ETH | 0x26b4afb60d6c903165150c6f0aa14f8016be4aec | |
@ -35,7 +36,7 @@ The terra testnet can be used just like a normal localterra network (can be sele
### Algorand ### Algorand
The `admin.py` deployment tool can be used to generate a set of five prefunded accounts using the `--fundDevAccounts` option, with the following addresses and mnemonics: The `admin.py` deployment tool can be used to generate a set of five prefunded accounts using the `--fundDevAccounts` option, with the following addresses and mnemonics:
DEV7AREMQSPWWDDFFJ3A5OIMMDCZN4YT5U2MQBN76Y4J5ERQQ3MWPIHUYA 800M ALGO DEV7AREMQSPWWDDFFJ3A5OIMMDCZN4YT5U2MQBN76Y4J5ERQQ3MWPIHUYA 800M ALGO
provide warfare better filter glory civil help jacket alpha penalty van fiber code upgrade web more curve sauce merit bike satoshi blame orphan absorb modify provide warfare better filter glory civil help jacket alpha penalty van fiber code upgrade web more curve sauce merit bike satoshi blame orphan absorb modify

View File

@ -21,7 +21,7 @@ const interateToStandardTransactionCount = async () => {
to: accounts[0], to: accounts[0],
from: accounts[0], from: accounts[0],
value: 530, value: 530,
}) });
} }
const burnCount = await web3.eth.getTransactionCount(accounts[0], "latest"); const burnCount = await web3.eth.getTransactionCount(accounts[0], "latest");
@ -31,7 +31,7 @@ const interateToStandardTransactionCount = async () => {
return Promise.resolve(); return Promise.resolve();
}; };
module.exports = async function (callback) { module.exports = async function(callback) {
try { try {
const accounts = await web3.eth.getAccounts(); const accounts = await web3.eth.getAccounts();
@ -96,6 +96,25 @@ module.exports = async function (callback) {
throw new Error("unexpected WETH token address"); throw new Error("unexpected WETH token address");
} }
// deploy token contract
const accountantTokenAddress = (
await ERC20.new("Accountant Test Token", "GA")
).address;
const accountantToken = new web3.eth.Contract(
ERC20.abi,
accountantTokenAddress
);
console.log("Accountant test token deployed at: " + accountantTokenAddress);
// mint 1000 units
await accountantToken.methods
.mint(accounts[9], "1000000000000000000000")
.send({
from: accounts[0],
gas: 1000000,
});
callback(); callback();
} catch (e) { } catch (e) {
callback(e); callback(e);

View File

@ -148,6 +148,12 @@
"name": "Wrapped Ether", "name": "Wrapped Ether",
"symbol": "WETH", "symbol": "WETH",
"decimals": 18 "decimals": 18
},
"testGA": {
"address": "0xf19A2A01B70519f67ADb309a994Ec8c69A967E8b",
"name": "Accountant Test Token",
"symbol": "GA",
"decimals": 18
} }
} }
}, },

View File

@ -23,7 +23,7 @@ export const ETH_PRIVATE_KEY7 =
export const ETH_PRIVATE_KEY8 = export const ETH_PRIVATE_KEY8 =
"0x829e924fdf021ba3dbbc4225edfece9aca04b929d6e75613329ca6f1d31c0bb4"; // account 8 - unused "0x829e924fdf021ba3dbbc4225edfece9aca04b929d6e75613329ca6f1d31c0bb4"; // account 8 - unused
export const ETH_PRIVATE_KEY9 = export const ETH_PRIVATE_KEY9 =
"0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773"; // account 9 - unused "0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773"; // account 9 - accountant tests
export const SOLANA_HOST = ci export const SOLANA_HOST = ci
? "http://solana-devnet:8899" ? "http://solana-devnet:8899"
: "http://localhost:8899"; : "http://localhost:8899";

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,32 @@
{ {
"name": "@wormhole-foundation/wormchain-contract-tools", "name": "@wormhole-foundation/wormchain-contract-tools",
"version": "0.0.1", "version": "0.0.1",
"description": "scripts for working with wormchain contracts", "description": "scripts for working with wormchain contracts",
"main": "deploy_wormchain.ts", "main": "deploy_wormchain.ts",
"scripts": { "scripts": {
"deploy-wormchain": "ts-node deploy_wormchain.ts", "deploy-wormchain": "ts-node deploy_wormchain.ts",
"test-wormchain": "ts-node test_wormchain.ts", "test-accountant": "ts-node test_accountant.ts",
"deploy-and-test": "npm run deploy-wormchain && npm run test-wormchain" "test-wormchain": "ts-node test_wormchain.ts",
}, "deploy-and-test": "npm run deploy-wormchain && npm run test-wormchain"
"keywords": [], },
"author": "", "keywords": [],
"dependencies": { "author": "",
"@certusone/wormhole-sdk": "0.9.9", "dependencies": {
"@cosmjs/cosmwasm-stargate": "0.29.5", "@certusone/wormhole-sdk": "0.9.9",
"@wormhole-foundation/wormchain-sdk": "file:../../ts-sdk", "@cosmjs/cosmwasm-stargate": "0.29.5",
"cosmwasm": "1.1.1", "@improbable-eng/grpc-web-node-http-transport": "0.15.0",
"dotenv": "16.0.3", "@wormhole-foundation/wormchain-sdk": "file:../../ts-sdk",
"elliptic": "6.5.4", "cosmwasm": "1.1.1",
"ethers": "5.7.2", "dotenv": "16.0.3",
"js-sha3": "0.8.0", "elliptic": "6.5.4",
"web3-eth-abi": "1.8.1", "ethers": "5.7.2",
"yargs": "17.6.2" "js-sha3": "0.8.0",
}, "web3-eth-abi": "1.8.1",
"devDependencies": { "yargs": "17.6.2"
"@types/elliptic": "6.4.14", },
"ts-node": "10.9.1", "devDependencies": {
"typescript": "4.9.4" "@types/elliptic": "6.4.14",
} "ts-node": "10.9.1",
"typescript": "4.9.4"
}
} }

View File

@ -0,0 +1,4 @@
#!/bin/sh
set -e
while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' guardian:6060/readyz)" != "200" ]]; do sleep 5; done
CI=true npm run test-accountant

View File

@ -0,0 +1,371 @@
import "dotenv/config";
import {
approveEth,
attestFromEth,
CHAIN_ID_BSC,
CHAIN_ID_ETH,
CONTRACTS,
createWrappedOnEth,
getEmitterAddressEth,
getForeignAssetEth,
getSignedVAAWithRetry,
hexToUint8Array,
parseSequenceFromLogEth,
redeemOnEth,
serialiseVAA,
sign,
TokenBridgeTransfer,
transferFromEth,
tryNativeToUint8Array,
uint8ArrayToHex,
VAA,
} from "@certusone/wormhole-sdk";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { ethers } from "ethers";
import * as devnetConsts from "./devnet-consts.json";
import { parseUnits } from "ethers/lib/utils";
if (process.env.INIT_SIGNERS_KEYS_CSV === "undefined") {
let msg = `.env is missing. run "make contracts-tools-deps" to fetch.`;
console.error(msg);
throw msg;
}
// TODO: consider using jest
/*
* Goals:
* 1. Ensure a token can be sent from its origin chain
* 2. Ensure a token can be sent back from a foreign chain
* 3. Ensure spoofed tokens for more than the outstanding amount rejects successfully
* 4. Validate the guardian metrics for each of these cases
* 5. Bonus: Validate the on chain contract state via queries
*/
const ci = !!process.env.CI;
const GUARDIAN_HOST = ci ? "guardian" : "localhost";
const GUARDIAN_RPCS = [`http://${GUARDIAN_HOST}:7071`];
const GUARDIAN_METRICS = `http://${GUARDIAN_HOST}:6060/metrics`;
const ETH_NODE_URL = ci ? "ws://eth-devnet:8545" : "ws://localhost:8545";
const BSC_NODE_URL = ci ? "ws://eth-devnet2:8545" : "ws://localhost:8546";
const ETH_PRIVATE_KEY9 =
"0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773";
const ETH_GA_TEST_TOKEN =
devnetConsts.chains[CHAIN_ID_ETH].addresses.testGA.address;
const DECIMALS = devnetConsts.chains[CHAIN_ID_ETH].addresses.testGA.decimals;
const VAA_SIGNERS = process.env.INIT_SIGNERS_KEYS_CSV.split(",");
const GOVERNANCE_CHAIN = Number(devnetConsts.global.governanceChainId);
const GOVERNANCE_EMITTER = devnetConsts.global.governanceEmitterAddress;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Guardian metrics are prometheus data
const fetchGlobalAccountantMetrics = async (): Promise<{
global_accountant_connection_errors_total: number;
global_accountant_error_events_received: number;
global_accountant_events_received: number;
global_accountant_submit_failures: number;
global_accountant_total_balance_errors: number;
global_accountant_total_digest_mismatches: number;
global_accountant_transfer_vaas_outstanding: number;
global_accountant_transfer_vaas_submitted: number;
global_accountant_transfer_vaas_submitted_and_approved: number;
}> =>
(await (await fetch(GUARDIAN_METRICS)).text())
.split("\n")
.filter((m) => m.startsWith("global_accountant"))
.reduce((p, m) => {
const [k, v] = m.split(" ");
p[k] = Number(v);
return p;
}, {} as any);
(async () => {
//
// PREAMBLE
//
// create a signer for Eth
const ethProvider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
const ethSigner = new ethers.Wallet(ETH_PRIVATE_KEY9, ethProvider);
// create a signer for BSC
const bscProvider = new ethers.providers.WebSocketProvider(BSC_NODE_URL);
const bscSigner = new ethers.Wallet(ETH_PRIVATE_KEY9, bscProvider);
let attestedAddress = "";
//
// STEP 0 - attest the token
//
{
attestedAddress = await getForeignAssetEth(
CONTRACTS.DEVNET.bsc.token_bridge,
bscProvider,
CHAIN_ID_ETH,
tryNativeToUint8Array(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH)
);
if (attestedAddress && attestedAddress !== ethers.constants.AddressZero) {
console.log("already attested");
} else {
console.log("attesting...");
// attest the test token
const receipt = await attestFromEth(
CONTRACTS.DEVNET.ethereum.token_bridge,
ethSigner,
ETH_GA_TEST_TOKEN
);
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogEth(
receipt,
CONTRACTS.DEVNET.ethereum.core
);
const emitterAddress = getEmitterAddressEth(
CONTRACTS.DEVNET.ethereum.token_bridge
);
console.log(`fetching vaa ${sequence}...`);
// poll until the guardian(s) witness and sign the vaa
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
GUARDIAN_RPCS,
CHAIN_ID_ETH,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
console.log("creating...");
await createWrappedOnEth(
CONTRACTS.DEVNET.bsc.token_bridge,
bscSigner,
signedVAA
);
attestedAddress = await getForeignAssetEth(
CONTRACTS.DEVNET.bsc.token_bridge,
bscProvider,
CHAIN_ID_ETH,
tryNativeToUint8Array(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH)
);
}
}
//
// STEP 1 - send the token out
//
{
const beforeMetrics = await fetchGlobalAccountantMetrics();
const amount = parseUnits("1", DECIMALS);
// approve the bridge to spend tokens
console.log("approving...");
await approveEth(
CONTRACTS.DEVNET.ethereum.token_bridge,
ETH_GA_TEST_TOKEN,
ethSigner,
amount
);
// transfer tokens out
console.log("transferring...");
const receipt = await transferFromEth(
CONTRACTS.DEVNET.ethereum.token_bridge,
ethSigner,
ETH_GA_TEST_TOKEN,
amount,
CHAIN_ID_BSC,
tryNativeToUint8Array(await bscSigner.getAddress(), CHAIN_ID_BSC)
);
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogEth(
receipt,
CONTRACTS.DEVNET.ethereum.core
);
const emitterAddress = getEmitterAddressEth(
CONTRACTS.DEVNET.ethereum.token_bridge
);
console.log(`fetching vaa ${sequence}...`);
// poll until the guardian(s) witness and sign the vaa
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
GUARDIAN_RPCS,
CHAIN_ID_ETH,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
console.log("redeeming...");
await redeemOnEth(CONTRACTS.DEVNET.bsc.token_bridge, bscSigner, signedVAA);
const afterMetrics = await fetchGlobalAccountantMetrics();
console.log(
"approved b/a:",
beforeMetrics.global_accountant_transfer_vaas_submitted_and_approved,
afterMetrics.global_accountant_transfer_vaas_submitted_and_approved
);
if (
afterMetrics.global_accountant_events_received <=
beforeMetrics.global_accountant_events_received ||
afterMetrics.global_accountant_transfer_vaas_submitted <=
beforeMetrics.global_accountant_transfer_vaas_submitted ||
afterMetrics.global_accountant_transfer_vaas_submitted_and_approved <=
beforeMetrics.global_accountant_transfer_vaas_submitted_and_approved
) {
throw new Error("Expected metrics change did not occur");
}
}
//
// STEP 2 - send the token back
//
{
const beforeMetrics = await fetchGlobalAccountantMetrics();
const amount = parseUnits("1", DECIMALS);
// approve the bridge to spend tokens
console.log("approving...");
await approveEth(
CONTRACTS.DEVNET.bsc.token_bridge,
attestedAddress,
bscSigner,
amount
);
// transfer tokens out
console.log("transferring...");
const receipt = await transferFromEth(
CONTRACTS.DEVNET.bsc.token_bridge,
bscSigner,
attestedAddress,
amount,
CHAIN_ID_ETH,
tryNativeToUint8Array(await ethSigner.getAddress(), CHAIN_ID_ETH)
);
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogEth(
receipt,
CONTRACTS.DEVNET.bsc.core
);
const emitterAddress = getEmitterAddressEth(
CONTRACTS.DEVNET.bsc.token_bridge
);
console.log(`fetching vaa ${sequence}...`);
// poll until the guardian(s) witness and sign the vaa
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
GUARDIAN_RPCS,
CHAIN_ID_BSC,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
console.log("redeeming...");
await redeemOnEth(
CONTRACTS.DEVNET.ethereum.token_bridge,
ethSigner,
signedVAA
);
const afterMetrics = await fetchGlobalAccountantMetrics();
console.log(
"approved b/a:",
beforeMetrics.global_accountant_transfer_vaas_submitted_and_approved,
afterMetrics.global_accountant_transfer_vaas_submitted_and_approved
);
if (
afterMetrics.global_accountant_events_received <=
beforeMetrics.global_accountant_events_received ||
afterMetrics.global_accountant_transfer_vaas_submitted <=
beforeMetrics.global_accountant_transfer_vaas_submitted ||
afterMetrics.global_accountant_transfer_vaas_submitted_and_approved <=
beforeMetrics.global_accountant_transfer_vaas_submitted_and_approved
) {
throw new Error("Expected metrics change did not occur");
}
}
//
// STEP 3a - redeem spoofed tokens
//
{
console.log("redeeming spoofed tokens");
let vaa: VAA<TokenBridgeTransfer> = {
version: 1,
guardianSetIndex: 0,
signatures: [],
timestamp: 0,
nonce: 0,
emitterChain: CHAIN_ID_ETH,
emitterAddress: getEmitterAddressEth(
CONTRACTS.DEVNET.ethereum.token_bridge
),
sequence: BigInt(979999116 + Math.floor(Math.random() * 100000000)),
consistencyLevel: 0,
payload: {
module: "TokenBridge",
type: "Transfer",
tokenChain: CHAIN_ID_ETH,
tokenAddress: uint8ArrayToHex(
tryNativeToUint8Array(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH)
),
amount: parseUnits("9000", DECIMALS).toBigInt(),
toAddress: uint8ArrayToHex(
tryNativeToUint8Array(await bscSigner.getAddress(), CHAIN_ID_BSC)
),
chain: CHAIN_ID_BSC,
fee: BigInt(0),
},
};
vaa.signatures = sign(VAA_SIGNERS, vaa);
await redeemOnEth(
CONTRACTS.DEVNET.bsc.token_bridge,
bscSigner,
hexToUint8Array(serialiseVAA(vaa))
);
}
//
// STEP 3b - send the spoofed tokens back
//
{
const beforeMetrics = await fetchGlobalAccountantMetrics();
const amount = parseUnits("9000", DECIMALS);
// approve the bridge to spend tokens
console.log("approving...");
await approveEth(
CONTRACTS.DEVNET.bsc.token_bridge,
attestedAddress,
bscSigner,
amount
);
// transfer tokens out
console.log("transferring...");
const receipt = await transferFromEth(
CONTRACTS.DEVNET.bsc.token_bridge,
bscSigner,
attestedAddress,
amount,
CHAIN_ID_ETH,
tryNativeToUint8Array(await ethSigner.getAddress(), CHAIN_ID_ETH)
);
console.log("waiting 30s to fetch metrics...");
await sleep(30 * 1000); // give the guardian a few seconds to pick up the transfers and attempt to submit them
const afterMetrics = await fetchGlobalAccountantMetrics();
console.log(
"balance errors b/a:",
beforeMetrics.global_accountant_total_balance_errors,
afterMetrics.global_accountant_total_balance_errors
);
if (
afterMetrics.global_accountant_error_events_received <=
beforeMetrics.global_accountant_error_events_received ||
afterMetrics.global_accountant_transfer_vaas_submitted <=
beforeMetrics.global_accountant_transfer_vaas_submitted ||
afterMetrics.global_accountant_total_balance_errors <=
beforeMetrics.global_accountant_total_balance_errors
) {
throw new Error("Expected metrics change did not occur");
}
}
ethProvider.destroy();
bscProvider.destroy();
console.log("success!");
})();