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

test(cosmic-swingset): pay 10 BLD for account with 0.25 IST to start #6187

Merged
merged 1 commit into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 11 additions & 1 deletion packages/cosmic-swingset/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ VOTE_PROPOSAL = 1
VOTE_OPTION = yes

GOSRC = ../../golang/cosmos
# For deep (cross-vat) stacks, try...
# DEBUG ?= SwingSet:ls,SwingSet:vat,track-turns
DEBUG ?= SwingSet:ls,SwingSet:vat
AG_SOLO = DEBUG=$(DEBUG) $(shell cd ../solo/bin && pwd)/ag-solo
AGC = DEBUG=$(DEBUG) PATH="$$PWD/bin:$$PATH" $(GOSRC)/build/agd
Expand Down Expand Up @@ -150,10 +152,12 @@ scenario2-run-chain: ../vats/decentral-devnet-config.json
$(AGC) --home=t1/n0 start --log_level=warn $(AGC_START_ARGS)

# Run a chain with an explicit halt.
BLOCKS_TO_RUN=3
scenario2-run-chain-to-halt: t1/decentral-economy-config.json
CHAIN_BOOTSTRAP_VAT_CONFIG="$$PWD/t1/decentral-economy-config.json" \
$(AGC) --home=t1/n0 start --log_level=warn --halt-height=$$(($(INITIAL_HEIGHT) + 3)); \
$(AGC) --home=t1/n0 start --log_level=warn --halt-height=$$(($(INITIAL_HEIGHT) + $(BLOCKS_TO_RUN))); \
test "$$?" -eq 98
echo ran to $(INITIAL_HEIGHT) + $(BLOCKS_TO_RUN)

# Blow away all client state to try again without resetting the chain.
scenario2-reset-client:
Expand Down Expand Up @@ -227,6 +231,12 @@ provision-acct:
tx swingset provision-one t1/$(BASE_PORT) $(ACCT_ADDR) SMART_WALLET \
--gas=auto --gas-adjustment=$(GAS_ADJUSTMENT) --broadcast-mode=block --yes

provision-my-acct:
$(AGCH) --chain-id=$(CHAIN_ID) \
--home t1/8000/ag-cosmos-helper-statedir --keyring-backend=test --from=ag-solo \
tx swingset provision-one t1/$(BASE_PORT) $(ACCT_ADDR) SMART_WALLET \
--gas=auto --gas-adjustment=$(GAS_ADJUSTMENT) --broadcast-mode=block --yes

FROM_KEY=bootstrap
SIGN_MODE=
wallet-action: wait-for-cosmos
Expand Down
153 changes: 153 additions & 0 deletions packages/cosmic-swingset/test/scenario2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/* eslint-disable no-await-in-loop */
const { freeze, entries } = Object;

const onlyStderr = ['ignore', 'ignore', 'inherit'];
const noOutput = ['ignore', 'ignore', 'ignore'];
// const noisyDebug = ['ignore', 'inherit', 'inherit'];

export const pspawn =
(bin, { spawn, cwd }) =>
(args = [], opts = {}) => {
let child;
const exit = new Promise((resolve, reject) => {
// console.debug('spawn', bin, args, { cwd: makefileDir, ...opts });
child = spawn(bin, args, { cwd, ...opts });
child.addListener('exit', code => {
if (code !== 0) {
reject(Error(`exit ${code} from: ${bin} ${args}`));
return;
}
resolve(0);
});
});
return { child, exit };
};

/**
* Shared state for tests using scenario2 chain in ../
*
* @param {object} io
* @param {*} io.pspawnMake promise-style spawn of 'make' with cwd set
* @param {*} io.pspawnAgd promise-style spawn of 'ag-chain-cosmos' with cwd set
* @param {typeof console.log} io.log
*/
export const makeScenario2 = ({ pspawnMake, pspawnAgd, log }) => {
const runMake = (args, opts = { stdio: onlyStderr }) => {
// console.debug('make', ...args);
log('make', ...args);

return pspawnMake(args, opts).exit;
};

// {X: 1} => ['X=1'], using JSON.stringify to mitigate injection risks.
const bind = obj =>
entries(obj)
.filter(([_n, v]) => typeof v !== 'undefined')
.map(([n, v]) => [`${n}=${JSON.stringify(v)}`])
.flat();

return freeze({
runMake,
setup: () => runMake(['scenario2-setup'], { stdio: noOutput }),
runToHalt: ({
BLOCKS_TO_RUN = undefined,
INITIAL_HEIGHT = undefined,
} = {}) =>
runMake([
'scenario2-run-chain-to-halt',
...bind({ BLOCKS_TO_RUN, INITIAL_HEIGHT }),
]),
export: () =>
pspawnAgd(['export', '--home=t1/n0'], { stdio: onlyStderr }).exit,
});
};

