sememe 0.0.0-stage → 0.4.21
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 +2667 -2
- package/bin/sememe.mjs +556 -0
- package/index.mjs +48 -0
- package/interface/ata-ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL.json +62 -0
- package/interface/native-11111111111111111111111111111111.json +226 -0
- package/interface/native-ComputeBudget111111111111111111111111111111.json +72 -0
- package/interface/raydium-675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8.json +352 -0
- package/interface/token-TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.json +334 -0
- package/interface/token-TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.json +535 -0
- package/interface/token-metadata.json +132 -0
- package/package.json +71 -4
- package/src/account-list.mjs +308 -0
- package/src/account-union.mjs +523 -0
- package/src/account.mjs +199 -0
- package/src/acquire.mjs +145 -0
- package/src/adopt.mjs +205 -0
- package/src/ata-interface.mjs +276 -0
- package/src/ata.mjs +223 -0
- package/src/base58.mjs +96 -0
- package/src/canonical.mjs +24 -0
- package/src/cli-surface.mjs +300 -0
- package/src/codec.mjs +1069 -0
- package/src/conformance.mjs +431 -0
- package/src/corpus/harvest-compute-budget-2026-10-31.json +19122 -0
- package/src/corpus/harvest-system-2026-10-31.json +26090 -0
- package/src/decode.mjs +286 -0
- package/src/dialect.mjs +145 -0
- package/src/discriminators.mjs +177 -0
- package/src/drift.mjs +172 -0
- package/src/gap.mjs +205 -0
- package/src/lighthouse.mjs +168 -0
- package/src/lock.mjs +440 -0
- package/src/native-audit.mjs +56 -0
- package/src/native-corpus.mjs +57 -0
- package/src/native-interface.mjs +252 -0
- package/src/native.mjs +324 -0
- package/src/peer.mjs +95 -0
- package/src/program-id.mjs +74 -0
- package/src/pull.mjs +117 -0
- package/src/rawle.mjs +450 -0
- package/src/raydium-interface.mjs +747 -0
- package/src/raydium.mjs +391 -0
- package/src/receipt-digest.mjs +84 -0
- package/src/receipt-name.mjs +128 -0
- package/src/receipt-order.mjs +56 -0
- package/src/shank.mjs +73 -0
- package/src/short-forms.mjs +209 -0
- package/src/store-read.mjs +250 -0
- package/src/store-write.mjs +46 -0
- package/src/token-interface.mjs +394 -0
- package/src/token-metadata-interface.mjs +371 -0
- package/src/token-metadata.mjs +317 -0
- package/src/token.mjs +443 -0
- package/src/union.mjs +428 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/LICENSE.txt +201 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/compute-budget-interface/src/lib.rs +93 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/system-interface/src/instruction.rs +1899 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/LICENSE +21 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/lighthouse.json +2769 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/LICENSE +201 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/instruction.rs +1189 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/state.rs +1348 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/LICENSE +201 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/instruction.rs +169 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/lib.rs +12 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/program/src/processor.rs +308 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/LICENSE.txt +176 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/interface/src/instruction.rs +1963 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/LICENSE.txt +176 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/interface/src/instruction.rs +2868 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/LICENSE +201 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/error.rs +58 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/instruction.rs +596 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/lib.rs +21 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/state.rs +229 -0
package/src/shank.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Shank / Kinobi dialect support for IDL-driven decoding.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: not every published IDL is an Anchor IDL. Shank-generated
|
|
4
|
+
// IDLs (and the Codama/Kinobi clients built from them) describe the SAME Borsh
|
|
5
|
+
// field layouts but a DIFFERENT dispatch and length dialect:
|
|
6
|
+
// - instructions carry `discriminant: { type: "u8", value: N }` — a single
|
|
7
|
+
// variant-order byte — instead of an 8-byte Anchor sighash;
|
|
8
|
+
// - enums are Borsh 1-byte variant indexes (sememe already decoded these);
|
|
9
|
+
// - the lighthouse family encodes array/string lengths as LEB128
|
|
10
|
+
// (`CompactU64`, `CompactBytes`, `*Assertions` — custom types the IDL
|
|
11
|
+
// references but does not define; the wire form is pinned upstream in
|
|
12
|
+
// lighthouse_common::CompactU64 / LEB128Vec and mirrored by the program's
|
|
13
|
+
// own generated JS client, clients/js/src/hooked.ts at Jac0xb/lighthouse).
|
|
14
|
+
//
|
|
15
|
+
// Measured ground truth (2026-11-04): all 42 Lighthouse payloads in the
|
|
16
|
+
// 156-fixture foreign sweep (step-ingestooor @001cf39) dispatch on their shank
|
|
17
|
+
// discriminant byte (0x06 = AssertAccountInfoMulti, 0x0a =
|
|
18
|
+
// AssertTokenAccountMulti — both present in the first-party IDL) and decode +
|
|
19
|
+
// re-encode byte-exactly under this dialect.
|
|
20
|
+
//
|
|
21
|
+
// Rules, all falsifiable:
|
|
22
|
+
// - A shank discriminant is NEVER overwritten by Anchor's sighash derivation:
|
|
23
|
+
// the two schemes are independent; deriving one from the other produces a
|
|
24
|
+
// table that matches no real wire. (The pre-fix behavior filled all 18
|
|
25
|
+
// lighthouse instructions with bogus sighashes — measured wrong.)
|
|
26
|
+
// - The hooked LEB128 types engage only for an IDL in the shank dialect and
|
|
27
|
+
// only for a name the IDL does not define itself; a name defined in the
|
|
28
|
+
// IDL's own `types` always wins, and an unknown undefined name still
|
|
29
|
+
// refuses (`unknown defined type`). Nothing is guessed from shape.
|
|
30
|
+
|
|
31
|
+
const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
32
|
+
|
|
33
|
+
/** True when `instruction` carries a shank 1-byte discriminant. */
|
|
34
|
+
export function isShankDiscriminant(instruction) {
|
|
35
|
+
const raw = instruction?.discriminant;
|
|
36
|
+
return isPlainObject(raw) && raw.type === 'u8'
|
|
37
|
+
&& Number.isInteger(raw.value) && raw.value >= 0 && raw.value <= 255;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The 1-byte discriminator as a Buffer, or null when not shank-shaped. */
|
|
41
|
+
export function shankDiscriminatorBytes(instruction) {
|
|
42
|
+
return isShankDiscriminant(instruction) ? Buffer.from([instruction.discriminant.value]) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The hooked LEB128 types, pinned from lighthouse_common (Jac0xb/lighthouse,
|
|
47
|
+
* programs/lighthouse/src/instruction.rs: `CompactU64`, `LEB128Vec<T>`) and
|
|
48
|
+
* the program's generated clients/js/src/hooked.ts:
|
|
49
|
+
* - scalar: unsigned LEB128 (7-bit groups, little-endian, max 10 bytes)
|
|
50
|
+
* - bytes: unsigned LEB128 length prefix + that many raw bytes
|
|
51
|
+
* - vec: unsigned LEB128 length prefix + that many Borsh elements
|
|
52
|
+
* The `*Assertions` element types stay in the IDL's own `types`; only the
|
|
53
|
+
* length dialect is hooked.
|
|
54
|
+
*/
|
|
55
|
+
export const SHANK_LEB128_TYPES = new Map([
|
|
56
|
+
['CompactU64', { kind: 'scalar' }],
|
|
57
|
+
['CompactBytes', { kind: 'bytes' }],
|
|
58
|
+
['AccountInfoAssertions', { kind: 'vec', element: 'AccountInfoAssertion' }],
|
|
59
|
+
['AccountDataAssertions', { kind: 'vec', element: 'AccountDataAssertion' }],
|
|
60
|
+
['MintAccountAssertions', { kind: 'vec', element: 'MintAccountAssertion' }],
|
|
61
|
+
['TokenAccountAssertions', { kind: 'vec', element: 'TokenAccountAssertion' }],
|
|
62
|
+
['StakeAccountAssertions', { kind: 'vec', element: 'StakeAccountAssertion' }],
|
|
63
|
+
['UpgradeableLoaderStateAssertions', { kind: 'vec', element: 'UpgradeableLoaderStateAssertion' }],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether an IDL is in the shank dialect: declared via metadata.origin, or
|
|
68
|
+
* shaped so (at least one instruction carries a 1-byte discriminant).
|
|
69
|
+
*/
|
|
70
|
+
export function isShankIdl(idl) {
|
|
71
|
+
if (idl?.metadata?.origin === 'shank') return true;
|
|
72
|
+
return (idl?.instructions ?? []).some((instruction) => isShankDiscriminant(instruction));
|
|
73
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Chain-verified short wire forms: payloads the DEPLOYED program executed that
|
|
2
|
+
// the stored interface describes as one field LONGER than the bytes on the wire.
|
|
3
|
+
//
|
|
4
|
+
// Why this table exists, and why it is not simply "decode when a payload ends
|
|
5
|
+
// at a field boundary":
|
|
6
|
+
//
|
|
7
|
+
// Byte-exact re-encoding is NOT sufficient evidence for an absent field. With
|
|
8
|
+
// `trailing` it is: a complete described prefix that re-encodes exactly means
|
|
9
|
+
// the extra bytes are extra, and the described part is whole. With `absent` it
|
|
10
|
+
// is not — a payload truncated by a capture bug also ends on a field boundary
|
|
11
|
+
// and also re-encodes byte-exactly to the shortened bytes. So offline, on bytes
|
|
12
|
+
// alone, "the deployed program accepts a shorter form" and "this payload was
|
|
13
|
+
// cut short" are indistinguishable. src/codec.mjs therefore keeps
|
|
14
|
+
// `absent: 'error'` as the default and refuses both.
|
|
15
|
+
//
|
|
16
|
+
// What makes the shorter form KNOWABLE is a chain witness. Each entry below was
|
|
17
|
+
// measured on mainnet by tools/short-form-wire-forms.mjs against
|
|
18
|
+
// research/short-form-wire-forms-2026-11-03.json (the 4-block window) and
|
|
19
|
+
// research/short-form-wire-forms-2026-11-04.json (the 32-block window):
|
|
20
|
+
//
|
|
21
|
+
// - the transaction's `err` field is null (the chain accepted it),
|
|
22
|
+
// - the witness instruction bytes are byte-IDENTICAL to the fixture payload,
|
|
23
|
+
// - the program's own log line names the instruction the decode will report.
|
|
24
|
+
//
|
|
25
|
+
// On the 2026-11-03 12,572-payload sample that resolved 171 of the 172
|
|
26
|
+
// refusals: 0 of them were genuine chain-rejected refusals. The 2026-11-04
|
|
27
|
+
// 89,546-payload window resolved 350 of 350 refusals the same way, and found
|
|
28
|
+
// three MORE short forms the first window had never seen — because the table
|
|
29
|
+
// is keyed on program + discriminator + exact length, and the first window
|
|
30
|
+
// simply did not contain them: pump.fun's bonding curve executes the 24-byte
|
|
31
|
+
// `buy` form that had only been witnessed on PumpSwap, plus `buy_exact_sol_in`
|
|
32
|
+
// and the 146-byte `create_v2`, which omits THREE declared fields.
|
|
33
|
+
//
|
|
34
|
+
// The table is the product form of those measurements — the evidence travels
|
|
35
|
+
// with the decoder, so `absent: 'verified'` can name these forms while still
|
|
36
|
+
// refusing every other payload that happens to end on a field boundary.
|
|
37
|
+
//
|
|
38
|
+
// A payload must match program + discriminator + EXACT wire length. Matching
|
|
39
|
+
// the discriminator alone is not enough: the 25-byte form of
|
|
40
|
+
// e1f7501ed5b38488 is a different, fully-described shape and must not be routed
|
|
41
|
+
// through the absent path (it is not absent — both OptionBools are present).
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Verified short wire forms.
|
|
45
|
+
*
|
|
46
|
+
* `program` deployed program address the witness belongs to
|
|
47
|
+
* `bytes` exact payload length, discriminator included
|
|
48
|
+
* `absent` declared fields the wire does not carry, in declaration order —
|
|
49
|
+
* named here so a decode that reports a DIFFERENT absent set does
|
|
50
|
+
* not silently count as verified
|
|
51
|
+
*/
|
|
52
|
+
const VERIFIED_SHORT_FORMS = [
|
|
53
|
+
{
|
|
54
|
+
program: '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
|
|
55
|
+
discriminator: 'e1f7501ed5b38488',
|
|
56
|
+
bytes: 24,
|
|
57
|
+
instruction: 'buy_exact_quote_in_v3',
|
|
58
|
+
absent: ['partial_fill'],
|
|
59
|
+
witnesses: 152,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
program: '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
|
|
63
|
+
discriminator: '07051dc4f5176550',
|
|
64
|
+
bytes: 24,
|
|
65
|
+
instruction: 'buy_v3',
|
|
66
|
+
absent: ['partial_fill'],
|
|
67
|
+
witnesses: 1,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
program: 'pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA',
|
|
71
|
+
discriminator: 'c62e1552b4d9e870',
|
|
72
|
+
bytes: 24,
|
|
73
|
+
instruction: 'buy_exact_quote_in',
|
|
74
|
+
absent: ['track_volume'],
|
|
75
|
+
witnesses: 17,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
program: 'pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA',
|
|
79
|
+
discriminator: '66063d1201daebea',
|
|
80
|
+
bytes: 24,
|
|
81
|
+
instruction: 'buy',
|
|
82
|
+
absent: ['track_volume'],
|
|
83
|
+
witnesses: 1,
|
|
84
|
+
},
|
|
85
|
+
// --- 2026-11-04 window (research/short-form-wire-forms-2026-11-04.json) ---
|
|
86
|
+
// The same 24-byte `buy` form, on pump.fun's BONDING CURVE program. The
|
|
87
|
+
// discriminator and the length match the PumpSwap entry above, but the
|
|
88
|
+
// program is a different deployment, so the 2026-11-03 window could not
|
|
89
|
+
// witness it: the key is program + discriminator + exact length, and only
|
|
90
|
+
// one of the two programs was measured then.
|
|
91
|
+
{
|
|
92
|
+
program: '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
|
|
93
|
+
discriminator: '66063d1201daebea',
|
|
94
|
+
bytes: 24,
|
|
95
|
+
instruction: 'buy',
|
|
96
|
+
absent: ['track_volume'],
|
|
97
|
+
witnesses: 7,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
program: '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
|
|
101
|
+
discriminator: '38fc74089edfcd5f',
|
|
102
|
+
bytes: 24,
|
|
103
|
+
instruction: 'buy_exact_sol_in',
|
|
104
|
+
absent: ['track_volume'],
|
|
105
|
+
witnesses: 1,
|
|
106
|
+
},
|
|
107
|
+
// The first short form that omits MORE THAN ONE declared field: the
|
|
108
|
+
// interface declares three, the wire carries none of them.
|
|
109
|
+
{
|
|
110
|
+
program: '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
|
|
111
|
+
discriminator: 'd6904cec5f8b31b4',
|
|
112
|
+
bytes: 146,
|
|
113
|
+
instruction: 'create_v2',
|
|
114
|
+
absent: ['is_cashback_enabled', 'creator_fee_bps', 'is_holder_reward'],
|
|
115
|
+
witnesses: 1,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
export const SHORT_FORM_SOURCE =
|
|
120
|
+
'research/short-form-wire-forms-2026-11-03.json + research/short-form-wire-forms-2026-11-04.json '
|
|
121
|
+
+ '(chain witnesses: err:null, byte-identical payload, matching program log line)';
|
|
122
|
+
|
|
123
|
+
export const VERIFIED = VERIFIED_SHORT_FORMS;
|
|
124
|
+
export { VERIFIED_SHORT_FORMS };
|
|
125
|
+
|
|
126
|
+
/** Look up a payload in the chain-verified short-form table; null when absent. */
|
|
127
|
+
export function verifiedShortForm(program, data) {
|
|
128
|
+
if (!program || !(data instanceof Uint8Array)) return null;
|
|
129
|
+
if (data.length < 8) return null;
|
|
130
|
+
const discriminator = Buffer.from(data.subarray(0, 8)).toString('hex');
|
|
131
|
+
return VERIFIED_SHORT_FORMS.find((entry) =>
|
|
132
|
+
entry.program === program
|
|
133
|
+
&& entry.discriminator === discriminator
|
|
134
|
+
&& entry.bytes === data.length) ?? null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve an `absent` policy into the codec policy for one payload.
|
|
139
|
+
*
|
|
140
|
+
* 'error' never decode absent fields (the strict default)
|
|
141
|
+
* 'report' decode any payload that ends on a field boundary (caller risk)
|
|
142
|
+
* 'verified' decode only forms in the chain-verified table; anything else
|
|
143
|
+
* stays strict — the evidence-gated policy
|
|
144
|
+
*/
|
|
145
|
+
export function resolveAbsentPolicy(program, data, policy) {
|
|
146
|
+
if (policy === undefined || policy === null || policy === 'error') return { policy: 'error', form: null };
|
|
147
|
+
if (policy === 'report') return { policy: 'report', form: null };
|
|
148
|
+
if (policy !== 'verified') {
|
|
149
|
+
throw new Error(`unknown absent policy ${JSON.stringify(policy)}`);
|
|
150
|
+
}
|
|
151
|
+
const form = verifiedShortForm(program, data);
|
|
152
|
+
return { policy: form ? 'report' : 'error', form };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Assert that a completed decode reproduces the measured wire form.
|
|
157
|
+
*
|
|
158
|
+
* Throws when it does not. This is the single place the rule lives, so the
|
|
159
|
+
* product paths (single-source and union) cannot drift apart from it, and so
|
|
160
|
+
* the rule can be tested directly with synthetic decodes — a different missing
|
|
161
|
+
* set, a different name — instead of only through a fixture store.
|
|
162
|
+
*/
|
|
163
|
+
export function assertShortFormEvidence(form, decoded, bytes) {
|
|
164
|
+
const evidence = describeShortForm(form, decoded, bytes);
|
|
165
|
+
if (!evidence.verified) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`short-form evidence does not cover this decode: measured absent `
|
|
168
|
+
+ `${JSON.stringify(form.absent)} for ${form.instruction}, decoded `
|
|
169
|
+
+ `${JSON.stringify(evidence.decodeAbsence)} — refusing instead of trusting stale evidence`);
|
|
170
|
+
}
|
|
171
|
+
return evidence;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Does a completed decode reproduce the measured wire form?
|
|
176
|
+
*
|
|
177
|
+
* Pure on purpose: the rules for trusting old chain evidence are worth testing
|
|
178
|
+
* directly with synthetic decodes (a different missing set, a different name)
|
|
179
|
+
* rather than only through a fixture store. Returns the evidence object with
|
|
180
|
+
* `verified` set — it never throws, so a caller can choose to refuse.
|
|
181
|
+
*
|
|
182
|
+
* `decoded` may be a codec result (`instruction`/`event`) or a union result
|
|
183
|
+
* (`name`); both carry `absent`.
|
|
184
|
+
*/
|
|
185
|
+
export function describeShortForm(form, decoded, bytes) {
|
|
186
|
+
const named = decoded.absent ?? [];
|
|
187
|
+
const sameSet = named.length === form.absent.length
|
|
188
|
+
&& form.absent.every((field) => named.includes(field));
|
|
189
|
+
const decodedName = decoded.name
|
|
190
|
+
?? (decoded.kind === 'event' ? decoded.event : decoded.instruction);
|
|
191
|
+
return {
|
|
192
|
+
// Verified means: THIS decode named exactly the fields the chain witness
|
|
193
|
+
// saw missing, under the name the witness's log line carried. A decoder
|
|
194
|
+
// that reports a different absence has not reproduced the measured form —
|
|
195
|
+
// the IDL gained or lost a field since the witness was taken — and stale
|
|
196
|
+
// evidence must not be spent on a shape nobody measured.
|
|
197
|
+
verified: sameSet && (decodedName === undefined || decodedName === form.instruction),
|
|
198
|
+
program: form.program,
|
|
199
|
+
discriminator: form.discriminator,
|
|
200
|
+
bytes: form.bytes,
|
|
201
|
+
instruction: form.instruction,
|
|
202
|
+
absent: form.absent,
|
|
203
|
+
decodeAbsence: named,
|
|
204
|
+
decodedName,
|
|
205
|
+
witnesses: form.witnesses,
|
|
206
|
+
source: SHORT_FORM_SOURCE,
|
|
207
|
+
wireBytes: bytes.length,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Read one stored IDL for a program, verified against its receipt.
|
|
2
|
+
//
|
|
3
|
+
// The store layout is the one produced by `sememe pull` / `sememe adopt`:
|
|
4
|
+
// <store>/receipts/<program>-<slot>-<source>-<fingerprint>.json
|
|
5
|
+
// <store>/objects/<fingerprint>.json
|
|
6
|
+
// The receipt binds the program id to the object, so decoding is offline and
|
|
7
|
+
// reproducible: same store + same payload => same result, no network call.
|
|
8
|
+
//
|
|
9
|
+
// The receipt is a claim; the recomputed hash is the check. Every step that can
|
|
10
|
+
// lie on the way to a decode is refused with a named reason instead of being
|
|
11
|
+
// tolerated:
|
|
12
|
+
// - a receipt whose program field is not the program asked for (a receipt
|
|
13
|
+
// swapped in from another program's file)
|
|
14
|
+
// - a receipt pointing at an object that is not in the store
|
|
15
|
+
// - an object that does not re-hash to the fingerprint the receipt claims
|
|
16
|
+
// (the defect measured 2026-10-30: before this check, a tampered object —
|
|
17
|
+
// valid JSON, one instruction renamed — was decoded happily and its
|
|
18
|
+
// tampered name reported as provenance)
|
|
19
|
+
// - an object declaring a different program than the receipt binds
|
|
20
|
+
// - a receipt whose DECLARED origin or body no longer matches its own
|
|
21
|
+
// digest (src/receipt-digest.mjs): the fingerprint covers the OBJECT, so
|
|
22
|
+
// before the receipt carried a digest, rewriting `origin` in the receipt
|
|
23
|
+
// left `sememe verify` green over a rewritten provenance label — measured
|
|
24
|
+
// 2026-11-07: violations 0 after `origin` was changed to
|
|
25
|
+
// `attacker/rewritten-origin`. A legacy receipt without digests is read as
|
|
26
|
+
// it always was: its origin is then declared WITHOUT a binding.
|
|
27
|
+
// An unreadable or tampered store must never become another source's names.
|
|
28
|
+
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { canonicalJson } from './canonical.mjs';
|
|
33
|
+
import { compareReceiptNames } from './receipt-order.mjs';
|
|
34
|
+
import { classifyReceiptFiles, isReceiptFile } from './receipt-name.mjs';
|
|
35
|
+
import { receiptIntegrity } from './receipt-digest.mjs';
|
|
36
|
+
import { assertIdlShape, declaredProgram } from './dialect.mjs';
|
|
37
|
+
|
|
38
|
+
export { canonicalJson } from './canonical.mjs';
|
|
39
|
+
|
|
40
|
+
/** Read and verify one receipt+object pair. Throws with a named reason.
|
|
41
|
+
*
|
|
42
|
+
* Exported for `src/lock.mjs`: the lock must re-check every receipt through
|
|
43
|
+
* THIS reader — a second, weaker copy of the checks would recreate the "one
|
|
44
|
+
* store, two strengths" failure the header warns about. */
|
|
45
|
+
export async function readVerified(storeDir, program, receiptName) {
|
|
46
|
+
let receipt;
|
|
47
|
+
try {
|
|
48
|
+
receipt = JSON.parse(await readFile(path.join(storeDir, 'receipts', receiptName), 'utf8'));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw Error(`store receipt ${receiptName} is unreadable: ${String(error?.message ?? error).slice(0, 140)}`);
|
|
51
|
+
}
|
|
52
|
+
if (receipt?.program !== program) {
|
|
53
|
+
throw Error(`store receipt ${receiptName} names program ${receipt?.program} — it is not a receipt for ${program}`);
|
|
54
|
+
}
|
|
55
|
+
// The receipt's OWN integrity, before it is allowed to name anything. A
|
|
56
|
+
// rewritten declared origin is refused HERE, on every read — decode, union
|
|
57
|
+
// and verify all see the same refusal, and the reason says which kind of
|
|
58
|
+
// rewrite happened (the lock reports the two differently).
|
|
59
|
+
const integrity = receiptIntegrity(receipt);
|
|
60
|
+
if (integrity) {
|
|
61
|
+
const error = Error(`store receipt ${receiptName}: ${integrity.reason}`);
|
|
62
|
+
error.code = integrity.code;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
const fingerprint = String(receipt.fingerprint).replace(/^sha256:/, '');
|
|
66
|
+
if (!/^[a-f0-9]{64}$/.test(fingerprint)) throw Error(`store receipt has a malformed fingerprint: ${receipt.fingerprint}`);
|
|
67
|
+
const objectPath = path.join(storeDir, 'objects', `${fingerprint}.json`);
|
|
68
|
+
let raw;
|
|
69
|
+
try { raw = await readFile(objectPath); }
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error?.code === 'ENOENT') {
|
|
72
|
+
throw Error(`store receipt ${receiptName} claims object ${fingerprint} but the store object is missing`);
|
|
73
|
+
}
|
|
74
|
+
throw Error(`store object ${fingerprint}.json is unreadable: ${String(error?.message ?? error).slice(0, 140)}`);
|
|
75
|
+
}
|
|
76
|
+
let idl;
|
|
77
|
+
try { idl = JSON.parse(raw); }
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw Error(`store object ${fingerprint}.json is not JSON: ${String(error?.message ?? error).slice(0, 140)}`);
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(idl.instructions)) throw Error(`stored object for ${program} has no instructions`);
|
|
82
|
+
// Same two checks as the write path (src/adopt.mjs), applied on every
|
|
83
|
+
// read: a dialect the codec cannot read is refused by NAME before any
|
|
84
|
+
// decode can crash unnamed, and a declared program that differs from the
|
|
85
|
+
// receipt's program is a re-labelling no matter which field carries the
|
|
86
|
+
// declaration (`address`, `metadata.address` or `program_id`).
|
|
87
|
+
assertIdlShape(idl, (reason) => {
|
|
88
|
+
const error = Error(`stored object ${fingerprint} for ${program}: ${reason}`);
|
|
89
|
+
error.code = 'unsupported-idl-dialect';
|
|
90
|
+
return error;
|
|
91
|
+
});
|
|
92
|
+
const declared = declaredProgram(idl);
|
|
93
|
+
if (declared && declared !== program) {
|
|
94
|
+
throw Error(`stored object ${fingerprint} declares program ${declared}, not the receipt's ${program}`);
|
|
95
|
+
}
|
|
96
|
+
if (idl.address && idl.address !== program) {
|
|
97
|
+
throw Error(`stored object ${fingerprint} declares program ${idl.address}, not the receipt's ${program}`);
|
|
98
|
+
}
|
|
99
|
+
// `pull` writes sha256(canonicalJson(idl) + '\n'), so verify with the same
|
|
100
|
+
// canonicalizer rather than with JSON.stringify, whose key order is not the
|
|
101
|
+
// stored order. A receipt without this check is a filename, not provenance.
|
|
102
|
+
const actual = createHash('sha256').update(`${canonicalJson(idl)}\n`).digest('hex');
|
|
103
|
+
if (actual !== fingerprint) {
|
|
104
|
+
throw Error(
|
|
105
|
+
`stored object for ${program} re-hashes to sha256:${actual} but receipt ${receiptName} `
|
|
106
|
+
+ `claims sha256:${fingerprint}: object and receipt disagree, refusing to decode`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return { idl, receipt, receiptName, objectPath };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function receiptNames(storeDir, program) {
|
|
113
|
+
const receipts = path.join(storeDir, 'receipts');
|
|
114
|
+
let names;
|
|
115
|
+
try { names = await readdir(receipts); }
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error?.code === 'ENOENT') {
|
|
118
|
+
// `code` names the KIND of failure so a caller can distinguish "this
|
|
119
|
+
// store does not exist" from the absence of "this program is not in it"
|
|
120
|
+
// WITHOUT parsing prose — src/conformance.mjs needs exactly that
|
|
121
|
+
// (measured 2026-11-10: it swallowed both and reported a green gate).
|
|
122
|
+
// The message is unchanged: it is quoted by bin/sememe.mjs, the README
|
|
123
|
+
// and several tests.
|
|
124
|
+
const missing = Error(`no sememe store at ${storeDir}: run "sememe pull" first`);
|
|
125
|
+
missing.code = 'no-sememe-store';
|
|
126
|
+
throw missing;
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
const candidates = names.filter((name) => name.startsWith(`${program}-`) && isReceiptFile(name));
|
|
131
|
+
if (!candidates.length) {
|
|
132
|
+
// The store EXISTS and simply holds no receipt for this program: an
|
|
133
|
+
// acquisition gap, never damage. Callers must be able to report it as
|
|
134
|
+
// such instead of mistaking it for a reader refusal.
|
|
135
|
+
const absent = Error(`no stored IDL for program ${program} in ${storeDir}`);
|
|
136
|
+
absent.code = 'no-stored-idl';
|
|
137
|
+
throw absent;
|
|
138
|
+
}
|
|
139
|
+
// Numeric slot order, NOT byte order: the slot in the filename is a decimal
|
|
140
|
+
// NUMBER, and "99999999" sorts after "123456789" as a string — so a plain
|
|
141
|
+
// sort picks the OLDER receipt as "last = newest" whenever two slot strings
|
|
142
|
+
// differ in length (measured 2026-11-07: plain decode and lock said slot
|
|
143
|
+
// 99999999, the union path — which sorts by numeric slot — said 123456789).
|
|
144
|
+
// ONE ordering, shared with src/lock.mjs via src/receipt-order.mjs.
|
|
145
|
+
candidates.sort(compareReceiptNames);
|
|
146
|
+
return candidates;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Find the newest stored IDL for a program id, verified against its receipt. */
|
|
150
|
+
export async function findStoredIdl(storeDir, program) {
|
|
151
|
+
const candidates = await receiptNames(storeDir, program);
|
|
152
|
+
return readVerified(storeDir, program, candidates[candidates.length - 1]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Find the store's sources for a program, split by provenance.
|
|
157
|
+
*
|
|
158
|
+
* `primary` is the newest ON-CHAIN receipt (a real slot) and `secondary` is the
|
|
159
|
+
* newest ADOPTED receipt (a published copy, slot null). The split is what keeps
|
|
160
|
+
* the union honest: a published copy must never silently become the default
|
|
161
|
+
* single source, and an on-chain pull must never be dropped because a file was
|
|
162
|
+
* adopted later.
|
|
163
|
+
*
|
|
164
|
+
* TWO RULES THAT MUST NOT DIVERGE (measured 2026-11-07, this host):
|
|
165
|
+
*
|
|
166
|
+
* 1. Adopted copies are ordered by the SHARED receipt ordering
|
|
167
|
+
* (src/receipt-order.mjs) — the exact rule `findStoredIdl` (the plain
|
|
168
|
+
* decode) and `src/lock.mjs` apply. They used to be ordered by their
|
|
169
|
+
* human-readable LABEL, which embeds the ORIGIN the caller typed: with
|
|
170
|
+
* two adopted copies the union path read the copy the origin spelling
|
|
171
|
+
* pointed at while the plain decode and the lock read the copy the
|
|
172
|
+
* fingerprint pointed at. Same store, same bytes, two answers —
|
|
173
|
+
* plain `decode` said `sell_v3`, `decode --union` refused the payload.
|
|
174
|
+
* 2. When the store holds NO on-chain receipt and MULTIPLE adopted copies,
|
|
175
|
+
* the newest adopted copy is the `primary` and the next one the
|
|
176
|
+
* `secondary`, so the union path arbitrates the copies the store actually
|
|
177
|
+
* holds instead of consulting one and ignoring the rest. With exactly one
|
|
178
|
+
* adopted copy and no pull, `primary` stays null (the sole-source case):
|
|
179
|
+
* nothing on chain means there is no on-chain primary.
|
|
180
|
+
*
|
|
181
|
+
* Everything the pair does not consult is returned as `additional`, so the
|
|
182
|
+
* caller can NAME it instead of pretending the store holds two sources.
|
|
183
|
+
*
|
|
184
|
+
* Both are re-hashed against their receipts here; an unreadable or tampered
|
|
185
|
+
* object is refused by name, never skipped silently.
|
|
186
|
+
*/
|
|
187
|
+
export async function findStoredIdls(storeDir, program) {
|
|
188
|
+
const candidates = await receiptNames(storeDir, program);
|
|
189
|
+
const onChain = [];
|
|
190
|
+
const adopted = [];
|
|
191
|
+
for (const name of candidates) {
|
|
192
|
+
// The slot segment is the second field: `<program>-<slot>-<source>-<hash>`.
|
|
193
|
+
const rest = name.slice(`${program}-`.length);
|
|
194
|
+
const slotSegment = rest.split('-')[0];
|
|
195
|
+
const source = await readVerified(storeDir, program, name);
|
|
196
|
+
const entry = {
|
|
197
|
+
// Human-readable provenance; the receipt filename stays in idlMeta.receipt.
|
|
198
|
+
// An ADOPTED origin is DECLARED, never verified — the label says so, so no
|
|
199
|
+
// reader can present a caller-typed string as a checked fact. A legacy
|
|
200
|
+
// receipt without originStatus is declared too (adopt never verified it).
|
|
201
|
+
label: source.receipt.slot === null || source.receipt.slot === undefined
|
|
202
|
+
? `${source.receipt.source} (adopted, declared origin: ${source.receipt.origin ?? source.receipt.fingerprint.slice(0, 12)})`
|
|
203
|
+
: `${source.receipt.source} @slot ${source.receipt.slot}`,
|
|
204
|
+
idl: source.idl,
|
|
205
|
+
idlMeta: {
|
|
206
|
+
fingerprint: source.receipt.fingerprint,
|
|
207
|
+
slot: source.receipt.slot ?? null,
|
|
208
|
+
source: source.receipt.source,
|
|
209
|
+
origin: source.receipt.origin ?? null,
|
|
210
|
+
originStatus: source.receipt.originStatus
|
|
211
|
+
?? (source.receipt.origin !== undefined && source.receipt.origin !== null ? 'declared' : null),
|
|
212
|
+
receipt: source.receiptName,
|
|
213
|
+
},
|
|
214
|
+
objectPath: source.objectPath,
|
|
215
|
+
};
|
|
216
|
+
if (source.receipt.slot === null || source.receipt.slot === undefined) adopted.push(entry);
|
|
217
|
+
else if (/^\d+$/.test(slotSegment)) onChain.push(entry);
|
|
218
|
+
else adopted.push(entry);
|
|
219
|
+
}
|
|
220
|
+
// Ordered by the ONE shared rule (numeric slot, filename tiebreak) — the
|
|
221
|
+
// same order `candidates` already arrives in from `receiptNames`, and the
|
|
222
|
+
// same order `findStoredIdl` and src/lock.mjs apply. Never the label (it
|
|
223
|
+
// carries the origin string the caller typed) and never the raw string (it
|
|
224
|
+
// orders by digits, not by value).
|
|
225
|
+
onChain.sort((a, b) => compareReceiptNames(a.idlMeta.receipt, b.idlMeta.receipt));
|
|
226
|
+
// Adopted copies are ordered by the SAME shared rule — numeric slot first
|
|
227
|
+
// (they all carry filename segment 0), then the filename byte-wise. Never
|
|
228
|
+
// the label: it carries the origin string the caller typed, and two copies
|
|
229
|
+
// of one program then answered from two different orderings.
|
|
230
|
+
adopted.sort((a, b) => compareReceiptNames(a.idlMeta.receipt, b.idlMeta.receipt));
|
|
231
|
+
|
|
232
|
+
let primary = onChain.length ? onChain[onChain.length - 1] : null;
|
|
233
|
+
let secondary = adopted.length ? adopted[adopted.length - 1] : null;
|
|
234
|
+
if (!primary && adopted.length >= 2) {
|
|
235
|
+
// No pull in the store, but several verified published copies: they ARE the
|
|
236
|
+
// union's two sources. Ranking them by the shared rule keeps the primary
|
|
237
|
+
// equal to the copy `findStoredIdl` reads, so plain and union decode agree.
|
|
238
|
+
primary = adopted[adopted.length - 1];
|
|
239
|
+
secondary = adopted[adopted.length - 2];
|
|
240
|
+
}
|
|
241
|
+
const consulted = new Set([primary, secondary].filter(Boolean));
|
|
242
|
+
const additional = [...onChain, ...adopted].filter((entry) => !consulted.has(entry));
|
|
243
|
+
return {
|
|
244
|
+
primary,
|
|
245
|
+
secondary,
|
|
246
|
+
additional,
|
|
247
|
+
onChain,
|
|
248
|
+
adopted,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Atomic no-replace writer for store objects and receipts.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from src/pull.mjs so that `sememe adopt` (a published copy with no
|
|
4
|
+
// RPC involved) can share the exact same write contract as `sememe pull`:
|
|
5
|
+
// - temp file created O_EXCL with 0600
|
|
6
|
+
// - fsync before install
|
|
7
|
+
// - hard-link install: another writer (or a symlink planted at the target)
|
|
8
|
+
// cannot be overwritten; a losing writer verifies the installed bytes
|
|
9
|
+
// - any existing destination with DIFFERENT bytes is an error, never replaced
|
|
10
|
+
// A store entry is content-addressed: overwriting one digest with other bytes
|
|
11
|
+
// would break every receipt that points at it.
|
|
12
|
+
|
|
13
|
+
import { link, lstat, open, readFile, rm } from 'node:fs/promises';
|
|
14
|
+
|
|
15
|
+
export async function writeOnce(destination, content) {
|
|
16
|
+
try {
|
|
17
|
+
const entry = await lstat(destination);
|
|
18
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw Error(`non-regular store entry: ${destination}`);
|
|
19
|
+
const current = await readFile(destination);
|
|
20
|
+
if (!current.equals(content)) throw Error(`digest collision or corrupted local object: ${destination}`);
|
|
21
|
+
return;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
24
|
+
}
|
|
25
|
+
const temporary = `${destination}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
|
26
|
+
let handle;
|
|
27
|
+
try {
|
|
28
|
+
handle = await open(temporary, 'wx', 0o600);
|
|
29
|
+
await handle.writeFile(content);
|
|
30
|
+
await handle.sync();
|
|
31
|
+
await handle.close(); handle = null;
|
|
32
|
+
// Atomic no-replace installation: hard-link fails if another writer won.
|
|
33
|
+
// Rename would overwrite a concurrently created or symlinked digest path.
|
|
34
|
+
try { await link(temporary, destination); }
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
37
|
+
const entry = await lstat(destination);
|
|
38
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw Error(`non-regular store entry: ${destination}`);
|
|
39
|
+
const current = await readFile(destination);
|
|
40
|
+
if (!current.equals(content)) throw Error(`digest collision or corrupted local object: ${destination}`);
|
|
41
|
+
}
|
|
42
|
+
} finally {
|
|
43
|
+
if (handle) await handle.close();
|
|
44
|
+
await rm(temporary, { force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|