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,13 @@
|
|
|
1
|
+
export type Network = 'testnet-10' | 'testnet-11' | 'mainnet';
|
|
2
|
+
export declare const KEY_FILE: string;
|
|
3
|
+
type Sdk = any;
|
|
4
|
+
/** Loads the vendored rusty-kaspa WASM bindings. Node 21+ supplies the WebSocket global itself. */
|
|
5
|
+
export declare function loadSdk(): Promise<Sdk>;
|
|
6
|
+
/** The anchor key, or null if none has been generated. Never printed by anything here. */
|
|
7
|
+
export declare function loadAnchorKey(): string | null;
|
|
8
|
+
/** Writes a key to ~/.metered/kaspa.env. Refuses to overwrite: losing one loses its funds. */
|
|
9
|
+
export declare function saveAnchorKey(privateKeyHex: string): void;
|
|
10
|
+
/** The address this key pays from, which is also where a faucet should send test coins. */
|
|
11
|
+
export declare function anchorAddress(privateKeyHex: string, network: Network): Promise<string>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=kaspa.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kaspa.d.ts","sourceRoot":"","sources":["../../tools/kaspa.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,OAAO,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;AAa9D,eAAO,MAAM,QAAQ,QAA2C,CAAC;AAGjE,KAAK,GAAG,GAAG,GAAG,CAAC;AAWf,mGAAmG;AACnG,wBAAsB,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,CAe5C;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,IAAI,MAAM,GAAG,IAAI,CAI7C;AAED,8FAA8F;AAC9F,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAUzD;AAED,2FAA2F;AAC3F,wBAAsB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAI5F"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading the Kaspa SDK, and holding the key that pays for anchors.
|
|
3
|
+
*
|
|
4
|
+
* THE SDK IS NOT VENDORED HERE, deliberately. It is a 12 MB WASM blob, and this repository's whole
|
|
5
|
+
* posture is that everything it contains can be verified by CI on a clean checkout. The same
|
|
6
|
+
* arrangement already applies to `silverc` and the debugger: a heavy external artefact is found by
|
|
7
|
+
* environment variable and its absence fails with instructions rather than a stack trace. The
|
|
8
|
+
* encoder was vendored because the golden vector could not otherwise be regenerated; nothing about
|
|
9
|
+
* a checkpoint anchor has that property, because anchoring needs a live node either way.
|
|
10
|
+
*
|
|
11
|
+
* THE KEY LIVES OUTSIDE THE REPOSITORY, at ~/.metered/kaspa.env, for the reason the provider keys
|
|
12
|
+
* do: .gitignore stops a commit and does not stop a read, and anything working on this project can
|
|
13
|
+
* read anything inside it.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
/**
|
|
20
|
+
* The rusty-kaspa WASM bindings, which are NOT vendored here: 12 MB of WASM with its own release
|
|
21
|
+
* cadence. Point METERED_KASPA_SDK at `kaspa.js` from a nodejs build, or put the same line in
|
|
22
|
+
* ~/.metered/kaspa.env beside the key -- outside the repository, like everything else that is
|
|
23
|
+
* specific to one machine.
|
|
24
|
+
*
|
|
25
|
+
* Only the chain tooling needs it. The specification, the conformance vectors and both
|
|
26
|
+
* implementations run without it.
|
|
27
|
+
*/
|
|
28
|
+
const SDK_ENV = 'METERED_KASPA_SDK';
|
|
29
|
+
export const KEY_FILE = join(homedir(), '.metered', 'kaspa.env');
|
|
30
|
+
let cached = null;
|
|
31
|
+
/** Read a NAME=value line from the key file, so a machine-specific path stays out of the repository. */
|
|
32
|
+
function fromKeyFile(name) {
|
|
33
|
+
if (!existsSync(KEY_FILE))
|
|
34
|
+
return undefined;
|
|
35
|
+
const match = readFileSync(KEY_FILE, 'utf8').match(new RegExp(`^${name}=(.+?)[ \\t\\r]*$`, 'm'));
|
|
36
|
+
return match?.[1];
|
|
37
|
+
}
|
|
38
|
+
/** Loads the vendored rusty-kaspa WASM bindings. Node 21+ supplies the WebSocket global itself. */
|
|
39
|
+
export async function loadSdk() {
|
|
40
|
+
if (cached)
|
|
41
|
+
return cached;
|
|
42
|
+
const entry = process.env[SDK_ENV] ?? fromKeyFile(SDK_ENV);
|
|
43
|
+
if (!entry || !existsSync(entry)) {
|
|
44
|
+
throw new Error(`Kaspa SDK not found${entry ? ` at ${entry}` : ''}.\n\n` +
|
|
45
|
+
`Set ${SDK_ENV} to kaspa.js from a rusty-kaspa WASM nodejs build, or add\n` +
|
|
46
|
+
`${SDK_ENV}=<path> to ${KEY_FILE}.`);
|
|
47
|
+
}
|
|
48
|
+
const imported = await import(pathToFileURL(entry).href);
|
|
49
|
+
const sdk = (imported.default ?? imported);
|
|
50
|
+
sdk.initConsolePanicHook?.();
|
|
51
|
+
cached = sdk;
|
|
52
|
+
return sdk;
|
|
53
|
+
}
|
|
54
|
+
/** The anchor key, or null if none has been generated. Never printed by anything here. */
|
|
55
|
+
export function loadAnchorKey() {
|
|
56
|
+
if (!existsSync(KEY_FILE))
|
|
57
|
+
return null;
|
|
58
|
+
const match = readFileSync(KEY_FILE, 'utf8').match(/^METERED_ANCHOR_KEY=([0-9a-fA-F]{64})\s*$/m);
|
|
59
|
+
return match ? match[1].toLowerCase() : null;
|
|
60
|
+
}
|
|
61
|
+
/** Writes a key to ~/.metered/kaspa.env. Refuses to overwrite: losing one loses its funds. */
|
|
62
|
+
export function saveAnchorKey(privateKeyHex) {
|
|
63
|
+
if (loadAnchorKey())
|
|
64
|
+
throw new Error(`${KEY_FILE} already holds a key; refusing to overwrite it`);
|
|
65
|
+
mkdirSync(join(homedir(), '.metered'), { recursive: true });
|
|
66
|
+
writeFileSync(KEY_FILE, '# Metered -- the key that pays for checkpoint anchors. TESTNET ONLY.\n' +
|
|
67
|
+
'# Outside the repository on purpose: .gitignore stops a commit, not a read.\n' +
|
|
68
|
+
`METERED_ANCHOR_KEY=${privateKeyHex}\n`, { mode: 0o600 });
|
|
69
|
+
}
|
|
70
|
+
/** The address this key pays from, which is also where a faucet should send test coins. */
|
|
71
|
+
export async function anchorAddress(privateKeyHex, network) {
|
|
72
|
+
const sdk = await loadSdk();
|
|
73
|
+
const priv = new sdk.PrivateKey(privateKeyHex);
|
|
74
|
+
return priv.toKeypair().toAddress(new sdk.NetworkId(network)).toString();
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=kaspa.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kaspa.js","sourceRoot":"","sources":["../../tools/kaspa.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-deadline.d.ts","sourceRoot":"","sources":["../../tools/live-deadline.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm run live:deadline` -- prove SPEC.md 7.3a on chain: what a provider loses by not posting.
|
|
3
|
+
*
|
|
4
|
+
* A real session runs over HTTP. Both parties count, both sign, and the provider ends up holding
|
|
5
|
+
* doubly-signed States for work it genuinely delivered. Then it does the one thing 7.3a forbids:
|
|
6
|
+
* nothing. The covenant UTXO ages past the response window with no claim pending, the buyer calls
|
|
7
|
+
* `expire`, and consensus hands the entire balance back to the buyer.
|
|
8
|
+
*
|
|
9
|
+
* THE POINT IS THAT NOTHING HERE IS AN ATTACK. The buyer does not forge, race, or exploit; it
|
|
10
|
+
* waits and then follows the rules. The provider is not cheated -- it holds perfectly good
|
|
11
|
+
* evidence of a debt, and the chain releases the money anyway, because a signed State that was
|
|
12
|
+
* never posted is a claim nobody made.
|
|
13
|
+
*
|
|
14
|
+
* This runs on testnet-10 and costs test coins. It is the demonstration behind the rule.
|
|
15
|
+
*/
|
|
16
|
+
import { mkdtempSync } from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { randomBytes } from 'node:crypto';
|
|
20
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
21
|
+
import { publicKeyHex } from '../src/encoding.js';
|
|
22
|
+
import { serveMetered } from '../src/http/serve.js';
|
|
23
|
+
import { MeteredService } from '../src/http/service.js';
|
|
24
|
+
import { openSession, runBabel } from '../src/http/client.js';
|
|
25
|
+
import { meterFor } from '../src/meter.js';
|
|
26
|
+
import { shouldSettle } from '../src/deadline.js';
|
|
27
|
+
import { loadSdk, loadAnchorKey } from './kaspa.js';
|
|
28
|
+
import { compileWithState, covenantAddress } from './covenant.js';
|
|
29
|
+
import { awaitUtxo, buildExpire, awaitWindow } from './live-steps.js';
|
|
30
|
+
const NETWORK = 'testnet-10';
|
|
31
|
+
const FUND = 50000000n;
|
|
32
|
+
const FUND_FEE = 250000n;
|
|
33
|
+
const CLOSE_FEE = 400000n;
|
|
34
|
+
const WINDOW = 2;
|
|
35
|
+
const PRICE = 3630;
|
|
36
|
+
const BABEL = 10;
|
|
37
|
+
const dir = mkdtempSync(join(tmpdir(), 'metered-deadline-'));
|
|
38
|
+
const meter = meterFor('o200k_base');
|
|
39
|
+
const deliver = (prompt, max) => new TextEncoder().encode(Array.from({ length: max }, (_, i) => `${prompt}${i}`).join(' '));
|
|
40
|
+
/** What the provider's policy WOULD have told it, had it been asked. */
|
|
41
|
+
const POLICY = { settleByAgeDaa: 1, settleAtUnsettledSompi: 50_000 };
|
|
42
|
+
async function main() {
|
|
43
|
+
const funder = loadAnchorKey();
|
|
44
|
+
if (!funder)
|
|
45
|
+
throw new Error('no anchor key; run: npm run anchor -- address');
|
|
46
|
+
const buyerSk = bytesToHex(randomBytes(32));
|
|
47
|
+
const providerSk = bytesToHex(randomBytes(32));
|
|
48
|
+
const terms = {
|
|
49
|
+
v: 1, scheme: 'metered', network: 'kaspa:testnet-10', asset: 'KAS',
|
|
50
|
+
unit: 'llm.output_tokens.v1', meter: 'o200k_base',
|
|
51
|
+
unitPriceSompi: PRICE, babelUnits: BABEL, maxBabels: 16,
|
|
52
|
+
toleranceAbs: 1, checkpointEvery: 0, responseWindowDaa: WINDOW,
|
|
53
|
+
};
|
|
54
|
+
const service = new MeteredService({
|
|
55
|
+
terms, providerSk, providerPubkey: publicKeyHex(providerSk), meter, deliver,
|
|
56
|
+
});
|
|
57
|
+
const server = serveMetered({ service });
|
|
58
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
59
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
60
|
+
const sdk = await loadSdk();
|
|
61
|
+
const networkId = new sdk.NetworkId(NETWORK);
|
|
62
|
+
const priv = new sdk.PrivateKey(funder);
|
|
63
|
+
const from = priv.toKeypair().toAddress(networkId).toString();
|
|
64
|
+
const buyerAddr = new sdk.PrivateKey(buyerSk).toKeypair().toAddress(networkId).toString();
|
|
65
|
+
const rpc = new sdk.RpcClient({ resolver: new sdk.Resolver(), encoding: sdk.Encoding.Borsh, networkId });
|
|
66
|
+
await rpc.connect();
|
|
67
|
+
try {
|
|
68
|
+
const { offer, session } = await openSession(base, buyerSk, meter, 'kaspa:testnet-10');
|
|
69
|
+
const built = compileWithState(dir, {
|
|
70
|
+
parties: offer.partiesCommitment, sessionId: offer.sessionId, window: WINDOW, seq: -1, sompi: 0,
|
|
71
|
+
});
|
|
72
|
+
const address = await covenantAddress(built.hex, NETWORK);
|
|
73
|
+
const { entries } = await rpc.getUtxosByAddresses([from]);
|
|
74
|
+
const src = entries.reduce((a, b) => (b.amount > a.amount ? b : a));
|
|
75
|
+
const fund = sdk.createTransaction([src], [{ address, amount: FUND }, { address: from, amount: src.amount - FUND - FUND_FEE }], 0n, undefined, 0);
|
|
76
|
+
fund.version = 1;
|
|
77
|
+
fund.gas = 0n;
|
|
78
|
+
for (const i of fund.inputs) {
|
|
79
|
+
i.sigOpCount = 0;
|
|
80
|
+
i.computeBudget = 10;
|
|
81
|
+
}
|
|
82
|
+
fund.finalize();
|
|
83
|
+
await rpc.submitTransaction({ transaction: sdk.signTransaction(fund, [priv], true), allowOrphan: false });
|
|
84
|
+
const utxo = await awaitUtxo(rpc, address, FUND);
|
|
85
|
+
if (!utxo)
|
|
86
|
+
throw new Error('the covenant UTXO never appeared');
|
|
87
|
+
console.log(`\n 1. FUNDED ${Number(FUND) / 1e8} TKAS at ${address.slice(0, 30)}...`);
|
|
88
|
+
// ---- real work, really agreed.
|
|
89
|
+
let owed = 0;
|
|
90
|
+
for (let i = 0; i < 2; i += 1) {
|
|
91
|
+
const out = await runBabel(base, session, `question-${i}-`);
|
|
92
|
+
owed = out.state.cumulativeSompi;
|
|
93
|
+
console.log(` babel ${i}: ${out.billedUnits} units, ${owed} sompi owed and DOUBLY SIGNED`);
|
|
94
|
+
}
|
|
95
|
+
const verdict = shouldSettle(POLICY, { ageDaa: WINDOW, unsettledSompi: owed });
|
|
96
|
+
console.log(`\n 2. POLICY says settle: ${verdict.settle} -- ${verdict.why ?? 'nothing owed'}`);
|
|
97
|
+
console.log(' the provider ignores it and posts nothing. That is the whole experiment.');
|
|
98
|
+
await awaitWindow(rpc, WINDOW);
|
|
99
|
+
console.log(`\n 3. AGED past the ${WINDOW}-DAA window with NO claim pending`);
|
|
100
|
+
const close = buildExpire(sdk, { buyerPk: offer.buyerPubkey, providerPk: offer.providerPubkey, signerSk: buyerSk,
|
|
101
|
+
redeem: built.hex, tag: built.entries.expire?.dispatch_tag ?? '' }, utxo, [{ address: buyerAddr, amount: utxo.amount - CLOSE_FEE }], WINDOW);
|
|
102
|
+
const id = (await rpc.submitTransaction({ transaction: close, allowOrphan: false })).transactionId;
|
|
103
|
+
const got = await awaitUtxo(rpc, buyerAddr, utxo.amount - CLOSE_FEE);
|
|
104
|
+
if (!got)
|
|
105
|
+
throw new Error('the close did not land');
|
|
106
|
+
console.log(`\n 4. CLOSED the buyer took everything back ${id}`);
|
|
107
|
+
console.log(`\n PROVIDER OWED ${owed} sompi, doubly signed, for work actually delivered`);
|
|
108
|
+
console.log(` PROVIDER RECEIVED 0`);
|
|
109
|
+
console.log(` BUYER RECEIVED ${Number(utxo.amount - CLOSE_FEE) / 1e8} TKAS -- the entire balance`);
|
|
110
|
+
console.log('\n Nothing here was an attack. The buyer waited, then followed the rules.');
|
|
111
|
+
console.log(' SPEC.md 7.3a is the rule that makes this the provider\'s own fault.\n');
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await new Promise((r) => server.close(() => r()));
|
|
115
|
+
await rpc.disconnect().catch(() => undefined);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
main().catch((err) => {
|
|
119
|
+
console.error(`\n ${err instanceof Error ? err.message : String(err)}\n`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
122
|
+
//# sourceMappingURL=live-deadline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-deadline.js","sourceRoot":"","sources":["../../tools/live-deadline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-expire.d.ts","sourceRoot":"","sources":["../../tools/live-expire.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm run live:expire` -- open a real covenant on testnet-10 and close it with `expire`.
|
|
3
|
+
*
|
|
4
|
+
* WHY EXPIRE AND NOT SETTLE. `settle` hands the funds back to the SAME covenant with updated
|
|
5
|
+
* state, and a continuation output must carry a KIP-20 CovenantBinding tying it to the authorizing
|
|
6
|
+
* input. That plumbing now exists in tools/live-settle.ts. `expire` still needs none of it: it
|
|
7
|
+
* pays plain P2PK addresses and continues nothing, and Argent lowers `emits {}` to
|
|
8
|
+
* OpAuthOutputCount == 0, which an unbound payout already satisfies. That is what made it the
|
|
9
|
+
* first covenant path to run on chain, and the first the ARGENT port ran too.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT PROVES. The whole covenant executes under real consensus: the parties commitment, the
|
|
12
|
+
* relative timelock, the signature check, and the no-claim refund branch that step 3.4 added
|
|
13
|
+
* after finding the funding UTXO would otherwise be unspendable forever.
|
|
14
|
+
*
|
|
15
|
+
* Everything happens in ONE run because the session keys live only in memory. Two earlier
|
|
16
|
+
* attempts generated keys, funded a covenant and then exited on an unrelated error, stranding the
|
|
17
|
+
* coins at an address whose keys no longer exist. Testnet, so it cost nothing but the lesson.
|
|
18
|
+
*/
|
|
19
|
+
import { mkdtempSync } from 'node:fs';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { randomBytes } from 'node:crypto';
|
|
23
|
+
import { blake3 } from '@noble/hashes/blake3';
|
|
24
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
25
|
+
import { publicKeyHex } from '../src/encoding.js';
|
|
26
|
+
import { loadSdk, loadAnchorKey } from './kaspa.js';
|
|
27
|
+
import { compileWithState, covenantAddress } from './covenant.js';
|
|
28
|
+
import { profileFromArgv } from './covenant-profile.js';
|
|
29
|
+
const NETWORK = 'testnet-10';
|
|
30
|
+
const FUND_SOMPI = 50000000n;
|
|
31
|
+
const FUND_FEE = 250000n;
|
|
32
|
+
// expire does TWO signature checks, so it needs compute budget 21, not 10 -- and mass rises with
|
|
33
|
+
// the budget, so the node demands ~313,800 for a one-output close. Measured by being refused.
|
|
34
|
+
const SPEND_FEE = 350000n;
|
|
35
|
+
const EXPIRE_BUDGET = 21;
|
|
36
|
+
const WINDOW = 2;
|
|
37
|
+
/** kaspa-depin's lesson: createInputSignature returns a signature SCRIPT; strip the 0x41 push. */
|
|
38
|
+
const bareSig = (s) => (s.length === 132 && s.startsWith('41') ? s.slice(2) : s);
|
|
39
|
+
async function main() {
|
|
40
|
+
const funder = loadAnchorKey();
|
|
41
|
+
if (!funder)
|
|
42
|
+
throw new Error('no anchor key; run: npm run anchor -- address');
|
|
43
|
+
const dir = mkdtempSync(join(tmpdir(), 'metered-expire-'));
|
|
44
|
+
const buyerSk = bytesToHex(randomBytes(32));
|
|
45
|
+
const providerSk = bytesToHex(randomBytes(32));
|
|
46
|
+
const buyerPk = publicKeyHex(buyerSk);
|
|
47
|
+
const providerPk = publicKeyHex(providerSk);
|
|
48
|
+
const parties = bytesToHex(blake3(new Uint8Array([...hexToBytes(buyerPk), ...hexToBytes(providerPk)]), { dkLen: 32 }));
|
|
49
|
+
const sessionId = bytesToHex(randomBytes(16));
|
|
50
|
+
// `npm run live:expire -- --profile ag` closes with the ARGENT covenant instead. `expire` is
|
|
51
|
+
// the entry that needs no KIP-20 plumbing: Argent lowers `emits {}` to OpAuthOutputCount == 0,
|
|
52
|
+
// and an unbound P2PK payout already has zero authorised outputs.
|
|
53
|
+
const profile = profileFromArgv();
|
|
54
|
+
const compiled = compileWithState(dir, { parties, sessionId, window: WINDOW, seq: -1, sompi: 0 }, profile.contract);
|
|
55
|
+
const address = await covenantAddress(compiled.hex, NETWORK);
|
|
56
|
+
const tag = compiled.entries.expire?.dispatch_tag;
|
|
57
|
+
if (!tag)
|
|
58
|
+
throw new Error('the artifact has no expire entry');
|
|
59
|
+
console.log(`\n covenant ${compiled.hex.length / 2} bytes [${profile.key}] ${profile.contract}`);
|
|
60
|
+
console.log(` address ${address}`);
|
|
61
|
+
console.log(` closing with EXPIRE, no claim posted -- the buyer takes it all back (SPEC 7.2)`);
|
|
62
|
+
const sdk = await loadSdk();
|
|
63
|
+
const networkId = new sdk.NetworkId(NETWORK);
|
|
64
|
+
const priv = new sdk.PrivateKey(funder);
|
|
65
|
+
const funderAddr = priv.toKeypair().toAddress(networkId).toString();
|
|
66
|
+
const buyerAddr = new sdk.PrivateKey(buyerSk).toKeypair().toAddress(networkId).toString();
|
|
67
|
+
const rpc = new sdk.RpcClient({ resolver: new sdk.Resolver(), encoding: sdk.Encoding.Borsh, networkId });
|
|
68
|
+
await rpc.connect();
|
|
69
|
+
try {
|
|
70
|
+
const { entries } = await rpc.getUtxosByAddresses([funderAddr]);
|
|
71
|
+
const utxo = entries.reduce((a, b) => (b.amount > a.amount ? b : a));
|
|
72
|
+
const fundTx = sdk.createTransaction([utxo], [{ address, amount: FUND_SOMPI }, { address: funderAddr, amount: utxo.amount - FUND_SOMPI - FUND_FEE }], 0n, undefined, 0);
|
|
73
|
+
fundTx.version = 1;
|
|
74
|
+
fundTx.gas = 0n;
|
|
75
|
+
for (const i of fundTx.inputs) {
|
|
76
|
+
i.sigOpCount = 0;
|
|
77
|
+
i.computeBudget = 10;
|
|
78
|
+
}
|
|
79
|
+
fundTx.finalize();
|
|
80
|
+
const fundId = (await rpc.submitTransaction({ transaction: sdk.signTransaction(fundTx, [priv], true), allowOrphan: false })).transactionId;
|
|
81
|
+
console.log(` funded ${Number(FUND_SOMPI) / 1e8} TKAS txid ${fundId}`);
|
|
82
|
+
let cov = null;
|
|
83
|
+
for (let i = 0; i < 40 && !cov; i += 1) {
|
|
84
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
85
|
+
const res = await rpc.getUtxosByAddresses([address]);
|
|
86
|
+
cov = res.entries.find((e) => e.amount === FUND_SOMPI) ?? null;
|
|
87
|
+
}
|
|
88
|
+
if (!cov)
|
|
89
|
+
throw new Error('the covenant UTXO never appeared');
|
|
90
|
+
// The window is RELATIVE (SPEC 7.3): ageDaa lowers to OpCheckSequenceVerify, which reads the
|
|
91
|
+
// spending input's sequence. The UTXO must also actually BE that old, so wait for the DAA
|
|
92
|
+
// score to advance past it.
|
|
93
|
+
const startDaa = (await rpc.getBlockDagInfo()).virtualDaaScore;
|
|
94
|
+
while ((await rpc.getBlockDagInfo()).virtualDaaScore < startDaa + BigInt(WINDOW) + 2n) {
|
|
95
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
96
|
+
}
|
|
97
|
+
console.log(` aged past the ${WINDOW}-DAA response window`);
|
|
98
|
+
const spend = sdk.createTransaction([cov], [{ address: buyerAddr, amount: cov.amount - SPEND_FEE }], 0n, undefined, 0);
|
|
99
|
+
spend.version = 1;
|
|
100
|
+
spend.gas = 0n;
|
|
101
|
+
spend.inputs[0].sigOpCount = 0;
|
|
102
|
+
spend.inputs[0].computeBudget = EXPIRE_BUDGET;
|
|
103
|
+
spend.inputs[0].sequence = BigInt(WINDOW);
|
|
104
|
+
const sig = bareSig(sdk.createInputSignature(spend, 0, new sdk.PrivateKey(buyerSk)));
|
|
105
|
+
const sb = new sdk.ScriptBuilder();
|
|
106
|
+
sb.addData(buyerPk);
|
|
107
|
+
sb.addData(providerPk);
|
|
108
|
+
sb.addData(sig);
|
|
109
|
+
sb.addData(tag);
|
|
110
|
+
sb.addData(compiled.hex);
|
|
111
|
+
spend.inputs[0].signatureScript = sb.toString();
|
|
112
|
+
spend.finalize();
|
|
113
|
+
const id = (await rpc.submitTransaction({ transaction: spend, allowOrphan: false })).transactionId;
|
|
114
|
+
console.log(`\n EXPIRED the covenant ran under real consensus and paid out`);
|
|
115
|
+
console.log(` refund ${Number(cov.amount - SPEND_FEE) / 1e8} TKAS -> ${buyerAddr}`);
|
|
116
|
+
console.log(` txid ${id}`);
|
|
117
|
+
console.log(` explorer https://explorer-tn10.kaspa.org/txs/${id}\n`);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await rpc.disconnect().catch(() => undefined);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
main().catch((err) => {
|
|
124
|
+
console.error(`\n ${err instanceof Error ? err.message : String(err)}\n`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
});
|
|
127
|
+
//# sourceMappingURL=live-expire.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-expire.js","sourceRoot":"","sources":["../../tools/live-expire.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-settle.d.ts","sourceRoot":"","sources":["../../tools/live-settle.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm run live:settle` -- drive a real multi-chunk session on testnet-10, including a SUPERSEDE.
|
|
3
|
+
*
|
|
4
|
+
* The single settle that ran first proved the covenant accepts a doubly-signed State. It did not
|
|
5
|
+
* prove the thing the two-phase design exists for: that a LATER State replaces a pending one and
|
|
6
|
+
* a STALE one cannot. This runs all three steps against real consensus --
|
|
7
|
+
*
|
|
8
|
+
* 1. settle seq 0 the first claim posts
|
|
9
|
+
* 2. settle seq 0 again refused; the guard is `seq > pendingSeq`, strictly
|
|
10
|
+
* 3. settle seq 1 supersedes, and the state advances again
|
|
11
|
+
* 4. expire closes it, and the money SPLITS
|
|
12
|
+
*
|
|
13
|
+
* Step 2 is the one worth having. It is the stale-close threat -- "settle an old, lower-seq
|
|
14
|
+
* State" -- and until now it had only ever been refused by a simulator.
|
|
15
|
+
*
|
|
16
|
+
* Step 4 is the other one worth having. Every earlier expire closed a session with NO claim
|
|
17
|
+
* posted -- one output, nothing to divide. This one pays the provider exactly what it is owed and
|
|
18
|
+
* refunds the rest, which is the only branch where getting an output value wrong costs somebody.
|
|
19
|
+
*
|
|
20
|
+
* The simulator is still the pre-flight for every spend EXPECTED to succeed, and it emits the
|
|
21
|
+
* authoritative signature script. See tools/live-steps.ts for why the redeem script must be
|
|
22
|
+
* appended to it by hand.
|
|
23
|
+
*/
|
|
24
|
+
import { mkdtempSync } from 'node:fs';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { randomBytes } from 'node:crypto';
|
|
28
|
+
import { blake3 } from '@noble/hashes/blake3';
|
|
29
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
30
|
+
import { publicKeyHex, signState, digestHex } from '../src/encoding.js';
|
|
31
|
+
import { loadSdk, loadAnchorKey } from './kaspa.js';
|
|
32
|
+
import { compileWithState, covenantAddress } from './covenant.js';
|
|
33
|
+
import { preflightSigscript, awaitUtxo, withState, buildExpire, awaitWindow, x } from './live-steps.js';
|
|
34
|
+
import { profileFromArgv, COVENANT_ID } from './covenant-profile.js';
|
|
35
|
+
import { bindContinuation, fundAsCovenant, covenantIdOf, assertInherited } from './covenant-binding.js';
|
|
36
|
+
// `npm run live:settle -- --profile ag` drives the ARGENT covenant instead.
|
|
37
|
+
const PROFILE = profileFromArgv();
|
|
38
|
+
const NETWORK = 'testnet-10';
|
|
39
|
+
const FUND_SOMPI = 50000000n;
|
|
40
|
+
const FUND_FEE = 250000n;
|
|
41
|
+
const SPEND_FEE = 360000n; // measured: mass 3,429 -> 342,900 required
|
|
42
|
+
const SPEND_BUDGET = 21; // two checkMsgSig calls
|
|
43
|
+
const WINDOW = 2;
|
|
44
|
+
const CLOSE_FEE = 400000n; // the covenant allowance in full; expire is the costliest path
|
|
45
|
+
const PRICE = 3630;
|
|
46
|
+
const CHUNK = 550;
|
|
47
|
+
const dir = mkdtempSync(join(tmpdir(), 'metered-settle-'));
|
|
48
|
+
/** Build one settle spend: pre-flight it, then hand back a signed transaction ready to submit. */
|
|
49
|
+
function buildSettle(sdk, args, pending, claim, redeem, utxo, outAddress, expectPass) {
|
|
50
|
+
const state = {
|
|
51
|
+
v: 1, sessionId: args.sessionId, seq: claim.seq,
|
|
52
|
+
cumulativeUnits: claim.cumulativeUnits, cumulativeSompi: claim.cumulativeSompi,
|
|
53
|
+
prevState: claim.prevState,
|
|
54
|
+
};
|
|
55
|
+
const ctor = [x(args.parties), x(args.sessionId), WINDOW, pending.seq, pending.sompi];
|
|
56
|
+
const outValue = utxo.amount - SPEND_FEE;
|
|
57
|
+
const sigscript = preflightSigscript({
|
|
58
|
+
name: `live settle seq ${claim.seq}${expectPass ? '' : ' (stale)'}`,
|
|
59
|
+
function: 'settle',
|
|
60
|
+
constructor_args: ctor,
|
|
61
|
+
args: [
|
|
62
|
+
x(args.buyer.pk), x(args.provider.pk),
|
|
63
|
+
x(signState(state, args.buyer.sk)), x(signState(state, args.provider.sk)),
|
|
64
|
+
claim.seq, claim.cumulativeUnits, claim.cumulativeSompi, x(claim.prevState ?? '00'.repeat(32)),
|
|
65
|
+
],
|
|
66
|
+
expect: expectPass ? 'pass' : 'fail',
|
|
67
|
+
tx: {
|
|
68
|
+
active_input_index: 0,
|
|
69
|
+
inputs: [{ utxo_value: Number(utxo.amount), constructor_args: ctor, ...(PROFILE.binds ? { covenant_id: COVENANT_ID } : {}) }],
|
|
70
|
+
outputs: [{
|
|
71
|
+
value: Number(outValue), constructor_args: ctor,
|
|
72
|
+
state: PROFILE.outState({
|
|
73
|
+
parties: args.parties, sessionId: args.sessionId, window: WINDOW,
|
|
74
|
+
seq: claim.seq, sompi: claim.cumulativeSompi,
|
|
75
|
+
}),
|
|
76
|
+
...(PROFILE.binds ? { covenant_id: COVENANT_ID, authorizing_input: 0 } : {}),
|
|
77
|
+
}],
|
|
78
|
+
},
|
|
79
|
+
}, dir, expectPass, PROFILE.contract);
|
|
80
|
+
const tx = sdk.createTransaction([utxo], [{ address: outAddress, amount: outValue }], 0n, undefined, 0);
|
|
81
|
+
tx.version = 1;
|
|
82
|
+
tx.gas = 0n;
|
|
83
|
+
tx.inputs[0].sigOpCount = 0;
|
|
84
|
+
tx.inputs[0].computeBudget = SPEND_BUDGET;
|
|
85
|
+
const push = new sdk.ScriptBuilder();
|
|
86
|
+
push.addData(redeem);
|
|
87
|
+
tx.inputs[0].signatureScript = sigscript + push.toString();
|
|
88
|
+
const binding = bindContinuation(sdk, tx, utxo, PROFILE.binds);
|
|
89
|
+
tx.finalize();
|
|
90
|
+
return { tx, state, outValue, binding };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Fund the covenant from the anchor key and wait for its UTXO.
|
|
94
|
+
*
|
|
95
|
+
* AN ARGENT SESSION MUST BE FUNDED AS A COVENANT, not merely paid into, and this cost a rejected
|
|
96
|
+
* transaction to learn. `OpAuthOutputCount(activeInput)` asks how many outputs are authorised by
|
|
97
|
+
* the input being spent -- and an input that belongs to NO covenant authorises nothing, whatever
|
|
98
|
+
* its outputs claim. So the identity has to exist before the first settle, which means the
|
|
99
|
+
* FUNDING transaction is the genesis.
|
|
100
|
+
*
|
|
101
|
+
* The simulator passed the same spend because its test case declares `covenant_id` on the input
|
|
102
|
+
* directly; on chain there was nothing to declare it. That disagreement between simulator and
|
|
103
|
+
* consensus is the finding, and it is why the pre-flight is a floor rather than a proof.
|
|
104
|
+
*
|
|
105
|
+
* The hand-written covenant needs none of this: it constrains `outputs[0]` by convention and
|
|
106
|
+
* never asks who authorised it.
|
|
107
|
+
*/
|
|
108
|
+
async function payInto(sdk, rpc, priv, from, address) {
|
|
109
|
+
const { entries } = await rpc.getUtxosByAddresses([from]);
|
|
110
|
+
const source = entries.reduce((a, b) => (b.amount > a.amount ? b : a));
|
|
111
|
+
const tx = sdk.createTransaction([source], [{ address, amount: FUND_SOMPI }, { address: from, amount: source.amount - FUND_SOMPI - FUND_FEE }], 0n, undefined, 0);
|
|
112
|
+
tx.version = 1;
|
|
113
|
+
tx.gas = 0n;
|
|
114
|
+
for (const i of tx.inputs) {
|
|
115
|
+
i.sigOpCount = 0;
|
|
116
|
+
i.computeBudget = 10;
|
|
117
|
+
}
|
|
118
|
+
fundAsCovenant(tx, PROFILE.binds);
|
|
119
|
+
tx.finalize();
|
|
120
|
+
await rpc.submitTransaction({ transaction: sdk.signTransaction(tx, [priv], true), allowOrphan: false });
|
|
121
|
+
return awaitUtxo(rpc, address, FUND_SOMPI);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Submit a spend that MUST be rejected, and return why. A silent success here would mean the
|
|
125
|
+
* supersede guard does not hold on chain, which is the one outcome that must never pass quietly.
|
|
126
|
+
*/
|
|
127
|
+
async function expectRefusal(rpc, tx) {
|
|
128
|
+
try {
|
|
129
|
+
await rpc.submitTransaction({ transaction: tx, allowOrphan: false });
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
return (err instanceof Error ? err.message : String(err)).split(':').pop()?.trim() ?? 'refused';
|
|
133
|
+
}
|
|
134
|
+
throw new Error('CONSENSUS ACCEPTED A STALE STATE -- the supersede guard does not hold on chain');
|
|
135
|
+
}
|
|
136
|
+
async function main() {
|
|
137
|
+
const funder = loadAnchorKey();
|
|
138
|
+
if (!funder)
|
|
139
|
+
throw new Error('no anchor key; run: npm run anchor -- address');
|
|
140
|
+
const buyer = { sk: bytesToHex(randomBytes(32)), pk: '' };
|
|
141
|
+
const provider = { sk: bytesToHex(randomBytes(32)), pk: '' };
|
|
142
|
+
buyer.pk = publicKeyHex(buyer.sk);
|
|
143
|
+
provider.pk = publicKeyHex(provider.sk);
|
|
144
|
+
const parties = bytesToHex(blake3(new Uint8Array([...hexToBytes(buyer.pk), ...hexToBytes(provider.pk)]), { dkLen: 32 }));
|
|
145
|
+
const sessionId = bytesToHex(randomBytes(16));
|
|
146
|
+
const ids = { parties, sessionId, buyer, provider };
|
|
147
|
+
const opened = compileWithState(dir, { parties, sessionId, window: WINDOW, seq: -1, sompi: 0 }, PROFILE.contract);
|
|
148
|
+
const spanEnd = opened.span.offset + opened.span.len;
|
|
149
|
+
const address = await covenantAddress(opened.hex, NETWORK);
|
|
150
|
+
console.log(`\n covenant ${opened.hex.length / 2} bytes [${PROFILE.key}] state span ${opened.span.len}`);
|
|
151
|
+
console.log(` address ${address}`);
|
|
152
|
+
const sdk = await loadSdk();
|
|
153
|
+
const networkId = new sdk.NetworkId(NETWORK);
|
|
154
|
+
const priv = new sdk.PrivateKey(funder);
|
|
155
|
+
const funderAddr = priv.toKeypair().toAddress(networkId).toString();
|
|
156
|
+
const rpc = new sdk.RpcClient({ resolver: new sdk.Resolver(), encoding: sdk.Encoding.Borsh, networkId });
|
|
157
|
+
await rpc.connect();
|
|
158
|
+
try {
|
|
159
|
+
const utxo0 = await payInto(sdk, rpc, priv, funderAddr, address);
|
|
160
|
+
if (!utxo0)
|
|
161
|
+
throw new Error('the covenant UTXO never appeared');
|
|
162
|
+
console.log(` funded ${Number(FUND_SOMPI) / 1e8} TKAS`);
|
|
163
|
+
// ---- 1. the first claim
|
|
164
|
+
const claim0 = { seq: 0, cumulativeUnits: CHUNK, cumulativeSompi: CHUNK * PRICE, prevState: null };
|
|
165
|
+
const redeem1 = withState(opened.hex, claim0.seq, claim0.cumulativeSompi, spanEnd);
|
|
166
|
+
const addr1 = await covenantAddress(redeem1, NETWORK);
|
|
167
|
+
const first = buildSettle(sdk, ids, { seq: -1, sompi: 0 }, claim0, opened.hex, utxo0, addr1, true);
|
|
168
|
+
const id0 = (await rpc.submitTransaction({ transaction: first.tx, allowOrphan: false })).transactionId;
|
|
169
|
+
const utxo1 = await awaitUtxo(rpc, addr1, first.outValue);
|
|
170
|
+
if (!utxo1)
|
|
171
|
+
throw new Error('the seq-0 continuation never appeared');
|
|
172
|
+
console.log(`\n 1. SETTLED seq 0 ${id0}`);
|
|
173
|
+
if (PROFILE.binds)
|
|
174
|
+
console.log(` covenant ${first.binding} ${covenantIdOf(first.tx)}`);
|
|
175
|
+
// ---- 2. THE STALE CLOSE. Consensus must refuse this: seq 0 is not > pendingSeq 0.
|
|
176
|
+
const stale = buildSettle(sdk, ids, { seq: 0, sompi: claim0.cumulativeSompi }, claim0, redeem1, utxo1, addr1, false);
|
|
177
|
+
console.log(` 2. REFUSED seq 0 again -- ${await expectRefusal(rpc, stale.tx)}`);
|
|
178
|
+
// ---- 3. supersede with a strictly higher seq
|
|
179
|
+
const claim1 = {
|
|
180
|
+
seq: 1, cumulativeUnits: CHUNK * 2, cumulativeSompi: CHUNK * 2 * PRICE,
|
|
181
|
+
prevState: digestHex(first.state),
|
|
182
|
+
};
|
|
183
|
+
const redeem2 = withState(opened.hex, claim1.seq, claim1.cumulativeSompi, spanEnd);
|
|
184
|
+
const addr2 = await covenantAddress(redeem2, NETWORK);
|
|
185
|
+
const second = buildSettle(sdk, ids, { seq: 0, sompi: claim0.cumulativeSompi }, claim1, redeem1, utxo1, addr2, true);
|
|
186
|
+
const id1 = (await rpc.submitTransaction({ transaction: second.tx, allowOrphan: false })).transactionId;
|
|
187
|
+
const utxo2 = await awaitUtxo(rpc, addr2, second.outValue);
|
|
188
|
+
if (!utxo2)
|
|
189
|
+
throw new Error('the seq-1 continuation never appeared');
|
|
190
|
+
console.log(` 3. SETTLED seq 1 ${id1}`);
|
|
191
|
+
if (PROFILE.binds) {
|
|
192
|
+
console.log(` covenant ${second.binding} ${covenantIdOf(second.tx)}`);
|
|
193
|
+
assertInherited(second.binding, second.tx, first.tx);
|
|
194
|
+
}
|
|
195
|
+
console.log(` ${Number(second.outValue) / 1e8} TKAS at the seq-1 covenant, state advanced twice`);
|
|
196
|
+
// ---- 4. CLOSE IT. The branch where the money actually SPLITS: the provider takes the pending
|
|
197
|
+
// claim, the buyer takes the remainder. Until now expire had only ever closed a session with
|
|
198
|
+
// NO claim posted, which is the easy half -- one output, nothing to divide.
|
|
199
|
+
const buyerAddr = new sdk.PrivateKey(buyer.sk).toKeypair().toAddress(networkId).toString();
|
|
200
|
+
const providerAddr = new sdk.PrivateKey(provider.sk).toKeypair().toAddress(networkId).toString();
|
|
201
|
+
const owed = BigInt(claim1.cumulativeSompi);
|
|
202
|
+
const refund = utxo2.amount - owed - CLOSE_FEE;
|
|
203
|
+
// SPEC 7.3: the window is RELATIVE, so the UTXO must actually be that old before ageDaa passes.
|
|
204
|
+
await awaitWindow(rpc, WINDOW);
|
|
205
|
+
const closeTx = buildExpire(sdk, {
|
|
206
|
+
buyerPk: buyer.pk, providerPk: provider.pk, signerSk: provider.sk,
|
|
207
|
+
redeem: redeem2, tag: opened.entries.expire?.dispatch_tag ?? '',
|
|
208
|
+
}, utxo2, [{ address: providerAddr, amount: owed }, { address: buyerAddr, amount: refund }], WINDOW);
|
|
209
|
+
const id2 = (await rpc.submitTransaction({ transaction: closeTx, allowOrphan: false })).transactionId;
|
|
210
|
+
const paid = await awaitUtxo(rpc, providerAddr, owed);
|
|
211
|
+
const refunded = await awaitUtxo(rpc, buyerAddr, refund);
|
|
212
|
+
if (!paid || !refunded)
|
|
213
|
+
throw new Error('the split did not land -- one side was not paid');
|
|
214
|
+
console.log(` 4. CLOSED seq 1 ${id2}`);
|
|
215
|
+
console.log(`\n VERIFIED provider ${Number(owed) / 1e8} TKAS, buyer ${Number(refund) / 1e8} TKAS`);
|
|
216
|
+
console.log(` full lifecycle on chain: settle, refuse a stale one, supersede, split\n`);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
await rpc.disconnect().catch(() => undefined);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
main().catch((err) => {
|
|
223
|
+
console.error(`\n ${err instanceof Error ? err.message : String(err)}\n`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
});
|
|
226
|
+
//# sourceMappingURL=live-settle.js.map
|