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

Core-eval governance support #5042

Merged
merged 9 commits into from
Apr 8, 2022
89 changes: 89 additions & 0 deletions golang/cosmos/x/swingset/types/proposal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package types

import (
"encoding/json"
"testing"

"gopkg.in/yaml.v2"

"github.com/stretchr/testify/require"
)

func formatOutput(i interface{}) (string, error) {
out, err := json.Marshal(i)
if err != nil {
return "", err
}

err = json.Unmarshal(out, &i)
if err != nil {
return "", err
}

out, err = yaml.Marshal(i)
if err != nil {
return "", err
}

return string(out), nil
}

func TestCoreEvalProposal(t *testing.T) {
ce1 := CoreEval{
JsonPermits: `{
"consume": {
"aggregators": true
}
}`,
JsCode: `// create-gov.js initially created this file.
/* eslint-disable */

const AGORIC_INSTANCE_NAME = "BLD-USD priceAggregator";

AGORIC_INSTANCE_NAME;
`,
}
cep := NewCoreEvalProposal("test title", "test description", []CoreEval{ce1})

require.Equal(t, "test title", cep.GetTitle())
require.Equal(t, "test description", cep.GetDescription())
require.Equal(t, RouterKey, cep.ProposalRoute())
require.Equal(t, ProposalTypeCoreEval, cep.ProposalType())

text, err := formatOutput(cep)
require.NoError(t, err)
require.Equal(t, `title: test title
description: test description
evals:
- json_permits: |-
{
"consume": {
"aggregators": true
}
}
js_code: |
// create-gov.js initially created this file.
/* eslint-disable */

const AGORIC_INSTANCE_NAME = "BLD-USD priceAggregator";

AGORIC_INSTANCE_NAME;
`, text)
require.Nil(t, cep.ValidateBasic())

ce2 := CoreEval{JsonPermits: `{"a": true}`, JsCode: "bar"}
cep = NewCoreEvalProposal("test title", "test description", []CoreEval{ce2})
require.NoError(t, cep.ValidateBasic())

ce3 := CoreEval{JsonPermits: "", JsCode: "bar"}
cep = NewCoreEvalProposal("test title", "test description", []CoreEval{ce3})
require.Error(t, cep.ValidateBasic())

ce4 := CoreEval{JsonPermits: `{"a": true}`, JsCode: ""}
cep = NewCoreEvalProposal("test title", "test description", []CoreEval{ce4})
require.Error(t, cep.ValidateBasic())

ce5 := CoreEval{JsonPermits: "BAD-JSON", JsCode: "bar"}
cep = NewCoreEvalProposal("test title", "test description", []CoreEval{ce5})
require.Error(t, cep.ValidateBasic())
}
3 changes: 1 addition & 2 deletions packages/agoric-cli/tools/getting-started.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const gettingStartedWorkflowTest = async (t, options = {}) => {
}

