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

refactor: make ballotCounters durable. #8125

Closed
wants to merge 1 commit into from
Closed
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
28 changes: 14 additions & 14 deletions packages/governance/src/binaryVoteCounter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { makeExo, keyEQ, makeScalarMapStore } from '@agoric/store';
import { E } from '@endo/eventual-send';

import {
buildQuestion,
ChoiceMethod,
coerceQuestionSpec,
positionIncluded,
prepareDurableQuestionKit,
} from './question.js';
import { scheduleClose } from './closingRule.js';
import {
Expand Down Expand Up @@ -38,19 +38,11 @@ const validateBinaryQuestionSpec = questionSpec => {
// independently. The standard Zoe start function is at the bottom of this file.

/** @type {BuildVoteCounter} */
const makeBinaryVoteCounter = (
questionSpec,
threshold,
instance,
publisher,
) => {
validateBinaryQuestionSpec(questionSpec);

const question = buildQuestion(questionSpec, instance);
const makeBinaryVoteCounter = (question, threshold, instance, publisher) => {
const details = question.getDetails();

let isOpen = true;
const positions = questionSpec.positions;
const positions = question.getDetails().positions;
/** @type { PromiseRecord<Position> } */
const outcomePromise = makePromiseKit();
/** @type { PromiseRecord<VoteStatistics> } */
Expand Down Expand Up @@ -118,7 +110,7 @@ const makeBinaryVoteCounter = (
} else if (tally[1] > tally[0]) {
outcomePromise.resolve(positions[1]);
} else {
outcomePromise.resolve(questionSpec.tieOutcome);
outcomePromise.resolve(question.getDetails().tieOutcome);
}

// XXX if we should distinguish ties, publish should be called in if above
Expand Down Expand Up @@ -201,20 +193,28 @@ const makeBinaryVoteCounter = (
// It schedules the closing of the vote, finally inserting the contract
// instance in the publicFacet before returning public and creator facets.

/** @typedef {import('@agoric/vat-data').Baggage} Baggage */

/**
* @param {ZCF<{questionSpec: QuestionSpec, quorumThreshold: bigint}>} zcf
* @param {{outcomePublisher: Publisher<OutcomeRecord>}} outcomePublisher
* @param {Baggage} baggage
*/
const start = (zcf, { outcomePublisher }) => {
const start = (zcf, { outcomePublisher }, baggage) => {
// There are a variety of ways of counting quorums. The parameters must be
// visible in the terms. We're doing a simple threshold here. If we wanted to
// discount abstentions, we could refactor to provide the quorumCounter as a
// component.
// TODO(hibbert) checking the quorum should be pluggable and legible.
const { questionSpec, quorumThreshold } = zcf.getTerms();
const makeDurableQuestionKit = prepareDurableQuestionKit(baggage);

validateBinaryQuestionSpec(questionSpec);
const question = makeDurableQuestionKit(questionSpec, zcf.getInstance());

// The closeFacet is exposed for testing, but doesn't escape from a contract
const { publicFacet, creatorFacet, closeFacet } = makeBinaryVoteCounter(
questionSpec,
question,
quorumThreshold,
zcf.getInstance(),
outcomePublisher,
Expand Down
2 changes: 1 addition & 1 deletion packages/governance/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export {
QuorumRule,
coerceQuestionSpec,
positionIncluded,
buildQuestion,
prepareDurableQuestionKit,
} from './question.js';

export {
Expand Down
29 changes: 16 additions & 13 deletions packages/governance/src/multiCandidateVoteCounter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { keyEQ, makeExo, makeScalarMapStore } from '@agoric/store';
import { E } from '@endo/eventual-send';
import { makePromiseKit } from '@endo/promise-kit';
import {
buildQuestion,
prepareDurableQuestionKit,
ChoiceMethod,
coerceQuestionSpec,
ElectionType,
Expand Down Expand Up @@ -32,19 +32,16 @@ const validateQuestionSpec = questionSpec => {

/** @type {BuildMultiVoteCounter} */
const makeMultiCandidateVoteCounter = (
questionSpec,
question,
threshold,
instance,
publisher,
) => {
validateQuestionSpec(questionSpec);

const question = buildQuestion(questionSpec, instance);
const details = question.getDetails();

let isOpen = true;
const positions = questionSpec.positions;
const maxChoices = questionSpec.maxChoices;
const positions = details.positions;
const maxChoices = details.maxChoices;

/** @type { PromiseRecord<Position[]> } */
const outcomePromise = makePromiseKit();
Expand Down Expand Up @@ -107,7 +104,7 @@ const makeMultiCandidateVoteCounter = (
let winningPositions = [];
for (const position of sortedPositions) {
if (position.total > 0n) {
if (winningPositions.length < questionSpec.maxWinners) {
if (winningPositions.length < details.maxWinners) {
winningPositions.push(position);
} else if (
winningPositions[winningPositions.length - 1].total === position.total
Expand All @@ -125,15 +122,15 @@ const makeMultiCandidateVoteCounter = (
winningPositions = winningPositions.map(p => p.position);

if (winningPositions.length === 0) {
outcomePromise.resolve([questionSpec.tieOutcome]);
} else if (winningPositions.length <= questionSpec.maxWinners) {
outcomePromise.resolve([details.tieOutcome]);
} else if (winningPositions.length <= details.maxWinners) {
outcomePromise.resolve(winningPositions);
} else {
const untiedPositions = winningPositions.slice(0, tieIndex);
const tiedPositions = winningPositions.slice(tieIndex);
const tieWinners = breakTie(
tiedPositions,
questionSpec.maxWinners - untiedPositions.length,
details.maxWinners - untiedPositions.length,
);

outcomePromise.resolve(untiedPositions.concat(tieWinners));
Expand Down Expand Up @@ -216,16 +213,22 @@ const makeMultiCandidateVoteCounter = (
});
};

/** @typedef {import('@agoric/vat-data').Baggage} Baggage */

/**
* @param {ZCF<{questionSpec: QuestionSpec, quorumThreshold: bigint }>} zcf
* @param {{outcomePublisher: Publisher<MultiOutcomeRecord>}} outcomePublisher
* @param {Baggage} baggage
*/
const start = (zcf, { outcomePublisher }) => {
const start = (zcf, { outcomePublisher }, baggage) => {
const { questionSpec, quorumThreshold } = zcf.getTerms();
const makeDurableQuestionKit = prepareDurableQuestionKit(baggage);
validateQuestionSpec(questionSpec);
const question = makeDurableQuestionKit(questionSpec, zcf.getInstance());

const { publicFacet, creatorFacet, closeFacet } =
makeMultiCandidateVoteCounter(
questionSpec,
question,
quorumThreshold,
zcf.getInstance(),
outcomePublisher,
Expand Down
89 changes: 52 additions & 37 deletions packages/governance/src/question.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { makeExo, mustMatch, keyEQ, M } from '@agoric/store';
import { makeHandle } from '@agoric/zoe/src/makeHandle.js';
import { mustMatch, keyEQ, M } from '@agoric/store';
import { defineDurableHandle } from '@agoric/zoe/src/makeHandle.js';
import { prepareExoClass } from '@agoric/vat-data';
import { InstanceHandleShape } from '@agoric/zoe/src/typeGuards.js';

import { QuestionI, QuestionSpecShape } from './typeGuards.js';

Expand All @@ -13,13 +15,14 @@ import { QuestionI, QuestionSpecShape } from './typeGuards.js';
* "unranked" is more formally known as "approval" voting, but this is hard for
* people to intuit when there are only two alternatives.
*/
const ChoiceMethod = /** @type {const} */ ({
export const ChoiceMethod = /** @type {const} */ ({
UNRANKED: 'unranked',
ORDER: 'order',
PLURALITY: 'plurality',
});
harden(ChoiceMethod);

const ElectionType = /** @type {const} */ ({
export const ElectionType = /** @type {const} */ ({
// A parameter is named, and a new value proposed
PARAM_CHANGE: 'param_change',
// choose one or multiple winners, depending on ChoiceMethod
Expand All @@ -29,22 +32,26 @@ const ElectionType = /** @type {const} */ ({
API_INVOCATION: 'api_invocation',
OFFER_FILTER: 'offer_filter',
});
harden(ElectionType);

const QuorumRule = /** @type {const} */ ({
export const QuorumRule = /** @type {const} */ ({
MAJORITY: 'majority',
NO_QUORUM: 'no_quorum',
// The election isn't valid unless all voters vote
ALL: 'all',
});
harden(QuorumRule);

/** @type {PositionIncluded} */
const positionIncluded = (positions, p) => positions.some(e => keyEQ(e, p));
export const positionIncluded = (positions, p) =>
positions.some(e => keyEQ(e, p));
harden(positionIncluded);

/**
* @param {QuestionSpec} allegedQuestionSpec
* @returns {QuestionSpec}
*/
const coerceQuestionSpec = ({
export const coerceQuestionSpec = ({
method,
issue,
positions,
Expand Down Expand Up @@ -77,38 +84,46 @@ const coerceQuestionSpec = ({

return question;
};
harden(coerceQuestionSpec);

/** @type {BuildQuestion} */
const buildQuestion = (questionSpec, counterInstance) => {
const questionHandle = makeHandle('Question');
/** @typedef {import('@agoric/vat-data').Baggage} Baggage */

/** @type {Question} */
return makeExo('question details', QuestionI, {
getVoteCounter() {
return counterInstance;
/**
* @param {Baggage} baggage
* @returns {BuildQuestion}
*/
export const prepareDurableQuestionKit = baggage => {
const makeDurableHandle = defineDurableHandle(baggage, 'question');
return prepareExoClass(
baggage,
'question details',
QuestionI,
(questionSpec, counterInstance) => ({
questionSpec,
counterInstance,
questionHandle: makeDurableHandle(),
}),
{
getVoteCounter() {
return this.state.counterInstance;
},
getDetails() {
const { state } = this;

return harden({
...state.questionSpec,
questionHandle: state.questionHandle,
counterInstance: state.counterInstance,
});
},
},
getDetails() {
return harden({
...questionSpec,
questionHandle,
counterInstance,
});
{
stateShape: harden({
questionSpec: QuestionSpecShape,
counterInstance: InstanceHandleShape,
questionHandle: M.remotable('Question'),
}),
},
});
};

harden(buildQuestion);
harden(ChoiceMethod);
harden(ElectionType);
harden(coerceQuestionSpec);
harden(positionIncluded);
harden(QuorumRule);

export {
buildQuestion,
ChoiceMethod,
ElectionType,
coerceQuestionSpec,
positionIncluded,
QuorumRule,
);
};
harden(prepareDurableQuestionKit);
4 changes: 2 additions & 2 deletions packages/governance/src/types-ambient.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@

/**
* @callback BuildVoteCounter
* @param {QuestionSpec} questionSpec
* @param {Question} question
* @param {bigint} threshold - questionSpec includes quorumRule; the electorate
* converts that to a number that the counter can enforce.
* @param {Instance} instance
Expand All @@ -256,7 +256,7 @@

/**
* @callback BuildMultiVoteCounter
* @param {QuestionSpec} questionSpec
* @param {Question} question
* @param {bigint} threshold - questionSpec includes quorumRule; the electorate
* converts that to a number that the counter can enforce.
* @param {Instance} instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Generated by [AVA](https://avajs.dev).
'published.committees.Economic_Committee.latestOutcome',
{
outcome: 'fail',
question: Object @Alleged: QuestionHandle {},
question: Object @Alleged: questionHandle {},
reason: 'No quorum',
},
],
Expand All @@ -43,7 +43,7 @@ Generated by [AVA](https://avajs.dev).
text: 'why not?',
},
],
questionHandle: Object @Alleged: QuestionHandle {},
questionHandle: Object @Alleged: questionHandle {},
quorumRule: 'majority',
tieOutcome: {
text: 'why not?',
Expand Down
Binary file not shown.
Loading
Loading