Skip to content

Commit

Permalink
Merge pull request #6187 from Agoric/dc-test-starter-ist
Browse files Browse the repository at this point in the history
test(cosmic-swingset): pay 10 BLD for account with 0.25 IST to start
  • Loading branch information
mergify[bot] authored Jan 10, 2023
2 parents ab176ab + e0e0c36 commit 1e64d5a
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 40 deletions.
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' },
]);
});

0 comments on commit 1e64d5a

Please sign in to comment.