Skip to content

Commit

Permalink
ag-solo: use helper CLI instead of rest-server, it cannot sign txns
Browse files Browse the repository at this point in the history
closes Agoric#3
  • Loading branch information
warner committed May 31, 2019
1 parent e2f9468 commit 46fb5e8
Showing 1 changed file with 55 additions and 115 deletions.
170 changes: 55 additions & 115 deletions lib/ag-solo/chain-cosmos-sdk.js
Original file line number Diff line number Diff line change
@@ -1,83 +1,50 @@
import path from 'path';
import http from 'http';
import fetch from 'node-fetch';
import { spawn } from 'child_process';
import { execFileSync } from 'child_process';
import djson from 'deterministic-json';
import { createHash } from 'crypto';
import connect from 'lotion-connect';

export async function connectToChain(basedir, GCI, rpcAddresses, myAddr, inbound) {
//const connection = await connect(GCI, { nodes: [`ws://localhost:${rpcPort}`]});
//makeChainFollower(connection, MYNAME, 'chain', inbound);

// We spawn a long-running copy of 'ag-cosmos-helper', the
// swingset-flavored cosmos-sdk CLI tool. We tell that process to listen on
// its REST port, and then we make HTTP calls to that port. This is the
// middle ground between our original hackish approach (shelling out to a
// brand new copy of ag-cosmos-helper each time we wanted to send a
// message) and the too-hard-to-do-right-this-instant less-hackish approach
// (building an FFI binding to the same golang code that ag-cosmos-helper
// uses, and calling it directly).
// Each time we read our mailbox from the chain's state, and each time we
// send an outbound message to the chain, we shell out to a one-shot copy
// of 'ag-cosmos-helper', the swingset-flavored cosmos-sdk CLI tool.

// We originally thought we could use the tool's "rest-server" mode, leave
// it running for the duration of our process, but it turns out that the
// rest-server cannot sign transactions (that ability was removed as a
// security concern in https://github.com/cosmos/cosmos-sdk/issues/3641)

// A better approach I'm hopeful we can achieve is an FFI binding to the
// same golang code that powers ag-cosmos-helper, so we can call the
// query/tx functions directly without the overhead of spawning a
// subprocess and encoding everything as strings over stdio.

// the 'ag-cosmos-helper' tool in our repo is built by 'make install' and
// put into the user's $GOPATH/bin . That's a bit intrusive, ideally it
// would live in the build tree along with bin/css-solo . But for now we
// would live in the build tree along with bin/ag-solo . But for now we
// assume that 'ag-cosmos-helper' is on $PATH somewhere.

if (rpcAddresses.length > 1) {
// We want the rest server to rotate/failover between multiple supplied
// ports, so we can give client nodes a list of validators to follow, and
// any given validator can restart without clients failing, so 1: we need
// to find out if the rest-server automatically reconnects, and 2: we
// need to spin up multiple rest-servers (one per rpcAddress) if they
// can't do the multiple-server thing themselves.
console.log('NOTE: multiple rpc ports provided, but I can only use one right now');
}

// the rest-server defaults to '--laddr tcp://localhost:1317' , but it
// might behoove us to allocate a new port so we don't collide with
// anything else on the box (we generally avoid relying on default ports)

// TODO: --chain-id matches something in the genesis block, should we
// include it in the arguments to `css-solo set-gci-ingress`? It's included
// in the GCI, but if we also need it to establish an RPC connection, then
// it needs to be learned somehow, rather than being hardcoded.

const serverDir = path.join(basedir, 'ag-cosmos-helper-statedir');

const args = [ 'rest-server',
'--node', rpcAddresses[0],
'--chain-id', 'agchain',
'--output', 'json',
'--trust-node', 'false',
'--home', serverDir,
];

const p = spawn('ag-cosmos-helper', args);
p.stdout.on('data', _data => 'ignored');
//p.stdout.on('data', data => console.log(`ag-cosmos-helper rest-server stdout: ${data}`));
p.stderr.on('data', data => console.log(`ag-cosmos-helper rest-server stderr: ${data}`));
p.on('close', code => console.log(`ag-cosmos-helper rest-server exited with ${code}`));
console.log(`started ag-cosmos-helper rest-server`);

const restURL = `http://localhost:1317`;
const mailboxURL = `${restURL}/swingset/mailbox/${myAddr}`;
const helperDir = path.join(basedir, 'ag-cosmos-helper-statedir');

function getMailbox() {
return fetch(mailboxURL)
.then(res => res.json()) // .json() returns a Promise
.then(r => {
if (!r.value) {
console.log(`no value in getMailbox query, got`, r);
// error is probably r.codespace/r.code, with human-readable
// r.message
}
return JSON.parse(r.value);
});
const args = ['query', 'swingset', 'mailbox', myAddr,
'--chain-id', 'agchain', '--output', 'json',
'--home', helperDir,
];
const stdout = execFileSync('ag-cosmos-helper', args);
console.log(` helper said: ${stdout}`);
const mailbox = JSON.parse(JSON.parse(stdout).value);
// mailbox is [[[num,msg], ..], ack]
return mailbox;
}

getMailbox().then(m => console.log(`mailbox is`, m));

const rpcURL = `http://${rpcAddresses[0]}`;
const rpcWSURL = `ws://${rpcAddresses[0]}`;
function getGenesis() {
Expand Down Expand Up @@ -119,68 +86,41 @@ export async function connectToChain(basedir, GCI, rpcAddresses, myAddr, inbound

c.lightClient.on('update', _a => {
console.log(`new block on ${GCI}, fetching mailbox`);
getMailbox().then(m => {
const [outbox, ack] = m;
inbound(GCI, outbox, ack);
});
const [outbox, ack] = getMailbox();
inbound(GCI, outbox, ack);
});

async function deliver(newMessages, acknum) {
// each message must include { account_number, sequence } for
// ordering/replay protection. These must be learned from the chain (from
// the 'auth' module, which manages accounts, which are indexed by
// address even though the messages contain an account_number). The
// 'account_number' is (I'd hope) static, so we could fetch it once at
// startup. The 'sequence' number increase with each txn, and we're the
// only party with this private key, so we could also fetch it once at
// startup (or remember it in our local state) and just increment it with
// each message. For now we stay lazy and fetch it every time.
console.log(`delivering to chain`, GCI, newMessages, acknum);

const accountURL = `${restURL}/auth/accounts/${myAddr}`;
console.log(`accountURL is ${accountURL}`);
const ans = await fetch(accountURL)
.then(r => {
console.log(`r is`, r);
if (r.status === 204) {
throw new Error(`account query returned empty body: the chain doesn't know us`);
}
return r.json();
})
.then(a => {
return { account_number: a.value.account_number,
sequence: a.value.sequence,
};
},
err => {
console.log(`error in .json`, err);
throw err;
});
console.log(`account_ans:`, ans);

const url = `${restURL}/swingset/mailbox`;
const body = {
base_req: {
chain_id: 'agchain',
from: myAddr,
sequence: ans.sequence,
account_number: ans.account_number,
password: 'mmmmmmmm',
},
peer: myAddr, // TODO: combine peer and submitter in the message format?
submitter: myAddr,
// TODO: remove this JSON.stringify change 'deliverMailboxReq' to have
// more structure than a single string
deliver: JSON.stringify([newMessages, acknum]),
};
return fetch(url, { method: 'POST',
body: JSON.stringify(body),
//headers: { 'Content-Type': 'application/json' } // do we need it?
})
.then(res => res.json())
.then(json => {
console.log(`POST done`, JSON.stringify(json));
});
// TODO: combine peer and submitter in the message format (i.e. remove
// the extra 'myAddr' after 'tx swingset deliver'). All messages from
// solo vats are "from" the signer, and messages relayed from another
// chain will have other data to demonstrate which chain it comes from

// TODO: remove this JSON.stringify([newMessages, acknum]): change
// 'deliverMailboxReq' to have more structure than a single string, and
// have the CLI handle variable args better

const args = ['tx', 'swingset', 'deliver', myAddr,
JSON.stringify([newMessages, acknum]),
'--from', 'ag-solo', '--yes',
'--chain-id', 'agchain',
'--home', helperDir,
];
const password = 'mmmmmmmm\n';
try {
console.log(`running helper`, args);
const stdout = execFileSync('ag-cosmos-helper', args,
{ input: Buffer.from(`${password}`),
});
console.log(` helper said: ${stdout}`);
} catch (e) {
console.log(`helper failed`);
console.log(`rc: ${e.status}`);
console.log(`stdout:`, e.stdout.toString());
console.log(`stderr:`, e.stderr.toString());
}

}

Expand Down

0 comments on commit 46fb5e8

Please sign in to comment.