Stranger pass of 21 Oct: x402 verify-payments reads all wallet transfers in windows under the endpoint range limit (was UNMEASURED on the live testnet; now VALID), device-operation reputation is byte-identical in JS --canonical and Python, module README figures as measured, .gitignore for the keys the examples write, temporary directories removed by the probes
This commit is contained in:
parent
82c5b261ac
commit
fcece14a12
26
.gitignore
vendored
26
.gitignore
vendored
@ -1,2 +1,28 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
# Private keys written by the usage examples of the component READMEs (run them in a clone and `git add -A` would commit them):
|
||||
# identity/
|
||||
issuer.keys.json
|
||||
holder.keys.json
|
||||
phone.keys.json
|
||||
session.keys.json
|
||||
# device-identity/ and device-operation/
|
||||
maker.keys.json
|
||||
owner.keys.json
|
||||
device.key.json
|
||||
d1.key.json
|
||||
d2.key.json
|
||||
# pq-pki/ (sealed CA and server keys; api.p8.pem is an UNENCRYPTED PKCS#8 file)
|
||||
root.key
|
||||
issuing.key
|
||||
api.key
|
||||
api.p8.pem
|
||||
# verify-layer/ (keygen --out keys: the operator's head key)
|
||||
head.key.pem
|
||||
# agents/ (agent-cli.mjs id --out agent/, the approvers' human.key.pem, the hosted witness's testnet.key)
|
||||
agent.key.pem
|
||||
human.key.pem
|
||||
testnet.key
|
||||
# the default data directories of the KMS and of the secrets service (sealed key files, the audit log)
|
||||
/pq-kms/data/
|
||||
/pq-secrets/data/
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Citations in this repository that you cannot open
|
||||
|
||||
This file is generated by the publication stager of this repository (scripts/legaturi-declaratie.cjs in the Aere working
|
||||
tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-20, of every path cited in this
|
||||
tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-21, of every path cited in this
|
||||
repository that does not resolve to a published file.
|
||||
|
||||
A citation is a promise that a claim is checkable. Where the target is not published, or names a file the reader creates
|
||||
|
||||
10
README.md
10
README.md
@ -25,7 +25,7 @@ notarization command of the verification layer and for the agents' x402 wallet (
|
||||
| [`conformance/`](conformance/) | a conformance suite for the AIP-23 proof kinds: one verdict form over the verifiers published here (four levels: integrity, authenticity, anchoring, finality; each passed, failed or not measured, with the named rule), 260 rules of seventeen verifiers read from their code (the presentation verifier of identity/ and the device verifier of device-identity/ among them since 2026-10-05; the SD-JWT, SD-JWT VC, Travel Rule and compliance-policy verifiers since 2026-10-18, so nothing of identity/ is left out; the operation-log verifier and the reputation of device-operation/ since 2026-10-20), 791 vectors derived from the producers and from evidence recorded on testnet 28001 (each invalid one is one operation away from one that is not, or one change at its producer, and must be refused for exactly its named rules), and a runner that scores any implementation given as a command, in any language; a second implementation in Python claims 8 of the 260 rules. Not a certification and not an audit |
|
||||
| [`identity/`](identity/) | post-quantum credentials: an issuer signs claims (hybrid Ed25519 + ML-DSA-65, both required), the holder shows only the claims it picks (selective disclosure in the manner of SD-JWT, RFC 9901, in a format of its own), bound to the verifier's audience and nonce; delegation to a phone, an agent or a ten-minute session key that can only narrow; revocation and an issuer status list in the manner of W3C Bitstring Status List; all checked offline from the files; and compliance without surveillance: a verifier's policy (issuers, required claims) judged on a presentation, recorded as an AIP-23 envelope that carries no personal data; and the Travel Rule between VASPs, the IVMS101 data sealed for the receiving VASP with a hybrid X25519 + ML-KEM-768 key encapsulation, signed, bound to the transfer and acknowledged; and the same credentials as standard SD-JWT (IETF RFC 9901): issued with the issuer's Ed25519 key (EdDSA) or its ML-DSA-65 key, presented with a key binding JWT, and verified against the RFC's own example vectors and, since 2026-10-18, interoperable in both directions with a foreign library (@sd-jwt/core 0.23.0 with jose 6.2.12, ML-DSA-65 included; `interop/`), plus the SD-JWT VC profile (draft-ietf-oauth-sd-jwt-vc-19, `dc+sd-jwt`, `vct`, a Token Status List judged offline) (`identity-cli.mjs verify-sdjwt`, behind Aere Cloud's POST /v1/identity/sd-jwt/verify, and `verify-sdjwt-vc`) |
|
||||
| [`device-identity/`](device-identity/) | what a machine is, as a chain of signed statements checked offline: the manufacturer's credential for the device key (the manufacturer and the owners sign hybrid, Ed25519 + ML-DSA-65, both required; the device key is ML-DSA-65), a firmware release with a floor and retractions (a rolled-back or retracted firmware is refused), ownership by transfers each signed by the owner before, the current owner's authorization, a status registry in which the manufacturer's retirement is final and an owner's lasts as long as its ownership (after a sale the records of the previous owner are named and not applied: a seller could date one before the sale after it; a retirement by an owner that survives a sale needs a witness of time that is not built), and the device's fresh attestation to the verifier's nonce, with an AIP-23 envelope that can be notarized and read back under a post-quantum certified anchor; one verifier makes every check it can reach, names every rule that refused and says VALID, PARTIAL (what it was not handed, or could not read) or INVALID. The device key is a software key: no TPM, secure element or TEE is used or checked, and the README says what that leaves open |
|
||||
| [`device-operation/`](device-operation/) | what a machine says about its own operation over time, as a log it signs with the device key that device-identity/ certifies: each record names the one before it, a key rotation is consented by the old key and certified by the same manufacturer, a retirement by the manufacturer holds from its date, and two different records of one key for one position are kept as a proof of equivocation that verifies on its own; the times are bounded by AIP-23 time anchors and by blocks the verifier reads, and without them a time is the machine's word, which the verdict says; one verifier with named rules says VALID, PARTIAL or INVALID. A reputation is computed from the verified log by a published, deterministic, integer-only function (availability per window, claimed and witnessed, gaps, equivocations, retirements, age) and returned with the record each figure comes from, in two implementations, JavaScript and Python, that give the same text. It is not a token and it is not written on any chain. The records are the word of the device's own sensors and software, the device key is a software key, and no anchor of an operation record has been notarized on a chain yet |
|
||||
| [`device-operation/`](device-operation/) | what a machine says about its own operation over time, as a log it signs with the device key that device-identity/ certifies: each record names the one before it, a key rotation is consented by the old key and certified by the same manufacturer, a retirement by the manufacturer holds from its date, and two different records of one key for one position are kept as a proof of equivocation that verifies on its own; the times are bounded by AIP-23 time anchors and by blocks the verifier reads, and without them a time is the machine's word, which the verdict says; one verifier with named rules says VALID, PARTIAL or INVALID. A reputation is computed from the verified log by a published, deterministic, integer-only function (availability per window, claimed and witnessed, gaps, equivocations, retirements, age) and returned with the record each figure comes from, in two implementations, JavaScript and Python, that print the same bytes (`operation-cli.mjs reputation --canonical` and `reputation.py`, on Windows too since 2026-10-21). It is not a token and it is not written on any chain. The records are the word of the device's own sensors and software, the device key is a software key, and no anchor of an operation record has been notarized on a chain yet |
|
||||
| [`exposure-report/`](exposure-report/) | classifies Bitcoin and Ethereum addresses by whether their public key is already readable from chain data, in three declared modes (offline, by address or script type only; through your own Electrum server, Esplora index, Bitcoin Core or Ethereum JSON-RPC, one declared target; through a third-party provider only with written consent, recorded in the report), with exactly two labels ("exposed (on-chain evidence)", "no on-chain evidence of exposure") and "unmeasured" when the reading did not happen, the evidence named for each address, and a dated report (JSON, Markdown, an AIP-23 `compliance` envelope that binds the JSON). It records what was readable on the date of the report: it does not protect, move or recover any coins, it does not change what a spend reveals, and it says nothing about the future. The one component with npm dependencies (`@noble/hashes`, `@noble/curves` 1.x: `npm install --ignore-scripts` in its directory) |
|
||||
|
||||
Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them.
|
||||
@ -42,21 +42,21 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
|
||||
| pq-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`pq-kms/test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`pq-kms/test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`pq-kms/test/control-negativ-hsm-incredere.mjs`) |
|
||||
| pq-secrets | 43/43 on 2026-10-18 (`node test/proba-secrete.mjs`: the whole flow with keys born at run time, each refusal by name, a presentation signed for one request refused on any other, grant attenuation, the same refusal for every unauthorized caller, concurrent writes, rotation, revoked versions, the value searched for as a string in the log and on disk, the log intact after 100 operations, every envelope VALID for the AIP-23 reference verifier when `AERE_VERIFY_PROOF` names it, otherwise skipped by name); an adversarial review ran more than sixty attacks before publication, and the three that still succeed are limits written in the README | 43/43 on 2026-10-18 (`node test/control-negativ-secrete.mjs`: each guard planted out in a copy, the named checks must fail; `--autoproba` 5/5) |
|
||||
| pq-pki | 28/28 on 2026-10-19 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5; since 2026-10-19 the signature algorithm a certificate or a revocation list declares must be its issuer key's | 26/26 (`node test/control-negativ.mjs`) |
|
||||
| crypto-inventory | 68/68 (`node test/proba.mjs`, measured 2026-10-19 with the Rust, Swift and Kotlin detectors, glob imports, libcrux, BoringSSL, the libsignal Java/Node/Swift/Android APIs, CryptoKit, CommonCrypto, Security.framework, Android Keystore, Tink and OkHttp included, and since 0.4.1 a TLS version given through a constant or a conditional); cost on hostile input 13/13 linear (`node test/proba-timp.mjs`) | 38/38 (`node test/control-negativ.mjs`); cost 5/5 in this repository (`node test/control-negativ-timp.mjs`; its fourth case compares with version 0.1.0 from the development history and is skipped here) |
|
||||
| crypto-inventory | 68/68 (`node test/proba.mjs`, measured 2026-10-19 with the Rust, Swift and Kotlin detectors, glob imports, libcrux, BoringSSL, the libsignal Java/Node/Swift/Android APIs, CryptoKit, CommonCrypto, Security.framework, Android Keystore, Tink and OkHttp included, and since 0.4.1 a TLS version given through a constant or a conditional); cost on hostile input 13/13 linear (`node test/proba-timp.mjs`) | 38/38 (`node test/control-negativ.mjs`); cost 4/4 here (`node test/control-negativ-timp.mjs`: P1 to P4, each quadratic form put back, and one Swift comment rescan, turns its case of the cost probe red; its first case, V0, runs the inventory of before the repair taken from the development history, which this repository does not carry: it prints `SARIT V0` (skipped, not measured) and the exit code is still 0; 5/5 in the development repository, both measured on 2026-10-21) |
|
||||
| verify-layer | 45/45 with the AIP-23 reference verifier, measured from a fresh clone on 2026-10-18 (`AERE_VERIFY_PROOF=<verify-proof.mjs> node proba-sidecar.mjs`, where verify-proof.mjs is the one served at aere.network/tools with verify-anchor.mjs, mpt.mjs, falcon512.mjs, registry-v3.mjs and package.json beside it and `npm install` done there; verify-proof.mjs alone gives 41/45, because the four finality checks need the anchor verifier); without it 40 run, 5 are reported as skipped and the exit code is 2; the cloud adapters 60/60 with the same verifier on 2026-10-18 (`node proba-adaptoare-cloud.mjs`: nine fixtures derived from the vendors' documented CLI and REST examples by `fixturi-cloud/deriva.mjs`; without the verifier 59 run and one is skipped; a real account is unmeasured and the README says so); the Merkle tree 20/20 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 37/37 from a fresh clone of this repository on 2026-10-18 (`node control-negativ-sidecar.mjs`: the ten original plantings plus the cloud adapter rules; the case that compares with the development history is skipped here, and is the 38th in the development tree); the tree 14/14 (`node control-negativ-arbore.mjs`) |
|
||||
| proof-kinds | 32/32 on 2026-10-19 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`: sixteen kinds, the allowed values of the `secret-access` fields and the head of the agents witness checkpoint included) | nine negative controls inside the test |
|
||||
| readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) |
|
||||
| control-plane | planner 44/44 (2026-10-18: a key agreement or a client-side TLS configured by the application itself is not routed to the TLS gateway; a self-signed trust anchor is blocked, not "automatic through PKI"; test code is LOW with its own scope), command line 10/10, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers (all five server profiles measured end to end on 2026-10-18: nginx, Apache, HAProxy, Node, Go), compliance report 30/30 (with the AIP-23 verifier; test-code findings are counted apart and are not "exposed today"), console 8/8; organization judgement 74/74 on 2026-10-20 (`node proba-organizatie.mjs`: artifacts made by the real producers of each kind, the context of the relying party set against that of the pack, fourteen forged packs that used to pass now refused; 75/75 with `--online` on testnet 28001, said as a testnet) and its view in a real Chromium 48/48 (`node proba-organizatie-web.mjs`); the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`); organization judgement 45/45 guards removed, each turning its named row red (`node control-negativ-organizatie.mjs`), and 20/20 defects planted in its view (`node control-negativ-organizatie-web.mjs`) |
|
||||
| identity | 52/52 (`node proba-identity.mjs`, a presentation optionally signed for one exact request (`request`) included, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 18/18 (`node proba-conformitate.mjs`) and Travel Rule 20/20 (`node proba-travel-rule.mjs`) with the same verifier (without it each reports one test unmeasured and exits 2; 2026-10-18: a missing originator id or nonce is a refusal, found when the verifier was enrolled in the conformance suite), SD-JWT 31/31 (`node proba-sdjwt.mjs`, the RFC 9901 vectors and fixtures derived from @sd-jwt/core 0.23.0 included; since 2026-10-19 the keys of an AERE identity are judged on their ML-DSA-65 half by default, the Ed25519 half only on request, and every verdict says `postQuantum`), SD-JWT VC 11/11 (`node proba-sdjwt-vc.mjs`); interoperability with @sd-jwt/core and jose in both directions 26/26 and 28/28 (`interop/`, with those libraries installed in a folder you name; otherwise reported unmeasured), measured 2026-10-18, before the 2026-10-19 change; the scripts now ask for the Ed25519 half explicitly and have not been re-run since | 57/57 (`node control-negativ-identity.mjs`); compliance 17/17 (`node control-negativ-conformitate.mjs`); Travel Rule 20/20 (`node control-negativ-travel-rule.mjs`); SD-JWT 39/39 (`node control-negativ-sdjwt.mjs`); SD-JWT VC 20/20 (`node control-negativ-sdjwt-vc.mjs`) |
|
||||
| agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 59/59 on 2026-10-19 (since then an equivocation proof must be made with the agent's ML-DSA-65 key), approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); the hosted witness 154/154 without a network, measured from a fresh clone of this repository on 2026-10-19 (four more rows, for the Cloud gateway door, run only where the gateway lives), with a local notary mock (`AERE_VERIFY_PROOF=<verify-proof.mjs> node proba-martor-agenti.mjs`; the AIP-23 rows need the verifier, without it they are skipped by name and the exit code is 2), and 75/75 on testnet 28001 earlier on 2026-10-19 with the form before that day's corrections; the current form has not been run on testnet yet (`proba-martor-agenti.mjs --testnet`, needs a funded testnet key or the faucet: a checkpoint notarized, the receipt VALID with post-quantum finality, a backdated ledger caught with it; evidence in `agents/dovezi-martor-agenti-28001/`); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`agents/x402/proba-x402-testnet.mjs`, `agents/x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 28/28 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); the hosted witness 131/131, plus three changes of order that must leave the probe green (`node control-negativ-martor-agenti.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository |
|
||||
| agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 59/59 on 2026-10-19 (since then an equivocation proof must be made with the agent's ML-DSA-65 key), approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); the hosted witness 154/154 without a network, measured from a fresh clone of this repository on 2026-10-19 (four more rows, for the Cloud gateway door, run only where the gateway lives), with a local notary mock (`AERE_VERIFY_PROOF=<verify-proof.mjs> node proba-martor-agenti.mjs`; the AIP-23 rows need the verifier, without it they are skipped by name and the exit code is 2), and 75/75 on testnet 28001 earlier on 2026-10-19 with the form before that day's corrections; the current form has not been run on testnet yet (`proba-martor-agenti.mjs --testnet`, needs a funded testnet key or the faucet: a checkpoint notarized, the receipt VALID with post-quantum finality, a backdated ledger caught with it; evidence in `agents/dovezi-martor-agenti-28001/`); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 24/24 without a network (on 2026-10-21; its `--all-transfers` reads the range in windows of at most 5,000 blocks, an unread window is `UNMEASURED`, never empty; the two recorded testnet runs checked again on 28001 that day, `node x402/verifica-plati.mjs <evidence> --rpc https://testnet-rpc.aere.network --all-transfers`: both `VALID`, about 3.2 million blocks each in 635 to 637 windows, 5.5 to 8.5 minutes in four runs; until that day both answered `UNMEASURED`, the endpoint refusing the single request); on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`agents/x402/proba-x402-testnet.mjs`, `agents/x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 28/28 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); the hosted witness 131/131, plus three changes of order that must leave the probe green (`node control-negativ-martor-agenti.mjs`); x402 36/36 on 2026-10-21 (`node x402/control-negativ-wallet.mjs`, the windows of `--all-transfers` included); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository |
|
||||
| proof-of-ai | 6/6 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-proof-of-ai.mjs`); without it the probe reports NEMASURAT and exits 2 | one negative control inside the test (an envelope touched after its hash: INVALID) |
|
||||
| payments | 23/23 without a chain (`node proba-risc.mjs`: the verdicts under a policy, the reproducible verdict hash, a receipt verified, nine forged ones refused by name, a malformed payment refused by name); 9/9 on testnet 28001 on 2026-10-02 (`AERE_TESTNET_KEY_FILE=... AERE_ETHERS_DIR=... node proba-plata-28001.mjs`: a payment allowed, paid, its receipt notarized and read back with finality PASSED, the public reader VALID; a payment above the limit refused with nothing sent; the fourth in the window refused; a forged receipt and a loosened policy INVALID) | 10/10 (`node proba-risc.mjs --control-negativ`: form, asset, counterparty, rate, per-payment limit, window, linkage, USD arithmetic, verdict reproduction, chain facts, each planted out on a copy and each forged fixture then accepted) |
|
||||
| market-settlement | 20/20 on testnet 28001 on 2026-10-02 (`AERE_TESTNET_KEY_FILE=... AERE_ETHERS_DIR=... node proba-piata-28001.mjs`: five scenarios on AereComputeMarketV3 deployed from the committed artifact, every refusal by its named custom error, the provider's notarized envelope read back under a post-quantum certified anchor); the judge on the recorded timelines 26/26 without a chain (`node proba-judecata.mjs`: seven honest readings VALID, eighteen forged or malformed ones INVALID for their named reason, the settlement receipt) | 10/10 (`node proba-judecata.mjs --control-negativ`: form, completeness of the timeline, posting, deadline, window, arbiter, payout, result bytes, replay, proof, each planted out on a copy and each forged fixture then accepted) |
|
||||
| market-settlement | 20/20 on testnet 28001 on 2026-10-02 (`AERE_TESTNET_KEY_FILE=... AERE_ETHERS_DIR=... node proba-piata-28001.mjs`: five scenarios on AereComputeMarketV3 deployed from the committed artifact, every refusal by its named custom error, the provider's notarized envelope read back under a post-quantum certified anchor); the judge on the recorded timelines 26/26 without a chain (`node proba-judecata.mjs`: seven honest readings VALID, eighteen forged or malformed ones INVALID for their named reason, the settlement receipt); the senders of the recorded timeline read again from the raw transactions on 2026-10-21, 21 events of 19 transactions each equal to the recorded one (`AERE_ETHERS_DIR=... node proba-cronologie-citire.mjs`, reads only; its control row, a real 0x50 transaction read raw, found none in the last 400 blocks that day and is not measured) | 10/10 (`node proba-judecata.mjs --control-negativ`: form, completeness of the timeline, posting, deadline, window, arbiter, payout, result bytes, replay, proof, each planted out on a copy and each forged fixture then accepted) |
|
||||
| verifiable-data | 23/23 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-verifiable-data.mjs`: honest answers VALID, ten forged or malformed ones INVALID for their named reason, availability sampling on a local server; the envelope row is NEMASURAT without the verifier, exit 2); the two chain rows of `--viu` measured on testnet 28001 on 2026-10-02 (a version notarized with a test key and read back under a post-quantum certified anchor; evidence in `dovezi/`) | 5/5 (`node proba-verifiable-data.mjs --control-negativ`: field commitment, index order, completeness, consistency, sampling, each planted out on a copy and each forged fixture then accepted) |
|
||||
| ai-trust | 16/16 with the AIP-23 verifier, measured 2026-10-05 (`AERE_VERIFY_PROOF=... node proba-lant-de-incredere.mjs`: the whole chain on a committed fixture, then thirteen bundles broken on purpose, each refused for its named reason, among them an agent with a classical key and an audit head signed with one); without the verifier it exits 2 | 11/11 (`node proba-lant-de-incredere.mjs --control-negativ`) |
|
||||
| conformance | 12/12 on 2026-10-20 in the development repository, with the AIP-23 reference verifier of that repository (byte for byte the one served at aere.network/tools), Python 3 and testnet 28001 read (`AERE_VERIFY_PROOF=<verify-proof.mjs> node proba-conformance.mjs --online`: the rules and the vectors are what the code derives, every rule has a vector, every invalid vector is one operation from its base or one declared change at its producer, the published verifiers pass 791/791, an independent Python implementation passes the 171 it claims and the other 620 are reported not covered, 225,345 malformed inputs and 183 malformed envelope files each get a verdict); 11/11 from a fresh clone of this repository on 2026-10-20, alone in an empty folder, nothing written beside it and its tree left clean (the same command; the twelfth row, the comparison with the published AIP-23 kind vectors, needs their generator, which is not part of this copy, and is reported as information); without `--online` the nine chain vectors are reported unmeasured and the exit code is 2 | 342/342 on 2026-10-20: 260/260 rules (`node proba-conformance.mjs --control-negativ --online`: each rule planted out of its verifier in a copy; the vectors that expect it, and only they, must fail), then 76/76 conditions of the device verifier's compound checks, each planted out alone (exactly the vectors written for it must fail), then six controls of the instruments |
|
||||
| device-identity | 372/372 with the AIP-23 verifier, measured 2026-10-14 (`AERE_VERIFY_PROOF=... node proba-device.mjs`: fifty-four bundles that must be VALID; seventy-seven with something not brought, not read or not readable, each PARTIAL for its named causes; two hundred and four attack rows, each refused for exactly its named rules, among them a clone with another key, a rolled-back or retracted firmware (also when the release is left out of the bundle), a previous owner that still signs, an owner's retirement written under no ownership head, a replayed attestation, an attestation dated ahead of the verifier's clock, a manufacturer the verifier does not trust, a retirement, a classical key under the ML-DSA-65 name, a signature in another base64 text, a day that does not exist; 804 damaged bundles each given a verdict; the command line through files only; the evidence recorded on testnet 28001 judged again offline); the chain as witness 10/10 on testnet 28001 on 2026-10-05, before the repairs of that day (`proba-device-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier; not run again since: the bundles and readings it recorded give the same verdicts under the verifier of now) | 220/220 (`node control-negativ-device.mjs`: the untouched copy green; each of the 47 rules taken out of a copy, the rows that expect it, and only they, go red; 172 more plantings, most of them one condition of a compound check taken out alone, each with its rows) |
|
||||
| device-operation | 107/107 on 2026-10-20 (`node proba-operation.mjs`: 18 logs that must be VALID, 15 PARTIAL each for its named causes, 48 attack rows each refused for exactly its named rules, 459 damaged logs each given a verdict, none counting a record the device did not sign; an adversarial review of 2026-10-20 found seven defects, among them a forged equivocation and a retired key that erased a witnessed history, each repaired with its row); the reputation's two implementations pass its 103 vectors 103/103 each through `../conformance/conformance.mjs`; the same figures from a fresh clone of this repository on 2026-10-20 | 62/62 on 2026-10-20 (`node control-negativ-operation.mjs`: each of the 17 rules taken out of a copy, then 44 more plantings, each turning exactly its rows red; each defect of the review is put back by one planting; 62/62 also from the fresh clone) |
|
||||
| device-operation | 107/107 on 2026-10-20 (`node proba-operation.mjs`: 18 logs that must be VALID, 15 PARTIAL each for its named causes, 48 attack rows each refused for exactly its named rules, 459 damaged logs each given a verdict, none counting a record the device did not sign; an adversarial review of 2026-10-20 found seven defects, among them a forged equivocation and a retired key that erased a witnessed history, each repaired with its row); the reputation's two implementations pass its 103 vectors 103/103 each through `../conformance/conformance.mjs`; the same figures from a fresh clone of this repository on 2026-10-20 | 63/63 on 2026-10-21 (`node control-negativ-operation.mjs`: the untouched copy green, each of the 17 rules taken out of a copy, then 45 more plantings, each turning exactly its rows red; each defect of the review is put back by one planting; the 45th, since 2026-10-21, makes `reputation.py` write CRLF and turns the byte comparison of the two command lines red; 62/62 on 2026-10-20, also from the fresh clone) |
|
||||
| exposure-report | 164 passed and 1 `NEMASURAT` of 165 rows in the AERE development tree on 2026-10-14 (`node proba-expunere.mjs`: codec checks with real public vectors, mode 1 and mode 2 through four mock targets that serve recorded responses, the network guard's proof that in modes 1 and 2 nothing leaves the machine, the language gate, the AIP-23 `compliance` envelopes judged by the reference verifier; the `NEMASURAT` row is mode 3 against a real provider, which would send addresses to a third party); in this copy, after `npm install --ignore-scripts` in its directory: 153 passed, 0 red, 0 broken, 12 `NEMASURAT` with the reason (the rows compared with the AIP-23 verifier, the anchor verifier's RLP encoder and MPT walker and the shield's header pins, none of which is in this repository: with `AERE_PROOF_PROTOCOL_VERIFY`, `AERE_VERIFY_PROOF` and `AERE_EXPUNERE_PINI` naming them the rows run, 162 passed and 3 `NEMASURAT`; the scan of the directory for names from the AERE host inventory; the git-ignore row outside a git tree); the exit code is 0 unless a row is red or broken | inside the test, 2026-10-14: each rule has its reverse planting in a copy of the tool (a foreign provider planted on each network path is caught, a label planted in the language gate is refused, each classification rule taken out turns its row red, a `@noble/curves` 2.x planted first is passed over and alone is named as missing), and the recorded fixtures are judged against their declared date |
|
||||
|
||||
Code comments, most function and variable names (also many exported between the files of a component), test names and control
|
||||
|
||||
@ -35,6 +35,12 @@ Node.js 24 and `ethers` (for EIP-712 and secp256k1: `npm install` here). Payment
|
||||
`verifica-plati.mjs` then proves from the outside that a wallet's on-chain payments were allowed by the agent's policy: the ledger
|
||||
verifies, each payment names an allowed entry, its transaction emits `AuthorizationUsed(wallet, sha256(entry hash))` and the
|
||||
`Transfer` of that amount, and with `--all-transfers` every transfer out of the wallet since a block is one of those payments.
|
||||
The blocks from `fromBlock` to the head read once with `eth_blockNumber` are read in windows of at most `--window` blocks (5,000 by
|
||||
default; public endpoints refuse a long range, and on 2026-10-21 the testnet's refused 10,000 with `Requested range exceeds maximum
|
||||
RPC range limit`), four at a time. A window counts only when the endpoint answers with a list; an error, an answer that is not a list
|
||||
or a failed request leaves the window unread, and then the check is `UNMEASURED` with the count of unread windows, never "no
|
||||
transfers" (a transfer out of the wallet that is not a payment, found in a window that was read, is `INVALID` all the same). Above
|
||||
`--max-requests` windows (2,000 by default) nothing is asked and the check is `UNMEASURED`, saying how many windows it would need.
|
||||
|
||||
## Use
|
||||
|
||||
@ -90,8 +96,8 @@ Measured on 2026-09-29 (Node.js 24.14.1, ethers 6.16.0):
|
||||
|---|---|
|
||||
| `node proba-wallet.mjs` | 25/25 without a network, real EIP-712 signatures and ML-DSA-65 keys: five paid purchases through a local facilitator, the sixth refused by the policy; attacks by the holder of the agent key (a forged "allowed" entry over the limit, an entry for another amount, the same entry twice, a branch with its equivocation proof, a new ledger under a changed policy, a backdated entry, two branches sent at the same time), the owner's revocation, human approval, a payment for another requirement and a replayed payment at the seller |
|
||||
| `node proba-cosign.mjs` | 16/16 without a network (the contract's ERC-1271 check emulated in the local facilitator): three purchases from a 2-of-2 wallet with both halves in order, the fourth refused by the policy; the agent alone, and the policy signer alone, cannot pay; a compromised wallet service that changes the recipient, the payer, the amount, the nonce, the validity, the digest, or signs with another key: the agent refuses to co-sign; the co-signing service over HTTP |
|
||||
| `node proba-verifica-plati.mjs` | 14/14 without a network, on the chain responses recorded for both testnet runs below, including the contract's code (one byte changed, another signer named, or no code at the wallet: `INVALID`) |
|
||||
| `node control-negativ-wallet.mjs` | 30/30: each guard of the wallet, the client (including the agent's co-signing checks), the seller and the verifier (including the contract code) removed in a copy, and the named check must fail |
|
||||
| `node proba-verifica-plati.mjs` | 24/24 on 2026-10-21 without a network, on the chain responses recorded for the testnet runs below (the latest EOA wallet run and the 2-of-2 run; until that day the pattern of the EOA file also matched the 2-of-2 file, so rows 1-10 judged the 2-of-2 run and no row judged an EOA run), including the contract's code (one byte changed, another signer named, or no code at the wallet: `INVALID`); and `--all-transfers` on an endpoint that refuses long ranges: 3,178,224 blocks read in 636 windows of 5,000 (`VALID`), the transfers found in different windows, a foreign transfer in the last window (`INVALID`), one request over the whole range, a window answering an error, a non-list or HTTP 429, a head below `fromBlock`, a range above the request cap (each `UNMEASURED`, never `VALID`), and a foreign transfer found while another window is unread (`INVALID`) |
|
||||
| `node control-negativ-wallet.mjs` | 36/36 on 2026-10-21: each guard of the wallet, the client (including the agent's co-signing checks), the seller and the verifier (including the contract code, and since 2026-10-21 the windows of `--all-transfers`: one request instead of windows, an unread window or a non-list taken for an empty one, an unread window hiding a foreign transfer, the request cap, a head below `fromBlock`) removed in a copy, and the named check must fail |
|
||||
| `AERE_TESTNET_KEY_FILE=<funded testnet key> node proba-x402-testnet.mjs` | 9/9 on the public testnet 28001 through its x402 facilitator: a fresh wallet funded with 0.05 tUSD, three purchases of 0.01 paid and settled on chain, the fourth refused by the policy, a replayed payment refused, the balances and the used authorization nonces read on chain, and the evidence file verified `VALID` (a changed amount, or a payment removed from the file: `INVALID`) |
|
||||
| `AERE_TESTNET_KEY_FILE=<funded testnet key> node proba-cosign-testnet.mjs` | 13/13 on the public testnet 28001: an `AereAgentWallet2of2` deployed from `AereAgentWallet2of2.json` with two fresh keys, its code on chain exactly the compiled code; `isValidSignature` on chain yes only for agent then policy signer, no for seven other forms; three purchases of 0.01 from it through the testnet facilitator, the fourth refused by the policy; the agent alone and the policy signer alone refused by the facilitator, and by the token itself asked on chain without the facilitator; balances and nonces on chain; the evidence verified `VALID` with the contract code (another policy signer named: `INVALID`) |
|
||||
| `node recompileaza-contract.mjs --solc <solc 0.8.23>` | `IDENTICAL`: 1193 bytes of creation code and 934 of runtime code, with the native solc 0.8.23+commit.f704f362 |
|
||||
@ -106,5 +112,11 @@ node verifica-plati.mjs dovezi-28001/plati-agent-2026-09-29-20-23-16.json --rpc
|
||||
node verifica-plati.mjs dovezi-28001/plati-agent-2of2-2026-09-29-21-29-48.json --rpc https://testnet-rpc.aere.network --all-transfers
|
||||
```
|
||||
|
||||
Measured on 2026-10-21 against the testnet endpoint: both `VALID` (exit 0), every transfer out of each wallet since its `fromBlock`
|
||||
read in 636 and 635 windows of 5,000 blocks (about 3.2 million blocks each, 3 transfers each, all three payments of the ledger), in
|
||||
8 min 18 s and 6 min 55 s (the endpoint took 3 to 10 seconds per window of old blocks), and again from a fresh clone of the
|
||||
public copy as staged that evening, `VALID` both, in 6 min 6 s and 5 min 34 s. Before that day both commands answered
|
||||
`UNMEASURED` (exit 2): they asked for the whole range in one request, and the endpoint refused it.
|
||||
|
||||
tUSD is the testnet's EIP-3009 token, minted by a faucet and worth nothing. Nothing here has been run on the Aere Network mainnet.
|
||||
Code comments and test names are in Romanian; messages, data and the documentation are in English. No third party has reviewed it.
|
||||
|
||||
@ -49,6 +49,16 @@ const PLANTARI = [
|
||||
['verificatorul nu mai cere suma din Transfer', 'verifica-plati.mjs', '&& BigInt(l.data) === BigInt(a.amount)));', '));', '7. CONTROL', V],
|
||||
['verificatorul nu mai cere ca orice Transfer sa fie in registru', 'verifica-plati.mjs', 'straine.length === 0,', 'true,', '8. CONTROL', V],
|
||||
['verificatorul nu mai cere lantul dosarului', 'verifica-plati.mjs', 'lantul === chain,', 'true,', '9. CONTROL', V],
|
||||
// --all-transfers pe o usa care refuza intervalele mari (2026-10-21, trecerea straina): paginarea si clasificarea fiecarei ferestre
|
||||
['verificatorul nu mai pagineaza (o singura cerere de la fromBlock la cap)', 'verifica-plati.mjs', 'for (let a = from; a <= to; a += window) ferestre.push([a, Math.min(a + window - 1, to)]);',
|
||||
"if (process.env.AERE_PLANTA_NICIODATA === 'da') for (let a = from; a <= to; a += window) ferestre.push([a, Math.min(a + window - 1, to)]); else ferestre.push([from, to]);", '15. usa cu limita', V],
|
||||
['o fereastra cu eroare se numara drept goala', 'verifica-plati.mjs', 'if (j && Array.isArray(j.result)) { peFereastra[k] = j.result; return; }',
|
||||
"if ((j && Array.isArray(j.result)) || process.env.AERE_PLANTA_NICIODATA !== 'da') { peFereastra[k] = j && Array.isArray(j.result) ? j.result : []; return; }", '18. CONTROL', V],
|
||||
['un rezultat care nu e lista se numara drept gol', 'verifica-plati.mjs', 'if (j && Array.isArray(j.result)) { peFereastra[k] = j.result; return; }',
|
||||
'if (j && !j.error && !j.transport) { peFereastra[k] = Array.isArray(j.result) ? j.result : []; return; }', '19. CONTROL', V],
|
||||
['o fereastra necitita ascunde un Transfer strain gasit', 'verifica-plati.mjs', 'if (straine.length || !r.unreadable) {', "if ((straine.length && process.env.AERE_PLANTA_NICIODATA === 'da') || !r.unreadable) {", '21. CONTROL', V],
|
||||
['plafonul de cereri nu mai e respectat', 'verifica-plati.mjs', 'if (ferestre.length > maxRequests) return', "if (ferestre.length > maxRequests && process.env.AERE_PLANTA_NICIODATA === 'da') return", '22. CONTROL', V],
|
||||
['un cap sub fromBlock e luat drept interval gol', 'verifica-plati.mjs', '|| to < from) nu(', "|| (to < from && process.env.AERE_PLANTA_NICIODATA === 'da')) nu(", '23. CONTROL', V],
|
||||
// portofelul-contract 2-din-2 in dosar: codul de pe lant
|
||||
['verificatorul nu mai judeca portofelul-contract', 'verifica-plati.mjs', ' if (d.walletContract) {', " if (d.walletContract && process.env.AERE_PLANTA_NICIODATA === 'da') {", '14. CONTROL', V],
|
||||
['verificatorul cere doar lungimea codului, nu codul', 'verifica-plati.mjs', 'cod === asteptat, cod === asteptat ?', 'cod.length === asteptat.length, cod === asteptat ?', '12. CONTROL', V],
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
// node inregistreaza-rpc.mjs <dovada.json> --rpc <url> --out <rpc-inregistrat.json>
|
||||
import fs from 'node:fs';
|
||||
import { incarcaEthers } from './wallet.mjs';
|
||||
import { jurnalePaginate } from './verifica-plati.mjs';
|
||||
|
||||
const a = process.argv.slice(2); const get = (f) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : undefined; };
|
||||
const f = a[0]; const rpc = get('--rpc'); const out = get('--out');
|
||||
@ -14,8 +15,16 @@ const apel = async (method, params) => { const r = await fetch(rpc, { method: 'P
|
||||
const chainId = await apel('eth_chainId', []);
|
||||
const receipts = {};
|
||||
for (const p of d.payments) receipts[p.transaction.toLowerCase()] = await apel('eth_getTransactionReceipt', [p.transaction]);
|
||||
const logs = await apel('eth_getLogs', [{ address: d.token, fromBlock: '0x' + Number(d.fromBlock).toString(16), toBlock: 'latest',
|
||||
topics: [ethers.id('Transfer(address,address,uint256)'), '0x' + '0'.repeat(24) + d.wallet.toLowerCase().slice(2)] }]);
|
||||
const code = d.walletContract ? await apel('eth_getCode', [d.wallet, 'latest']) : undefined;
|
||||
fs.writeFileSync(out, JSON.stringify({ v: 1, recordedAt: new Date().toISOString(), rpc, chainId, receipts, transfersOut: logs, ...(code ? { code } : {}) }, null, 1) + '\n');
|
||||
console.log(`recorded chain ${Number(chainId)}: ${Object.keys(receipts).length} receipts, ${logs.length} transfers out of the wallet${code ? `, the wallet code (${(code.length - 2) / 2} bytes)` : ''} -> ${out}`);
|
||||
// 2026-10-21: in ferestre, ca verificatorul (o usa publica refuza un interval lung); o fereastra necitita OPRESTE inregistrarea, fiindca o
|
||||
// inregistrare cu o fereastra lipsa ar fi o lista de transferuri mai scurta decat lantul, pe care proba ar lua-o drept adevar
|
||||
const capul = Number(await apel('eth_blockNumber', []));
|
||||
const apelBrut = async (method, params) => { const r = await fetch(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) }); return r.json(); };
|
||||
const citite = await jurnalePaginate(apelBrut, { address: d.token, topics: [ethers.id('Transfer(address,address,uint256)'), '0x' + '0'.repeat(24) + d.wallet.toLowerCase().slice(2)] }, { from: Number(d.fromBlock), to: capul });
|
||||
// process.exitCode, nu process.exit(): dupa un fetch, iesirea imediata cade pe Windows in libuv (UV_HANDLE_CLOSING, lectia din 2026-09-11)
|
||||
if (citite.refused || citite.unreadable) { console.error(`not recorded: ${citite.refused || `${citite.unreadable} of ${citite.windows} windows unread (first: ${citite.first})`}`); process.exitCode = 2; }
|
||||
else {
|
||||
const logs = citite.logs;
|
||||
const code = d.walletContract ? await apel('eth_getCode', [d.wallet, 'latest']) : undefined;
|
||||
fs.writeFileSync(out, JSON.stringify({ v: 1, recordedAt: new Date().toISOString(), rpc, chainId, receipts, transfersOut: logs, ...(code ? { code } : {}) }, null, 1) + '\n');
|
||||
console.log(`recorded chain ${Number(chainId)}: ${Object.keys(receipts).length} receipts, ${logs.length} transfers out of the wallet${code ? `, the wallet code (${(code.length - 2) / 2} bytes)` : ''} -> ${out}`);
|
||||
}
|
||||
|
||||
@ -13,8 +13,12 @@ const AICI = path.dirname(fileURLToPath(import.meta.url));
|
||||
const D = path.join(AICI, 'dovezi-28001');
|
||||
// doua rulari pe 28001: portofelul EOA (plati-agent-<data>) si portofelul-contract 2-din-2 (plati-agent-2of2-<data>), fiecare cu inregistrarea ei
|
||||
const ultimul = (re) => fs.readdirSync(D).filter((n) => re.test(n)).sort().pop();
|
||||
const dovadaF = ultimul(/^plati-agent-\d.*\.json$/), inregF = ultimul(/^rpc-inregistrat-\d.*\.json$/);
|
||||
// 2026-10-21: tiparul EOA cere ANUL dupa prefix (\d{4}-); `\d.*` se potrivea si cu "2of2-", care sorteaza ultimul, deci randurile 1-10
|
||||
// judecau tot dosarul 2-din-2 si dosarul portofelului EOA nu era judecat de nicio proba, desi tabelul spunea "both testnet runs"
|
||||
const dovadaF = ultimul(/^plati-agent-\d{4}-.*\.json$/), inregF = ultimul(/^rpc-inregistrat-\d{4}-.*\.json$/);
|
||||
const dovada2F = ultimul(/^plati-agent-2of2-.*\.json$/), inreg2F = ultimul(/^rpc-inregistrat-2of2-.*\.json$/);
|
||||
if (dovadaF && dovadaF === dovada2F) { console.log('NEMASURAT: tiparul dosarului EOA a ales dosarul 2-din-2'); process.exit(2); }
|
||||
console.log(` dosare: EOA ${dovadaF} / ${inregF}; 2-din-2 ${dovada2F} / ${inreg2F}`);
|
||||
let ok = 0, rau = 0;
|
||||
const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; };
|
||||
if (!dovadaF || !inregF || !dovada2F || !inreg2F) { console.log(`NEMASURAT: lipseste un dosar-dovada sau o inregistrare RPC in ${D}`); process.exit(2); }
|
||||
@ -25,22 +29,43 @@ const inreg2 = JSON.parse(fs.readFileSync(path.join(D, inreg2F), 'utf8'));
|
||||
const copie = (o) => JSON.parse(JSON.stringify(o));
|
||||
|
||||
// un RPC local peste inregistrare (sau peste o varianta schimbata a ei)
|
||||
async function rpcDin(rec) {
|
||||
// 2026-10-21 (trecerea straina, D1): macheta serveste si eth_blockNumber (implicit cel mai inalt bloc din inregistrare: chitante si
|
||||
// transferuri; `o.cap` il muta), filtreaza transferurile pe [fromBlock, toBlock] ca o usa reala, refuza ca usa testnetului un interval
|
||||
// mai lung de `o.limita` blocuri (-32005), si raspunde stricat pe ferestrele pe care `o.strica(a, b)` le numeste. Numara cererile eth_getLogs.
|
||||
const nrBloc = (x) => parseInt(x.blockNumber, 16);
|
||||
async function rpcDin(rec, o = {}) {
|
||||
const cap = o.cap ?? Math.max(...rec.transfersOut.map(nrBloc), ...Object.values(rec.receipts).filter(Boolean).map(nrBloc));
|
||||
const limita = o.limita ?? Infinity;
|
||||
const stat = { getLogs: 0, maxInterval: 0 };
|
||||
const trimite = (res, corp, cod = 200) => { res.writeHead(cod, { 'content-type': 'application/json' }); res.end(typeof corp === 'string' ? corp : JSON.stringify(corp)); };
|
||||
const srv = http.createServer((req, res) => { let s = ''; req.on('data', (x) => { s += x; }); req.on('end', () => {
|
||||
const q = JSON.parse(s); let result = null;
|
||||
if (q.method === 'eth_chainId') result = rec.chainId;
|
||||
else if (q.method === 'eth_blockNumber') result = '0x' + cap.toString(16);
|
||||
else if (q.method === 'eth_getTransactionReceipt') result = rec.receipts[String(q.params[0]).toLowerCase()] || null;
|
||||
else if (q.method === 'eth_getLogs') result = rec.transfersOut;
|
||||
else if (q.method === 'eth_getLogs') {
|
||||
stat.getLogs++;
|
||||
const f = q.params[0], a = parseInt(f.fromBlock, 16), b = f.toBlock === 'latest' ? cap : parseInt(f.toBlock, 16);
|
||||
stat.maxInterval = Math.max(stat.maxInterval, b - a + 1);
|
||||
if (b - a + 1 > limita) { trimite(res, { jsonrpc: '2.0', id: q.id, error: { code: -32005, message: 'Requested range exceeds maximum RPC range limit' } }); return; }
|
||||
const stricat = o.strica && o.strica(a, b);
|
||||
if (stricat) { trimite(res, stricat.corp(q.id), stricat.cod || 200); return; }
|
||||
result = rec.transfersOut.filter((l) => nrBloc(l) >= a && nrBloc(l) <= b);
|
||||
}
|
||||
// 2026-10-11 (D-FAZA2-04b, R2): macheta serveste numai ce e INREGISTRAT; o inregistrare fara `code` nu devine "fara cod",
|
||||
// ci o eroare JSON-RPC, pe care verificatorul trebuie sa o trateze ca necunoscut (inregistrarile EOA poarta explicit "0x")
|
||||
else if (q.method === 'eth_getCode') { if (rec.code === undefined) { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: q.id, error: { code: -32000, message: 'World state unavailable' } })); return; } result = rec.code; }
|
||||
res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: q.id, result }));
|
||||
}); });
|
||||
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
|
||||
return { url: `http://127.0.0.1:${srv.address().port}`, close: () => srv.close() };
|
||||
return { url: `http://127.0.0.1:${srv.address().port}`, close: () => srv.close(), stat };
|
||||
}
|
||||
async function judeca(dov, rec, allTransfers = true) { const r = await rpcDin(rec); try { return await verificaPlati(dov, { rpc: r.url, allTransfers }); } finally { r.close(); } }
|
||||
// aceeasi judecata pe o usa cu forma data (o), cu fereastra si plafonul date verificatorului (v); intoarce si ce a numarat macheta
|
||||
async function judecaUsa(dov, rec, o = {}, v = {}) { const r = await rpcDin(rec, o); try { return { ...(await verificaPlati(dov, { rpc: r.url, allTransfers: true, ...v })), stat: r.stat }; } finally { r.close(); } }
|
||||
const pica = (v, re) => v.checks.some((c) => c.pass === false && re.test(c.name));
|
||||
const toate = /all transfers out of the wallet|every Transfer out of the wallet/;
|
||||
const verificarea = (v) => v.checks.find((c) => toate.test(c.name)) || { name: '(lipseste)', pass: undefined, detail: '' };
|
||||
|
||||
try {
|
||||
const v0 = await judeca(dovada, inreg);
|
||||
@ -78,6 +103,58 @@ try {
|
||||
cer(pica(await judeca(d13, inreg2), e2), '13. CONTROL: dosarul numeste alt semnatar al agentului decat cel din codul de pe lant -> INVALID');
|
||||
const r14 = copie(inreg2); r14.code = '0x'; // 2026-10-11: controlul spune EXPLICIT "cont fara cod"; o inregistrare fara camp e tacere, nu absenta (R2)
|
||||
cer(pica(await judeca(dovada2, r14), e2), '14. CONTROL: la adresa portofelului nu e niciun cod (un cont cu o cheie), dar dosarul spune 2-din-2 -> INVALID');
|
||||
|
||||
// 2026-10-21 (trecerea straina, D1): --all-transfers pe o usa care refuza intervalele mari. Pe 28001 capul era 7.200.317, deci de la
|
||||
// fromBlock-ul dosarului EOA (4.022.094) sunt 3.178.224 de blocuri; usa testnetului accepta 5.001 si refuza 10.000.
|
||||
// Ferestrele si blocurile se DERIVA din dosarul si inregistrarea alese mai sus, nu se scriu fix.
|
||||
const from = Number(dovada.fromBlock), capViu = from + 3178223;
|
||||
const blocuri = inreg.transfersOut.map(nrBloc), capInreg = Math.max(...blocuri, ...Object.values(inreg.receipts).map(nrBloc));
|
||||
const W = 8, nrFerestre = Math.ceil((capInreg - from + 1) / W), ferestreCuTransfer = new Set(blocuri.map((b) => Math.floor((b - from) / W))).size;
|
||||
const transfer = (l, bloc, tx) => ({ ...l, blockNumber: '0x' + bloc.toString(16), transactionHash: tx });
|
||||
const strain = (bloc) => { const r = copie(inreg); r.transfersOut.push(transfer(r.transfersOut[0], bloc, '0x' + 'ee'.repeat(32))); return r; };
|
||||
const v15 = await judecaUsa(dovada, inreg, { cap: capViu, limita: 5000 });
|
||||
const c15 = verificarea(v15);
|
||||
cer(v15.verdict === 'VALID' && c15.pass === true && v15.stat.getLogs === 636 && v15.stat.maxInterval <= 5000 && new RegExp(`${blocuri.length} transfers, 636 windows`).test(c15.name),
|
||||
`15. usa cu limita de 5.000 de blocuri, 3.178.224 de blocuri de citit (ca 28001 pe 2026-10-21): VALID, ${v15.stat.getLogs} cereri, cel mai lung interval ${v15.stat.maxInterval} (${c15.name.slice(0, 110)})`);
|
||||
// ferestre de W blocuri pe [fromBlock, capul inregistrarii]: transferurile cad in mai multe ferestre DIFERITE
|
||||
const v16 = await judecaUsa(dovada, inreg, { limita: 10 }, { window: W });
|
||||
const c16 = verificarea(v16);
|
||||
cer(ferestreCuTransfer >= 2 && v16.verdict === 'VALID' && c16.pass === true && v16.stat.getLogs === nrFerestre
|
||||
&& new RegExp(`${blocuri.length} transfers, ${nrFerestre} windows of at most ${W} blocks\\)`).test(c16.name),
|
||||
`16. ferestre de ${W} blocuri pe o usa cu limita 10: ${blocuri.length} transferuri gasite in ${ferestreCuTransfer} ferestre diferite din ${nrFerestre}, VALID (${c16.name.slice(0, 110)})`);
|
||||
const v16b = await judecaUsa(dovada, strain(capInreg), { limita: 10 }, { window: W });
|
||||
cer(v16b.verdict === 'INVALID' && verificarea(v16b).pass === false, `16b. CONTROL: un Transfer strain in ULTIMA fereastra (blocul capului) -> ${v16b.verdict}`);
|
||||
// fara paginare: o singura cerere peste tot intervalul (fereastra mai mare decat intervalul), pe usa cu limita
|
||||
const v17 = await judecaUsa(dovada, inreg, { cap: capViu, limita: 5000 }, { window: 10000000 });
|
||||
const c17 = verificarea(v17);
|
||||
cer(v17.verdict === 'UNMEASURED' && c17.pass === null && /Requested range exceeds/.test(c17.detail) && !/\d+ transfers/.test(c17.name),
|
||||
`17. CONTROL: fara paginare (o singura cerere de la fromBlock la cap) pe usa cu limita -> ${v17.verdict}, cu refuzul usii, niciodata "0 transferuri"`);
|
||||
// o fereastra cu eroare nu se numara drept goala: Transferul strain sta exact in fereastra necitita (prima, care incepe la fromBlock)
|
||||
const strica1 = (forma) => ({ strica: (a) => a === from ? forma : null, limita: 10 });
|
||||
const eroare = { corp: (id) => ({ jsonrpc: '2.0', id, error: { code: -32000, message: 'internal error' } }) };
|
||||
const unaDin = new RegExp(`1 of ${nrFerestre} windows`);
|
||||
const v18 = await judecaUsa(dovada, strain(from + 1), strica1(eroare), { window: W });
|
||||
const c18 = verificarea(v18);
|
||||
cer(v18.verdict === 'UNMEASURED' && c18.pass === null && unaDin.test(c18.detail),
|
||||
`18. CONTROL: fereastra cu Transferul strain raspunde cu o eroare JSON-RPC -> ${v18.verdict} (${String(c18.detail).slice(0, 70)}), nu VALID`);
|
||||
const v19 = await judecaUsa(dovada, strain(from + 1), strica1({ corp: (id) => ({ jsonrpc: '2.0', id, result: null }) }), { window: W });
|
||||
cer(v19.verdict === 'UNMEASURED' && unaDin.test(verificarea(v19).detail) && /not a list/.test(verificarea(v19).detail),
|
||||
`19. CONTROL: aceeasi fereastra raspunde cu un rezultat care nu e lista (null) -> ${v19.verdict}, nu "goala"`);
|
||||
const v20 = await judecaUsa(dovada, strain(from + 1), strica1({ cod: 429, corp: () => 'Too Many Requests' }), { window: W });
|
||||
cer(v20.verdict === 'UNMEASURED' && unaDin.test(verificarea(v20).detail),
|
||||
`20. CONTROL: aceeasi fereastra raspunde HTTP 429 cu text, nu JSON -> ${v20.verdict}`);
|
||||
// un Transfer strain intr-o fereastra CITITA da INVALID chiar daca alta fereastra e necitita
|
||||
const v21 = await judecaUsa(dovada, strain(capInreg), strica1(eroare), { window: W });
|
||||
cer(v21.verdict === 'INVALID' && verificarea(v21).pass === false && /1 unreadable/.test(verificarea(v21).name),
|
||||
`21. CONTROL: Transfer strain in ultima fereastra, prima fereastra necitita -> ${v21.verdict} (INVALID bate NEMASURAT)`);
|
||||
// plafonul de cereri: 20 de milioane de blocuri la 5.000 cer 4.001 ferestre, peste plafonul implicit de 2.000 -> nu se cere nimic
|
||||
const v22 = await judecaUsa(dovada, inreg, { cap: from + 20000000, limita: 5000 });
|
||||
cer(v22.verdict === 'UNMEASURED' && v22.stat.getLogs === 0 && /4001 requests .* above the cap of 2000/.test(verificarea(v22).detail),
|
||||
`22. CONTROL: intervalul cere 4.001 ferestre, peste plafonul de 2.000 -> ${v22.verdict}, ${v22.stat.getLogs} cereri eth_getLogs`);
|
||||
// un cap SUB fromBlock (o usa ramasa in urma) nu e un interval gol
|
||||
const v23 = await judecaUsa(dovada, inreg, { cap: from - 1 });
|
||||
cer(v23.verdict === 'UNMEASURED' && v23.stat.getLogs === 0 && /is not at or above the evidence's fromBlock/.test(verificarea(v23).detail),
|
||||
`23. CONTROL: capul usii e sub fromBlock-ul dosarului -> ${v23.verdict}, nu "0 transferuri"`);
|
||||
} catch (e) { cer(false, `proba s-a oprit: ${String(e.message || e).slice(0, 160)}`); }
|
||||
console.log(`\nverifica-plati: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`);
|
||||
process.exitCode = rau ? 1 : 0;
|
||||
|
||||
@ -7,23 +7,61 @@
|
||||
// 3. pe lant, tranzactia platii are status 1 si emite, din contractul activului, AuthorizationUsed(portofel, nonce) cu nonce =
|
||||
// sha256(hash-ul intrarii) si Transfer(portofel, destinatar, suma) - deci plata de pe lant e chiar cea din registru;
|
||||
// 4. cu --all-transfers: ORICE Transfer din portofel in intervalul de blocuri e una din platile de mai sus (nicio plata fara intrare);
|
||||
// intervalul [fromBlock, capul citit o data cu eth_blockNumber] se citeste in FERESTRE de cel mult --window blocuri (implicit 5000),
|
||||
// fiindca usile publice refuza un interval mare (2026-10-21, trecerea straina: un singur eth_getLogs de la 4.022.094 la varful lui
|
||||
// 28001 a primit `-32005 Requested range exceeds maximum RPC range limit`; usa testnetului accepta 5.001 de blocuri si refuza 10.000).
|
||||
// Un raspuns e MASURAT numai cand `result` e o lista; orice altceva (eroare JSON-RPC, transport, rezultat care nu e lista) e o fereastra
|
||||
// NECITITA, numarata separat, si verificarea iese NEMASURAT, niciodata "0 transferuri" (lectia din 2026-10-15 a lui CLAUDE.md). Un
|
||||
// Transfer strain gasit intr-o fereastra citita da INVALID oricum. Peste --max-requests ferestre (implicit 2000) nu se cere nimic:
|
||||
// verificarea iese NEMASURAT si spune cate ar fi trebuit;
|
||||
// 5. cand dosarul numeste un portofel-contract 2-din-2 (walletContract: {agentSigner, policySigner}): codul de pe lant al portofelului e
|
||||
// EXACT codul compilat din AereAgentWallet2of2.sol (artefactul de langa verificator) cu cei doi semnatari in locul imutabilelor, deci
|
||||
// din portofel nu a putut plati nici agentul singur, nici serviciul de politica singur.
|
||||
// Ce nu dovedeste: ca serviciul cumparat a fost livrat; ca portofelul nu a semnat si autorizari nedecontate (acelea nu misca bani).
|
||||
// node verifica-plati.mjs <dovada.json> --rpc <url> [--all-transfers] iesire 0 VALID, 1 INVALID, 2 NEMASURAT
|
||||
// node verifica-plati.mjs <dovada.json> --rpc <url> [--all-transfers [--window <blocuri>] [--max-requests <n>]]
|
||||
// iesire 0 VALID, 1 INVALID, 2 NEMASURAT
|
||||
import fs from 'node:fs';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { verifyLedger } from '../agent-ledger.mjs';
|
||||
import { incarcaEthers, nonceForEntry, assetId } from './wallet.mjs';
|
||||
import { incarcaArtefact, codulAsteptat, amprentaSursei } from './contract-2of2.mjs';
|
||||
|
||||
export async function verificaPlati(d, { rpc, allTransfers = false, fetchImpl = fetch } = {}) {
|
||||
// fereastra implicita: limita masurata a lui rpc2 (5.000, CLAUDE.md 2026-10-15) si sub cea a usii testnetului (refuza 10.000);
|
||||
// plafonul: 2000 de ferestre = 10 milioane de blocuri la fereastra implicita (28001 avea 7,2 milioane pe 2026-10-21)
|
||||
export const FEREASTRA_IMPLICITA = 5000, CERERI_MAXIME = 2000;
|
||||
// cate ferestre se cer deodata: putine, ca usa publica sa nu le refuze pentru ritm (un refuz pentru ritm ar iesi NEMASURAT, nu fals)
|
||||
const PARALEL = 4;
|
||||
const hex = (n) => '0x' + n.toString(16);
|
||||
|
||||
// citeste un filtru de jurnale pe [from, to] in ferestre de cel mult `window` blocuri. apelBrut(method, params) intoarce raspunsul JSON
|
||||
// intreg (sau arunca la transport); o fereastra e masurata NUMAI daca `result` e o lista. Intoarce { logs, windows, unreadable, first }
|
||||
// sau { refused } cand intervalul cere mai mult de `maxRequests` ferestre (atunci nu se cere nimic).
|
||||
export async function jurnalePaginate(apelBrut, filtru, { from, to, window = FEREASTRA_IMPLICITA, maxRequests = CERERI_MAXIME }) {
|
||||
const ferestre = [];
|
||||
for (let a = from; a <= to; a += window) ferestre.push([a, Math.min(a + window - 1, to)]);
|
||||
if (ferestre.length > maxRequests) return { refused: `blocks ${from}..${to} need ${ferestre.length} requests of ${window} blocks, above the cap of ${maxRequests} (--max-requests); nothing was asked` };
|
||||
// fiecare fereastra isi tine rezultatul pe pozitia ei, deci jurnalele ies in ordinea lantului oricum ar sosi raspunsurile
|
||||
const peFereastra = new Array(ferestre.length).fill(null), erori = []; let i = 0;
|
||||
const citeste = async (k) => {
|
||||
const [a, b] = ferestre[k];
|
||||
let j; try { j = await apelBrut('eth_getLogs', [{ ...filtru, fromBlock: hex(a), toBlock: hex(b) }]); } catch (e) { j = { transport: String(e.message || e) }; }
|
||||
if (j && Array.isArray(j.result)) { peFereastra[k] = j.result; return; }
|
||||
erori.push([k, `${a}..${b}: ${j && j.error ? String(j.error.message).slice(0, 80) : j && j.transport ? j.transport.slice(0, 80) : 'the result is not a list'}`]);
|
||||
};
|
||||
await Promise.all(Array.from({ length: Math.min(PARALEL, ferestre.length) }, async () => { while (i < ferestre.length) await citeste(i++); }));
|
||||
erori.sort((x, y) => x[0] - y[0]);
|
||||
return { logs: peFereastra.filter(Boolean).flat(), windows: ferestre.length, unreadable: erori.length, first: erori.length ? erori[0][1] : null };
|
||||
}
|
||||
|
||||
export async function verificaPlati(d, { rpc, allTransfers = false, fetchImpl = fetch, window = FEREASTRA_IMPLICITA, maxRequests = CERERI_MAXIME } = {}) {
|
||||
const ethers = incarcaEthers();
|
||||
const rez = []; const ok = (name, pass, detail = '') => { rez.push({ name, pass, detail }); return pass; };
|
||||
const apel = async (method, params) => {
|
||||
const apelBrut = async (method, params) => {
|
||||
const r = await fetchImpl(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
|
||||
const j = await r.json(); if (j.error) throw new Error(`${method}: ${String(j.error.message).slice(0, 80)}`); return j.result;
|
||||
return r.json();
|
||||
};
|
||||
const apel = async (method, params) => {
|
||||
const j = await apelBrut(method, params); if (j.error) throw new Error(`${method}: ${String(j.error.message).slice(0, 80)}`); return j.result;
|
||||
};
|
||||
const chain = Number(String(d.network).split(':')[1]);
|
||||
let lantul;
|
||||
@ -51,11 +89,24 @@ export async function verificaPlati(d, { rpc, allTransfers = false, fetchImpl =
|
||||
&& l.topics[2].toLowerCase() === topicAdresa(a.to) && BigInt(l.data) === BigInt(a.amount)));
|
||||
}
|
||||
if (allTransfers) {
|
||||
const nu = (detail) => rez.push({ name: 'all transfers out of the wallet', pass: null, detail });
|
||||
try {
|
||||
const logs = await apel('eth_getLogs', [{ address: token, fromBlock: '0x' + Number(d.fromBlock || 0).toString(16), toBlock: 'latest', topics: [T_TRANSFER, topicAdresa(wallet)] }]);
|
||||
const straine = logs.filter((l) => !txPlati.has(l.transactionHash.toLowerCase()));
|
||||
ok(`every Transfer out of the wallet since block ${d.fromBlock || 0} is a payment in the ledger (${logs.length} transfers)`, straine.length === 0, straine.map((l) => l.transactionHash.slice(0, 12)).join(', '));
|
||||
} catch (x) { rez.push({ name: 'all transfers out of the wallet', pass: null, detail: x.message }); }
|
||||
const from = Number(d.fromBlock || 0), to = Number(await apel('eth_blockNumber', []));
|
||||
if (!Number.isSafeInteger(from) || from < 0) nu(`the evidence's fromBlock is not a block number (${d.fromBlock})`);
|
||||
else if (!Number.isSafeInteger(to) || to < from) nu(`the RPC's head ${to} is not at or above the evidence's fromBlock ${from}`);
|
||||
else {
|
||||
const r = await jurnalePaginate(apelBrut, { address: token, topics: [T_TRANSFER, topicAdresa(wallet)] }, { from, to, window, maxRequests });
|
||||
if (r.refused) nu(r.refused);
|
||||
else {
|
||||
const straine = r.logs.filter((l) => !txPlati.has(String(l.transactionHash).toLowerCase()));
|
||||
// un Transfer strain gasit e INVALID oricum; altfel, o singura fereastra necitita face verificarea NEMASURAT, nu "goala"
|
||||
if (straine.length || !r.unreadable) {
|
||||
ok(`every Transfer out of the wallet from block ${from} to ${to} is a payment in the ledger (${r.logs.length} transfers, ${r.windows} windows of at most ${window} blocks${r.unreadable ? `, ${r.unreadable} unreadable` : ''})`,
|
||||
straine.length === 0, straine.map((l) => String(l.transactionHash).slice(0, 12)).join(', '));
|
||||
} else nu(`${r.unreadable} of ${r.windows} windows of at most ${window} blocks could not be read and are not counted as empty (first: ${r.first})`);
|
||||
}
|
||||
}
|
||||
} catch (x) { nu(x.message); }
|
||||
}
|
||||
if (d.walletContract) {
|
||||
const wc = d.walletContract; let art = null, asteptat = null;
|
||||
@ -74,10 +125,14 @@ export async function verificaPlati(d, { rpc, allTransfers = false, fetchImpl =
|
||||
|
||||
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
||||
const a = process.argv.slice(2); const get = (f) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : undefined; };
|
||||
const f = a.find((x) => !x.startsWith('--') && x !== get('--rpc'));
|
||||
if (!f || !get('--rpc')) { console.error('usage: node verifica-plati.mjs <evidence.json> --rpc <url> [--all-transfers]'); process.exitCode = 2; }
|
||||
else {
|
||||
const r = await verificaPlati(JSON.parse(fs.readFileSync(f, 'utf8')), { rpc: get('--rpc'), allTransfers: a.includes('--all-transfers') });
|
||||
const valori = ['--rpc', '--window', '--max-requests'].map(get);
|
||||
const f = a.find((x) => !x.startsWith('--') && !valori.includes(x));
|
||||
const intreg = (x, implicit) => x === undefined ? implicit : /^[1-9][0-9]*$/.test(x) ? Number(x) : NaN;
|
||||
const window = intreg(get('--window'), FEREASTRA_IMPLICITA), maxRequests = intreg(get('--max-requests'), CERERI_MAXIME);
|
||||
if (!f || !get('--rpc') || !Number.isSafeInteger(window) || !Number.isSafeInteger(maxRequests)) {
|
||||
console.error('usage: node verifica-plati.mjs <evidence.json> --rpc <url> [--all-transfers [--window <blocks, default 5000>] [--max-requests <n, default 2000>]]'); process.exitCode = 2;
|
||||
} else {
|
||||
const r = await verificaPlati(JSON.parse(fs.readFileSync(f, 'utf8')), { rpc: get('--rpc'), allTransfers: a.includes('--all-transfers'), window, maxRequests });
|
||||
for (const c of r.checks) console.log(` ${c.pass === true ? 'OK ' : c.pass === false ? 'FAIL' : '-- '} ${c.name}${c.detail ? ' (' + c.detail + ')' : ''}`);
|
||||
console.log(r.verdict);
|
||||
process.exitCode = r.verdict === 'VALID' ? 0 : r.verdict === 'INVALID' ? 1 : 2;
|
||||
|
||||
@ -37,9 +37,14 @@ const run = (args, cwd) => { const r = spawnSync(process.execPath, args, { cwd,
|
||||
const rehash = (env) => ({ ...env, statementHash: sha(Buffer.from(JSON.stringify(env.statement), 'utf8')) });
|
||||
const clone = (x) => JSON.parse(JSON.stringify(x));
|
||||
|
||||
// 2026-10-21 (the stranger pass): every laboratory directory is remembered and removed when the process exits, on every path (the end,
|
||||
// an exception, process.exit); until then each run left two aere-ai-trust-lab-* directories in TEMP (22 after one pass with its control)
|
||||
const LABORATOARE = [];
|
||||
process.on('exit', () => { for (const d of LABORATOARE) { try { fs.rmSync(d, { recursive: true, force: true, maxRetries: 3 }); } catch { /* left for the system */ } } });
|
||||
|
||||
/** the laboratory pieces: a model artifact attested, a host that ran it, an agent allowed to use it, an output */
|
||||
export function laborator({ toolAllowed = true } = {}) {
|
||||
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-ai-trust-lab-'));
|
||||
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-ai-trust-lab-')); LABORATOARE.push(T);
|
||||
// version: the committed fixture (the artifact and its hybrid-signed Proof of Software attestation); the digest is recomputed here,
|
||||
// never read from the attestation, so a fixture whose artifact and attestation drifted apart is caught at P1
|
||||
const art = path.join(T, 'model-weights.bin'); fs.copyFileSync(path.join(FIXTURI, 'model-weights.bin'), art); const artSha = sha(fs.readFileSync(art));
|
||||
|
||||
@ -186,8 +186,8 @@ named. Everything that comes from the pack is shown as text.
|
||||
## Tests
|
||||
|
||||
bash probele-b1.sh # all seven below, each VERDE / ROSU / STRICAT (in the development repository only)
|
||||
node proba-plan-migrare.mjs # 30: real certificates and keys (openssl) through the real inventory; the scanner's ids derived from its source
|
||||
node proba-control-plane.mjs # 9: the command line end to end, an all-post-quantum tree gives an empty plan
|
||||
node proba-plan-migrare.mjs # 44: real certificates and keys (openssl) through the real inventory; the scanner's ids derived from its source
|
||||
node proba-control-plane.mjs # 10: the command line end to end, an all-post-quantum tree gives an empty plan
|
||||
node proba-executa-migrare.mjs # 30: on real products started locally (KMS, PQ CA, gateway), with the attacks of the review
|
||||
node proba-remediere.mjs # 33: real TLS servers on 127.0.0.1 scanned by the readiness scanner, before and after the recipe
|
||||
node control-negativ-remediere.mjs # 7 of 7 guards broken in a copy, each turns the test red
|
||||
@ -204,4 +204,6 @@ named. Everything that comes from the pack is shown as text.
|
||||
node control-negativ-organizatie-web.mjs # 20 defects planted in a copy of the page, each turns its named check red
|
||||
|
||||
The report test runs the envelope through the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools>`).
|
||||
The counts above were measured on 2026-10-21 in the development repository and from a fresh clone of the public one (until
|
||||
that day the planner and command-line lines said 30 and 9, counts from before rows were added to those two probes).
|
||||
No third party has reviewed any of this.
|
||||
|
||||
@ -9,7 +9,7 @@ It is a single Node.js script with no dependencies: only built-in `node:*` modul
|
||||
scanned tree is executed, and no network request is made.
|
||||
|
||||
```
|
||||
node tools/crypto-inventory/inventar.mjs scan ./my-service --out cbom.json --summary
|
||||
node inventar.mjs scan ./my-service --out cbom.json --summary # from this folder; from the root of a clone: node crypto-inventory/inventar.mjs ...
|
||||
```
|
||||
|
||||
## What it detects
|
||||
|
||||
@ -244,10 +244,16 @@ node operation-cli.mjs block --rpc <endpoint> --number 6157024 --out block.json
|
||||
node operation-cli.mjs verify --log log.json --now <time> --trust <manufacturer id> --status status.json \
|
||||
--reading reading.json --block block.json [--other records.json] [--json]
|
||||
node operation-cli.mjs facts --log log.json --now <time> ... --out facts.json
|
||||
node operation-cli.mjs reputation --facts facts.json --from 2026-10-01T00:00:00Z --to 2026-11-01T00:00:00Z --window 3600
|
||||
python3 reputation.py --facts facts.json --from <ms> --to <ms> --window 3600 # the second implementation: the same text
|
||||
node operation-cli.mjs reputation --facts facts.json --from 2026-10-01T00:00:00Z --to 2026-11-01T00:00:00Z --window 3600 # indented, to read
|
||||
node operation-cli.mjs reputation --facts facts.json --from 1790812800000 --to 1793491200000 --window 3600 --canonical # canonical JSON
|
||||
python3 reputation.py --facts facts.json --from 1790812800000 --to 1793491200000 --window 3600 # the second implementation
|
||||
```
|
||||
|
||||
The last two lines print the same bytes, the line end included (`cmp` says nothing), on Windows as on Linux: the canonical JSON
|
||||
(keys sorted, no spaces) and one `\n`. Without `--canonical` the first implementation indents its output, so the text differs while
|
||||
the values are the same. `reputation.py` takes the times in milliseconds only; `operation-cli.mjs` takes milliseconds or an RFC 3339
|
||||
UTC time (2026-10-01T00:00:00Z is 1790812800000).
|
||||
|
||||
`notarize` refuses chain 2800 unless `AERE_CONFIRM_MAINNET=yes`: there it is a real transaction paid by the key's account.
|
||||
`reading` needs the AIP-23 reference verifier (`verify-proof.mjs` with the files beside it, `npm install` done there). A file given to
|
||||
`--status`, `--status-list`, `--reading`, `--block` or `--other` holds one object, a JSON list of them, or `{ "records": [...] }`.
|
||||
@ -316,9 +322,9 @@ const rep = reputation(r.facts, { from, to, windowS: 3600 });
|
||||
| what | command | measured |
|
||||
|---|---|---|
|
||||
| the probe | `node proba-operation.mjs` | 107/107 on 2026-10-20 (Node.js 24.14.1, Python 3.14.4, Windows): 18 logs that must be VALID, each with exactly its named causes; 15 PARTIAL, each for exactly its causes; 48 attack rows, each refused for exactly its named rules (four of them also require the facts to be of the formula's form, or to hold no equivocation); the proof of equivocation on its own, and every proof the verifier kept verifying on its own (7); the producers and the separation of purposes (2); the facts and the reputation (8); the two implementations, the vectors and six margins of the JSON text (6); 459 damaged logs each given a verdict, none counting a record the device did not sign; the command line through files only, the blocks read by the `block` command from a local endpoint (2). The 12 rows added by the adversarial review of 2026-10-20 were each red on the code before it, for the reason they name |
|
||||
| the negative control | `node control-negativ-operation.mjs` | 62/62 on 2026-10-20: the untouched copy green; each of the 17 rules taken out of a copy, the rows that expect it, and only they, go red; 44 more plantings, each with exactly its rows: one condition of each compound check at a time (two conditions that guard one thing taken out together, and said so), the bounds and the counting of the facts, the reading of the registry and of the records beside the log, and both implementations of the reputation (their check, their form, their formula, and the Python reader of the JSON text); each defect the review of 2026-10-20 found is put back by one planting. A planting whose pattern is not found exactly once, or after which the probe does not reach its summary, counts as a failure |
|
||||
| the negative control | `node control-negativ-operation.mjs` | 63/63 on 2026-10-21 (62/62 on 2026-10-20): the untouched copy green; each of the 17 rules taken out of a copy, the rows that expect it, and only they, go red; 45 more plantings (the 45th, since 2026-10-21: `reputation.py` made to write CRLF, which turns C01, and only it, red), each with exactly its rows: one condition of each compound check at a time (two conditions that guard one thing taken out together, and said so), the bounds and the counting of the facts, the reading of the registry and of the records beside the log, and both implementations of the reputation (their check, their form, their formula, and the Python reader of the JSON text); each defect the review of 2026-10-20 found is put back by one planting. A planting whose pattern is not found exactly once, or after which the probe does not reach its summary, counts as a failure |
|
||||
| the vectors | `node genereaza-vectori.mjs --verifica` | 103 vectors in the form of the conformance suite (`vectori/`), derived from logs the producers made once (`fixturi/sursa-vectori.json`: eight scenarios, every key discarded): 56 VALID (eight logs, seven windowings), 19 INVALID `reputation.figures` and 28 INVALID `reputation.form`, each one operation away from a VALID one and checked at generation to be refused for exactly its rule. `LA ZI` on 2026-10-20. Two of the windowings (`quarter`, `cut`) were added that day, when the suite enrolled the reputation: until then no vector told `<` from `<=` at the end of a window or at `to` (an implementation with `<=` at either passed all 87); now they fail 6 and 4 |
|
||||
| two implementations | `node ../conformance/conformance.mjs --vectors vectori --impl "node reputation.mjs"` and `--impl "python3 reputation.py"` | 103/103 each on 2026-10-20; and in the probe, byte for byte the same text on 249 computations over the facts of every row, and the same verdict on six margins of the JSON text that a vector written by `JSON.stringify` cannot carry (a whole number of 5,000 digits in the claim and in the facts, a claim nested 50,000 deep, 2^53 + 1, 1e-400, a member written twice) |
|
||||
| two implementations | `node ../conformance/conformance.mjs --vectors vectori --impl "node reputation.mjs"` and `--impl "python3 reputation.py"` | 103/103 each on 2026-10-20; and in the probe, byte for byte the same text on 249 computations over the facts of every row, the two command lines byte for byte, line end included (row C01, since 2026-10-21: until then Python wrote CRLF on Windows and the row compared trimmed text; 107/107 on 2026-10-21 on Windows, and the control's planting of the CRLF turns C01, and only it, red), and the same verdict on six margins of the JSON text that a vector written by `JSON.stringify` cannot carry (a whole number of 5,000 digits in the claim and in the facts, a claim nested 50,000 deep, 2^53 + 1, 1e-400, a member written twice) |
|
||||
| the conformance suite | `node ../conformance/proba-conformance.mjs` | since 2026-10-20 the verifier (17 rules, `device-operation-log`) and the reputation (2 rules, `device-reputation`) are enrolled there, each rule planted out of a copy of this component's file by the suite's own control; see that README for what it measured |
|
||||
| cost | (measurement, not a gate) | a log of 10,000 records with 1,000 anchors, judged with 100,000 block readings: 4.1 s, 294 MB resident; 2,000 records: 1.0 s. The reputation of 10,000 records in 10,000 windows: 66 ms. Measured before the review of 2026-10-20 and not again after it |
|
||||
|
||||
|
||||
@ -71,6 +71,8 @@ export const PLANTARI = [
|
||||
['reputation (Python): a whole number read digit by digit (int() refuses more than 4300 digits)', PY, 'val, i = num(m.group()), m.end()', "val, i = (int(m.group()) if m.group().lstrip('-').isdigit() else num(m.group())), m.end()", ['R14']],
|
||||
['reputation (Python): the files read with json.loads (one recursion per level)', PY, 'return loads(f.read())', 'return json.loads(f.read(), parse_float=num)', ['R14']],
|
||||
['reputation (Python): a claim nested deeper than the encoder goes not caught', PY, 'except (ValueError, TypeError, OverflowError, RecursionError):', 'except (ValueError, TypeError, OverflowError):', ['R14']],
|
||||
// 2026-10-21 (the stranger pass): the line end Python writes on Windows, planted on every system (the default would differ only there)
|
||||
['reputation (Python): the command line writes CRLF, as Python does on Windows by default', PY, "sys.stdout.reconfigure(newline='\\n')", "sys.stdout.reconfigure(newline='\\r\\n')", ['C01']],
|
||||
];
|
||||
|
||||
const a = process.argv.slice(2); const doar = a.includes('--doar') ? a[a.indexOf('--doar') + 1] : null;
|
||||
|
||||
@ -330,9 +330,9 @@ spune('9. the command line, through files only');
|
||||
const js = oc(['reputation', '--facts', f('facts.json'), '--from', from, '--to', to, '--window', '60', '--canonical']);
|
||||
const pyr = py ? spawnSync(py, [path.join(AICI, 'reputation.py'), '--facts', f('facts.json'), '--from', from, '--to', to, '--window', '60'], { encoding: 'utf8' }) : null;
|
||||
const rep = js.status === 0 ? JSON.parse(js.stdout) : null;
|
||||
rand('C01', 'two device keys, two credentials, ten records with a rotation, an anchor, and the blocks read by the block command from a local endpoint, all through files: verify says VALID (exit 0), 10 of 10 minutes covered and 1 witnessed, and the reputation is the same text from both implementations',
|
||||
!py ? 'NEMASURAT' : facuti && ver.status === 0 && /^VALID/m.test(ver.stdout) && !!rep && rep.availability.claimed === 10 && rep.availability.windows === 10 && rep.availability.witnessed === 1 && pyr.status === 0 && pyr.stdout.trim() === js.stdout.trim() ? 'VERDE' : 'ROSU',
|
||||
`steps ${pasi.map((x) => x.status).join('')}, verify ${ver.status}: ${String(ver.stdout).trim().split('\n').slice(-3).join(' | ')} ${String(ver.stderr).slice(0, 200)}` + (rep ? ' ' + JSON.stringify(rep.availability) : ''));
|
||||
rand('C01', 'two device keys, two credentials, ten records with a rotation, an anchor, and the blocks read by the block command from a local endpoint, all through files: verify says VALID (exit 0), 10 of 10 minutes covered and 1 witnessed, and the reputation is the same BYTES from both command lines (`operation-cli.mjs reputation --canonical` and `reputation.py --facts`, the line end included: before 2026-10-21 Python wrote CRLF on Windows and this row compared trimmed text)',
|
||||
!py ? 'NEMASURAT' : facuti && ver.status === 0 && /^VALID/m.test(ver.stdout) && !!rep && rep.availability.claimed === 10 && rep.availability.windows === 10 && rep.availability.witnessed === 1 && pyr.status === 0 && pyr.stdout === js.stdout ? 'VERDE' : 'ROSU',
|
||||
`steps ${pasi.map((x) => x.status).join('')}, verify ${ver.status}: ${String(ver.stdout).trim().split('\n').slice(-3).join(' | ')} ${String(ver.stderr).slice(0, 200)}` + (rep ? ' ' + JSON.stringify(rep.availability) : '') + (pyr && pyr.stdout !== js.stdout ? ` | the two outputs differ: JS ${Buffer.byteLength(js.stdout)} bytes ending ${JSON.stringify(js.stdout.slice(-2))}, Python ${Buffer.byteLength(pyr.stdout)} bytes ending ${JSON.stringify(pyr.stdout.slice(-2))}` : ''));
|
||||
const Lf = JSON.parse(fs.readFileSync(f('log.json'), 'utf8')); Lf.records[3].statement.counter = 999; fs.writeFileSync(f('tampered.json'), JSON.stringify(Lf));
|
||||
const t1 = oc(['verify', ...comun.map((x) => (x === f('log.json') ? f('tampered.json') : x))]), t2 = oc(['verify', ...comun.filter((x, i, a) => x !== '--status' && a[i - 1] !== '--status')]), t3 = oc(['verify', '--log', f('log.json')]);
|
||||
const t4 = oc(['verify', '--log', f('log.json'), '--now', 'yesterday']);
|
||||
|
||||
@ -313,6 +313,10 @@ def main(a):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 2026-10-21 (the stranger pass): the same BYTES as `operation-cli.mjs reputation --canonical` on every system. Python's text-mode
|
||||
# stdout turns '\n' into '\r\n' on Windows, Node's console.log writes '\n' everywhere; without this the two outputs differ by the
|
||||
# line ending on Windows, and only there.
|
||||
sys.stdout.reconfigure(newline='\n')
|
||||
try:
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
except (ValueError, OSError, KeyError, TypeError) as e:
|
||||
|
||||
@ -33,9 +33,19 @@ claim with it. Until 2026-10-03 the time was taken from the claim. Without a cer
|
||||
AERE_TESTNET_KEY_FILE=<key> AERE_ETHERS_DIR=<dir with node_modules/ethers> node proba-piata-28001.mjs # the flow on 28001 (five scenarios)
|
||||
node proba-judecata.mjs # the judge on the recorded timelines: 7 honest VALID, 18 forged or malformed INVALID, the receipt
|
||||
node proba-judecata.mjs --control-negativ # 10 plantings on a copy of piata.mjs; each forged fixture must then be accepted
|
||||
AERE_ETHERS_DIR=<dir with node_modules/ethers> node proba-cronologie-citire.mjs # reads only: the sender of every recorded event, read raw
|
||||
node extrage-artefact.mjs # regenerate AereComputeMarketV3.json from the contracts build (refuses a changed source)
|
||||
```
|
||||
|
||||
`proba-cronologie-citire.mjs` (2026-10-20) checks the change of that day: `piata.mjs` takes the sender of an event from the RAW
|
||||
transaction (`eth_getTransactionByHash`), because ethers does not decode the post-quantum transaction type 0x50 (AIP-20, live on 28001).
|
||||
For every event of the newest evidence file in `dovezi/` (written with the form before, which asked ethers), the sender read raw now
|
||||
must be exactly the recorded one. Measured on 2026-10-21 from a fresh clone of this repository, reading testnet 28001: 19 distinct
|
||||
transactions, 21 events, every sender equal, exit 0. Its control row, a real 0x50 transaction of 28001 that ethers refuses and the raw
|
||||
form reads, looks for one in the last 400 blocks; that day it found none and said `NEMASURAT` on that row (the exit code does not count
|
||||
it), so the raw reading of a 0x50 transaction was not measured by this run. The whole timeline is not read again: a query from the
|
||||
job's block to the head passes the range limit of the testnet's endpoint.
|
||||
|
||||
## What it is not, and what is not measured
|
||||
|
||||
- **Not a marketplace.** No discovery, no pricing, no reputation, no real GPU, storage or inference behind the workload; the task is a
|
||||
|
||||
@ -279,10 +279,13 @@ one defect at a time (KDF without the ML-KEM secret, KDF without the X25519 secr
|
||||
accepts one signature, ignored aad, ignored minimum version, unchained audit writer, verifier that skips
|
||||
the chain, verifier that skips row MACs, private key written in clear, key file MAC not checked, root
|
||||
key generated silently, fingerprint not checked, rewrap returning plaintext, missing auth accepted, body
|
||||
limit ignored), and requires the named tests to fail with the named message. It reads the suite's JSON
|
||||
output, not its exit code, and checks that the original files are byte-identical afterwards.
|
||||
limit ignored, a mutation kept on disk when its audit row cannot be written), and requires the named tests
|
||||
to fail with the named message. It reads the suite's JSON output, not its exit code, and checks that the
|
||||
original files are byte-identical afterwards.
|
||||
|
||||
Measured on 2026-09-25, Windows 11, Node 24.14.1, OpenSSL 3.5.5: 61/61 tests pass; the negative control
|
||||
catches 15/15 plantings; the control's self-test gives 5/5 decoys their failure verdict. Not measured:
|
||||
Measured on 2026-10-21, Windows 11, Node 24.14.1, OpenSSL 3.5.5, from a fresh clone of the public
|
||||
repository: 62/62 tests pass; the negative control catches 16/16 plantings; the control's self-test gives
|
||||
5/5 decoys their failure verdict. (On 2026-09-25 this paragraph said 61/61 and 15/15: the test and the
|
||||
planting for the audit rollback were added that day and the figures were not brought along.) Not measured:
|
||||
Linux or macOS (including whether the `0o600`/`0o700` file modes are applied; Windows ignores them),
|
||||
throughput under load, behaviour with concurrent processes, side channels.
|
||||
|
||||
@ -9,7 +9,7 @@ import { buildProof, KINDS, SCHEME, AGENT_WITNESS_CHECKPOINT } from './proof-kin
|
||||
|
||||
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
||||
const VERIFY = process.env.AERE_VERIFY_PROOF ? path.resolve(process.env.AERE_VERIFY_PROOF) : path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs');
|
||||
if (!fs.existsSync(VERIFY)) { console.log(`NEMASURAT: verificatorul AIP-23 nu e la ${VERIFY}; dati AERE_VERIFY_PROOF (de ex. verify-proof.mjs din aere-node/tools)`); process.exit(2); }
|
||||
if (!fs.existsSync(VERIFY)) { console.log(`NEMASURAT: verificatorul AIP-23 nu e la ${VERIFY}; dati AERE_VERIFY_PROOF (verify-proof.mjs servit la aere.network/tools, cu fisierele de langa el si npm install facut acolo; vezi README.md)`); process.exit(2); }
|
||||
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-'));
|
||||
let rele = 0; const cer = (n, c) => { console.log(` [${c ? 'OK ' : 'RAU '}] ${n}`); if (!c) rele++; };
|
||||
function verdict(plicPath) { try { return JSON.parse(execFileSync(process.execPath, [VERIFY, plicPath, '--json'], { encoding: 'utf8' })).verdict; } catch (e) { try { return JSON.parse((e.stdout || '')).verdict; } catch { return '?'; } } }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user