metered-protocol 1.0.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/LICENSE +21 -0
- package/README.md +103 -0
- package/contracts/metered.ag +135 -0
- package/contracts/metered_ag.tests.json +1226 -0
- package/contracts/metered_session.sil +237 -0
- package/contracts/metered_session.tests.json +1113 -0
- package/dist/contracts/metered.ag +135 -0
- package/dist/contracts/metered_ag.tests.json +1226 -0
- package/dist/contracts/metered_session.sil +237 -0
- package/dist/contracts/metered_session.tests.json +1113 -0
- package/dist/src/bias.d.ts +48 -0
- package/dist/src/bias.d.ts.map +1 -0
- package/dist/src/bias.js +43 -0
- package/dist/src/bias.js.map +1 -0
- package/dist/src/checkpoint.d.ts +57 -0
- package/dist/src/checkpoint.d.ts.map +1 -0
- package/dist/src/checkpoint.js +84 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/deadline.d.ts +71 -0
- package/dist/src/deadline.d.ts.map +1 -0
- package/dist/src/deadline.js +77 -0
- package/dist/src/deadline.js.map +1 -0
- package/dist/src/encoding.d.ts +72 -0
- package/dist/src/encoding.d.ts.map +1 -0
- package/dist/src/encoding.js +187 -0
- package/dist/src/encoding.js.map +1 -0
- package/dist/src/history.d.ts +33 -0
- package/dist/src/history.d.ts.map +1 -0
- package/dist/src/history.js +38 -0
- package/dist/src/history.js.map +1 -0
- package/dist/src/http/buyer.d.ts +57 -0
- package/dist/src/http/buyer.d.ts.map +1 -0
- package/dist/src/http/buyer.js +151 -0
- package/dist/src/http/buyer.js.map +1 -0
- package/dist/src/http/client.d.ts +55 -0
- package/dist/src/http/client.d.ts.map +1 -0
- package/dist/src/http/client.js +83 -0
- package/dist/src/http/client.js.map +1 -0
- package/dist/src/http/harness.d.ts +18 -0
- package/dist/src/http/harness.d.ts.map +1 -0
- package/dist/src/http/harness.js +16 -0
- package/dist/src/http/harness.js.map +1 -0
- package/dist/src/http/protocol.d.ts +84 -0
- package/dist/src/http/protocol.d.ts.map +1 -0
- package/dist/src/http/protocol.js +22 -0
- package/dist/src/http/protocol.js.map +1 -0
- package/dist/src/http/provider.d.ts +123 -0
- package/dist/src/http/provider.d.ts.map +1 -0
- package/dist/src/http/provider.js +197 -0
- package/dist/src/http/provider.js.map +1 -0
- package/dist/src/http/serve.d.ts +34 -0
- package/dist/src/http/serve.d.ts.map +1 -0
- package/dist/src/http/serve.js +171 -0
- package/dist/src/http/serve.js.map +1 -0
- package/dist/src/http/service.d.ts +86 -0
- package/dist/src/http/service.d.ts.map +1 -0
- package/dist/src/http/service.js +161 -0
- package/dist/src/http/service.js.map +1 -0
- package/dist/src/index.d.ts +68 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +62 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/meter.d.ts +39 -0
- package/dist/src/meter.d.ts.map +1 -0
- package/dist/src/meter.js +100 -0
- package/dist/src/meter.js.map +1 -0
- package/dist/src/offer.d.ts +6 -0
- package/dist/src/offer.d.ts.map +1 -0
- package/dist/src/offer.js +92 -0
- package/dist/src/offer.js.map +1 -0
- package/dist/src/reconcile.d.ts +18 -0
- package/dist/src/reconcile.d.ts.map +1 -0
- package/dist/src/reconcile.js +101 -0
- package/dist/src/reconcile.js.map +1 -0
- package/dist/src/reservation.d.ts +51 -0
- package/dist/src/reservation.d.ts.map +1 -0
- package/dist/src/reservation.js +124 -0
- package/dist/src/reservation.js.map +1 -0
- package/dist/src/session.d.ts +40 -0
- package/dist/src/session.d.ts.map +1 -0
- package/dist/src/session.js +92 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/signer.d.ts +31 -0
- package/dist/src/signer.d.ts.map +1 -0
- package/dist/src/signer.js +57 -0
- package/dist/src/signer.js.map +1 -0
- package/dist/src/store.d.ts +30 -0
- package/dist/src/store.d.ts.map +1 -0
- package/dist/src/store.js +120 -0
- package/dist/src/store.js.map +1 -0
- package/dist/src/types.d.ts +80 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +9 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tools/anchor-cli.d.ts +2 -0
- package/dist/tools/anchor-cli.d.ts.map +1 -0
- package/dist/tools/anchor-cli.js +101 -0
- package/dist/tools/anchor-cli.js.map +1 -0
- package/dist/tools/anchor.d.ts +68 -0
- package/dist/tools/anchor.d.ts.map +1 -0
- package/dist/tools/anchor.js +136 -0
- package/dist/tools/anchor.js.map +1 -0
- package/dist/tools/chain.d.ts +18 -0
- package/dist/tools/chain.d.ts.map +1 -0
- package/dist/tools/chain.js +18 -0
- package/dist/tools/chain.js.map +1 -0
- package/dist/tools/conformance.d.ts +2 -0
- package/dist/tools/conformance.d.ts.map +1 -0
- package/dist/tools/conformance.js +220 -0
- package/dist/tools/conformance.js.map +1 -0
- package/dist/tools/contracts.d.ts +2 -0
- package/dist/tools/contracts.d.ts.map +1 -0
- package/dist/tools/contracts.js +68 -0
- package/dist/tools/contracts.js.map +1 -0
- package/dist/tools/covenant-binding.d.ts +40 -0
- package/dist/tools/covenant-binding.d.ts.map +1 -0
- package/dist/tools/covenant-binding.js +60 -0
- package/dist/tools/covenant-binding.js.map +1 -0
- package/dist/tools/covenant-profile.d.ts +41 -0
- package/dist/tools/covenant-profile.d.ts.map +1 -0
- package/dist/tools/covenant-profile.js +74 -0
- package/dist/tools/covenant-profile.js.map +1 -0
- package/dist/tools/covenant.d.ts +30 -0
- package/dist/tools/covenant.d.ts.map +1 -0
- package/dist/tools/covenant.js +54 -0
- package/dist/tools/covenant.js.map +1 -0
- package/dist/tools/demo-report.d.ts +24 -0
- package/dist/tools/demo-report.d.ts.map +1 -0
- package/dist/tools/demo-report.js +40 -0
- package/dist/tools/demo-report.js.map +1 -0
- package/dist/tools/demo.d.ts +2 -0
- package/dist/tools/demo.d.ts.map +1 -0
- package/dist/tools/demo.js +204 -0
- package/dist/tools/demo.js.map +1 -0
- package/dist/tools/dust-map.d.ts +2 -0
- package/dist/tools/dust-map.d.ts.map +1 -0
- package/dist/tools/dust-map.js +63 -0
- package/dist/tools/dust-map.js.map +1 -0
- package/dist/tools/expire-cases.d.ts +4 -0
- package/dist/tools/expire-cases.d.ts.map +1 -0
- package/dist/tools/expire-cases.js +211 -0
- package/dist/tools/expire-cases.js.map +1 -0
- package/dist/tools/fee-check.d.ts +2 -0
- package/dist/tools/fee-check.d.ts.map +1 -0
- package/dist/tools/fee-check.js +127 -0
- package/dist/tools/fee-check.js.map +1 -0
- package/dist/tools/fixtures.d.ts +39 -0
- package/dist/tools/fixtures.d.ts.map +1 -0
- package/dist/tools/fixtures.js +31 -0
- package/dist/tools/fixtures.js.map +1 -0
- package/dist/tools/gen-contract-tests.d.ts +2 -0
- package/dist/tools/gen-contract-tests.d.ts.map +1 -0
- package/dist/tools/gen-contract-tests.js +170 -0
- package/dist/tools/gen-contract-tests.js.map +1 -0
- package/dist/tools/kaspa.d.ts +13 -0
- package/dist/tools/kaspa.d.ts.map +1 -0
- package/dist/tools/kaspa.js +76 -0
- package/dist/tools/kaspa.js.map +1 -0
- package/dist/tools/live-deadline.d.ts +2 -0
- package/dist/tools/live-deadline.d.ts.map +1 -0
- package/dist/tools/live-deadline.js +122 -0
- package/dist/tools/live-deadline.js.map +1 -0
- package/dist/tools/live-expire.d.ts +2 -0
- package/dist/tools/live-expire.d.ts.map +1 -0
- package/dist/tools/live-expire.js +127 -0
- package/dist/tools/live-expire.js.map +1 -0
- package/dist/tools/live-settle.d.ts +2 -0
- package/dist/tools/live-settle.d.ts.map +1 -0
- package/dist/tools/live-settle.js +226 -0
- package/dist/tools/live-settle.js.map +1 -0
- package/dist/tools/live-steps.d.ts +63 -0
- package/dist/tools/live-steps.d.ts.map +1 -0
- package/dist/tools/live-steps.js +155 -0
- package/dist/tools/live-steps.js.map +1 -0
- package/dist/tools/model.d.ts +32 -0
- package/dist/tools/model.d.ts.map +1 -0
- package/dist/tools/model.js +99 -0
- package/dist/tools/model.js.map +1 -0
- package/dist/tools/send.d.ts +2 -0
- package/dist/tools/send.d.ts.map +1 -0
- package/dist/tools/send.js +41 -0
- package/dist/tools/send.js.map +1 -0
- package/dist/tools/session-chain.d.ts +69 -0
- package/dist/tools/session-chain.d.ts.map +1 -0
- package/dist/tools/session-chain.js +113 -0
- package/dist/tools/session-chain.js.map +1 -0
- package/dist/tools/sighash.d.ts +9 -0
- package/dist/tools/sighash.d.ts.map +1 -0
- package/dist/tools/sighash.js +68 -0
- package/dist/tools/sighash.js.map +1 -0
- package/dist/tools/sigscript-check.d.ts +2 -0
- package/dist/tools/sigscript-check.d.ts.map +1 -0
- package/dist/tools/sigscript-check.js +113 -0
- package/dist/tools/sigscript-check.js.map +1 -0
- package/dist/tools/sigscript.d.ts +61 -0
- package/dist/tools/sigscript.d.ts.map +1 -0
- package/dist/tools/sigscript.js +70 -0
- package/dist/tools/sigscript.js.map +1 -0
- package/dist/tools/state-layout.d.ts +2 -0
- package/dist/tools/state-layout.d.ts.map +1 -0
- package/dist/tools/state-layout.js +57 -0
- package/dist/tools/state-layout.js.map +1 -0
- package/package.json +59 -0
- package/spec/CONFORMANCE.md +80 -0
- package/spec/SPEC.md +821 -0
- package/spec/conformance-vectors.json +677 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session driver -- the pieces of SPEC.md §§3-8, in the order a real chunk goes through them.
|
|
3
|
+
*
|
|
4
|
+
* Nothing new is decided here. Every rule lives in its own module and this composes them, which is
|
|
5
|
+
* deliberate: the rules are testable without a session, and the ORDER is testable without
|
|
6
|
+
* re-testing the rules.
|
|
7
|
+
*
|
|
8
|
+
* THE ONE THING WORTH READING TWICE. A Reservation's `cumulativeSompi` and a State's are different
|
|
9
|
+
* numbers and must not be conflated. The Reservation authorises `units × unitPriceSompi` -- what
|
|
10
|
+
* the buyer is willing to be exposed to for this chunk. The State settles `min(buyer, provider) ×
|
|
11
|
+
* unitPriceSompi` -- what was actually delivered, per §5 rule 6. Authorised is a ceiling, billed
|
|
12
|
+
* is a fact, and billing the authorised figure would quietly discard rule 6 and with it the
|
|
13
|
+
* entire reason the counts are two-sided.
|
|
14
|
+
*/
|
|
15
|
+
import { digestHex } from './encoding.js';
|
|
16
|
+
import { acceptReservation, priceOf } from './reservation.js';
|
|
17
|
+
import { reconcileBabel } from './reconcile.js';
|
|
18
|
+
import { newBiasState, observeResidual, biasAlarm } from './bias.js';
|
|
19
|
+
import { signStateWithObligations } from './signer.js';
|
|
20
|
+
export class SessionHalted extends Error {
|
|
21
|
+
halt;
|
|
22
|
+
constructor(halt) {
|
|
23
|
+
super(`${halt.reason}: ${halt.detail}`);
|
|
24
|
+
this.halt = halt;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Drives one session for BOTH parties, which is what a test needs and what a real deployment
|
|
29
|
+
* splits in half. Keeping them together here is the only way to assert that the two halves agree.
|
|
30
|
+
*/
|
|
31
|
+
export class Session {
|
|
32
|
+
offer;
|
|
33
|
+
buyerStore;
|
|
34
|
+
providerStore;
|
|
35
|
+
checkpointer;
|
|
36
|
+
cursor = null;
|
|
37
|
+
bias = newBiasState();
|
|
38
|
+
constructor(offer, buyerStore, providerStore, checkpointer) {
|
|
39
|
+
this.offer = offer;
|
|
40
|
+
this.buyerStore = buyerStore;
|
|
41
|
+
this.providerStore = providerStore;
|
|
42
|
+
this.checkpointer = checkpointer;
|
|
43
|
+
}
|
|
44
|
+
get residualState() {
|
|
45
|
+
return this.bias;
|
|
46
|
+
}
|
|
47
|
+
get cumulativeSompi() {
|
|
48
|
+
return this.cursor?.cumulativeSompi ?? 0;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Run one chunk end to end: reserve, reconcile, bill, doubly sign, checkpoint.
|
|
52
|
+
*
|
|
53
|
+
* Throws SessionHalted on any rule failure. §1: "The only remedy for disagreement is to stop."
|
|
54
|
+
*/
|
|
55
|
+
chunk(reservation, buyerMeasurement, providerMeasurement, keys) {
|
|
56
|
+
acceptReservation(this.offer, reservation, this.cursor);
|
|
57
|
+
const outcome = reconcileBabel(this.offer, buyerMeasurement, providerMeasurement, reservation.seq);
|
|
58
|
+
if (!outcome.ok)
|
|
59
|
+
throw new SessionHalted(outcome);
|
|
60
|
+
// §5.1.1. The residual is observed BEFORE the State is signed, so an alarming chunk is never
|
|
61
|
+
// settled -- halting after signing would leave a signed State the counterparty could close on.
|
|
62
|
+
this.bias = observeResidual(this.bias, outcome.residual);
|
|
63
|
+
if (biasAlarm(this.bias)) {
|
|
64
|
+
throw new SessionHalted({
|
|
65
|
+
ok: false,
|
|
66
|
+
reason: 'bias',
|
|
67
|
+
detail: `CUSUM reached ${this.bias.sum} after ${this.bias.babels} chunks`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const state = {
|
|
71
|
+
v: 1,
|
|
72
|
+
sessionId: this.offer.sessionId,
|
|
73
|
+
seq: reservation.seq,
|
|
74
|
+
cumulativeUnits: outcome.billedCumulativeUnits,
|
|
75
|
+
cumulativeSompi: priceOf(this.offer, outcome.billedCumulativeUnits),
|
|
76
|
+
prevState: this.cursor?.stateDigest ?? null,
|
|
77
|
+
};
|
|
78
|
+
// Both parties sign the same bytes, each under its own obligations (§4). Neither signature
|
|
79
|
+
// depends on the other existing, so the order here carries no meaning.
|
|
80
|
+
const buyerSig = signStateWithObligations(this.buyerStore, state, keys.buyerSk);
|
|
81
|
+
const providerSig = signStateWithObligations(this.providerStore, state, keys.providerSk);
|
|
82
|
+
const checkpoint = this.checkpointer.record(this.offer, state);
|
|
83
|
+
this.cursor = {
|
|
84
|
+
seq: state.seq,
|
|
85
|
+
cumulativeUnits: state.cumulativeUnits,
|
|
86
|
+
cumulativeSompi: state.cumulativeSompi,
|
|
87
|
+
stateDigest: digestHex(state),
|
|
88
|
+
};
|
|
89
|
+
return { state, buyerSig, providerSig, billedUnits: outcome.billedUnits, checkpointed: checkpoint !== null };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { State } from './types.js';
|
|
2
|
+
/** What a party must durably hold. Small on purpose: this gets written on every chunk. */
|
|
3
|
+
export interface SignerRecord {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
/** Highest seq this party has signed. -1 before anything is signed. */
|
|
6
|
+
highestSeq: number;
|
|
7
|
+
/** Digest of the State at `highestSeq`, which the next State's `prevState` must match. */
|
|
8
|
+
lastStateDigest: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Durable storage. Synchronous by design -- an async save invites a caller to forget the await,
|
|
12
|
+
* which turns record-then-send back into send-then-record silently.
|
|
13
|
+
*/
|
|
14
|
+
export interface SignerStore {
|
|
15
|
+
load(sessionId: string): SignerRecord | null;
|
|
16
|
+
save(record: SignerRecord): void;
|
|
17
|
+
}
|
|
18
|
+
export declare class SignerObligationError extends Error {
|
|
19
|
+
}
|
|
20
|
+
export declare const emptyRecord: (sessionId: string) => SignerRecord;
|
|
21
|
+
/**
|
|
22
|
+
* Sign a State, or refuse. The refusals ARE the feature.
|
|
23
|
+
*
|
|
24
|
+
* §4.4's restart rule needs no separate code path: the record is loaded from the store on every
|
|
25
|
+
* call, so a fresh process behaves exactly like a long-running one. A restart that forgets is a
|
|
26
|
+
* store that lost data, not a signer that skipped a step.
|
|
27
|
+
*/
|
|
28
|
+
export declare function signStateWithObligations(store: SignerStore, state: State, privateKeyHex: string): string;
|
|
29
|
+
/** An in-memory store. Fine for tests; a real party needs something that survives a crash. */
|
|
30
|
+
export declare function memoryStore(): SignerStore;
|
|
31
|
+
//# sourceMappingURL=signer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../../src/signer.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,0FAA0F;AAC1F,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;CAClC;AAED,qBAAa,qBAAsB,SAAQ,KAAK;CAAG;AAEnD,eAAO,MAAM,WAAW,GAAI,WAAW,MAAM,KAAG,YACQ,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,KAAK,EACZ,aAAa,EAAE,MAAM,GACpB,MAAM,CA4BR;AAED,8FAA8F;AAC9F,wBAAgB,WAAW,IAAI,WAAW,CAQzC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPEC.md §4 -- the signer obligations.
|
|
3
|
+
*
|
|
4
|
+
* These are the rules that lose money without any message being malformed. Nothing here is
|
|
5
|
+
* checkable by a counterparty and nothing here appears on the wire; a party that ignores them
|
|
6
|
+
* produces perfectly valid States and then gets closed against on a stale one.
|
|
7
|
+
*
|
|
8
|
+
* RECORD-THEN-SEND, never send-then-record. §4.2 is the whole reason this module holds a store
|
|
9
|
+
* rather than a variable. The dangerous interleaving is: sign, transmit, crash, restart, and sign
|
|
10
|
+
* a DIFFERENT State at the same seq because the first was never written down. The counterparty
|
|
11
|
+
* now holds two signed States at one seq and picks whichever pays it more, which is §4.1's
|
|
12
|
+
* violation arriving through the back door. So the write happens before the signature is returned
|
|
13
|
+
* to the caller, and a store that throws means no signature was ever handed out.
|
|
14
|
+
*/
|
|
15
|
+
import { digestHex, signState } from './encoding.js';
|
|
16
|
+
export class SignerObligationError extends Error {
|
|
17
|
+
}
|
|
18
|
+
export const emptyRecord = (sessionId) => ({ sessionId, highestSeq: -1, lastStateDigest: null });
|
|
19
|
+
/**
|
|
20
|
+
* Sign a State, or refuse. The refusals ARE the feature.
|
|
21
|
+
*
|
|
22
|
+
* §4.4's restart rule needs no separate code path: the record is loaded from the store on every
|
|
23
|
+
* call, so a fresh process behaves exactly like a long-running one. A restart that forgets is a
|
|
24
|
+
* store that lost data, not a signer that skipped a step.
|
|
25
|
+
*/
|
|
26
|
+
export function signStateWithObligations(store, state, privateKeyHex) {
|
|
27
|
+
const record = store.load(state.sessionId) ?? emptyRecord(state.sessionId);
|
|
28
|
+
// §4.1 and §4.4: at most one State per seq, and never at or below the highest already signed.
|
|
29
|
+
if (state.seq <= record.highestSeq) {
|
|
30
|
+
throw new SignerObligationError(`refusing to sign seq ${state.seq}: already signed ${record.highestSeq} for this session`);
|
|
31
|
+
}
|
|
32
|
+
// §4.3: the chain must be unbroken. A State whose prevState does not match what this party last
|
|
33
|
+
// agreed to is either a fork or a splice, and signing it endorses a history it never saw.
|
|
34
|
+
if (state.prevState !== record.lastStateDigest) {
|
|
35
|
+
throw new SignerObligationError(`refusing to sign seq ${state.seq}: prevState ${state.prevState ?? 'null'} does not chain to ` +
|
|
36
|
+
`${record.lastStateDigest ?? 'null'}`);
|
|
37
|
+
}
|
|
38
|
+
// §4.2: RECORD, then sign, then return. The signature does not escape this function until the
|
|
39
|
+
// store has accepted the write -- so a throw here means nothing was ever transmitted.
|
|
40
|
+
store.save({
|
|
41
|
+
sessionId: state.sessionId,
|
|
42
|
+
highestSeq: state.seq,
|
|
43
|
+
lastStateDigest: digestHex(state),
|
|
44
|
+
});
|
|
45
|
+
return signState(state, privateKeyHex);
|
|
46
|
+
}
|
|
47
|
+
/** An in-memory store. Fine for tests; a real party needs something that survives a crash. */
|
|
48
|
+
export function memoryStore() {
|
|
49
|
+
const rows = new Map();
|
|
50
|
+
return {
|
|
51
|
+
load: (sessionId) => rows.get(sessionId) ?? null,
|
|
52
|
+
save: (record) => {
|
|
53
|
+
rows.set(record.sessionId, { ...record });
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=signer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.js","sourceRoot":"","sources":["../../src/signer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAqBrD,MAAM,OAAO,qBAAsB,SAAQ,KAAK;CAAG;AAEnD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,SAAiB,EAAgB,EAAE,CAC7D,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CACtC,KAAkB,EAClB,KAAY,EACZ,aAAqB;IAErB,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAE3E,8FAA8F;IAC9F,IAAI,KAAK,CAAC,GAAG,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACnC,MAAM,IAAI,qBAAqB,CAC7B,wBAAwB,KAAK,CAAC,GAAG,oBAAoB,MAAM,CAAC,UAAU,mBAAmB,CAC1F,CAAC;IACJ,CAAC;IAED,gGAAgG;IAChG,0FAA0F;IAC1F,IAAI,KAAK,CAAC,SAAS,KAAK,MAAM,CAAC,eAAe,EAAE,CAAC;QAC/C,MAAM,IAAI,qBAAqB,CAC7B,wBAAwB,KAAK,CAAC,GAAG,eAAe,KAAK,CAAC,SAAS,IAAI,MAAM,qBAAqB;YAC5F,GAAG,MAAM,CAAC,eAAe,IAAI,MAAM,EAAE,CACxC,CAAC;IACJ,CAAC;IAED,8FAA8F;IAC9F,sFAAsF;IACtF,KAAK,CAAC,IAAI,CAAC;QACT,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,UAAU,EAAE,KAAK,CAAC,GAAG;QACrB,eAAe,EAAE,SAAS,CAAC,KAAK,CAAC;KAClC,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;AACzC,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,WAAW;IACzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC7C,OAAO;QACL,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI;QAChD,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE;YACf,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;QAC5C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SignerStore } from './signer.js';
|
|
2
|
+
import type { SessionHistory } from './history.js';
|
|
3
|
+
import type { SessionSnapshot } from './http/provider.js';
|
|
4
|
+
/**
|
|
5
|
+
* A SignerStore that survives a restart, and a power cut.
|
|
6
|
+
*
|
|
7
|
+
* Later records win: the log is a history of what was signed, and §4.4 only ever moves `highestSeq`
|
|
8
|
+
* upward, so replaying it in order lands on the same state the process held.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fileStore(path: string): SignerStore;
|
|
11
|
+
/** A SessionHistory that survives a restart. SPEC.md §3.1a; see src/history.ts for why it matters. */
|
|
12
|
+
export declare function fileHistory(path: string): SessionHistory;
|
|
13
|
+
/** Durable storage for open sessions, so a restart does not make every buyer start again. */
|
|
14
|
+
export interface SessionStore {
|
|
15
|
+
load(sessionId: string): SessionSnapshot | null;
|
|
16
|
+
save(snapshot: SessionSnapshot): void;
|
|
17
|
+
drop(sessionId: string): void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sessions that survive a restart.
|
|
21
|
+
*
|
|
22
|
+
* Weaker guarantees than `fileStore`, deliberately. A signer record must be durable BEFORE a
|
|
23
|
+
* signature exists, because losing one lets a party sign twice at the same seq; losing a session
|
|
24
|
+
* costs at most the babel in flight, which the buyer simply reserves again. So this appends
|
|
25
|
+
* without fsync -- fast enough to write on every message -- and the last few entries may be lost
|
|
26
|
+
* to a power cut. That is a considered trade, not an oversight: paying for durability here would
|
|
27
|
+
* put a disk flush on the path of every delivery, in exchange for something nobody loses money to.
|
|
28
|
+
*/
|
|
29
|
+
export declare function fileSessionStore(path: string): SessionStore;
|
|
30
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/store.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AA+B1D;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAanD;AAED,sGAAsG;AACtG,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAaxD;AAGD,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IACtC,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAa3D"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable stores for the two obligations that lose something when a process forgets.
|
|
3
|
+
*
|
|
4
|
+
* SPEC.md §4.2 (record-then-send) and §4.4 (the restart rule) are already implemented by
|
|
5
|
+
* `signer.ts`: it loads from the store on every call, so a fresh process behaves exactly like a
|
|
6
|
+
* long-running one, and "a restart that forgets is a store that lost data, not a signer that
|
|
7
|
+
* skipped a step." This is the store that does not lose data. `memoryStore()` is the one that
|
|
8
|
+
* does, and it is honest about it.
|
|
9
|
+
*
|
|
10
|
+
* SPEC.md §3.1a needs the same thing for a different reason: a buyer that forgets which session
|
|
11
|
+
* identifiers it has seen is unprotected against a provider that waits for it to restart.
|
|
12
|
+
*
|
|
13
|
+
* WHY APPEND-ONLY, AND WHY fsync.
|
|
14
|
+
*
|
|
15
|
+
* A signature must not exist unless the record is already durable. Rewriting a whole JSON file
|
|
16
|
+
* per save has a window where the old file is gone and the new one is not yet written; a crash
|
|
17
|
+
* inside it loses EVERY record, not one. Appending a line cannot lose what came before.
|
|
18
|
+
*
|
|
19
|
+
* Writing without fsync puts the record in the operating system's buffer, not on the disk. The
|
|
20
|
+
* process crashing is survivable either way; the machine losing power is not. §4.2 says the
|
|
21
|
+
* write happens before the signature is returned, and a buffered write has not happened.
|
|
22
|
+
*
|
|
23
|
+
* A TORN FINAL LINE IS DISCARDED, and that is correct rather than merely convenient: if the
|
|
24
|
+
* append did not complete, `save` did not return, so no signature was ever handed out. The
|
|
25
|
+
* record describes something that never happened.
|
|
26
|
+
*/
|
|
27
|
+
import { appendFileSync, closeSync, existsSync, fsyncSync, openSync, readFileSync, writeSync } from 'node:fs';
|
|
28
|
+
/** Read a JSONL file, discarding any trailing line that a crash cut in half. */
|
|
29
|
+
function replay(path) {
|
|
30
|
+
if (!existsSync(path))
|
|
31
|
+
return [];
|
|
32
|
+
const rows = [];
|
|
33
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
34
|
+
if (!line.trim())
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
rows.push(JSON.parse(line));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Only the LAST line can legitimately be torn. An unparseable line anywhere else means the
|
|
41
|
+
// file was damaged by something other than a crash, and replaying past it would silently
|
|
42
|
+
// resurrect a stale record -- so stop here and keep what was whole.
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return rows;
|
|
47
|
+
}
|
|
48
|
+
/** Append one record and put it on the disk before returning. */
|
|
49
|
+
function appendDurable(path, row) {
|
|
50
|
+
const fd = openSync(path, 'a');
|
|
51
|
+
try {
|
|
52
|
+
writeSync(fd, `${JSON.stringify(row)}\n`);
|
|
53
|
+
fsyncSync(fd);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
closeSync(fd);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A SignerStore that survives a restart, and a power cut.
|
|
61
|
+
*
|
|
62
|
+
* Later records win: the log is a history of what was signed, and §4.4 only ever moves `highestSeq`
|
|
63
|
+
* upward, so replaying it in order lands on the same state the process held.
|
|
64
|
+
*/
|
|
65
|
+
export function fileStore(path) {
|
|
66
|
+
const rows = new Map();
|
|
67
|
+
for (const r of replay(path))
|
|
68
|
+
rows.set(r.sessionId, r);
|
|
69
|
+
return {
|
|
70
|
+
load: (sessionId) => rows.get(sessionId) ?? null,
|
|
71
|
+
save: (record) => {
|
|
72
|
+
// Disk first, memory second. The other order would let an in-process reader see a record
|
|
73
|
+
// that a crash one instruction later would erase -- which is send-then-record wearing a
|
|
74
|
+
// different hat.
|
|
75
|
+
appendDurable(path, record);
|
|
76
|
+
rows.set(record.sessionId, { ...record });
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** A SessionHistory that survives a restart. SPEC.md §3.1a; see src/history.ts for why it matters. */
|
|
81
|
+
export function fileHistory(path) {
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
const key = (provider, sessionId) => `${provider}/${sessionId}`;
|
|
84
|
+
for (const r of replay(path)) {
|
|
85
|
+
seen.add(key(r.provider, r.sessionId));
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
seen: (provider, sessionId) => seen.has(key(provider, sessionId)),
|
|
89
|
+
record: (provider, sessionId) => {
|
|
90
|
+
appendDurable(path, { provider, sessionId });
|
|
91
|
+
seen.add(key(provider, sessionId));
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Sessions that survive a restart.
|
|
97
|
+
*
|
|
98
|
+
* Weaker guarantees than `fileStore`, deliberately. A signer record must be durable BEFORE a
|
|
99
|
+
* signature exists, because losing one lets a party sign twice at the same seq; losing a session
|
|
100
|
+
* costs at most the babel in flight, which the buyer simply reserves again. So this appends
|
|
101
|
+
* without fsync -- fast enough to write on every message -- and the last few entries may be lost
|
|
102
|
+
* to a power cut. That is a considered trade, not an oversight: paying for durability here would
|
|
103
|
+
* put a disk flush on the path of every delivery, in exchange for something nobody loses money to.
|
|
104
|
+
*/
|
|
105
|
+
export function fileSessionStore(path) {
|
|
106
|
+
const rows = new Map();
|
|
107
|
+
for (const r of replay(path))
|
|
108
|
+
rows.set(r.id, r.snapshot);
|
|
109
|
+
const append = (id, snapshot) => {
|
|
110
|
+
appendFileSync(path, `${JSON.stringify({ id, snapshot })}
|
|
111
|
+
`);
|
|
112
|
+
rows.set(id, snapshot);
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
load: (sessionId) => rows.get(sessionId) ?? null,
|
|
116
|
+
save: (snapshot) => append(snapshot.offer.sessionId, snapshot),
|
|
117
|
+
drop: (sessionId) => append(sessionId, null),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire messages of SPEC.md §3, as types.
|
|
3
|
+
*
|
|
4
|
+
* These are the shapes the covenant measurement pinned. Nothing here should change without
|
|
5
|
+
* changing §3, because `spec/vectors.ts` and `contracts/metered_session.tests.json` both encode
|
|
6
|
+
* the same fields and the golden vector is compared byte for byte.
|
|
7
|
+
*/
|
|
8
|
+
/** SPEC.md §3.1. The provider's signed commitment, carried in the HTTP 402 response. */
|
|
9
|
+
export interface Offer {
|
|
10
|
+
v: 1;
|
|
11
|
+
scheme: 'metered';
|
|
12
|
+
network: string;
|
|
13
|
+
asset: 'KAS';
|
|
14
|
+
sessionId: string;
|
|
15
|
+
unit: string;
|
|
16
|
+
meter: string;
|
|
17
|
+
unitPriceSompi: number;
|
|
18
|
+
babelUnits: number;
|
|
19
|
+
maxBabels: number;
|
|
20
|
+
toleranceAbs: number;
|
|
21
|
+
checkpointEvery: number;
|
|
22
|
+
responseWindowDaa: number;
|
|
23
|
+
buyerPubkey: string;
|
|
24
|
+
providerPubkey: string;
|
|
25
|
+
partiesCommitment: string;
|
|
26
|
+
sig?: string;
|
|
27
|
+
}
|
|
28
|
+
/** SPEC.md §3.2. The buyer's authorisation for ONE chunk. Never for the session. */
|
|
29
|
+
export interface Reservation {
|
|
30
|
+
v: 1;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
seq: number;
|
|
33
|
+
units: number;
|
|
34
|
+
cumulativeUnits: number;
|
|
35
|
+
cumulativeSompi: number;
|
|
36
|
+
prevState: string | null;
|
|
37
|
+
sig?: string;
|
|
38
|
+
}
|
|
39
|
+
/** SPEC.md §3.3. One per party per chunk boundary. */
|
|
40
|
+
export interface Measurement {
|
|
41
|
+
v: 1;
|
|
42
|
+
sessionId: string;
|
|
43
|
+
seq: number;
|
|
44
|
+
by: 'buyer' | 'provider';
|
|
45
|
+
units: number;
|
|
46
|
+
cumulativeUnits: number;
|
|
47
|
+
contentDigest: string;
|
|
48
|
+
measurementId: string;
|
|
49
|
+
sig?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* SPEC.md §3.4. The only object that can settle, and the only one whose signatures cover a byte
|
|
53
|
+
* concatenation (§3.4.1) rather than canonical JSON.
|
|
54
|
+
*/
|
|
55
|
+
export interface State {
|
|
56
|
+
v: 1;
|
|
57
|
+
sessionId: string;
|
|
58
|
+
seq: number;
|
|
59
|
+
cumulativeUnits: number;
|
|
60
|
+
cumulativeSompi: number;
|
|
61
|
+
prevState: string | null;
|
|
62
|
+
buyerSig?: string;
|
|
63
|
+
providerSig?: string;
|
|
64
|
+
}
|
|
65
|
+
/** Why a session stopped. SPEC.md §5: "the only remedy for disagreement is to stop." */
|
|
66
|
+
export type HaltReason = 'signature' | 'sequence' | 'content-digest' | 'tolerance-babel' | 'tolerance-cumulative' | 'bias';
|
|
67
|
+
export interface Halt {
|
|
68
|
+
ok: false;
|
|
69
|
+
reason: HaltReason;
|
|
70
|
+
detail: string;
|
|
71
|
+
}
|
|
72
|
+
export interface Reconciled {
|
|
73
|
+
ok: true;
|
|
74
|
+
/** Rule 6: the LOWER of the two counts, which is what gets billed. */
|
|
75
|
+
billedUnits: number;
|
|
76
|
+
billedCumulativeUnits: number;
|
|
77
|
+
/** SPEC.md §5.1: providerUnits − buyerUnits, the quantity the bias detector consumes. */
|
|
78
|
+
residual: number;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,wFAAwF;AACxF,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,CAAC,CAAC;IACL,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,oFAAoF;AACpF,MAAM,WAAW,WAAW;IAC1B,CAAC,EAAE,CAAC,CAAC;IACL,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,sDAAsD;AACtD,MAAM,WAAW,WAAW;IAC1B,CAAC,EAAE,CAAC,CAAC;IACL,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,OAAO,GAAG,UAAU,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,CAAC,CAAC;IACL,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wFAAwF;AACxF,MAAM,MAAM,UAAU,GAClB,WAAW,GACX,UAAU,GACV,gBAAgB,GAChB,iBAAiB,GACjB,sBAAsB,GACtB,MAAM,CAAC;AAEX,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,IAAI,CAAC;IACT,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire messages of SPEC.md §3, as types.
|
|
3
|
+
*
|
|
4
|
+
* These are the shapes the covenant measurement pinned. Nothing here should change without
|
|
5
|
+
* changing §3, because `spec/vectors.ts` and `contracts/metered_session.tests.json` both encode
|
|
6
|
+
* the same fields and the golden vector is compared byte for byte.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchor-cli.d.ts","sourceRoot":"","sources":["../../tools/anchor-cli.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm run anchor -- <command>` -- set up and exercise the checkpoint anchor on testnet.
|
|
3
|
+
*
|
|
4
|
+
* address generate the anchor key if there is none, and show where to fund it
|
|
5
|
+
* balance what the anchor key can currently pay for
|
|
6
|
+
* test anchor one digest for real, then read it back off the chain and verify it
|
|
7
|
+
*
|
|
8
|
+
* TESTNET ONLY, and the network is not a flag by accident. Nothing in this project has any
|
|
9
|
+
* business writing to mainnet, and a default that could be overridden by a typo is the kind of
|
|
10
|
+
* thing that only has to be wrong once.
|
|
11
|
+
*/
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
13
|
+
import { blake3Hex } from '../src/encoding.js';
|
|
14
|
+
import { loadAnchorKey, saveAnchorKey, anchorAddress, KEY_FILE, loadSdk } from './kaspa.js';
|
|
15
|
+
import { submitAnchor, ANCHOR_FEE_SOMPI, currentTip, readAnchoredPayload } from './anchor.js';
|
|
16
|
+
const NETWORK = 'testnet-10';
|
|
17
|
+
/** Existing key, or a fresh one. Generating is safe: an unfunded key holds nothing to lose. */
|
|
18
|
+
function keyOrCreate() {
|
|
19
|
+
const existing = loadAnchorKey();
|
|
20
|
+
if (existing)
|
|
21
|
+
return { key: existing, created: false };
|
|
22
|
+
const key = randomBytes(32).toString('hex');
|
|
23
|
+
saveAnchorKey(key);
|
|
24
|
+
return { key, created: true };
|
|
25
|
+
}
|
|
26
|
+
async function utxoTotal(address) {
|
|
27
|
+
const sdk = await loadSdk();
|
|
28
|
+
const rpc = new sdk.RpcClient({
|
|
29
|
+
resolver: new sdk.Resolver(),
|
|
30
|
+
encoding: sdk.Encoding.Borsh,
|
|
31
|
+
networkId: new sdk.NetworkId(NETWORK),
|
|
32
|
+
});
|
|
33
|
+
await rpc.connect();
|
|
34
|
+
try {
|
|
35
|
+
const { entries } = await rpc.getUtxosByAddresses([address]);
|
|
36
|
+
return entries.reduce((sum, e) => sum + e.amount, 0n);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await rpc.disconnect().catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function showAddress() {
|
|
43
|
+
const { key, created } = keyOrCreate();
|
|
44
|
+
const address = await anchorAddress(key, NETWORK);
|
|
45
|
+
console.log(`\n ${created ? 'Generated a new anchor key' : 'Using the existing anchor key'}`);
|
|
46
|
+
console.log(` stored at ${KEY_FILE} (outside the repository, never printed)\n`);
|
|
47
|
+
console.log(` network ${NETWORK}`);
|
|
48
|
+
console.log(` address ${address}\n`);
|
|
49
|
+
console.log(' Fund it from a testnet-10 faucet. One anchor costs 200,000 sompi (0.002 KAS),');
|
|
50
|
+
console.log(' so even a small faucet drip covers hundreds of checkpoints.\n');
|
|
51
|
+
}
|
|
52
|
+
async function showBalance() {
|
|
53
|
+
const key = loadAnchorKey();
|
|
54
|
+
if (!key)
|
|
55
|
+
return void console.log('\n No anchor key yet. Run: npm run anchor -- address\n');
|
|
56
|
+
const address = await anchorAddress(key, NETWORK);
|
|
57
|
+
const total = await utxoTotal(address);
|
|
58
|
+
const anchors = total / ANCHOR_FEE_SOMPI;
|
|
59
|
+
console.log(`\n address ${address}`);
|
|
60
|
+
console.log(` balance ${total} sompi (${Number(total) / 1e8} KAS)`);
|
|
61
|
+
console.log(` buys ${anchors} anchor${anchors === 1n ? '' : 's'}\n`);
|
|
62
|
+
if (total === 0n)
|
|
63
|
+
console.log(' Unfunded. Send testnet-10 coins to the address above.\n');
|
|
64
|
+
}
|
|
65
|
+
async function testAnchor() {
|
|
66
|
+
const key = loadAnchorKey();
|
|
67
|
+
if (!key)
|
|
68
|
+
return void console.log('\n No anchor key yet. Run: npm run anchor -- address\n');
|
|
69
|
+
const cfg = { network: NETWORK, privateKeyHex: key };
|
|
70
|
+
const digest = blake3Hex(`metered/anchor-test/${Date.now()}`);
|
|
71
|
+
// Captured BEFORE submitting, or the forward walk could start past our own block.
|
|
72
|
+
const from = await currentTip(cfg);
|
|
73
|
+
console.log(`\n anchoring ${digest}`);
|
|
74
|
+
const txid = await submitAnchor(cfg, digest);
|
|
75
|
+
console.log(` accepted ${txid}`);
|
|
76
|
+
console.log(' reading it back off the chain -- acceptance is not proof...');
|
|
77
|
+
const payload = await readAnchoredPayload(cfg, txid, from);
|
|
78
|
+
const stored = payload.toLowerCase();
|
|
79
|
+
console.log(` payload ${stored || '(EMPTY)'}`);
|
|
80
|
+
if (stored !== digest) {
|
|
81
|
+
throw new Error(`the chain stored ${stored || 'nothing'}, not the digest -- the anchor is worthless`);
|
|
82
|
+
}
|
|
83
|
+
console.log(' VERIFIED the chain stored exactly the digest that was anchored');
|
|
84
|
+
console.log(` explorer https://explorer-tn10.kaspa.org/txs/${txid}\n`);
|
|
85
|
+
}
|
|
86
|
+
const COMMANDS = {
|
|
87
|
+
address: showAddress,
|
|
88
|
+
balance: showBalance,
|
|
89
|
+
test: testAnchor,
|
|
90
|
+
};
|
|
91
|
+
const command = process.argv[2] ?? 'address';
|
|
92
|
+
const run = COMMANDS[command];
|
|
93
|
+
if (!run) {
|
|
94
|
+
console.error(`unknown command '${command}'. Try: ${Object.keys(COMMANDS).join(', ')}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
run().catch((err) => {
|
|
98
|
+
console.error(`\n ${err instanceof Error ? err.message : String(err)}\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
});
|
|
101
|
+
//# sourceMappingURL=anchor-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchor-cli.js","sourceRoot":"","sources":["../../tools/anchor-cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A real Kaspa anchor for SPEC.md §8 checkpoints -- the `Anchor` that src/checkpoint.ts takes.
|
|
3
|
+
*
|
|
4
|
+
* Every hard-won detail below came from kaspa-depin's proven implementation, and each one cost a
|
|
5
|
+
* live run to learn. They are restated rather than imported because that repository is a separate
|
|
6
|
+
* project and this one must build without it; but they are its findings, not new ones.
|
|
7
|
+
*
|
|
8
|
+
* BUILD THE TRANSACTION BY HAND. `createTransactions` hands back a PendingTransaction whose
|
|
9
|
+
* `.transaction` is a COPY -- setting subnetworkId and payload on it mutates a throwaway, the
|
|
10
|
+
* chain receives neither, and nothing reports a problem. A first attempt confirmed happily and
|
|
11
|
+
* landed a plain payment with an empty payload. `createTransaction` returns a Transaction we
|
|
12
|
+
* own, so what is signed is what was built.
|
|
13
|
+
*
|
|
14
|
+
* A VERSION 1 TRANSACTION BUDGETS ITS INPUTS. Toccata replaced sigOpCount with an explicit
|
|
15
|
+
* computeBudget, and a node refuses a v1 transaction still carrying the old field.
|
|
16
|
+
*
|
|
17
|
+
* WHAT THIS DELIBERATELY DOES NOT DO: wait. §8 requires checkpointing to be non-blocking because
|
|
18
|
+
* Study B measured p90 confirmation at 1,879 ms. This resolves once the node ACCEPTS the
|
|
19
|
+
* transaction, not once it is mined -- src/checkpoint.ts already treats the record as pending and
|
|
20
|
+
* never awaits it. Proving a checkpoint later means fetching the block, which is a verifier's job
|
|
21
|
+
* and needs nothing from here.
|
|
22
|
+
*/
|
|
23
|
+
import { type Network } from './kaspa.js';
|
|
24
|
+
import type { Anchor } from '../src/checkpoint.js';
|
|
25
|
+
/** KIP-21 subnetwork. Anchors go in their own lane so the stream is separable later. */
|
|
26
|
+
export declare const ANCHOR_LANE = "00a0c40100000000000000000000000000000000";
|
|
27
|
+
/**
|
|
28
|
+
* Measured, not guessed, and the first three guesses were wrong. At budget 10 the mass is 1,666
|
|
29
|
+
* and the node requires 166,600 sompi; this pays 200,000 for headroom -- about 0.002 KAS, the
|
|
30
|
+
* figure SPEC.md §0.1 quotes from Study B.
|
|
31
|
+
*/
|
|
32
|
+
export declare const ANCHOR_FEE_SOMPI = 200000n;
|
|
33
|
+
export interface AnchorConfig {
|
|
34
|
+
network: Network;
|
|
35
|
+
privateKeyHex: string;
|
|
36
|
+
/** ws(s):// node URL. Omitted, the public Resolver pool is used -- which speaks borsh only. */
|
|
37
|
+
url?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Put one digest on chain. Resolves with the transaction id once the node has accepted it.
|
|
41
|
+
*
|
|
42
|
+
* The payload is the raw 32 digest bytes. There is no framing: a checkpoint proves a State existed
|
|
43
|
+
* before a block, and the State's digest is the whole claim.
|
|
44
|
+
*/
|
|
45
|
+
export declare function submitAnchor(cfg: AnchorConfig, digest: string): Promise<string>;
|
|
46
|
+
/** A tip to start a forward walk from. Must be captured BEFORE submitting. */
|
|
47
|
+
export declare function currentTip(cfg: AnchorConfig): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Find an anchor in a block and return the payload the chain actually stored.
|
|
50
|
+
*
|
|
51
|
+
* ACCEPTANCE IS NOT PROOF, and this exists because of that. kaspa-depin's first live anchor
|
|
52
|
+
* confirmed perfectly and landed a plain payment with an EMPTY payload, because the payload had
|
|
53
|
+
* been set on a copy. Nothing anywhere reported a problem. The only way to know a checkpoint is
|
|
54
|
+
* real is to read it back off the chain.
|
|
55
|
+
*
|
|
56
|
+
* SCANNING TIPS DOES NOT WORK. At ten blocks a second a block stops being a tip within about a
|
|
57
|
+
* second, so polling tipHashes misses the block nearly every time and reports a timeout that
|
|
58
|
+
* reads like a failure. Walking forward from a hash captured before submission is the only
|
|
59
|
+
* approach that does not race the DAG.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readAnchoredPayload(cfg: AnchorConfig, txid: string, fromHash: string, timeoutMs?: number): Promise<string>;
|
|
62
|
+
/**
|
|
63
|
+
* The `Anchor` src/checkpoint.ts consumes. Note what it returns: a promise the session never
|
|
64
|
+
* awaits. A failure marks the checkpoint record and stops there, because §8 is explicit that
|
|
65
|
+
* checkpoints are evidence rather than safety -- losing one costs provability, never funds.
|
|
66
|
+
*/
|
|
67
|
+
export declare function kaspaAnchor(cfg: AnchorConfig): Anchor;
|
|
68
|
+
//# sourceMappingURL=anchor.d.ts.map
|