diff --git a/packages/governance/exported.js b/packages/governance/exported.js new file mode 100644 index 000000000000..f4cba017ea13 --- /dev/null +++ b/packages/governance/exported.js @@ -0,0 +1 @@ +import './src/types.js'; diff --git a/packages/governance/src/committeeRegistrar.js b/packages/governance/src/committeeRegistrar.js index 56e826553f4b..f95c099d1d03 100644 --- a/packages/governance/src/committeeRegistrar.js +++ b/packages/governance/src/committeeRegistrar.js @@ -12,15 +12,17 @@ import { QuorumRule } from './question.js'; const { ceilDivide } = natSafeMath; -// Each CommitteeRegistrar represents a particular set of voters. The number of -// voters is visible in the terms. -// -// This contract creates an electorate that is not visible to observers. There -// may be uses for such a structure, but it is not appropriate for elections -// where the set of voters needs to be known, unless the contract is used in a -// way that makes the distribution of voter facets visible. - -/** @type {ContractStartFn} */ +/** + * Each CommitteeRegistrar represents a particular set of voters. The number of + * voters is visible in the terms. + * + * This contract creates an electorate that is not visible to observers. There + * may be uses for such a structure, but it is not appropriate for elections + * where the set of voters needs to be known, unless the contract is used in a + * way that makes the distribution of voter facets visible. + * + * @type {ContractStartFn} + */ const start = zcf => { /** * @typedef {Object} QuestionRecord @@ -31,7 +33,6 @@ const start = zcf => { /** @type {Store, QuestionRecord>} */ const allQuestions = makeStore('Question'); const { subscription, publication } = makeSubscriptionKit(); - const invitations = []; const getOpenQuestions = async () => { const isOpenPQuestions = allQuestions.keys().map(key => { @@ -69,9 +70,10 @@ const start = zcf => { }; const { committeeName, committeeSize } = zcf.getTerms(); - for (let i = 0; i < committeeSize; i += 1) { - invitations[i] = makeCommitteeVoterInvitation(i); - } + + const invitations = harden( + [...Array(committeeSize).keys()].map(makeCommitteeVoterInvitation), + ); /** @type {AddQuestion} */ const addQuestion = async (voteCounter, questionSpec) => { diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index 6e9c7d1a352f..ee60c10d36d0 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -4,7 +4,6 @@ import { E } from '@agoric/eventual-send'; import { Far } from '@agoric/marshal'; import { setupGovernance, validateParamChangeQuestion } from './governParam.js'; -import { assertContractGovernance } from './validators.js'; const { details: X } = assert; @@ -20,8 +19,6 @@ const validateQuestionDetails = async (zoe, registrar, details) => { .electionManager; const governorPublic = E(zoe).getPublicFacet(governorInstance); - await assertContractGovernance(zoe, governedInstance, governorInstance); - return Promise.all([ E(governorPublic).validateVoteCounter(counterInstance), E(governorPublic).validateRegistrar(registrar), diff --git a/packages/governance/src/index.js b/packages/governance/src/index.js new file mode 100644 index 000000000000..ab357ba31390 --- /dev/null +++ b/packages/governance/src/index.js @@ -0,0 +1,31 @@ +// @ts-check + +export { + ChoiceMethod, + ElectionType, + QuorumRule, + looksLikeQuestionSpec, + positionIncluded, + looksLikeIssueForType, + buildUnrankedQuestion, +} from './question.js'; + +export { + validateQuestionDetails, + validateQuestionFromCounter, +} from './contractGovernor.js'; + +export { handleParamGovernance } from './contractHelper.js'; + +export { + makeParamChangePositions, + validateParamChangeQuestion, + assertBallotConcernsQuestion, +} from './governParam.js'; + +export { ParamType, assertType } from './paramManager.js'; + +export { + assertContractGovernance, + assertContractRegistrar, +} from './validators.js'; diff --git a/packages/governance/src/question.js b/packages/governance/src/question.js index 266aeb5af140..47557eb196d4 100644 --- a/packages/governance/src/question.js +++ b/packages/governance/src/question.js @@ -56,16 +56,17 @@ const QuorumRule = { ALL: 'all', }; -/** @param {SimpleIssue} issue */ +/** @type {LooksLikeSimpleIssue} */ const looksLikeSimpleIssue = issue => { - assert.typeof( - issue.text, - 'string', + assert.typeof(issue, 'object', X`Issue ("${issue}") must be a record`); + assert( + issue && typeof issue.text === 'string', X`Issue ("${issue}") must be a record with text: aString`, ); + return undefined; }; -/** @param {ParamChangeIssue} issue */ +/** @type {LooksLikeParamChangeIssue} */ const looksLikeParamChangeIssue = issue => { assert.typeof( issue.paramSpec, @@ -101,6 +102,7 @@ const positionIncluded = (positions, p) => positions.some(e => sameStructure(e, p)); // QuestionSpec contains the subset of QuestionDetails that can be specified before +/** @type {LooksLikeClosingRule} */ function looksLikeClosingRule(closingRule) { const { timer, deadline } = closingRule; Nat(deadline); diff --git a/packages/governance/src/types.js b/packages/governance/src/types.js index 47c67e979a3a..fbc22741b5b5 100644 --- a/packages/governance/src/types.js +++ b/packages/governance/src/types.js @@ -168,7 +168,6 @@ * available to the Registrar, which should wrap and attenuate it so each * voter gets only the ability to cast their own vote at a weight specified by * the registrar. - * attenuated wrapper that doesn't make that available! * @property {SubmitVote} submitVote */ @@ -205,14 +204,33 @@ /** * @callback LooksLikeQuestionSpec - * @param {QuestionSpec} allegedQuestionSpec - * @returns {QuestionSpec} + * @param {unknown} allegedQuestionSpec + * @returns { asserts allegedQuestionSpec is QuestionSpec } + */ + +/** + * @callback LooksLikeParamChangeIssue + * @param {unknown} issue + * @returns { asserts issue is ParamChangeIssue } */ /** * @callback LooksLikeIssueForType * @param {ElectionType} electionType - * @param {Issue} issue + * @param {unknown} issue + * @returns { asserts issue is Issue } + */ + +/** + * @callback LooksLikeSimpleIssue + * @param {unknown} issue + * @returns { asserts issue is SimpleIssue } + */ + +/** + * @callback LooksLikeClosingRule + * @param {unknown} closingRule + * @returns { asserts closingRule is ClosingRule } */ /** @@ -225,10 +243,10 @@ /** * @typedef {Object} RegistrarPublic * @property {() => Subscription} getQuestionSubscription - * @property {() => ERef[]>} getOpenQuestions, + * @property {() => Promise[]>} getOpenQuestions, * @property {() => string} getName * @property {() => Instance} getInstance - * @property {(h: Handle<'Question'>) => ERef} getQuestion + * @property {(h: Handle<'Question'>) => Promise} getQuestion */ /** @@ -241,16 +259,16 @@ * addQuestion() can be used directly when the creator doesn't need any * reassurance. When someone needs to connect addQuestion to the Registrar * instance, getPoserInvitation() lets them get addQuestion with assurance. - * @property {() => ERef} getPoserInvitation + * @property {() => Promise} getPoserInvitation * @property {AddQuestion} addQuestion - * @property {() => Invitation[]} getVoterInvitations + * @property {() => Promise[]} getVoterInvitations * @property {() => Subscription} getQuestionSubscription * @property {() => RegistrarPublic} getPublicFacet */ /** * @typedef {Object} ClosingRule - * @property {Timer} timer + * @property {ERef} timer * @property {Timestamp} deadline */ @@ -391,8 +409,8 @@ /** * @typedef {Object} GovernorPublic - * @property {() => ERef} getRegistrar - * @property {() => ERef} getGovernedContract + * @property {() => Promise} getRegistrar + * @property {() => Promise} getGovernedContract * @property {(voteCounter: Instance) => Promise} validateVoteCounter * @property {(regP: ERef) => Promise} validateRegistrar * @property {(details: QuestionDetails) => boolean} validateTimer @@ -427,18 +445,18 @@ * A powerful facet that carries access to both the creatorFacet to be passed * to the caller and the paramManager, which will be used exclusively by the * ContractGovenor. - * @property {() => ERef} getLimitedCreatorFacet + * @property {() => Promise} getLimitedCreatorFacet * @property {() => ParamManagerRetriever} getParamMgrRetriever */ /** * @typedef {Object} GovernedContractCreatorFacet * @property {VoteOnParamChange} voteOnParamChange - * @property {() => ERef} getCreatorFacet - creator + * @property {() => Promise} getCreatorFacet - creator * facet of the governed contract, without the tightly held ability to change * param values. * @property {() => any} getPublicFacet - public facet of the governed contract - * @property {() => ERef} getInstance - instance of the governed + * @property {() => Promise} getInstance - instance of the governed * contract */ @@ -523,7 +541,7 @@ * * Validate that the question details correspond to a parameter change question * that the registrar hosts, and that the voteCounter and other details are - * consistent. + * consistent with it. * * @param {ERef} zoe * @param {Instance} registrar diff --git a/packages/governance/src/validators.js b/packages/governance/src/validators.js index c015f4e569ee..54a4a981e2a2 100644 --- a/packages/governance/src/validators.js +++ b/packages/governance/src/validators.js @@ -16,10 +16,12 @@ const assertContractGovernance = async ( zoe, allegedGoverned, allegedGovernor, + contractGovernorInstallation, ) => { const allegedGovernorPF = E(zoe).getPublicFacet(allegedGovernor); const realGovernedP = E(allegedGovernorPF).getGovernedContract(); const allegedGovernedTermsP = E(zoe).getTerms(allegedGoverned); + const [ { electionManager: realGovernorInstance }, realGovernedInstance, @@ -35,7 +37,15 @@ const assertContractGovernance = async ( X`The alleged governed did not match the governed contract retrieved from the governor`, ); - // TODO(3344): assert the installation once Zoe validates installations + const governorInstallationFromGoverned = await E( + zoe, + ).getInstallationForInstance(realGovernorInstance); + + assert( + governorInstallationFromGoverned === contractGovernorInstallation, + X`The governed contract is not governed by an instance of the provided installation.`, + ); + return { governor: realGovernorInstance, governed: realGovernedInstance }; }; diff --git a/packages/governance/test/swingsetTests/committeeBinary/bootstrap.js b/packages/governance/test/swingsetTests/committeeBinary/bootstrap.js index 1545c2e29b16..2932f19de052 100644 --- a/packages/governance/test/swingsetTests/committeeBinary/bootstrap.js +++ b/packages/governance/test/swingsetTests/committeeBinary/bootstrap.js @@ -9,7 +9,7 @@ import { QuorumRule, ElectionType, looksLikeQuestionSpec, -} from '../../../src/question.js'; +} from '../../../src/index.js'; const { quote: q } = assert; @@ -24,7 +24,7 @@ const createQuestion = async (qDetails, closingTime, tools, quorumRule) => { const { issue, positions, electionType } = qDetails; const closingRule = { timer: tools.timer, - deadline: 3n, + deadline: closingTime, }; const questionSpec = looksLikeQuestionSpec( @@ -66,8 +66,7 @@ const committeeBinaryStart = async ( positions: [harden({ text: 'Eeny' }), harden({ text: 'Meeny' })], electionType, }; - const eeny = details.positions[0]; - const meeny = details.positions[1]; + const [eeny, meeny] = details.positions; const tools = { registrarFacet, installations, timer }; const { counterInstance } = await createQuestion( details, diff --git a/packages/governance/test/swingsetTests/committeeBinary/test-committee.js b/packages/governance/test/swingsetTests/committeeBinary/test-committee.js index 092a7fadf5bc..9d835fd78313 100644 --- a/packages/governance/test/swingsetTests/committeeBinary/test-committee.js +++ b/packages/governance/test/swingsetTests/committeeBinary/test-committee.js @@ -101,7 +101,7 @@ const expectedCommitteeBinaryTwoQuestionsLog = [ 'Carol voted on {"text":"Choose"} for {"text":"Two Potato"}', 'Dave voted on {"text":"Choose"} for {"text":"One Potato"}', 'Emma voted on {"text":"Choose"} for {"text":"One Potato"}', - '@@ schedule task for:3, currently: 0 @@', + '@@ schedule task for:4, currently: 0 @@', 'Alice voted on {"text":"How high?"} for {"text":"1 foot"}', 'Bob voted on {"text":"How high?"} for {"text":"2 feet"}', 'Carol voted on {"text":"How high?"} for {"text":"1 foot"}', @@ -117,8 +117,8 @@ const expectedCommitteeBinaryTwoQuestionsLog = [ '@@ tick:2 @@', '@@ tick:3 @@', '&& running a task scheduled for 3. &&', - '&& running a task scheduled for 3. &&', '@@ tick:4 @@', + '&& running a task scheduled for 4. &&', 'vote outcome: {"text":"One Potato"}', 'vote outcome: {"text":"1 foot"}', ]; diff --git a/packages/governance/test/swingsetTests/contractGovernor/vat-voter.js b/packages/governance/test/swingsetTests/contractGovernor/vat-voter.js index a3981c5ff9da..636cec23f648 100644 --- a/packages/governance/test/swingsetTests/contractGovernor/vat-voter.js +++ b/packages/governance/test/swingsetTests/contractGovernor/vat-voter.js @@ -3,7 +3,10 @@ import { E } from '@agoric/eventual-send'; import { Far } from '@agoric/marshal'; -import { assertContractRegistrar } from '../../../src/validators.js'; +import { + assertContractRegistrar, + assertContractGovernance, +} from '../../../src/validators.js'; import { validateQuestionFromCounter, validateQuestionDetails, @@ -36,6 +39,13 @@ const build = async (log, zoe) => { counterInstance, ); + const contractGovernanceP = assertContractGovernance( + zoe, + governedInstance, + governorInstance, + installations.contractGovernor, + ); + const [ governedParam, questionDetails, @@ -44,6 +54,7 @@ const build = async (log, zoe) => { governedInstallation, governorInstallation, validatedQuestion, + contractGovernance, ] = await Promise.all([ E.get(E(zoe).getTerms(governedInstance)).main, E(E(zoe).getPublicFacet(counterInstance)).getDetails(), @@ -52,6 +63,7 @@ const build = async (log, zoe) => { E(zoe).getInstallationForInstance(governedInstance), E(zoe).getInstallationForInstance(governorInstance), validateQuestionFromCounterP, + contractGovernanceP, ]); assertBallotConcernsQuestion(governedParam[0].name, questionDetails); @@ -71,6 +83,10 @@ const build = async (log, zoe) => { questionDetails, ); assert(validatedQuestion, X`governor failed to validate registrar`); + assert( + contractGovernance, + X`governor and governed aren't tightly linked`, + ); log(`Voter ${name} validated all the things`); }, diff --git a/packages/governance/test/unitTests/test-ballotBuilder.js b/packages/governance/test/unitTests/test-ballotBuilder.js index 229621bddfec..d9d58fd45dc1 100644 --- a/packages/governance/test/unitTests/test-ballotBuilder.js +++ b/packages/governance/test/unitTests/test-ballotBuilder.js @@ -14,7 +14,7 @@ import { ChoiceMethod, ElectionType, QuorumRule, -} from '../../src/question.js'; +} from '../../src/index.js'; const issue = harden({ text: 'will it blend?' }); const positions = [harden({ text: 'yes' }), harden({ text: 'no' })]; diff --git a/packages/governance/test/unitTests/test-ballotCount.js b/packages/governance/test/unitTests/test-ballotCount.js index 196d8aef6308..fc6214fc2890 100644 --- a/packages/governance/test/unitTests/test-ballotCount.js +++ b/packages/governance/test/unitTests/test-ballotCount.js @@ -12,8 +12,8 @@ import { ElectionType, QuorumRule, looksLikeQuestionSpec, -} from '../../src/question.js'; -import { makeParamChangePositions } from '../../src/governParam.js'; + makeParamChangePositions, +} from '../../src/index.js'; const ISSUE = harden({ text: 'Fish or cut bait?' }); const FISH = harden({ text: 'Fish' }); diff --git a/packages/governance/test/unitTests/test-committee.js b/packages/governance/test/unitTests/test-committee.js index ffc0d2bcf218..55ada94eae50 100644 --- a/packages/governance/test/unitTests/test-committee.js +++ b/packages/governance/test/unitTests/test-committee.js @@ -16,7 +16,7 @@ import { ElectionType, QuorumRule, looksLikeQuestionSpec, -} from '../../src/question.js'; +} from '../../src/index.js'; const filename = new URL(import.meta.url).pathname; const dirname = path.dirname(filename);