agent-receipt-verify 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -4
- package/bench/eip3009-beleid.mjs +113 -0
- package/bench/robustness.mjs +264 -0
- package/bin/cli.mjs +12 -0
- package/package.json +6 -3
- package/src/eip3009-autorisatie.mjs +41 -8
- package/src/settlement-link-vectors.mjs +128 -0
- package/tools/bouw-afrekening-vectoren.mjs +273 -0
- package/tools/pack-inhoud.mjs +96 -0
- package/vectors/settlement-link/find-candidates-onchain.json +44 -0
- package/vectors/settlement-link/no-candidates-not-measured.json +41 -0
- package/vectors/settlement-link/opens-live-2.json +48 -0
- package/vectors/settlement-link/opens-live-3.json +48 -0
- package/vectors/settlement-link/opens-live.json +48 -0
- package/vectors/settlement-link/two-identical-payments.json +86 -0
- package/vectors/settlement-link/wrong-nonce-live-2.json +46 -0
- package/vectors/settlement-link/wrong-nonce-live-3.json +46 -0
- package/vectors/settlement-link/wrong-nonce-live.json +46 -0
- package/vectors/settlement-link/wrong-salt-live-2.json +46 -0
- package/vectors/settlement-link/wrong-salt-live-3.json +46 -0
- package/vectors/settlement-link/wrong-salt-live.json +46 -0
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Independent verification of payment- and outcome-proof receipts used by AI agents.
|
|
4
4
|
|
|
5
|
-
An agent economy has two kinds of receipt. One says
|
|
6
|
-
(delegation: UCAN, Authproof, Agent Passport). The other says
|
|
7
|
-
and whether the output matches
|
|
5
|
+
An agent economy has two kinds of receipt. One says who was allowed to act
|
|
6
|
+
(delegation: UCAN, Authproof, Agent Passport). The other says what was paid for
|
|
7
|
+
and whether the output matches (settlement and outcome: x402 offer-receipt, INAM
|
|
8
8
|
countersigned receipts, and the proofs ERC-8004 consumers count).
|
|
9
9
|
|
|
10
10
|
`receipt-commons/receipt-interop` covers the first kind. This covers the second.
|
|
@@ -90,6 +90,23 @@ settlements, starting from the receipt, the salt and the asset as §5.6.4
|
|
|
90
90
|
requires. Their result and ours are in
|
|
91
91
|
[x402#3827](https://github.com/x402-foundation/x402/pull/3827#issuecomment-6230063003).
|
|
92
92
|
|
|
93
|
+
## Recompute it yourself, without an account
|
|
94
|
+
|
|
95
|
+
Two of the cross-checks run offline from a single command, so nothing here has to be
|
|
96
|
+
taken on trust:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
npx -y agent-receipt-verify kanon
|
|
100
|
+
npx -y agent-receipt-verify beleid
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The first checks our EIP-3009 verifier against the ten published vectors of
|
|
104
|
+
iamonuwa/kanon. The second exists because agreement can hide a question: all ten of
|
|
105
|
+
those vectors cover underpayment only, so they say nothing about overpayment, or
|
|
106
|
+
about one nonce reused across two payers. Those are the two open questions in
|
|
107
|
+
[x402#2725](https://github.com/x402-foundation/x402/issues/2725), and the command
|
|
108
|
+
prints exactly where the two possible answers diverge and where they do not.
|
|
109
|
+
|
|
93
110
|
## Linking a receipt to its settlement
|
|
94
111
|
|
|
95
112
|
A receipt with an empty `transaction` field can be matched to a payment only by
|
|
@@ -170,7 +187,7 @@ not show that anyone else is using it.
|
|
|
170
187
|
|
|
171
188
|
## Honest limits
|
|
172
189
|
|
|
173
|
-
The format checks test the
|
|
190
|
+
The format checks test the form of a receipt: signatures, content addressing,
|
|
174
191
|
and whether the hashes match the bytes a counterparty observed. They do not tell
|
|
175
192
|
you whether an operator is honest, or whether a registry holds a receipt as
|
|
176
193
|
finalized.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Een matrix over de twee open vragen uit x402#2725, zodat een antwoord daar een
|
|
2
|
+
// meting is en geen mening.
|
|
3
|
+
//
|
|
4
|
+
// DE TWEE VRAGEN, letterlijk van de steller van dat issue: moet het betaalde bedrag
|
|
5
|
+
// precies kloppen of mag er te veel betaald worden, en moet hergebruik van een nonce
|
|
6
|
+
// per betaler worden bijgehouden. Geen van beide wordt gedekt door een gepubliceerde
|
|
7
|
+
// vector, en dus heeft onze eigen "eens op tien van tien" er niets over gezegd. Dat is
|
|
8
|
+
// precies waarom dit bestand bestaat: een cijfer dat klopt kan een vraag verbergen.
|
|
9
|
+
//
|
|
10
|
+
// WAT HIER GEMETEN WORDT. Dezelfde tien vectoren onder beide bedragbeleiden, plus twee
|
|
11
|
+
// gevallen die zij niet dekken en die het verschil zichtbaar maken. De uitslag is een
|
|
12
|
+
// tabel waarin te zien is waar de twee keuzes uit elkaar lopen en waar niet.
|
|
13
|
+
//
|
|
14
|
+
// DE UITVOER IS ENGELS, de commentaren zijn dat niet. Sinds 0.2.2 is dit een eigen
|
|
15
|
+
// commando (`agent-receipt-verify beleid`), dus de regels die iemand op zijn scherm
|
|
16
|
+
// krijgt zijn voor een vreemde bedoeld. Een meting die niemand kan lezen is geen
|
|
17
|
+
// meting die iemand overneemt.
|
|
18
|
+
//
|
|
19
|
+
// Gebruik: node bench/eip3009-beleid.mjs of npx agent-receipt-verify beleid
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs'
|
|
22
|
+
import path from 'node:path'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
import { beoordeelVector, beoordeelAutorisatie } from '../src/eip3009-autorisatie.mjs'
|
|
25
|
+
|
|
26
|
+
const wortel = fileURLToPath(new URL('..', import.meta.url))
|
|
27
|
+
const MAP = path.join(wortel, 'vectors', 'kanon')
|
|
28
|
+
const lees = (n) => JSON.parse(fs.readFileSync(path.join(MAP, n), 'utf8'))
|
|
29
|
+
|
|
30
|
+
const vectoren = fs.readdirSync(MAP).filter((f) => f.endsWith('.json')).sort()
|
|
31
|
+
|
|
32
|
+
console.log('EIP-3009: the two open questions from x402 issue 2725, measured')
|
|
33
|
+
console.log('')
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
console.log('1. The same ten vectors under both amount policies')
|
|
37
|
+
console.log('')
|
|
38
|
+
const uitkomst = {}
|
|
39
|
+
for (const beleid of ['exact', 'at-least']) {
|
|
40
|
+
let eens = 0
|
|
41
|
+
const afwijkend = []
|
|
42
|
+
for (const f of vectoren) {
|
|
43
|
+
const v = lees(f)
|
|
44
|
+
const u = await beoordeelAutorisatie(v.input, { ...(v.context ?? {}), doelNetwerk: v.network, amountPolicy: beleid })
|
|
45
|
+
if (u.valid === v.expected.valid && u.reason_code === v.expected.reason_code) eens++
|
|
46
|
+
else afwijkend.push(v.id + ' (' + u.reason_code + ' against ' + v.expected.reason_code + ')')
|
|
47
|
+
}
|
|
48
|
+
uitkomst[beleid] = { eens, afwijkend }
|
|
49
|
+
console.log(' ' + beleid.padEnd(9) + ' agrees on ' + eens + ' of ' + vectoren.length +
|
|
50
|
+
(afwijkend.length ? ' differs: ' + afwijkend.join(', ') : ''))
|
|
51
|
+
}
|
|
52
|
+
console.log('')
|
|
53
|
+
console.log(' Both policies reach the same verdict on all ten. That is the point: the')
|
|
54
|
+
console.log(' published vectors only cover UNDERpayment, so they cannot answer the')
|
|
55
|
+
console.log(' question. Anyone who stops here thinks they know.')
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
console.log('')
|
|
59
|
+
console.log('2. The case no vector covers: paying too much')
|
|
60
|
+
console.log('')
|
|
61
|
+
{
|
|
62
|
+
const basis = lees('valid-baseline-001.json')
|
|
63
|
+
for (const beleid of ['exact', 'at-least']) {
|
|
64
|
+
const v = JSON.parse(JSON.stringify(basis))
|
|
65
|
+
v.input.accepted.amount = String(BigInt(v.input.payload.authorization.value) - 1n)
|
|
66
|
+
const u = await beoordeelAutorisatie(v.input, { ...(v.context ?? {}), doelNetwerk: v.network, amountPolicy: beleid })
|
|
67
|
+
console.log(' ' + beleid.padEnd(9) + ' signed ' + v.input.payload.authorization.value +
|
|
68
|
+
', asked ' + v.input.accepted.amount + ' -> ' + (u.valid ? 'VALID' : u.reason_code))
|
|
69
|
+
}
|
|
70
|
+
console.log('')
|
|
71
|
+
console.log(' Two reference implementations reject this:')
|
|
72
|
+
console.log(' go/mechanisms/evm/exact/facilitator/eip3009.go:65 Cmp(requiredValue) != 0')
|
|
73
|
+
console.log(' python/x402/mechanisms/evm/exact/facilitator.py:235 value != amount')
|
|
74
|
+
console.log(' and that same python says on line 168: "Amount exactly matches requirements.amount"')
|
|
75
|
+
console.log(' So exact is our default. It stays a choice, which is why it is printed in')
|
|
76
|
+
console.log(' the result and not buried in the code.')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
console.log('')
|
|
81
|
+
console.log('3. The second case no vector covers: the same nonce, a DIFFERENT payer')
|
|
82
|
+
console.log('')
|
|
83
|
+
{
|
|
84
|
+
const basis = lees('valid-baseline-001.json')
|
|
85
|
+
const nonce = basis.input.payload.authorization.nonce
|
|
86
|
+
const andereBetaler = '0x000000000000000000000000000000000000dEaD'
|
|
87
|
+
|
|
88
|
+
// Per betaler, zoals het token-contract het doet.
|
|
89
|
+
const perBetaler = await beoordeelVector({
|
|
90
|
+
...basis,
|
|
91
|
+
context: { ...basis.context, seen_nonces: { [andereBetaler]: [nonce] } },
|
|
92
|
+
})
|
|
93
|
+
// De grove vorm: een lijst zonder onderscheid naar betaler.
|
|
94
|
+
const plat = await beoordeelVector({
|
|
95
|
+
...basis,
|
|
96
|
+
context: { ...basis.context, seen_nonces: [nonce] },
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
console.log(' nonce already spent by ANOTHER address')
|
|
100
|
+
console.log(' per payer (how the contract does it) -> ' + (perBetaler.valid ? 'VALID' : perBetaler.reason_code))
|
|
101
|
+
console.log(' the same nonce spent by THIS payer')
|
|
102
|
+
console.log(' flat list -> ' + (plat.valid ? 'VALID' : plat.reason_code))
|
|
103
|
+
console.log('')
|
|
104
|
+
console.log(' EIP-3009 tracks this per address itself: mapping(address => mapping(bytes32')
|
|
105
|
+
console.log(' => bool)), readable as authorizationState(address authorizer, bytes32 nonce),')
|
|
106
|
+
console.log(' and the spec says nonces are random 32 bytes, unique to the authorizer. A')
|
|
107
|
+
console.log(' verifier holding a list without the payer is therefore STRICTER than the')
|
|
108
|
+
console.log(' chain, and could refuse a valid payment from someone else.')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('')
|
|
112
|
+
console.log('Short version: on the ten published vectors neither choice makes a difference.')
|
|
113
|
+
console.log('On the two cases nobody published, it decides everything.')
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// BENCHMARK: kan onze verifieerder voor de gek gehouden worden, en kunnen wij zelf
|
|
2
|
+
// een geldig tegengetekend bonnetje maken?
|
|
3
|
+
//
|
|
4
|
+
// WAAROM DIT APART VAN DE TESTS STAAT. De tests tonen aan dat wij op de vier
|
|
5
|
+
// gepubliceerde vectoren hetzelfde oordeel geven als hun eigen verifieerder. Dat is
|
|
6
|
+
// correctheid, en correctheid is niet hetzelfde als POSITIE. Een controle die alles
|
|
7
|
+
// goedkeurt geeft op vier vectoren precies dezelfde uitslag als een goede controle,
|
|
8
|
+
// zolang drie van die vier negatief zijn en hij per ongeluk de juiste reden noemt.
|
|
9
|
+
//
|
|
10
|
+
// Deze benchmark meet daarom twee dingen die de vectoren niet kunnen meten:
|
|
11
|
+
//
|
|
12
|
+
// 1. PRECISIE ONDER AANVAL. Neem het POSITIEVE geval en verander er systematisch
|
|
13
|
+
// een ding in. Elke mutatie MOET afgekeurd worden. Een mutatie die we
|
|
14
|
+
// goedkeuren is geen cosmetisch gebrek maar precies het gat waardoor een
|
|
15
|
+
// vervalst bonnetje erdoor komt, en dat is het enige dat deze verifieerder te
|
|
16
|
+
// verkopen heeft.
|
|
17
|
+
//
|
|
18
|
+
// 2. KUNNEN WIJ TEGENPARTIJ ZIJN. De onderhouder van INAM vroeg of wij de
|
|
19
|
+
// tegenpartij willen zijn op een echt bonnetje. Dat beloven zonder te weten of
|
|
20
|
+
// wij er zelf een kunnen maken en ondertekenen, is een belofte op krediet. Dit
|
|
21
|
+
// deel bouwt er een, met eigen sleutels, en laat onze eigen verifieerder erover
|
|
22
|
+
// oordelen. Lukt dat niet, dan is het antwoord nee en weten we dat nu.
|
|
23
|
+
//
|
|
24
|
+
// Geen netwerk, geen sleutel van de koning, alleen node-ingebouwde crypto.
|
|
25
|
+
|
|
26
|
+
import { createHash, generateKeyPairSync, sign as edSign } from 'node:crypto'
|
|
27
|
+
import { readFileSync } from 'node:fs'
|
|
28
|
+
import { fileURLToPath } from 'node:url'
|
|
29
|
+
import { dirname, join } from 'node:path'
|
|
30
|
+
import { canoniek, toetsGeval } from '../src/inam-countersigned.mjs'
|
|
31
|
+
|
|
32
|
+
const HIER = dirname(fileURLToPath(import.meta.url))
|
|
33
|
+
const suite = JSON.parse(readFileSync(join(HIER, '..', 'vectors', 'inam', 'vectors.json'), 'utf8'))
|
|
34
|
+
const positief = suite.cases.find((c) => c.id === 'POSITIVE')
|
|
35
|
+
if (!positief) {
|
|
36
|
+
console.error('geen POSITIVE-vector gevonden, deze benchmark heeft er een nodig')
|
|
37
|
+
process.exit(2)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sha256hex = (s) => createHash('sha256').update(s, 'utf8').digest('hex')
|
|
41
|
+
|
|
42
|
+
// ── deel 1: precisie onder aanval ────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/** Maak een kopie van het geval met een mutatie in het ondertekende bestand. */
|
|
45
|
+
function muteer(naam, fn) {
|
|
46
|
+
const f = JSON.parse(positief.feedbackFile)
|
|
47
|
+
fn(f)
|
|
48
|
+
return { naam, geval: { ...positief, feedbackFile: JSON.stringify(f) } }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const eersteTeken = (s) => (s[0] === 'a' ? 'b' : 'a') + s.slice(1)
|
|
52
|
+
|
|
53
|
+
const mutaties = [
|
|
54
|
+
muteer('handtekening van de aanvrager omgedraaid', (f) => {
|
|
55
|
+
f.inam.receipt.signatures.agentA = eersteTeken(f.inam.receipt.signatures.agentA)
|
|
56
|
+
}),
|
|
57
|
+
muteer('handtekening van de uitvoerder omgedraaid', (f) => {
|
|
58
|
+
f.inam.receipt.signatures.agentB = eersteTeken(f.inam.receipt.signatures.agentB)
|
|
59
|
+
}),
|
|
60
|
+
muteer('beide handtekeningen weg', (f) => {
|
|
61
|
+
f.inam.receipt.signatures = {}
|
|
62
|
+
}),
|
|
63
|
+
muteer('de uitvoer-hash veranderd', (f) => {
|
|
64
|
+
f.inam.receipt.result.outputHash = 'sha256:' + '0'.repeat(64)
|
|
65
|
+
}),
|
|
66
|
+
muteer('de opdracht-hash veranderd', (f) => {
|
|
67
|
+
f.inam.receipt.task.specHash = 'sha256:' + '0'.repeat(64)
|
|
68
|
+
}),
|
|
69
|
+
muteer('het bonnetje-kenmerk veranderd', (f) => {
|
|
70
|
+
f.inam.receipt.receiptId = 'sha256:' + '1'.repeat(64)
|
|
71
|
+
}),
|
|
72
|
+
muteer('de opdracht-naam veranderd', (f) => {
|
|
73
|
+
f.inam.receipt.jobId = 'http:iets-anders'
|
|
74
|
+
}),
|
|
75
|
+
muteer('de aanvrager vervangen door een ander adres', (f) => {
|
|
76
|
+
f.clientAddress = 'eip155:8453:0x9999999999999999999999999999999999999999'
|
|
77
|
+
}),
|
|
78
|
+
muteer('de waarde opgeschroefd', (f) => {
|
|
79
|
+
f.value = 999
|
|
80
|
+
}),
|
|
81
|
+
muteer('de uitvoerder vervangen door de aanvrager', (f) => {
|
|
82
|
+
f.inam.receipt.agentB = { ...f.inam.receipt.agentA, role: 'worker' }
|
|
83
|
+
}),
|
|
84
|
+
muteer('een veld toegevoegd aan de ondertekende inhoud', (f) => {
|
|
85
|
+
f.inam.receipt.task.extra = 'ik was hier'
|
|
86
|
+
}),
|
|
87
|
+
muteer('de rol van de aanvrager omgedraaid', (f) => {
|
|
88
|
+
f.inam.receipt.agentA.role = 'worker'
|
|
89
|
+
}),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
console.log('DEEL 1: precisie onder aanval')
|
|
93
|
+
console.log(' Elke mutatie van ondertekende inhoud MOET afgekeurd worden.')
|
|
94
|
+
console.log('')
|
|
95
|
+
|
|
96
|
+
// Eerst de controle dat het onveranderde geval wel wordt geaccepteerd. Zonder die
|
|
97
|
+
// regel meet deel 1 niets: een verifieerder die alles afkeurt haalt hier anders
|
|
98
|
+
// een perfecte score.
|
|
99
|
+
const basis = toetsGeval(positief, suite)
|
|
100
|
+
console.log(' basislijn, onveranderd: ' + (basis.accepted ? 'GEACCEPTEERD (goed)' : 'AFGEKEURD op ' + basis.failedChecks.join(',') + ' (FOUT, dan meet de rest niets)'))
|
|
101
|
+
|
|
102
|
+
let afgekeurd = 0
|
|
103
|
+
let doorgelaten = 0
|
|
104
|
+
let alleenBytes = 0
|
|
105
|
+
|
|
106
|
+
// De byte-hash (feedbackHashMatchesFile) faalt bij ELKE mutatie, want het bestand is
|
|
107
|
+
// opnieuw geserialiseerd. Dat maakt 12 van 12 een misleidend cijfer: het zegt niet of
|
|
108
|
+
// de INHOUDELIJKE controles de mutatie pakken. Wie dat onderscheid niet maakt, meet
|
|
109
|
+
// zijn eigen serialisatie in plaats van zijn verifieerder. Daarom wordt die ene check
|
|
110
|
+
// hier apart gezet en geteld hoeveel mutaties ALLEEN daardoor sneuvelen.
|
|
111
|
+
const BYTE_CHECK = 'feedbackHashMatchesFile'
|
|
112
|
+
|
|
113
|
+
for (const m of mutaties) {
|
|
114
|
+
let uit
|
|
115
|
+
try {
|
|
116
|
+
uit = toetsGeval(m.geval, suite)
|
|
117
|
+
} catch (e) {
|
|
118
|
+
uit = { accepted: false, failedChecks: ['wierp een fout: ' + String(e.message).slice(0, 40)] }
|
|
119
|
+
}
|
|
120
|
+
const inhoudelijk = uit.failedChecks.filter((c) => c !== BYTE_CHECK)
|
|
121
|
+
if (uit.accepted) {
|
|
122
|
+
doorgelaten++
|
|
123
|
+
console.log(' DOORGELATEN ' + m.naam + ' <- dit is een gat')
|
|
124
|
+
} else if (inhoudelijk.length === 0) {
|
|
125
|
+
alleenBytes++
|
|
126
|
+
console.log(' alleen bytes ' + m.naam + ' <- geen inhoudelijke controle pakt dit')
|
|
127
|
+
} else {
|
|
128
|
+
afgekeurd++
|
|
129
|
+
console.log(' inhoudelijk ' + m.naam + ' (' + inhoudelijk.slice(0, 3).join(', ') + ')')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
console.log('')
|
|
133
|
+
console.log(' ' + afgekeurd + ' van ' + mutaties.length + ' mutaties door een INHOUDELIJKE controle gepakt')
|
|
134
|
+
console.log(' ' + alleenBytes + ' alleen door de byte-hash, ' + doorgelaten + ' helemaal doorgelaten')
|
|
135
|
+
if (alleenBytes > 0) {
|
|
136
|
+
console.log('')
|
|
137
|
+
console.log(' Let op wat dat betekent. De byte-hash vergelijkt het bestand met een hash in een')
|
|
138
|
+
console.log(' on-chain gebeurtenis. Waar die gebeurtenis er niet is, of waar dezelfde partij')
|
|
139
|
+
console.log(' beide kan zetten, is die controle geen bescherming. Een mutatie die alleen daar')
|
|
140
|
+
console.log(' op sneuvelt, komt in zo een geval dus wel door.')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── deel 2: kunnen wij tegenpartij zijn ──────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
console.log('')
|
|
146
|
+
console.log('DEEL 2: kunnen wij zelf een tegengetekend bonnetje maken')
|
|
147
|
+
|
|
148
|
+
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
149
|
+
function base58encode(buf) {
|
|
150
|
+
let getal = 0n
|
|
151
|
+
for (const b of buf) getal = getal * 256n + BigInt(b)
|
|
152
|
+
let uit = ''
|
|
153
|
+
while (getal > 0n) {
|
|
154
|
+
uit = B58[Number(getal % 58n)] + uit
|
|
155
|
+
getal /= 58n
|
|
156
|
+
}
|
|
157
|
+
for (const b of buf) {
|
|
158
|
+
if (b === 0) uit = '1' + uit
|
|
159
|
+
else break
|
|
160
|
+
}
|
|
161
|
+
return uit
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** did:key voor een ed25519 publieke sleutel, multicodec 0xed01. */
|
|
165
|
+
function didKey(publiek) {
|
|
166
|
+
const raw = publiek.export({ format: 'der', type: 'spki' }).subarray(-32)
|
|
167
|
+
return 'did:key:z' + base58encode(Buffer.concat([Buffer.from([0xed, 0x01]), raw]))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const paarA = generateKeyPairSync('ed25519')
|
|
171
|
+
const paarB = generateKeyPairSync('ed25519')
|
|
172
|
+
const didA = didKey(paarA.publicKey)
|
|
173
|
+
const didB = didKey(paarB.publicKey)
|
|
174
|
+
console.log(' twee verse sleutels gemaakt, geen sleutel van de koning gebruikt')
|
|
175
|
+
|
|
176
|
+
// De bytes die een tegenpartij in het echt zou hebben gezien. Hier verzonnen, maar
|
|
177
|
+
// de hashes eroverheen worden echt berekend, want dat is de hele constructie.
|
|
178
|
+
const verzoek = { method: 'POST', url: 'https://api.tokenizen.nl/api/paid/verify-ownership', body: '{"endpoint":"https://example.org/sse"}' }
|
|
179
|
+
const antwoord = { body: '{"ok":true,"method":"dns-txt"}' }
|
|
180
|
+
const u = new URL(verzoek.url)
|
|
181
|
+
const specHash = 'sha256:' + sha256hex(verzoek.method + ' ' + u.pathname + u.search + '\n' + verzoek.body)
|
|
182
|
+
const outputHash = 'sha256:' + sha256hex(antwoord.body)
|
|
183
|
+
|
|
184
|
+
const kern = {
|
|
185
|
+
agentA: { id: didA, role: 'requester' },
|
|
186
|
+
agentB: { id: didB, role: 'worker' },
|
|
187
|
+
jobId: 'http:tokenizen-conformance-check',
|
|
188
|
+
receiptVersion: '1.0',
|
|
189
|
+
task: { capability: 'endpoint.ownership-verification', createdAt: '2026-10-17T10:00:00.000Z', specHash },
|
|
190
|
+
result: { completedAt: '2026-10-17T10:00:01.000Z', outputHash },
|
|
191
|
+
settlement: { rail: 'x402', network: 'eip155:84532', amount: '1000' },
|
|
192
|
+
verification: { method: 'dns-txt' },
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const zeven = {}
|
|
196
|
+
for (const k of ['jobId', 'agentA', 'agentB', 'task', 'result', 'settlement', 'verification']) zeven[k] = kern[k]
|
|
197
|
+
const receiptId = 'sha256:' + sha256hex(canoniek(zeven))
|
|
198
|
+
|
|
199
|
+
const teTekenen = { ...kern, receiptId }
|
|
200
|
+
const inhoud = canoniek(teTekenen)
|
|
201
|
+
const sigA = edSign(null, Buffer.from(inhoud, 'utf8'), paarA.privateKey).toString('base64')
|
|
202
|
+
const sigB = edSign(null, Buffer.from(inhoud, 'utf8'), paarB.privateKey).toString('base64')
|
|
203
|
+
|
|
204
|
+
const bestand = {
|
|
205
|
+
agentId: 'did:tokenizen:conformance',
|
|
206
|
+
agentRegistry: 'eip155:84532',
|
|
207
|
+
clientAddress: 'eip155:84532:0x3333333333333333333333333333333333333333',
|
|
208
|
+
createdAt: '2026-10-17T10:00:02.000Z',
|
|
209
|
+
value: 100,
|
|
210
|
+
valueDecimals: 0,
|
|
211
|
+
tag1: 'conformance',
|
|
212
|
+
tag2: 'x402',
|
|
213
|
+
inam: { receipt: { ...teTekenen, signatures: { agentA: sigA, agentB: sigB } } },
|
|
214
|
+
}
|
|
215
|
+
const bestandBytes = JSON.stringify(bestand)
|
|
216
|
+
|
|
217
|
+
const eigenSuite = {
|
|
218
|
+
suite: 'eigen-rondgang',
|
|
219
|
+
parties: {
|
|
220
|
+
requester: { did: didA, erc8004Address: '0x3333333333333333333333333333333333333333' },
|
|
221
|
+
provider: { did: didB, erc8004Address: '0x4444444444444444444444444444444444444444' },
|
|
222
|
+
},
|
|
223
|
+
observed: { request: verzoek, response: antwoord },
|
|
224
|
+
}
|
|
225
|
+
const eigenGeval = {
|
|
226
|
+
id: 'EIGEN-RONDGANG',
|
|
227
|
+
feedbackFile: bestandBytes,
|
|
228
|
+
onchainEvent: {
|
|
229
|
+
clientAddress: '0x3333333333333333333333333333333333333333',
|
|
230
|
+
value: 100,
|
|
231
|
+
feedbackHash: null,
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const oordeel = toetsGeval(eigenGeval, eigenSuite)
|
|
236
|
+
const alleenKeccak = oordeel.failedChecks.length === 1 && oordeel.failedChecks[0] === 'feedbackHashMatchesFile'
|
|
237
|
+
console.log(' ons eigen bonnetje door onze eigen verifieerder: ' + (oordeel.accepted ? 'GEACCEPTEERD' : 'afgekeurd op ' + oordeel.failedChecks.join(', ')))
|
|
238
|
+
if (!oordeel.accepted && alleenKeccak) {
|
|
239
|
+
console.log(' (alleen de keccak-controle faalt, en die vergelijkt met een on-chain gebeurtenis die hier')
|
|
240
|
+
console.log(' niet bestaat omdat er niets is uitgezonden. De acht controles die over het BONNETJE gaan,')
|
|
241
|
+
console.log(' inclusief beide handtekeningen en het herrekende kenmerk, slagen.)')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const kanTegenpartijZijn = oordeel.accepted || alleenKeccak
|
|
245
|
+
console.log(' conclusie: wij ' + (kanTegenpartijZijn ? 'KUNNEN' : 'kunnen NIET') + ' tegenpartij zijn op een echt bonnetje')
|
|
246
|
+
|
|
247
|
+
// ── samenvatting ─────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
console.log('')
|
|
250
|
+
console.log('SAMENVATTING')
|
|
251
|
+
console.log(' basislijn geaccepteerd : ' + (basis.accepted ? 'ja' : 'NEE'))
|
|
252
|
+
console.log(' inhoudelijk gepakt : ' + afgekeurd + '/' + mutaties.length)
|
|
253
|
+
console.log(' alleen door de byte-hash : ' + alleenBytes)
|
|
254
|
+
console.log(' doorgelaten : ' + doorgelaten)
|
|
255
|
+
console.log(' eigen bonnetje maakbaar : ' + (kanTegenpartijZijn ? 'ja' : 'NEE'))
|
|
256
|
+
|
|
257
|
+
// alleenBytes maakt dit NIET rood: het is een eigenschap van de constructie en een
|
|
258
|
+
// bevinding om te melden, geen gebrek in onze code. Doorgelaten wel, want dat is een gat.
|
|
259
|
+
const faalt = !basis.accepted || doorgelaten > 0 || !kanTegenpartijZijn
|
|
260
|
+
if (faalt) {
|
|
261
|
+
console.log('')
|
|
262
|
+
console.log(' Dit is een bevinding, niet alleen een cijfer. Repareer het voor er een claim op gebaseerd wordt.')
|
|
263
|
+
}
|
|
264
|
+
process.exitCode = faalt ? 1 : 0
|
package/bin/cli.mjs
CHANGED
|
@@ -52,6 +52,16 @@ function gebruik() {
|
|
|
52
52
|
console.log(' npx github:holistis/agent-receipt-verify kanon')
|
|
53
53
|
console.log(' cross-check our independent EIP-3009 verifier against the ten published')
|
|
54
54
|
console.log(' vectors of iamonuwa/kanon, offline, no account and no key.')
|
|
55
|
+
console.log('')
|
|
56
|
+
console.log(' npx github:holistis/agent-receipt-verify beleid')
|
|
57
|
+
console.log(' the same ten vectors under both amount policies, plus the two cases no')
|
|
58
|
+
console.log(' published vector covers: overpayment, and one nonce across two payers.')
|
|
59
|
+
console.log(' Those two are the open questions in x402 issue 2725.')
|
|
60
|
+
console.log('')
|
|
61
|
+
console.log(' npx github:holistis/agent-receipt-verify link-vectors')
|
|
62
|
+
console.log(' twelve vectors for the half AFTER paying: does this receipt belong to')
|
|
63
|
+
console.log(' that one settlement? Eleven run offline, the chain-reading one is')
|
|
64
|
+
console.log(' marked and skipped rather than failed.')
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
// Het argument bepaalt welke module draait. De modules zijn ook los te draaien en te
|
|
@@ -64,6 +74,8 @@ const onderdelen = {
|
|
|
64
74
|
inam: '../src/inam-countersigned.mjs',
|
|
65
75
|
liveness: '../src/x402-index-liveness.mjs',
|
|
66
76
|
kanon: '../src/eip3009-autorisatie.mjs',
|
|
77
|
+
beleid: '../bench/eip3009-beleid.mjs',
|
|
78
|
+
'link-vectors': '../src/settlement-link-vectors.mjs',
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
if (!eerste || eerste === '--help' || eerste === '-h' || eerste === 'help') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-receipt-verify",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Checks x402 endpoint conformance before an agent pays, and links the receipt to its settlement.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"node": ">=20"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node test/interop.mjs && node test/canonical.mjs && node test/link-onchain.mjs && node test/profiel.mjs && node test/eip3009.mjs && node test/conformance.mjs && node test/jws-offer.mjs && node test/census.mjs && node test/poortwachters.mjs && node test/metingen-bundel.mjs && node test/mcp-server.mjs && node test/cli.mjs && node test/bundel.mjs",
|
|
11
|
+
"test": "node test/interop.mjs && node test/canonical.mjs && node test/link-onchain.mjs && node test/profiel.mjs && node test/eip3009.mjs && node test/conformance.mjs && node test/jws-offer.mjs && node test/census.mjs && node test/poortwachters.mjs && node test/metingen-bundel.mjs && node test/mcp-server.mjs && node test/cli.mjs && node test/bundel.mjs && node test/settlement-link-vectors.mjs",
|
|
12
12
|
"link-example": "node src/x402-link-onchain.mjs --receipt=examples/receipt-live-base-sepolia.json && node src/x402-link-onchain.mjs --receipt=examples/receipt-live-base-sepolia-2.json && node src/x402-link-onchain.mjs --receipt=examples/receipt-live-base-sepolia-3.json",
|
|
13
13
|
"conformance": "node src/x402-conformance-check.mjs",
|
|
14
14
|
"bench": "node bench/robustness.mjs",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"metingen-bundel": "node src/x402-metingen-bundel.mjs",
|
|
19
19
|
"mcp": "node src/mcp-server.mjs",
|
|
20
20
|
"bundel": "node tools/bundel-bouw.mjs build/bundle",
|
|
21
|
-
"kanon": "node src/eip3009-autorisatie.mjs --vectoren"
|
|
21
|
+
"kanon": "node src/eip3009-autorisatie.mjs --vectoren",
|
|
22
|
+
"bench-eip3009": "node bench/eip3009-beleid.mjs",
|
|
23
|
+
"link-vectors": "node src/settlement-link-vectors.mjs"
|
|
22
24
|
},
|
|
23
25
|
"optionalDependencies": {
|
|
24
26
|
"ethers": "^6.17.0"
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
"NOTICE",
|
|
36
38
|
"CONFORMANCE.md",
|
|
37
39
|
"README.md",
|
|
40
|
+
"bench/",
|
|
38
41
|
"tools/"
|
|
39
42
|
],
|
|
40
43
|
"author": "Abdellah Ouadoudi (https://github.com/holistis)",
|
|
@@ -141,19 +141,52 @@ export async function beoordeelAutorisatie(input, context = {}) {
|
|
|
141
141
|
return { valid: false, reason_code: 'SIGNER_MISMATCH', hersteld }
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
// 5. Het bedrag, pas NA de handtekening: een authentiek ondertekende
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// 5. Het bedrag, pas NA de handtekening: een authentiek ondertekende afwijking.
|
|
145
|
+
//
|
|
146
|
+
// DE STANDAARD NEEMT HIER GEEN POSITIE IN, DUS WIJ MAKEN DE KEUZE ZICHTBAAR. Twee
|
|
147
|
+
// referentie-implementaties eisen GELIJKHEID en wijzen dus ook te veel betalen af:
|
|
148
|
+
// go/mechanisms/evm/exact/facilitator/eip3009.go regel 65 (`Cmp(requiredValue) != 0`,
|
|
149
|
+
// fout ErrAuthorizationValueMismatch) en python/x402/mechanisms/evm/exact/
|
|
150
|
+
// facilitator.py regel 235 (`!=`), met op regel 168 de eigen zin "Amount exactly
|
|
151
|
+
// matches requirements.amount". Onze eerste versie liet te veel betalen toe en zou
|
|
152
|
+
// dus op dat ene geval van hen afwijken, terwijl geen enkele gepubliceerde vector
|
|
153
|
+
// het dekt. Daarom staat 'exact' nu als standaard, is 'at-least' een bewuste keuze,
|
|
154
|
+
// en draagt de uitslag ALTIJD welke van de twee gebruikt is.
|
|
155
|
+
const beleid = context.amountPolicy === 'at-least' ? 'at-least' : 'exact'
|
|
156
|
+
const betaald = BigInt(auth.value ?? 0)
|
|
157
|
+
const gevraagd = BigInt(accepted.amount ?? 0)
|
|
158
|
+
const bedragFout = beleid === 'exact' ? betaald !== gevraagd : betaald < gevraagd
|
|
159
|
+
if (bedragFout) {
|
|
160
|
+
return { valid: false, reason_code: 'AMOUNT_INSUFFICIENT', amountPolicy: beleid }
|
|
147
161
|
}
|
|
148
162
|
|
|
149
|
-
// 6. Is deze nonce al verbruikt
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
163
|
+
// 6. Is deze nonce al verbruikt, PER BETALER en niet over iedereen heen.
|
|
164
|
+
//
|
|
165
|
+
// EIP-3009 houdt dit zelf per adres bij: `mapping(address => mapping(bytes32 =>
|
|
166
|
+
// bool))`, en de leesfunctie heet `authorizationState(address authorizer, bytes32
|
|
167
|
+
// nonce)`. De spec zegt er bovendien bij dat nonces willekeurige 32 bytes zijn,
|
|
168
|
+
// uniek per adres, en juist geen volgnummers. Een verifieerder die EEN lijst
|
|
169
|
+
// bijhoudt zonder onderscheid naar betaler is dus STRENGER dan de keten zelf: hij
|
|
170
|
+
// zou een geldige betaling van een andere partij weigeren die toevallig dezelfde
|
|
171
|
+
// 32 bytes koos. Onze eerste versie deed precies dat.
|
|
172
|
+
//
|
|
173
|
+
// Twee vormen worden aanvaard. Een platte lijst hoort bij DEZE betaler, want zo
|
|
174
|
+
// gebruiken de gepubliceerde vectoren hem. Een object met het adres als sleutel is
|
|
175
|
+
// de nauwkeurige vorm en komt overeen met wat het token-contract doet.
|
|
176
|
+
const betaler = String(auth.from ?? '').toLowerCase()
|
|
177
|
+
const bron = context.seen_nonces
|
|
178
|
+
let gezien = []
|
|
179
|
+
if (Array.isArray(bron)) gezien = bron
|
|
180
|
+
else if (bron && typeof bron === 'object') {
|
|
181
|
+
for (const [adres, lijst] of Object.entries(bron)) {
|
|
182
|
+
if (String(adres).toLowerCase() === betaler) gezien = lijst ?? []
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (gezien.map((n) => String(n).toLowerCase()).includes(String(auth.nonce).toLowerCase())) {
|
|
153
186
|
return { valid: false, reason_code: 'NONCE_REPLAY' }
|
|
154
187
|
}
|
|
155
188
|
|
|
156
|
-
return { valid: true, reason_code: 'VALID', ondertekenaar: hersteld }
|
|
189
|
+
return { valid: true, reason_code: 'VALID', ondertekenaar: hersteld, amountPolicy: beleid }
|
|
157
190
|
}
|
|
158
191
|
|
|
159
192
|
/** Een hele vector beoordelen, met zijn eigen context en doelnetwerk. */
|