const olddir = process.cwd();
const { name, removeCallback } = tmp.dirSync({
const { name } = tmp.dirSync({
unsafeCleanup: true,
prefix: 'agoric-cli-test-',
});
Expand Down Expand Up @@ -261,6 +261,5 @@ export const gettingStartedWorkflowTest = async (t, options = {}) => {
runFinalizers();
process.off('SIGINT', runFinalizers);
process.chdir(olddir);
removeCallback();
}
};
33 changes: 29 additions & 4 deletions packages/cosmic-swingset/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ VOTE_OPTION = yes
GOSRC = ../../golang/cosmos
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
AGCH = $(GOSRC)/build/agd

SHELL = bash

default: all

# By default, make the fake chain in scenario3 produce
# "blocks" as soon as they come in.
FAKE_CHAIN_DELAY = 0
Expand All @@ -36,6 +40,29 @@ AGC_START_ARGS =

BIN := $(shell echo $${GOBIN-$${GOPATH-$$HOME/go}/bin})

EVAL_CLEAN=$(EVAL_CODE)-clean.js

$(EVAL_CLEAN): $(EVAL_CODE)
./scripts/clean-core-eval.js $< >[email protected] || { rm -f [email protected]; exit 1; }
mv [email protected] $@

start-runStake: test/runStake/gov-start-runStake.js
make EVAL_CODE=test/runStake/gov-start-runStake.js \
EVAL_PERMIT=test/runStake/gov-start-runStake-permit.json \
VOTE_PROPOSAL=$(VOTE_PROPOSAL) scenario2-core-eval scenario2-vote

start-committee: test/runStake/gov-start-econCommittee.js
make EVAL_CODE=test/runStake/gov-start-econCommittee.js \
EVAL_PERMIT=test/runStake/gov-start-runStake-permit.json \
VOTE_PROPOSAL=$(VOTE_PROPOSAL) scenario2-core-eval scenario2-vote

gov-q:
$(AGCH) query gov proposals --output json | \
jq -c '.proposals[] | [.proposal_id,.voting_end_time,.status]'

deploy-runStake: ../run-protocol/test/runStake/runStake-deploy.js
echo agoric deploy ../run-protocol/test/runStake/runStake-deploy.js

# This probably shouldn't install, but that's what published instructions
# expect.
all: build-chain install-nobuild
Expand Down Expand Up @@ -64,8 +91,6 @@ scenario1-run-chain:
scenario1-run-client:
AG_SOLO_BASEDIR=$$PWD/t9/$(BASE_PORT) $(AG_SOLO) setup --network-config=http://localhost:8001/network-config --webport=$(BASE_PORT)

AGC = DEBUG=$(DEBUG) PATH="$$PWD/bin:$$PATH" $(GOSRC)/build/agd
AGCH = $(GOSRC)/build/agd
scenario2-setup: all scenario2-setup-nobuild
scenario2-setup-nobuild:
rm -rf t1
Expand Down Expand Up @@ -192,9 +217,9 @@ t1-start-ag-solo: t1/$(BASE_PORT)/cosmos-client-account.setup t1/$(BASE_PORT)/co
SOLO_OTEL_EXPORTER_PROMETHEUS_PORT=$(SOLO_OTEL_EXPORTER_PROMETHEUS_PORT) \
$(AG_SOLO) start

scenario2-core-eval:
scenario2-core-eval: $(EVAL_CLEAN)
$(AGCH) --home=t1/bootstrap tx gov submit-proposal swingset-core-eval \
$(EVAL_PERMIT) $(EVAL_CODE) \
$(EVAL_PERMIT) $(EVAL_CLEAN) \
--title="Swingset core eval" --description="Evaluate $(EVAL_CODE)" --deposit=$(EVAL_DEPOSIT) \
--gas=auto --gas-adjustment=$(GAS_ADJUSTMENT) \
--yes --chain-id=$(CHAIN_ID) --keyring-backend=test --from=bootstrap -b block
Expand Down
2 changes: 1 addition & 1 deletion packages/cosmic-swingset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
"@agoric/telemetry": "^0.1.1",
"@agoric/vats": "^0.7.0",
"@agoric/xsnap": "^0.11.2",
"@endo/far": "^0.1.9",
"@endo/import-bundle": "^0.2.41",
"@endo/init": "^0.5.37",
"@endo/marshal": "^0.6.3",
"@iarna/toml": "^2.2.3",
"@opentelemetry/sdk-metrics-base": "^0.27.0",
"agoric": "^0.14.1",
Expand Down
71 changes: 71 additions & 0 deletions packages/cosmic-swingset/scripts/clean-core-eval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#! /usr/bin/env node
import '@endo/init';
import * as farExports from '@endo/far';
import { isEntrypoint } from '../src/is-entrypoint.js';

export const compartmentEvaluate = code => {
// const permit = true;
// const powers = {};
const modules = {}; // TODO

// Inspired by ../repl.js:
const globals = harden({
...modules,
...farExports,
assert,
console: {
// Ensure we don't pollute stdout.
debug: console.warn,
log: console.warn,
info: console.warn,
warn: console.warn,
error: console.error,
},
});

// Evaluate the code in the context of the globals.
const compartment = new Compartment(globals);
harden(compartment.globalThis);
return compartment.evaluate(code);
};

export const htmlStartCommentPattern = new RegExp(`(${'<'})(!--)`, 'g');
export const htmlEndCommentPattern = new RegExp(`(--)(${'>'})`, 'g');
export const importPattern = new RegExp(
'(^|[^.])\\bimport(\\s*(?:\\(|/[/*]))',
'g',
);

// Neutralize HTML comments and import expressions.
export const defangEvaluableCode = code =>
code
.replace(importPattern, '$1import\\$2') // avoid SES_IMPORT_REJECTED
.replace(htmlStartCommentPattern, '$1\\$2') // avoid SES_HTML_COMMENT_REJECTED
.replace(htmlEndCommentPattern, '$1\\$2'); // avoid SES_HTML_COMMENT_REJECTED

export const main = async (argv, { readFile, stdout }) => {
const [_node, _script, fn] = argv;
if (fn === undefined) {
console.error(`Usage: ${_script} <filename>`);
process.exit(1);
}
const text = await readFile(fn, 'utf-8');
const clean = defangEvaluableCode(text);
const withURL = `${clean}\n//# sourceURL=${fn}\n`;

// end-of-line whitespace disrupts YAML formatting
const trimmed = withURL.replace(/[\r\t ]+$/gm, '');
compartmentEvaluate(trimmed);
stdout.write(trimmed);
};

if (isEntrypoint(import.meta.url)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting approach. isEntrypoint is powerful, but I suppose so was require.main.

A Stackoverflow post has a succinct version:

if (import.meta.url === `file://${process.argv[1]}`) {
  // module was not imported but called directly
}

but that's less robust; it's followed by one that uses url.pathToFileURL as in isEntrypoint.

/* global process */
main([...process.argv], {
readFile: (await import('fs/promises')).readFile,
stdout: process.stdout,
}).catch(err => {
process.exitCode = 1;
console.error(err);
});
}
7 changes: 7 additions & 0 deletions packages/cosmic-swingset/src/is-entrypoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Detect if this is run as a script.
import url from 'url';
import process from 'process';

// FIXME: Should maybe be exported by '@endo/something'?
export const isEntrypoint = href =>
String(href) === url.pathToFileURL(process.argv[1] ?? '/').href;
2 changes: 2 additions & 0 deletions packages/cosmic-swingset/test/gov-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ const behavior = ({ consume: { client } }) => {
E(client).assignBundle([addr => ({ youAreGoverned: addr })]);
};

`import('foo')`;

// "export" our behavior by way of the completion value of this script.
behavior;
56 changes: 56 additions & 0 deletions packages/cosmic-swingset/test/test-clean-core-eval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import '@endo/init';
import test from 'ava';
import {
defangEvaluableCode,
compartmentEvaluate,
} from '../scripts/clean-core-eval.js';

test('defangEvaluableCode is working', t => {
const samples = [
[''],
[
`\n// <!-- foo\n\n`,
`\n// <\\!-- foo\n\n`,
{ message: /\(SES_HTML_COMMENT_REJECTED\)/ },
],
[
`\n// --> foo\n\n`,
`\n// --\\> foo\n\n`,
{ message: /\(SES_HTML_COMMENT_REJECTED\)/ },
],
[
`\n\`import('foo')\`\n`,
`\n\`import\\('foo')\`\n`,
{ message: /\(SES_IMPORT_REJECTED\)/ },
],
[
`\n123; import('foo')\n`,
`\n123; import\\('foo')\n`,
{ message: /\(SES_IMPORT_REJECTED\)/ },
{ message: /Invalid or unexpected token/ },
],
];
for (const [
code,
transformed,
originalExpected,
defangedExpected,
] of samples) {
const defanged = defangEvaluableCode(code);
if (originalExpected) {
// Transform is necessary, and the original code throws.
t.is(defanged, transformed);
t.throws(() => compartmentEvaluate(code), originalExpected);
} else {
// No transform is necessary, and the code doesn't throw.
t.is(defanged, code);
t.notThrows(() => compartmentEvaluate(code));
}
if (defangedExpected) {
t.throws(() => compartmentEvaluate(defanged), defangedExpected);
} else {
// The transformed code doesn't throw.
t.notThrows(() => compartmentEvaluate(defanged));
}
}
});
7 changes: 4 additions & 3 deletions packages/deploy-script-support/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,17 @@
"dependencies": {
"@agoric/assert": "^0.3.16",
"@agoric/ertp": "^0.13.3",
"@endo/eventual-send": "^0.14.8",
"@agoric/import-manager": "^0.2.35",
"@agoric/nat": "^4.1.0",
"@agoric/notifier": "^0.3.35",
"@agoric/store": "^0.6.10",
"@agoric/vats": "^0.7.0",
"@agoric/zoe": "^0.21.3",
"@endo/base64": "^0.2.21",
"@endo/bundle-source": "^2.1.1",
"@endo/marshal": "^0.6.3",
"@endo/promise-kit": "^0.2.37"
"@endo/far": "^0.1.9",
"@endo/promise-kit": "^0.2.37",
"@endo/zip": "^0.2.21"
},
"devDependencies": {
"@agoric/swingset-vat": "^0.25.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/deploy-script-support/src/assertOfferResult.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-check
import { assert, details as X } from '@agoric/assert';
import { E } from '@endo/eventual-send';
import { E } from '@endo/far';

/** @type {AssertOfferResult} */
export const assertOfferResult = async (seat, expectedOfferResult) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/deploy-script-support/src/depositInvitation.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-check

import { E } from '@endo/eventual-send';
import { E } from '@endo/far';

/** @type {MakeDepositInvitation} */
export const makeDepositInvitation = zoeInvitationPurse => {
Expand Down
35 changes: 35 additions & 0 deletions packages/deploy-script-support/src/endo-pieces-contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { E, Far } from '@endo/far';
import { encodeBase64, decodeBase64 } from '@endo/base64';
import { ZipWriter } from '@endo/zip';

export const start = () => {
const makeBundler = ({ zoe }) => {
const nameToContent = new Map();

return Far('Bundler', {
add: (name, encodedContent) => {
nameToContent.set(name, decodeBase64(encodedContent));
},
install: bundleShell => {
const writer = new ZipWriter();
for (const [name, content] of nameToContent.entries()) {
writer.write(name, content);
}
const endoZipBase64 = encodeBase64(writer.snapshot());
const bundle = { ...bundleShell, endoZipBase64 };
return E(zoe)
.install(bundle)
.finally(() => nameToContent.clear());
},
clear: () => {
nameToContent.clear();
},
});
};
const publicFacet = Far('endoCAS', {
makeBundler,
});

return harden({ publicFacet });
};
harden(start);
Loading