@bitsocial/pubsub-voting 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -13
- package/dist/chain/types.d.ts +10 -9
- package/dist/client/voter.d.ts +79 -19
- package/dist/client/voter.js +82 -66
- package/dist/errors.d.ts +6 -8
- package/dist/errors.js +8 -11
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/rules/erc20-balance.d.ts +0 -1
- package/dist/rules/erc20-balance.js +0 -2
- package/dist/rules/erc5192-min-balance.d.ts +0 -1
- package/dist/rules/erc5192-min-balance.js +0 -2
- package/dist/rules/erc721-min-balance.d.ts +0 -1
- package/dist/rules/erc721-min-balance.js +0 -2
- package/dist/rules/gate.d.ts +164 -0
- package/dist/rules/gate.js +209 -0
- package/dist/rules/registry.d.ts +11 -5
- package/dist/rules/registry.js +18 -9
- package/dist/schema/common.d.ts +5 -5
- package/dist/schema/common.js +5 -5
- package/dist/schema/criteria.d.ts +58 -29
- package/dist/schema/criteria.js +123 -23
- package/dist/signer/eip712.d.ts +2 -2
- package/dist/signer/eip712.js +1 -1
- package/dist/tally/tally.d.ts +2 -1
- package/dist/tally/tally.js +10 -14
- package/dist/tally/types.d.ts +1 -1
- package/dist/transport/integration/harness.js +3 -3
- package/dist/verify/background.d.ts +8 -6
- package/dist/verify/background.js +57 -42
- package/dist/verify/bundle.d.ts +12 -10
- package/dist/verify/bundle.js +69 -38
- package/dist/verify/types.d.ts +37 -16
- package/package.json +1 -1
- package/dist/chain/ticker.d.ts +0 -15
- package/dist/chain/ticker.js +0 -25
package/dist/schema/common.js
CHANGED
|
@@ -5,14 +5,14 @@ import { z } from "zod";
|
|
|
5
5
|
* There is no pkc-js author in this library: a vote is signed directly by the
|
|
6
6
|
* gating-chain wallet that holds the Pass/ERC-20 (see DESIGN.md "Identity: the
|
|
7
7
|
* voting wallet, nothing else"). What survives from the old author/wallet schema is
|
|
8
|
-
*
|
|
9
|
-
* - `ChainTickerSchema`: the chain identifier ("base", "eth", ...) used by criteria
|
|
10
|
-
* and chain-reading rules.
|
|
8
|
+
* one small, identity-agnostic primitive reused across the wire:
|
|
11
9
|
* - `SignatureSchema`: the detached `{ signature, type }` shape (kept from pkc-js),
|
|
12
10
|
* here carrying the wallet's EIP-712 ballot signature.
|
|
11
|
+
*
|
|
12
|
+
* Chains are named by numeric `chainId` (`criteria.bucketChainId`), never by ticker: a ticker is
|
|
13
|
+
* a label local to a document, and two documents spelling one chain differently would be two
|
|
14
|
+
* topics for one contest.
|
|
13
15
|
*/
|
|
14
|
-
/** Chain ticker is unrestricted ("base", "eth", ...), matching pkc-js. */
|
|
15
|
-
export const ChainTickerSchema = z.string().min(1);
|
|
16
16
|
/**
|
|
17
17
|
* A detached signature with an explicit scheme tag. For a Votes bundle the `type` is
|
|
18
18
|
* the EIP-712 scheme ({@link EIP712_SIGNATURE_TYPE}) and `signature` is the hex
|
|
@@ -19,7 +19,7 @@ import { z } from "zod";
|
|
|
19
19
|
export declare const VoteRangeSchema: z.ZodObject<{
|
|
20
20
|
min: z.ZodNumber;
|
|
21
21
|
max: z.ZodNumber;
|
|
22
|
-
}, z.core.$
|
|
22
|
+
}, z.core.$strict>;
|
|
23
23
|
/**
|
|
24
24
|
* A reference to a rule by `type`, plus rule-specific options.
|
|
25
25
|
* Kept loose on purpose: each rule owns and validates its own option schema
|
|
@@ -31,60 +31,89 @@ export declare const RuleRefSchema: z.ZodObject<{
|
|
|
31
31
|
type: z.ZodString;
|
|
32
32
|
}, z.core.$loose>;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* The gate: a boolean tree over rule references, deciding who may vote.
|
|
35
|
+
*
|
|
36
|
+
* A leaf wraps one {@link RuleRefSchema}; `all` and `any` compose leaves into conjunction and
|
|
37
|
+
* disjunction, so a contest can require "holds the Pass AND is not on the deny list", or "holds
|
|
38
|
+
* the Pass OR is a moderator", without either rule knowing the other exists. Composition is the
|
|
39
|
+
* document's business, never a rule's: a rule still answers exactly one question about one wallet
|
|
40
|
+
* (see rules/types.ts), and `rules/gate.ts` folds the answers.
|
|
35
41
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* (`PubsubVoterOptions.chains` maps ticker/chainId to a client), and two honest verifiers
|
|
40
|
-
* reading the same pinned block through different gateways compute identical results. Keeping
|
|
41
|
-
* URLs out means an operator can swap a dead RPC provider without changing the document's
|
|
42
|
-
* bytes — i.e. without forking the topic and orphaning the contest's votes.
|
|
42
|
+
* The leaf is WRAPPED (`{ rule: { type, ... } }`) rather than bare because {@link RuleRefSchema}
|
|
43
|
+
* is loose by design — a custom rule may carry an option named `all` or `any`, which would make a
|
|
44
|
+
* bare leaf structurally ambiguous with a branch exactly when someone writes such a rule.
|
|
43
45
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* Canonicity constraints, all of them load-bearing rather than stylistic. The topic is the CID of
|
|
47
|
+
* these bytes, so any document that differs in bytes but not in meaning is a silent topic FORK —
|
|
48
|
+
* two peers running identical rules on two topics, each invisible to the other:
|
|
49
|
+
* - a branch needs at least TWO children, so `{ all: [X] }` cannot exist alongside `X`;
|
|
50
|
+
* - a branch may not REPEAT a child (compared by canonical bytes, so two leaves of one rule type
|
|
51
|
+
* on different options stay distinct requirements) — a repeat among siblings says nothing the
|
|
52
|
+
* shorter tree does not. Across BRANCHES a rule may repeat, deliberately: that is how a gate
|
|
53
|
+
* expresses a requirement no repetition-free tree can ("any two of these three" is
|
|
54
|
+
* `{ any: [{ all: [A, B] }, { all: [A, C] }, { all: [B, C] }] }`). The price is that some
|
|
55
|
+
* redundant spellings survive — `{ all: [{ any: [A, B] }, A] }` is `A` by absorption — so a
|
|
56
|
+
* leaf's identity is NOT unique within a gate, and its position is what identifies it
|
|
57
|
+
* (`EligibilityCheck.leaf`);
|
|
58
|
+
* - a branch may not nest a branch of its OWN kind: `{ all: [{ all: [A, B] }, C] }` admits,
|
|
59
|
+
* scores, blames and penalizes exactly as `{ all: [A, B, C] }` does, since min and `some` are
|
|
60
|
+
* associative, so the nesting carries no meaning and only new bytes;
|
|
61
|
+
* - depth is capped at {@link MAX_GATE_DEPTH} and leaves at {@link MAX_GATE_LEAVES}, because a
|
|
62
|
+
* criteria document is attacker-supplied input that every peer parses and evaluates.
|
|
63
|
+
*
|
|
64
|
+
* Child ORDER is deliberately significant rather than normalized away: it is what the lazy forward
|
|
65
|
+
* gate evaluates in, so it decides which rule's chain read is paid first and the order failures are
|
|
66
|
+
* reported. Two orderings are two documents, and an author picks the one that reads best.
|
|
48
67
|
*/
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
}
|
|
68
|
+
export interface GateLeaf {
|
|
69
|
+
rule: RuleRef;
|
|
70
|
+
}
|
|
71
|
+
export interface GateAll {
|
|
72
|
+
all: GateNode[];
|
|
73
|
+
}
|
|
74
|
+
export interface GateAny {
|
|
75
|
+
any: GateNode[];
|
|
76
|
+
}
|
|
77
|
+
export type GateNode = GateLeaf | GateAll | GateAny;
|
|
78
|
+
/** Maximum nesting depth of the gate tree (a leaf alone is depth 1). */
|
|
79
|
+
export declare const MAX_GATE_DEPTH = 4;
|
|
80
|
+
/** Maximum number of rule references in one gate tree. */
|
|
81
|
+
export declare const MAX_GATE_LEAVES = 8;
|
|
82
|
+
export declare const GateSchema: z.ZodPipe<z.ZodUnknown, z.ZodType<GateNode, unknown, z.core.$ZodTypeInternals<GateNode, unknown>>>;
|
|
52
83
|
/**
|
|
53
84
|
* The dependency manifest. A client reads this on join and checks that it
|
|
54
85
|
* implements every named rule; if not, it is too old and must recuse
|
|
55
86
|
* itself rather than miscount. This is how criteria upgrades fork cleanly.
|
|
87
|
+
*
|
|
88
|
+
* Strict for the same reason the top level is: the topic is derived from the PARSED document, so
|
|
89
|
+
* an unknown key must fail loudly rather than be stripped — a stripping schema would drop it and
|
|
90
|
+
* derive a different topic from the one the author's bytes imply. That is what makes a document
|
|
91
|
+
* still carrying the pre-`bucketChainId` `chains` map (or the even older `rpcUrls`) an error
|
|
92
|
+
* instead of a silent re-topic.
|
|
56
93
|
*/
|
|
57
94
|
export declare const RequiresSchema: z.ZodObject<{
|
|
58
95
|
rules: z.ZodArray<z.ZodString>;
|
|
59
|
-
|
|
60
|
-
chainId: z.ZodNumber;
|
|
61
|
-
}, z.core.$strict>>;
|
|
62
|
-
}, z.core.$strip>;
|
|
96
|
+
}, z.core.$strict>;
|
|
63
97
|
export declare const CriteriaSchema: z.ZodObject<{
|
|
64
98
|
name: z.ZodString;
|
|
65
99
|
contestId: z.ZodString;
|
|
66
100
|
voteSchema: z.ZodObject<{
|
|
67
101
|
min: z.ZodNumber;
|
|
68
102
|
max: z.ZodNumber;
|
|
69
|
-
}, z.core.$
|
|
103
|
+
}, z.core.$strict>;
|
|
70
104
|
maxVotesPerAddress: z.ZodNumber;
|
|
105
|
+
bucketChainId: z.ZodNumber;
|
|
71
106
|
blocksPerBucket: z.ZodNumber;
|
|
72
107
|
voteExpiryBuckets: z.ZodNumber;
|
|
73
|
-
|
|
74
|
-
type: z.ZodString;
|
|
75
|
-
}, z.core.$loose>;
|
|
108
|
+
gate: z.ZodPipe<z.ZodUnknown, z.ZodType<GateNode, unknown, z.core.$ZodTypeInternals<GateNode, unknown>>>;
|
|
76
109
|
weight: z.ZodObject<{
|
|
77
110
|
type: z.ZodString;
|
|
78
111
|
}, z.core.$loose>;
|
|
79
112
|
requires: z.ZodObject<{
|
|
80
113
|
rules: z.ZodArray<z.ZodString>;
|
|
81
|
-
|
|
82
|
-
chainId: z.ZodNumber;
|
|
83
|
-
}, z.core.$strict>>;
|
|
84
|
-
}, z.core.$strip>;
|
|
114
|
+
}, z.core.$strict>;
|
|
85
115
|
}, z.core.$strict>;
|
|
86
116
|
export type VoteRange = z.infer<typeof VoteRangeSchema>;
|
|
87
117
|
export type RuleRef = z.infer<typeof RuleRefSchema>;
|
|
88
|
-
export type ChainConfig = z.infer<typeof ChainConfigSchema>;
|
|
89
118
|
export type Requires = z.infer<typeof RequiresSchema>;
|
|
90
119
|
export type Criteria = z.infer<typeof CriteriaSchema>;
|
package/dist/schema/criteria.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { encodeCanonical } from "../encoding/canonical.js";
|
|
3
3
|
/**
|
|
4
4
|
* The criteria document.
|
|
5
5
|
*
|
|
@@ -17,7 +17,7 @@ import { ChainTickerSchema } from "./common.js";
|
|
|
17
17
|
* a non-canonical change silently changes the topic.
|
|
18
18
|
*/
|
|
19
19
|
/** Inclusive numeric bounds for a single `vote` value. v1 is { min: 1, max: 1 }. */
|
|
20
|
-
export const VoteRangeSchema = z.
|
|
20
|
+
export const VoteRangeSchema = z.strictObject({
|
|
21
21
|
min: z.number().int(),
|
|
22
22
|
max: z.number().int()
|
|
23
23
|
});
|
|
@@ -31,33 +31,108 @@ export const VoteRangeSchema = z.object({
|
|
|
31
31
|
export const RuleRefSchema = z.looseObject({
|
|
32
32
|
type: z.string().min(1)
|
|
33
33
|
});
|
|
34
|
+
/** Maximum nesting depth of the gate tree (a leaf alone is depth 1). */
|
|
35
|
+
export const MAX_GATE_DEPTH = 4;
|
|
36
|
+
/** Maximum number of rule references in one gate tree. */
|
|
37
|
+
export const MAX_GATE_LEAVES = 8;
|
|
38
|
+
const GateNodeSchema = z.lazy(() => z.union([
|
|
39
|
+
z.strictObject({ rule: RuleRefSchema }),
|
|
40
|
+
z.strictObject({ all: z.array(GateNodeSchema).min(2) }),
|
|
41
|
+
z.strictObject({ any: z.array(GateNodeSchema).min(2) })
|
|
42
|
+
]));
|
|
43
|
+
/** Depth (a leaf is 1), leaf count, and the redundant spellings, in one walk. */
|
|
44
|
+
function gateShape(node) {
|
|
45
|
+
if ("rule" in node)
|
|
46
|
+
return { depth: 1, leaves: 1, redundant: undefined };
|
|
47
|
+
const kind = "all" in node ? "all" : "any";
|
|
48
|
+
const children = "all" in node ? node.all : node.any;
|
|
49
|
+
let depth = 0;
|
|
50
|
+
let leaves = 0;
|
|
51
|
+
let redundant;
|
|
52
|
+
// Canonical bytes are the identity: two children that encode identically ARE the same
|
|
53
|
+
// requirement, however differently they were written. Siblings only — a rule repeated in
|
|
54
|
+
// another branch is how a gate expresses "any two of these three".
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const child of children) {
|
|
57
|
+
const shape = gateShape(child);
|
|
58
|
+
depth = Math.max(depth, shape.depth);
|
|
59
|
+
leaves += shape.leaves;
|
|
60
|
+
redundant ??= shape.redundant;
|
|
61
|
+
if (kind in child) {
|
|
62
|
+
redundant ??= `a \`${kind}\` nested directly inside an \`${kind}\` says nothing its parent does not; inline its children`;
|
|
63
|
+
}
|
|
64
|
+
const bytes = bytesToHex(encodeCanonical(child));
|
|
65
|
+
if (seen.has(bytes))
|
|
66
|
+
redundant ??= `a \`${kind}\` repeats one of its children; drop the duplicate`;
|
|
67
|
+
seen.add(bytes);
|
|
68
|
+
}
|
|
69
|
+
return { depth: depth + 1, leaves, redundant };
|
|
70
|
+
}
|
|
71
|
+
const bytesToHex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
34
72
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* reading the same pinned block through different gateways compute identical results. Keeping
|
|
42
|
-
* URLs out means an operator can swap a dead RPC provider without changing the document's
|
|
43
|
-
* bytes — i.e. without forking the topic and orphaning the contest's votes.
|
|
44
|
-
*
|
|
45
|
-
* Strict on purpose: the topic is derived from the PARSED document, so an unknown key must
|
|
46
|
-
* fail loudly here — a plain (stripping) object would silently drop it and derive a different
|
|
47
|
-
* topic than the author's raw document implies. This also makes pre-v1 documents that still
|
|
48
|
-
* carry `rpcUrls` a loud error instead of a silent re-topic.
|
|
73
|
+
* Structural bounds, checked on the RAW value before the recursive schema ever descends into it.
|
|
74
|
+
* {@link GateNodeSchema} is `z.lazy` and {@link gateShape} recurses, so a pathological document
|
|
75
|
+
* overflows the stack long before any cap can fire — and a `RangeError` escaping `safeParse`
|
|
76
|
+
* breaks the one guarantee that call makes. This walk is iterative and stops at the first node
|
|
77
|
+
* past a bound; anything within them it passes through untouched, so the real schema still
|
|
78
|
+
* produces the precise error for an ordinary authoring mistake.
|
|
49
79
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
const MAX_GATE_NODES = MAX_GATE_LEAVES * 2;
|
|
81
|
+
function checkGateBounds(raw, ctx) {
|
|
82
|
+
const stack = [{ node: raw, depth: 1 }];
|
|
83
|
+
let nodes = 0;
|
|
84
|
+
while (stack.length > 0) {
|
|
85
|
+
const { node, depth } = stack.pop();
|
|
86
|
+
if (depth > MAX_GATE_DEPTH) {
|
|
87
|
+
ctx.addIssue({ code: "custom", message: `gate tree is more than ${MAX_GATE_DEPTH} levels deep` });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
nodes += 1;
|
|
91
|
+
if (nodes > MAX_GATE_NODES) {
|
|
92
|
+
ctx.addIssue({ code: "custom", message: `gate tree has more than ${MAX_GATE_NODES} nodes` });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (typeof node !== "object" || node === null)
|
|
96
|
+
continue; // not a node; the schema says so
|
|
97
|
+
const branch = node;
|
|
98
|
+
const children = Array.isArray(branch.all) ? branch.all : branch.any;
|
|
99
|
+
if (!Array.isArray(children))
|
|
100
|
+
continue;
|
|
101
|
+
for (const child of children)
|
|
102
|
+
stack.push({ node: child, depth: depth + 1 });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export const GateSchema = z
|
|
106
|
+
.unknown()
|
|
107
|
+
.superRefine(checkGateBounds)
|
|
108
|
+
.pipe(GateNodeSchema)
|
|
109
|
+
.superRefine((node, ctx) => {
|
|
110
|
+
const { depth, leaves, redundant } = gateShape(node);
|
|
111
|
+
if (depth > MAX_GATE_DEPTH) {
|
|
112
|
+
ctx.addIssue({ code: "custom", message: `gate tree is ${depth} levels deep; the maximum is ${MAX_GATE_DEPTH}` });
|
|
113
|
+
}
|
|
114
|
+
if (leaves > MAX_GATE_LEAVES) {
|
|
115
|
+
ctx.addIssue({ code: "custom", message: `gate tree names ${leaves} rules; the maximum is ${MAX_GATE_LEAVES}` });
|
|
116
|
+
}
|
|
117
|
+
// Every redundant spelling is a topic fork waiting to happen: it means the same thing as a
|
|
118
|
+
// shorter tree while encoding to different bytes, so two authors expressing one contest can
|
|
119
|
+
// land on two topics. Same reason a branch may not have a single child.
|
|
120
|
+
if (redundant !== undefined)
|
|
121
|
+
ctx.addIssue({ code: "custom", message: `gate tree has a redundant spelling: ${redundant}` });
|
|
52
122
|
});
|
|
53
123
|
/**
|
|
54
124
|
* The dependency manifest. A client reads this on join and checks that it
|
|
55
125
|
* implements every named rule; if not, it is too old and must recuse
|
|
56
126
|
* itself rather than miscount. This is how criteria upgrades fork cleanly.
|
|
127
|
+
*
|
|
128
|
+
* Strict for the same reason the top level is: the topic is derived from the PARSED document, so
|
|
129
|
+
* an unknown key must fail loudly rather than be stripped — a stripping schema would drop it and
|
|
130
|
+
* derive a different topic from the one the author's bytes imply. That is what makes a document
|
|
131
|
+
* still carrying the pre-`bucketChainId` `chains` map (or the even older `rpcUrls`) an error
|
|
132
|
+
* instead of a silent re-topic.
|
|
57
133
|
*/
|
|
58
|
-
export const RequiresSchema = z.
|
|
59
|
-
rules: z.array(z.string().min(1)).nonempty()
|
|
60
|
-
chains: z.record(ChainTickerSchema, ChainConfigSchema)
|
|
134
|
+
export const RequiresSchema = z.strictObject({
|
|
135
|
+
rules: z.array(z.string().min(1)).nonempty()
|
|
61
136
|
});
|
|
62
137
|
export const CriteriaSchema = z
|
|
63
138
|
.object({
|
|
@@ -76,12 +151,37 @@ export const CriteriaSchema = z
|
|
|
76
151
|
* always allowed as withdrawal/abstention regardless of this cap.
|
|
77
152
|
*/
|
|
78
153
|
maxVotesPerAddress: z.number().int().positive(),
|
|
154
|
+
/**
|
|
155
|
+
* The chain whose blocks this contest counts in, by numeric chain id.
|
|
156
|
+
*
|
|
157
|
+
* The contest has exactly ONE clock, and this names it: `blocksPerBucket` and
|
|
158
|
+
* `voteExpiryBuckets` are measured in its blocks, a ballot's `blockNumber` and the
|
|
159
|
+
* `sampleBlock` every rule is handed are numbers on it, the tie-break seed is the hash of
|
|
160
|
+
* its bucket boundary block, and its id is bound into every EIP-712 ballot domain.
|
|
161
|
+
*
|
|
162
|
+
* It is a chain ID rather than a ticker because that is the identity the signature domain
|
|
163
|
+
* already carries — a ticker is a label local to a document, and two documents spelling
|
|
164
|
+
* one chain differently would be two topics for one contest. Rules do not name a chain at
|
|
165
|
+
* all: they read this one. Gating across several chains is future work, and needs an
|
|
166
|
+
* answer for what block a rule on a SECOND chain is handed before it can ship — see
|
|
167
|
+
* DESIGN.md "Open questions".
|
|
168
|
+
*
|
|
169
|
+
* RPC endpoints are deliberately not part of the criteria: which gateway a client trusts
|
|
170
|
+
* is client-local configuration (`PubsubVoterOptions.chains` maps this id to a client),
|
|
171
|
+
* and two honest verifiers reading the same pinned block through different gateways
|
|
172
|
+
* compute identical results. That keeps an operator's dead-RPC swap from forking the
|
|
173
|
+
* topic and orphaning the contest's votes.
|
|
174
|
+
*/
|
|
175
|
+
bucketChainId: z.number().int().positive(),
|
|
79
176
|
/** Block bucket size; all verifiers price the same block per bucket. */
|
|
80
177
|
blocksPerBucket: z.number().int().positive(),
|
|
81
178
|
/** How many buckets a bundle stays valid after its blockNumber. */
|
|
82
179
|
voteExpiryBuckets: z.number().int().positive(),
|
|
83
|
-
/**
|
|
84
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Who may vote (gates a wallet in or out): one rule, or a boolean tree of them.
|
|
182
|
+
* A single-rule gate is spelled `{ rule: { type, ... } }` — see {@link GateSchema}.
|
|
183
|
+
*/
|
|
184
|
+
gate: GateSchema,
|
|
85
185
|
/** How much an eligible vote counts. */
|
|
86
186
|
weight: RuleRefSchema,
|
|
87
187
|
/** Dependency manifest + version negotiation. */
|
package/dist/signer/eip712.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { Vote } from "../schema/votes.js";
|
|
|
17
17
|
* - `votes`: each community (`{ name, publicKey }`) + numeric vote.
|
|
18
18
|
* - `blockNumber`: the LWW key and the bucketized block every verifier reads at.
|
|
19
19
|
*
|
|
20
|
-
* The `domain.chainId` is the
|
|
20
|
+
* The `domain.chainId` is the contest's `bucketChainId`, giving cross-chain/cross-app domain
|
|
21
21
|
* separation for free. This module is pure: no key material, no network, no viem import
|
|
22
22
|
* — it only shapes the object both sides feed to viem.
|
|
23
23
|
*/
|
|
@@ -96,7 +96,7 @@ export interface BallotTypedData {
|
|
|
96
96
|
export declare function ballotTypedData(args: {
|
|
97
97
|
/** The criteria CID's raw binary bytes (`(await criteriaCid(criteria)).bytes`). */
|
|
98
98
|
criteriaCid: Uint8Array;
|
|
99
|
-
/** The
|
|
99
|
+
/** The chain the contest counts in (`criteria.bucketChainId`). */
|
|
100
100
|
chainId: number;
|
|
101
101
|
votes: Vote[];
|
|
102
102
|
blockNumber: number;
|
package/dist/signer/eip712.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - `votes`: each community (`{ name, publicKey }`) + numeric vote.
|
|
17
17
|
* - `blockNumber`: the LWW key and the bucketized block every verifier reads at.
|
|
18
18
|
*
|
|
19
|
-
* The `domain.chainId` is the
|
|
19
|
+
* The `domain.chainId` is the contest's `bucketChainId`, giving cross-chain/cross-app domain
|
|
20
20
|
* separation for free. This module is pure: no key material, no network, no viem import
|
|
21
21
|
* — it only shapes the object both sides feed to viem.
|
|
22
22
|
*/
|
package/dist/tally/tally.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ import type { Tally } from "./types.js";
|
|
|
25
25
|
export interface TallyDeps {
|
|
26
26
|
criteria: Criteria;
|
|
27
27
|
registry: RuleRegistry;
|
|
28
|
-
|
|
28
|
+
/** The contest's one chain client — the weight rule reads it too (DESIGN.md "One clock"). */
|
|
29
|
+
chain: ChainClient;
|
|
29
30
|
bucketMath: BucketMath;
|
|
30
31
|
/**
|
|
31
32
|
* The CRDT's current bundles (one per wallet, LWW-resolved; empty-votes bundles are
|
package/dist/tally/tally.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { base58btc } from "multiformats/bases/base58";
|
|
2
2
|
import { sha256 } from "multiformats/hashes/sha2";
|
|
3
|
-
import { tickerForRef } from "../chain/ticker.js";
|
|
4
3
|
import { makeMemoryRuleCache } from "../rules/cache.js";
|
|
5
4
|
import { scoreOrZero } from "../rules/result.js";
|
|
6
5
|
import { UnknownRuleError } from "../errors.js";
|
|
@@ -16,24 +15,21 @@ function compareBytes(x, y) {
|
|
|
16
15
|
return x.length - y.length;
|
|
17
16
|
}
|
|
18
17
|
export function makeTally(deps) {
|
|
19
|
-
const { criteria, registry,
|
|
20
|
-
const readHead = deps.readHead ?? (async ({ chain }) => ({ block: Number(await
|
|
21
|
-
// Resolve the weight rule
|
|
18
|
+
const { criteria, registry, chain, bucketMath, current, bucketBlockHash } = deps;
|
|
19
|
+
const readHead = deps.readHead ?? (async ({ chain: client }) => ({ block: Number(await client.getBlockNumber()) }));
|
|
20
|
+
// Resolve the weight rule and its options once (see verify/bundle.ts).
|
|
22
21
|
const weight = registry[criteria.weight.type];
|
|
23
22
|
if (!weight)
|
|
24
23
|
throw new UnknownRuleError("weight", criteria.weight.type);
|
|
25
24
|
const weightOptions = weight.optionsSchema.parse(criteria.weight);
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// `deps.ruleCache` MUST be namespaced by the weight rule's OWN chain, not the gating chain —
|
|
32
|
-
// `weightChain` here resolves through `criteria.weight`'s ticker, which may name a different
|
|
33
|
-
// entry of `requires.chains` (the voter derives it that way; see client/voter.ts).
|
|
25
|
+
// The weight rule picks its own block exactly as a gate rule does (see rules/types.ts): it is
|
|
26
|
+
// handed the bundle's pinned sample block and this verifier's head, and reads whichever it
|
|
27
|
+
// needs. The tally never asks what kind of rule it is holding. It reads the contest's one
|
|
28
|
+
// chain — a weight rule on a second chain is the same open question as a gate leaf on one
|
|
29
|
+
// (DESIGN.md "Open questions").
|
|
34
30
|
const weightCtx = {
|
|
35
|
-
chain
|
|
36
|
-
head: () => readHead({ chain
|
|
31
|
+
chain,
|
|
32
|
+
head: () => readHead({ chain }),
|
|
37
33
|
cache: deps.ruleCache ?? makeMemoryRuleCache()
|
|
38
34
|
};
|
|
39
35
|
const weightFor = async (wallet, blockNumber) => {
|
package/dist/tally/types.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface CommunityTally {
|
|
|
25
25
|
/** Summed weight of upvotes counted so far, in rule score units (`bigint`). */
|
|
26
26
|
weight: bigint;
|
|
27
27
|
/**
|
|
28
|
-
* True once every bundle contributing to this row has had its gate
|
|
28
|
+
* True once every bundle contributing to this row has had its gate confirmed
|
|
29
29
|
* by an on-chain read at its bucket block. `false` means at least one contribution is still
|
|
30
30
|
* awaiting its background gate read — never that one failed (a failed gate evicts the
|
|
31
31
|
* bundle and recounts the row).
|
|
@@ -37,7 +37,7 @@ const CURRENT_BUCKET = 0;
|
|
|
37
37
|
const BLOCKS_PER_BUCKET = 43_200;
|
|
38
38
|
const VOTE_EXPIRY_BUCKETS = 30;
|
|
39
39
|
/** A permissive default verifier; individual tests swap it via {@link VoteNode.setVerifier}. */
|
|
40
|
-
const okVerifier = () => ({ valid: true,
|
|
40
|
+
const okVerifier = () => ({ valid: true, resolvedNames: {} });
|
|
41
41
|
/**
|
|
42
42
|
* One real loopback libp2p + Helia node carrying gossipsub (topic-scoped score params) and
|
|
43
43
|
* `@libp2p/fetch` — the raw host node with nothing else wired. {@link makeVoteNode} builds the
|
|
@@ -118,8 +118,8 @@ export async function makeVoteNode(topic, options = {}) {
|
|
|
118
118
|
verify: (bundle) => verifyImpl(bundle),
|
|
119
119
|
verifyOffline: (bundle) => verifyImpl(bundle),
|
|
120
120
|
// These harness nodes drive the transport, not the eligibility surface; nothing here
|
|
121
|
-
// calls
|
|
122
|
-
|
|
121
|
+
// calls checkGates, so it admits rather than pretending to model a gate.
|
|
122
|
+
checkGates: async () => ({ kind: "leaf", leaf: 0, satisfied: true, score: 1n, penalize: false })
|
|
123
123
|
};
|
|
124
124
|
const admit = async ({ cid, bytes }) => {
|
|
125
125
|
await blockstore.put(cid, bytes);
|
|
@@ -3,7 +3,7 @@ import type { VotesBundle } from "../schema/votes.js";
|
|
|
3
3
|
import type { Criteria } from "../schema/criteria.js";
|
|
4
4
|
import type { RuleRegistry } from "../rules/types.js";
|
|
5
5
|
import type { ChainClient, BucketMath, NameResolver } from "../chain/types.js";
|
|
6
|
-
import {
|
|
6
|
+
import type { RuleCache } from "../rules/cache.js";
|
|
7
7
|
import { type NameResolutionCache } from "./name-resolution-cache.js";
|
|
8
8
|
import type { VerdictCache } from "./cache.js";
|
|
9
9
|
import type { VerifyFail } from "./types.js";
|
|
@@ -52,18 +52,20 @@ export interface PendingBundle {
|
|
|
52
52
|
export interface BackgroundVerifierDeps {
|
|
53
53
|
criteria: Criteria;
|
|
54
54
|
registry: RuleRegistry;
|
|
55
|
-
|
|
55
|
+
/** The contest's one chain client — every rule reads it (DESIGN.md "One clock"). */
|
|
56
|
+
chain: ChainClient;
|
|
56
57
|
bucketMath: BucketMath;
|
|
57
58
|
nameResolvers: NameResolver[];
|
|
58
59
|
/**
|
|
59
|
-
*
|
|
60
|
-
* forward-gate verifier, so neither re-reads what
|
|
60
|
+
* One memo per gate leaf, in `gateLeaves` order, handed to each rule as `ctx.cache`
|
|
61
|
+
* (rules/cache.ts). Shared with the inline forward-gate verifier, so neither re-reads what
|
|
62
|
+
* the other settled.
|
|
61
63
|
*/
|
|
62
|
-
|
|
64
|
+
ruleCaches?: readonly RuleCache[];
|
|
63
65
|
/**
|
|
64
66
|
* This verifier's current head, handed to the rule as `ctx.head`. Resolved by the rule at
|
|
65
67
|
* most once per batch, so a round stays batchable. Never called by a rule that scores pinned
|
|
66
|
-
* historical state. Defaults to the
|
|
68
|
+
* historical state. Defaults to the contest chain's own `getBlockNumber()`; the voter injects
|
|
67
69
|
* its coalesced reader.
|
|
68
70
|
*/
|
|
69
71
|
readHead?: (args: {
|