@bitsocial/pubsub-voting 0.4.1 → 0.6.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 +89 -26
- package/dist/chain/types.d.ts +10 -9
- package/dist/client/voter.d.ts +175 -34
- package/dist/client/voter.js +368 -106
- package/dist/errors.d.ts +6 -12
- package/dist/errors.js +8 -19
- 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/chase.d.ts +8 -0
- package/dist/transport/chase.js +5 -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
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
import { tickerForRef } from "../chain/ticker.js";
|
|
2
|
-
import { makeMemoryRuleCache } from "../rules/cache.js";
|
|
3
1
|
import { GATE_GRACE_MS, GATE_RETRY_MS } from "./gate-grace.js";
|
|
4
|
-
import {
|
|
5
|
-
import { UnknownRuleError } from "../errors.js";
|
|
2
|
+
import { dedupeLeaves, evaluateGate, gateBlame, gatePenalize, gateReason, resolveGate } from "../rules/gate.js";
|
|
6
3
|
import { resolveNameThroughCache } from "./name-resolution-cache.js";
|
|
7
4
|
const RETRY_BASE_MS = 2_000;
|
|
8
5
|
const RETRY_CAP_MS = 60_000;
|
|
9
6
|
export function makeBackgroundVerifier(deps) {
|
|
10
|
-
const { criteria, registry,
|
|
7
|
+
const { criteria, registry, chain, bucketMath, nameResolvers, nameResolutionCache, cache, limit } = deps;
|
|
11
8
|
const retryBaseMs = deps.retryBaseMs ?? RETRY_BASE_MS;
|
|
12
9
|
const retryCapMs = deps.retryCapMs ?? RETRY_CAP_MS;
|
|
13
10
|
const gateGraceMs = deps.gateGraceMs ?? GATE_GRACE_MS;
|
|
14
11
|
const gateRetryMs = deps.gateRetryMs ?? GATE_RETRY_MS;
|
|
15
|
-
// Resolve
|
|
16
|
-
//
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const ruleOptions = rule.optionsSchema.parse(criteria.rule);
|
|
22
|
-
const ruleChain = chainFor(tickerForRef(criteria, criteria.rule, ruleOptions));
|
|
23
|
-
const readHead = deps.readHead ?? (async ({ chain }) => ({ block: Number(await chain.getBlockNumber()) }));
|
|
24
|
-
const ctx = {
|
|
25
|
-
chain: ruleChain,
|
|
26
|
-
head: () => readHead({ chain: ruleChain }),
|
|
27
|
-
cache: deps.ruleCache ?? makeMemoryRuleCache()
|
|
28
|
-
};
|
|
12
|
+
// Resolve every gate leaf — rule, options, chain, memo — once (same call as verify/bundle.ts,
|
|
13
|
+
// so the two verifiers cannot resolve a gate differently).
|
|
14
|
+
const readHead = deps.readHead ?? (async ({ chain: client }) => ({ block: Number(await client.getBlockNumber()) }));
|
|
15
|
+
const leaves = resolveGate({ criteria, registry, chain, readHead, caches: deps.ruleCaches });
|
|
16
|
+
// Which leaves ask the same question: a rule named twice in one gate is batched once.
|
|
17
|
+
const { representatives, ofLeaf } = dedupeLeaves(leaves);
|
|
29
18
|
const queue = [];
|
|
30
19
|
/** CIDs queued or in-flight, so a re-chased root cannot double-verify a bundle. */
|
|
31
20
|
const inFlight = new Set();
|
|
@@ -49,15 +38,22 @@ export function makeBackgroundVerifier(deps) {
|
|
|
49
38
|
return bucketMath.sampleBlockForBucket(bucketMath.bucketForBlock(bundle.blockNumber));
|
|
50
39
|
}
|
|
51
40
|
/**
|
|
52
|
-
* Gate stage for one round's batch: hand every not-yet-gated wallet to
|
|
53
|
-
* `evaluateMany` when
|
|
54
|
-
*
|
|
41
|
+
* Gate stage for one round's batch: hand every not-yet-gated wallet to EACH gate leaf at once
|
|
42
|
+
* — `evaluateMany` when the rule has one, `limit`-bounded per-wallet `evaluate` otherwise —
|
|
43
|
+
* then fold each item's leaf answers into its verdict.
|
|
44
|
+
*
|
|
45
|
+
* Note this deliberately does NOT short-circuit the tree the way the inline gate does. Here
|
|
46
|
+
* the axis of batching is the rule, not the wallet: one `evaluateMany` per leaf covers the
|
|
47
|
+
* whole round, so scoring every leaf costs one round trip per leaf no matter how many wallets
|
|
48
|
+
* are pending, while short-circuiting per wallet would fragment those batches into
|
|
49
|
+
* per-wallet reads. Collecting all is the cheaper shape on this path, and it is also what
|
|
50
|
+
* gives every item a complete blame set.
|
|
55
51
|
*
|
|
56
|
-
* No grouping and no cache lookups here
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
52
|
+
* No grouping and no cache lookups here: which block each wallet is read at, and what may be
|
|
53
|
+
* memoized under which key, are the rule's decisions (rules/types.ts, rules/cache.ts).
|
|
54
|
+
* Duplicate wallets are still collapsed before the calls, because that is a property of THIS
|
|
55
|
+
* batch rather than of any rule. Throws on the FIRST infra failure: the round's unfinished
|
|
56
|
+
* items are re-queued by the caller.
|
|
61
57
|
*/
|
|
62
58
|
async function gateStage(items) {
|
|
63
59
|
const pending = items.filter((item) => !item.gateDone);
|
|
@@ -73,14 +69,24 @@ export function makeBackgroundVerifier(deps) {
|
|
|
73
69
|
at.set(key, wallets.length);
|
|
74
70
|
wallets.push(wallet);
|
|
75
71
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
// One batched call per distinct QUESTION, those in parallel: each is a single multicall
|
|
73
|
+
// for the whole round, and the voter's per-client in-flight budget still bounds what
|
|
74
|
+
// reaches an RPC. A rule named twice in one gate ("any two of these three") is asked once
|
|
75
|
+
// and its answer fanned back out to both positions.
|
|
76
|
+
const byQuestion = await Promise.all(representatives.map((leaf) => {
|
|
77
|
+
const { rule, options, ctx } = leaves[leaf];
|
|
78
|
+
return rule.evaluateMany
|
|
79
|
+
? rule.evaluateMany({ options, wallets, ctx }).then(({ results }) => results)
|
|
80
|
+
: Promise.all(wallets.map((wallet) => limit(() => rule.evaluate({ options, wallet, ctx }))));
|
|
81
|
+
}));
|
|
79
82
|
for (const item of pending) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
item.
|
|
83
|
-
|
|
83
|
+
const wallet = at.get(`${item.bundle.address.toLowerCase()}:${sampleBlockFor(item.bundle)}`);
|
|
84
|
+
// Every leaf is already scored, so this "evaluation" reads the batch — no chain work.
|
|
85
|
+
item.gate = await evaluateGate({
|
|
86
|
+
node: criteria.gate,
|
|
87
|
+
evaluate: async (leaf) => byQuestion[ofLeaf[leaf]][wallet],
|
|
88
|
+
collectAll: true
|
|
89
|
+
});
|
|
84
90
|
item.gateDone = true;
|
|
85
91
|
}
|
|
86
92
|
}
|
|
@@ -144,20 +150,29 @@ export function makeBackgroundVerifier(deps) {
|
|
|
144
150
|
requeue.push(item); // gate read never happened (infra) — retry the whole item
|
|
145
151
|
continue;
|
|
146
152
|
}
|
|
147
|
-
if (item.
|
|
148
|
-
|
|
153
|
+
if (item.gate !== undefined && item.gate.satisfied !== true) {
|
|
154
|
+
// The rules' own wording, narrowed to the failures that explain the refusal, plus
|
|
155
|
+
// the recursive answer to "may this be blamed on the sender" (rules/gate.ts).
|
|
156
|
+
const failures = gateBlame(item.gate).map((leaf) => ({
|
|
157
|
+
type: leaves[leaf.leaf].ref.type,
|
|
158
|
+
error: leaf.error ?? ""
|
|
159
|
+
}));
|
|
160
|
+
const reason = gateReason(item.gate);
|
|
161
|
+
if (gatePenalize(item.gate)) {
|
|
149
162
|
// Provable, deterministic reject — safe to cache so a re-publish short-circuits.
|
|
150
163
|
const verdict = {
|
|
151
164
|
valid: false,
|
|
152
165
|
disposition: "reject",
|
|
153
|
-
reason: `not admitted: ${
|
|
166
|
+
reason: `not admitted: ${reason}`,
|
|
167
|
+
failures
|
|
154
168
|
};
|
|
155
169
|
cache.set(item.cid, verdict);
|
|
156
170
|
deps.onEvict(item.cid, verdict);
|
|
157
171
|
settle(item);
|
|
158
172
|
continue;
|
|
159
173
|
}
|
|
160
|
-
//
|
|
174
|
+
// No failure the gate will blame anyone for (see rules/types.ts, RuleResult.penalize,
|
|
175
|
+
// and rules/gate.ts for how that folds through `all`/`any`):
|
|
161
176
|
// the failure means "not yet", not "no". The wallet may have acquired the asset in
|
|
162
177
|
// a block this verifier has not seen, or may acquire it a moment from now — a
|
|
163
178
|
// client that signs the instant it mints races its own transaction. Evicting here
|
|
@@ -174,7 +189,8 @@ export function makeBackgroundVerifier(deps) {
|
|
|
174
189
|
const verdict = {
|
|
175
190
|
valid: false,
|
|
176
191
|
disposition: "ignore",
|
|
177
|
-
reason: `not admitted: ${
|
|
192
|
+
reason: `not admitted: ${reason} (still true after the grace window)`,
|
|
193
|
+
failures
|
|
178
194
|
};
|
|
179
195
|
deps.onEvict(item.cid, verdict);
|
|
180
196
|
settle(item);
|
|
@@ -198,7 +214,7 @@ export function makeBackgroundVerifier(deps) {
|
|
|
198
214
|
if (item.bundle.votes.some((v) => v.community.name))
|
|
199
215
|
deps.onNameResolved(item.cid);
|
|
200
216
|
// Fully settled: store the terminal valid verdict (same shape the forward-gate caches).
|
|
201
|
-
cache.set(item.cid, { valid: true,
|
|
217
|
+
cache.set(item.cid, { valid: true, resolvedNames: item.resolvedNames });
|
|
202
218
|
settle(item);
|
|
203
219
|
}
|
|
204
220
|
if (requeue.length > 0) {
|
|
@@ -260,8 +276,7 @@ export function makeBackgroundVerifier(deps) {
|
|
|
260
276
|
...entry,
|
|
261
277
|
gateDone: false,
|
|
262
278
|
gateNotified: false,
|
|
263
|
-
|
|
264
|
-
gateFailed: undefined,
|
|
279
|
+
gate: undefined,
|
|
265
280
|
resolvedNames: {},
|
|
266
281
|
queuedAt: Date.now()
|
|
267
282
|
});
|
package/dist/verify/bundle.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Criteria } from "../schema/criteria.js";
|
|
2
2
|
import type { RuleRegistry } from "../rules/types.js";
|
|
3
3
|
import type { ChainClient, BucketMath, NameResolver } from "../chain/types.js";
|
|
4
|
-
import {
|
|
4
|
+
import type { RuleCache } from "../rules/cache.js";
|
|
5
5
|
import { type NameResolutionCache } from "./name-resolution-cache.js";
|
|
6
6
|
import type { BundleVerifier } from "./types.js";
|
|
7
7
|
/**
|
|
@@ -32,12 +32,12 @@ export interface BundleVerifierDeps {
|
|
|
32
32
|
criteria: Criteria;
|
|
33
33
|
/** The criteria document's CID bytes (`(await criteriaCid(criteria)).bytes`) — signature binding. */
|
|
34
34
|
criteriaCid: Uint8Array;
|
|
35
|
-
/** The
|
|
35
|
+
/** The contest's numeric chainId (`criteria.bucketChainId`, bound in the ballot domain). */
|
|
36
36
|
chainId: number;
|
|
37
37
|
/** Resolved rule registry (built-ins + host overrides). */
|
|
38
38
|
registry: RuleRegistry;
|
|
39
|
-
/**
|
|
40
|
-
|
|
39
|
+
/** The contest's one chain client — every rule reads it (DESIGN.md "One clock"). */
|
|
40
|
+
chain: ChainClient;
|
|
41
41
|
/** Bucket math for `criteria.blocksPerBucket`. */
|
|
42
42
|
bucketMath: BucketMath;
|
|
43
43
|
/** Host-injected community-name resolvers (`PubsubVoterOptions.nameResolvers`). */
|
|
@@ -55,13 +55,15 @@ export interface BundleVerifierDeps {
|
|
|
55
55
|
block: number;
|
|
56
56
|
}>;
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
* a wallet's gate read from repeating per bundle — an
|
|
60
|
-
* bundles, or an eligible one cycling choices, costs
|
|
61
|
-
*
|
|
62
|
-
* the
|
|
58
|
+
* One memo per gate leaf (see rules/cache.ts), in `gateLeaves` order, handed to each rule as
|
|
59
|
+
* its `ctx.cache`. This is what keeps a wallet's gate read from repeating per bundle — an
|
|
60
|
+
* ineligible wallet minting fresh-signed bundles, or an eligible one cycling choices, costs
|
|
61
|
+
* one read per key per rule epoch rather than one per bundle. Each leaf gets its OWN
|
|
62
|
+
* namespace, so two leaves of the same rule `type` on different options can never read each
|
|
63
|
+
* other's answers. Defaults to private in-memory caches (unit tests); the voter injects the
|
|
64
|
+
* persistent, contest-shared ones.
|
|
63
65
|
*/
|
|
64
|
-
|
|
66
|
+
ruleCaches?: readonly RuleCache[];
|
|
65
67
|
/**
|
|
66
68
|
* Optional persistent cache of name resolutions (the pkc-js rule — see
|
|
67
69
|
* verify/name-resolution-cache.ts). When present, a carried name is resolved live at most
|
package/dist/verify/bundle.js
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { makeMemoryRuleCache } from "../rules/cache.js";
|
|
3
|
-
import { gateFailure, scoreOrZero } from "../rules/result.js";
|
|
4
|
-
import { UnknownRuleError } from "../errors.js";
|
|
1
|
+
import { dedupeLeaves, evaluateGate, gateBlame, gatePenalize, gateReason, resolveGate } from "../rules/gate.js";
|
|
5
2
|
import { verifyBundleSignature } from "./signature.js";
|
|
6
3
|
import { checkBundleConstraints } from "./constraints.js";
|
|
7
4
|
import { resolveNameThroughCache } from "./name-resolution-cache.js";
|
|
8
5
|
export function makeBundleVerifier(deps) {
|
|
9
|
-
const { criteria, criteriaCid, chainId, registry,
|
|
10
|
-
// Resolve
|
|
11
|
-
//
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
6
|
+
const { criteria, criteriaCid, chainId, registry, chain, bucketMath, nameResolvers, nameResolutionCache } = deps;
|
|
7
|
+
// Resolve every gate leaf — rule, options, memo — once: they are fixed by the criteria, so
|
|
8
|
+
// none of it is recomputed per bundle. Same for which leaves ask the same question.
|
|
9
|
+
const readHead = deps.readHead ?? (async ({ chain: client }) => ({ block: Number(await client.getBlockNumber()) }));
|
|
10
|
+
const leaves = resolveGate({ criteria, registry, chain, readHead, caches: deps.ruleCaches });
|
|
11
|
+
const { ofLeaf } = dedupeLeaves(leaves);
|
|
12
|
+
/**
|
|
13
|
+
* Score one leaf for one wallet, sharing one answer between leaves that ask the same question
|
|
14
|
+
* (`dedupeLeaves`). The per-wallet map is what makes the sharing real: the fold evaluates
|
|
15
|
+
* leaves concurrently, so two positions of one rule would otherwise both miss its memo before
|
|
16
|
+
* either wrote to it.
|
|
17
|
+
*/
|
|
18
|
+
const scoreLeaf = (wallet) => {
|
|
19
|
+
const asked = new Map();
|
|
20
|
+
return (leaf) => {
|
|
21
|
+
const question = ofLeaf[leaf];
|
|
22
|
+
const already = asked.get(question);
|
|
23
|
+
if (already)
|
|
24
|
+
return already;
|
|
25
|
+
const { rule, options, ctx } = leaves[leaf];
|
|
26
|
+
const answer = rule.evaluate({ options, wallet, ctx });
|
|
27
|
+
asked.set(question, answer);
|
|
28
|
+
return answer;
|
|
29
|
+
};
|
|
24
30
|
};
|
|
25
31
|
// Stage 1, shared by `verify` and `verifyOffline`: signature + constraints, local and µs.
|
|
26
32
|
const verifyOffline = async (bundle) => {
|
|
@@ -33,25 +39,47 @@ export function makeBundleVerifier(deps) {
|
|
|
33
39
|
};
|
|
34
40
|
return {
|
|
35
41
|
verifyOffline,
|
|
36
|
-
// The gate step on its own, against the SAME
|
|
37
|
-
// is the entire value of exposing it: a caller asking "would this wallet's vote
|
|
38
|
-
// can never drift from what the gate actually does.
|
|
39
|
-
|
|
42
|
+
// The gate step on its own, against the SAME leaves/options/ctxs `verify` uses below —
|
|
43
|
+
// which is the entire value of exposing it: a caller asking "would this wallet's vote
|
|
44
|
+
// count?" can never drift from what the gate actually does. Every leaf is scored here
|
|
45
|
+
// (`collectAll`), because naming each failure is what this call is for.
|
|
46
|
+
async checkGates({ address, sampleBlock }) {
|
|
47
|
+
// A leaf whose read FAILED is not a leaf that said no. Under an `any`, refusing the
|
|
48
|
+
// whole check because one rule's RPC timed out would tell a wallet that qualifies
|
|
49
|
+
// through another branch that it is ineligible — over an outage it cannot act on. So
|
|
50
|
+
// a failed read is folded as unknown, and only if the tree cannot be decided without
|
|
51
|
+
// it does the original error surface (never a verdict invented from a missing read).
|
|
52
|
+
let firstError;
|
|
53
|
+
let failed = false;
|
|
54
|
+
const gate = await evaluateGate({
|
|
55
|
+
node: criteria.gate,
|
|
56
|
+
evaluate: scoreLeaf({ address, sampleBlock }),
|
|
57
|
+
collectAll: true,
|
|
58
|
+
tolerateLeafErrors: true,
|
|
59
|
+
onLeafError: (_leaf, error) => {
|
|
60
|
+
if (!failed)
|
|
61
|
+
[failed, firstError] = [true, error];
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
if (gate.satisfied === undefined)
|
|
65
|
+
throw firstError;
|
|
66
|
+
return gate;
|
|
67
|
+
},
|
|
40
68
|
async verify(bundle) {
|
|
41
69
|
const offline = await verifyOffline(bundle);
|
|
42
70
|
if (!offline.valid)
|
|
43
71
|
return offline;
|
|
44
|
-
// 3. Gate (chain) —
|
|
45
|
-
// sees fit
|
|
46
|
-
// block
|
|
72
|
+
// 3. Gate (chain) — each leaf rule scores this wallet, reading and memoizing however
|
|
73
|
+
// it sees fit, and `all`/`any` fold the answers (rules/gate.ts). The bundle's
|
|
74
|
+
// bucketized sample block is handed over as the pinned block the ballot names; a
|
|
75
|
+
// rule scoring current state ignores it for `ctx.head`. Lazy on this path: a
|
|
76
|
+
// determined tree stops, so a composite gate costs only the leaves it needed.
|
|
47
77
|
const sampleBlock = bucketMath.sampleBlockForBucket(bucketMath.bucketForBlock(bundle.blockNumber));
|
|
48
|
-
const gate = await
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ctx
|
|
78
|
+
const gate = await evaluateGate({
|
|
79
|
+
node: criteria.gate,
|
|
80
|
+
evaluate: scoreLeaf({ address: bundle.address, sampleBlock })
|
|
52
81
|
});
|
|
53
|
-
|
|
54
|
-
if (gateFailed) {
|
|
82
|
+
if (gate.satisfied !== true) {
|
|
55
83
|
// Disposition comes from the rule's own answer, never from the rule's identity.
|
|
56
84
|
// A failure the rule stands behind is identical on every honest verifier, so it is
|
|
57
85
|
// a `reject`: the sender is penalized and the verdict cached as terminal. One it
|
|
@@ -61,13 +89,16 @@ export function makeBundleVerifier(deps) {
|
|
|
61
89
|
// uncached, so it is re-judged rather than frozen (the same treatment community
|
|
62
90
|
// name resolution has always had, step 4 below).
|
|
63
91
|
//
|
|
64
|
-
// The reason is the
|
|
65
|
-
// holds too few, holds none, or faces a contract that gates nothing, and
|
|
66
|
-
//
|
|
92
|
+
// The reason is the RULES' wording, verbatim: only they know whether this wallet
|
|
93
|
+
// holds too few, holds none, or faces a contract that gates nothing, and those
|
|
94
|
+
// sentences are what reach the voter through `VoteEvictedError` — narrowed to the
|
|
95
|
+
// failures that actually explain the refusal (`gateBlame`), so a wallet is never
|
|
96
|
+
// told to go acquire something a satisfied `any` branch never needed.
|
|
67
97
|
return {
|
|
68
98
|
valid: false,
|
|
69
|
-
disposition:
|
|
70
|
-
reason: `not admitted: ${
|
|
99
|
+
disposition: gatePenalize(gate) ? "reject" : "ignore",
|
|
100
|
+
reason: `not admitted: ${gateReason(gate)}`,
|
|
101
|
+
failures: gateBlame(gate).map((leaf) => ({ type: leaves[leaf.leaf].ref.type, error: leaf.error ?? "" }))
|
|
71
102
|
};
|
|
72
103
|
}
|
|
73
104
|
// 4. Community-name resolution (network) — a carried name is a claim, verified against
|
|
@@ -102,7 +133,7 @@ export function makeBundleVerifier(deps) {
|
|
|
102
133
|
}
|
|
103
134
|
resolvedNames[name] = record.publicKey;
|
|
104
135
|
}
|
|
105
|
-
return { valid: true,
|
|
136
|
+
return { valid: true, resolvedNames };
|
|
106
137
|
}
|
|
107
138
|
};
|
|
108
139
|
}
|
package/dist/verify/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { VotesBundle } from "../schema/votes.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { GateResult } from "../rules/gate.js";
|
|
3
3
|
/**
|
|
4
4
|
* Verification interfaces, design only.
|
|
5
5
|
*
|
|
@@ -36,6 +36,17 @@ export type VerifyFail = {
|
|
|
36
36
|
valid: false;
|
|
37
37
|
disposition: VerdictDisposition;
|
|
38
38
|
reason: string;
|
|
39
|
+
/**
|
|
40
|
+
* Present when the GATE refused: one entry per rule whose failure explains the refusal
|
|
41
|
+
* (rules/gate.ts `gateBlame` — not every failed leaf, since one inside a satisfied `any`
|
|
42
|
+
* cost the wallet nothing). `reason` is these same sentences joined, kept so a caller that
|
|
43
|
+
* only renders a string needs no change; this is the structured form, so a client listing
|
|
44
|
+
* "what you are missing" never has to split one.
|
|
45
|
+
*/
|
|
46
|
+
failures?: readonly {
|
|
47
|
+
type: string;
|
|
48
|
+
error: string;
|
|
49
|
+
}[];
|
|
39
50
|
};
|
|
40
51
|
export type VerifyResult = VerifyOk | VerifyFail;
|
|
41
52
|
/** Stage 1: ballot signature only. No chain access. */
|
|
@@ -52,17 +63,20 @@ export interface OfflineBundleVerifier {
|
|
|
52
63
|
}): Promise<VerifyResult>;
|
|
53
64
|
}
|
|
54
65
|
/**
|
|
55
|
-
* A passing full-bundle verdict. Beyond `valid: true` it carries the work the gate already
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
66
|
+
* A passing full-bundle verdict. Beyond `valid: true` it carries the work the gate already did so
|
|
67
|
+
* downstream stages need not redo it: `resolvedNames` maps each vote that carried a
|
|
68
|
+
* `community.name` to the `publicKey` the name resolved to (equal to the claimed key, since a
|
|
69
|
+
* mismatch fails the gate). Votes with no name are absent. Lets a UI show a verified name without
|
|
70
|
+
* re-resolving.
|
|
71
|
+
*
|
|
72
|
+
* The gate's folded SCORE is deliberately not here. Nothing downstream reads it — a vote's
|
|
73
|
+
* magnitude comes from `criteria.weight`, never from the gate — and a number no one consumes grows
|
|
74
|
+
* semantics by accident, which a min-across-`all` fold over unrelated rules ("holds 5" and "not
|
|
75
|
+
* banned = 1") cannot survive. A client that wants per-rule scores asks `checkEligibility`, which
|
|
76
|
+
* reports each leaf's own.
|
|
62
77
|
*/
|
|
63
78
|
export interface BundleVerdictValid {
|
|
64
79
|
valid: true;
|
|
65
|
-
ruleScore: bigint;
|
|
66
80
|
resolvedNames: Record<string, string>;
|
|
67
81
|
}
|
|
68
82
|
/**
|
|
@@ -85,25 +99,32 @@ export interface BundleVerifier {
|
|
|
85
99
|
*/
|
|
86
100
|
verifyOffline(bundle: VotesBundle): Promise<VerifyResult>;
|
|
87
101
|
/**
|
|
88
|
-
* Step 3 alone, for a wallet rather than a bundle:
|
|
89
|
-
* {@link
|
|
90
|
-
* count?" through the very same rule
|
|
91
|
-
* the forward gate uses — never a reimplementation of them.
|
|
102
|
+
* Step 3 alone, for a wallet rather than a bundle: score EVERY leaf of the gate tree and hand
|
|
103
|
+
* back the folded {@link GateResult}. Backs `Contest.checkEligibility`, so a client can ask
|
|
104
|
+
* "would this vote count?" through the very same rule instances, options, chain clients, head
|
|
105
|
+
* reader and memos the forward gate uses — never a reimplementation of them.
|
|
106
|
+
*
|
|
107
|
+
* Unlike `verify`, this never short-circuits: a caller asking which rules a wallet fails needs
|
|
108
|
+
* all of their answers, not just the first one that settled the outcome.
|
|
109
|
+
*
|
|
110
|
+
* Also unlike `verify`, a leaf whose chain read THROWS does not fail the call outright — it is
|
|
111
|
+
* folded as unknown, so a wallet admitted by a branch that did answer still gets its answer.
|
|
112
|
+
* The error is re-thrown only when the tree cannot be decided without that leaf.
|
|
92
113
|
*
|
|
93
114
|
* `sampleBlock` is the pinned block the prospective ballot would name (the caller's current
|
|
94
115
|
* bucket). A head-scoring rule ignores it exactly as it does during verification.
|
|
95
116
|
*/
|
|
96
|
-
|
|
117
|
+
checkGates(args: {
|
|
97
118
|
address: string;
|
|
98
119
|
sampleBlock: number;
|
|
99
|
-
}): Promise<
|
|
120
|
+
}): Promise<GateResult>;
|
|
100
121
|
}
|
|
101
122
|
/**
|
|
102
123
|
* The per-bundle record of the two deferred *network* checks. The offline checks (signature,
|
|
103
124
|
* constraints) are never recorded here — they are synchronous preconditions for admission, so
|
|
104
125
|
* an admitted bundle has always passed them.
|
|
105
126
|
*
|
|
106
|
-
* - `chainVerified`: the gate
|
|
127
|
+
* - `chainVerified`: the gate admitted the wallet at the bucket block. `false`
|
|
107
128
|
* means "not yet read", never "failed" — a failed gate evicts the bundle instead.
|
|
108
129
|
* - `nameResolved`: `undefined` when the bundle carries no `community.name`; `false` while
|
|
109
130
|
* the carried name is unresolved; `true` once it resolved to the claimed `publicKey`. A
|
package/package.json
CHANGED
package/dist/chain/ticker.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Criteria, RuleRef } from "../schema/criteria.js";
|
|
2
|
-
/**
|
|
3
|
-
* Resolve which chain a rule reads. A rule's parsed options may name a
|
|
4
|
-
* `chain` ticker (e.g. `erc5192-min-balance` -> "base"); a chainless rule (e.g.
|
|
5
|
-
* `constant`) names none, so callers fall back to the first configured chain. Shared by the
|
|
6
|
-
* verifier, the tally, and the facade so the fallback rule stays identical everywhere.
|
|
7
|
-
*/
|
|
8
|
-
/** The `chain` ticker named in a rule's parsed options, or `undefined` if none. */
|
|
9
|
-
export declare function chainTickerOf(options: unknown): string | undefined;
|
|
10
|
-
/**
|
|
11
|
-
* The chain ticker a rule ref uses: its own `chain` option, else the first chain in
|
|
12
|
-
* `requires.chains`. Throws if neither exists (a chainless rule with no configured
|
|
13
|
-
* chains cannot be read).
|
|
14
|
-
*/
|
|
15
|
-
export declare function tickerForRef(criteria: Criteria, ref: RuleRef, options: unknown): string;
|
package/dist/chain/ticker.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
/**
|
|
3
|
-
* Resolve which chain a rule reads. A rule's parsed options may name a
|
|
4
|
-
* `chain` ticker (e.g. `erc5192-min-balance` -> "base"); a chainless rule (e.g.
|
|
5
|
-
* `constant`) names none, so callers fall back to the first configured chain. Shared by the
|
|
6
|
-
* verifier, the tally, and the facade so the fallback rule stays identical everywhere.
|
|
7
|
-
*/
|
|
8
|
-
/** The `chain` ticker named in a rule's parsed options, or `undefined` if none. */
|
|
9
|
-
export function chainTickerOf(options) {
|
|
10
|
-
const parsed = z.object({ chain: z.string().min(1) }).safeParse(options);
|
|
11
|
-
return parsed.success ? parsed.data.chain : undefined;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* The chain ticker a rule ref uses: its own `chain` option, else the first chain in
|
|
15
|
-
* `requires.chains`. Throws if neither exists (a chainless rule with no configured
|
|
16
|
-
* chains cannot be read).
|
|
17
|
-
*/
|
|
18
|
-
export function tickerForRef(criteria, ref, options) {
|
|
19
|
-
const ticker = chainTickerOf(options) ?? Object.keys(criteria.requires.chains)[0];
|
|
20
|
-
if (!ticker) {
|
|
21
|
-
throw new Error(`criteria rule "${ref.type}" names no chain and requires.chains is empty; ` +
|
|
22
|
-
`cannot resolve a chain client to read it`);
|
|
23
|
-
}
|
|
24
|
-
return ticker;
|
|
25
|
-
}
|