/**
* Wallet utilities for scenario2.
*
* @param {object} io
* @param {*} io.runMake from makeScenario2 above
* @param {*} io.pspawnAgd as to makeScenario2 above
* @param {(ms: number) => Promise<void>} io.delay
* @param {typeof console.log} io.log
*/
export const makeWalletTool = ({ runMake, pspawnAgd, delay, log }) => {
/**
* @param {string[]} args
* @returns {Promise<any>} JSON.parse of stdout of `ag-chain-cosmos query <...args>`
* @throws if agd exits non-0 or gives empty output
*/
const query = async args => {
const parts = [];
const cmd = pspawnAgd(['query', ...args]);
cmd.child.stdout.on('data', chunk => parts.push(chunk));
await cmd.exit;
const txt = parts.join('').trim();
if (txt === '') {
throw Error(`empty output from: query ${args}`);
}
return JSON.parse(txt);
};

const queryBalance = addr =>
query(['bank', 'balances', addr, '--output', 'json']).then(b => {
console.log(addr, b);
return b;
});

let currentHeight;
const waitForBlock = async (why, targetHeight, relative = false) => {
if (relative) {
if (typeof currentHeight === 'undefined') {
throw Error('cannot use relative before starting');
}
targetHeight += currentHeight;
}
for (;;) {
try {
const info = await query(['block']);
currentHeight = Number(info?.block?.header?.height);
if (currentHeight >= targetHeight) {
log(info?.block?.header?.time, ' block ', currentHeight);
return currentHeight;
}
console.log(why, ':', currentHeight, '<', targetHeight, '5 sec...');
} catch (reason) {
console.warn(why, '2:', reason?.message, '5sec...');
}
await delay(5000);
}
};

// one tx per block per address
const myTurn = new Map(); // addr -> blockHeight
const waitMyTurn = async (why, addr) => {
const lastTurn = myTurn.get(addr) || 0;
const nextHeight = await waitForBlock(why, lastTurn + 1);
myTurn.set(addr, nextHeight);
};

// {X: 1} => ['X=1'], using JSON.stringify to mitigate injection risks.
const bind = obj =>
entries(obj)
.filter(([_n, v]) => typeof v !== 'undefined')
.map(([n, v]) => [`${n}=${JSON.stringify(v)}`])
.flat();

return freeze({
query,
queryBalance,
waitForBlock,
fundAccount: async (ACCT_ADDR, FUNDS) => {
const a4 = ACCT_ADDR.slice(-4);
await waitMyTurn(`fund ${a4}`, 'bootstrap');
await runMake([...bind({ ACCT_ADDR, FUNDS }), 'fund-acct']);
await waitForBlock(`${a4}'s funds to appear`, 2, true);
return queryBalance(ACCT_ADDR);
},
provisionMine: ACCT_ADDR =>
waitMyTurn('provision', ACCT_ADDR).then(() =>
runMake([...bind({ ACCT_ADDR }), 'provision-my-acct']),
),
});
};
74 changes: 35 additions & 39 deletions packages/cosmic-swingset/test/test-make.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,41 @@
import test from 'ava';
import { spawn } from 'child_process';
import path from 'path';

const filename = new URL(import.meta.url).pathname;
const dirname = path.dirname(filename);
// Use ambient authority only in test.before()
import { spawn as ambientSpawn } from 'child_process';
import * as ambientPath from 'path';

test('make and exec', async t => {
await new Promise(resolve =>
spawn('make', ['scenario2-setup'], {
cwd: `${dirname}/..`,
stdio: ['ignore', 'ignore', 'inherit'],
}).addListener('exit', code => {
t.is(code, 0, 'make scenario2-setup exits successfully');
resolve();
}),
);
await new Promise(resolve =>
spawn('bin/ag-chain-cosmos', {
cwd: `${dirname}/..`,
stdio: ['ignore', 'ignore', 'inherit'],
}).addListener('exit', code => {
t.is(code, 0, 'exec exits successfully');
resolve();
}),
);
await new Promise(resolve =>
spawn('make', ['scenario2-run-chain-to-halt'], {
cwd: `${dirname}/..`,
stdio: ['ignore', 'ignore', 'inherit'],
}).addListener('exit', code => {
t.is(code, 0, 'make scenario2-run-chain-to-halt is successful');
resolve();
}),
import { makeScenario2, pspawn } from './scenario2.js';

test.before(async t => {
const filename = new URL(import.meta.url).pathname;
const dirname = ambientPath.dirname(filename);
const makefileDir = ambientPath.join(dirname, '..');

const io = { spawn: ambientSpawn, cwd: makefileDir };
const pspawnMake = pspawn('make', io);
const pspawnAgd = pspawn('bin/ag-chain-cosmos', io);
const scenario2 = makeScenario2({ pspawnMake, pspawnAgd, log: t.log });
await scenario2.setup();

t.context = { scenario2, pspawnAgd };
});

test.serial('make and exec', async t => {
const { pspawnAgd, scenario2 } = t.context;
t.log('exec agd');
t.is(await pspawnAgd([]).exit, 0, 'exec agd exits successfully');
t.log('run chain to halt');
t.is(
await scenario2.runToHalt(),
0,
'make scenario2-run-chain-to-halt is successful',
);
await new Promise(resolve =>
spawn('bin/ag-chain-cosmos', ['export', '--home=t1/n0'], {
cwd: `${dirname}/..`,
stdio: ['ignore', 'ignore', 'inherit'],
}).addListener('exit', code => {
t.is(code, 0, 'export exits successfully');
resolve();
}),
t.log('resume chain and halt');
t.is(
await scenario2.runToHalt(),
0,
'make scenario2-run-chain-to-halt succeeds again',
);
t.log('export');
t.is(await scenario2.export(), 0, 'export exits successfully');
});
105 changes: 105 additions & 0 deletions packages/cosmic-swingset/test/test-provision-smartwallet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* global setTimeout */
import test from 'ava';

// Use ambient authority only in test.before()
import { spawn as ambientSpawn } from 'child_process';
import * as ambientPath from 'path';
import * as ambientFs from 'fs';

import { makeScenario2, makeWalletTool, pspawn } from './scenario2.js';

// module account address for 'vbank/provision'; aka "megz"
//
// It seems to be some sort of hash of the name, 'vbank/provision'.
// Lack of documentation is a known issue:
// https://github.com/cosmos/cosmos-sdk/issues/8411
//
// In `startWalletFactory.js` we have:
// `E(bankManager).getModuleAccountAddress('vbank/provision')`
// Then in `vat-bank.js` we have a `VBANK_GET_MODULE_ACCOUNT_ADDRESS`
// call across the bridge to golang; `vbank.go` handles it
// by way of `GetModuleAccountAddress` which calls into the cosmos-sdk
// `x/auth` module... over hill and dale, we seem to end up
// with `crypto.AddressHash([]byte(name))` at
// https://github.com/cosmos/cosmos-sdk/blob/512953cd689fd96ef454e424c81c1a0da5782074/x/auth/types/account.go#L158
//
// Whether this implementation is even correct seems to be
// at issue:
// ModuleAccount addresses don't follow ADR-028
// https://github.com/cosmos/cosmos-sdk/issues/13782 Nov 2022
const provisionPoolModuleAccount =
'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346';

test.before(async t => {
const filename = new URL(import.meta.url).pathname;
const dirname = ambientPath.dirname(filename);
const makefileDir = ambientPath.join(dirname, '..');

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

const io = { spawn: ambientSpawn, cwd: makefileDir };
const pspawnMake = pspawn('make', io);
const pspawnAgd = pspawn('bin/ag-chain-cosmos', io);
const scenario2 = makeScenario2({ pspawnMake, pspawnAgd, delay, log: t.log });
const walletTool = makeWalletTool({
runMake: scenario2.runMake,
pspawnAgd,
delay,
log: t.log,
});
await scenario2.setup();

const { readFile } = ambientFs.promises;
const readItem = f => readFile(f, 'utf-8').then(line => line.trim());
const soloAddr = await readItem('./t1/8000/ag-cosmos-helper-address');
const bootstrapAddr = await readItem('./t1/bootstrap-address');
// console.debug('scenario2 addresses', { soloAddr, bootstrapAddr });

t.context = { scenario2, walletTool, pspawnAgd, bootstrapAddr, soloAddr };
});

// SKIP: struggling with timing issues resulting in one of...
// Error: cannot grab 250000uist coins: 0uist is smaller than 250000uist: insufficient funds
// error: code = NotFound desc = account agoric1mhu... not found
// Sometimes I can get this test to work alone, but not
// if run with the test above.
// TODO: https://github.com/Agoric/agoric-sdk/issues/6766
test.skip('integration test: smart wallet provision', async t => {
const { scenario2, walletTool, soloAddr } = t.context;

const enoughBlocksToProvision = 7;
const provision = async () => {
// idea: scenario2.waitForBlock(2, true); // let bootstrap account settle down.
// idea: console.log('bootstrap ready', scenario2.queryBalance(bootstrapAddr));
t.log('Fund pool with USDC');
await walletTool.fundAccount(
provisionPoolModuleAccount,
`${234e6}ibc/usdc1234`,
);
t.log('Fund user account with some BLD');
await walletTool.fundAccount(soloAddr, `${123e6}ubld`);
t.log('Provision smart wallet');
await walletTool.provisionMine(soloAddr, soloAddr);

await walletTool.waitForBlock(
'provision to finish',
enoughBlocksToProvision,
true,
);
return walletTool.queryBalance(soloAddr);
};

const queryGrace = 6; // time to query state before shutting down
const [_run, addrQ] = await Promise.all([
scenario2.runToHalt({
BLOCKS_TO_RUN: enoughBlocksToProvision + queryGrace,
}),
provision(),
]);

t.log('verify 10BLD spent, 0.25 IST received');
t.deepEqual(addrQ.balances, [
{ amount: `${(123 - 10) * 1e6}`, denom: 'ubld' },
{ amount: `${0.25 * 1e6}`, denom: 'uist' },
]);
});