From a654c3d5222f82abef2aa698e5eb05f684aae4f0 Mon Sep 17 00:00:00 2001 From: Aere Network Date: Tue, 20 Oct 2026 01:04:12 +0300 Subject: [PATCH] agents: the hosted witness for agent ledgers (sandbox on testnet 28001), its probe and negative control; crypto-inventory 0.4.1 resolves a minimum TLS version given through a constant or a ternary; proof-kinds and conformance gain the agent-witness-checkpoint kind; the reproduction scripts of the Shield recipes no longer carry a workstation path --- CITATIONS-UNRESOLVED.md | 4 +- README.md | 12 +- agents/README.md | 325 ++- agents/agent-cli.mjs | 197 +- agents/control-negativ-martor-agenti.mjs | 325 +++ .../martor-agenti-2026-10-19T05-35-18.json | 385 ++++ agents/martor-agenti-serviciu.mjs | 245 +++ agents/martor-agenti.mjs | 1129 +++++++++++ agents/proba-martor-agenti.mjs | 1756 +++++++++++++++++ conformance/README.md | 18 +- conformance/genereaza-vectori.mjs | 4 +- conformance/vectori/INDEX.json | 29 +- .../agent-witness-checkpoint.form.json | 53 + .../agent-witness-checkpoint.integrity.json | 54 + .../agent-witness-checkpoint.valid.json | 44 + .../retete-2026-10-18/scaneaza-local.mjs | 14 +- crypto-inventory/README.md | 9 +- crypto-inventory/lib/context.mjs | 21 + crypto-inventory/lib/detect-js.mjs | 28 +- crypto-inventory/lib/scan.mjs | 2 +- crypto-inventory/test/control-negativ.mjs | 24 + crypto-inventory/test/echivalenta-0.1.0.mjs | 6 +- crypto-inventory/test/fixturi/js/tls.cjs | 15 +- crypto-inventory/test/proba.mjs | 3 +- proof-kinds/README.md | 23 +- proof-kinds/proba-proof-kinds.mjs | 26 +- proof-kinds/proof-kinds.mjs | 29 + 27 files changed, 4737 insertions(+), 43 deletions(-) create mode 100644 agents/control-negativ-martor-agenti.mjs create mode 100644 agents/dovezi-martor-agenti-28001/martor-agenti-2026-10-19T05-35-18.json create mode 100644 agents/martor-agenti-serviciu.mjs create mode 100644 agents/martor-agenti.mjs create mode 100644 agents/proba-martor-agenti.mjs create mode 100644 conformance/vectori/envelope/agent-witness-checkpoint.form.json create mode 100644 conformance/vectori/envelope/agent-witness-checkpoint.integrity.json create mode 100644 conformance/vectori/envelope/agent-witness-checkpoint.valid.json diff --git a/CITATIONS-UNRESOLVED.md b/CITATIONS-UNRESOLVED.md index 1ff1453..c935ddf 100644 --- a/CITATIONS-UNRESOLVED.md +++ b/CITATIONS-UNRESOLVED.md @@ -12,13 +12,14 @@ wrote it. Paths that DO resolve are written relative to a side-by-side checkout of the Aere repositories, so they begin with a repository name, for example `aere-contracts/contracts/pqc/AereCryptoRegistry.sol`. -Unresolvable distinct paths in this repository: **19**. +Unresolvable distinct paths in this repository: **21**. - `ContainerApps_ListBySubscription.json` cited in: aere-quantum/verify-layer/fixturi-cloud/README.md - `VirtualMachine_List_MaximumSet_Gen.json` cited in: aere-quantum/verify-layer/fixturi-cloud/README.md - `addresses.txt` cited in: aere-quantum/exposure-report/README.md - `antete.mjs` cited in: aere-quantum/exposure-report/README.md - `btc/antete.mjs` cited in: aere-quantum/exposure-report/README.md +- `ciob.json` cited in: aere-quantum/agents/README.md - `data/audit.log` cited in: aere-quantum/pq-kms/README.md - `data/root-check.json` cited in: aere-quantum/pq-kms/README.md - `data/service.json` cited in: aere-quantum/pq-secrets/README.md @@ -27,6 +28,7 @@ Unresolvable distinct paths in this repository: **19**. - `libs.versions.toml` cited in: aere-quantum/crypto-inventory/README.md - `plic-aip23.json` cited in: aere-quantum/exposure-report/README.md - `proba-client-real.mjs` cited in: aere-quantum/readiness/README.md +- `probele-b2.sh` cited in: aere-quantum/agents/README.md - `raport.json` cited in: aere-quantum/exposure-report/README.md - `raport.md` cited in: aere-quantum/exposure-report/README.md - `service.json` cited in: aere-quantum/pq-secrets/README.md diff --git a/README.md b/README.md index c950030..d4dce71 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ notarization command of the verification layer and for the agents' x402 wallet ( | [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL | | [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Kotlin, Swift, Go, Rust, PEM blocks, dependency manifests including Cargo.toml, Package.swift, Podfile and Gradle): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow | | [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and from 2026-10-18 the same for what the operator's own cloud CLI exports (AWS ECS, EC2, Lambda; Azure VMs, Container Instances, Container Apps, Web Apps; GCP Compute Engine, Cloud Run), read from the export file and never from an account, and the head of the chain can be signed by the operator (ML-DSA-65) and notarized on Aere Network under a post-quantum validator certificate; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth); the same log as a Merkle tree (RFC 9162): signed tree heads, and inclusion and consistency proofs of a few hashes that anyone checks without the log | -| [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fifteen proof kinds (`secret-access` since 2026-10-18), one envelope format, digests instead of raw content | +| [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: sixteen proof kinds (`secret-access` since 2026-10-18, `agent-witness-checkpoint` since 2026-10-19), one envelope format, digests instead of raw content | | [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass | | [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again | -| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, either holding the payment key for the owner or co-signing from a 2-of-2 contract wallet that neither the agent nor the owner can spend alone; both run on the public testnet with their evidence; and the chain as the witness of a ledger: a notarized head, read through the AIP-23 verifier under a post-quantum certified anchor, bounds entry times from below (a backdated entry is caught) | +| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, either holding the payment key for the owner or co-signing from a 2-of-2 contract wallet that neither the agent nor the owner can spend alone; both run on the public testnet with their evidence; and the chain as the witness of a ledger: a notarized head, read through the AIP-23 verifier under a post-quantum certified anchor, bounds entry times from below (a backdated entry is caught); and a hosted witness (sandbox on testnet 28001; mainnet at activation): the agent sends its ledger head signed with its ML-DSA-65 key, never the ledger, the witness checks it continues the last head it holds (a conflicting head is a proof of equivocation, kept and returned), logs it in an RFC 9162 tree whose checkpoints it signs and notarizes, and returns a receipt with an inclusion proof that anyone checks without the witness and that the ledger verifier takes as an anchor | | [`proof-of-ai/`](proof-of-ai/) | the AIP-23 envelope of an AI agent's action: model, version, weights digest, prompt, input, output, tools, agent, requester, as digests; verified by the same AIP-23 verifier as every other proof | | [`payments/`](payments/) | the risk step and the receipt of a payment for a person or a company: a declared policy (per-payment and sliding-window limits in USD per payer, assets allowed, counterparties refused with their source) gives a verdict anyone reproduces from the same inputs, with the signals it does not have named as unmeasured (no sanctions list, no identity tier); the receipt is an instruction (payer, payee, the amount in the asset and in USD at the rate declared as an input, the transaction, the verdict with its policy under hash) committed to by an AIP-23 settlement envelope, notarized and read back under a post-quantum certified anchor; a reader checks it against the policy, the inputs and the chain with plain JSON-RPC | | [`market-settlement/`](market-settlement/) | the dispute and settlement rail of a verifiable infrastructure market, measured end to end on testnet 28001: an order with its reward in escrow and a provider bond, the result committed on chain with its execution envelope notarized under a post-quantum certified anchor, a challenge window in which anyone disputes with a recomputed result and a bond, an arbiter that can only route the escrow (with a permissionless 14-day timeout), deadline reclaims, and an AIP-23 settlement receipt; a pure judge over the job's timeline that checks deadlines, the window, who resolved, the payout, the replay and the proof | @@ -41,13 +41,13 @@ 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-18 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); cost on hostile input 13/13 linear (`node test/proba-timp.mjs`) | 35/35 (`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 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) | | verify-layer | 45/45 with the AIP-23 reference verifier, measured from a fresh clone on 2026-10-18 (`AERE_VERIFY_PROOF= 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 | 28/28 on 2026-10-18 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`: fifteen kinds, the allowed values of the `secret-access` fields included) | seven negative controls inside the test | +| proof-kinds | 30/30 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 included) | eight 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; 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`) | | 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); 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 `) | 28/28 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.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 71/71 without a network, with a local notary mock (`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 on 2026-10-19 (`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 `) | 28/28 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); the hosted witness 24/24 (`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 | | 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) | @@ -64,4 +64,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ... ## Licence -MIT, see [LICENSE](LICENSE). Files: 967 (pq-gateway 9, pq-kms 10, pq-secrets 6, pq-pki 6, crypto-inventory 73, verify-layer 56, proof-kinds 3, control-plane 27, agents 39, identity 24, proof-of-ai 3, ai-trust 6, verifiable-data 7, market-settlement 9, payments 8, conformance 643, device-identity 11, exposure-report 18, readiness 4). +MIT, see [LICENSE](LICENSE). Files: 975 (pq-gateway 9, pq-kms 10, pq-secrets 6, pq-pki 6, crypto-inventory 73, verify-layer 56, proof-kinds 3, control-plane 27, agents 44, identity 24, proof-of-ai 3, ai-trust 6, verifiable-data 7, market-settlement 9, payments 8, conformance 646, device-identity 11, exposure-report 18, readiness 4). diff --git a/agents/README.md b/agents/README.md index 3b09682..c3b51e2 100644 --- a/agents/README.md +++ b/agents/README.md @@ -6,7 +6,8 @@ actions need human approval, who may revoke the agent), and a ledger in which ev policy, signed by the agent and chained. A verifier that does not trust the agent re-runs the policy over the whole ledger and says whether any allowed action broke it. Humans approve and revoke with their own ML-DSA-65 keys. -Node.js 24 only (`node:crypto` with ML-DSA), no dependencies, no network. It moves no money: `amount` is the unit the policy names; +Node.js 24 only (`node:crypto` with ML-DSA), no dependencies, no network (except the chain witnesses below, which read a chain, and the +hosted witness, which is a service). It moves no money: `amount` is the unit the policy names; moving value is the job of a payment layer, which can ask for this ledger as evidence that an action was allowed. ## Quick start (command line) @@ -18,7 +19,7 @@ node agent-cli.mjs policy --spec spec.json --out policy.json node agent-cli.mjs record --key agent/agent.key.pem --policy policy.json --ledger ledger.json --action pay.json node agent-cli.mjs approve --key alice/human.key.pem --policy policy.json --action pay.json --out approval.json node agent-cli.mjs record ... --approvals approval.json,approval2.json -node agent-cli.mjs verify --ledger ledger.json --policy policy.json [--revocations r.json] [--anchors a.json] [--not-before ] +node agent-cli.mjs verify --ledger ledger.json --policy policy.json [--revocations r.json] [--anchors a.json] [--not-before ] [--witness ] node agent-cli.mjs revoke --key owner/human.key.pem --policy policy.json --out revocation.json node agent-cli.mjs equivocation --a ledger-a.json --b ledger-b.json --out proof.json node agent-cli.mjs verify-equivocation --proof proof.json @@ -84,6 +85,322 @@ proves nothing. `anchors` answers not measured (exit 2) while the finality is pe anchor it did not certify. `notarize` reads the chain id from the endpoint and refuses chain 2800 (a real transaction paid by the key's account) unless `AERE_CONFIRM_MAINNET=yes`. +## A hosted witness (2026-10-19) + +**Sandbox: testnet 28001; mainnet: at activation.** Every answer of the service says so in its `sandbox` field. + +The section above asks the agent's owner to run a notary transaction, a funded wallet and the AIP-23 verifier. The hosted witness takes the +first two off the owner: the agent sends its ledger head, signed with its ML-DSA-65 key, and gets back a receipt that anyone can check +without the witness. `martor-agenti.mjs` is the format, the judge and the client; `martor-agenti-serviciu.mjs` is the service. + +The form is the one transparency logs already use: + +- The head is a checkpoint of the agent's log, accepted the way a C2SP tlog-witness accepts a log's checkpoint (`add-checkpoint`): it + must be signed by the log's key (here the agent's ML-DSA-65 key, from which the agent id is derived), it must name as its predecessor + exactly the last head the witness holds for that agent and policy (`previous`, `null` for the first; otherwise 409 with that head, as + C2SP answers 409 with its latest size), and it is written to disk before the answer. The ledger is a hash chain, not a Merkle tree, so + a consistency proof between two heads would need the entries, that is the actions, which the witness never sees. The agent signs its + predecessor instead, and the descent itself is checked by whoever holds the ledger: every witnessed head must be an entry of the ledger + shown. +- Two heads signed by the agent that give one position two hashes (the same `seq`, or a `previous` against a head already witnessed, also + when the new head repeats a head the witness holds) are a proof of equivocation (`aere-agent-head-equivocation`): the witness refuses the + head and returns the proof in the 409. It keeps one proof per agent and policy (one proof is enough to show the equivocation; a further one + is returned, `kept: false`, not stored). A kept proof belongs to the shard of the head it contradicts and leaves with it, so every kept proof + has a session of its own with a head in a kept shard: there are at most as many as heads kept (40,000), and that bound is the cap, so nobody + can fill the store to push out, or keep out, another agent's proof. Until the second review (2026-10-19, R2-02) the cap was 2,000 in all, and + anyone could fill it with sessions of their own (two requests per session), after which a real equivocation of another agent was returned + but not kept, and `verify --witness-url` showed that agent clean. The one proof of a session is the one that lives longest: a new proof + whose contradicted head sits in a NEWER shard than the head of the kept proof takes its place (`replaced` names the proof it took the place + of, which then answers 404); until the third review (R3-02) an agent could fill the place of its session with an equivocation against a head + of the old shard, so that the equivocation that mattered was returned not kept, and after the next rotation the session had no proof left. + `/health` counts the proofs returned and not kept + (`equivocationsNotKept`: `onePerSession`, `cap`). `GET .../heads` names the kept proofs by id, and `GET .../equivocation/` returns one. + Anyone checks a proof with the public key inside (`verify-equivocation`). +- The accepted heads are the leaves of an RFC 9162 log, as in Certificate Transparency and Sigstore Rekor. On receipt the witness + returns a promise signed with its ML-DSA-65 key that it saw the head at the receive time (the role of an SCT, or of Rekor's signed + entry timestamp; an AIP-23 `time` envelope). Within the maximum merge delay (RFC 9162's MMD; 1800 s here, checkpoints every 10 + minutes) it signs a checkpoint (the log's name, its size and root, as in C2SP tlog-checkpoint; an AIP-23 envelope of kind + `aere-proof-of-agent-witness-checkpoint`, registered in `proof-kinds/` with a vocabulary for each field) and notarizes it on AereNotary, so + the checkpoint's time is the chain's, certified post-quantum through the AIP-23 verifier, not the witness's word. Consecutive checkpoints + name each other and come with RFC 9162 consistency proofs. +- The log is **sharded**, the way Rekor and Certificate Transparency shard theirs: a shard holds at most 20,000 heads; when it is full the + witness signs its final checkpoint and starts the next shard, under a new log name (`aere-agent-witness/28001/`, the first shard keeps + the plain name), whose first checkpoint names the final checkpoint of the shard before. The witness keeps the current shard and the one + before; an older shard is removed from its disk and memory, but only once its final checkpoint is notarized: while it is not, a head that + would make the log rotate gets 503 `log_busy` with `code: notarization_behind` (and `Retry-After`), and the heads of that shard stay served + (until the second review, R2-07, the shard was removed anyway, and heads answered 202 with a `mergeBy` were left without a certified time). + A new shard is made whole under a temporary name and then renamed (R2-08: a write that failed between the folder and its `ciob.json`, then a + restart, used to lose the link between the shards for good). A shard to remove is first renamed (`.sters--*`, an atomic rename) and only + then deleted, in the background; the next open deletes whatever carries that name, and a shard older than the two kept that lacks its + `ciob.json` or its `frunze.ndjson` is taken as the rest of a removal, not as a shard (third review, R3-01: a stop during the background + deletion used to leave a rest that the opening kept as an unnotarized shard, empty, which no notarization could ever settle, so every new head + got 503 `notarization_behind` for good, or, with checkpoints left in it, the witness refused to start). Until 2026-10-19 a full log answered 503 to everyone, for good, and anyone + could fill it. +- The receipt is that checkpoint, with the head received, its position, the receive time and the RFC 9162 inclusion proof (the role of + Rekor's `verification.inclusionProof`). It is `VALID` for the AIP-23 reference verifier: form, integrity, the witness's ML-DSA-65 + signature and, once a post-quantum anchor covers the notarization, finality. + +| route (behind the gateway; the service listens on 127.0.0.1) | what it does | +|---|---| +| `POST /v1/agents/witness` | a signed head (`martor-agenti.mjs request`). 202 accepted, with the signed promise; 200 already seen; 409 `equivocation` (the proof, and whether it was kept), `stale_head` (older than the last head witnessed) or `not_successor` (with the last head); 400, 403, 422 a refusal that names its rule (`continut`: any field besides the head and its signature, so a ledger's content is never taken; `semnatura`, `cheie`, `identitate`, `sesiune`, `integritate`, `timp`: a head declared more than 300 s from the witness clock); 413 above 16 KB; 429 above 60 heads per client (an IPv6 client counts by its /64) or 30 changes per agent in 10 minutes (the per-agent count takes only requests whose signature verifies AND that change the state, a new head or a newly kept proof: neither a forged request, nor a replay of the agent's public head, nor a head refused with 503 uses up its quota); 503 `log_busy` above 833 new heads per hour from everyone (20,000 / 24, so a shard never fills in less than a day), or with `code: notarization_behind` while a rotation would remove a shard whose final checkpoint is not notarized, both with `Retry-After` (also through the gateway). The gateway refuses a POST that carries an `Origin` header (a web page, through the open CORS of `/v1/`): a head is sent by the agent, not by a browser | +| `GET /v1/agents/witness/` | the state of a head (its id is the statementHash of the head): `pending`, `checkpointed` (with an unnotarized receipt) or `notarized` (with the receipt); 404 for a head the witness does not hold, also one of a shard it no longer keeps | +| `GET /v1/agents/witness//heads?session=[&from=&limit=<1..100>]` | one page of the witnessed heads of an agent under one policy, in order, with `next` (the seq to ask from, or null), the latest head and the ids of the kept equivocation proofs, and `signedPage`: the same page (each head `{seq, hash, id}`, `next`, `count`, `latest`, the kept proofs) with its time, signed by the witness with its ML-DSA-65 key; nothing is read from disk | +| `GET /v1/agents/witness//heads[?after=]` | the sessions (policies) of an agent, at most 100 per page, each with its number of heads, its latest head and its number of kept proofs (not signed) | +| `GET /v1/agents/witness/equivocation/` | one kept proof of equivocation | +| `GET /v1/agents/witness/checkpoint[?shard=][&from=]` | the latest checkpoint of the current shard, or of the kept shard `k` (for a frozen shard: its final checkpoint), and the latest notarized one, with a consistency proof in that shard from `from`, and the shards kept; a monitor that held a checkpoint of a shard that has since been frozen asks `?shard=&from=` (until the second review, R2-09, only the current shard was served) | +| `GET /v1/agents/witness/health` | the witness id, the log and the current shard, `leaves` and `maxLeaves`, the counts (also the proofs not kept, and whether the rotation waits for a notarization), the last checkpoint and the last notarization (a failed notarization shows a code, such as `unfunded`, never its message), and the rate limiters (`limiters`: keys held, the cap, keys dropped at the cap) | + +The Aere Cloud route `/v1/agents/witness` is not open yet; until it is, a witness can be run by anyone (below), and the commands are the +same with its address. The id of the hosted witness is published when the route opens; a verifier pins it. + +``` +node martor-agenti.mjs submit --key agent/agent.key.pem --ledger ledger.json --url # previous = the witness's last head of this agent + # nothing new to send: exit 0, no request (2 if the + # witness then does not give the state of that head); + # a 200 or 202 that is not the state of the head sent: 2 +node martor-agenti.mjs receipt --url --id 0x --ledger ledger.json [--witness aere-witness:] --out receipt.json + # exit 0 once notarized, 2 before (and on 503, + # 429, or a 200 that is not JSON); writes the + # answer ONLY if it is the receipt of that head of + # that ledger and the witness signature verifies: + # else 2, the reason named, nothing written +node agent-cli.mjs verify --ledger ledger.json --policy policy.json --anchors receipt.json \ + --rpc https://testnet-rpc.aere.network --chain 28001 --verify-proof \ + --witness aere-witness: [--witness-url ] +node martor-agenti.mjs verify-receipt --receipt receipt.json --ledger ledger.json [--witness ] # the local checks alone, no network; + # a pending answer: 2; an empty --witness: wrong usage, 2 +node martor-agenti.mjs verify-equivocation --proof proof.json +node martor-agenti.mjs request --key agent/agent.key.pem --ledger ledger.json --previous |none --out head.json # the signed head, to POST yourself +``` + +`receipt` writes what the agent will hand over in `--anchors`, and what the agent hands over is its own responsibility: a receipt it hands over +that is tied to another ledger, or whose witness signature does not verify, is a fault of the ledger (below). So `receipt` writes the answer (to +`--out`, or to the standard output) only once it has checked it here: the receipt of the head asked (`--id`), a head of the ledger given +(`--ledger`: the same agent, policy, seq and hash, and its predecessor), the witness signature verified with the witness pinned by `--witness` when it +is given, otherwise with the key the receipt names (and the message then says the witness is not pinned), and the other local checks of +`verify-receipt`. Anything else is exit 2 with the reason named, and nothing is written; `--ledger` is required. So the documented flow (submit, +`receipt --out`, `verify --anchors`) cannot produce a file that comes out FAILED because of the address it read from (until the sixth review, N2, +`receipt` wrote whatever the address answered, and a proxy without any key at the agent's `--url` could give an honest agent the true receipt of +another agent's head, or one with a broken signature, which then came out FAILED when handed over). + +`verify --anchors` takes a receipt (or the answer of `GET /v1/agents/witness/` as it is) and accepts it as an anchor only after +checking it here: the witness's signature on the checkpoint, that the witness is one the verifier pinned (`--witness`, repeatable), the +agent's signature on the head, the leaf recomputed from the head and the receive time, the inclusion proof up to the checkpoint's root, the +head and its predecessor as entries of the ledger shown, and then the checkpoint judged by the AIP-23 verifier on the chain (`--verify-proof`, +`--rpc`): the witness's signature and the post-quantum finality `PASSED`, whose first-seen time F must not be before the receive time +(beyond 300 s) nor later than the maximum merge delay (the witness's and the verifier's, `--max-merge-delay`, 1800 s by default). +`--chain` and `--max-merge-delay` must be positive integers, and `AERE_WITNESS_WAIT_MAX_S` (below) a whole number of seconds; anything else is +wrong usage (exit 2). A receipt without `--rpc`, in `--anchors` or taken from the address, is a NOT-MEASURED piece with the rule `rpc`, judged in +its place after everything that does not need the chain (until the sixth review, N1, a receipt in `--anchors` without `--rpc` was wrong usage, +returned before the signed list was judged, so any object shaped like a receipt put there by the agent hid a branch the list showed). With +`--witness-url`, the verifier also reads the full list of heads +that witness holds for this agent and policy. Only the pages SIGNED by the witness count (`signedPage`, checked against the agent, the policy and +the page asked, chained by `next`, adding up to the `count` and `latest` it signs), and each must be signed within 300 s of the verifier's clock: +a server at that address that is not the witness cannot sign the witness's list, and an old page replayed by a proxy is not the list of now +(until the second review, R2-01, the list and the witness id, read from `/health`, were not signed, so a hostile server at the address passed as +the pinned witness and hid a head). Without `--witness`, the witness that signs the list is then the pinned one. + +**`verify` is a complete evaluation followed by one verdict** (fifth review, 2026-10-19; the same text stands beside the code in +`agent-cli.mjs`). Until then it decided on the way, returning 2 or 1 at the first sign, in the order it read things, and the agent chose that +order: a fresh head still pending at the witness, a `{"status":"pending"}` put in `--anchors` or an old copy of a receipt hid a backdating that +the verified receipts proved (R5-01); a list signed by another witness, or a true plain anchor beside the receipts, came out FAILED though +nothing showed a fault of the ledger (R5-02); and a receipt that did not hold because the notary was late, or because the RPC the verifier +chose served a wrong header, gave 1 when the agent handed it over and 2 when it was taken from the address (R5-03). + +1. **Gather.** Every piece of evidence is collected before anything is decided: every entry of `--anchors`, whatever its form; with + `--witness-url`, the signed list of heads, each head in it, and, for every held head without a VERIFIED receipt in `--anchors` (a receipt + with the same id that does not hold does not count), its receipt taken from the address. Nothing returns during the gathering. A + verification with no receipt, no answer of a witness and neither `--witness` nor `--witness-url` does not load the witness module + (`martor-agenti.mjs`) at all, as before. +2. **Classify.** Each piece gets one class. + - VERIFIED: it holds and gives a bound (an anchor of the ledger). + - LEDGER-FAULT, a fault of the ledger that the evidence PROVES: a head the pinned witness holds that is not an entry of the ledger (a + branch); a kept proof of equivocation; a receipt of the pinned witness tied to another ledger (another agent, policy or branch, also + through the signed predecessor of its head); a receipt the AGENT hands over whose signature of the witness it names (a pinned one, or any + when none is pinned) does not verify; an entry backdated against a bound given by VERIFIED evidence (and any other refusal of the ledger + itself by `verifyLedger`). The two kinds of receipt the agent answers for are the ones `receipt` no longer writes (above, since the sixth + review, N2): a file `receipt` wrote for this ledger is not one of them, whatever the address answered. + - NOT-MEASURED: anything else. A head or receipt pending or not notarized yet; a wrong form or wrong types; a receipt judged without `--rpc` + (`[rpc]`); a notary that is behind (`[intarziere]`); a finality the AIP-23 verifier refuses or leaves pending through the RPC the verifier + chose (`[lant]`); a receipt or a + list of another witness than the pinned one (`[martor]`); a plain anchor under a pinned witness (`[martor]`); an answer the address breaks, + a receipt with a broken signature taken from the address included (an address cannot make a FAILED); `--witness` with no receipt of a + pinned witness and no list (`[martor]`). +3. **Verdict.** FAILED (exit 1) if there is at least one LEDGER-FAULT; otherwise NOT MEASURED (exit 2) if a REQUIRED piece is NOT-MEASURED; + otherwise OK (exit 0). Every piece is required except a receipt, or an answer without a receipt, in `--anchors` whose head has a VERIFIED + receipt of its own: that old copy is listed as superseded. So a proven fault cannot be hidden by a piece that could not be measured, however + early it comes, and an outage of the witness, its notary or the verifier's RPC cannot turn an honest ledger into a FAILED. The order in which + the pieces come does not change the verdict: the negative control reverses it in three ways (the held heads before `--anchors`, `--anchors` + from the last, the held heads from the last) and requires the whole probe to stay green. + +Rule (c), the types: the type of every value that comes from outside (`--anchors`, the address, the service) is judged before it is used. A +negative `receivedAtMs`, an object (`{"toString":0}`) where a string or a number is expected, in a receipt, in a page of the list, in a `status` +or in the answers to `submit`, is a NOT-MEASURED piece with its rule named (`chitanta-forma`, `punct-integritate`, `lant`, a page refused by its +form), or for `submit` exit 2 with `--out` not written, never an exception (until the fifth review, R5-04, these came out 1 through an +exception; on the "nothing new" path of `submit`, the unsigned `latest` of the page and the state of the head it names, until the sixth, N3). The output: one line per piece on stderr (`ok` verified, `FAIL` a fault of the ledger, `--` not measured and required, `..` +superseded) and a closing line; the JSON result carries `verdict` (`OK`, `FAILED`, `NOT MEASURED`), `ok` (true, false, null) and `evidence` +(each piece with its `source`, `class` `VERIFIED`, `LEDGER-FAULT` or `NOT-MEASURED`, `required`, `seq` and `rule`). + +The class of an entry of `--anchors`, by its form: + +| an entry of `--anchors` | no witness pinned | `--witness` or `--witness-url` | +|---|---|---| +| a receipt, or a GET answer that carries one, that holds here and on the chain | VERIFIED; its witness is not pinned, so the chain alone bounds the head, and the result says so | VERIFIED if its witness is a pinned one; a receipt of another witness, its signature valid: NOT-MEASURED `[martor]` | +| a receipt whose signature of the witness it names does not verify (`receipt` does not write one since the sixth review, N2) | LEDGER-FAULT (`[punct-semnatura]`, or `[martor]` for a key that is not the key of that witness) | LEDGER-FAULT when it names a pinned witness | +| a receipt of the witness for another ledger: another agent, policy or branch, or a head whose signed predecessor is not an entry (`receipt` does not write one for the ledger it is given since the sixth review, N2) | LEDGER-FAULT `[registru]` | LEDGER-FAULT `[registru]` | +| a receipt whose inclusion proof, leaf, promise, receive rule or head signature fails, the witness signature valid | NOT-MEASURED, by the rule that fails | the same | +| a receipt not notarized yet, notarized too late (`[intarziere]`), with a finality refused or pending (`[lant]`), with no AIP-23 verifier, or judged without `--rpc` (`[rpc]`; until the sixth review, N1, wrong usage, returned before the list was judged) | NOT-MEASURED | the same | +| a receipt with wrong types (`receivedAtMs` -1, an object for a hash or a chain id) | NOT-MEASURED `[chitanta-forma]` or the rule of the field | the same | +| an answer of the witness without a receipt (`{"status":"pending"}`, `checkpointed`) | NOT-MEASURED `[fara-chitanta]` | the same | +| a plain anchor `{seq, hash, at}` | VERIFIED, as the verifier's own anchor (anchors of your own run, such as `agent-ancora.mjs anchors`); one that is not an entry of the ledger: LEDGER-FAULT `[ancora]` | NOT-MEASURED `[martor]`: the word of whoever wrote the file, not the witness's (until the fifth review, R5-02, FAILED) | +| anything else: null, a number, a string, an object of no known form | NOT-MEASURED `[forma]` | the same | + +A receipt taken from `--witness-url` is classified the same way, except that a broken signature of the witness is NOT-MEASURED there (the +address, not the agent, gave it). Anchors of your own run are judged in a run without `--witness` and `--witness-url`; each anchor only adds +constraints, so two runs catch what one would. + +So an honest ledger whose last head the witness notarized late (a notary outage beyond the merge delay) is not measured, not refused, whether +the agent hands over that receipt or the verifier takes it (until the fourth review, R4-01, it came out FAILED when taken, and until the fifth, +R5-03, when handed over); a proxy at the address that breaks the answer for one head gives not measured, never FAILED and never an exception +(R4-02, R5-04); and an agent that hands over only the receipt of an old head does not leave the entries after an INTERMEDIATE head backdatable +down to the old one (R4-03), whatever else it puts in `--anchors` (R5-01). With `--witness-url` and a witness that holds no head of the ledger, +the list is judged and a NOTE says that the time of the entries is not bounded by the witness. Without `--witness-url` the bound is still the +one of the receipts the agent hands over. + +**The cost, and the read limit of the witness** (fifth review, R5-06). The verifier takes one receipt per held head that has no verified receipt +in `--anchors`, and runs the AIP-23 verifier once per checkpoint (the heads of one round share it). The service gives 1,200 reads per client in 10 +minutes, the pages of the list included (a page holds 100 heads). A read over the limit answers 429 with `Retry-After`, the seconds until the +client's window opens again, and the verifier waits as asked, within one budget for the whole run: `AERE_WITNESS_WAIT_MAX_S`, 900 s by default, +named in a NOTE when it was used. Measured on 2026-10-19 with the real limits: an honest ledger with 1,250 held heads and no receipt handed over, +exit 0 after waiting 595 s (601 s in all; until this review, exit 2 at the head seq 1,187, whose read was refused with 429). By arithmetic, not measured: the default budget covers +two windows of reads (one wait of at most 600 s), so about 2,370 held heads; each further 1,200 heads ask up to 600 s more of budget, and a run +whose budget is spent is NOT MEASURED, with the 429, its `Retry-After` and the seconds already waited named. An agent that hands over its own +receipts spares those reads. The gateway passes `Retry-After` through (read in its code, not measured live); the limits of the public door +itself (the gateway and its edge) were not measured. + +**A head sent for an older entry is a statement about now** (fifth review, a finding rejected by its verifier, kept here as the intended +behaviour). A head says "my head was `seq` at `createdAt`". An agent that comes back after a time offline and sends, one by one, heads for +entries it wrote meanwhile (`submit --seq ` for each), signs for each an old position at a NEW time; the entries after it, written before, +then declare times before that head, and `verify --witness-url`, which takes every head the witness holds as a bound, gives FAILED `backdated` +(without `--witness-url`, with the receipts of older heads only, 0). The signed statement is false, and nothing tells it apart from a real +backdating, so FAILED is the honest answer. The recovery that does not produce it is `submit` without `--seq`, which sends the current tip. + +The arguments of both command lines come in pairs, a flag and its value. A flag that takes a value and is given without one (the last on the +line, or followed by another flag) or with an empty value, a flag the command does not know (a typo such as `--ledgr`, or the form +`--flag=value`), and a flag given twice (but `--witness`, which repeats) are wrong usage (exit 2), in `agent-cli.mjs` and in +`martor-agenti.mjs` (third review, R3-03, and fourth, R4-05: `--witness-url` or `--policy-hash` put last, `--revocations ""`, +`verify-receipt --ledger ""`, `--witness-url=`, `--policy-hash=`, `verify-receipt --witness=` or `--ledgr` used to drop the list +of the witness, the pinned policy, the revocations, the pinned witness or the link of the receipt to the ledger, without a word); only +`--reason` may be empty. The result names the witness of every receipt (`witnessReceipts`, with `pinned`), and when a witness was not pinned its +`time.lowerBound` says the bound is the chain's alone. + +An end-to-end run with a witness of your own on testnet 28001 (Node.js 24; ethers for the notarization: `npm install` in `x402/`): + +``` +node agent-cli.mjs id --out agent/ +node agent-cli.mjs policy --spec spec.json --out policy.json +node agent-cli.mjs record --key agent/agent.key.pem --policy policy.json --ledger ledger.json --action pay.json +node martor-agenti-serviciu.mjs --stare witness/ --port 8802 --cheie testnet.key \ + [--robinet ] # notarizes on testnet 28001 every 10 minutes (--interval); --fara-lant: no chain at all +node martor-agenti.mjs submit --key agent/agent.key.pem --ledger ledger.json --url http://127.0.0.1:8802 +node martor-agenti.mjs receipt --url http://127.0.0.1:8802 --id 0x --ledger ledger.json --out receipt.json # after the next checkpoint +node agent-cli.mjs verify --ledger ledger.json --policy policy.json --anchors receipt.json \ + --rpc https://testnet-rpc.aere.network --chain 28001 --verify-proof verify-proof.mjs --witness-url http://127.0.0.1:8802 +``` + +`--cheie` is a testnet key file (a hex key, or a `d=` line); if it does not exist the service makes one there (mode 0600) and, given +`--robinet`, funds it from the public testnet faucet; it never prints it. The service reads the chain id from `--rpc` and refuses any chain +but 28001 before it touches the key. + +**What a receipt proves.** That a head of the agent's ledger (its sequence number and entry hash), signed by the agent, was in the log of +the witness that signed the receipt no later than a time the chain certifies post-quantum (the checkpoint's first-seen time on testnet +28001), with its inclusion checkable by anyone from the receipt alone. As an anchor: the agent signed that its head was `seq` at +`createdAt`, so an entry written after the head that declares a time before `createdAt` minus 300 s contradicts the agent's own signature +and is caught as backdated, and an entry up to the head cannot declare a time after it. The chain bounds `createdAt` to between F minus the +merge delay and F, each widened by 600 s (the witness's 300 s rule between `createdAt` and the receive time, plus 300 s between the receive +time and F). **Only a witness the verifier pins** (`--witness`, or the one at `--witness-url`) tightens it to 300 s around its clock and +refuses an equivocating branch: any ML-DSA-65 key can be a witness of itself, so an agent can run its own witness, set its clock back to +F minus 2,100 s, and obtain a receipt for a head claimed at F minus 2,400 s, or for a branch the hosted witness refused as an equivocation. +Without pinning such a receipt is still accepted, as the agent's own signed statement bounded by the chain, and the result says so. + +**What it does not prove.** + +- The witness never sees, and never judges, the actions: it receives a sequence number and a hash, and refuses any other field. Whether an + action was allowed is the ledger verifier's job (`verify`), with the ledger. +- The lower bound on the time of the entries after a head rests on the clock of a pinned witness up to 300 s; beyond that it is the chain's, + within the merge delay. Without `--witness-url` it holds only after the heads whose receipts the agent hands over; with it, after every head + the witness holds (the complete evaluation above), and a head held with no verified receipt makes the verdict not measured, unless the + evidence proves a fault of the ledger. A witness that colludes with the agent (or a witness the agent runs, when the verifier pins none) can accept a head + claimed up to the merge delay earlier than the truth. +- The witness checks that a new head names its last head as the predecessor, not that it descends from it (that needs the entries). A + branch that forks after the last head for which the agent hands over a receipt passes the receipts: it is caught only with the full list + of heads the witness holds (`verify --witness-url`), not with the receipts the agent chooses to give. The list is signed by the witness page by + page; what the signature does not cover is the order of the requests: a page is fresh for 300 s, so a proxy that recorded a page can replay it + within those 300 s, and a branch whose head reached the witness within the last 300 s may be hidden that way. A witness that lies in its own + signed list is the witness lying, which no list can catch (C2SP cosigning by other witnesses is not built here, below). +- The witness's memory is two shards (at most 40,000 heads; at the hourly cap, at least the last day), and a third only while the final + checkpoint of the oldest one waits for its notarization. A head of an older shard is no longer served (`GET` answers 404; its receipt, once + notarized, stays verifiable without the witness), and the witness no longer remembers it: an agent whose last head was there starts again with + `previous` null, and an equivocation against a head the witness no longer holds is not caught by the witness, nor kept (a kept proof leaves + with the shard of the head it contradicts; receipts already given still show the equivocation to whoever holds both). +- Sandbox: checkpoints are notarized on the public testnet 28001 with a test key; nothing here runs on chain 2800 (mainnet at activation). +- The availability of a witness is not proven: a witness that is down or that drops a head breaks only its promise, which the agent + holds signed. A flood can delay new heads for everyone (503 `log_busy` above the hourly cap, 429 per client); it cannot fill the log for + good, and it cannot lose a head already answered. A notarization that fails for as long as a shard takes to fill (at least a day at the + cap) stops new heads (503 `notarization_behind`) until it succeeds again: the log waits rather than drop heads it promised to anchor. Each rate + limiter keeps at most 100,000 client keys; at that cap the key with the oldest window is dropped and the new client gets in (until the second + review, R2-03, every NEW client was refused while the cap was full, so whoever filled 100,000 keys locked out every new client). A flood + from more than 100,000 keys in 10 minutes therefore makes the per-client count partial (a dropped client starts a fresh window), never a + lockout; `/health` shows the keys held and dropped. Whether the Cloudflare edge delivers IPv6 client addresses to the witness is not + measured; the per-client count groups an IPv6 address by its /64 either way. +- The log refuses to start on a damaged state: a leaf line damaged in the middle (not a line cut at the tail, which is never answered and + is cut), a leaf without its head, or a checkpoint signed over more leaves than are on disk or with another root (the witness would + otherwise sign a fork of its own log). The service then stays down until a person looks, so whoever runs it should alarm on its unit + stopping. A write that fails halfway is cut back before the answer, so it does not leave such a line. The lock of the state (`.lacat`) + carries the process id, the start of the machine (`boot_id` on Linux; elsewhere the boot time from `os.uptime`, within 120 s) and, where + `/proc` has it, the start time of the process: a lock left by an unclean stop (a power cut, a reset) whose process id now belongs to another + live process no longer keeps the witness from starting (until the second review, R2-10, it did, at every restart). On a system without + `/proc` (Windows, the laboratory) a lock of the same boot whose process id was reused by another live process is still taken as held. +- One witness is not a network of witnesses: someone who sees only one witness cannot know what another was shown; C2SP-style + cosigning of the witness's checkpoints by other witnesses is not built here. + +**Bounds**, derived from measurement (2026-10-19, Node.js 24.14.1, Windows; `martor-agenti.mjs` writes the arithmetic beside each constant): + +- Shards: at most 20,000 heads each, two kept (a third only while its final checkpoint waits for the notarization). At the hourly cap of 833 + new heads (20,000 / 24) a shard takes at least a day to fill, so a head is served, and remembered by the witness, for at least a day after its + shard is frozen. +- The WORST state, measured after the second review (2026-10-19, R2-02 and R2-06): both shards full (40,000 heads), one checkpoint per head (low + traffic gives that: at most one checkpoint per round, whenever a head came) and one kept proof of equivocation per head (a session per head: + the bound of the proofs by construction). The first review's bounds were measured with one checkpoint every 1,000 heads and 2,000 proofs at + most, and missed this state: with one checkpoint per head, the memory per interval of the Merkle tree (rebuilt at every open, and growing with + every receipt served from a frozen shard) took ~5 KB per checkpoint, about 372 MB at the open (measured by a verifier), and above the unit's + cap of then (`MemoryMax=400M`) once the limiters were full. The log's tree is now incremental (only the perfect subtrees, 32 bytes each, at most + 2n per shard; no memory per interval), checked against `merkleTree` of `aere-verify-layer` on every size up to 70 and every pair of sizes up to 40. +- Disk, measured on that state: 1,320 MB (33 KB per head: the signed head and promise ~15.5 KB, the leaf line 0.35 KB, a checkpoint ~7.9 KB, a kept + proof ~7.9 KB, which holds only the new head: the head it contradicts is already in its shard). The installer asks for 2,048 MB free (~1.55x). +- Memory, measured on that state in a fresh process: the open peaks at 266 MB of RSS (50 MB of it the empty process), serving 2,000 receipts, + 2,000 signed pages of heads, 2,000 kept proofs and a consistency proof of the frozen shard peaks at 277 MB, and with the three rate limiters at + their key cap (100,000 keys each) at 330 MB (314 MB after a collection). The unit's `MemoryMax` is 512M (~1.55x), and the installer asks the host + for at least that much available memory. A state with one checkpoint per 1,000 heads now retains ~340 bytes of heap per head (before: ~900). +- Time: the open of the worst state took 37 s on the laptop (reading 40,000 checkpoints and 40,000 proofs; on Linux not measured). Signing a page + of 100 heads takes ~1.1 ms. Per-request latency was measured only before the second review (no submission above 200 ms, rotations included); + the worst-state run, with a checkpoint built after every head and fsync off, averaged 8.6 ms per head. A shard is removed from disk after the + answer, in the background. A page of `GET .../heads` reads nothing from disk. + +**Going back to a version before the second or the third review** (the deployer's rollback restores the unit and the tools, not the state): the state +format stays `aere-agent-witness-state-2`, so the version before opens it; what differs is this. Kept proofs are now written to `echivocari-2/` +(only the new head); the version before reads only `echivocari/`, so it does not see them (and does not delete them), and they are back when +this version runs again, unless the head they contradict was removed meanwhile. Proofs of the version before are read by this one, tied to the +shard of the head they contradict, and dropped when that head is no longer kept. The lock is JSON now; the version before reads it as no +process, removes it and writes its own. The version before removes, at its first open, a shard older than the two newest even when this version +kept it waiting for its notarization, and its heads then lose their certified time, as they did before the review. Going back also puts back the +unit of before (`MemoryMax=400M`); on a state with one checkpoint per head the version before needed ~372 MB at its open (measured by a verifier +of the second review) plus ~60 MB of limiters at their cap, above that cap. Since the third review (R3-01) a removed shard is renamed +`cioburi/.sters--*` before it is deleted, and, when the rename fails, marked with a `.de-sters` file: the versions before do not know either. +They ignore a `.sters-*` folder (not a shard name), which then stays on disk until this version opens the state again; and a shard folder marked +`.de-sters` whose deletion was interrupted, or a rest without `ciob.json`, they keep as a shard (the defect R3-01 repairs). A proof that replaced +another (R3-02) is an ordinary proof of form 2 for them. + ## What the verifier can and cannot see - **Time.** An entry's time is the statement of whoever holds the agent key. The verifier bounds it from above with its clock. It @@ -119,6 +436,10 @@ Measured on 2026-09-29 (Node.js 24.14.1, Windows): | `AERE_TESTNET_KEY_FILE= node proba-agent-ancora-testnet.mjs` | 7/7 on the public testnet 28001, measured 2026-09-30: a real ledger's head notarized, read back under a post-quantum certified anchor by the published AIP-23 verifier within 20 s, the certified time equal to the contract's first-seen time, the honest continuation valid with the anchor, a backdated continuation valid without it and caught with it, another branch refused; evidence in `dovezi-ancora-28001/` | | `node control-negativ-ancora.mjs` | 11/11: each anchoring guard removed in a copy turns its named test red | | `node control-negativ-aprobare.mjs` | 28/28 (2026-10-19): each guard is removed in a copy, one at a time, and the named test must fail for that reason; a test that stops before its summary counts as a failure of the control | +| `node proba-martor-agenti.mjs` | 158/158 offline, measured 2026-10-19 after the sixth review of the hosted witness (two reviewers and two verifiers in each of the first two, a reviewer and a verifier in the third, the fourth and the fifth, and as the sixth a targeted verification of the fifth's repairs; 18 findings in the first, all confirmed; 15 confirmed and one rejected in the second; 5 confirmed in the third; 6 confirmed and one rejected in the fourth; 6 confirmed and one rejected in the fifth; 4 in the sixth, N1..N4, of which N4 is this table; until the sixth review this row said 138/138, "after the fourth"), with keys made at run time and a local notary mock named as such: two agents, successive heads, a head that skips, an older head and one that does not continue the last (409), equivocation at the same position and through `previous` caught and returned, one proof kept per agent and policy, the named refusals of a request (form, the ledger's content, signature, key, identity, session, integrity, time), the rate limits, checkpoints signed by the witness with inclusion and consistency proofs, a restart on the same state, a leaf line cut at the tail by a crash, any chain but 28001 refused before the key, receipts checked without the witness and as anchors (backdating caught with the anchor and not without it), the command line, the gateway's door with and without `AERE_AGENTS_WITNESS_PORT` (in the development repository; the gateway is not part of this copy, where those rows are information), the checkpoint `VALID` for the AIP-23 reference verifier (`AERE_VERIFY_PROOF`; without it those rows are skipped by name and the exit code is 2); and a row for each finding (`9.R`): a pinned witness and the receipt of a witness the agent runs itself, the full list of heads (`--witness-url`), the sharded log and the hourly cap, an equivocation on the "already seen" path, `--chain 2800x`, `submit` with nothing new, the vocabulary of the checkpoint kind, the cap on kept proofs, the paginated `GET .../heads`, a replay of the agent's public head, the limiters (IPv6 /64, key cap and cleanup) and the door's refusal of a browser `Origin`, short and failed writes, a damaged state refused at start, error codes instead of messages; and a row for each finding of the second review (`10.R2-`, with `10.P2-` the controls of the guards its repairs added): the list of heads signed by the witness (a hostile proxy that drops a head, one that changes only the unsigned fields, an impostor, an old page replayed), kept proofs tied to the shard of the head they contradict (a stranger who fills the cap no longer keeps out another agent's proof), the limiter at its key cap, a list with malformed elements, plain anchors and an empty anchors file under a pinned witness, the incremental Merkle tree against `merkleTree` and the heap of a state with one checkpoint per head, the rotation that waits for a notarization, a shard born whole and one repaired, the route to a frozen shard, the lock after a reboot, the exit codes of `receipt` and `verify-receipt`, the log budget read before the agent quota, `Retry-After` through the door, and the "root" half of the opening guard. Run on a copy with the files of before the first review and its probe, 22 of its 23 rows failed (the 23rd, `9.P11`, is the control of a guard the repair added, which the old code passed for another reason); run on a copy with the files of before the second review (cf56bcda6) and this probe, 21 of the 24 new rows fail, each on its scenario, and the other 3 (`10.R2-15` twice, `10.P2-13`) pass there too: the behaviour was already right, what was missing was a test that could turn red; and a row for each finding of the third review (`11.R3-`, with `11.P3-` the controls of its new guards): the rest of a shard removal interrupted by a stop (in any order, and a rest of the form before, left in place), the one proof of a session that lives longest, flags given without a value, the receipt of the last head the witness holds under `--witness-url` (and a pending one: exit 2), and the exit codes of `receipt` and `submit` on answers that are not the state of a head. Run on a copy with the files of c34b34f1b and this probe, 7 of the 8 new rows fail, each on its scenario, and the 122 rows of before pass; the 8th, `11.P3-04b`, gives the check of the list against the ledger a row of its own, which the receipt of the last head (R3-04) can no longer hide: it passes there too; and a row for each finding of the fourth review (`12.R4-`, `12.P4-`), on the rule of `--witness-url`: a receipt notarized late taken from the address (not measured, not FAILED), a proxy that breaks the answer for one held head (not measured, no exception), a receipt for EVERY held head (an intermediate one too), a held head whose signed predecessor is not in the ledger (FAILED), `submit` on a 200 or 202 that is not the state of the head sent, flags written `--flag=value`, mistyped or given twice, the choice between two proofs of a session at the opening, and the `.de-sters` mark. Run on a copy with the files of c5183bcb8 and this probe, 5 of the 8 new rows fail, each on its scenario, and the 130 rows of before pass; the other 3 (`12.P4-03b` and the two `12.R4-06`) give their own row to guards that were right there, which is what R4-06 found missing; and a row for each finding of the fifth review (`13.R5-`, `13.P5-`), `verify` as a complete evaluation followed by one verdict: a head still pending, a `{"status":"pending"}` or an old copy of a receipt beside a backdating the verified receipts prove, a list signed by another witness and a plain anchor under a pinned one, a notary outage and a finality refused through the verifier's RPC on both paths, wrong types without an exception, a receipt of the agent that does not hold beside a pending head in both orders, and the wait on a 429. Run by the verifier of those repairs on a copy with the files of b55038c2a and that probe (no gateway in the copy), 129/150: 15 of the 16 new rows fail, each on its scenario, and so do 6 rows of before whose expected verdict the fifth review changed; `13.P5-01` passes there too; and a row for each finding of the sixth (`14.N`): a receipt without `--rpc` beside a branch the signed list shows (not measured `[rpc]`, the branch FAILED), `receipt` through a proxy without any key (the true receipt of another agent's head, a broken witness signature, the receipt of another head of the same ledger) and against `--ledger` and `--witness`, and wrong types on the "nothing new" path of `submit`. Run on a copy with the files of 786aa0038 and this probe (no gateway in the copy, so its 4 rows are information), 149/154: the 4 new rows fail, `14.N1` and `14.N3` on their scenarios and the two `14.N2` rows already at `--ledger`, a flag that version refuses (that `receipt` through such a proxy exited 0 and wrote the file there was measured by the targeted verification), and so does `10.R2-11`, which now calls `receipt` with `--ledger` too; the other rows of before pass | +| `node proba-martor-agenti.mjs --testnet` | 75/75 on the public testnet 28001, measured 2026-10-19 at 05:35 UTC with the version BEFORE the adversarial review (the 70 offline rows of that version plus 5): a fresh test key funded from the faucet, a checkpoint notarized on AereNotary (block 6,828,096), the receipt `VALID` for the AIP-23 reference verifier with the witness's signature and the post-quantum finality `PASSED` (51 s after the notarization), `verify --anchors receipt.json` on the live chain: the honest ledger 0, the backdated one 1. Evidence in `dovezi-martor-agenti-28001/` (no private key). **The versions after the first and the second review were not run on the testnet** (not measured: the faucet allows one claim per 24 h, and it was cooling down); their testnet rows also pin the witness (`--witness`), with the receipt that pinning now requires. Without a funded key and a faucet that answers, the testnet rows are skipped by name (exit 2) | +| `node control-negativ-martor-agenti.mjs` | 131/131 plantations red on their row and 3/3 neutral ones green (2026-10-19, after the sixth review; 1,102 s with 12 plantations at a time, the default; after the fourth review, with 112 plantations, ~475 s with 12 and ~747 s with 4, `AERE_CONTROL_PARALEL=4`; the B2 runner gives this test 1650 s since the sixth review, 1.5 x the measurement on an idle laptop, after the 1200 s of before stopped a green control at 1,212 s inside the suite while another workload held the processor (the arithmetic is in `probele-b2.sh`); until the sixth review this row said 112/112 "after the fourth", and the fifth's 123 plantations and 3 neutral ones were not written here): every one of the 99 guard markers (`PAZA-MARTOR`) of the witness, the service and, since the second review, the command line (`agent-cli.mjs`) has a plantation that removes it, and a coverage gate counts a marker without one as a failure of the control (with its own check: one plantation taken out, the gate names its marker); the guards the repairs added (the per-agent quota after "already seen", the "already seen" exit after the predecessor check, the write to the last byte and the cut-back, the paging, the /64 key and the key cleanup, the error codes, the command line's pinned witnesses, `--chain` and an unanswering `--witness-url`, the vocabulary in `proof-kinds/`, the door's `Origin` refusal) are planted too. Each removal is made in a copy, and the named row must fail; a pattern not found once, or a test that stops before its summary, counts as a failure of the control. The guards of the second review are planted too (the signed page: its form, integrity, signature, age and count, and reading it instead of the unsigned fields; the list signed by another witness than the pinned one; proofs tied to the shard of the head they contradict and the cap derived from the shards; the limiter dropping its oldest key; a malformed list; plain anchors and no receipt under a pinned witness; the rotation and the opening that keep an unnotarized shard; the shard born whole and the link repaired; the route to a kept shard; the lock across a reboot and a reused process id; the exit codes; the budget read before the quota; `Retry-After` through the door), and the opening guard is split into its size and its root, each with its row. So are those of the third (the rename before a shard is deleted and the removal of a rest at the opening; the newer proof taking the place of the older; a flag without a value, in both command lines; the receipt of the last head under `--witness-url`; the exit codes of `receipt` and `submit`), and those of the fourth (every held head with its receipt, the receipt of a head required, its id read without an exception, a receipt taken from the address judged as testimony both ways, `submit` on the main path, unknown and repeated flags in both command lines, the choice between two proofs at the opening, and the two halves of the rest of a removal, each with its row). Those of the fifth review are planted too (the order of the classes in the verdict, an early exit on the way, the class of each receipt both ways, the old copy, the types, the wait on a 429), with three NEUTRAL plantations that reverse the order in which the evidence comes and require the whole probe to stay green; and those of the sixth (a receipt without `--rpc` as a not-measured piece, planted both ways: the early exit of before and the guard removed; `receipt` writing only a verified receipt of the head asked, with its id, its ledger and its pinned witness each planted; the types on the "nothing new" path of `submit`). After the first review the control planted 64 guards over 45 markers and did not look at `agent-cli.mjs`; until the first review it planted 24, which covered 20 of the 34 markers of then | +| `bash ../../cloud-gateway/proba-desfasoara-martor-agenti.sh` (development repository) | 12/12 (2026-10-19): the rollback functions and the laboratory copy of the deployer, extracted from its host script and run on the laptop with `systemctl` replaced: a reinstall that fails before step 5 leaves the live service and its unit untouched, one that fails after it puts the old unit back and restarts the service, a first install that fails is removed, the laboratory is beside the state (not under `/root`) and removed on the way back, a laboratory copy of a real state opens with the same leaves; and as negative control the rollback of before the review (it stopped the live service and deleted its unit) and five plantations, each red on its scenario | The data format is version 2 (2026-09-29). Code comments, test names and most internal names are in Romanian; messages, data and the exported interface are in English. No third party has reviewed this component. diff --git a/agents/agent-cli.mjs b/agents/agent-cli.mjs index c82b995..7959db7 100644 --- a/agents/agent-cli.mjs +++ b/agents/agent-cli.mjs @@ -11,6 +11,18 @@ // node agent-cli.mjs approve --key human.key.pem --policy p.json --action a.json [--valid 600] [--out ap.json] // node agent-cli.mjs revoke --key owner.key.pem --policy p.json [--at ] [--reason text] [--out rv.json] // node agent-cli.mjs verify --ledger l.json --policy p.json [--policy-hash 0x..] [--revocations r1.json,r2.json] [--anchors a.json] [--not-before ] +// a.json: anchors {seq, hash, at}, or receipts of the agents witness (martor-agenti.mjs), which then need --rpc and +// --verify-proof (or AERE_VERIFY_PROOF) [--chain 28001] [--max-merge-delay ]; without --rpc or the verifier such a +// receipt is not measured (exit 2, unless another piece proves a fault of the ledger) +// [--witness aere-witness:<40 hex>[,...]] the witnesses whose receipts are accepted (a receipt of any other, or a plain anchor: +// not measured; no receipt of one: exit 2); without it any ML-DSA-65 key is a witness of +// itself, and the result says the bound is the chain's alone +// [--witness-url ] the full list of heads that witness holds for this agent and policy, signed by it page by page within the +// last 300 s, must be entries of this ledger, and every held head needs a verified receipt (taken from the +// address when --anchors has none; a 429 is waited out as its Retry-After asks, within AERE_WITNESS_WAIT_MAX_S +// seconds, 900 by default); no signed list, or one signed by another witness than --witness: not measured +// verify gathers every piece of evidence first, then gives ONE verdict: 1 if a piece proves a fault of the ledger, else 2 if a +// required piece could not be measured, else 0 (README: "verify is a complete evaluation followed by one verdict") // node agent-cli.mjs equivocation --a l1.json --b l2.json [--out proof.json] // node agent-cli.mjs verify-equivocation --proof proof.json // Iesiri: 0 da (permis, valid, gasit), 1 nu (invalid, negasit, refuzat de verificare), 3 actiune refuzata de politica, 2 folosire gresita. @@ -43,8 +55,35 @@ const acum = () => Math.floor(Date.now() / 1000); export function main(argv) { const [cmd, ...rest] = argv; - const get = (f) => { const i = rest.indexOf(f); return i >= 0 ? rest[i + 1] : undefined; }; - const lista = (f) => (get(f) ? String(get(f)).split(',').filter(Boolean) : []); + // un steag dat fara valoare (pus ultimul, sau urmat de alt steag) ori cu valoarea goala e folosire gresita (2), nu un steag nedat (revizia B3 a + // treia, R3-03): pana atunci `--witness-url` sau `--policy-hash` puse ultimele, `--revocations ""` (o variabila goala) scoteau in tacere lista + // martorului, pinul politicii sau revocarile, si un registru prins iesea 0. Numai `--reason` are voie sa fie gol. + const GOALE_PERMISE = ['--reason']; + const get = (f) => { const i = rest.indexOf(f); if (i < 0) return undefined; const v = rest[i + 1]; if (!(v !== undefined && !/^--[a-z]/.test(v) && (v !== '' || GOALE_PERMISE.includes(f))) /*PAZA-MARTOR:steag-fara-valoare*/) throw new Folosire(`${f} needs a value`); return v; }; + // (un element gol din lista, `a.json,`, ajunge la citesteJson, care il refuza ca folosire gresita) + const lista = (f) => { const v = get(f); return v === undefined ? [] : String(v).split(','); }; + // toate aparitiile unui steag repetabil (--witness a --witness b, sau --witness a,b) + const toate = (f) => rest.flatMap((x, i) => (x === f ? (rest[i + 1] === undefined ? [''] : String(rest[i + 1]).split(',')) : [])); + // un numar intreg pozitiv; altfel folosire gresita (revizia B3, constatarea 5: `--chain 2800x` dadea NaN si paza lantului cadea DESCHISA) + const intregPozitiv = (f) => { const v = get(f); if (v === undefined) return undefined; if (!/^[1-9]\d{0,14}$/.test(String(v))) throw new Folosire(`${f} must be a positive integer, not "${v}"`); return Number(v); }; + // STEAGURILE CUNOSCUTE ale fiecarei comenzi (revizia B3 a patra, R4-05): argumentele vin in perechi steag + valoare, si un argument care nu e un + // steag al comenzii (`--witness-url=`, o greseala de tastare ca `--ledgr`, o valoare ramasa fara steag) e folosire gresita (2). Pana atunci + // cititorul cauta potrivirea exacta si nu refuza nimic necunoscut, deci `--witness-url=` sau `--policy-hash=` erau "nedate" si paza lor + // cadea in tacere, cu exit 0. Un steag dat de doua ori e si el folosire gresita, afara de --witness (repetabil). Valorile le judeca `get`. + const STEAGURI = { id: ['--out'], human: ['--out'], policy: ['--spec', '--out'], check: ['--policy', '--action', '--spent'], + record: ['--key', '--policy', '--ledger', '--action', '--approvals', '--provenance'], approve: ['--key', '--policy', '--action', '--valid', '--out'], + revoke: ['--key', '--policy', '--at', '--reason', '--out'], + verify: ['--ledger', '--policy', '--policy-hash', '--revocations', '--anchors', '--not-before', '--rpc', '--verify-proof', '--chain', '--max-merge-delay', '--witness', '--witness-url'], + equivocation: ['--a', '--b', '--out'], 'verify-equivocation': ['--proof'] }; + if (Object.hasOwn(STEAGURI, cmd)) { + const vazute = new Set(); + for (let i = 0; i < rest.length; i += 2) { + const s = String(rest[i]); + if (!(STEAGURI[cmd].includes(s) /*PAZA-MARTOR:steag-necunoscut*/)) throw new Folosire(`${cmd}: unknown argument ${JSON.stringify(s.slice(0, 60))} (its flags: ${STEAGURI[cmd].join(' ')}; the value follows its flag, never --flag=value)`); + if (!(!vazute.has(s) || s === '--witness') /*PAZA-MARTOR:steag-dublu*/) throw new Folosire(`${cmd}: ${s} is given twice`); + vazute.add(s); + } + } switch (cmd) { case 'id': case 'human': { @@ -110,9 +149,150 @@ export function main(argv) { const ledger = citesteJson(get('--ledger'), 'ledger'); const j = citesteJson(get('--policy'), 'policy'); const pinned = get('--policy-hash') ?? j.policyHash; - const v = verifyLedger(ledger, { policy: j.policy || j, policyHash: pinned, revocations: lista('--revocations').map((f) => citesteJson(f, 'revocations')), - anchors: get('--anchors') ? citesteJson(get('--anchors'), 'anchors') : [], notBefore: get('--not-before') != null ? Number(get('--not-before')) : null }); - console.log(JSON.stringify(v, null, 1)); return v.ok ? 0 : 1; + const revocari = lista('--revocations').map((f) => citesteJson(f, 'revocations')); + const notBefore = get('--not-before') != null ? Number(get('--not-before')) : null; + const date = get('--anchors') ? citesteJson(get('--anchors'), 'anchors') : []; + const toate2 = Array.isArray(date) ? date : [date]; + // argumentele martorului se judeca INAINTE de orice: o valoare gresita e folosire gresita (2), niciodata o paza care cade deschisa + const chain = intregPozitiv('--chain'), mergeMax = intregPozitiv('--max-merge-delay'); + const martori = toate('--witness'); + if (martori.some((w) => !/^aere-witness:[0-9a-f]{40}$/.test(w))) throw new Folosire('--witness must be aere-witness:<40 hex> (repeatable, or comma-separated)'); + const urlMartor = get('--witness-url'); + if (urlMartor !== undefined && !/^https?:\/\/[^\s]+$/.test(String(urlMartor))) throw new Folosire('--witness-url must be the base URL of a witness (http:// or https://)'); + const rpc = get('--rpc'), verifyProof = get('--verify-proof') || process.env.AERE_VERIFY_PROOF; + // bugetul de asteptare pe 429 (R5-06): un numar intreg de secunde; o valoare gresita e folosire gresita, nu o tacere pe valoarea implicita + const asteptareMax = process.env.AERE_WITNESS_WAIT_MAX_S; + if (asteptareMax !== undefined && asteptareMax !== '' && !/^\d{1,6}$/.test(asteptareMax)) throw new Folosire('AERE_WITNESS_WAIT_MAX_S must be a whole number of seconds (0 to 999999)'); + // o chitanta fara --rpc (din --anchors sau adusa) e o dovada NEMASURATA cu regula `rpc`, judecata la locul ei, dupa tot ce nu cere lantul (lista, + // capetele tinute, ancorele); pana la a sasea revizie (N1) o chitanta din --anchors fara --rpc iesea aici cu 2 (folosire gresita), INAINTEA listei + // semnate, deci o ramura aratata de lista se ascundea punand in --anchors orice obiect cu forma de chitanta + const ob = (x) => !!x && typeof x === 'object' && !Array.isArray(x); // ca esteChitanta din martor-agenti.mjs (incarcat abia mai jos) + const esteChitanta = (a) => ob(a) && (ob(a.witnessReceipt) || (ob(a.receipt) && ob(a.receipt.witnessReceipt))); + // o chitanta a martorului gazduit (martor-agenti.mjs) e primita ca ancora numai dupa ce ESTE verificata aici: includerea refacuta local si + // momentul capului de jurnal certificat de verificatorul AIP-23 pe lant + const dinMartor = (a) => !!a && typeof a === 'object' && !Array.isArray(a) && ['witnessReceipt', 'receipt', 'promise', 'status'].some((k) => a[k] !== undefined); + const ancoraSimpla = (a) => !!a && typeof a === 'object' && !Array.isArray(a) && !dinMartor(a) && Number.isSafeInteger(a.seq) && a.seq >= 0 && typeof a.hash === 'string' && /^[0-9a-f]{64}$/.test(a.hash) && Number.isSafeInteger(a.at); + const txt = (x) => (typeof x === 'string' ? x.slice(0, 120) : `<${x === null ? 'null' : Array.isArray(x) ? 'array' : typeof x}>`); + return (async () => { + // ================================================================================================================================ + // EVALUAREA COMPLETA, UN SINGUR VERDICT (revizia B3 a cincea, 2026-10-19, R5-01..R5-05; scrisa si in README). Pana atunci verificarea + // decidea PE DRUM, iesind cu 2 sau 1 la primul semn, in ordinea citirii, iar ordinea o alegea agentul: un cap proaspat in asteptare, un + // {"status":"pending"} in --anchors sau o copie veche a unei chitante ascundeau o antedatare DOVEDITA (R5-01), iar o chitanta cazuta din + // vina notarului dadea 1 predata si 2 adusa (R5-03). Acum: + // 1. Se ADUNA toate dovezile: fiecare intrare din --anchors (orice forma), lista semnata a martorului, fiecare cap tinut, chitanta + // adusa de la adresa pentru fiecare cap tinut fara o chitanta VERIFICATA in --anchors. Nimic nu iese din functie in timpul adunarii. + // 2. Fiecare dovada primeste o CLASA: VERIFICATA (tine, da o margine), VINA-REGISTRULUI (dovedita: un cap tinut de martor care nu e + // intrare a registrului, o echivocare pastrata, o chitanta a martorului fixat legata de alt registru, o chitanta predata de agent cu + // semnatura martorului pe care il numeste INVALIDA, o intrare antedatata fata de o margine din dovezi VERIFICATE), NEMASURATA + // (orice altceva: in asteptare, forma gresita, notarul in urma, finalitatea data de RPC-ul verificatorului, o lista semnata de alt + // martor decat cel fixat, o ancora simpla sub un martor fixat, o adresa care strica raspunsul). + // 3. VERDICTUL: FAILED daca exista o VINA-REGISTRULUI; altfel NOT MEASURED daca o dovada CERUTA e NEMASURATA; altfel OK. Deci o vina + // dovedita nu poate fi ascunsa de un NEMASURAT, oricat de devreme vine, iar o pana a martorului nu face dintr-un registru cinstit un + // FAILED. O dovada e CERUTA afara de o chitanta (sau un raspuns in asteptare) din --anchors al carei cap are deja o chitanta VERIFICATA. + // REGULA (c): nicio forma de raspuns nu arunca; tipurile se judeca inainte de folosire (martor-agenti.mjs: sir(), chitanta-tipuri). + // ================================================================================================================================ + // modulul martorului se incarca numai cand o dovada sau un steag il cere: un registru cu ancore simple (sau fara) se verifica si fara el + const M = urlMartor !== undefined || martori.length > 0 || toate2.some(dinMartor) ? await import('./martor-agenti.mjs') : null; + const dovezi = []; + const adauga = (d) => { dovezi.push({ ceruta: true, ...d }); }; + let fixati = martori.length ? martori : null; + const fixare = !!fixati || urlMartor !== undefined; + // buget comun de asteptare pe 429 (Retry-After), pentru lista si chitantele aduse (R5-06) + const buget = { ramasS: asteptareMax ? Number(asteptareMax) : 900, asteptatS: 0 }; + const cere = M ? M.cereRabdator(buget) : null; + // 1a. lista semnata a capetelor (sub --witness-url) + let tinute = null; + if (urlMartor !== undefined) { + const c = await M.citesteCapeteMartorului(urlMartor, ledger.agentId, ledger.session, { cere }); + if (!c.ok) adauga({ sursa: 'list', clasa: 'NEMASURATA', detail: `the witness at --witness-url did not give a list of heads it signed: ${c.motiv}` }); + else if (!(!fixati || fixati.includes(c.witness)) /*PAZA-MARTOR:martor-url*/) adauga({ sursa: 'list', clasa: 'NEMASURATA', rule: 'martor', detail: `the list at --witness-url is signed by ${c.witness}, not by a pinned witness (${fixati.join(', ')}): not the testimony of the pinned witness (another address, or a server without its key)` }); + else { + fixati = fixati || [c.witness]; + const vc = M.verificaCapeteMartorului(ledger, c); + for (const x of vc.randuri) adauga({ sursa: `list of ${c.witness} (signed at ${c.at})`, clasa: x.pass === true ? 'VERIFICATA' : x.pass === false ? 'VINA' : 'NEMASURATA', rule: x.rule, seq: x.seq, detail: x.detail }); + tinute = c.heads; + } + } + // o chitanta, din --anchors (`agent`) sau adusa de la adresa (`adusa`), judecata local si apoi pe lant; fara exceptie pe nicio forma + const idCap = (a) => { const p = M.chitantaDin(a); const w = p && typeof p === 'object' ? p.witnessReceipt : null; const h = w && typeof w === 'object' ? w.head : null; return h && typeof h === 'object' && typeof h.statementHash === 'string' ? h.statementHash : null; } /*PAZA-MARTOR:chitanta-id-sigur*/; + const areVerificator = !!verifyProof && fs.existsSync(verifyProof); + const o = { verifyProof, rpc, chain: chain ?? null, mergeMax, martori: null, verdicte: new Map() }; + const evalueaza = async (x, sursa) => { + const plic = M.chitantaDin(x); + const l = M.verificaChitanta(plic, { ledger, martori: fixati }); + if (!l.ok) { + // vina registrului: o chitanta a martorului fixat legata de alt registru (alt agent, alta politica, alta ramura); si, numai pentru una PREDATA + // de agent, o semnatura INVALIDA a martorului pe care il numeste (cand e fixat, sau cand nu e fixat niciunul) + const vp = M.verificaPunct(plic); + const numit = vp.rule === 'martor' || vp.rule === 'punct-semnatura' ? plic.statement.witness : null; + const falsificata = sursa === 'agent' && numit !== null && (!fixati || fixati.includes(numit)) /*PAZA-MARTOR:chitanta-falsificata*/; + const vina = l.rule === 'registru' /*PAZA-MARTOR:chitanta-alt-registru*/ || falsificata; + return { chitanta: true, clasa: vina ? 'VINA' : 'NEMASURATA', rule: l.rule, detail: l.error }; + } + if (!rpc /*PAZA-MARTOR:fara-rpc*/) return { chitanta: true, seq: l.head.seq, clasa: 'NEMASURATA', rule: 'rpc', detail: 'a witness receipt needs --rpc : the chain is read, not trusted from the receipt' }; + if (!areVerificator) return { chitanta: true, seq: l.head.seq, clasa: 'NEMASURATA', rule: 'lant', detail: 'a witness receipt needs the AIP-23 reference verifier (--verify-proof or AERE_VERIFY_PROOF)' }; + // pe lant: nenotarizata, intarziata, cu finalitatea refuzata de RPC-ul ales de verificator: vina martorului, a notarului sau a RPC-ului, nu a + // registrului, pe ambele drumuri (R5-03: pana atunci o chitanta predata de agent dadea aici FAILED, iar aceeasi, adusa, NEMASURAT) + const r = await M.ancoreDinChitante(ledger, [plic], { ...o, martori: fixati }); + const rd = r.randuri[0] || {}; + if (r.stare === 'OK' && r.anchors.length === 1) return { chitanta: true, seq: l.head.seq, clasa: 'VERIFICATA', ancora: r.anchors[0], detail: rd.detail }; + return { chitanta: true, seq: l.head.seq, clasa: 'NEMASURATA' /*PAZA-MARTOR:lant-nemasurat*/, rule: rd.rule || 'lant', detail: rd.detail || 'not judged' }; + }; + // 1b. fiecare intrare din --anchors, oricare ar fi forma ei + const fazaAnchors = async () => { for (const [i, a] of toate2.entries()) { + const sursa = `anchors[${i}]`; + if (esteChitanta(a)) { const e = await evalueaza(a, 'agent'); adauga({ sursa, id: idCap(a), ...e }); continue; } + if (dinMartor(a)) { adauga({ sursa, id: typeof a.id === 'string' && /^0x[0-9a-f]{64}$/.test(a.id) ? a.id : null, clasa: 'NEMASURATA', rule: 'fara-chitanta', detail: `an answer of the witness that carries no receipt yet (status ${txt(a.status)}): ask the witness again after its next checkpoint` }); continue; } + if (ancoraSimpla(a)) { + if (fixare /*PAZA-MARTOR:martor-simple*/) adauga({ sursa, seq: a.seq, clasa: 'NEMASURATA', rule: 'martor', detail: 'a plain anchor {seq, hash, at} under a pinned witness is not the testimony of the witness (anchors of your own run, such as agent-ancora.mjs anchors, are judged in a run without --witness and --witness-url)' }); + else adauga({ sursa, seq: a.seq, clasa: 'VERIFICATA', ancora: { seq: a.seq, hash: a.hash, at: a.at }, detail: `a plain anchor at seq ${a.seq}, taken as the verifier's own (no witness pinned)` }); + continue; + } + adauga({ sursa, clasa: 'NEMASURATA', rule: 'forma', detail: `not a piece of evidence: neither a receipt, nor an answer of the witness, nor an anchor {seq, hash, at} (${txt(a)})` }); + } }; + // 1c. fiecare cap tinut de martor, cu o chitanta VERIFICATA: a agentului, altfel adusa de la adresa (oricare cap, nu doar ultimul; o chitanta a + // agentului cu acelasi id care nu tine nu tine locul celei verificate, R5-01) + const descrie = (r) => (!r.status ? `the witness did not answer (${txt(r.eroare)})` : r.status === 429 ? `the witness answered HTTP 429 (Retry-After ${r.retryAfter === null ? 'none' : r.retryAfter + ' s'}; ${buget.asteptatS} s already waited of a budget of ${buget.asteptatS + buget.ramasS} s, AERE_WITNESS_WAIT_MAX_S)` + : r.status !== 200 ? `the witness answered HTTP ${r.status}` : !(r.j && typeof r.j === 'object' && !Array.isArray(r.j)) ? 'the answer is not the state of a head (not JSON)' + : !M.esteChitanta(r.j) ? (typeof r.j.status === 'string' && r.j.status !== 'notarized' ? `the witness has it ${txt(r.j.status)}, not notarized yet (ask again after its checkpoint is notarized)` : 'the answer carries no receipt') + : idCap(r.j) === null ? 'the receipt in the answer has no head' : 'the answer is the receipt of another head'); + const fazaCapete = async () => { for (const h of tinute || [] /*PAZA-MARTOR:martor-fiecare-cap*/) { + if (dovezi.some((d) => d.clasa === 'VERIFICATA' && d.id === h.id) /*PAZA-MARTOR:martor-chitanta-ceruta*/) continue; + const sursa = `seq ${h.seq}, taken from --witness-url`; + const r = await M.stareaCapuluiLa(urlMartor, h.id, { cere }); + if (!(r.status === 200 && M.esteChitanta(r.j) && idCap(r.j) === h.id)) { adauga({ sursa, seq: h.seq, id: h.id, clasa: 'NEMASURATA', rule: 'adusa', detail: `the witness holds seq ${h.seq} and no verified receipt of it was in --anchors; ${descrie(r)}: the entries after seq ${h.seq} are not bounded by the witness` }); continue; } + const e = await evalueaza(r.j, 'adusa'); + adauga({ sursa, seq: h.seq, id: h.id, ...e, detail: e.clasa === 'NEMASURATA' ? `${e.detail}: the entries after seq ${h.seq} are not bounded by the witness` : e.detail }); + } }; + // adunarea: --anchors, apoi capetele tinute (o chitanta VERIFICATA a agentului scuteste aducerea); ordinea nu schimba verdictul, numai cate se aduc + await fazaAnchors(); await fazaCapete(); + // 1d. --witness cere o legatura SEMNATA de martorul fixat: fara nicio chitanta de judecat, marginea martorului nu e masurata + if (!(!martori.length || dovezi.some((d) => d.chitanta) || (tinute && tinute.length)) /*PAZA-MARTOR:martor-fara-chitanta*/) adauga({ sursa: 'witness', clasa: 'NEMASURATA', rule: 'martor', detail: '--witness was given, but there is no receipt of a pinned witness: the bound of the witness was not measured' }); + // o chitanta (sau un raspuns in asteptare) din --anchors al carei cap are o chitanta VERIFICATA nu mai e ceruta (o copie veche) + for (const d of dovezi) if (d.clasa === 'NEMASURATA' && d.id && dovezi.some((x) => x !== d && x.clasa === 'VERIFICATA' && x.id === d.id) /*PAZA-MARTOR:copie-veche*/) d.ceruta = false; + // 2. registrul, judecat cu ancorele VERIFICATE (o antedatare fata de ele e vina registrului) + const ancore = dovezi.filter((d) => d.clasa === 'VERIFICATA' && d.ancora).map((d) => d.ancora).sort((a, b) => a.seq - b.seq); + const v = verifyLedger(ledger, { policy: j.policy || j, policyHash: pinned, revocations: revocari, anchors: ancore, notBefore }); + if (!v.ok) adauga({ sursa: 'ledger', clasa: 'VINA', rule: v.rule, detail: v.error }); + // 3. UN SINGUR verdict, dupa ce s-a adunat tot + const vina = dovezi.filter((d) => d.clasa === 'VINA'), nemasurate = dovezi.filter((d) => d.clasa === 'NEMASURATA' && d.ceruta); + const verdict = vina.length ? 'FAILED' : nemasurate.length ? 'NOT MEASURED' : 'OK' /*PAZA-MARTOR:verdict-vina-intai*/; + for (const d of dovezi) console.error(`${d.clasa === 'VERIFICATA' ? 'ok' : d.clasa === 'VINA' ? 'FAIL' : d.ceruta ? '--' : '..'} ${d.sursa}: ${d.rule ? '[' + d.rule + '] ' : ''}${d.detail}${d.ceruta ? '' : ' (superseded by a verified receipt of the same head)'}`); + const chit = ancore.filter((a) => a.witness); + if (chit.length) { + v.witnessReceipts = chit.map((a) => ({ seq: a.seq, witness: a.witness, pinned: a.witnessPinned, chainLowerBound: a.chainLowerBound })); + if (v.time && chit.some((a) => !a.witnessPinned)) v.time.lowerBound = 'witnessed by an UNPINNED witness: the chain alone bounds each head to no earlier than its chainLowerBound (F minus the merge delay minus 600 s); pass --witness to pin the witness'; + } + if (!fixati && chit.length) console.error('NOTE: no witness was pinned (--witness or --witness-url): each receipt above bounds its head by the chain alone, not by the witness clock'); + if (tinute && !tinute.length && verdict === 'OK') console.error('NOTE: the witness holds no head of this ledger: the list was checked, but the time of the entries is not bounded by the witness'); + if (buget.asteptatS) console.error(`NOTE: the witness rate-limited this verification (HTTP 429): ${buget.asteptatS} s waited, as its Retry-After asked, of a budget of ${buget.asteptatS + buget.ramasS} s (AERE_WITNESS_WAIT_MAX_S)`); + v.verdict = verdict; v.ok = verdict === 'OK' ? true : verdict === 'FAILED' ? false : null; + v.evidence = dovezi.map((d) => ({ source: d.sursa, class: d.clasa === 'VINA' ? 'LEDGER-FAULT' : d.clasa === 'VERIFICATA' ? 'VERIFIED' : 'NOT-MEASURED', required: d.ceruta, ...(d.seq !== undefined ? { seq: d.seq } : {}), ...(d.rule ? { rule: d.rule } : {}) })); + if (verdict === 'FAILED') console.error(`FAILED: ${vina.length} piece(s) of evidence show a fault of the ledger (${[...new Set(vina.map((d) => d.rule || 'ledger'))].join(', ')})`); + if (verdict === 'NOT MEASURED') console.error(`NOT MEASURED: ${nemasurate.length} required piece(s) of evidence could not be verified, and none shows a fault of the ledger (${[...new Set(nemasurate.map((d) => d.rule || 'list'))].join(', ')})`); + console.log(JSON.stringify(v, null, 1)); + return verdict === 'OK' ? 0 : verdict === 'FAILED' ? 1 : 2; + })(); } case 'equivocation': { const p = findEquivocation(citesteJson(get('--a'), 'a'), citesteJson(get('--b'), 'b')); @@ -130,8 +310,7 @@ export function main(argv) { } if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { - let cod; - try { cod = main(process.argv.slice(2)); } - catch (e) { console.error(`agent: ${e.message}`); cod = e instanceof Folosire ? 2 : 1; } - process.exitCode = cod; + // main intoarce un cod, sau (verify cu o chitanta a martorului, care citeste lantul) o promisiune de cod + Promise.resolve().then(() => main(process.argv.slice(2))) + .then((cod) => { process.exitCode = cod; }, (e) => { console.error(`agent: ${e.message}`); process.exitCode = e instanceof Folosire ? 2 : 1; }); } diff --git a/agents/control-negativ-martor-agenti.mjs b/agents/control-negativ-martor-agenti.mjs new file mode 100644 index 0000000..f11a1fe --- /dev/null +++ b/agents/control-negativ-martor-agenti.mjs @@ -0,0 +1,325 @@ +// Controlul negativ al martorului gazduit al agentilor (martor-agenti.mjs, martor-agenti-serviciu.mjs, proba-martor-agenti.mjs; B3, 2026-10-19): +// fiecare paznic scos intr-o COPIE a componentei trebuie sa inroseasca proba NUMITA, cu proba chiar rulata (rezumatul ei exista); pe copia +// neatinsa, proba e verde. Trei stari, ca la celelalte controale ale agentilor: ROSU cum trebuia (randul numit pica), CONTROL CAZUT (randul a +// ramas verde) si STRICAT (tiparul nu apare exact o data, sau proba nu ajunge la rezumat), iar ultimele doua se numara esecuri. +// Plantarile se ancoreaza pe partea STABILA a randului (conditia cu markerul ei PAZA-MARTOR), nu pe tot randul, si toate ruleaza (JavaScript: +// o conditie inlocuita cu `false`), deci un rosu e al probei, nu al unui fisier care nu se mai incarca. +// ACOPERIREA (2026-10-19, revizia adversariala B3, constatarea 6): pana la reparatie controlul planta 24 de paznici si README-ul spunea "each +// guard", desi primire (de doua ori), martor, lant (pe lant si pe nivelul semnaturii), promisiune, sesiune, integritate si punct-integritate nu +// erau plantati. Acum FIECARE marker PAZA-MARTOR din martor-agenti.mjs si martor-agenti-serviciu.mjs trebuie sa fie in tiparul unei plantari, sau +// pe lista EXCEPTII cu motivul scris; un marker neacoperit e un ESEC al controlului. Garda isi are si ea martorul: cu o plantare scoasa, trebuie +// sa numeasca exact markerul ei. Pe langa markeri, plantarile pazesc si ordinea si forma reparatiilor din revizie (cota inainte de "deja vazut", +// scrierea pana la ultimul octet, taierea inapoi, paginarea, cheia /64, linia de comanda, vocabularul felului in proof-kinds, usa gateway-ului). +// A DOUA REVIZIE (2026-10-19, 15 constatari R2-01..R2-15): fiecare paznic nou are plantarea lui, tinta randul lui 10.R2- / 10.P2-; garda de +// acoperire priveste acum si agent-cli.mjs (R2-15: potrivirea --witness / --witness-url si ancorele simple sub un martor fixat nu aveau nici +// plantare, nici rand), iar paznicul radacinii de la deschidere e impartit in doua (marimea, radacina), fiecare cu randul lui. +// A TREIA REVIZIE (2026-10-19, 5 constatari R3-01..R3-05): fiecare paznic nou are plantarea lui, tinta randul lui 11.R3- / 11.P3-. +// A PATRA REVIZIE (2026-10-19, 6 constatari R4-01..R4-06): regula de proiectare a lui --witness-url (agent-cli.mjs, README) si paznicii ei, fiecare +// cu plantarea lui si tinta 12.R4- / 12.P4-; paznicul `martor-adusa-vina` are doua plantari, una pe fiecare sens (aduse FAILED / ramura 2). +// A CINCEA REVIZIE (2026-10-19, 6 constatari R5-01..R5-06): verify e o EVALUARE COMPLETA cu un singur verdict (agent-cli.mjs, README); paznicii ei +// (ordinea claselor in verdict, clasa fiecarei chitante, copia veche, tipurile, asteptarea pe 429) au plantarile lor, tinta 13.R5- / 13.P5-, iar +// plantarile NEUTRE rastoarna ordinea in care vin dovezile si cer proba verde intreaga. Plantarea scurtaturii `--witness ocolit` a disparut cu scurtatura. +// A SASEA REVIZIE (2026-10-19, verificarea tintita a reparatiilor R5, N1..N3): o chitanta fara --rpc ca dovada NEMASURATA [rpc] (doua plantari, una pe +// fiecare sens), `receipt` care scrie numai chitanta verificata a capului cerut (id, registru, martorul fixat, fiecare cu plantarea lui) si tipurile pe +// drumul "nimic nou" al lui submit; tinta 14.N. + + +// Copia pastreaza asezarea relativa (dosarul componentei si surorile ei, `../aere-verify-layer` sau `../verify-layer`, `../proof-kinds`), derivata +// din importurile modulului, in arborele de dezvoltare si in copia publicata. Verificatorul AIP-23 si gateway-ul (daca sunt) se dau probei +// din locul lor, neatinse: AERE_VERIFY_PROOF, AERE_GATEWAY; plantarea usii gateway-ului lucreaza pe o copie a dosarului cloud-gateway. +// node control-negativ-martor-agenti.mjs iesire 0 = martorul verde, acoperirea intreaga si toate plantarile rosii pe randul lor +import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const MA = 'martor-agenti.mjs', MS = 'martor-agenti-serviciu.mjs', CLI = 'agent-cli.mjs', PKF = '../proof-kinds/proof-kinds.mjs', GWF = 'gateway'; +const PLANTARI = [ + // [nume, fisier, tipar, inlocuire, randul probei (inceputul numelui lui)] + // ---- cererea (verificaCapSemnat) + ['plicul neobiect nejudecat (forma)', MA, 'if (!(obiect(env) && obiect(env.statement) /*PAZA-MARTOR:forma*/)) return', 'if (false) return', '2. CONTROL ritm pe client'], + ['forma capului nejudecata', MA, 'if (!(formaBuna /*PAZA-MARTOR:forma*/)) return', 'if (false) return', '1. CONTROL: forma capului'], + ['continutul registrului primit in cerere', MA, 'if (!(inPlus.length === 0 /*PAZA-MARTOR:continut*/)) return', 'if (false) return', '1. CONTROL: intrarile registrului langa cap'], + ['sesiunea nederivata din agent si politica', MA, 'if (!(st.session === sessionId(st.agentId, st.policyHash) /*PAZA-MARTOR:sesiune*/)) return', 'if (false) return', '1. CONTROL: o sesiune care nu e cea derivata'], + ['integritatea capului nejudecata', MA, 'if (!(sha(text) === env.statementHash /*PAZA-MARTOR:integritate*/)) return', 'if (false) return', '1. CONTROL: declaratia schimbata fara hash refacut'], + ['cheia agentului poate fi oricare', MA, 'if (!(!!pub && pub.asymmetricKeyType === ALG /*PAZA-MARTOR:cheie*/)) return', 'if (!(!!pub /*PAZA-MARTOR:cheie*/)) return', '1. CONTROL: un cap semnat cu o cheie Ed25519'], + ['agentId nederivat din cheie', MA, 'if (!(agentIdFromKey(pub) === st.agentId /*PAZA-MARTOR:identitate*/)) return', 'if (false) return', '1. CONTROL: cheia lui A semneaza un cap cu id-ul lui B'], + ['semnatura capului nejudecata', MA, 'if (!(verificaSemnaturaText(text, pub, env.signature.signature) /*PAZA-MARTOR:semnatura*/)) return', 'if (false) return', '1. CONTROL: declaratia schimbata si hash-ul refacut'], + ['momentul cererii nejudecat fata de ceasul martorului', MA, 'if (!(Math.abs(t - Number(acumS)) <= toleranta /*PAZA-MARTOR:timp*/)) return', 'if (false) return', '1. CONTROL: un cap declarat cu 400 s'], + // ---- capul de jurnal si chitanta (verificaPunct, verificaChitanta) + ['cheia care semneaza capul de jurnal nu trebuie sa fie a martorului numit', MA, 'if (!(!!pub && pub.asymmetricKeyType === ALG && witnessIdFromKey(pub) === witnessId /*PAZA-MARTOR:martor*/)) return', 'if (false) return', '3. CONTROL chitanta: capul de jurnal semnat de alta cheie'], + ['semnatura martorului pe capul de jurnal nejudecata', MA, 'if (!(verificaSemnaturaText(JSON.stringify(env.statement), pub, s.signature) /*PAZA-MARTOR:punct-semnatura*/)) return', 'if (false) return', '3. CONTROL chitanta: capul de jurnal schimbat (hash refacut'], + ['forma capului de jurnal nejudecata', MA, 'if (!(obiect(env) && env.kind === CHECKPOINT_KIND + \'-attestation\' && formaPunctului(st) /*PAZA-MARTOR:punct-forma*/)) return', 'if (false) return', '3. CONTROL chitanta: capul de jurnal cu marimea 0'], + ['integritatea capului de jurnal nejudecata', MA, 'if (!(H32.test(sir(env.statementHash)) && sha(JSON.stringify(st)) === env.statementHash /*PAZA-MARTOR:punct-integritate*/)) return', 'if (false) return', '3. CONTROL chitanta: capul de jurnal schimbat fara hash refacut'], + ['forma chitantei nejudecata', MA, '&& Array.isArray(w.path) /*PAZA-MARTOR:chitanta-forma*/)) return', '&& Array.isArray(w.path) || true)) return', '3. CONTROL chitanta: chitanta fara leafHash'], + ['martorul FIXAT de verificator nejudecat', MA, 'if (!(!fixati || fixati.includes(st.witness) /*PAZA-MARTOR:martor*/)) return', 'if (false) return', '9.R1 un martor COERENT'], + ['frunza nelegata de momentul primirii', MA, 'if (!(frunzaMartor(w.head.statementHash, w.receivedAtMs) === w.leafHash /*PAZA-MARTOR:frunza*/)) return', 'if (false) return', '3. CONTROL chitanta: alt moment de primire'], + ['dovada de includere falsa primita', MA, 'if (!(w.treeSize === st.treeSize && verifyInclusion({ leafHash: w.leafHash, index: w.leafIndex, treeSize: w.treeSize, path: w.path, rootHash: st.root }) /*PAZA-MARTOR:includere*/)) return', 'if (false) return', '3. CONTROL chitanta: un element al drumului schimbat'], + ['regula de primire a martorului nejudecata in chitanta', MA, 'if (!(Math.abs(C - R) <= toleranta /*PAZA-MARTOR:primire*/)) return', 'if (false) return', '3. CONTROL: o chitanta a unui martor care a scris in frunza'], + ['promisiunea nejudecata', MA, 'verificaSemnaturaMartor(pr, st.witness).ok /*PAZA-MARTOR:promisiune*/)) return', 'verificaSemnaturaMartor(pr, st.witness).ok || true)) return', '3. CONTROL chitanta: promisiunea cu alt moment'], + ['capul martorizat nu mai trebuie sa fie al registrului verificat', MA, '&& (!h.previous || (!!pe && pe.hash === h.previous.hash)) /*PAZA-MARTOR:registru*/)) return', '&& true) && false) return', '3. CONTROL chitanta: judecata pe alta ramura'], + // ---- ancorele (ancoreDinChitante) + ['lantul notarizarii nu mai trebuie sa fie al capului de jurnal si cel cerut', MA, 'if (!(lantNotar === plic.statement.chainId && (!chain || chain === plic.statement.chainId) /*PAZA-MARTOR:lant*/)) {', 'if (false) {', '4. CONTROL: chitanta pe alta ramura'], + ['plicul judecat de verificatorul AIP-23 nu mai trebuie sa fie acesta', MA, "if (!(String(v.statementHash || '').toLowerCase() === plic.statementHash /*PAZA-MARTOR:lant*/)) {", 'if (false) {', '4. CONTROL: verificatorul a judecat alt statementHash'], + ['semnatura martorului la verificatorul AIP-23 nu mai trebuie sa fie PASSED', MA, "if (!(!!sg && sg.state === 'PASSED' /*PAZA-MARTOR:lant*/)) {", 'if (false) {', '4. CONTROL: semnatura martorului FAILED'], + ['finalitatea nu mai trebuie sa fie PASSED', MA, "if (!(fs2.stare === 'PASSED' /*PAZA-MARTOR:lant*/)) {", 'if (false) {', '4. CONTROL: finalitate in asteptare'], + ['primirea dupa momentul lantului primita', MA, 'if (!(l.receivedAt <= F + toleranta /*PAZA-MARTOR:primire*/)) {', 'if (false) {', '4. CONTROL: lantul a vazut capul de jurnal INAINTE'], + ['intarzierea maxima nejudecata', MA, 'if (!(F - l.receivedAt <= mmd + toleranta /*PAZA-MARTOR:intarziere*/)) {', 'if (false) {', '4. CONTROL: capul de jurnal vazut de lant la 4000 s'], + ['radacina neverificata pe lant (momentul luat din notarizarea scrisa in chitanta)', MA, 'const fs2 = firstSeenFromVerdict(v);', "const fs2 = { stare: 'PASSED', at: Number(n.firstSeen) };", '4. CONTROL: finalitate FAILED'], + ['lantul cerut nevalid primit de biblioteca', MA, 'if (!(chain == null || (Number.isSafeInteger(chain) && chain > 0))) throw', 'if (false) throw', '9.R5 agent-cli verify --chain 2800x'], + // ---- lista intreaga a capetelor martorului (verificaCapeteMartorului) + // (tinta mutata la a treia revizie: de la R3-04 chitanta ultimului cap prinde si ea ramura din 9.R2, deci randul acela nu mai deosebea paznicul) + ['un cap martorizat care nu e in registrul aratat primit', MA, 'if (!(!!e && e.hash === h.hash /*PAZA-MARTOR:registru*/)) {', 'if (false) {', '11.P3-04b CONTROL'], + ['o dovada de echivocare tinuta de martor ignorata', MA, 'if (!(eq.length === 0 /*PAZA-MARTOR:echivocare*/)) {', 'if (false) {', '9.R2 CONTROL: --witness-url'], + // ---- deschiderea starii + ['un rand de frunza stricat in mijloc taiat in tacere', MA, 'if (!(bun /*PAZA-MARTOR:frunze-mijloc*/)) throw', 'if (false) throw', '9.R16 o stare cu un rand de frunza stricat'], + ['randul taiat la coada ramas in jurnal', MA, "if (coada !== '' /*PAZA-MARTOR:coada*/) {", 'if (false) {', '3. CONTROL: un rand de frunza taiat la coada'], + ['un cap de jurnal peste frunze pierdute primit la deschidere (marimea)', MA, 'if (!(p.treeSize <= ciob.frunze.length /*PAZA-MARTOR:radacina-marime*/)) throw', 'if (false) throw', '9.R16 o stare cu un rand de frunza stricat'], + // revizia B3 a doua, R2-15: jumatatea "radacina" are acum plantarea ei si randul ei (o frunza din mijloc inlocuita de alt rand valid) + ['radacina capului de jurnal nejudecata la deschidere', MA, 'if (!(ciob.arbore.root(p.treeSize) === p.root /*PAZA-MARTOR:radacina*/)) throw', 'if (false) throw', '10.R2-15 o stare cu o frunza din mijloc'], + ['cioburile vechi pastrate (memoria si discul cresc)', MA, 'for (const j of [...cioburi.keys()]) if (j <= k - CIOBURI_PASTRATE) scoateCiob(j);', '', '9.R3/R12 cioburile'], + // ---- primirea (primeste, echivoca) + ['mai multe dovezi pastrate pe agent si politica', MA, '!z.eq /*PAZA-MARTOR:echivocare-una*/', 'true', '2. CONTROL: zece echivocari'], + ['dovezile pastrate fara plafon total', MA, 'echivocari.size < maxEchivocari /*PAZA-MARTOR:echivocare-plafon*/', 'true', '9.R9 un agent cu 12 politici'], + ['pastrarea unei dovezi noi nu consuma cota agentului', MA, 'if (limitaAgent && limitaAgent(proof.agentId) /*PAZA-MARTOR:ritm*/) return', 'if (false) return', '9.P11 CONTROL (paznicul nou al lui R11)'], + ['echivocarea nepastrata (fara index)', MA, 'echivocari.set(pid, q) /*PAZA-MARTOR:echivocare*/;', '', '2. GET heads?session'], + ['echivocarea la aceeasi pozitie nedetectata', MA, 'if (laSeq && !(laSeq.e === st.hash) /*PAZA-MARTOR:echivocare*/) return', 'if (false) return', '2. ECHIVOCARE: alta ramura a lui A'], + ['echivocarea prin predecesor nedetectata', MA, 'if (laPrev && !(laPrev.e === st.previous.hash) /*PAZA-MARTOR:echivocare*/) return', 'if (false) return', '2. ECHIVOCARE prin predecesor'], + ['"deja vazut" judecat inaintea predecesorului contrazis', MA, 'const laPrev = st.previous ? gaseste(lista, st.previous.seq) : null;', 'const laPrev = st.previous ? gaseste(lista, st.previous.seq) : null; if (laSeq && laSeq.e === st.hash) return stareaCapului(laSeq.h);', '9.R4 un cap la un seq deja tinut'], + ['un cap mai VECHI primit', MA, 'if (!(!ultim || st.seq > ultim.q) /*PAZA-MARTOR:vechi*/) return', 'if (false) return', '2. CONTROL: un cap mai VECHI'], + ['consistenta capetelor nejudecata (succesorul)', MA, 'if (!(prevBun /*PAZA-MARTOR:succesor*/)) return', 'if (false) return', '2. CONTROL: un cap care nu numeste ca predecesor'], + ['limita de ritm pe agent scoasa', MA, 'if (limitaAgent && limitaAgent(st.agentId) /*PAZA-MARTOR:ritm*/) return', 'if (false) return', '2. CONTROL ritm pe agent'], + ['limita pe agent judecata pe corpul neautentificat', MA, 'const v = verificaCapSemnat(env, { acumS: acumMs / 1000, toleranta });', "if (limitaAgent && env && env.statement && limitaAgent(String(env.statement.agentId))) return { cod: 429, corp: { error: 'rate_limited' } }; const v = verificaCapSemnat(env, { acumS: acumMs / 1000, toleranta });", '2. CONTROL: 14 cereri cu semnatura rea'], + ['cota pe agent consumata de un cap deja vazut (retrimiterea capului public)', MA, 'if (dupaCap.has(id)) return stareaCapului(id);', "if (limitaAgent && limitaAgent(st.agentId)) return { cod: 429, corp: { error: 'rate_limited' } }; if (dupaCap.has(id)) return stareaCapului(id);", '9.R11 un strain retrimite'], + ['bugetul jurnalului pe ora scos', MA, 'if (bugetPlin /*PAZA-MARTOR:buget*/) return', 'if (false) return', '9.R12 bugetul jurnalului'], + ['ciobul plin nerotit', MA, 'if (!(cur.frunze.length < maxFrunze /*PAZA-MARTOR:plin*/)) roteste();', 'if (false) roteste();', '9.R3/R12 cioburile'], + // ---- scrierile durabile, paginarea, erorile publice + ['o scriere scurta neterminata (un singur writeSync)', MA, 'off += n;', 'off = buf.length;', '9.R16 fiecare fs.writeSync'], + ['o scriere cazuta lasata pe disc (fara taiere inapoi)', MA, 'try { fs.ftruncateSync(fd, inainte); fs.fsyncSync(fd); } catch { try { fs.truncateSync(f, inainte); } catch { e.taiereEsuata = true; } }', '', '9.R16 o scriere a frunzei care cade'], + ['mesajul notarizarii cazute public in lastError', MA, "else p.lastError = rez && typeof rez.cod === 'string' && /^[a-z_]{1,40}$/.test(rez.cod) ? rez.cod : 'notarization_failed';", 'else p.lastError = String(rez && rez.motiv);', '9.R18 o notarizare cazuta'], + ['GET heads nepaginat', MA, 'const a = primulDeLa(l, from); const pagina = l.slice(a, a + limit);', 'const a = 0; const pagina = l;', '9.R10 GET heads?session'], + ['submit iese 1 cand martorul tine deja intrarea ceruta', MA, 'if (ultim.seq === s && e && e.hash === ultim.hash) {', 'if (false) {', '9.R7 submit fara nimic nou'], + // ---- serviciul + ['limita de ritm pe client scoasa', MS, 'if (peClient(client) /*PAZA-MARTOR:ritm*/) return', 'if (false) return', '2. CONTROL ritm pe client'], + ['plafonul de chei al limitatorului scos', MS, 'if (!(m.size < maxChei /*PAZA-MARTOR:ritm-plafon*/)) {', 'if (false) {', '9.R13 limitatorul'], + ['cheile expirate ale limitatorului necuratate', MS, 'if (m.size >= maxChei && acum - ultimaCuratare >= 1000) curata(acum);', '', '9.R13 limitatorul'], + ['cheia unui client IPv6 pe adresa intreaga (nu pe /64)', MS, 'return tot.slice(0, 4).map(', 'return tot.slice(0, 8).map(', '9.R13 cheia clientului'], + ['mesajul unei erori interne in raspunsul public', MS, "code: e && /^E[A-Z]+$/.test(String(e.code)) ? 'io_error' : 'internal'", "code: 'io_error', detail: String(e && e.message)", '9.R16/R18 GET pe un cap'], + ['notarizarea pe alt lant decat 28001', MS, 'if (!(chainId === LANT /*PAZA-MARTOR:sandbox*/)) return', 'if (false) return', '3. CONTROL: notarizatorul pe un RPC care serveste lantul 2800'], + // ---- linia de comanda a verificatorului + ['martorii fixati nu mai ajung la verificare', CLI, 'let fixati = martori.length ? martori : null;', 'let fixati = null;', '9.R17 agent-cli verify'], + ['--chain nevalid primit (paza care cade deschisa)', CLI, 'if (!/^[1-9]\\d{0,14}$/.test(String(v))) throw new Folosire(', 'if (false) throw new Folosire(', '9.R5 agent-cli verify --chain 2800x'], + ['un martor care nu raspunde la --witness-url luat drept verde', CLI, "if (!c.ok) adauga({ sursa: 'list', clasa: 'NEMASURATA',", "if (!c.ok) adauga({ sursa: 'list', clasa: 'VERIFICATA',", '9.R2 CONTROL: --witness-url'], + // ---- a doua revizie adversariala B3 (2026-10-19): fiecare paznic nou, cu randul lui 10.R2- / 10.P2- + // R2-01: lista capetelor, semnata de martor pagina cu pagina, din ultimele 300 s + ['forma paginii de capete nejudecata', MA, 'if (!(formaBuna /*PAZA-MARTOR:pagina-forma*/)) return', 'if (false) return', '10.P2-01 CONTROL'], + ['integritatea paginii de capete nejudecata', MA, 'if (!(H32.test(sir(env.statementHash)) && sha(JSON.stringify(st)) === env.statementHash /*PAZA-MARTOR:pagina-integritate*/)) return', 'if (false) return', '10.P2-01 CONTROL'], + ['semnatura martorului pe pagina de capete nejudecata', MA, 'if (!(s.ok /*PAZA-MARTOR:pagina-semnatura*/)) return', 'if (false) return', '10.R2-01 --witness '], + ['o pagina veche (reluata) primita drept lista de acum', MA, 'if (!(Math.abs(t - Number(acumS)) <= toleranta /*PAZA-MARTOR:pagina-vechi*/)) return', 'if (false) return', '10.R2-01 --witness '], + ['paginile care nu dau numarul si ultimul cap semnate primite', MA, 'if (!(aduna /*PAZA-MARTOR:pagina-numar*/)) return', 'if (false) return', '10.P2-01 CONTROL'], + ['lista citita din campurile NESEMNATE de langa pagina', MA, 'witness = st.witness; heads.push(...st.heads);', 'witness = st.witness; heads.push(...(Array.isArray(r.j.heads) ? r.j.heads : []).map((h) => ({ seq: h.seq, hash: h.hash, id: h.id })));', '10.R2-01 --witness '], + ['lista semnata de alt martor decat cel fixat primita', CLI, 'else if (!(!fixati || fixati.includes(c.witness)) /*PAZA-MARTOR:martor-url*/)', 'else if (false)', '10.R2-15 agent-cli verify --witness '], + // R2-02: dovezile de echivocare legate de ciobul capului contrazis; plafonul = marginea prin constructie + ['dovada legata de ciobul curent, nu de al capului contrazis', MA, 'k: tinut.k /*PAZA-MARTOR:echivocare-ciob*/', 'k: cur.k', '10.R2-02 maxFrunze 4'], + ['plafonul dovezilor fix (2.000), nu marginea lor prin constructie', MA, 'if (maxEchivocari == null) maxEchivocari = CIOBURI_PASTRATE * maxFrunze;', 'if (maxEchivocari == null) maxEchivocari = 2000;', '10.P2-02 CONTROL'], + // R2-03: la plafonul de chei se scoate cheia cea mai veche + ['la plafonul de chei clientul NOU e refuzat (nu e scoasa cheia cea mai veche)', MS, '{ m.delete(m.keys().next().value); scoase++; }', 'return true;', '10.R2-03 limitatorul'], + // R2-04: o lista cu elemente care nu sunt capete + ['un element care nu e cap primit de verificaCapeteMartorului', MA, 'if (!(rau < 0 /*PAZA-MARTOR:capete-forma*/)) return', 'if (false) return', '10.R2-04 verificaCapeteMartorului'], + // R2-05: sub un martor fixat, numai chitante ale lui + ['ancora simpla primita sub un martor fixat', CLI, 'if (fixare /*PAZA-MARTOR:martor-simple*/) adauga(', 'if (false) adauga(', '10.R2-05 registrul antedatat'], + ['--witness fara nicio chitanta a lui primit', CLI, 'if (!(!martori.length || dovezi.some((d) => d.chitanta) || (tinute && tinute.length)) /*PAZA-MARTOR:martor-fara-chitanta*/)', 'if (false)', '10.R2-05 registrul antedatat'], + // R2-07: un ciob nu se scoate pana nu i s-a notarizat capul de jurnal final + ['rotirea scoate un ciob nenotarizat', MA, 'if (!(!plin || !rotireaPierdeNenotarizat() /*PAZA-MARTOR:rotire-notarizata*/)) {', 'if (false) {', '10.R2-07 maxFrunze 2'], + ['la deschidere un ciob vechi nenotarizat sters', MA, 'if (!(faraLant || finalNotarizatPeDisc(k) /*PAZA-MARTOR:ciob-vechi-notarizat*/)) {', 'if (false) {', '10.P2-07 CONTROL'], + // R2-08: ciobul nou se naste intreg; un ciob fara ciob.json isi reface legatura + ['ciobul nou nascut pe jumatate (dosarul inaintea lui ciob.json)', MA, "scrieAtomic(path.join(t, 'ciob.json'), meta); fs.renameSync(t, d) /*PAZA-MARTOR:ciob-intreg*/; fsyncDosar(dc);", 'fs.renameSync(t, d); scrieAtomic(fcb, meta);', '10.R2-08 rotirea cade'], + ['legatura ciobului fara ciob.json nerefacuta', MA, 'previousShardFinal: legat /*PAZA-MARTOR:ciob-legatura*/', 'previousShardFinal: null', '10.P2-08 CONTROL'], + // R2-09: ruta capului de jurnal pentru un ciob pastrat (si inghetat) + ['ruta capului de jurnal numai pe ciobul curent', MA, 'const c = shard == null ? cur : cioburi.get(Number(shard)) /*PAZA-MARTOR:ciob-cerut*/;', 'const c = cur;', '10.R2-09 un monitor'], + // R2-10: lacatul isi poarta pornirea masinii si a procesului + ['lacatul de dinainte de ultima pornire a masinii tinut', MA, 'if (!(o.boot == null || aceeasiPornire(o.boot, boot) /*PAZA-MARTOR:lacat-pornire*/)) return', 'if (false) return', '10.R2-10 lacatTinut'], + ['lacatul cu pid-ul luat de alt proces tinut', MA, 'if (!(acum == null || acum === String(o.start) /*PAZA-MARTOR:lacat-proces*/)) return', 'if (false) return', '10.R2-10 lacatTinut'], + // R2-11 / R2-12: codurile de iesire ale lui martor-agenti.mjs si valorile goale ale lui --witness + ['receipt iese 1 la un martor care raspunde 503 sau 429', MA, 'if (r.status >= 500 || r.status === 429 /*PAZA-MARTOR:cod-indisponibil*/) {', 'if (false) {', '10.R2-11 receipt'], + ['verify-receipt iese 1 pe raspunsul unui cap pending', MA, "if (obiect(x) && !esteChitanta(x) && x.status === 'pending' /*PAZA-MARTOR:cod-pending*/) {", 'if (false) {', '10.R2-11 receipt'], + ['valorile goale ale lui --witness aruncate (verify-receipt nefixat)', MA, "(rest[i + 1] === undefined ? [''] : String(rest[i + 1]).split(',')) /*PAZA-MARTOR:steag-gol*/", "(rest[i + 1] === undefined ? [] : String(rest[i + 1]).split(',').filter(Boolean))", '10.R2-12 verify-receipt'], + // R2-13: bugetul jurnalului citit inaintea cotei agentului, consumat dupa ea + ['cota agentului consumata inaintea bugetului jurnalului', MA, 'const bugetPlin = ', "if (limitaAgent && limitaAgent(st.agentId)) return { cod: 429, corp: { error: 'rate_limited' } }; const bugetPlin = ", '10.R2-13 cota pe agent 3'], + ['bugetul jurnalului consumat la citire (serviciul fara `ar`)', MS, "{ ar: () => acceptate.ar('jurnal') /*PAZA-MARTOR:buget-citit*/ }", '{}', '10.P2-13 CONTROL prin serviciu'], + // R2-14: Retry-After prin usa gateway-ului + ['usa gateway-ului arunca Retry-After', GWF, "? { 'retry-after': ra } : {})", '? {} : {})', '10.R2-14 prin usa gateway-ului'], + // ---- a treia revizie adversariala B3 (2026-10-19): fiecare paznic nou, cu randul lui 11.R3- / 11.P3- + // R3-01: un ciob de scos se redenumeste intai; un rest al formei de dinainte se sterge la deschidere + ['ciobul de scos sters pe loc, fara sa fie redenumit intai', MA, 'fs.renameSync(d, t) /*PAZA-MARTOR:ciob-sters-intai*/; fsyncDosar(dc); tinta = t;', 'void t;', '11.R3-01 o oprire'], + ['restul unei stergeri intrerupte tinut drept un ciob la deschidere', MA, 'if (restDeStergere(k) /*PAZA-MARTOR:ciob-rest*/) {', 'if (false) {', '11.P3-01 CONTROL'], + // revizia B3 a patra, R4-06: fiecare jumatate a restului cu plantarea si randul ei + ['un rest fara ciob.json sau frunze.ndjson tinut drept un ciob', MA, "(!fs.existsSync(path.join(d, 'ciob.json')) || !fs.existsSync(path.join(d, 'frunze.ndjson'))) /*PAZA-MARTOR:ciob-fara-fisiere*/", 'false', '11.P3-01 CONTROL'], + ['un ciob marcat .de-sters tinut drept un ciob', MA, "fs.existsSync(path.join(dc, String(k), '.de-sters')) /*PAZA-MARTOR:ciob-marcat*/", 'false', '12.R4-06 redenumirea'], + // R3-02: dovada care traieste cel mai mult pe sesiune + ['o dovada contra unui cap din ciobul vechi tine locul celei care conteaza', MA, 'const inlocuieste = !!q0 && q0.k < tinut.k /*PAZA-MARTOR:echivocare-mai-noua*/;', 'const inlocuieste = false;', '11.R3-02 agentul X'], + // R3-03: un steag fara valoare, sau cu valoarea goala, e folosire gresita + ['un steag fara valoare primit in tacere (martor-agenti.mjs)', MA, "if (!(v !== undefined && v !== '' && !/^--[a-z]/.test(v)) /*PAZA-MARTOR:steag-fara-valoare*/) throw", 'if (false) throw', '11.R3-03 agent-cli verify'], + ['un steag fara valoare primit in tacere (agent-cli.mjs)', CLI, "if (!(v !== undefined && !/^--[a-z]/.test(v) && (v !== '' || GOALE_PERMISE.includes(f))) /*PAZA-MARTOR:steag-fara-valoare*/) throw", 'if (false) throw', '11.R3-03 agent-cli verify'], + // R3-04: chitanta ultimului cap tinut de martor, sub --witness-url + ['chitanta unui cap tinut nu mai e ceruta (marginea o dau chitantele alese de agent)', CLI, "if (dovezi.some((d) => d.clasa === 'VERIFICATA' && d.id === h.id) /*PAZA-MARTOR:martor-chitanta-ceruta*/) continue;", 'if (true) continue;', '11.R3-04 martorul fixat'], + // revizia B3 a cincea, R5-01: numai o chitanta VERIFICATA a capului scuteste aducerea (forma veche: orice chitanta cu acelasi id, verificata sau nu) + ['o chitanta NEVERIFICATA cu acelasi id (o copie veche) scuteste aducerea chitantei capului tinut', CLI, "if (dovezi.some((d) => d.clasa === 'VERIFICATA' && d.id === h.id) /*PAZA-MARTOR:martor-chitanta-ceruta*/) continue;", 'if (dovezi.some((d) => d.id === h.id)) continue;', '13.R5-01 copia VECHE'], + // R3-05: codurile lui receipt si submit + ['submit "nimic nou" iese 0 pe o stare a capului care nu e a lui', MA, "if (!(r.status === 200 && obiect(r.j) && r.j.id === ultim.id && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-nimic-nou*/) {", 'if (false) {', '11.R3-05 receipt'], + ['receipt pe un 200 care nu e JSON (exceptie, 1)', MA, "if (!(obiect(r.j) && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-ne-json*/) {", 'if (false) {', '11.R3-05 receipt'], + // ---- a patra revizie adversariala B3 (2026-10-19): regula de proiectare a lui --witness-url si paznicii ei, cu randurile 12.R4- / 12.P4- + ['numai ULTIMUL cap tinut cu chitanta (capetele intermediare fara)', CLI, 'tinute || [] /*PAZA-MARTOR:martor-fiecare-cap*/', '(tinute || []).slice(-1)', '12.R4-03 martorul tine'], + ['id-ul capului unei chitante citit fara garda (exceptie pe o forma gresita)', CLI, "const idCap = (a) => { const p = M.chitantaDin(a); const w = p && typeof p === 'object' ? p.witnessReceipt : null; const h = w && typeof w === 'object' ? w.head : null; return h && typeof h === 'object' && typeof h.statementHash === 'string' ? h.statementHash : null; } /*PAZA-MARTOR:chitanta-id-sigur*/;", 'const idCap = (a) => M.chitantaDin(a).witnessReceipt.head.statementHash;', '12.R4-02 proxy'], + // (revizia a cincea: paznicul `martor-adusa-vina` al formei de dinainte e acum clasa dovezii, pe ORICE drum: `chitanta-alt-registru`, cu cele doua sensuri) + ['o chitanta care nu tine LOCAL judecata vina registrului, oricare ar fi regula (o includere stricata face un FAILED)', CLI, "const vina = l.rule === 'registru' /*PAZA-MARTOR:chitanta-alt-registru*/ || falsificata;", 'const vina = true;', '13.R5-05 chitanta agentului cu includerea stricata'], + ['o chitanta a martorului fixat pentru alt registru (o ramura) luata drept NEMASURAT', CLI, "l.rule === 'registru' /*PAZA-MARTOR:chitanta-alt-registru*/", 'false', '12.P4-03b CONTROL'], + ['submit pe un 200/202 care nu e starea capului trimis iese 0', MA, "(r.status === 200 || r.status === 202) && !(obiect(r.j) && H32.test(sir(r.j.id)) && r.j.session === env.statement.session && r.j.seq === env.statement.seq && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-trimis*/", 'false', '12.R4-04 submit'], + ['un steag necunoscut primit in tacere (agent-cli.mjs)', CLI, 'if (!(STEAGURI[cmd].includes(s) /*PAZA-MARTOR:steag-necunoscut*/)) throw', 'if (false) throw', '12.R4-05 agent-cli'], + ['un steag necunoscut primit in tacere (martor-agenti.mjs)', MA, 'if (!(STEAGURI[cmd].includes(s) /*PAZA-MARTOR:steag-necunoscut*/)) throw', 'if (false) throw', '12.R4-05 agent-cli'], + ['un steag dat de doua ori primit (agent-cli.mjs)', CLI, "if (!(!vazute.has(s) || s === '--witness') /*PAZA-MARTOR:steag-dublu*/) throw", 'if (false) throw', '12.R4-05 agent-cli'], + ['un steag dat de doua ori primit (martor-agenti.mjs)', MA, "if (!(!vazute.has(s) || s === '--witness') /*PAZA-MARTOR:steag-dublu*/) throw", 'if (false) throw', '12.R4-05 agent-cli'], + ['departajarea a doua dovezi la deschidere rasturnata (ramane cea veche)', MA, 'vechi && vechi.k >= x.k /*PAZA-MARTOR:echivocare-departajare*/', 'vechi && vechi.k <= x.k', '12.R4-06 o inlocuire'], + // ---- a cincea revizie adversariala B3 (2026-10-19): verify ca EVALUARE COMPLETA cu un singur verdict, tipurile, asteptarea pe 429; randurile 13.R5- / 13.P5- + // R5-01: verdictul se da DUPA ce s-a adunat tot, iar o vina dovedita trece inaintea oricarui NEMASURAT + ['un NEMASURAT judecat inaintea unei vine dovedite (verdictul dupa ordinea claselor)', CLI, "const verdict = vina.length ? 'FAILED' : nemasurate.length ? 'NOT MEASURED' : 'OK' /*PAZA-MARTOR:verdict-vina-intai*/;", "const verdict = nemasurate.length ? 'NOT MEASURED' : vina.length ? 'FAILED' : 'OK';", '13.R5-01 capul seq 4'], + ['un NEMASURAT iesit pe drum, inaintea judecatii (forma de dinainte de a cincea revizie)', CLI, 'const adauga = (d) => { dovezi.push({ ceruta: true, ...d }); };', "const adauga = (d) => { dovezi.push({ ceruta: true, ...d }); if (d.clasa === 'NEMASURATA') { console.error('NOT MEASURED: ' + d.detail + ' (planted: an early exit)'); process.exit(2); } };", '13.R5-01 capul seq 4'], + ['o copie veche a unei chitante ramane ceruta langa chitanta verificata a aceluiasi cap', CLI, "dovezi.some((x) => x !== d && x.clasa === 'VERIFICATA' && x.id === d.id) /*PAZA-MARTOR:copie-veche*/", 'false', '13.R5-01 copia VECHE'], + // R5-03 / R5-05: clasa unei chitante care nu tine: vina agentului numai pe o semnatura INVALIDA a martorului fixat (predata) sau pe alt registru + ['o pana a notarului sau a RPC-ului ales de verificator judecata vina registrului', CLI, "clasa: 'NEMASURATA' /*PAZA-MARTOR:lant-nemasurat*/", "clasa: 'VINA'", '13.R5-03 registru CINSTIT'], + ['o chitanta a agentului cu semnatura martorului fixat INVALIDA luata drept NEMASURATA', CLI, "const falsificata = sursa === 'agent' && numit !== null && (!fixati || fixati.includes(numit)) /*PAZA-MARTOR:chitanta-falsificata*/;", 'const falsificata = false;', '13.P5-05 CONTROL vina agentului'], + ['o chitanta ADUSA cu semnatura martorului invalida judecata vina registrului (o adresa face un FAILED)', CLI, "sursa === 'agent' && numit !== null", 'numit !== null', '13.P5-05 CONTROL un proxy'], + // R5-04: regula (c), tipurile judecate inainte de folosire + ['submit primeste un 200/202 al carui `status` nu e un sir ({"toString":0}): --out scris si iesire 0', MA, "r.j.seq === env.statement.seq && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-trimis*/", 'r.j.seq === env.statement.seq) /*PAZA-MARTOR:cod-trimis*/', '13.R5-04 submit'], + ['un receivedAtMs negativ primit de forma chitantei (exceptia vine mai jos, in frunza)', MA, 'w.receivedAtMs >= 0 /*PAZA-MARTOR:chitanta-tipuri*/', 'true', '13.R5-04 un proxy'], + ['un camp care trebuie sa fie sir trecut prin String() (exceptie pe {"toString":0})', MA, "const sir = (x) => (typeof x === 'string' ? x : '');", 'const sir = (x) => String(x);', '13.R5-04 un proxy'], + // R5-06: o citire peste limita spune cand se poate reincerca, iar verificatorul asteapta + ['pe un 429 cu Retry-After verificatorul nu mai asteapta', MA, 'if (!(r.status === 429 && Number.isSafeInteger(r.retryAfter) && r.retryAfter <= buget.ramasS && i < 50) /*PAZA-MARTOR:asteapta-429*/) return r;', 'if (true) return r;', '13.R5-06 serviciul'], + ['429-ul unei citiri fara Retry-After', MS, "{ 'retry-after': String(ra) } /*PAZA-MARTOR:get-retry-after*/", '{}', '13.R5-06 serviciul'], + // ---- a sasea revizie B3 (2026-10-19, verificarea tintita a reparatiilor R5): N1..N3, randurile 14.N + // N1: o chitanta fara --rpc e o dovada NEMASURATA [rpc], judecata la locul ei; doua plantari pe acelasi paznic, una pe fiecare sens (iesirea timpurie + // a formei de dinainte, care ascunde ramura aratata de lista; si paznicul scos, care ia chitanta drept VERIFICATA fara lant) + ['o chitanta fara --rpc iese pe drum cu 2, inaintea judecatii (forma de dinainte de a sasea revizie: folosire gresita)', CLI, 'if (!rpc /*PAZA-MARTOR:fara-rpc*/) return {', "if (!rpc) throw new Folosire('a witness receipt needs --rpc (planted: the early exit of before the sixth review)'); if (false) return {", '14.N1 fara --rpc'], + ['o chitanta fara --rpc judecata VERIFICATA (marginea luata fara lant)', CLI, 'if (!rpc /*PAZA-MARTOR:fara-rpc*/) return {', 'if (false) return {', '14.N1 fara --rpc'], + // N2: receipt scrie numai chitanta capului cerut, a registrului dat, cu semnatura martorului verificata (cu cel fixat, daca e dat) + ['receipt scrie raspunsul adresei fara sa il verifice (forma de dinainte de a sasea revizie)', MA, 'if (!(!!l && l.ok) /*PAZA-MARTOR:receipt-verificata*/) {', 'if (false) {', '14.N2 receipt printr-un proxy'], + ['receipt nu cere ca chitanta sa fie a capului cerut (--id)', MA, 'capPrimit === idc /*PAZA-MARTOR:receipt-id*/', 'true', '14.N2 receipt printr-un proxy'], + ['receipt nu leaga chitanta de registrul dat (--ledger)', MA, 'ledger /*PAZA-MARTOR:receipt-registru*/,', 'ledger: null,', '14.N2 receipt fata de --ledger'], + ['receipt nu cere martorul fixat prin --witness', MA, 'martori: martori.length ? martori : null /*PAZA-MARTOR:receipt-fixat*/', 'martori: null', '14.N2 receipt fata de --ledger'], + // N3: regula (c) pe drumul "nimic nou" al lui submit + ['submit "nimic nou" foloseste `latest` (campul nesemnat) fara sa ii judece tipurile', MA, 'if (!(ultim === null || (obiect(ultim) && Number.isSafeInteger(ultim.seq) && ultim.seq >= 0 && HASH_INTRARE.test(sir(ultim.hash)) && H32.test(sir(ultim.id)))) /*PAZA-MARTOR:nimic-nou-latest*/) {', 'if (false) {', '14.N3 submit'], + ['submit "nimic nou" primeste o stare a capului cu status care nu e sir ({"toString":0}): --out scris, apoi exceptie', MA, "r.j.id === ultim.id && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-nimic-nou*/", 'r.j.id === ultim.id) /*PAZA-MARTOR:cod-nimic-nou*/', '14.N3 submit'], + // ---- vocabularul felului (proof-kinds, citit si de judecatorul martorului) si usa gateway-ului + ['vocabularul felului agent-witness-checkpoint slabit in proof-kinds', PKF, 'treeSize: [(v) => Number.isSafeInteger(v) && v >= 1,', 'treeSize: [(v) => true,', '9.R8 felul agent-witness-checkpoint'], + ['usa gateway-ului primeste un POST din browser (Origin)', GWF, "if (req.method === 'POST' && req.headers.origin !== undefined) return json(res, 403,", 'if (false) return json(res, 403,', '9.R13 gateway: un POST'], +]; +// PLANTARILE NEUTRE (revizia B3 a cincea, R5-05): schimba numai ORDINEA in care verify aduna si judeca dovezile. Verdictul e o functie de multimea +// dovezilor, nu de ordinea lor, deci cu oricare din ele proba trebuie sa ramana VERDE intreaga; un rand rosu aici e un ESEC al componentei (ordinea +// a ajuns sa conteze), numarat ca orice control cazut. [nume, fisier, tipar, inlocuire] +const NEUTRE = [ + ['ordinea adunarii rasturnata: capetele tinute (si chitantele lor aduse) inaintea lui --anchors', CLI, 'await fazaAnchors(); await fazaCapete();', 'await fazaCapete(); await fazaAnchors();'], + ['intrarile din --anchors judecate de la ultima la prima', CLI, 'for (const [i, a] of toate2.entries()) {', 'for (const [i, a] of [...toate2.entries()].reverse()) {'], + ['capetele tinute parcurse de la ultimul la primul', CLI, 'for (const h of tinute || [] /*PAZA-MARTOR:martor-fiecare-cap*/) {', 'for (const h of [...(tinute || [])].reverse()) {'], +]; +// markerii care NU au plantare, fiecare cu motivul (azi niciunul: o exceptie noua se scrie aici, cu motivul, nu se lasa neacoperita). De la a doua +// revizie (R2-15) garda priveste si agent-cli.mjs: paznicii martorului din linia de comanda poarta markeri, deci si plantari. +const EXCEPTII = []; +const PROPRII = ['agent-policy.mjs', 'agent-ledger.mjs', 'agent-aprobare.mjs', 'agent-ancora.mjs', CLI, MA, MS, 'proba-martor-agenti.mjs']; +// surorile: din importurile relative '..//' ale modulului (aceeasi asezare in arborele de dezvoltare si in copia publicata) +const SURORI = [...fs.readFileSync(path.join(AICI, MA), 'utf8').matchAll(/from '\.\.\/([^'/]+)\/([^'/]+)'/g)].map((x) => [x[1], x[2]]); +const DEV_VERIFY = path.resolve(AICI, '..', '..', 'verificator-falcon', 'verify-proof.mjs'); +const VERIFY = process.env.AERE_VERIFY_PROOF !== undefined ? process.env.AERE_VERIFY_PROOF : (fs.existsSync(DEV_VERIFY) ? DEV_VERIFY : ''); +const GW_DIR = path.resolve(AICI, '..', '..', 'cloud-gateway'); const GW = path.join(GW_DIR, 'gateway.mjs'); +const fisierPlantat = (t, d, fisier) => (fisier === GWF ? path.join(t, 'cloud-gateway', 'gateway.mjs') : path.join(d, fisier)); +function copie(cuGateway = false) { + const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-martor-ctl-')); const d = path.join(t, path.basename(AICI)); fs.mkdirSync(d); + for (const f of PROPRII) fs.copyFileSync(path.join(AICI, f), path.join(d, f)); + for (const [dir, f] of SURORI) { fs.mkdirSync(path.join(t, dir), { recursive: true }); fs.copyFileSync(path.join(AICI, '..', dir, f), path.join(t, dir, f)); } + if (cuGateway) fs.cpSync(GW_DIR, path.join(t, 'cloud-gateway'), { recursive: true, filter: (s) => !/node_modules/.test(s) }); + return { t, d }; +} +function ruleaza(d, gateway = fs.existsSync(GW) ? GW : null) { + return new Promise((resolve) => { + const c = spawn(process.execPath, [path.join(d, 'proba-martor-agenti.mjs')], { env: { ...process.env, AERE_VERIFY_PROOF: VERIFY ? path.resolve(VERIFY) : '', ...(gateway ? { AERE_GATEWAY: gateway } : {}) } }); + let out = ''; const ceas = setTimeout(() => c.kill(), 300000); + c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; }); + c.on('close', (cod) => { clearTimeout(ceas); const linii = out.split('\n'); resolve({ cod, rulat: /martor-agenti: \d+\/\d+ cum trebuia/.test(out), rosii: linii.filter((l) => l.startsWith(' RAU ')), sarite: linii.filter((l) => l.startsWith(' -- ')) }); }); + }); +} +// o plantare NEUTRA: proba rulata si verde intreaga (zero rosii, zero sarite in afara usii gateway-ului) +async function neutru([nume, fisier, din, inl]) { + const { t, d } = copie(); + try { + const f = fisierPlantat(t, d, fisier); const src = fs.readFileSync(f, 'utf8'); + if (src.split(din).length !== 2) return [false, ` STRICAT (neutra) ${nume}: tiparul apare de ${src.split(din).length - 1} ori in ${fisier}`]; + fs.writeFileSync(f, src.replace(din, () => inl)); + const r = await ruleaza(d); + if (!r.rulat) return [false, ` STRICAT (neutra) ${nume}: proba nu a ajuns la rezumat (cod ${r.cod})`]; + const sarite = r.sarite.filter((l) => !/usa gateway-ului/.test(l)); + if (!r.rosii.length && !sarite.length) return [true, ` VERDE cum trebuia (neutra) ${nume} (proba intreaga verde: ordinea nu schimba nimic)`]; + return [false, ` CONTROL CAZUT (neutra) ${nume}: ${r.rosii.length} rosii, ${sarite.length} sarite: ${[...r.rosii, ...sarite].join(' | ').slice(0, 300)}`]; + } finally { fs.rmSync(t, { recursive: true, force: true }); } +} +async function planteaza([nume, fisier, din, inl, tinta]) { + const cuGw = fisier === GWF; if (cuGw && !fs.existsSync(GW)) return [true, ` (sarit pe nume: ${nume}: gateway.mjs nu e in aceasta copie)`]; + const { t, d } = copie(cuGw); + try { + const f = fisierPlantat(t, d, fisier); const src = fs.readFileSync(f, 'utf8'); + if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: tiparul apare de ${src.split(din).length - 1} ori in ${fisier}`]; + fs.writeFileSync(f, src.replace(din, () => inl)); + const r = await ruleaza(d, cuGw ? f : undefined); + if (!r.rulat) return [false, ` STRICAT ${nume}: proba nu a ajuns la rezumat (cod ${r.cod})`]; + if (r.rosii.some((l) => l.startsWith(' RAU ' + tinta))) return [true, ` ROSU cum trebuia ${nume} (randul '${tinta}' pica)`]; + return [false, ` CONTROL CAZUT ${nume}: randul '${tinta}' a ramas verde (${r.rosii.length} rosii altundeva)`]; + } finally { fs.rmSync(t, { recursive: true, force: true }); } +} +// ACOPERIREA: fiecare aparitie a unui marker PAZA-MARTOR din MA si MS sta in tiparul unei plantari (sau pe EXCEPTII); intoarce markerii neacoperiti +function acoperire(plantari) { + const lipsa = []; let total = 0; + for (const fisier of [MA, MS, CLI]) { + const src = fs.readFileSync(path.join(AICI, fisier), 'utf8'); + const spatii = plantari.filter((p) => p[1] === fisier).map((p) => { const i = src.indexOf(p[2]); return i < 0 ? null : [i, i + p[2].length]; }).filter(Boolean); + for (const mk of src.matchAll(/PAZA-MARTOR:([a-z-]+)/g)) { + total++; + const rand = src.slice(0, mk.index).split('\n').length; + if (spatii.some(([a, b]) => mk.index >= a && mk.index < b)) continue; + if (EXCEPTII.some(([f, nume, ancora]) => f === fisier && nume === mk[1] && src.slice(Math.max(0, mk.index - 200), mk.index + 50).includes(ancora))) continue; + lipsa.push(`${fisier}:${rand} ${mk[1]}`); + } + } + return { total, lipsa }; +} +let rele = 0; +for (const [dir, f] of SURORI) if (!fs.existsSync(path.join(AICI, '..', dir, f))) { console.log(` STRICAT sora ${dir}/${f} nu e langa componenta: copia nu se poate face`); process.exitCode = 1; process.exit(); } +const ac = acoperire(PLANTARI); +if (!ac.lipsa.length) console.log(` OK acoperirea: toti cei ${ac.total} markeri PAZA-MARTOR din ${MA}, ${MS} si ${CLI} au plantarea lor (exceptii: ${EXCEPTII.length})`); + +else { rele++; console.log(` ESEC acoperirea: ${ac.lipsa.length} din ${ac.total} markeri fara plantare si fara exceptie scrisa: ${ac.lipsa.join(', ')}`); } +// martorul acoperirii: cu o plantare scoasa, garda trebuie sa numeasca exact markerul ei (altfel o garda care nu vede nimic ar iesi verde) +const scoasa = PLANTARI.findIndex((p) => p[0] === 'promisiunea nejudecata'); +const ac2 = acoperire(PLANTARI.filter((_, i) => i !== scoasa)); +if (scoasa >= 0 && ac2.lipsa.length === ac.lipsa.length + 1 && ac2.lipsa.some((x) => / promisiune$/.test(x))) console.log(` OK martorul acoperirii: fara plantarea '${PLANTARI[scoasa][0]}' garda numeste markerul ei (${ac2.lipsa.find((x) => / promisiune$/.test(x))})`); +else { rele++; console.log(` STRICAT martorul acoperirii: fara o plantare, garda nu a numit markerul ei (${JSON.stringify(ac2.lipsa).slice(0, 200)})`); } +// --numai-acoperirea: acoperirea si unicitatea fiecarui tipar in fisierul lui, fara rularile probei (o verificare rapida in timpul lucrului) +if (process.argv.includes('--numai-acoperirea')) { + const { t, d } = copie(fs.existsSync(GW)); + for (const [nume, fisier, din] of [...PLANTARI, ...NEUTRE]) { if (fisier === GWF && !fs.existsSync(GW)) continue; const n = fs.readFileSync(fisierPlantat(t, d, fisier), 'utf8').split(din).length - 1; if (n !== 1) { rele++; console.log(` STRICAT ${nume}: tiparul apare de ${n} ori in ${fisier}`); } } + fs.rmSync(t, { recursive: true, force: true }); + console.log(rele ? `ACOPERIREA: ${rele} probleme` : `ACOPERIREA: ${ac.total} markeri acoperiti, ${PLANTARI.length} tipare unice, ${NEUTRE.length} neutre (probele NU au rulat)`); process.exitCode = rele ? 1 : 0; process.exit(); +} +const c0 = copie(); const m = await ruleaza(c0.d); fs.rmSync(c0.t, { recursive: true, force: true }); +// martorul: proba ajunge la rezumat, zero rosii; un rand sarit e primit numai pe nume (usa gateway-ului cand gateway-ul nu e in aceasta copie) +const saritePermise = m.sarite.filter((l) => !/usa gateway-ului/.test(l)); +if (m.rulat && !m.rosii.length && !saritePermise.length) console.log(` OK martorul: copia neatinsa verde (cod ${m.cod}${m.sarite.length ? ', ' + m.sarite.length + ' rand sarit pe nume: usa gateway-ului' : ''})`); +else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.rosii.length} rosii, ${saritePermise.length} sarite: ${[...m.rosii, ...saritePermise].join(' | ').slice(0, 400)})`); } +const LUCRARI = [...PLANTARI.map((p) => () => planteaza(p)), ...NEUTRE.map((p) => () => neutru(p))]; +const rez = new Array(LUCRARI.length); let i = 0; +// cate plantari ruleaza deodata: AERE_CONTROL_PARALEL (implicit 12). Masurat 2026-10-19 pe laptop (16 fire): dupa prima revizie 4 deodata 529 s, +// 8 deodata 311 s, 12 deodata 270 s; dupa a patra (112 plantari) ~475 s cu 12 si ~747 s cu 4; dupa a sasea (134 de lucrari: +// 131 de plantari si 3 neutre, proba de 158 de randuri) masurarea e in README, randul acestui control (pana la a sasea revizie comentariul trimitea la +// masurarile de dupa a cincea, 126 de lucrari, care nu ajunsesera in README). Rulatorul B2 (scripts/deschise/ruleaza-probe.sh) da acestui control +// 1650 s din a sasea revizie (1200 s pana atunci; aritmetica in probele-b2.sh); fiecare rulare a probei din el are 300 s (ceasul din ruleaza()). +const PARALEL = /^[1-9]\d?$/.test(String(process.env.AERE_CONTROL_PARALEL || '')) ? Number(process.env.AERE_CONTROL_PARALEL) : 12; +await Promise.all(Array.from({ length: PARALEL }, async () => { while (i < LUCRARI.length) { const k = i++; rez[k] = await LUCRARI[k](); } })); +for (const [bun, linie] of rez) { console.log(linie); if (!bun) rele++; } +console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `DOVEDIT: martorul verde, acoperirea intreaga (${ac.total} markeri), ${PLANTARI.length} din ${PLANTARI.length} paznici scosi -> randul lor rosu, ${NEUTRE.length} din ${NEUTRE.length} plantari neutre (ordinea) -> proba verde intreaga`); +process.exitCode = rele ? 1 : 0; diff --git a/agents/dovezi-martor-agenti-28001/martor-agenti-2026-10-19T05-35-18.json b/agents/dovezi-martor-agenti-28001/martor-agenti-2026-10-19T05-35-18.json new file mode 100644 index 0000000..d19ca15 --- /dev/null +++ b/agents/dovezi-martor-agenti-28001/martor-agenti-2026-10-19T05-35-18.json @@ -0,0 +1,385 @@ +{ + "v": 1, + "kind": "aere-agent-witness-evidence", + "network": "eip155:28001", + "rpc": "https://testnet-rpc.aere.network", + "witness": "aere-witness:fe5657cab99b3636a22cdc68b2dc952e77e9223d", + "policy": { + "policy": { + "v": 1, + "kind": "aere-agent-policy", + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "spend": { + "amount": "1000", + "windowSeconds": 3600, + "asset": "AERE" + }, + "tools": null, + "recipients": [ + "0xbbbb" + ] + }, + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b" + }, + "ledgerBefore": { + "version": "aere-agent-ledger/2 (2026-09-29)", + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n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\nk/orF/12\n-----END PUBLIC KEY-----\n", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "entries": [ + { + "seq": 0, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "body": { + "v": 2, + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "at": 1792387118, + "action": { + "kind": "payment", + "to": "0xbbbb", + "amount": "10", + "at": 1792387118 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 0 + }, + "signature": "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", + "hash": "932ce0bd9da3549bfb83473da0ef076104efa0e6a592fe452a1adfb0a0570f7e" + }, + { + "seq": 1, + "prev": "932ce0bd9da3549bfb83473da0ef076104efa0e6a592fe452a1adfb0a0570f7e", + "body": { + "v": 2, + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "at": 1792387123, + "action": { + "kind": "payment", + "to": "0xbbbb", + "amount": "10", + "at": 1792387123 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 1 + }, + "signature": "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", + "hash": "6032a854c357ad3f1aeacb7419bf3c3f0c29bc34b84024bd18a12c54c7300f99" + } + ] + }, + "head": { + "v": 1, + "kind": "aere-agent-ledger-head-attestation", + "statement": { + "v": 1, + "kind": "aere-agent-ledger-head", + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "session": "86f9d221fce6611824990720800c7d36", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "seq": 1, + "hash": "6032a854c357ad3f1aeacb7419bf3c3f0c29bc34b84024bd18a12c54c7300f99", + "createdAt": "2026-10-19T05:33:38.130Z", + "previous": null + }, + "statementHash": "0x25f1993926057b1da46cd815b3e7ae0d84d4ac777344c4476a87f4e3c13966fc", + "signature": { + "scheme": "ml-dsa-65", + "publicKey": "-----BEGIN PUBLIC KEY-----\n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\nk/orF/12\n-----END PUBLIC KEY-----\n", + "signature": "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" + } + }, + "receipt": { + "v": 1, + "kind": "aere-proof-of-agent-witness-checkpoint-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-agent-witness-checkpoint", + "root": "0x1b4af9bfc354446fc9ea2ec1583e02fe916f9aa83862c64230f699806fde2027", + "log": "aere-agent-witness/28001", + "witness": "aere-witness:fe5657cab99b3636a22cdc68b2dc952e77e9223d", + "chainId": 28001, + "treeSize": 1, + "maxMergeDelay": 1800, + "createdAt": "2026-10-19T05:33:38.138Z" + }, + "statementHash": "0xe1fcad7a17d13430cb3295fb26efe1aa8e2695fa7f1c35dfcfed56d5d17994bd", + "signature": { + "scheme": "ml-dsa-65", + "publicKey": "-----BEGIN PUBLIC KEY-----\n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\nUM6NT1Wq\n-----END PUBLIC KEY-----\n", + "signature": "yU5hkWtIHkmJwbek0zQfjJiTz/5/2UJcEyPJNsrrc4qLES+SKfYsL14VdSLwg0tH+8XV0JiIMrYwsaMVi4ab58YmKpKuBx31AZrM/Yj1D6usdCNdm8rSnhsy0Ewd1l73S9Vad6aAt7E3q17UTIKDJR/4R5zTnYgchv9tlWxYEOcxtl4QR9evr7NyRVziYy2VPZKN8Hk5KoH0iMGqscq0VSD3Gwp4RaJWQEZ1roL7j6gRQb9yLZYTjOFj2YJPqiRurBffke64UCDeZyKVVqLAFZhDsxUZgzvr03ttTFaYwnE5wN8fSomQfVpKX6xaHhKG2LFDNQVtH0J17AN0lWEBCJVZGQ3qpcav4LsRqmKbBSiD0BogIi3w/QP0HaRcVEMHNTRmnQCfG7w7BPfRs8szNZaAATRe17woIT10EDgJRjgf9TL6fZl7KlPtSaAZYoUHU6+GUY9rn6O9EfoBOzQAFAI9y/NBPzCOu3v4eUG94ds+2LSw+nW74SKKWBhomvSinTQ9KbAPXoAgCe/yihdjnUqSZ33GHgh4LRZq8TTEH9GQ9V817d0TEq8bhTfDu/1JgNq+W4rpeE8khCy8g0Pc7iXCKOnX8ORF8+dH0s9czXtLogwszkjU7R1oG3TmgVbnMHDAxvfXVmxisWAMSRc1xH5f9ok3blZmITQD+gx/eYd0DK3PC6zU7ECjAkB19h/GbAp5S1QSxfW9g00XN2ZqRliZLwlccw2kq7Fmt/XDJDYuRANopwwsq3wlhaE4xDiARiUTkfuxIKELCgBFfVa54loFPgXYk5ZxiGHHHva72uzovqmj5m8E6DByX7JaNNOJw1OdHY2oULLyYzD5FpMqSy+MA+g/cwadrsYiqd2JFgteroooyFrpctWonZoxau0Yq6sWwaJW4GofBfPhKo/wxM38wMvTUkFjzDmqOom2zolAjPwBJxRWbn4e68hNoaGKVG2MwrQg90Dhv8iH3+PhpSNQ8V+7Eofm5rXBMMhcBA/nJxRot3Y0UhezSOcRf6BM3m2xfkhvkzNN3R6QJN6U5qIBTkQ3FRWOhR5e6AgOYHU+UB9bAB4nVftOZY3OfvR1sRrtiGdKfcJVvZCqWyhnO3E6gxAbUf+EkiTW89x7SeeBK8PZilvwXes37J92eDcP3gAMMbHi/Lb9x5tKlqb1efd1UcrCKtTH0CIWtmzTftg5TGujCf0hrZ8t28gATdJV8Qbjsf+Ox9pRiRKettaLb7fUyv7g145ZxUFSuvnicujZXk2ni+HhxcFDrFwddGXTe/YWUjokICgojjCI5ZI09xJxup8uTyT9tWDBYOgu9E3RPWYGXaXzy0YPRLgw7B2RnTjA8pMuyExVS2yi1y+XivFrwXNk+udQcGPhW+dsGX84SlbGwO9LFv/hTFK8CQo/USmi0E/0h9VL+z3ugm+mH9hdTVUS1NRWg8L9106iHTP9lBj2XsSBMrkFQKHC93sqlCN4EKyTpNdqOYdwKtYwdsKzBh8/pwwHAHSv7dAjoIuCh5sUJf1PCqbRskMrI2a5dkaZzUjxJSYSUTaUQQP3GFkL+er/K8tijK9ziFLRZMS+x1C3i4hqHFwIt979sF76MIenfg9JAzj0P1hHeSDFKQ8/VKCd/TcbJz+ge9scMSq6JQ2/HsoaqB4/rCxSZ6NcVNjpt5Cx8h3wiafcGKFxSkZDDm4OaRJI14D8UpK5gphP+mKXea/EwuZC+13SeaxsNhAS/p63NJ3mHG7f0vZu1QOBkm7R3jkJONiFhX+IeRG8XJZts/k71OYE5r9OaRa6DPTf1md6zMMcin4hnAjZQh9DhRa9McVckk+fuSGAtEiMq6Fdem7/BjJq2EvixfnS8ONtzQTtzj/AWCA5XSGFcxGbehbBxinoF/tV6zCne33MHmCfTMCWMSUw1uA+4lAZoijdZunvg8UUbkMb/ualUKbAEDEjlFCKi2pch3q+ljxfNQXbmHwVRk6Doe+k4qDcjA2Pk84x5c4ecF+S+cq4L7NsmXQFpFcIZW2P9fmmv8qPUDEa18qxfXPrCDATsl0gITidwC5qpvbdO/1mAT/poVYwrOFsAVmyVnX3v895mCcj8l+QFn8e6AgPMb8UggOO/4yn+rAc1rNEdS69ZzApSZaQd/k/QmEZNBCrMbuQxzrrh2np2TYtAwo9bY+XxJzazvmzjpZGdd5ohbram0JaR3DfNVe517mPFD/K52D3ZyytLWpBKsN2bMp7ZrwlR4tlDdAknwjwGsT6Q5nOw3Wr8sdLSizPKqoXzlAUzkngclmf2w9voYXBOvZFPrxWUStQSNxi4zshnVnEyNhlxsFx//suiB6eQumu6oHT8mfEj1cxzNJIBr5Q2TEjaf6sTZUZJCS4uiWOiyIXnL8LUMc1NSB5fMXP3IcJsBlkGp79gCAsH4jYmTynf3vsIZTYW5MnP5v12lw17ueTNnKar/wWziQWlexrJL2xq513JSWaDMH0nQUQZMF/pUfYJB7sb0baX9kLL1rZA8hqDF2yLwMpZqulmIn/p++EZJUjRxb1r6q1gxMHCQvUbtJgtVRzNSSEApO1gO2MOAF3ttCdXB6Zp2WCmK+KS1qv2/j/hFJ5y/Gy/WbRBqw8+WO8yqVeN0L6173Mom/ebzMsVtUVh7aibdnpp5TzRUerhLOJdSguEIfZ7uRrt3Xwsm3SyySSNqYv4Yr1PwtTtGvs0y9d+yuwxHkv27ox/6kjFGb4ID/G0frtQwIPugtu+NpwXKy+oDHep1CfkWqZDSq+vlVihOAQRRd4QflPA6iMYoA3u9iBS0rkmSs/qnf1L3/TwsSb9NbglszfgqKuRQ86x0sFEHlcK6RO9IRC6P/Z/aRnvd3HTR5u8xrC11rtNqydscnePSbSAGTt17eIxwzm1eDtG711R87qM6VwEqg67OT7rHznY6kHUTALqTru0lKELh6HoI75KoOSh6YNMoM+r9f4ai6XGs9CzggrjFP5SafxeuTtycWCruqSoPm8UN1HFSV1BkTyNBkrcSky+1y0LVIU8nbMlYpUVoP6M5N1mLc6/7y9UjGajcrMj7QaAzHli/yUF0NGtJ64opal/b2d0qrv2Y0Dhi8CSmVxiXldFuIAvTTyRYXHJrw+1/MRF/zetX3fvv68T4TK/Fjd6YCDD871QZZmqovuC0qunIr/z0pYrhjZCCHxBV1UXdXX20DBFuj4NiU78BRqXKdsPQia4fdHDS9zScr1HkN2aKKmyxI04scSBx09tuY11hGkYMQds63zS0u9/xIMOVTeDfNowoWaCC/HwQWt4ZV3XD8HowHGKJjKPlwrQvLeoGvYHgm381uotVM/KTTnzlCvm9HQ0jiCuPKQwG1Bjmgbt10eq5Pi+vRxYdUWjGaV11g+q2b/IeLU9okaeniTfA/Qz5cSHnQeFM/IY1q/fFVwrxbKMj99JvAl11xubm3QQjveLNUlwHrbz/lVC3jXVOflnrYhFiKSh75JPwGMHl4yvqbgHASF5ivQ0I+t2SVHmmuJaE951qyf9pQc5TSu5DcA/VMbpXLipTEcu6D+Jl5zsya0Bu6+I6i08OvNtUDKxjDanAYwTmhojsFJM+GRMV4/GfLuAH2+cX5zv2DUv9W5IHqu6oDESYl3QLNrxy+Wdx0ZaEtu3ZFDyCTp19oWHgUqRMX1ql3a7K0YAo2Spy6GkW8nP2bxWAVeVS43auPePa3i07VmGNugTNejO/cb6vDK13Vx3MagV6SpPQUjLpVLYOjn6ZwjiCJNndQxxWYR54HT2VhKjE2ke6en17qAjkTqHGBdLGUbKB7J/fyqe5ONz/SYYVo46HRrq6vBn4YCH9Rh1dDm83SexADMqOwZO8pd6RTrel23ipbCle5Fn7i+3/SCFWmpEjibxEUsbD2u7td/lM8ekYXUjNGYUT/OwIVeLtQIDFxEBqoTI6BWpKXwATCsBcjRjPWwDJTwF7xdtByR+WvcqTDZF9cadxMkixbjwhldl8ciGq5UOyytjP4/+81aK4h6LTS43OUXzO29klgVJcVN9lVdZctAwug3Oek8FOKlhLEuws+n3LrXrb0DzSzn1H7a8VFy2bzYjhNbqp8qpzI/r3Ax+3nQWlj2yKAMmhQeeTL/xkDZW/7yL/qTE0Aia0dEdEOl31NhhVhk6rw4L6dsQMhnV4ktH0RHJzMvfGLy+84I8ojB7mzdZu0N9rQojCRcoP+9is9ltIH+uL4umigideeZTVYwnV49jtANUirMA6cWUUBZ7gllG8K7KCQ2icWobJRPzr8VdQ0cVHH36S7nIG9icpF4PSgzn7rNaw3GCcEaX9ij4Gvz3Gk29pJlOALqjsGk9nNOjrHU3BVNtM3wJjg7YpkGGl5pnKnj/ggtPFRdi42xx4Weov4AAAAAAAAAAAAAAAAAAAAAAAAABQoPFyAk" + }, + "notarization": { + "chainId": 28001, + "notary": "0x70099E62735500AA2F85B60C518551a57B202d54", + "firstSeen": 1792388029, + "txHash": "0x41fcb96d337eef07e2ce99739ac2a0940e501c76cee9e18fe252625842f69227", + "block": 6828096, + "submitter": "0xBAD08a6e956b77cAD3f66823BA7a6DD47005d131" + }, + "witnessReceipt": { + "format": "aere-agent-witness-receipt-1", + "head": { + "v": 1, + "kind": "aere-agent-ledger-head-attestation", + "statement": { + "v": 1, + "kind": "aere-agent-ledger-head", + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "session": "86f9d221fce6611824990720800c7d36", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "seq": 1, + "hash": "6032a854c357ad3f1aeacb7419bf3c3f0c29bc34b84024bd18a12c54c7300f99", + "createdAt": "2026-10-19T05:33:38.130Z", + "previous": null + }, + "statementHash": "0x25f1993926057b1da46cd815b3e7ae0d84d4ac777344c4476a87f4e3c13966fc", + "signature": { + "scheme": "ml-dsa-65", + "publicKey": "-----BEGIN PUBLIC KEY-----\n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\nk/orF/12\n-----END PUBLIC KEY-----\n", + "signature": "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" + } + }, + "receivedAtMs": 1792388018133, + "receivedAt": "2026-10-19T05:33:38.133Z", + "leafIndex": 0, + "treeSize": 1, + "leafHash": "0x1b4af9bfc354446fc9ea2ec1583e02fe916f9aa83862c64230f699806fde2027", + "path": [], + "promise": { + "v": 1, + "kind": "aere-proof-of-time-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-time", + "subjectHash": "0x25f1993926057b1da46cd815b3e7ae0d84d4ac777344c4476a87f4e3c13966fc", + "source": "aere-witness:fe5657cab99b3636a22cdc68b2dc952e77e9223d", + "at": "2026-10-19T05:33:38.133Z", + "createdAt": "2026-10-19T05:33:38.133Z" + }, + "statementHash": "0x0a3c2beb66b6e2cf31bbfaf80fae2ff6888e369c483cdce7c0aba32f2ced7658", + "signature": { + "scheme": "ml-dsa-65", + "publicKey": "-----BEGIN PUBLIC KEY-----\n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\nUM6NT1Wq\n-----END PUBLIC KEY-----\n", + "signature": "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" + } + } + } + }, + "aip23": { + "verdict": "VALID", + "levels": [ + { + "level": "form", + "state": "PASSED", + "detail": "kind=aere-proof-of-agent-witness-checkpoint-attestation" + }, + { + "level": "integrity", + "state": "PASSED", + "detail": "sha256(statement) == statementHash (0xe1fcad7a17d13430..)" + }, + { + "level": "signature", + "state": "PASSED", + "detail": "ml-dsa-65: verifies over the canonical statement text" + }, + { + "level": "finality", + "state": "PASSED", + "detail": "post-quantum: the state of block 6828127, the parent of anchor 6828128 whose post-quantum certificate verified here records this statementHash in AereNotary 0x70099e62735500aa2f85b60c518551a57b202d54 as first seen at 2026-10-19T05:33:49.000Z (1792388029); first seen in block 6828096, transaction 0x41fcb96d337eef07.. (located through the endpoint's logs, not part of what is certified)", + "certified": { + "chainId": 28001, + "notary": "0x70099e62735500aa2f85b60c518551a57b202d54", + "firstSeen": 1792388029, + "stateOfBlock": 6828127, + "anchor": 6828128 + } + } + ] + }, + "honest": { + "version": "aere-agent-ledger/2 (2026-09-29)", + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n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\nk/orF/12\n-----END PUBLIC KEY-----\n", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "entries": [ + { + "seq": 0, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "body": { + "v": 2, + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "at": 1792387118, + "action": { + "kind": "payment", + "to": "0xbbbb", + "amount": "10", + "at": 1792387118 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 0 + }, + "signature": "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", + "hash": "932ce0bd9da3549bfb83473da0ef076104efa0e6a592fe452a1adfb0a0570f7e" + }, + { + "seq": 1, + "prev": "932ce0bd9da3549bfb83473da0ef076104efa0e6a592fe452a1adfb0a0570f7e", + "body": { + "v": 2, + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "at": 1792387123, + "action": { + "kind": "payment", + "to": "0xbbbb", + "amount": "10", + "at": 1792387123 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 1 + }, + "signature": "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", + "hash": "6032a854c357ad3f1aeacb7419bf3c3f0c29bc34b84024bd18a12c54c7300f99" + }, + { + "seq": 2, + "prev": "6032a854c357ad3f1aeacb7419bf3c3f0c29bc34b84024bd18a12c54c7300f99", + "body": { + "v": 2, + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "at": 1792388083, + "action": { + "kind": "payment", + "to": "0xbbbb", + "amount": "10", + "at": 1792388083 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 2 + }, + "signature": "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", + "hash": "e6dab29070b8a57506ead833ae1b6951148cb6da350cf6ed2b675c2352ba2059" + } + ] + }, + "backdated": { + "version": "aere-agent-ledger/2 (2026-09-29)", + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n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\nk/orF/12\n-----END PUBLIC KEY-----\n", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "entries": [ + { + "seq": 0, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "body": { + "v": 2, + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "at": 1792387118, + "action": { + "kind": "payment", + "to": "0xbbbb", + "amount": "10", + "at": 1792387118 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 0 + }, + "signature": "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", + "hash": "932ce0bd9da3549bfb83473da0ef076104efa0e6a592fe452a1adfb0a0570f7e" + }, + { + "seq": 1, + "prev": "932ce0bd9da3549bfb83473da0ef076104efa0e6a592fe452a1adfb0a0570f7e", + "body": { + "v": 2, + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "at": 1792387123, + "action": { + "kind": "payment", + "to": "0xbbbb", + "amount": "10", + "at": 1792387123 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 1 + }, + "signature": "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", + "hash": "6032a854c357ad3f1aeacb7419bf3c3f0c29bc34b84024bd18a12c54c7300f99" + }, + { + "seq": 2, + "prev": "6032a854c357ad3f1aeacb7419bf3c3f0c29bc34b84024bd18a12c54c7300f99", + "body": { + "v": 2, + "agentId": "aere-agent:fc3651d19506cc9b91d84ac9e4cae2f6673af0df", + "policyHash": "0x835511d0a8a352fe7cc116833d444ea8497326a5332be0553475aa40289afe3b", + "session": "86f9d221fce6611824990720800c7d36", + "at": 1792387418, + "action": { + "kind": "payment", + "to": "0xbbbb", + "amount": "10", + "at": 1792387418 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 2 + }, + "signature": "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", + "hash": "8720911f2ada1371c8795d5275583ebcef46408260c8943445e1b5e275bd5db4" + } + ] + }, + "verdicts": { + "honestWithReceipt": 0, + "backdatedWithReceipt": 1 + } +} diff --git a/agents/martor-agenti-serviciu.mjs b/agents/martor-agenti-serviciu.mjs new file mode 100644 index 0000000..83aeed1 --- /dev/null +++ b/agents/martor-agenti-serviciu.mjs @@ -0,0 +1,245 @@ +#!/usr/bin/env node +// martor-agenti-serviciu.mjs: SERVICIUL martorului gazduit al agentilor (B3, 2026-10-19), sandbox catre testnetul 28001. Un proces pe loopback +// (Node 24: ML-DSA-65 din node:crypto), in spatele gateway-ului Aere Cloud (/v1/agents/witness, cum serveste adunatorul scutului /v1/shield/*): +// POST /v1/agents/witness un cap de registru semnat de agent (martor-agenti.mjs cerereMartor); 202 primit, cu +// promisiunea semnata de martor; 200 deja vazut; 409 echivocare (dovada intoarsa, si +// pastrata: una pe agent si politica), cap vechi, sau cap care nu continua ultimul; 4xx +// refuz numit; 429 ritm; 503 bugetul jurnalului pe ora (log_busy, cu Retry-After) +// GET /v1/agents/witness/0x starea capului: pending | checkpointed | notarized, cu chitanta (plicul AIP-23) +// GET /v1/agents/witness/aere-agent:<40 hex>/heads?session=<32 hex>[&from=][&limit<=100] o pagina a capetelor unei sesiuni, cu +// pagina SEMNATA de martor (`signedPage`, cu momentul ei), singura pe care o crede verificatorul +// GET /v1/agents/witness/aere-agent:<40 hex>/heads[?after=] sesiunile agentului (cel mult 100) +// GET /v1/agents/witness/equivocation/0x o dovada de echivocare pastrata +// GET /v1/agents/witness/checkpoint[?shard=][&from=] ultimul cap de jurnal al ciobului curent (sau al ciobului pastrat `k`, si al +// unuia inghetat: cel final) si cel notarizat, cu dovada de consistenta in acel ciob de la `from` +// GET /v1/agents/witness/health starea martorului si a limitatoarelor (chei tinute, scoase la plafon) +// La fiecare `--interval` minute: un cap de jurnal nou daca au venit capete, si notarizarea capetelor de jurnal nenotarizate (ultimul al fiecarui +// ciob pastrat) pe AereNotary 28001, cu o cheie de PROBA a testnetului (nascuta in starea serviciului daca lipseste, alimentata din robinet daca +// e dat). Orice alt lant decat 28001 e refuzat inainte de cheie: mainnet-ul cere tranzactiile fondatorului ("sandbox: 28001; mainnet: at activation"). +// Ce tine jurnalul de cereri: momentul, ruta (fara id-uri) si statusul; o eroare interna merge INTREAGA numai aici, raspunsul public poarta un cod +// (revizia B3, constatarea 18). Limitele de ritm stau in memorie, cu cheile sarate pe proces, fiecare cu un plafon de chei. +// +// node martor-agenti-serviciu.mjs --stare [--port 8802] [--interval 10] [--rpc https://testnet-rpc.aere.network] [--cheie ] +// [--robinet ] [--log aere-agent-witness/28001] [--merge-max 1800] [--fara-lant] [--o-data] +// --fara-lant laborator: capete de jurnal construite si semnate, nicio notarizare (chitantele raman `checkpointed`) +// --o-data un singur tur (cap de jurnal + notarizare), apoi iesire: 0 notarizat sau nimic de facut, 2 notarizarea n-a reusit +import fs from 'node:fs'; +import net from 'node:net'; +import path from 'node:path'; +import http from 'node:http'; +import crypto from 'node:crypto'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { deschideMartor, CERERE_MAX_OCTETI, SANDBOX, LANT, CHECKPOINT_KIND, PLAFON_FRUNZE, PAGINA_MAX } from './martor-agenti.mjs'; +import { notarizeEnvelope, cheiaDinFisier, NOTARI } from './agent-ancora.mjs'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +export const PORT_IMPLICIT = 8802; +export const RPC_28001 = 'https://testnet-rpc.aere.network'; +// limitele, pe fereastra de 10 minute: un agent cinstit trimite un cap la cateva minute; GET-urile sunt ieftine dar nu nelimitate. Bugetul +// jurnalului (capete NOI primite pe ora, de la toti) e DERIVAT din plafonul ciobului: PLAFON_FRUNZE / 24 pe ora, deci un ciob nu se umple in mai +// putin de o zi, iar un cap al lui se serveste cel putin o zi dupa ce ciobul e inghetat (martorul tine ciobul curent si pe cel de dinainte). +export const LIMITE = { postPeClient: { cereri: 60, ms: 600_000 }, postPeAgent: { cereri: 30, ms: 600_000 }, getPeClient: { cereri: 1200, ms: 600_000 }, + acceptatePeOra: { cereri: Math.floor(PLAFON_FRUNZE / 24), ms: 3_600_000 } }; +// plafonul de chei al unui limitator, DERIVAT din masuratoare (2026-10-19, README "Bounds"): 198 de octeti pe cheie (100.000 de chei = 19,8 MB pe +// limitator, deci ~60 MB pentru cele trei). La plafon (dupa curatarea cheilor expirate) se scoate cheia cu fereastra cea mai VECHE si clientul nou +// intra (revizia B3 a doua, R2-03): prima reparatie limita aici orice client NOU, deci cine umplea 100.000 de chei (de pe /64-uri reale) tinea +// afara pe toti clientii noi cat le reimprospata. Un client scos asa primeste doar o fereastra noua; un atacator care are deja 100.000 de chei are +// deja 100.000 de cote. Memoria nu creste cu adresele (pana la prima reparatie: 293 de octeti pe adresa, pe viata procesului, si niciun plafon). +export const LIMITATOR_MAX_CHEI = 100_000; +const taie = (s) => String(s ?? '').replace(/(0x)?[0-9a-fA-F]{40,}/g, '').slice(0, 300); + +/** + * Cheia unui client pentru limite (revizia B3, constatarea 13): o adresa IPv6 pe prefixul ei /64 (un client IPv6 primeste de obicei un /64 intreg, + * deci pe adresa intreaga ar fi avut 2^64 de chei), o adresa IPv4 mapata in IPv6 ca IPv4, o adresa IPv4 intreaga; orice altceva (o proba) ca atare. + */ +export function cheieClient(a) { + let s = String(a ?? '').trim().toLowerCase(); + if (s.startsWith('[') && s.includes(']')) s = s.slice(1, s.indexOf(']')); + const pct = s.indexOf('%'); if (pct >= 0) s = s.slice(0, pct); + const m4 = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(s); if (m4 && net.isIPv4(m4[1])) return m4[1]; + if (net.isIPv4(s)) return s; + if (net.isIPv6(s)) { + let [cap, coada = ''] = s.split('::'); const g = (t) => (t ? t.split(':') : []); + let a1 = g(cap), a2 = g(coada); + const v4 = (l) => { const u = l[l.length - 1]; if (u && u.includes('.')) { const o = u.split('.').map(Number); l.splice(-1, 1, ((o[0] << 8) | o[1]).toString(16), ((o[2] << 8) | o[3]).toString(16)); } }; + if (s.includes('::')) v4(a2); else v4(a1); + const tot = s.includes('::') ? [...a1, ...Array(Math.max(0, 8 - a1.length - a2.length)).fill('0'), ...a2] : a1; + return tot.slice(0, 4).map((x) => (parseInt(x, 16) || 0).toString(16)).join(':') + '::/64'; + } + return 'altul:' + s.slice(0, 64); +} + +/** + * Un limitator pe fereastra fixa (de la prima cerere a cheii), cu cheile sarate pe proces (nicio adresa sau id nu se tine in clar), cu cheile + * expirate CURATATE (la 1024 de apeluri, si cand plafonul e atins, dar cel mult o data pe secunda) si cu un PLAFON de chei: pana la reparatie + * nicio cheie nu se stergea si fiecare adresa noua lasa ~293 de octeti pe viata procesului (revizia B3, constatarea 13). La plafon se scoate cheia + * cu fereastra cea mai veche (harta e in ordinea inceputului ferestrei: o fereastra noua se muta la coada), numarata (`scoase`), in loc sa fie + * limitat clientul nou (revizia B3 a doua, R2-03). `ar(cheie)`: ar fi limitata cheia acum? fara sa consume nimic (bugetul jurnalului, R2-13). + */ +export function limitator({ cereri, ms }, { maxChei = LIMITATOR_MAX_CHEI, ceas = Date.now } = {}) { + const sare = crypto.randomBytes(16); const m = new Map(); let apeluri = 0, ultimaCuratare = 0, scoase = 0; + const cheieDe = (cheie) => crypto.createHash('sha256').update(sare).update(String(cheie)).digest('base64url').slice(0, 16); + const curata = (acum) => { ultimaCuratare = acum; for (const [k, v] of m) if (acum - v[0] >= ms) m.delete(k); }; + const f = (cheie) => { + const k = cheieDe(cheie); + const acum = ceas(); + if (++apeluri % 1024 === 0) curata(acum); + let v = m.get(k); + if (v && acum - v[0] >= ms) { m.delete(k); v[0] = acum; v[1] = 0; m.set(k, v); } + if (!v) { + if (m.size >= maxChei && acum - ultimaCuratare >= 1000) curata(acum); + if (!(m.size < maxChei /*PAZA-MARTOR:ritm-plafon*/)) { m.delete(m.keys().next().value); scoase++; } + v = [acum, 0]; m.set(k, v); + } + if (v[1] >= cereri) return true; + v[1]++; return false; + }; + f.ar = (cheie) => { const v = m.get(cheieDe(cheie)); return !!v && ceas() - v[0] < ms && v[1] >= cereri; }; + // cat mai are de asteptat cheia pana i se deschide o fereastra noua (ms; 0 daca nu e limitata), pentru Retry-After (revizia B3 a cincea, R5-06) + f.ramasMs = (cheie) => { const v = m.get(cheieDe(cheie)); return v && v[1] >= cereri ? Math.max(0, ms - (ceas() - v[0])) : 0; }; + f.marime = () => m.size; + f.scoase = () => scoase; + return f; +} +const loopback = (a) => /^(127\.|::1$|::ffff:127\.)/.test(String(a || '')); + +/** Serverul HTTP peste starea deschisa `m` (deschideMartor). Clientul real e X-Real-IP numai cand cererea vine de pe loopback (gateway-ul il pune). */ +export function porneste(m, { port = 0, limite = LIMITE, jurnal = () => {}, maxChei = LIMITATOR_MAX_CHEI } = {}) { + const peClient = limitator(limite.postPeClient, { maxChei }), peAgent = limitator(limite.postPeAgent, { maxChei }), citiri = limitator(limite.getPeClient, { maxChei }); + const acceptate = limite.acceptatePeOra ? limitator(limite.acceptatePeOra, { maxChei: 1 }) : null; + // bugetul jurnalului: martorul il CITESTE intai (`ar`, fara sa consume) si il consuma abia dupa cota agentului (R2-13) + const limitaGlobala = acceptate ? Object.assign(() => acceptate('jurnal'), { ar: () => acceptate.ar('jurnal') /*PAZA-MARTOR:buget-citit*/ }) : null; + // starea limitatoarelor, in /health: cate chei tin si cate au scos la plafon (o inundatie din multe adrese se vede, nu se ghiceste) + const stareLimite = () => Object.fromEntries([['postPerClient', peClient], ['postPerAgent', peAgent], ['getPerClient', citiri]].map(([n, l]) => [n, { keys: l.marime(), maxKeys: maxChei, droppedAtCap: l.scoase() }])); + const raspunde = (req, res, cod, obiect, antete = {}) => { + const text = Buffer.from(JSON.stringify(obiect)); + res.writeHead(cod, { 'content-type': 'application/json; charset=utf-8', 'content-length': text.length, 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', ...antete }); + res.end(text); + jurnal(`${req.method} ${String(req.url).replace(/0x[0-9a-fA-F]{64}/g, '0x..').replace(/aere-agent:[0-9a-f]{40}/g, 'aere-agent:..').replace(/\?.*$/, '').slice(0, 60)} ${cod}`); + }; + // o eroare interna: INTREAGA numai in jurnalul local; public un cod (fara cai, fara numele starii) + const interna = (req, res, e) => { jurnal(`internal error on ${req.method} ${String(req.url).replace(/\?.*$/, '').slice(0, 60)}: ${taie(e && (e.stack || e.message))}`); if (!res.headersSent) raspunde(req, res, 500, { error: 'witness_error', code: e && /^E[A-Z]+$/.test(String(e.code)) ? 'io_error' : 'internal', sandbox: SANDBOX }); }; + const RUTE = ['POST /v1/agents/witness', 'GET /v1/agents/witness/0x', 'GET /v1/agents/witness/aere-agent:<40 hex>/heads[?session=<32 hex>&from=&limit=<1..100>][?after=]', 'GET /v1/agents/witness/equivocation/0x', 'GET /v1/agents/witness/checkpoint[?shard=][&from=]', 'GET /v1/agents/witness/health']; + const intreg = (s, d) => (s === null ? d : /^\d{1,15}$/.test(s) ? Number(s) : NaN); + const handler = (req, res) => { + try { + const url = new URL(req.url, 'http://x'); const cale = url.pathname; + const client = cheieClient(loopback(req.socket.remoteAddress) && req.headers['x-real-ip'] ? String(req.headers['x-real-ip']) : String(req.socket.remoteAddress || '')); + if (req.method === 'POST' && cale === '/v1/agents/witness') { + if (peClient(client) /*PAZA-MARTOR:ritm*/) return raspunde(req, res, 429, { error: 'rate_limited', hint: `${limite.postPeClient.cereri} heads per ${limite.postPeClient.ms / 60000} minutes per client`, sandbox: SANDBOX }); + const ct = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase(); + const parti = []; let n = 0, prea = false; + req.on('data', (d) => { n += d.length; if (n > CERERE_MAX_OCTETI) { if (!prea) { prea = true; raspunde(req, res, 413, { error: 'body_too_large', max: CERERE_MAX_OCTETI, hint: 'a signed head is about 8 KB; the witness never takes the ledger', sandbox: SANDBOX }); req.destroy(); } } else parti.push(d); }); + req.on('end', () => { + if (prea) return; + try { + if (ct !== 'application/json') return raspunde(req, res, 415, { error: 'bad_content_type', hint: 'application/json: the signed head (martor-agenti.mjs request)', sandbox: SANDBOX }); + let env; try { env = JSON.parse(Buffer.concat(parti).toString('utf8')); } catch { return raspunde(req, res, 400, { error: 'bad_json', sandbox: SANDBOX }); } + const r = m.primeste(env, { limitaAgent: peAgent, limitaGlobala }); + return raspunde(req, res, r.cod, r.corp, r.corp && r.corp.retryAfterS ? { 'retry-after': String(r.corp.retryAfterS) } : {}); + } catch (e) { return interna(req, res, e); } + }); + req.on('error', () => {}); + return; + } + if (req.method !== 'GET') return raspunde(req, res, 405, { error: 'method_not_allowed', routes: RUTE, sandbox: SANDBOX }); + // o citire peste limita spune si cand se poate reincerca (Retry-After, secundele pana la fereastra noua a clientului): verificatorul aduce + // cate o chitanta pe cap tinut si asteapta cat spune antetul (revizia B3 a cincea, R5-06) + if (citiri(client)) { const ra = Math.max(1, Math.ceil(citiri.ramasMs(client) / 1000)); return raspunde(req, res, 429, { error: 'rate_limited', retryAfterS: ra, sandbox: SANDBOX }, { 'retry-after': String(ra) } /*PAZA-MARTOR:get-retry-after*/); } + if (cale === '/v1/agents/witness/health') return raspunde(req, res, 200, { ...m.sanatate(), limiters: stareLimite() }); + if (cale === '/v1/agents/witness/checkpoint') { + const sh = url.searchParams.get('shard'); if (sh !== null && !/^\d{1,9}$/.test(sh)) return raspunde(req, res, 400, { error: 'bad_shard', sandbox: SANDBOX }); + const r = m.capulJurnalului(url.searchParams.get('from'), sh === null ? null : Number(sh)); return raspunde(req, res, r.cod, r.corp); + } + const me = /^\/v1\/agents\/witness\/equivocation\/(0x[0-9a-fA-F]{64})$/.exec(cale); + if (me) { const r = m.dovadaEchivocarii(me[1]); return raspunde(req, res, r.cod, r.corp); } + const mi = /^\/v1\/agents\/witness\/(0x[0-9a-fA-F]{64})$/.exec(cale); + if (mi) { const r = m.stareaCapului(mi[1]); return raspunde(req, res, r.cod, r.corp); } + const ma = /^\/v1\/agents\/witness\/(aere-agent:[0-9a-f]{40})\/heads$/.exec(cale); + if (ma) { + const s = url.searchParams.get('session'), after = url.searchParams.get('after'); + const from = intreg(url.searchParams.get('from'), 0), limit = intreg(url.searchParams.get('limit'), PAGINA_MAX); + if (s !== null && !/^[0-9a-f]{32}$/.test(s)) return raspunde(req, res, 400, { error: 'bad_session', sandbox: SANDBOX }); + if (after !== null && !/^[0-9a-f]{32}$/.test(after)) return raspunde(req, res, 400, { error: 'bad_after', sandbox: SANDBOX }); + if (!(Number.isSafeInteger(from) && from >= 0 && Number.isSafeInteger(limit) && limit >= 1 && limit <= PAGINA_MAX)) return raspunde(req, res, 400, { error: 'bad_page', hint: `from >= 0, 1 <= limit <= ${PAGINA_MAX}`, sandbox: SANDBOX }); + return raspunde(req, res, 200, m.capeteAgent(ma[1], s, { from, limit, after })); + } + return raspunde(req, res, 404, { error: 'not_found', routes: RUTE, sandbox: SANDBOX }); + } catch (e) { return interna(req, res, e); } + }; + const server = http.createServer(handler); + return new Promise((ok) => server.listen(port, '127.0.0.1', () => ok({ server, port: server.address().port, close: () => new Promise((g) => server.close(() => g())) }))); +} + +/** + * Notarizatorul real: capul de jurnal pe AereNotary, NUMAI pe testnetul 28001 (lantul citit de la RPC, refuzat altfel inainte de cheie), cu o + * cheie de proba din fisier (nascuta acolo, 0600, daca lipseste; niciodata tiparita), alimentata din robinetul testnetului cand soldul nu + * ajunge si cand `robinet` numeste clientul lui (tools/aere-quantum-shield/robinet.mjs; fara el, `unfunded`, spus). Intoarce {ok, notarization} + * sau {ok: false, cod, motiv}: `cod` e cel public (unfunded, chain_refused, ...), `motiv` merge numai in jurnalul local. + */ +export function notarizatorReal({ rpc = RPC_28001, cheieFisier, robinet = null, ethers = null }) { + return async (env) => { + let E = ethers; + if (!E) { try { const { incarcaEthers } = await import(pathToFileURL(path.join(AICI, 'x402', 'wallet.mjs')).href); E = incarcaEthers(); } catch (e) { return { ok: false, cod: 'ethers_missing', motiv: 'ethers is not here: ' + taie(e.message) }; } } + try { + const provider = new E.JsonRpcProvider(rpc); + const chainId = Number((await provider.getNetwork()).chainId); + if (!(chainId === LANT /*PAZA-MARTOR:sandbox*/)) return { ok: false, cod: 'chain_refused', motiv: `REFUSED: this RPC serves chain ${chainId}; the agents witness notarizes on the public testnet ${LANT} only (mainnet at activation, with the founder's transactions)` }; + if (!fs.existsSync(cheieFisier)) { + const w = E.Wallet.createRandom(); + fs.mkdirSync(path.dirname(cheieFisier), { recursive: true }); + fs.writeFileSync(cheieFisier, `# AERE-SINTETIC: test key of the agents witness for testnet ${LANT}, made ${new Date().toISOString()}; funds only from the faucet\nd=${w.privateKey.slice(2)}\n`, { mode: 0o600, flag: 'wx' }); + } + const wallet = new E.Wallet(cheiaDinFisier(cheieFisier), provider); + const min = E.parseEther('0.01'); + if ((await provider.getBalance(wallet.address)) < min) { + if (!robinet) return { ok: false, cod: 'unfunded', motiv: 'unfunded: the test key has no testnet balance and no faucet client was given (--robinet)' }; + const { cereRobinet } = await import(pathToFileURL(path.resolve(robinet)).href); + const c = await cereRobinet(wallet.address); + if (!c.ok) return { ok: false, cod: 'unfunded', motiv: 'unfunded: the faucet refused: ' + taie(JSON.stringify(c.claim || c.motiv)) }; + let sold = 0n; for (let i = 0; i < 40 && sold < min; i++) { await new Promise((g) => setTimeout(g, 1500)); sold = await provider.getBalance(wallet.address); } + if (sold < min) return { ok: false, cod: 'unfunded', motiv: 'unfunded: the faucet transaction did not land in 60 s' }; + } + const r = await notarizeEnvelope({ envelope: env, kind: CHECKPOINT_KIND, rpc, keyFile: cheieFisier, ethers: E, notary: NOTARI[LANT], confirmMainnet: false }); + return r.ok ? { ok: true, notarization: { ...r.envelope.notarization, submitter: wallet.address } } : { ok: false, cod: 'notarization_failed', motiv: taie(r.motiv) }; + } catch (e) { return { ok: false, cod: 'notarization_failed', motiv: 'the notarization failed: ' + taie(e.shortMessage || e.message) }; } + }; +} + +/** Un tur: un cap de jurnal nou (daca au venit capete), apoi notarizarea fiecarui cap de jurnal nenotarizat (cel final al unui ciob inghetat intai). */ +export async function tur(m, { notarizeaza = null } = {}) { + const nou = m.construiestePunct(); + const deFacut = m.deNotarizate(); + if (!deFacut.length) return { built: nou ? nou.n : null, notarized: null, note: nou ? 'built' : 'nothing new' }; + if (!notarizeaza) return { built: nou ? nou.n : null, notarized: null, note: 'laboratory: no notarization' }; + let ultim = null; + for (const p of deFacut) { + const r = await notarizeaza(m.plicPunct(p)); + m.inregistreazaNotarizarea(p, r); + if (!r.ok) return { built: nou ? nou.n : null, notarized: ultim ? ultim.n : null, shard: p.k, firstSeen: null, error: r.motiv, code: r.cod || 'notarization_failed' }; + ultim = { n: p.n, k: p.k, firstSeen: r.notarization.firstSeen }; + } + return { built: nou ? nou.n : null, notarized: ultim.n, shard: ultim.k, firstSeen: ultim.firstSeen, error: null }; +} + +// ------------------------------------------------------------------------------------------------ CLI +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main(); +async function main() { + const arg = (n, d) => { const i = process.argv.indexOf(n); return i > 0 ? process.argv[i + 1] : d; }; + const stare = arg('--stare', null); + if (!stare) { console.error('usage: martor-agenti-serviciu.mjs --stare [--port 8802] [--interval 10] [--rpc url] [--cheie f] [--robinet robinet.mjs] [--log name] [--merge-max s] [--fara-lant] [--o-data]'); process.exitCode = 2; return; } + const scrieJurnal = (l) => console.log(new Date().toISOString() + ' ' + l); + let m; + const faraLant = process.argv.includes('--fara-lant'); + // laboratorul (--fara-lant) nu notarizeaza nimic: rotirea nu asteapta notarizarea capului de jurnal final al ciobului scos (R2-07) + try { m = deschideMartor(stare, { log: arg('--log', undefined), maxMergeDelay: arg('--merge-max', null) != null ? Number(arg('--merge-max')) : undefined, jurnal: scrieJurnal, faraLant }); } + catch (e) { console.error('cannot open the state: ' + taie(e.message)); process.exitCode = 2; return; } + const notarizeaza = faraLant ? null : notarizatorReal({ rpc: arg('--rpc', RPC_28001), cheieFisier: arg('--cheie', path.join(stare, 'cheie-de-proba-28001.key')), robinet: arg('--robinet', null) }); + const t = async () => { try { const r = await tur(m, { notarizeaza }); console.log(new Date().toISOString() + ' round: ' + JSON.stringify(r).slice(0, 300)); return r; } catch (e) { console.log(new Date().toISOString() + ' round UNMEASURED: ' + taie(e.message)); return { error: e.message }; } }; + if (process.argv.includes('--o-data')) { const r = await t(); m.inchide(); process.exitCode = r.error ? 2 : 0; return; } + const srv = await porneste(m, { port: Number(arg('--port', PORT_IMPLICIT)), jurnal: scrieJurnal }); + console.log(`agents witness ${m.witnessId} on 127.0.0.1:${srv.port} (log ${m.log}, state ${stare}, ${faraLant ? 'laboratory: no chain' : 'notarizing on chain ' + LANT}); ${SANDBOX}`); + const opreste = () => { m.inchide(); srv.close().then(() => process.exit(0)); }; + process.on('SIGTERM', opreste); process.on('SIGINT', opreste); + await t(); setInterval(t, Number(arg('--interval', 10)) * 60_000); +} diff --git a/agents/martor-agenti.mjs b/agents/martor-agenti.mjs new file mode 100644 index 0000000..9652fe8 --- /dev/null +++ b/agents/martor-agenti.mjs @@ -0,0 +1,1129 @@ +#!/usr/bin/env node +// martor-agenti.mjs: MARTORUL GAZDUIT al registrelor agentilor (roadmap pista B, B3, linia Agents, punctul 22, 2026-10-19). +// +// Ce lipsea dupa agent-ancora.mjs (lantul ca martor, 2026-09-30): ca sa aiba un moment certificat al capului, proprietarul agentului +// trebuia sa alerge singur notarul, portofelul de gaz si verificatorul AIP-23. Aici e un serviciu la care agentul TRIMITE capul +// registrului lui (semnat cu cheia lui ML-DSA-65) si primeste inapoi o dovada pe care o verifica oricine, fara martor. +// +// Forma e cea care exista deja, nu una inventata: +// - capul trimis e un CHECKPOINT al jurnalului agentului, ca in C2SP tlog-witness (`add-checkpoint`): martorul il primeste numai +// daca e semnat de cheia jurnalului (aici a agentului), daca numeste ca predecesor exact ultimul cap pe care martorul l-a vazut +// pentru acel jurnal (altfel 409 cu ultimul cap, ca 409-ul cu marimea ultimului checkpoint din C2SP), si il persista INAINTE sa +// raspunda. Registrul agentului e un lant de hash-uri, nu un arbore: o dovada de consistenta intre doua capete ar cere intrarile, +// adica actiunile, pe care martorul nu le vede niciodata. In locul ei agentul semneaza `previous` (capul pe care il continua); +// coborarea efectiva o verifica oricine tine registrul (ancorele trebuie sa fie toate pe registrul aratat). +// - doua afirmatii semnate de agent care dau doua hash-uri diferite aceleiasi pozitii (acelasi seq, sau `previous` contra unui cap deja +// vazut) sunt o dovada de ECHIVOCARE: martorul nu primeste capul, pastreaza dovada si o intoarce (C2SP: "MUST NOT cosign"). +// - capetele primite sunt frunzele unui jurnal transparent RFC 9162 (ca Rekor si Certificate Transparency): la primire o promisiune +// semnata de martor cu momentul primirii (analogul SCT / signed entry timestamp), apoi, la cel mult maxMergeDelay (MMD-ul din RFC 9162), +// un cap de jurnal (checkpoint: numele jurnalului, marimea, radacina, ca in C2SP tlog-checkpoint) semnat de martor si NOTARIZAT pe +// AereNotary (testnetul 28001 in sandbox), deci momentul lui e certificat post-cuantic de verificatorul AIP-23, nu declarat de martor. +// - chitanta = capul de jurnal (plic AIP-23 `aere-proof-of-agent-witness-checkpoint`, VALID la verificatorul de referinta) + capul primit +// + pozitia + dovada de includere (RFC 9162, 2.1.3), ca `verification.inclusionProof` din Rekor. +// - jurnalul e IMPARTIT IN CIOBURI (2026-10-19, revizia B3, constatarile 3 si 12), cum isi impart jurnalele Rekor si Certificate Transparency +// (sharding, temporal sharding): un ciob plin se INGHEATA cu un cap de jurnal final si martorul incepe ciobul urmator, cu nume nou +// (`/`), al carui prim cap de jurnal numeste capul final al celui de dinainte. Martorul tine ciobul curent si pe cel de +// dinainte (CIOBURI_PASTRATE); unul mai vechi se scoate de pe disc si din memorie, iar chitantele lui raman verificabile fara martor. +// Pana la reparatie un jurnal plin raspundea 503 tuturor, pentru totdeauna, si il putea umple oricine. +// - a doua revizie adversariala (2026-10-19, 15 constatari R2-01..R2-15): lista capetelor unui agent (GET .../heads?session) e SEMNATA de martor +// pagina cu pagina, cu momentul ei, si numai pagina semnata conteaza la `verify --witness-url` (pana atunci id-ul din /health si lista erau +// nesemnate, deci un server ostil la adresa data se dadea drept martorul fixat); o dovada de echivocare se leaga de ciobul capului pe care il +// contrazice (deci sunt cel mult atatea cat frunze pastrate si nimeni nu poate umple locul alteia); un ciob nu se scoate pana nu i s-a +// notarizat capul de jurnal final; arborele jurnalului e INCREMENTAL (fara memorie pe interval); lacatul starii isi poarta pornirea masinii. +// +// TIMPUL, spus exact (si scris in README): capul afirma "la createdAt capul meu e seq" (semnat de agent). Martorul il primeste numai la cel mult +// TOLERANTA_S de ceasul lui si scrie momentul primirii in frunza (sub radacina notarizata). Lantul certifica momentul F (firstSeen) al capului de +// jurnal; verificatorul cere primire <= F + toleranta si F - primire <= intarzierea maxima (a martorului si a lui). De aici ancora {seq, hash, at: +// createdAt} pentru verifyLedger: o intrare de DUPA cap care declara un moment sub createdAt - 300 s contrazice semnatura agentului insusi +// (antedatare prinsa). Lantul singur margineste createdAt la cel putin F - intarzierea maxima - 600 s; strangerea la 300 s de ceasul martorului +// tine NUMAI pentru un martor pe care verificatorul il FIXEAZA (`martori`, `--witness`): orice cheie ML-DSA-65 isi poate fi singura martor +// (revizia B3, constatarile 1 si 17), deci fara fixare rezultatul spune ca marginea e numai a lantului si numeste martorul fiecarei chitante. +// Martorul nu vede si nu judeca actiunile: primeste numai capete (seq + hash) si refuza orice camp in plus. +// +// node martor-agenti.mjs request --key agent.key.pem --ledger l.json [--seq N] [--previous |none] [--out head.json] +// node martor-agenti.mjs submit --key agent.key.pem --ledger l.json --url [--seq N] [--out response.json] (previous = ultimul cap al martorului) +// node martor-agenti.mjs receipt --url --id 0x --ledger l.json [--witness aere-witness:<40 hex>] [--out receipt.json] +// (scrie raspunsul NUMAI daca e chitanta capului cerut, a registrului dat, cu semnatura martorului verificata) +// node martor-agenti.mjs verify-receipt --receipt r.json [--ledger l.json] [--witness aere-witness:<40 hex>] numai local (fara lant) +// node martor-agenti.mjs verify-equivocation --proof p.json +// Lantul (momentul certificat) il judeca `agent-cli.mjs verify --anchors receipt.json --rpc --verify-proof --witness `. +// Iesiri: 0 da (si: nimic nou de trimis); 1 nu (refuzat, invalid); 2 folosire gresita sau NEMASURAT (martorul nu raspunde, raspunde 503 sau 429, +// chitanta inca nenotarizata, un cap inca pending). +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { fileURLToPath } from 'node:url'; +import { agentIdFromKey, sessionId, agentFromPrivateKeyPem } from './agent-ledger.mjs'; +import { HEAD_KIND, firstSeenFromVerdict, ruleazaVerificatorul } from './agent-ancora.mjs'; +import { leafHash, verifyInclusion, verifyConsistency } from '../verify-layer/arbore-merkle.mjs'; +import { buildProof, SCHEME } from '../proof-kinds/proof-kinds.mjs'; + +export { HEAD_KIND, verifyConsistency }; +export const VERSION = 'aere-agent-witness/1.1 (2026-10-19)'; +export const CHECKPOINT_KIND = 'aere-proof-of-agent-witness-checkpoint'; // proof-kinds: 'agent-witness-checkpoint' +export const PROMISE_KIND = 'aere-proof-of-time'; // proof-kinds: 'time' (un subiect a existat la un moment, dupa o sursa) +export const RECEIPT_FORMAT = 'aere-agent-witness-receipt-1'; +export const EQUIVOCATION_KIND = 'aere-agent-head-equivocation'; +export const LOG_IMPLICIT = 'aere-agent-witness/28001'; +export const LANT = 28001; // sandbox: testnetul public; mainnet-ul (2800) la activare, cu tranzactiile fondatorului +export const TOLERANTA_S = 300; // aceeasi ca a registrului (agent-ledger.mjs) +export const MERGE_MAX_S = 1800; // intarzierea maxima promisa intre primire si un cap notarizat care o acopera +export const CERERE_MAX_OCTETI = 16384; // un cap semnat ML-DSA-65 are ~8 KB (cheia PEM ~2,7 KB, semnatura ~4,4 KB in base64) +// PLAFOANELE starii, DERIVATE din masuratoare pe starea CEA MAI REA (2026-10-19, a doua revizie B3, R2-02 si R2-06; Node 24.14.1, Windows; pe +// Linux NEMASURAT; README, "Bounds"): doua cioburi pline (2 x 20.000 de frunze), un cap de jurnal pe frunza (trafic mic: cel mult un cap pe tur) +// si o dovada de echivocare pastrata pe frunza (o sesiune pe frunza: marginea dovezilor prin constructie, ECHIVOCARI_TOTAL): +// - pe disc 1.320 MB (33 KB pe frunza: capul semnat si promisiunea ~15,5 KB, randul frunzei 0,35 KB, capul de jurnal ~7,9 KB, dovada ~7,9 KB, +// numai capul nou); instalatorul cere 2048 MB liberi (~1,55x). +// - in memorie, intr-un proces nou: deschiderea 266 MB RSS la varf (procesul gol 50 MB; 37 s pe laptop), functionarea (2.000 de chitante, 2.000 +// de pagini semnate, 2.000 de dovezi, consistenta pe ciobul inghetat) 277 MB, cu cele trei limitatoare la plafonul de chei (3 x 100.000) 330 MB: +// MemoryMax=512M al unitatii (~1,55x). Prima masurare (un cap de jurnal la 1.000 de frunze, 180,6 MB, MemoryMax=400M) a ratat cazul acesta: cu +// un cap de jurnal pe frunza memoria pe interval a arborelui (~5 KB pe cap de jurnal) ducea redeschiderea la ~372 MB (masurat de verificator) +// si functionarea peste 400M; arborele e acum incremental (arboreIncremental, mai jos). +// - inainte de a doua revizie: nicio primire peste 200 ms (rotirile incluse; cea mai lunga 157 ms); dupa ea latenta pe cerere NEMASURATA (rularea +// starii celei mai rele, cu un cap de jurnal construit dupa fiecare frunza si fara fsync: 8,6 ms pe frunza in medie). +export const PLAFON_FRUNZE = 20000; // frunze pe ciob +export const CIOBURI_PASTRATE = 2; // ciobul curent si cel de dinainte +// dovezile de echivocare PASTRATE: cel mult UNA pe agent si politica ("o dovada ajunge"). Prima reparatie (constatarea 9) a pus si un plafon +// GLOBAL de 2.000, pe care il putea umple oricine cu sesiuni proprii (doua POST-uri pe sesiune), iar de acolo o echivocare REALA a altui agent nu +// mai era pastrata (revizia B3 a doua, R2-02). Acum o dovada se leaga de ciobul capului pe care il CONTRAZICE si se scoate odata cu el; fiecare +// dovada are deci o sesiune proprie cu cel putin o frunza intr-un ciob pastrat, iar numarul lor e marginit PRIN CONSTRUCTIE de frunzele pastrate: +// cel mult CIOBURI_PASTRATE x PLAFON_FRUNZE = 40.000. Plafonul global e chiar marginea aceasta (nu se atinge niciodata pe starea reala; il pot +// cobori numai probele), deci un strain nu poate nici impinge afara, nici bloca dovada altcuiva. Pe disc o dovada tine numai capul NOU semnat +// (capul contrazis e deja in `capete/` al ciobului ei): ~7,9 KB, deci cel mult 40.000 x 7,9 KB = ~316 MB; in memorie un rand de index (masurat in +// README, "Bounds"). Peste plafon (numai cu un plafon coborat) dovada se intoarce clientului, nepastrata, si /health numara refuzurile. +export const ECHIVOCARI_TOTAL = CIOBURI_PASTRATE * PLAFON_FRUNZE; +// lista capetelor unui agent (GET .../heads?session), SEMNATA de martor pagina cu pagina (revizia B3 a doua, R2-01); verificatorul o primeste +// numai daca e semnata de martorul fixat si e mai noua de TOLERANTA_S fata de ceasul lui (o pagina veche, reluata, nu e lista de acum) +export const PAGINA_KIND = 'aere-agent-witness-heads'; +export const PAGINA_MAX = 100; // capete pe pagina in GET .../heads (si sesiuni pe pagina, fara ?session) +export const SANDBOX = 'sandbox: testnet 28001; mainnet: at activation'; +const ALG = 'ml-dsa-65'; +const H32 = /^0x[0-9a-f]{64}$/; +const HASH_INTRARE = /^[0-9a-f]{64}$/; +const AGENT_ID = /^aere-agent:[0-9a-f]{40}$/; +const WITNESS_ID = /^aere-witness:[0-9a-f]{40}$/; +const SESIUNE = /^[0-9a-f]{32}$/; +const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/; +const sha = (b) => '0x' + crypto.createHash('sha256').update(typeof b === 'string' ? Buffer.from(b, 'utf8') : b).digest('hex'); +const obiect = (x) => !!x && typeof x === 'object' && !Array.isArray(x); +// REGULA (c), ca la agent-cli.mjs (revizia B3 a cincea, R5-04): tipul unei valori venite din afara se judeca INAINTE de orice folosire; String() pe +// un obiect {"toString": 0} arunca, deci in judecati un camp care trebuie sa fie sir trece prin sir(), care da sirul gol pentru orice alt tip +const sir = (x) => (typeof x === 'string' ? x : ''); +const CAMPURI_PLIC = ['v', 'kind', 'statement', 'statementHash', 'signature']; +const CAMPURI_CAP = ['v', 'kind', 'agentId', 'session', 'policyHash', 'seq', 'hash', 'createdAt', 'previous']; +const CAMPURI_SEMNATURA = ['scheme', 'publicKey', 'signature']; +// forma capului de jurnal: VOCABULARUL felului din proof-kinds, scris o singura data acolo (revizia B3, constatarea 8) +const FEL_PUNCT = SCHEME['agent-witness-checkpoint']; +export const eWitnessId = (x) => typeof x === 'string' && WITNESS_ID.test(x); + +/** Regulile pe care le numeste un refuz al martorului sau al verificarii unei chitante (campul `rule`). */ +export const REGULI = ['forma', 'continut', 'sesiune', 'integritate', 'cheie', 'identitate', 'semnatura', 'timp', 'echivocare', 'vechi', 'succesor', 'plin', + 'chitanta-forma', 'punct-forma', 'punct-integritate', 'punct-semnatura', 'martor', 'frunza', 'includere', 'primire', 'promisiune', 'registru', 'lant', 'intarziere', + 'pagina-forma', 'pagina-integritate', 'pagina-vechi']; +const refuz = (rule, error, extra = {}) => { if (!REGULI.includes(rule)) throw new Error('agent-witness: undeclared rule ' + rule); return { ok: false, rule, error, ...extra }; }; + +/** id-ul martorului, derivat din cheia lui publica ML-DSA-65, ca agentId */ +export const witnessIdFromKey = (pub) => 'aere-witness:' + crypto.createHash('sha256').update(pub.export({ type: 'spki', format: 'der' })).digest('hex').slice(0, 40); +const semneaza = (text, privateKey) => crypto.sign(null, Buffer.from(text, 'utf8'), privateKey).toString('base64'); +const cheiePublica = (pem) => { if (typeof pem !== 'string') return null; try { return crypto.createPublicKey(pem); } catch { return null; } }; +const verificaSemnaturaText = (text, pub, sig) => { try { return crypto.verify(null, Buffer.from(text, 'utf8'), pub, Buffer.from(String(sig), 'base64')); } catch { return false; } }; + +// ------------------------------------------------------------------------------------------------ capul trimis (partea agentului) +/** + * Cererea catre martor: capul registrului la `seq` (implicit ultima intrare), ca plic AIP-23 `aere-agent-ledger-head-attestation` SEMNAT cu + * cheia agentului (forma semnaturii AIP-23: ml-dsa-65 peste textul canonic al declaratiei). `previous` = capul pe care il continua: null + * pentru primul cap trimis, un seq (hash-ul se ia din registru) sau {seq, hash} (de la martor; trebuie sa fie in acest registru). + */ +export function cerereMartor(ledger, identity, { seq = null, previous = null, createdAt = new Date().toISOString() } = {}) { + if (!ledger || !Array.isArray(ledger.entries) || !ledger.entries.length) throw new Error('agent-witness: a ledger with at least one entry is required'); + if (!identity || !identity.privateKey || identity.agentId !== ledger.agentId) throw new Error('agent-witness: the identity is not the agent of this ledger'); + const s = seq == null ? ledger.entries.length - 1 : Number(seq); + const e = ledger.entries[s]; + if (!Number.isInteger(s) || !e || e.seq !== s || !HASH_INTRARE.test(String(e.hash))) throw new Error(`agent-witness: the ledger has no entry ${seq}`); + let prev = null; + if (previous != null) { + const ps = typeof previous === 'object' ? Number(previous.seq) : Number(previous); + const pe = ledger.entries[ps]; + if (!Number.isInteger(ps) || ps >= s || !pe) throw new Error(`agent-witness: the previous head ${ps} is not an entry before ${s}`); + if (typeof previous === 'object' && previous.hash !== pe.hash) throw new Error(`agent-witness: the previous head the witness holds (seq ${ps}) is not in this ledger: this ledger is another branch`); + prev = { seq: ps, hash: pe.hash }; + } + if (!ISO.test(String(createdAt))) throw new Error('agent-witness: createdAt must be an ISO time in UTC (YYYY-MM-DDTHH:MM:SS[.mmm]Z)'); + const statement = { v: 1, kind: HEAD_KIND, agentId: ledger.agentId, session: ledger.session, policyHash: String(ledger.policyHash).toLowerCase(), seq: s, hash: e.hash, createdAt, previous: prev }; + const text = JSON.stringify(statement); + return { v: 1, kind: HEAD_KIND + '-attestation', statement, statementHash: sha(text), + signature: { scheme: ALG, publicKey: identity.publicKey.export({ type: 'spki', format: 'pem' }), signature: semneaza(text, identity.privateKey) } }; +} + +/** + * Judecata unei cereri, aceeasi la martor (la primire, cu `acumS` = ceasul lui) si la verificator (fara ceas): forma EXACTA (orice camp in + * plus e refuzat: martorul primeste un cap si semnatura lui, nimic din registru), sesiunea derivata, integritatea, cheia ML-DSA-65 din care + * se deriva agentId, semnatura, si momentul declarat la cel mult `toleranta` de ceasul martorului. + */ +export function verificaCapSemnat(env, { acumS = null, toleranta = TOLERANTA_S } = {}) { + if (!(obiect(env) && obiect(env.statement) /*PAZA-MARTOR:forma*/)) return refuz('forma', 'not an envelope {v, kind, statement, statementHash, signature}'); + const inPlus = [...Object.keys(env).filter((k) => !CAMPURI_PLIC.includes(k)), ...Object.keys(env.statement).filter((k) => !CAMPURI_CAP.includes(k)).map((k) => 'statement.' + k), + ...(obiect(env.signature) ? Object.keys(env.signature).filter((k) => !CAMPURI_SEMNATURA.includes(k)).map((k) => 'signature.' + k) : []), + ...(obiect(env.statement.previous) ? Object.keys(env.statement.previous).filter((k) => !['seq', 'hash'].includes(k)).map((k) => 'statement.previous.' + k) : [])]; + if (!(inPlus.length === 0 /*PAZA-MARTOR:continut*/)) return refuz('continut', `the witness accepts a ledger head and its signature, never the ledger's content: field(s) ${inPlus.slice(0, 5).join(', ')} refused`); + const st = env.statement; + const lipsa = [...CAMPURI_PLIC.filter((k) => !(k in env)), ...CAMPURI_CAP.filter((k) => !(k in st)).map((k) => 'statement.' + k)]; + if (lipsa.length) return refuz('forma', `missing field(s): ${lipsa.join(', ')}`); + const pv = st.previous; + const formaBuna = env.v === 1 && env.kind === HEAD_KIND + '-attestation' && st.v === 1 && st.kind === HEAD_KIND && AGENT_ID.test(sir(st.agentId)) + && SESIUNE.test(sir(st.session)) && H32.test(sir(st.policyHash)) && Number.isSafeInteger(st.seq) && st.seq >= 0 && HASH_INTRARE.test(sir(st.hash)) + && typeof st.createdAt === 'string' && ISO.test(st.createdAt) && !Number.isNaN(Date.parse(st.createdAt)) + && (pv === null || (obiect(pv) && Number.isSafeInteger(pv.seq) && pv.seq >= 0 && pv.seq < st.seq && HASH_INTRARE.test(sir(pv.hash)))) + && H32.test(sir(env.statementHash)) && obiect(env.signature) && env.signature.scheme === ALG && typeof env.signature.publicKey === 'string' && typeof env.signature.signature === 'string'; + if (!(formaBuna /*PAZA-MARTOR:forma*/)) return refuz('forma', 'the head is not well formed (agentId, session, policyHash, seq, hash of 64 hex, createdAt in UTC, previous null or {seq < seq, hash}, an ml-dsa-65 signature)'); + if (!(st.session === sessionId(st.agentId, st.policyHash) /*PAZA-MARTOR:sesiune*/)) return refuz('sesiune', 'the session is not the one derived from the agent and the policy'); + const text = JSON.stringify(st); + if (!(sha(text) === env.statementHash /*PAZA-MARTOR:integritate*/)) return refuz('integritate', 'sha256(statement) is not the statementHash (modified head)'); + const pub = cheiePublica(env.signature.publicKey); + if (!(!!pub && pub.asymmetricKeyType === ALG /*PAZA-MARTOR:cheie*/)) return refuz('cheie', `the key is ${pub ? pub.asymmetricKeyType : 'unreadable'}, not ML-DSA-65: an agent identity is an ML-DSA-65 key`); + if (!(agentIdFromKey(pub) === st.agentId /*PAZA-MARTOR:identitate*/)) return refuz('identitate', 'the agentId is not derived from the key that signs (another agent\'s id)'); + if (!(verificaSemnaturaText(text, pub, env.signature.signature) /*PAZA-MARTOR:semnatura*/)) return refuz('semnatura', 'the signature does not verify over the canonical statement text'); + if (acumS != null) { + const t = Date.parse(st.createdAt) / 1000; + if (!(Math.abs(t - Number(acumS)) <= toleranta /*PAZA-MARTOR:timp*/)) return refuz('timp', `the head says ${st.createdAt}, more than ${toleranta} s away from the witness clock (${new Date(Number(acumS) * 1000).toISOString()})`); + } + return { ok: true, head: st, statementHash: env.statementHash, pub }; +} + +// ------------------------------------------------------------------------------------------------ echivocarea (numai capete) +// ce afirma un cap semnat despre pozitia `seq`: hash-ul lui, daca e chiar acolo, sau `previous.hash`, daca il numeste ca predecesor +const afirmatia = (st, seq) => (st.seq === seq ? st.hash : st.previous && st.previous.seq === seq ? st.previous.hash : undefined); +export const dovadaEchivocare = (a, b, seq) => ({ v: 1, kind: EQUIVOCATION_KIND, agentId: a.statement.agentId, session: a.statement.session, seq, a, b }); +/** id-ul unei dovezi: sha256 peste perechea de statementHash-uri (aceeasi pereche, acelasi id) */ +export const idEchivocare = (p) => sha(JSON.stringify([p.a.statementHash, p.b.statementHash])); +/** Doua capete semnate de aceeasi cheie de agent (ML-DSA-65), in aceeasi sesiune, care dau doua hash-uri diferite aceleiasi pozitii. */ +export function verificaEchivocare(p) { + if (!obiect(p) || p.kind !== EQUIVOCATION_KIND || p.v !== 1 || !Number.isSafeInteger(p.seq)) return refuz('echivocare', `not an ${EQUIVOCATION_KIND} v1`); + for (const x of [p.a, p.b]) { const v = verificaCapSemnat(x); if (!v.ok) return refuz(v.rule, 'a head of the proof does not verify: ' + v.error); } + const A = p.a.statement, B = p.b.statement; + if (A.agentId !== p.agentId || B.agentId !== p.agentId || A.session !== p.session || B.session !== p.session) return refuz('echivocare', 'the two heads are not of the same agent and session'); + const ha = afirmatia(A, p.seq), hb = afirmatia(B, p.seq); + if (ha === undefined || hb === undefined) return refuz('echivocare', `one of the heads says nothing about position ${p.seq}`); + if (ha === hb) return refuz('echivocare', `the two heads agree on position ${p.seq}`); + return { ok: true, agentId: p.agentId, session: p.session, seq: p.seq }; +} + +// ------------------------------------------------------------------------------------------------ frunza, promisiunea, capul de jurnal +const DOMENIU_FRUNZA = Buffer.from('aere-agent-witness-leaf/1\u0000', 'utf8'); +/** datele frunzei: domeniul, cei 32 de octeti ai statementHash-ului capului primit, momentul primirii (ms, u64 big-endian) */ +export function dateFrunza(headStatementHash, receivedAtMs) { + if (!H32.test(String(headStatementHash)) || !Number.isSafeInteger(receivedAtMs) || receivedAtMs < 0) throw new Error('agent-witness: a leaf needs a 32-byte head hash and a receive time in ms'); + const t = Buffer.alloc(8); t.writeBigUInt64BE(BigInt(receivedAtMs)); + return Buffer.concat([DOMENIU_FRUNZA, Buffer.from(headStatementHash.slice(2), 'hex'), t]); +} +export const frunzaMartor = (h, r) => leafHash(dateFrunza(h, r)); + +// ------------------------------------------------------------------------------------------------ arborele jurnalului, INCREMENTAL +// ARBORELE INCREMENTAL (revizia B3 a doua, R2-06): `merkleTree` din aere-verify-layer tine o memorie pe INTERVAL care nu se goleste, iar radacina +// fiecarei marimi noi adauga in ea ~log2(n) intervale. La deschidere martorul refacea radacina FIECARUI cap de jurnal semnat, deci o stare cu un cap +// de jurnal pe frunza (trafic mic: cel mult un cap pe tur) tinea ~5 KB pe cap de jurnal in plus (masurat de verificator: 372 MB la deschiderea a +// doua cioburi pline, fata de 172-183 MB cu un cap de jurnal la 1.000), si in functionare ciobul inghetat aduna la fel, cu fiecare chitanta +// servita. Aici se tin numai blocurile PERFECTE aliniate (2^h frunze de la un multiplu de 2^h), 32 de octeti fiecare intr-un Buffer pe nivel: cel +// mult 2n hash-uri (~1,3 MB la 20.000 de frunze), adaugate o data cu frunza; radacina oricarei marimi si orice dovada de includere sau de consistenta +// se calculeaza din ele, in O(log^2 n), FARA memorie. Calculul e cel din RFC 9162 (2.1.1, 2.1.3, 2.1.4), ca in merkleTree; proba compara cele +// doua pe toate marimile pana la 70 si pe toate perechile de marimi pana la 40, iar dovezile le verifica tot verificatorul comun (verifyInclusion, +// verifyConsistency). +const NOD_PREFIX = Buffer.from([0x01]); +const nodArbore = (a, b) => crypto.createHash('sha256').update(NOD_PREFIX).update(a).update(b).digest(); +const RADACINA_GOALA = crypto.createHash('sha256').update(Buffer.alloc(0)).digest(); +const sub2 = (n) => { let k = 1; while (k * 2 < n) k *= 2; return k; }; // cea mai mare putere a lui 2 strict mai mica decat n (n >= 2) +const hx32 = (b) => '0x' + b.toString('hex'); +export function arboreIncremental() { + const niv = []; // niv[h] = { b: Buffer, n }: blocurile perfecte de 2^h frunze, al j-lea la 32j + const pune = (h, hash) => { + let L = niv[h]; if (!L) L = niv[h] = { b: Buffer.alloc(32 * 64), n: 0 }; + if ((L.n + 1) * 32 > L.b.length) { const nb = Buffer.alloc(L.b.length * 2); L.b.copy(nb, 0, 0, L.n * 32); L.b = nb; } + hash.copy(L.b, L.n * 32); return ++L.n; + }; + const bloc = (h, j) => niv[h].b.subarray(32 * j, 32 * j + 32); + const marime = () => (niv[0] ? niv[0].n : 0); + // MTH(D[a:b]) din RFC 9162, 2.1.1: un bloc perfect aliniat se citeste, altfel se imparte la cea mai mare putere a lui 2 mai mica decat marimea + const mth = (a, b) => { + const n = b - a; if (n === 0) return RADACINA_GOALA; + if ((n & (n - 1)) === 0 && a % n === 0) return bloc(31 - Math.clz32(n), a / n); + const k = sub2(n); return nodArbore(mth(a, a + k), mth(a + k, b)); + }; + const drum = (m, a, b) => { if (b - a <= 1) return []; const k = sub2(b - a); return m < k ? [...drum(m, a, a + k), mth(a + k, b)] : [...drum(m - k, a + k, b), mth(a, a + k)]; }; + const sub = (m, a, b, compl) => { + const n = b - a; if (m === n) return compl ? [] : [mth(a, b)]; + const k = sub2(n); return m <= k ? [...sub(m, a, a + k, compl), mth(a + k, b)] : [...sub(m - k, a + k, b, false), mth(a, a + k)]; + }; + const root = (n = marime()) => { if (!Number.isInteger(n) || n < 0 || n > marime()) throw new Error(`merkle: tree size ${n} outside 0..${marime()}`); return hx32(mth(0, n)); }; + return { + get size() { return marime(); }, + /** o frunza noua (hash-ul ei de frunza, 0x + 64 hex); blocurile pe care le completeaza se calculeaza acum, o singura data */ + adauga(frunzaHex) { + if (typeof frunzaHex !== 'string' || !H32.test(frunzaHex)) throw new Error('merkle: a leaf hash is 0x + 64 hex'); + let h = 0; let n = pune(0, Buffer.from(frunzaHex.slice(2), 'hex')); + while (n % 2 === 0) { const p = nodArbore(bloc(h, n - 2), bloc(h, n - 1)); h++; n = pune(h, p); } + }, + root, + inclusionProof(index, treeSize = marime()) { + if (!Number.isInteger(treeSize) || treeSize < 1 || treeSize > marime()) throw new Error(`merkle: tree size ${treeSize} outside 1..${marime()}`); + if (!Number.isInteger(index) || index < 0 || index >= treeSize) throw new Error(`merkle: index ${index} outside the tree of ${treeSize}`); + return { index, treeSize, leafHash: hx32(bloc(0, index)), path: drum(index, 0, treeSize).map(hx32), rootHash: root(treeSize) }; + }, + consistencyProof(firstSize, secondSize = marime()) { + if (!Number.isInteger(secondSize) || secondSize < 1 || secondSize > marime()) throw new Error(`merkle: tree size ${secondSize} outside 1..${marime()}`); + if (!Number.isInteger(firstSize) || firstSize < 1 || firstSize > secondSize) throw new Error(`merkle: first size ${firstSize} outside 1..${secondSize}`); + return { firstSize, secondSize, firstRoot: root(firstSize), secondRoot: root(secondSize), path: sub(firstSize, 0, secondSize, true).map(hx32) }; + }, + }; +} + +function plicSemnat(env, cheie) { const text = JSON.stringify(env.statement); return { ...env, signature: { scheme: ALG, publicKey: cheie.publicKeyPem, signature: semneaza(text, cheie.privateKey) } }; } +/** promisiunea de la primire: plic AIP-23 `time` (capul, ca digest, a existat la momentul primirii, dupa martor), semnat de martor */ +export function promisiune(cheie, headStatementHash, receivedAtMs) { + const at = new Date(receivedAtMs).toISOString(); + return plicSemnat(buildProof('time', { subjectHash: headStatementHash, source: cheie.witnessId, at, createdAt: at }), cheie); +} +/** capul de jurnal: plic AIP-23 `agent-witness-checkpoint`, semnat de martor (constructorul felului ii judeca vocabularul) */ +export function capDeJurnal(cheie, { treeSize, root, previous = null, log, chainId, maxMergeDelay, createdAt = new Date().toISOString() }) { + return plicSemnat(buildProof('agent-witness-checkpoint', { root, previous: previous || undefined, log, witness: cheie.witnessId, chainId, treeSize, maxMergeDelay, createdAt }), cheie); +} +function verificaSemnaturaMartor(env, witnessId) { + const s = env && env.signature; + if (!obiect(s) || s.scheme !== ALG || typeof s.publicKey !== 'string' || typeof s.signature !== 'string') return refuz('punct-semnatura', 'the envelope carries no ml-dsa-65 signature of the witness'); + const pub = cheiePublica(s.publicKey); + if (!(!!pub && pub.asymmetricKeyType === ALG && witnessIdFromKey(pub) === witnessId /*PAZA-MARTOR:martor*/)) return refuz('martor', `the signing key is not the ML-DSA-65 key of ${witnessId}`); + if (!(verificaSemnaturaText(JSON.stringify(env.statement), pub, s.signature) /*PAZA-MARTOR:punct-semnatura*/)) return refuz('punct-semnatura', 'the witness signature does not verify over the canonical statement text'); + return { ok: true }; +} +/** forma unei declaratii de cap de jurnal: felul, versiunea, campurile obligatorii si VOCABULARUL lor, din proof-kinds (nicio a doua copie) */ +export const formaPunctului = (st) => obiect(st) && st.kind === CHECKPOINT_KIND && st.v === 1 && FEL_PUNCT.req.every((c) => st[c] !== undefined && st[c] !== null) + && Object.entries(FEL_PUNCT.valori).every(([c, [bun]]) => st[c] === undefined || bun(st[c])); +/** un cap de jurnal intreg: forma, integritatea, semnatura martorului pe care il numeste */ +export function verificaPunct(env) { + const st = env && env.statement; + if (!(obiect(env) && env.kind === CHECKPOINT_KIND + '-attestation' && formaPunctului(st) /*PAZA-MARTOR:punct-forma*/)) return refuz('punct-forma', `not an ${CHECKPOINT_KIND} attestation with a size, a root, a log name, a witness id, a chain and a merge delay (the vocabulary of the kind in proof-kinds)`); + if (!(H32.test(sir(env.statementHash)) && sha(JSON.stringify(st)) === env.statementHash /*PAZA-MARTOR:punct-integritate*/)) return refuz('punct-integritate', 'sha256(statement) is not the statementHash of the checkpoint (modified checkpoint)'); + return verificaSemnaturaMartor(env, st.witness); +} + +// ------------------------------------------------------------------------------------------------ chitanta (verificarea fara martor) +/** o chitanta, sau raspunsul GET al martorului care o poarta in `receipt` */ +export const esteChitanta = (x) => obiect(x) && (obiect(x.witnessReceipt) || (obiect(x.receipt) && obiect(x.receipt.witnessReceipt))); +export const chitantaDin = (x) => (obiect(x) && obiect(x.receipt) && obiect(x.receipt.witnessReceipt) ? x.receipt : x); +const listaMartori = (martori) => { + if (martori == null) return null; + if (!Array.isArray(martori) || !martori.length || !martori.every(eWitnessId)) throw new Error('agent-witness: pinned witnesses must be a non-empty list of aere-witness:<40 hex>'); + return martori; +}; +/** + * Verificarea LOCALA a unei chitante (nimic din retea): capul de jurnal semnat de martor, capul primit semnat de agent, frunza refacuta + * din capul primit si momentul primirii, dovada de includere pana la radacina capului de jurnal, regula de primire a martorului, cu + * `ledger` ca capul (si predecesorul lui) sunt intrari ale ACESTUI registru, si cu `martori` ca martorul e unul FIXAT de verificator. + * Momentul certificat il da `ancoreDinChitante` (lantul). + */ +export function verificaChitanta(plic, { ledger = null, toleranta = TOLERANTA_S, martori = null } = {}) { + const fixati = listaMartori(martori); + const w = obiect(plic) ? plic.witnessReceipt : null; + // (pana la a cincea revizie, R5-04, un receivedAtMs negativ trecea de aici si arunca mai jos, in dateFrunza; un leafHash {"toString": 0} arunca aici) + if (!(obiect(w) && w.format === RECEIPT_FORMAT && obiect(w.head) && Number.isSafeInteger(w.receivedAtMs) && w.receivedAtMs >= 0 /*PAZA-MARTOR:chitanta-tipuri*/ && Number.isSafeInteger(w.leafIndex) && w.leafIndex >= 0 + && Number.isSafeInteger(w.treeSize) && w.treeSize >= 1 && H32.test(sir(w.leafHash)) && Array.isArray(w.path) && w.path.every((x) => H32.test(sir(x))) && (w.promise === undefined || obiect(w.promise)) + && Array.isArray(w.path) /*PAZA-MARTOR:chitanta-forma*/)) return refuz('chitanta-forma', `not a receipt of the agents witness (${RECEIPT_FORMAT}: head, receivedAtMs, leafIndex, treeSize, leafHash, path)`); + const p = verificaPunct(plic); + if (!p.ok) return p; + const st = plic.statement; + // martorul fixat de verificator (revizia B3, constatarile 1 si 17): fara fixare orice cheie ML-DSA-65 e un martor valid al ei insasi + if (!(!fixati || fixati.includes(st.witness) /*PAZA-MARTOR:martor*/)) return refuz('martor', `the receipt is signed by ${st.witness}, not by a witness the verifier pinned (${fixati.join(', ')})`); + const c = verificaCapSemnat(w.head, { toleranta }); + if (!c.ok) return { ...c, error: 'the head in the receipt: ' + c.error }; + if (!(frunzaMartor(w.head.statementHash, w.receivedAtMs) === w.leafHash /*PAZA-MARTOR:frunza*/)) return refuz('frunza', 'the leaf is not the leaf of this head received at this time'); + if (!(w.treeSize === st.treeSize && verifyInclusion({ leafHash: w.leafHash, index: w.leafIndex, treeSize: w.treeSize, path: w.path, rootHash: st.root }) /*PAZA-MARTOR:includere*/)) return refuz('includere', 'the inclusion proof does not lead from the leaf to the root of this checkpoint (RFC 9162, 2.1.3)'); + const C = Date.parse(c.head.createdAt) / 1000, R = w.receivedAtMs / 1000; + if (!(Math.abs(C - R) <= toleranta /*PAZA-MARTOR:primire*/)) return refuz('primire', `the witness logged a head claimed at ${c.head.createdAt} as received ${Math.round(R - C)} s later: outside its own ${toleranta} s rule`); + if (w.promise !== undefined) { + const pr = w.promise; const ps = obiect(pr) ? pr.statement : null; + if (!(obiect(ps) && pr.kind === PROMISE_KIND + '-attestation' && ps.kind === PROMISE_KIND && ps.subjectHash === w.head.statementHash && ps.source === st.witness + && typeof ps.at === 'string' && Date.parse(ps.at) === w.receivedAtMs && sha(JSON.stringify(ps)) === pr.statementHash && verificaSemnaturaMartor(pr, st.witness).ok /*PAZA-MARTOR:promisiune*/)) return refuz('promisiune', 'the promise is not the witness-signed time of this head at this receive time'); + } + if (ledger) { + const h = c.head; const e = Array.isArray(ledger.entries) ? ledger.entries[h.seq] : null; + const pe = h.previous && Array.isArray(ledger.entries) ? ledger.entries[h.previous.seq] : null; + if (!(h.agentId === ledger.agentId && h.session === ledger.session && h.policyHash === sir(ledger.policyHash).toLowerCase() && !!e && e.hash === h.hash + && (!h.previous || (!!pe && pe.hash === h.previous.hash)) /*PAZA-MARTOR:registru*/)) return refuz('registru', `the witnessed head (seq ${h.seq}${h.previous ? ', previous ' + h.previous.seq : ''}) is not in this ledger: another agent, another policy or another branch`); + } + return { ok: true, head: c.head, checkpoint: { treeSize: st.treeSize, root: st.root, statementHash: plic.statementHash, witness: st.witness, chainId: st.chainId, maxMergeDelay: st.maxMergeDelay, log: st.log }, + witnessPinned: !!fixati, receivedAt: R, createdAt: C }; +} + +/** + * Ancorele registrului din chitantele martorului, pentru verifyLedger: fiecare chitanta verificata local (verificaChitanta, cu registrul si cu + * martorii fixati), apoi capul ei de jurnal judecat de verificatorul AIP-23 de referinta (`ruleaza`, implicit verify-proof.mjs): semnatura + * martorului PASSED si finalitate post-cuantica PASSED, de unde momentul F (firstSeen certificat). Se cere: primirea nu dupa F (+ toleranta), si + * F - primire sub intarzierea maxima a martorului si a verificatorului (`mergeMax`). Ancora: {seq, hash, at: createdAt}, cu martorul numit + * (`witness`), daca a fost fixat (`witnessPinned`) si marginea pe care o da lantul singur (`chainLowerBound` = F - intarzierea - 2 x toleranta). + * `chain` (lantul cerut) si `mergeMax` sunt numere intregi pozitive, altfel refuz (o valoare gresita nu are voie sa scoata paza). `verdicte` (o Map, + * optionala) tine verdictul verificatorului AIP-23 pe cap de jurnal, intre chemari. Intoarce {stare, anchors, randuri}. + */ +export async function ancoreDinChitante(ledger, plicuri, { verifyProof, rpc, chain = null, ruleaza = ruleazaVerificatorul, mergeMax = MERGE_MAX_S, toleranta = TOLERANTA_S, tmpDir = null, martori = null, verdicte = null } = {}) { + if (!(chain == null || (Number.isSafeInteger(chain) && chain > 0))) throw new Error(`agent-witness: the chain asked must be a positive integer, not ${chain}`); + if (!(Number.isSafeInteger(Number(mergeMax)) && Number(mergeMax) > 0)) throw new Error(`agent-witness: the maximum merge delay must be a positive integer of seconds, not ${mergeMax}`); + const fixati = listaMartori(martori); + const anchors = [], randuri = []; let stare = 'OK'; + const cade = (rand) => { randuri.push({ ...rand, pass: false }); stare = 'FAILED'; }; + const nem = (rand) => { randuri.push({ ...rand, pass: null }); if (stare === 'OK') stare = 'NEMASURAT'; }; + const dir = fs.mkdtempSync(path.join(tmpDir || os.tmpdir(), 'aere-martor-agenti-')); + try { + for (const [i, x] of plicuri.entries()) { + const plic = chitantaDin(x); + const l = verificaChitanta(plic, { ledger, toleranta, martori: fixati }); + if (!l.ok) { cade({ receipt: i, rule: l.rule, detail: l.error }); continue; } + const w = l.checkpoint.witness; + const n = plic.notarization; + if (!obiect(n)) { nem({ receipt: i, seq: l.head.seq, witness: w, detail: 'the checkpoint of this receipt is not notarized yet: ask the witness again later' }); continue; } + const lantNotar = Number.isSafeInteger(n.chainId) ? n.chainId : NaN; + const lant = chain || (Number.isSafeInteger(lantNotar) ? lantNotar : null); + if (!(lantNotar === plic.statement.chainId && (!chain || chain === plic.statement.chainId) /*PAZA-MARTOR:lant*/)) { cade({ receipt: i, seq: l.head.seq, witness: w, rule: 'lant', detail: `the checkpoint names chain ${plic.statement.chainId}, its notarization ${Number.isSafeInteger(n.chainId) ? n.chainId : typeof n.chainId}${chain ? ', the verifier asks ' + chain : ''}` }); continue; } + // un cap de jurnal (plicul si notarizarea lui) se judeca O DATA, oricate chitante il poarta (`verdicte`, impartit intre chemari: sub + // --witness-url verificatorul aduce chitanta fiecarui cap tinut, iar capetele aceluiasi tur impart capul de jurnal) + const cheieV = `${lant}|${plic.statementHash}|${JSON.stringify(n)}`; + let v = verdicte && verdicte.has(cheieV) ? verdicte.get(cheieV) : undefined; + if (v === undefined) { const f = path.join(dir, `${i}.json`); fs.writeFileSync(f, JSON.stringify(plic)); v = await ruleaza(verifyProof, f, { rpc, chain: lant }); if (verdicte) verdicte.set(cheieV, v); } + if (!v) { nem({ receipt: i, seq: l.head.seq, witness: w, detail: 'the AIP-23 verifier did not answer' }); continue; } + if (!(String(v.statementHash || '').toLowerCase() === plic.statementHash /*PAZA-MARTOR:lant*/)) { cade({ receipt: i, seq: l.head.seq, witness: w, rule: 'lant', detail: 'the AIP-23 verifier judged another statementHash' }); continue; } + const sg = Array.isArray(v.levels) ? v.levels.find((x2) => x2.level === 'signature') : null; + if (!(!!sg && sg.state === 'PASSED' /*PAZA-MARTOR:lant*/)) { (sg && sg.state === 'FAILED' ? cade : nem)({ receipt: i, seq: l.head.seq, witness: w, rule: 'lant', detail: `the AIP-23 verifier gives the witness signature ${sg ? sg.state : 'no level'}` }); continue; } + const fs2 = firstSeenFromVerdict(v); + if (fs2.stare === 'FAILED') { cade({ receipt: i, seq: l.head.seq, witness: w, rule: 'lant', detail: fs2.motiv }); continue; } + if (!(fs2.stare === 'PASSED' /*PAZA-MARTOR:lant*/)) { nem({ receipt: i, seq: l.head.seq, witness: w, detail: fs2.motiv }); continue; } + const F = fs2.at; + if (!(l.receivedAt <= F + toleranta /*PAZA-MARTOR:primire*/)) { cade({ receipt: i, seq: l.head.seq, witness: w, rule: 'primire', detail: `the witness says it received the head at ${new Date(l.receivedAt * 1000).toISOString()}, after the chain first saw the checkpoint that covers it (${new Date(F * 1000).toISOString()})` }); continue; } + const mmd = Math.min(Number(mergeMax), l.checkpoint.maxMergeDelay); + if (!(F - l.receivedAt <= mmd + toleranta /*PAZA-MARTOR:intarziere*/)) { cade({ receipt: i, seq: l.head.seq, witness: w, rule: 'intarziere', detail: `the checkpoint was first seen ${Math.round(F - l.receivedAt)} s after the head was received, more than the maximum merge delay ${mmd} s: the chain cannot vouch for the time the head claims` }); continue; } + const marginaLant = Math.floor(F - mmd - 2 * toleranta); + anchors.push({ seq: l.head.seq, hash: l.head.hash, at: Math.floor(l.createdAt), firstSeen: F, receivedAt: l.receivedAt, checkpoint: plic.statementHash, treeSize: l.checkpoint.treeSize, + witness: w, witnessPinned: !!fixati, chainLowerBound: marginaLant, chainId: lant, chainAsked: !!chain, + by: fixati ? 'agents witness receipt of a pinned witness: the head the agent signed at `at`, received by the witness within 300 s of its clock, in a checkpoint first seen on chain under a post-quantum certified anchor' + : 'agents witness receipt of an UNPINNED witness (any ML-DSA-65 key can witness itself): the head the agent signed at `at`; the chain alone bounds `at` to no earlier than `chainLowerBound`' }); + randuri.push({ receipt: i, seq: l.head.seq, witness: w, pass: true, detail: `seq ${l.head.seq} claimed at ${new Date(l.createdAt * 1000).toISOString()}, received ${Math.round(l.receivedAt - l.createdAt)} s later by ${w}${fixati ? ' (pinned)' : ` (NOT pinned: the chain alone bounds the claim to no earlier than ${new Date(marginaLant * 1000).toISOString()}; pass --witness to pin it)`}, checkpoint of size ${l.checkpoint.treeSize} first seen on chain ${lant}${chain ? '' : ' (the chain the receipt declares; pass --chain to require one)'} at ${new Date(F * 1000).toISOString()}` }); + } + } finally { fs.rmSync(dir, { recursive: true, force: true }); } + return { stare, anchors: anchors.sort((a, b) => a.seq - b.seq), randuri }; +} + +/** + * Registrul aratat contine TOATE capetele pe care martorul le tine pentru agent si politica? (revizia B3, constatarea 2) Chitantele le alege + * agentul; o ramura despartita DUPA ultimul cap pentru care da o chitanta trece de ele. `raspuns` = lista intreaga a martorului + * ({agentId, session, heads: [{seq, hash, id}], equivocations: [...]}, adunata din paginile GET .../heads de `citesteCapeteMartorului`). + * FAILED "registru" daca un cap martorizat nu e intrare a registrului, FAILED "echivocare" daca martorul tine o dovada pentru sesiune. + */ +export function verificaCapeteMartorului(ledger, raspuns) { + const randuri = []; let stare = 'OK'; + if (!(obiect(raspuns) && Array.isArray(raspuns.heads) && raspuns.agentId === ledger.agentId && raspuns.session === ledger.session)) return { stare: 'NEMASURAT', randuri: [{ pass: null, detail: 'the answer of the witness is not the list of heads of this agent and policy' }] }; + // un element care nu e {seq, hash} e un raspuns pe care nu il putem judeca (NEMASURAT), nu o ramura: pana la a doua revizie (R2-04) un `null` + // arunca o exceptie iesita cu 1, iar un `5` sau un `{}` dadea "FAIL [registru] ... seq undefined" + const rau = raspuns.heads.findIndex((h) => !(obiect(h) && Number.isSafeInteger(h.seq) && h.seq >= 0)); + if (!(rau < 0 /*PAZA-MARTOR:capete-forma*/)) return { stare: 'NEMASURAT', randuri: [{ pass: null, detail: `the witness listed an element ${rau} that is not a head {seq, hash}: an answer that cannot be judged` }] }; + for (const h of raspuns.heads) { + const e = Array.isArray(ledger.entries) ? ledger.entries[h.seq] : null; + if (!(!!e && e.hash === h.hash /*PAZA-MARTOR:registru*/)) { randuri.push({ pass: false, rule: 'registru', seq: h.seq, detail: `the witness holds head seq ${h.seq} (${sir(h.hash).slice(0, 16)}...), which is not an entry of this ledger: this ledger is another branch` }); stare = 'FAILED'; } + } + const eq = Array.isArray(raspuns.equivocations) ? raspuns.equivocations : []; + if (!(eq.length === 0 /*PAZA-MARTOR:echivocare*/)) { randuri.push({ pass: false, rule: 'echivocare', detail: `the witness keeps ${eq.length} proof(s) that this agent signed two heads for one position under this policy (${eq.map((x) => sir(obiect(x) ? x.id : '')).join(', ')})` }); stare = 'FAILED'; } + if (stare === 'OK') randuri.push({ pass: true, detail: `all ${raspuns.heads.length} head(s) the witness holds for this agent and policy are entries of this ledger${raspuns.heads.length ? ' (the last: seq ' + raspuns.heads[raspuns.heads.length - 1].seq + ')' : ''}; no equivocation kept` }); + return { stare, randuri }; +} +/** + * O pagina SEMNATA a capetelor unui agent si unei politici (revizia B3 a doua, R2-01): plicul `aere-agent-witness-heads-attestation` pe care + * martorul il pune langa fiecare pagina din GET .../heads?session (`signedPage`). Se cere: forma exacta (fiecare cap {seq, hash, id}, in ordine, + * de la `from`; `next`, `count`, `latest`, dovezile pastrate, momentul), agentul, sesiunea si `from` ceruti, integritatea, semnatura ML-DSA-65 a + * martorului numit in pagina, si momentul paginii la cel mult `toleranta` de ceasul verificatorului (o pagina veche, reluata de un intermediar, + * nu e lista de acum). Intoarce {ok, statement} sau un refuz numit. + */ +export function verificaPaginaCapetelor(env, { agentId, session, from, acumS = Date.now() / 1000, toleranta = TOLERANTA_S }) { + const st = obiect(env) ? env.statement : null; + const capBun = (h, i, l) => obiect(h) && Object.keys(h).length === 3 && Number.isSafeInteger(h.seq) && h.seq >= from && HASH_INTRARE.test(sir(h.hash)) && H32.test(sir(h.id)) && (i === 0 || h.seq > l[i - 1].seq); + const formaBuna = obiect(env) && env.kind === PAGINA_KIND + '-attestation' && obiect(st) && st.v === 1 && st.kind === PAGINA_KIND && eWitnessId(st.witness) + && st.agentId === agentId && st.session === session && st.from === from && sir(st.witness) === st.witness && Array.isArray(st.heads) && st.heads.length <= PAGINA_MAX && st.heads.every(capBun) + && (st.next === null || (Number.isSafeInteger(st.next) && st.heads.length > 0 && st.next > st.heads[st.heads.length - 1].seq)) + && Number.isSafeInteger(st.count) && st.count >= st.heads.length && Array.isArray(st.equivocations) && st.equivocations.every((q) => obiect(q) && H32.test(sir(q.id)) && Number.isSafeInteger(q.seq)) + && (st.latest === null || (obiect(st.latest) && Number.isSafeInteger(st.latest.seq) && HASH_INTRARE.test(sir(st.latest.hash)))) && typeof st.at === 'string' && ISO.test(st.at); + if (!(formaBuna /*PAZA-MARTOR:pagina-forma*/)) return refuz('pagina-forma', `not a page of heads signed by a witness (${PAGINA_KIND}: the agent, the policy and the page asked, each head {seq, hash, id} in order, next, count, latest, the kept proofs, the time)`); + if (!(H32.test(sir(env.statementHash)) && sha(JSON.stringify(st)) === env.statementHash /*PAZA-MARTOR:pagina-integritate*/)) return refuz('pagina-integritate', 'sha256(statement) is not the statementHash of the page (a modified page)'); + const s = verificaSemnaturaMartor(env, st.witness); + if (!(s.ok /*PAZA-MARTOR:pagina-semnatura*/)) return { ...s, error: 'the page of heads: ' + s.error }; + const t = Date.parse(st.at) / 1000; + if (!(Math.abs(t - Number(acumS)) <= toleranta /*PAZA-MARTOR:pagina-vechi*/)) return refuz('pagina-vechi', `the page was signed at ${st.at}, more than ${toleranta} s from the verifier clock: a page of another time, not the list of now`); + return { ok: true, statement: st }; +} +/** + * Lista intreaga a capetelor martorului de la `baza` pentru agent si sesiune, pagina cu pagina (`?from=&limit=`), din paginile SEMNATE de martor + * (verificaPaginaCapetelor): campurile nesemnate de langa ele, si /health, le poate scrie oricine sta la adresa aleasa (pana la a doua revizie, + * R2-01, id-ul martorului se lua nesemnat din /health). Paginile trebuie sa fie ale aceluiasi martor si sa se lege (`next` -> `from`), iar ultima + * sa dea chiar numarul si ultimul cap semnate. Intoarce {ok, witness, at, agentId, session, heads, equivocations} sau {ok: false, motiv}: un martor + * care nu raspunde nu e un zero. + */ +export async function citesteCapeteMartorului(baza, agentId, session, { cere = cereJson, paginiMax = 1000, acumS = () => Date.now() / 1000, toleranta = TOLERANTA_S } = {}) { + const U = String(baza).replace(/\/+$/, ''); + const heads = []; let from = 0, witness = null; + for (let p = 0; p < paginiMax; p++) { + const r = await cere(`${U}/v1/agents/witness/${agentId}/heads?session=${session}&from=${from}&limit=${PAGINA_MAX}`); + if (r.status !== 200 || !obiect(r.j)) return { ok: false, motiv: `the witness did not list the heads (${r.status || r.eroare})` }; + const v = verificaPaginaCapetelor(r.j.signedPage, { agentId, session, from, acumS: acumS(), toleranta }); + if (!v.ok) return { ok: false, rule: v.rule, motiv: `the page of heads from seq ${from} is not a list signed by a witness now: [${v.rule}] ${v.error}` }; + const st = v.statement; + if (witness && st.witness !== witness) return { ok: false, motiv: `the pages are signed by two witnesses (${witness}, ${st.witness})` }; + witness = st.witness; heads.push(...st.heads); + if (st.next === null) { + const ultim = heads[heads.length - 1]; + const aduna = heads.length === st.count && (ultim ? !!st.latest && st.latest.seq === ultim.seq && st.latest.hash === ultim.hash : st.latest === null); + if (!(aduna /*PAZA-MARTOR:pagina-numar*/)) return { ok: false, motiv: `the signed pages give ${heads.length} head(s), but the last page signs ${st.count} and another latest head: the list changed while it was read, ask again` }; + return { ok: true, witness, at: st.at, agentId, session, heads, equivocations: st.equivocations }; + } + from = st.next; + } + return { ok: false, motiv: `more than ${paginiMax} pages of heads` }; +} +/** + * O cerere care, pe un 429 al martorului, asteapta cat spune Retry-After si reincearca (revizia B3 a cincea, R5-06): verificatorul aduce cate o + * chitanta pe cap tinut, iar serviciul da 1.200 de citiri pe client in 10 minute. `buget` ({ ramasS }) e comun tuturor cererilor unei verificari: + * un 429 fara Retry-After, sau cu o asteptare peste ce a ramas din buget, se intoarce asa cum e (cel care cheama il numeste NEMASURAT). + */ +export function cereRabdator(buget, { cere = cereJson, dormi = (ms) => new Promise((g) => setTimeout(g, ms)) } = {}) { + return async (url, o) => { + for (let i = 0; ; i++) { + const r = await cere(url, o); + if (!(r.status === 429 && Number.isSafeInteger(r.retryAfter) && r.retryAfter <= buget.ramasS && i < 50) /*PAZA-MARTOR:asteapta-429*/) return r; + buget.ramasS -= r.retryAfter; buget.asteptatS = (buget.asteptatS || 0) + r.retryAfter; await dormi(r.retryAfter * 1000 + 50); + } + }; +} +/** Starea unui cap la martorul de la `baza` (GET /v1/agents/witness/): {status, j} sau {status: 0, eroare}. Chitanta din ea se verifica toata. */ +export const stareaCapuluiLa = (baza, id, { cere = cereJson } = {}) => cere(`${String(baza).replace(/\/+$/, '')}/v1/agents/witness/${id}`); +async function cereJson(url, o = {}) { + try { const r = await fetch(url, { ...o, signal: AbortSignal.timeout(30000), headers: { 'content-type': 'application/json', 'user-agent': 'aere-agent-witness/1', ...(o.headers || {}) } }); let j = null; try { j = await r.json(); } catch { j = null; } + const ra = r.headers.get('retry-after'); return { status: r.status, j, retryAfter: ra !== null && /^\d{1,6}$/.test(ra) ? Number(ra) : null }; } + catch (e) { return { status: 0, eroare: String(e.message || e).slice(0, 200) }; } +} + +// ------------------------------------------------------------------------------------------------ jurnalul martorului (partea serviciului) +/** cheia martorului (ML-DSA-65, PKCS#8 PEM, 0600), nascuta la prima deschidere a starii; nu se tipareste niciodata */ +export function cheiaMartorului(dir) { + const f = path.join(dir, 'witness.key.pem'); + if (!fs.existsSync(f)) { const { privateKey } = crypto.generateKeyPairSync(ALG); fs.writeFileSync(f, privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600, flag: 'wx' }); } + const privateKey = crypto.createPrivateKey(fs.readFileSync(f, 'utf8')); + if (privateKey.asymmetricKeyType !== ALG) throw new Error('agent-witness: the witness key is not ML-DSA-65'); + const publicKey = crypto.createPublicKey(privateKey); + return { privateKey, publicKey, publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }), witnessId: witnessIdFromKey(publicKey) }; +} +// SCRIERILE DURABILE (revizia B3, constatarea 16): fs.writeSync poate scrie MAI PUTIN decat i se da (discul aproape plin) fara sa arunce, iar +// pana la reparatie nimeni nu citea ce intoarce: in memorie frunza exista, pe disc ramanea jumatate de rand, si frunza urmatoare se lipea de el. +// Acum se scrie pana la ultimul octet; la esec fisierul se taie inapoi la marimea de dinainte (iar daca nici asta nu merge, martorul nu mai +// primeste nimic pana la repornire, cand randul taiat e ultimul si se taie la deschidere). +function scrieTot(fd, buf) { + let off = 0; + while (off < buf.length) { + const n = fs.writeSync(fd, buf, off, buf.length - off); + if (!(Number.isSafeInteger(n) && n > 0)) throw Object.assign(new Error('agent-witness: a write wrote nothing'), { code: 'EIO' }); + off += n; + } +} +const fsyncDosar = (d) => { if (process.platform === 'win32') return; let fd; try { fd = fs.openSync(d, 'r'); fs.fsyncSync(fd); } catch { /* sistemul nu permite: ramane fsync-ul fisierului */ } finally { if (fd !== undefined) fs.closeSync(fd); } }; +let contorTmp = 0; +function scrieAtomic(f, text) { + const t = `${f}.tmp-${process.pid}-${++contorTmp}`; const fd = fs.openSync(t, 'w'); + try { scrieTot(fd, Buffer.from(text, 'utf8')); fs.fsyncSync(fd); } catch (e) { fs.closeSync(fd); fs.rmSync(t, { force: true }); throw e; } + fs.closeSync(fd); fs.renameSync(t, f); fsyncDosar(path.dirname(f)); +} +function adaugaDurabil(f, text) { + const buf = Buffer.from(text, 'utf8'); const fd = fs.openSync(f, 'a'); let inainte; + try { inainte = fs.fstatSync(fd).size; scrieTot(fd, buf); fs.fsyncSync(fd); } + catch (e) { + if (inainte !== undefined) { + // pe Windows un descriptor deschis cu 'a' nu are dreptul de a taia (SetEndOfFile); atunci se taie dupa cale + try { fs.ftruncateSync(fd, inainte); fs.fsyncSync(fd); } catch { try { fs.truncateSync(f, inainte); } catch { e.taiereEsuata = true; } } + } + throw e; + } + finally { fs.closeSync(fd); } +} +const pidViu = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } }; +// LACATUL STARII (revizia B3 a doua, R2-10): pana la reparatie lacatul tinea numai pid-ul, iar dupa o oprire necurata (pana de curent, reset, +// panica) ramanea pe disc; daca dupa repornire pid-ul era al altui proces viu (zona deasa a pid-urilor de pornire; EPERM se numara "viu"), martorul +// refuza sa porneasca la fiecare incercare si unitatea ramanea `failed`. Acum lacatul isi poarta si pornirea MASINII (`boot_id` pe Linux; pe +// alte sisteme momentul pornirii din os.uptime, la cel mult 120 s) si momentul pornirii PROCESULUI (campul 22 din /proc//stat, unde exista): +// un lacat scris inainte de ultima pornire a masinii, sau al carui pid e acum al altui proces, nu tine starea. Un lacat in forma veche (numai +// pid-ul) se judeca la fel ca inainte. +export function idPornire() { + try { const b = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim(); if (/^[0-9a-f-]{36}$/.test(b)) return 'linux:' + b; } catch { /* nu e Linux */ } + return 'up:' + Math.round(Date.now() / 1000 - os.uptime()); +} +export function pornireProces(pid) { + try { const s = fs.readFileSync(`/proc/${pid}/stat`, 'utf8'); const f = s.slice(s.lastIndexOf(')') + 2).split(' '); return /^\d+$/.test(f[19] || '') ? f[19] : null; } catch { return null; } +} +const aceeasiPornire = (a, b) => (String(a).startsWith('up:') && String(b).startsWith('up:') ? Math.abs(Number(String(a).slice(3)) - Number(String(b).slice(3))) <= 120 : a === b); +/** Tine lacatul `text` starea? {tinut, pid, motiv}. `viu`, `pornire` si `boot` se pot inlocui (probele). */ +export function lacatTinut(text, { propriu = process.pid, viu = pidViu, pornire = pornireProces, boot = idPornire() } = {}) { + let o = null; try { o = JSON.parse(String(text)); } catch { o = null; } + if (typeof o === 'number') o = { pid: o }; // forma veche: numai pid-ul + if (!obiect(o) || !Number.isSafeInteger(o.pid) || o.pid <= 0) return { tinut: false, pid: null, motiv: 'an unreadable lock' }; + if (o.pid === propriu) return { tinut: false, pid: o.pid, motiv: 'the lock of this process' }; + if (!viu(o.pid)) return { tinut: false, pid: o.pid, motiv: 'its process is gone' }; + if (!(o.boot == null || aceeasiPornire(o.boot, boot) /*PAZA-MARTOR:lacat-pornire*/)) return { tinut: false, pid: o.pid, motiv: 'written before the last start of the machine' }; + const acum = o.start == null ? null : pornire(o.pid); + if (!(acum == null || acum === String(o.start) /*PAZA-MARTOR:lacat-proces*/)) return { tinut: false, pid: o.pid, motiv: 'its pid now belongs to another process' }; + return { tinut: true, pid: o.pid, motiv: 'held' }; +} +const numeCiob = (log, k) => (k === 0 ? log : `${log}/${k}`); +// cautarea dupa seq intr-o lista de capete ordonata crescator dupa seq +function gaseste(lista, seq) { let a = 0, b = lista.length - 1; while (a <= b) { const m = (a + b) >> 1; const q = lista[m].q; if (q === seq) return lista[m]; if (q < seq) a = m + 1; else b = m - 1; } return null; } +function primulDeLa(lista, seq) { let a = 0, b = lista.length; while (a < b) { const m = (a + b) >> 1; if (lista[m].q < seq) a = m + 1; else b = m; } return a; } + +/** + * Deschide (sau creeaza) starea martorului: `dir/martor.json` (jurnalul, lantul, martorul), cheia, `cioburi//` (frunzele `frunze.ndjson`, + * adaugate si sincronizate pe disc INAINTE de raspuns; capetele semnate primite `capete/`; capetele de jurnal si notarizarea lor `puncte/`), + * `echivocari-2/` (dovezile pastrate; `echivocari/` tine dovezile formei de dinainte de a doua revizie, citite mai departe). Un lacat (`.lacat`: pid-ul, + * pornirea masinii si a procesului) refuza un al doilea proces pe aceeasi stare. + * La deschidere se REFUZA o stare stricata (un rand de frunza stricat in mijloc, o frunza fara capul ei, un cap de jurnal semnat peste mai multe + * frunze decat sunt, sau a carui radacina nu se reface): martorul ar semna altfel o ramura a propriului jurnal. Un rand taiat la coada (proces + * oprit in mijlocul scrierii, deci fara raspuns) se taie. + * `maxEchivocari` (implicit CIOBURI_PASTRATE x maxFrunze, marginea prin constructie) il coboara numai o proba. `faraLant` (laboratorul, fara + * nicio notarizare): rotirea nu asteapta notarizarea capului de jurnal final al ciobului care se scoate. + */ +export function deschideMartor(dir, { log = LOG_IMPLICIT, chainId = LANT, maxMergeDelay = MERGE_MAX_S, maxFrunze = PLAFON_FRUNZE, maxEchivocari = null, + ceas = () => Date.now(), toleranta = TOLERANTA_S, jurnal = () => {}, faraLant = false } = {}) { + if (!(Number.isSafeInteger(maxFrunze) && maxFrunze >= 1)) throw new Error('agent-witness: maxFrunze must be an integer >= 1'); + // plafonul dovezilor = marginea lor prin constructie (revizia B3 a doua, R2-02): o dovada pe sesiune, legata de ciobul capului contrazis + if (maxEchivocari == null) maxEchivocari = CIOBURI_PASTRATE * maxFrunze; + if (!(Number.isSafeInteger(maxEchivocari) && maxEchivocari >= 0)) throw new Error('agent-witness: maxEchivocari must be an integer >= 0'); + for (const d of ['', 'cioburi', 'echivocari', 'echivocari-2']) fs.mkdirSync(path.join(dir, d), { recursive: true }); + const lacat = path.join(dir, '.lacat'); + if (fs.existsSync(lacat)) { const t = lacatTinut(fs.readFileSync(lacat, 'utf8')); if (t.tinut) throw new Error(`agent-witness: the state ${dir} is held by process ${t.pid}`); jurnal(`agent-witness: a lock of process ${t.pid} was left (${t.motiv}); removed`); fs.rmSync(lacat, { force: true }); } + fs.writeFileSync(lacat, JSON.stringify({ pid: process.pid, boot: idPornire(), start: pornireProces(process.pid) }), { flag: 'wx' }); + try { return deschide(); } catch (e) { try { fs.rmSync(lacat, { force: true }); } catch { /* idem */ } throw e; } + + function deschide() { + const cheie = cheiaMartorului(dir); + const fc = path.join(dir, 'martor.json'); + const conf = { log, chainId, witness: cheie.witnessId, maxMergeDelay }; + if (fs.existsSync(fc)) { + const v = JSON.parse(fs.readFileSync(fc, 'utf8')); + for (const k of Object.keys(conf)) if (v[k] !== conf[k]) throw new Error(`agent-witness: the state was made with ${k}=${v[k]}, not ${conf[k]}; a log never changes its name, chain, witness or merge delay`); + if (v.format === 'aere-agent-witness-state-1') { + // starea din forma 1 (un singur jurnal, la radacina): devine ciobul 0, cu aceleasi frunze, capete si capete de jurnal + const d0 = path.join(dir, 'cioburi', '0'); fs.mkdirSync(d0, { recursive: true }); + for (const f of ['frunze.ndjson', 'puncte', 'capete']) if (fs.existsSync(path.join(dir, f))) fs.renameSync(path.join(dir, f), path.join(d0, f)); + scrieAtomic(fc, JSON.stringify({ ...v, format: 'aere-agent-witness-state-2', migratedAt: new Date(ceas()).toISOString() }, null, 1) + '\n'); + jurnal('agent-witness: the state of form 1 became shard 0 of form 2'); + } else if (v.format !== 'aere-agent-witness-state-2') throw new Error(`agent-witness: unknown state format ${v.format}`); + } else scrieAtomic(fc, JSON.stringify({ format: 'aere-agent-witness-state-2', ...conf, createdAt: new Date(ceas()).toISOString() }, null, 1) + '\n'); + + // ---- cioburile: se pastreaza ultimele CIOBURI_PASTRATE; unul mai vechi ramas pe disc (o stergere intrerupta) se sterge acum, NUMAI daca + // capul lui de jurnal final e notarizat (revizia B3 a doua, R2-07: altfel capetele lui ar ramane fara moment certificat, pentru totdeauna); + // un ciob nascut pe jumatate (`.nou-*`, o creare intrerupta, R2-08) nu e un ciob si se sterge. + // STERGEREA unui ciob (revizia B3 a treia, R3-01): pana atunci rotirea stergea dosarul ciobului ASINCRON, pe loc, iar o oprire in acea + // fereastra (o repornire, un OOM, o pana) lasa un REST: ciob.json si frunze.ndjson plecau primele, puncte/ golit, capete/ pe jumatate. La + // deschidere R2-07 il judeca drept un ciob intreg nenotarizat si il TINEA: gol, nu mai putea fi notarizat niciodata, deci rotirea astepta + // pentru totdeauna (503 notarization_behind pentru toti), sau, cu capete de jurnal ramase, martorul refuza sa porneasca. Acum un ciob de + // scos se REDENUMESTE intai, sincron, in `.sters--*` (o redenumire e atomica), si abia apoi se sterge; deschiderea sterge fara judecata + // tot ce poarta acest nume. Daca redenumirea nu merge, dosarul primeste intai marcajul `.de-sters`. Iar un ciob mai vechi decat cele + // pastrate caruia ii lipseste ciob.json sau frunze.ndjson (un rest al formei de dinainte, care nu redenumea) nu e un ciob, ci un rest. + const dc = path.join(dir, 'cioburi'); + for (const f of fs.readdirSync(dc)) if (/^\.(nou|sters)-/.test(f)) { try { fs.rmSync(path.join(dc, f), { recursive: true, force: true, maxRetries: 3 }); } catch { /* la urmatoarea deschidere */ } } + let ks = fs.readdirSync(dc).filter((f) => /^\d+$/.test(f)).map(Number).sort((a, b) => a - b); + if (!ks.length) { creeazaCiobPeDisc(0, null); ks = [0]; } + const kMax = ks[ks.length - 1]; + const finalNotarizatPeDisc = (k) => { + try { const pd = path.join(dc, String(k), 'puncte'); const n = fs.readdirSync(pd).filter((f) => /^\d+\.json$/.test(f)).map((f) => Number(f.slice(0, -5))).sort((a, b) => b - a)[0]; + return n !== undefined && !!JSON.parse(fs.readFileSync(path.join(pd, n + '.json'), 'utf8')).notarization; } catch { return false; } + }; + // doua garzi, fiecare cu randul ei (revizia B3 a patra, R4-06: plantata intreaga, jumatatea marcajului nu era masurata singura) + const marcatDeSters = (k) => fs.existsSync(path.join(dc, String(k), '.de-sters')) /*PAZA-MARTOR:ciob-marcat*/; + const faraFisiereleCiobului = (k) => { const d = path.join(dc, String(k)); return (!fs.existsSync(path.join(d, 'ciob.json')) || !fs.existsSync(path.join(d, 'frunze.ndjson'))) /*PAZA-MARTOR:ciob-fara-fisiere*/; }; + const restDeStergere = (k) => marcatDeSters(k) || faraFisiereleCiobului(k); + // (o stergere care nu reuseste acum, de pilda fiindca cea asincrona a rotirii inca ruleaza, nu opreste pornirea: ciobul nu se mai incarca) + // (de la primul ciob vechi pastrat, toate cele de dupa raman: cioburile pastrate sunt mereu consecutive) + const pastrate = new Set(); let tine = false; + for (const k of ks) { + if (k > kMax - CIOBURI_PASTRATE) { pastrate.add(k); continue; } + if (restDeStergere(k) /*PAZA-MARTOR:ciob-rest*/) { jurnal(`agent-witness: shard ${k} is the rest of an interrupted removal (marked, or without ciob.json or frunze.ndjson): removed`); try { stergeDosarCiob(k, false); } catch (e) { jurnal(`agent-witness: shard ${k} could not be removed yet (${e.code || 'error'})`); } continue; } + if (tine) { pastrate.add(k); continue; } + if (!(faraLant || finalNotarizatPeDisc(k) /*PAZA-MARTOR:ciob-vechi-notarizat*/)) { pastrate.add(k); tine = true; jurnal(`agent-witness: shard ${k} is older than the shards kept, but its final checkpoint is not notarized: kept until it is`); continue; } + try { stergeDosarCiob(k, false); } catch (e) { jurnal(`agent-witness: shard ${k} is not kept and could not be removed yet (${e.code || 'error'})`); } + } + ks = ks.filter((k) => pastrate.has(k)); + for (let j = 1; j < ks.length; j++) if (ks[j] !== ks[j - 1] + 1) throw new Error(`agent-witness: shard ${ks[j - 1] + 1} is missing between ${ks[j - 1]} and ${ks[j]}`); + + const cioburi = new Map(); // k -> ciobul + const dupaCap = new Map(); // statementHash-ul capului -> frunza (din oricare ciob pastrat) + const capete = new Map(); // agentId|session -> { agentId, session, policyHash, heads: [frunze ordonate dupa seq] } + const sesiuniAgent = new Map(); // agentId -> Map(session -> sesiunea) + // dovezile pastrate: id -> { id, z (sesiunea), seq, k (ciobul capului contrazis), v1 (fisier al formei de dinainte, in echivocari/) }; sesiunea + // tine id-ul dovezii ei in `z.eq` (una pe sesiune) + const echivocari = new Map(); + const echivNepastrate = { una: 0, plafon: 0 }; // dovezi intoarse si nepastrate, de la pornire: sesiunea avea deja una / plafonul + const fisierEchiv = (q) => path.join(dir, q.v1 ? 'echivocari' : 'echivocari-2', q.id.slice(2) + '.json'); + let stricat = false; // o scriere a cazut si nici taierea inapoi nu a mers: nu se mai primeste nimic pana la repornire + // capetele refuzate de bugetul pe ora, numarate pe ora curenta: /health le arata (o inundatie care intarzie pe toti se vede, nu se ghiceste) + const refuzBuget = { de: 0, n: 0, total: 0, notarizare: 0 }; + const numaraRefuzBuget = (acumMs) => { if (acumMs - refuzBuget.de >= 3_600_000) { refuzBuget.de = acumMs; refuzBuget.n = 0; } refuzBuget.n++; refuzBuget.total++; }; + + // un ciob de scos: REDENUMIT intai (sincron, atomic) in `.sters--*`, apoi sters (asincron dupa o rotire, cu zeci de mii de fisiere); + // daca redenumirea nu merge, dosarul e marcat `.de-sters` inainte de stergere (revizia B3 a treia, R3-01). Un rest ramas dupa o oprire in + // timpul stergerii nu mai poate fi luat drept un ciob la deschidere. + function stergeDosarCiob(k, asincron) { + const d = path.join(dc, String(k)); let tinta = d; + try { const t = path.join(dc, `.sters-${k}-${process.pid}-${++contorTmp}`); fs.renameSync(d, t) /*PAZA-MARTOR:ciob-sters-intai*/; fsyncDosar(dc); tinta = t; } + catch (e) { if (e.code === 'ENOENT') return; try { fs.writeFileSync(path.join(d, '.de-sters'), ''); } catch { /* ramane restul: il judeca deschiderea */ } } + if (asincron) fs.rm(tinta, { recursive: true, force: true }, (e) => { if (e) jurnal(`agent-witness: shard ${k} could not be removed yet (${e.code || 'error'}); the next open removes it`); }); + else fs.rmSync(tinta, { recursive: true, force: true, maxRetries: 3 }); + } + // un ciob nou se naste INTREG (revizia B3 a doua, R2-08): dosarul cu ciob.json (care numeste capul final al ciobului de dinainte) se face sub un + // nume temporar si abia apoi se redenumeste; pana la reparatie mkdir venea intai, iar o scriere cazuta (ENOSPC) urmata de o repornire lasa un + // ciob fara ciob.json, pe care deschiderea il rescria cu previousShardFinal null: legatura dintre cioburi pierduta pe veci + function creeazaCiobPeDisc(k, inainte) { + const d = path.join(dc, String(k)); const fcb = path.join(d, 'ciob.json'); + const meta = JSON.stringify({ format: 'aere-agent-witness-shard-1', k, log: numeCiob(log, k), previousShardFinal: inainte, createdAt: new Date(ceas()).toISOString() }, null, 1) + '\n'; + if (fs.existsSync(d)) { if (!fs.existsSync(fcb)) scrieAtomic(fcb, meta); return d; } // un dosar al formei de dinainte, fara ciob.json + const t = path.join(dc, `.nou-${k}-${process.pid}-${++contorTmp}`); + try { + for (const s of ['', 'capete', 'puncte']) fs.mkdirSync(path.join(t, s), { recursive: true }); + scrieAtomic(path.join(t, 'ciob.json'), meta); fs.renameSync(t, d) /*PAZA-MARTOR:ciob-intreg*/; fsyncDosar(dc); + } catch (e) { try { fs.rmSync(t, { recursive: true, force: true }); } catch { /* la urmatoarea deschidere */ } throw e; } + return d; + } + const sesiunea = (a, s, p) => { + const key = a + '|' + s; let z = capete.get(key); + if (!z) { z = { key, agentId: a, session: s, policyHash: p, heads: [], eq: null }; capete.set(key, z); if (!sesiuniAgent.has(a)) sesiuniAgent.set(a, new Map()); sesiuniAgent.get(a).set(s, z); } + return z; + }; + function incarcaCiob(k) { + const d = path.join(dc, String(k)); + for (const s of ['capete', 'puncte']) fs.mkdirSync(path.join(d, s), { recursive: true }); + const fcb = path.join(d, 'ciob.json'); + let meta; + if (fs.existsSync(fcb)) meta = JSON.parse(fs.readFileSync(fcb, 'utf8')); + else { + // un ciob fara ciob.json (o creare intrerupta a formei de dinainte, R2-08): legatura se reface din capul de jurnal FINAL al ciobului de + // dinainte, daca el e pastrat si capul lui acopera toate frunzele lui (rotirea il scrie inainte de ciobul nou); altfel ramane null, spus + const pr = cioburi.get(k - 1); const fin = pr && pr.puncte.length ? pr.puncte[pr.puncte.length - 1] : null; + const legat = fin && fin.treeSize === pr.frunze.length ? fin.statementHash : null; + meta = { k, log: numeCiob(log, k), previousShardFinal: legat /*PAZA-MARTOR:ciob-legatura*/ }; + if (k > 0 && !legat) jurnal(`agent-witness: shard ${k} had no ciob.json and the final checkpoint of shard ${k - 1} is not here: its link to the shard before is lost`); + scrieAtomic(fcb, JSON.stringify({ format: 'aere-agent-witness-shard-1', ...meta, createdAt: new Date(ceas()).toISOString(), repairedAt: new Date(ceas()).toISOString() }, null, 1) + '\n'); + } + if (meta.k !== k || meta.log !== numeCiob(log, k)) throw new Error(`agent-witness: shard ${k} names itself ${meta.k} / ${meta.log}`); + const ciob = { k, dir: d, log: meta.log, inainte: meta.previousShardFinal || null, createdAt: meta.createdAt || null, ff: path.join(d, 'frunze.ndjson'), frunze: [], puncte: [], notarizate: [], arbore: arboreIncremental() }; + if (fs.existsSync(ciob.ff)) { + const text = fs.readFileSync(ciob.ff, 'utf8'); const randuri = text.split('\n'); const coada = randuri.pop(); + let octeti = 0; + for (const [i, r] of randuri.entries()) { + let o = null; try { o = JSON.parse(r); } catch { o = null; } + // numai ULTIMUL rand poate fi taiat (fara '\n'); un rand stricat urmat de altele e o stare stricata, nu o coada + const bun = obiect(o) && o.i === i && H32.test(String(o.h)) && Number.isSafeInteger(o.r) && o.r >= 0 && AGENT_ID.test(String(o.a)) && SESIUNE.test(String(o.s)) && Number.isSafeInteger(o.q) && o.q >= 0 + && (o.e === undefined || HASH_INTRARE.test(String(o.e))) && (o.p === undefined || H32.test(String(o.p))); + if (!(bun /*PAZA-MARTOR:frunze-mijloc*/)) throw new Error(`agent-witness: shard ${k}, frunze.ndjson line ${i} is damaged and lines follow it: refusing to start (cutting it would lose leaves already answered, under signed checkpoints)`); + const fh = path.join(d, 'capete', o.h.slice(2) + '.json'); + if (!fs.existsSync(fh)) throw new Error(`agent-witness: shard ${k}: the head of leaf ${i} is missing`); + let e = o.e, p = o.p; + if (e === undefined || p === undefined) { const st = JSON.parse(fs.readFileSync(fh, 'utf8')).envelope.statement; e = st.hash; p = st.policyHash; } // forma 1 + const z = sesiunea(o.a, o.s, p); + const x = { h: o.h, r: o.r, q: o.q, e, k, i, z }; + ciob.frunze.push(x); ciob.arbore.adauga(frunzaMartor(o.h, o.r)); dupaCap.set(x.h, x); z.heads.push(x); + octeti += Buffer.byteLength(r, 'utf8') + 1; + } + if (coada !== '' /*PAZA-MARTOR:coada*/) { fs.truncateSync(ciob.ff, octeti); jurnal(`agent-witness: shard ${k}: a leaf line cut at the tail (never answered) was cut`); } + } + // capetele de jurnal: in memorie numai rezumatul (plicul semnat sta pe disc si se citeste cand se cere o chitanta) + const nume = fs.readdirSync(path.join(d, 'puncte')).filter((f) => /^\d+\.json$/.test(f)).map((f) => Number(f.slice(0, -5))).sort((a, b) => a - b); + nume.forEach((n, j) => { if (n !== j) throw new Error(`agent-witness: shard ${k}: checkpoint ${j} is missing or misnumbered`); }); + for (const n of nume) { + const pf = JSON.parse(fs.readFileSync(path.join(d, 'puncte', n + '.json'), 'utf8')); const st = pf.envelope.statement; + const p = { k, n, statementHash: pf.envelope.statementHash, treeSize: st.treeSize, root: st.root, createdAt: st.createdAt, notarization: pf.notarization || null, attempts: pf.attempts || 0, lastError: pf.lastError || null }; + if (st.log !== ciob.log || st.witness !== cheie.witnessId) throw new Error(`agent-witness: shard ${k}: checkpoint ${n} is of another log or witness`); + // un cap de jurnal semnat peste mai multe frunze decat sunt pe disc, sau cu alta radacina: frunze confirmate s-au pierdut sau s-au schimbat. + // Doua garzi, fiecare cu proba ei (revizia B3 a doua, R2-15: o singura conditie plantata intreaga lasa jumatatea "radacina" nemasurata) + if (!(p.treeSize <= ciob.frunze.length /*PAZA-MARTOR:radacina-marime*/)) throw new Error(`agent-witness: shard ${k}: checkpoint ${n} was signed over ${p.treeSize} leaves, and the leaves on disk are ${ciob.frunze.length}: refusing to start (the witness would sign a fork of its own log)`); + if (!(ciob.arbore.root(p.treeSize) === p.root /*PAZA-MARTOR:radacina*/)) throw new Error(`agent-witness: shard ${k}: checkpoint ${n} was signed over ${p.treeSize} leaves with root ${String(p.root).slice(0, 18)}..., and the leaves on disk do not give it: refusing to start (the witness would sign a fork of its own log)`); + if (ciob.puncte.length && p.treeSize <= ciob.puncte[ciob.puncte.length - 1].treeSize) throw new Error(`agent-witness: shard ${k}: checkpoint ${n} does not grow the log`); + ciob.puncte.push(p); if (p.notarization) ciob.notarizate.push(p); + } + cioburi.set(k, ciob); + return ciob; + } + for (const k of ks) incarcaCiob(k); + let cur = cioburi.get(ks[ks.length - 1]); + // dovezile pastrate: in memorie numai indexul. Forma 2 (echivocari-2/, de la a doua revizie): {v: 2, ciob, held, seq, b}, legata de ciobul capului + // contrazis `held` (pastrat, altfel dovada se sterge). Forma 1 (echivocari/, dovada intreaga): legata acum tot de ciobul capului ei `a`, cand acesta + // e pastrat; altfel se sterge (fara capul contrazis, martorul nu mai tine nimic din ce contrazice ea) + for (const [sub, v1] of [['echivocari-2', false], ['echivocari', true]]) { + for (const f of fs.readdirSync(path.join(dir, sub)).filter((x) => /^[0-9a-f]{64}\.json$/.test(x))) { + const fp = path.join(dir, sub, f); let o = null; try { o = JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { o = null; } + let x = null, seq = null; + if (!v1 && obiect(o) && o.v === 2 && obiect(o.b) && H32.test(String(o.held))) { x = dupaCap.get(o.held) || null; seq = o.seq; if (x && x.k !== o.ciob) x = null; } + else if (v1 && obiect(o)) { const p = obiect(o.proof) ? o.proof : o; if (p.kind === EQUIVOCATION_KIND && obiect(p.a)) { x = dupaCap.get(String(p.a.statementHash)) || null; seq = p.seq; } } + if (!x || !Number.isSafeInteger(seq)) { fs.rmSync(fp, { force: true }); continue; } + // doua dovezi pe aceeasi sesiune (o inlocuire intrerupta, R3-02): ramane cea al carei cap contrazis sta in ciobul mai nou + const vechi = x.z.eq ? echivocari.get(x.z.eq) : null; + if (vechi && vechi.k >= x.k /*PAZA-MARTOR:echivocare-departajare*/) { fs.rmSync(fp, { force: true }); continue; } + if (vechi) { echivocari.delete(vechi.id); fs.rmSync(fisierEchiv(vechi), { force: true }); } + const id = '0x' + f.slice(0, 64); + echivocari.set(id, { id, z: x.z, seq, k: x.k, v1 }); x.z.eq = id; + } + } + + const fisierCap = (x) => path.join(dc, String(x.k), 'capete', x.h.slice(2) + '.json'); + const capulPrimit = (x) => JSON.parse(fs.readFileSync(fisierCap(x), 'utf8')); + const plicPunct = (p) => JSON.parse(fs.readFileSync(path.join(dc, String(p.k), 'puncte', p.n + '.json'), 'utf8')).envelope; + // capul de jurnal care acopera frunza: cel mai TIMPURIU notarizat (momentul cel mai strans), altfel cel mai timpuriu construit + function acoperitor(x) { + const c = cioburi.get(x.k); const caut = (l) => { let a = 0, b = l.length; while (a < b) { const m = (a + b) >> 1; if (l[m].treeSize > x.i) b = m; else a = m + 1; } return a < l.length ? l[a] : null; }; + return caut(c.notarizate) || caut(c.puncte); + } + function stareScurta(x) { + const p = acoperitor(x); + return { seq: x.q, hash: x.e, id: x.h, log: cioburi.get(x.k).log, leafIndex: x.i, receivedAt: new Date(x.r).toISOString(), + status: !p ? 'pending' : p.notarization ? 'notarized' : 'checkpointed', checkpoint: p ? { n: p.n, treeSize: p.treeSize, statementHash: p.statementHash } : null }; + } + function construiestePunctIn(c) { + const ultim = c.puncte[c.puncte.length - 1]; + if (!c.frunze.length || (ultim && ultim.treeSize >= c.frunze.length)) return null; + const n = c.puncte.length; + const env = capDeJurnal(cheie, { treeSize: c.frunze.length, root: c.arbore.root(c.frunze.length), previous: ultim ? ultim.statementHash : c.inainte, log: c.log, chainId, maxMergeDelay, createdAt: new Date(ceas()).toISOString() }); + scrieAtomic(path.join(c.dir, 'puncte', n + '.json'), JSON.stringify({ n, envelope: env, notarization: null, attempts: 0, lastError: null }) + '\n'); + const p = { k: c.k, n, statementHash: env.statementHash, treeSize: env.statement.treeSize, root: env.statement.root, createdAt: env.statement.createdAt, notarization: null, attempts: 0, lastError: null }; + c.puncte.push(p); return p; + } + function scoateCiob(k) { + const c = cioburi.get(k); if (!c) return; + cioburi.delete(k); + const atinse = new Set(); + for (const x of c.frunze) { dupaCap.delete(x.h); atinse.add(x.z); } + // o singura trecere pe sesiune (capetele unei sesiuni sunt ordonate, cele ale ciobului scos sunt la inceput) + for (const z of atinse) { + z.heads = z.heads.filter((x) => x.k > k); + if (!z.heads.length) { capete.delete(z.key); const m = sesiuniAgent.get(z.agentId); if (m) { m.delete(z.session); if (!m.size) sesiuniAgent.delete(z.agentId); } } + } + // dovezile capetelor acestui ciob pleaca odata cu ele (o dovada fara capul contrazis nu mai arata nimic din ce tine martorul) + for (const [id, q] of echivocari) if (q.k <= k) { echivocari.delete(id); if (q.z.eq === id) q.z.eq = null; try { fs.rmSync(fisierEchiv(q), { force: true }); } catch { /* se sterge la urmatoarea deschidere */ } } + // pe disc: dupa ce memoria nu il mai stie; redenumit intai, apoi sters asincron (zeci de mii de fisiere), iar o stergere intrerupta o reia + // urmatoarea deschidere, fara sa il mai poata lua drept un ciob (R3-01) + stergeDosarCiob(k, true); + } + // capul de jurnal final al unui ciob, notarizat (peste toate frunzele lui) + const finalNotarizat = (c) => { const p = c.puncte[c.puncte.length - 1]; return !!p && !!p.notarization && p.treeSize === c.frunze.length; }; + // o rotire acum ar scoate un ciob al carui cap de jurnal final nu e notarizat? (primeste refuza atunci capetele noi: R2-07) + const rotireaPierdeNenotarizat = () => !faraLant && [...cioburi.values()].some((c) => c.k <= cur.k + 1 - CIOBURI_PASTRATE && !finalNotarizat(c)); + // ciobul curent e plin: capul lui de jurnal FINAL (peste toate frunzele), apoi ciobul urmator, apoi se scoate cel mai vechi peste plafon (primeste + // nu cheama rotirea cat un ciob de scos e nenotarizat) + function roteste() { + const vechi = cur; + construiestePunctIn(vechi); + const fin = vechi.puncte[vechi.puncte.length - 1]; + const k = vechi.k + 1; creeazaCiobPeDisc(k, fin.statementHash); + cur = incarcaCiob(k); + for (const j of [...cioburi.keys()]) if (j <= k - CIOBURI_PASTRATE) scoateCiob(j); + jurnal(`agent-witness: shard ${vechi.k} (${vechi.log}) is full at ${vechi.frunze.length} leaves, final checkpoint ${fin.n}; now shard ${k} (${cur.log})`); + } + // un ciob mai vechi decat cel curent e INGHETAT: capul lui de jurnal final trebuie sa acopere toate frunzele (o rotire intrerupta) + for (const c of cioburi.values()) if (c !== cur) construiestePunctIn(c); + + function stareaCapului(id) { + const x = dupaCap.get(String(id).toLowerCase()); + if (!x) return { cod: 404, corp: { error: 'unknown_head', hint: 'POST the head first; the id is its statementHash. A head of a shard the sandbox no longer keeps is unknown too (its receipt stays verifiable without the witness)', sandbox: SANDBOX } }; + const c = cioburi.get(x.k); const cap = capulPrimit(x); const s = stareScurta(x); + const baza = { id: x.h, agentId: x.z.agentId, session: x.z.session, seq: x.q, log: c.log, leafIndex: x.i, receivedAt: s.receivedAt, promise: cap.promise, sandbox: SANDBOX }; + const p = acoperitor(x); + if (!p) return { cod: 200, corp: { status: 'pending', ...baza, maxMergeDelay, mergeBy: new Date(x.r + maxMergeDelay * 1000).toISOString(), hint: 'the next checkpoint will cover it' } }; + const d = c.arbore.inclusionProof(x.i, p.treeSize); + const receipt = { ...plicPunct(p), ...(p.notarization ? { notarization: p.notarization } : {}), + witnessReceipt: { format: RECEIPT_FORMAT, head: cap.envelope, receivedAtMs: x.r, receivedAt: s.receivedAt, leafIndex: x.i, treeSize: p.treeSize, leafHash: d.leafHash, path: d.path, promise: cap.promise } }; + return { cod: 200, corp: { status: p.notarization ? 'notarized' : 'checkpointed', ...baza, checkpoint: { n: p.n, treeSize: p.treeSize, statementHash: p.statementHash }, + ...(p.notarization ? {} : { notarization: p.lastError ? { state: 'retrying', lastError: p.lastError } : { state: 'pending' } }), receipt } }; + } + // o echivocare: dovada se intoarce MEREU; se PASTREAZA numai prima a sesiunii (agent si politica), legata de ciobul capului contrazis `tinut` + // (revizia B3 a doua, R2-02: legata de ciobul curent, cum era, o dovada traia mai mult decat capul ei, iar dovezile unor sesiuni proprii puteau + // umple plafonul global, dupa care echivocarea reala a altui agent nu mai era pastrata). Pe disc numai capul nou semnat; capul contrazis e in + // capete/ al ciobului ei. Un plafon sub marginea prin constructie (numai in probe) refuza pastrarea, numarata in /health. + // DOVADA CARE TRAIESTE CEL MAI MULT (revizia B3 a treia, R3-02): o dovada pastrata pleaca odata cu ciobul capului pe care il contrazice, deci un + // agent care isi ocupa singur locul sesiunii cu o echivocare contra unui cap din ciobul VECHI facea ca echivocarea care conteaza (contra unui + // cap din ciobul curent) sa fie intoarsa nepastrata, iar dupa rotire sesiunea ramanea fara nicio dovada. Acum o dovada noua al carei cap + // contrazis sta intr-un ciob mai NOU ia locul celei pastrate (tot una pe sesiune, deci aceeasi margine). + function echivoca(tinut, env, poz, limitaAgent, detail) { + const proof = dovadaEchivocare(capulPrimit(tinut).envelope, env, poz); const pid = idEchivocare(proof); const z = tinut.z; + let kept = echivocari.has(pid); let inlocuita = null; + const q0 = z.eq ? echivocari.get(z.eq) || null : null; + const inlocuieste = !!q0 && q0.k < tinut.k /*PAZA-MARTOR:echivocare-mai-noua*/; + if (!kept && (inlocuieste || !z.eq /*PAZA-MARTOR:echivocare-una*/) && (inlocuieste || echivocari.size < maxEchivocari /*PAZA-MARTOR:echivocare-plafon*/)) { + // pastrarea e o schimbare de stare: o numara limita pe agent (o dovada deja pastrata, nu) + if (limitaAgent && limitaAgent(proof.agentId) /*PAZA-MARTOR:ritm*/) return { cod: 429, corp: { error: 'rate_limited', detail: 'too many changes for this agent in the window', sandbox: SANDBOX } }; + const q = { id: pid, z, seq: poz, k: tinut.k /*PAZA-MARTOR:echivocare-ciob*/, v1: false }; + scrieAtomic(fisierEchiv(q), JSON.stringify({ v: 2, ciob: q.k, held: tinut.h, seq: poz, b: env }) + '\n'); + if (inlocuieste) { echivocari.delete(q0.id); try { fs.rmSync(fisierEchiv(q0), { force: true }); } catch { /* o dovada in plus pe disc: deschiderea pastreaza una pe sesiune */ } inlocuita = q0.id; } + echivocari.set(pid, q) /*PAZA-MARTOR:echivocare*/; z.eq = pid; + kept = true; + } else if (!kept) { if (z.eq) echivNepastrate.una++; else echivNepastrate.plafon++; } + return { cod: 409, corp: { error: 'equivocation', detail, proof, proofId: pid, kept, ...(inlocuita ? { replaced: inlocuita } : {}), ...(kept ? {} : { note: z.eq ? 'not kept: the witness keeps one proof per agent and policy (it holds ' + z.eq + ', which lives at least as long); this one is returned to you' : 'not kept: the witness keeps at most ' + maxEchivocari + ' proofs in all; this one is returned to you' }), sandbox: SANDBOX } }; + } + + return { + dir, cheie, witnessId: cheie.witnessId, log, chainId, maxMergeDelay, maxFrunze, + /** Primeste un cap trimis. Intoarce {cod, corp}: 202 primit, 200 deja vazut, 4xx refuz numit, 409 consistenta sau echivocare, 429 limita pe agent + * (`limitaAgent`: numara numai schimbarile de stare, dupa semnatura), 503 bugetul jurnalului pe ora (`limitaGlobala`) sau o scriere cazuta. */ + primeste(env, { limitaAgent = null, limitaGlobala = null } = {}) { + if (stricat) return { cod: 503, corp: { error: 'witness_error', code: 'io_error', detail: 'the witness could not write its log and accepts nothing until it restarts', sandbox: SANDBOX } }; + const acumMs = ceas(); + const v = verificaCapSemnat(env, { acumS: acumMs / 1000, toleranta }); + if (!v.ok) return { cod: ['cheie', 'identitate', 'semnatura'].includes(v.rule) ? 403 : v.rule === 'timp' ? 422 : 400, corp: { error: 'refused', rule: v.rule, detail: v.error, sandbox: SANDBOX } }; + const st = v.head; const id = v.statementHash; + // un cap DEJA VAZUT (acelasi plic, sau aceeasi pozitie cu acelasi hash si un predecesor care nu contrazice nimic) nu schimba nimic, deci + // nu consuma cota agentului: pana la reparatie oricine consuma cota unui agent retrimitandu-i capul PUBLIC (revizia B3, constatarea 11) + if (dupaCap.has(id)) return stareaCapului(id); + const z = capete.get(st.agentId + '|' + st.session); const lista = z ? z.heads : []; + const laSeq = gaseste(lista, st.seq); + const laPrev = st.previous ? gaseste(lista, st.previous.seq) : null; + // 1. aceeasi pozitie, alt hash: echivocare + if (laSeq && !(laSeq.e === st.hash) /*PAZA-MARTOR:echivocare*/) return echivoca(laSeq, env, st.seq, limitaAgent, `two heads signed by the agent give position ${st.seq} two hashes`); + // 2. predecesorul numit contra unui cap deja vazut la acea pozitie: echivocare, INAINTE de iesirea "deja vazut" (pana la reparatie un cap + // cu acelasi seq si hash dar cu predecesorul contrazis iesea 200 si contradictia semnata nu era pastrata; revizia B3, constatarea 4) + if (laPrev && !(laPrev.e === st.previous.hash) /*PAZA-MARTOR:echivocare*/) return echivoca(laPrev, env, st.previous.seq, limitaAgent, `the head names as its previous (seq ${st.previous.seq}) a hash the agent signed otherwise before`); + // 3. aceeasi pozitie, acelasi hash, nimic contrazis: acelasi cap, cerut din nou cu alt moment declarat + if (laSeq) return stareaCapului(laSeq.h); + const ultim = lista.length ? lista[lista.length - 1] : null; + const latest = ultim ? { seq: ultim.q, hash: ultim.e, id: ultim.h } : null; + // 4. un cap mai VECHI decat ultimul vazut (si nevazut la pozitia lui): o intoarcere in timp, refuzata + if (!(!ultim || st.seq > ultim.q) /*PAZA-MARTOR:vechi*/) return { cod: 409, corp: { error: 'stale_head', detail: `seq ${st.seq} is older than the last head witnessed for this agent and policy (seq ${ultim.q})`, latest, sandbox: SANDBOX } }; + // 5. succesorul: `previous` trebuie sa fie exact ultimul cap vazut (null daca niciunul), ca old size din C2SP tlog-witness + const prevBun = ultim ? !!st.previous && st.previous.seq === ultim.q && st.previous.hash === ultim.e : st.previous === null; + if (!(prevBun /*PAZA-MARTOR:succesor*/)) return { cod: 409, corp: { error: 'not_successor', detail: ultim ? `the head must name as previous the last head witnessed (seq ${ultim.q})` : 'the witness holds no head for this agent and policy: the first head names previous null', latest, sandbox: SANDBOX } }; + // 6. bugetul jurnalului pe ora (comun): un ciob nu se umple mai repede decat in o zi, deci chitantele lui se servesc cel putin o zi. Bugetul + // se CITESTE aici (`limitaGlobala.ar`, fara sa consume) si se consuma abia dupa cota agentului: un cap refuzat de buget nu schimba nimic, + // deci nu consuma cota agentului (revizia B3 a doua, R2-13: pana atunci pasul cotei venea intai, si un agent care reincerca in timpul + // unei inundatii primea 429 si dupa ce bugetul se elibera). O functie fara `.ar` (o proba) se cheama o data, aici. + const bugetPlin = limitaGlobala ? (typeof limitaGlobala.ar === 'function' ? limitaGlobala.ar() : limitaGlobala()) : false; + if (bugetPlin /*PAZA-MARTOR:buget*/) return (numaraRefuzBuget(acumMs), { cod: 503, corp: { error: 'log_busy', detail: 'the sandbox log takes a limited number of new heads per hour, for everyone; try again later', retryAfterS: 300, sandbox: SANDBOX } }); + // 7. ciobul curent plin si o rotire ar scoate un ciob al carui cap de jurnal final nu e inca notarizat: capetele NOI asteapta (revizia B3 a doua, + // R2-07: pana atunci ciobul se scotea oricum, iar capetele lui, primite cu 202 si "mergeBy", ramaneau fara moment certificat pentru totdeauna) + const plin = !(cur.frunze.length < maxFrunze); + if (!(!plin || !rotireaPierdeNenotarizat() /*PAZA-MARTOR:rotire-notarizata*/)) { refuzBuget.notarizare++; return { cod: 503, corp: { error: 'log_busy', code: 'notarization_behind', detail: 'the log is full and the final checkpoint of the shard it would remove is not notarized yet; new heads wait until it is', retryAfterS: 600, sandbox: SANDBOX } }; } + // 8. limita pe agent: numai o cerere AUTENTIFICATA care SCHIMBA starea (un cap nou) o consuma + if (limitaAgent && limitaAgent(st.agentId) /*PAZA-MARTOR:ritm*/) return { cod: 429, corp: { error: 'rate_limited', detail: 'too many heads for this agent in the window', sandbox: SANDBOX } }; + if (limitaGlobala && typeof limitaGlobala.ar === 'function') limitaGlobala(); // bugetul, acum consumat (a fost numai citit la pasul 6) + // 9. ciobul curent plin: rotirea (pana la reparatie: 503 log_full pentru totdeauna) + if (!(cur.frunze.length < maxFrunze /*PAZA-MARTOR:plin*/)) roteste(); + // primit: capul semnat si promisiunea pe disc, apoi frunza, sincronizate INAINTE de raspuns + const promise = promisiune(cheie, id, acumMs); + const fh = path.join(cur.dir, 'capete', id.slice(2) + '.json'); + scrieAtomic(fh, JSON.stringify({ envelope: env, receivedAtMs: acumMs, promise }) + '\n'); + const i = cur.frunze.length; + try { adaugaDurabil(cur.ff, JSON.stringify({ i, h: id, r: acumMs, a: st.agentId, s: st.session, p: st.policyHash, q: st.seq, e: st.hash }) + '\n'); } + catch (e) { if (e.taiereEsuata) stricat = true; try { fs.rmSync(fh, { force: true }); } catch { /* ramane un cap fara frunza, nefolosit */ } throw e; } + const zz = sesiunea(st.agentId, st.session, st.policyHash); + const x = { h: id, r: acumMs, q: st.seq, e: st.hash, k: cur.k, i, z: zz }; + cur.frunze.push(x); cur.arbore.adauga(frunzaMartor(id, acumMs)); dupaCap.set(id, x); zz.heads.push(x); + return { cod: 202, corp: { status: 'pending', id, agentId: st.agentId, session: st.session, seq: st.seq, log: cur.log, leafIndex: i, receivedAt: new Date(acumMs).toISOString(), promise, + maxMergeDelay, mergeBy: new Date(acumMs + maxMergeDelay * 1000).toISOString(), poll: '/v1/agents/witness/' + id, sandbox: SANDBOX } }; + }, + stareaCapului, + /** Un cap de jurnal nou peste toate frunzele ciobului curent, daca au venit frunze de la ultimul; semnat, scris pe disc. Intoarce-l sau null. */ + construiestePunct() { return construiestePunctIn(cur); }, + /** capetele de jurnal de notarizat: ultimul al fiecarui ciob pastrat, daca nu e notarizat (cele vechi nenotarizate sunt acoperite de el) */ + deNotarizate() { return [...cioburi.values()].map((c) => c.puncte[c.puncte.length - 1]).filter((p) => p && !p.notarization); }, + deNotarizat() { return this.deNotarizate()[0] || null; }, + plicPunct, + inregistreazaNotarizarea(ref, rez) { + const p = typeof ref === 'number' ? cur.puncte[ref] : (cioburi.get(ref && ref.k) || { puncte: [] }).puncte[ref && ref.n]; + if (!p) throw new Error('agent-witness: no checkpoint ' + JSON.stringify(ref)); + const c = cioburi.get(p.k); const f = path.join(c.dir, 'puncte', p.n + '.json'); const pf = JSON.parse(fs.readFileSync(f, 'utf8')); + p.attempts++; + // public numai un COD (unfunded, chain_refused, ...): mesajul intreg merge numai in jurnalul local (revizia B3, constatarea 18) + if (rez && rez.ok) { p.notarization = rez.notarization; p.lastError = null; c.notarizate.push(p); c.notarizate.sort((a, b) => a.treeSize - b.treeSize); } + else p.lastError = rez && typeof rez.cod === 'string' && /^[a-z_]{1,40}$/.test(rez.cod) ? rez.cod : 'notarization_failed'; + scrieAtomic(f, JSON.stringify({ ...pf, notarization: p.notarization, attempts: p.attempts, lastError: p.lastError }) + '\n'); + return p; + }, + /** + * Capetele martorizate ale unui agent. Cu `session`: o PAGINA a capetelor acelei sesiuni (`from` = primul seq, `limit` <= PAGINA_MAX), cu + * `next` (seq-ul de unde continua, sau null), ultimul cap si id-urile dovezilor pastrate (dovada intreaga: `dovadaEchivocarii`). Fara: sesiunile + * agentului (cel mult PAGINA_MAX, de la `after`), fiecare cu numarul capetelor si ultimul cap. Nimic citit de pe disc (revizia B3, constatarea 10). + * O pagina a unei sesiuni poarta si `signedPage`: aceeasi pagina (fiecare cap {seq, hash, id}, `next`, `count`, `latest`, dovezile pastrate) cu + * momentul ei, SEMNATA de martor (revizia B3 a doua, R2-01); verificatorul crede numai pagina semnata (verificaPaginaCapetelor). + */ + capeteAgent(agentId, session = null, { from = 0, limit = PAGINA_MAX, after = null } = {}) { + if (!(Number.isSafeInteger(from) && from >= 0 && Number.isSafeInteger(limit) && limit >= 1 && limit <= PAGINA_MAX)) throw new Error(`agent-witness: from >= 0 and 1 <= limit <= ${PAGINA_MAX}`); + const eqLista = (z) => (z && z.eq && echivocari.has(z.eq) ? [{ id: z.eq, seq: echivocari.get(z.eq).seq }] : []); + const m = sesiuniAgent.get(agentId); + if (session != null) { + const z = m ? m.get(session) || null : null; const l = z ? z.heads : []; + const a = primulDeLa(l, from); const pagina = l.slice(a, a + limit); + const ultim = l.length ? l[l.length - 1] : null; + const latest = ultim ? { seq: ultim.q, hash: ultim.e, id: ultim.h } : null; const next = a + limit < l.length ? l[a + limit].q : null; const equivocations = eqLista(z); + const st = { v: 1, kind: PAGINA_KIND, witness: cheie.witnessId, log, agentId, session, from, limit, count: l.length, latest, + heads: pagina.map((x) => ({ seq: x.q, hash: x.e, id: x.h })), next, equivocations, at: new Date(ceas()).toISOString() }; + const signedPage = plicSemnat({ v: 1, kind: PAGINA_KIND + '-attestation', statement: st, statementHash: sha(JSON.stringify(st)) }, cheie); + return { agentId, session, policyHash: z ? z.policyHash : null, count: l.length, latest, heads: pagina.map(stareScurta), next, equivocations, signedPage, sandbox: SANDBOX }; + } + const sortate = [...(m ? m.keys() : [])].sort().filter((s) => after == null || s > after); + const pagina = sortate.slice(0, PAGINA_MAX); + return { agentId, sessions: pagina.map((s) => { const z = m.get(s); const ultim = z.heads.length ? z.heads[z.heads.length - 1] : null; + return { session: s, policyHash: z.policyHash, count: z.heads.length, latest: ultim ? { seq: ultim.q, hash: ultim.e, id: ultim.h } : null, equivocations: eqLista(z).length }; }), + nextSession: sortate.length > PAGINA_MAX ? pagina[pagina.length - 1] : null, sandbox: SANDBOX }; + }, + /** o dovada de echivocare pastrata, dupa id-ul ei (forma 2: refacuta din capul contrazis, tinut de martor, si capul nou, din fisierul ei) */ + dovadaEchivocarii(id) { + const q = echivocari.get(String(id).toLowerCase()); + if (!q) return { cod: 404, corp: { error: 'unknown_equivocation', sandbox: SANDBOX } }; + const o = JSON.parse(fs.readFileSync(fisierEchiv(q), 'utf8')); + const proof = q.v1 ? (obiect(o.proof) ? o.proof : o) : dovadaEchivocare(capulPrimit(dupaCap.get(o.held)).envelope, o.b, o.seq); + return { cod: 200, corp: { id: q.id, agentId: q.z.agentId, session: q.z.session, seq: q.seq, proof, sandbox: SANDBOX } }; + }, + /** + * Ultimul cap de jurnal al unui ciob pastrat (implicit cel curent; `shard`: si unul INGHETAT, al carui ultim cap e cel final) si cel notarizat, + * iar cu `from` dovada de consistenta in acel ciob de la marimea aceea (RFC 9162, 2.1.4). Pana la a doua revizie (R2-09) numai ciobul curent: + * dupa rotire un monitor nu putea cere consistenta ciobului inghetat pana la capul lui final. + */ + capulJurnalului(from = null, shard = null) { + const c = shard == null ? cur : cioburi.get(Number(shard)) /*PAZA-MARTOR:ciob-cerut*/; + if (!c) return { cod: 404, corp: { error: 'unknown_shard', shardsKept: [...cioburi.keys()], sandbox: SANDBOX } }; + const ultim = c.puncte[c.puncte.length - 1] || null; const notarizat = c.notarizate[c.notarizate.length - 1] || null; + const corp = { log: c.log, shard: c.k, frozen: c !== cur, previousShardFinal: c.inainte, witness: cheie.witnessId, chainId, maxMergeDelay, leaves: c.frunze.length, maxLeaves: maxFrunze, + latest: ultim ? { ...plicPunct(ultim), ...(ultim.notarization ? { notarization: ultim.notarization } : {}) } : null, + latestNotarized: notarizat ? { ...plicPunct(notarizat), notarization: notarizat.notarization } : null, + shards: [...cioburi.values()].map((x) => ({ shard: x.k, log: x.log, leaves: x.frunze.length, frozen: x !== cur, previousShardFinal: x.inainte, latest: x.puncte.length ? x.puncte[x.puncte.length - 1].statementHash : null, finalNotarized: x !== cur ? finalNotarizat(x) : undefined })), sandbox: SANDBOX }; + if (from != null && ultim) { + const mm = Number(from); const ts = ultim.treeSize; + if (!Number.isSafeInteger(mm) || mm < 1 || mm > ts) return { cod: 400, corp: { error: 'bad_from', hint: `1..${ts}`, sandbox: SANDBOX } }; + corp.consistency = c.arbore.consistencyProof(mm, ts); + } + return { cod: 200, corp }; + }, + sanatate() { + const ultim = cur.puncte[cur.puncte.length - 1] || null; let notarizat = null; + for (const c of [...cioburi.values()].reverse()) if (!notarizat && c.notarizate.length) notarizat = c.notarizate[c.notarizate.length - 1]; + return { ok: !stricat, version: VERSION, log, witness: cheie.witnessId, chainId, maxMergeDelay, + shard: cur.k, shardLog: cur.log, leaves: cur.frunze.length, maxLeaves: maxFrunze, shardsKept: [...cioburi.keys()], leavesKept: [...cioburi.values()].reduce((a, c) => a + c.frunze.length, 0), + checkpoints: cur.puncte.length, equivocations: echivocari.size, maxEquivocations: maxEchivocari, sessions: capete.size, + // dovezile intoarse si NEPASTRATE de la pornire: sesiunea avea deja una (normal), sau plafonul (pe starea reala nu se atinge: R2-02) + equivocationsNotKept: { onePerSession: echivNepastrate.una, cap: echivNepastrate.plafon }, + logBusy: { refusedThisHour: ceas() - refuzBuget.de < 3_600_000 ? refuzBuget.n : 0, refusedSinceStart: refuzBuget.total, notarizationBehindSinceStart: refuzBuget.notarizare, + rotationWaitsForNotarization: !(cur.frunze.length < maxFrunze) && rotireaPierdeNenotarizat() }, + lastCheckpoint: ultim ? { shard: cur.k, n: ultim.n, treeSize: ultim.treeSize, createdAt: ultim.createdAt, notarized: !!ultim.notarization, lastError: ultim.lastError } : null, + lastNotarized: notarizat ? { shard: notarizat.k, n: notarizat.n, treeSize: notarizat.treeSize, firstSeen: notarizat.notarization.firstSeen, txHash: notarizat.notarization.txHash || null } : null, sandbox: SANDBOX }; + }, + inchide() { try { const t = lacatTinut(fs.readFileSync(lacat, 'utf8'), { propriu: -1 }); if (t.pid === process.pid) fs.rmSync(lacat, { force: true }); } catch { /* deja scos */ } }, + }; + } +} + +// ------------------------------------------------------------------------------------------------ linia de comanda (partea agentului) +class Folosire extends Error {} +async function main(argv) { + const [cmd, ...rest] = argv; + // un steag dat fara valoare (pus ultimul, sau urmat de alt steag) ori cu valoarea goala e folosire gresita (2), nu un steag nedat (revizia B3 a + // treia, R3-03: `verify-receipt --ledger ""`, sau `--ledger` pus ultimul, scotea in tacere legarea chitantei de registru) + const get = (f) => { const i = rest.indexOf(f); if (i < 0) return undefined; const v = rest[i + 1]; if (!(v !== undefined && v !== '' && !/^--[a-z]/.test(v)) /*PAZA-MARTOR:steag-fara-valoare*/) throw new Folosire(`${f} needs a value`); return v; }; + // STEAGURILE CUNOSCUTE ale fiecarei comenzi (revizia B3 a patra, R4-05): argumentele vin in perechi steag + valoare; un argument care nu e un + // steag al comenzii (`--witness=`, `--ledger=`, o greseala de tastare ca `--ledgr`) e folosire gresita (2), nu un steag nedat, iar un steag + // dat de doua ori la fel, afara de --witness (repetabil). Pana atunci `verify-receipt --witness=` iesea 0 pentru chitanta oricarui martor. + const STEAGURI = { request: ['--key', '--ledger', '--seq', '--previous', '--out'], submit: ['--key', '--ledger', '--url', '--seq', '--out'], receipt: ['--url', '--id', '--ledger', '--witness', '--out'], + 'verify-receipt': ['--receipt', '--ledger', '--witness'], 'verify-equivocation': ['--proof'] }; + if (Object.hasOwn(STEAGURI, cmd)) { + const vazute = new Set(); + for (let i = 0; i < rest.length; i += 2) { + const s = String(rest[i]); + if (!(STEAGURI[cmd].includes(s) /*PAZA-MARTOR:steag-necunoscut*/)) throw new Folosire(`${cmd}: unknown argument ${JSON.stringify(s.slice(0, 60))} (its flags: ${STEAGURI[cmd].join(' ')}; the value follows its flag, never --flag=value)`); + if (!(!vazute.has(s) || s === '--witness') /*PAZA-MARTOR:steag-dublu*/) throw new Folosire(`${cmd}: ${s} is given twice`); + vazute.add(s); + } + } + // toate aparitiile unui steag repetabil, CU valorile goale (revizia B3 a doua, R2-12): `--witness ""` (o variabila necompletata), `--witness ,` sau + // un `--witness` final fara valoare dadeau o lista goala, adica "nefixat", si comanda iesea 0 pentru chitanta oricarui martor; acum sunt folosire + // gresita, ca in agent-cli.mjs + const toate = (f) => rest.flatMap((x, i) => (x === f ? (rest[i + 1] === undefined ? [''] : String(rest[i + 1]).split(',')) /*PAZA-MARTOR:steag-gol*/ : [])); + const citeste = (f, ce) => { if (!f) throw new Folosire(`--${ce} is required`); try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { throw new Folosire(`cannot read ${ce} file ${f}: ${e.message}`); } }; + const scrie = (o, f) => { const s = JSON.stringify(o, null, 1) + '\n'; if (f) fs.writeFileSync(f, s); else process.stdout.write(s); }; + const identitate = () => { const k = get('--key'); if (!k) throw new Folosire('--key agent.key.pem is required'); return agentFromPrivateKeyPem(fs.readFileSync(k, 'utf8')); }; + const baza = () => { const u = get('--url'); if (!u || !/^https?:\/\//.test(u)) throw new Folosire('--url is required'); return u.replace(/\/+$/, ''); }; + const cere = cereJson; + if (cmd === 'request') { + const p = get('--previous'); const prev = p == null || p === 'none' ? null : Number(p); + const env = cerereMartor(citeste(get('--ledger'), 'ledger'), identitate(), { seq: get('--seq') != null ? Number(get('--seq')) : null, previous: prev }); + scrie(env, get('--out')); if (get('--out')) console.error(`head seq ${env.statement.seq}, previous ${prev ?? 'none'}: ${env.statementHash}`); return 0; + } + if (cmd === 'submit') { + const ledger = citeste(get('--ledger'), 'ledger'); const id = identitate(); const U = baza(); + const h = await cere(`${U}/v1/agents/witness/${ledger.agentId}/heads?session=${ledger.session}&from=0&limit=1`); + if (h.status !== 200 || !h.j) { console.error(`NOT MEASURED: the witness did not list the heads (${h.status || h.eroare})`); return 2; } + // regula (c) pe drumul "nimic nou" (revizia B3 a sasea, N3): `latest` vine din campul NESEMNAT al paginii, deci tipul fiecarei valori se judeca + // inainte de folosire: iesire 2, --out nescris, niciodata o exceptie. Pana atunci un {"toString":0} in seq sau in id arunca (iesire 1), iar un + // status {"toString":0} in starea capului scria --out si apoi arunca + const ultim = h.j.latest === undefined || h.j.latest === null ? null : h.j.latest; + if (!(ultim === null || (obiect(ultim) && Number.isSafeInteger(ultim.seq) && ultim.seq >= 0 && HASH_INTRARE.test(sir(ultim.hash)) && H32.test(sir(ultim.id)))) /*PAZA-MARTOR:nimic-nou-latest*/) { console.error('NOT MEASURED: the witness listed a latest head that is not {seq, hash, id} (a wrong form or wrong types): ask again later'); return 2; } + const s = get('--seq') != null ? Number(get('--seq')) : ledger.entries.length - 1; + // nimic nou de trimis (revizia B3, constatarea 7): martorul tine deja chiar intrarea ceruta; pana la reparatie iesea 1 ("REFUSED") + if (ultim && Array.isArray(ledger.entries)) { + const e = ledger.entries[ultim.seq]; + if (ultim.seq === s && e && e.hash === ultim.hash) { + const r = await cere(`${U}/v1/agents/witness/${ultim.id}`); + if (!r.status) { console.error(`NOT MEASURED: ${r.eroare}`); return 2; } + // starea capului trebuie sa fie chiar starea lui (200, JSON): un 503, un 429 sau o pagina HTML nu e "nimic nou", si nu se scrie in --out + // drept starea capului (revizia B3 a treia, R3-05) + if (!(r.status === 200 && obiect(r.j) && r.j.id === ultim.id && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-nimic-nou*/) { console.error(`NOT MEASURED: the witness lists seq ${s} as its last head of this ledger, but did not give its state (HTTP ${r.status}${obiect(r.j) && sir(r.j.error) ? ': ' + r.j.error : ''}${r.status === 200 && obiect(r.j) && r.j.id === ultim.id ? '; no status' : ''}); ask again later`); return 2; } + scrie(r.j, get('--out')); console.error(`nothing new: the witness already holds seq ${s} of this ledger (${r.j.status}) ${ultim.id}`); return 0; + } + if (!e || e.hash !== ultim.hash) { console.error(`REFUSED before sending: the witness holds seq ${ultim.seq} with a hash ${e ? 'this ledger does not have there' : 'beyond the end of this ledger'}: this ledger is another branch, or it lost entries`); return 1; } + if (ultim.seq > s) { console.error(`REFUSED before sending: the witness already holds a newer head of this ledger (seq ${ultim.seq}) than the one asked (seq ${s}): a head older than the last one witnessed is refused`); return 1; } + } + let env; try { env = cerereMartor(ledger, id, { seq: s, previous: ultim ? { seq: ultim.seq, hash: ultim.hash } : null }); } + catch (e) { console.error(`REFUSED before sending: ${e.message}`); return 1; } + const r = await cere(`${U}/v1/agents/witness`, { method: 'POST', body: JSON.stringify(env) }); + if (!r.status) { console.error(`NOT MEASURED: ${r.eroare}`); return 2; } + // un 200 sau 202 e un succes numai daca e chiar starea capului trimis (JSON, cu un id, sesiunea si pozitia lui; un 200 "deja vazut" poate purta + // id-ul capului tinut la aceeasi pozitie, cu alt moment declarat): o pagina HTML a unei margini sau a unui proxy nu dovedeste ca martorul a + // primit capul, deci NEMASURAT (2), si --out nu se scrie (revizia B3 a patra, R4-04) + if ((r.status === 200 || r.status === 202) && !(obiect(r.j) && H32.test(sir(r.j.id)) && r.j.session === env.statement.session && r.j.seq === env.statement.seq && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-trimis*/) { console.error(`NOT MEASURED: HTTP ${r.status}, but the answer is not the state of the head sent (${obiect(r.j) ? 'another id, session or position, or no status' : 'not JSON'}): ask the witness for ${env.statementHash} later`); return 2; } + scrie(r.j, get('--out')); console.error(`HTTP ${r.status}: ${obiect(r.j) ? sir(r.j.status) || sir(r.j.error) : 'not JSON'}${obiect(r.j) && sir(r.j.id) ? ' ' + r.j.id : ''}`); + return r.status === 200 || r.status === 202 ? 0 : r.status >= 500 || r.status === 429 ? 2 : 1; + } + if (cmd === 'receipt') { + const idc = String(get('--id') || ''); if (!H32.test(idc)) throw new Folosire('--id 0x<64 hex> (the statementHash of the head) is required'); + // ce scrie receipt e ce va preda agentul in --anchors, iar o chitanta predata care nu tine pe registru sau cu semnatura martorului invalida e VINA + // registrului (agentul raspunde de ce preda). Deci raspunsul se scrie NUMAI daca e chitanta capului cerut (--id), a ACESTUI registru (--ledger: + // acelasi agent, politica, seq si hash, si predecesorul), cu semnatura martorului verificata (cu cel fixat prin --witness, daca e dat; altfel cu + // cheia pe care o numeste chitanta, si mesajul spune ca martorul nu e fixat); altfel 2 cu motivul numit, si nimic scris (revizia B3 a sasea, N2: + // pana atunci un proxy fara cheie la --url dadea agentului cinstit chitanta adevarata a capului altui agent, sau una cu semnatura rupta, si + // fisierul predat iesea 1 din vina adresei) + const ledger = citeste(get('--ledger'), 'ledger'); if (!obiect(ledger)) throw new Folosire('--ledger must be the ledger of the agent (a JSON object)'); + const martori = toate('--witness'); if (martori.some((x) => !eWitnessId(x))) throw new Folosire('--witness aere-witness:<40 hex>[,...]'); + const r = await cere(`${baza()}/v1/agents/witness/${idc}`); + if (!r.status) { console.error(`NOT MEASURED: ${r.eroare}`); return 2; } + // un martor sau o usa care nu poate raspunde acum (503, 429, orice 5xx) e NEMASURAT, ca la submit, nu un refuz (revizia B3 a doua, R2-11) + if (r.status >= 500 || r.status === 429 /*PAZA-MARTOR:cod-indisponibil*/) { console.error(`NOT MEASURED: HTTP ${r.status}: ${obiect(r.j) ? sir(r.j.error) : 'not JSON'} (the witness cannot answer now; ask again later)`); return 2; } + if (r.status !== 200) { console.error(`HTTP ${r.status}: ${obiect(r.j) ? sir(r.j.error) : 'not JSON'}`); return 1; } + // un 200 care nu e JSON (o margine sau un proxy cu o pagina HTML) e NEMASURAT, nu o exceptie iesita cu 1 (revizia B3 a treia, R3-05) + if (!(obiect(r.j) && typeof r.j.status === 'string') /*PAZA-MARTOR:cod-ne-json*/) { console.error('NOT MEASURED: HTTP 200, but the answer is not the state of a head (not JSON, or no status): ask again later'); return 2; } + const plic = esteChitanta(r.j) ? chitantaDin(r.j) : null; + const capPrimit = plic && obiect(plic.witnessReceipt) && obiect(plic.witnessReceipt.head) ? plic.witnessReceipt.head.statementHash : null; + const l = plic && capPrimit === idc /*PAZA-MARTOR:receipt-id*/ ? verificaChitanta(plic, { ledger /*PAZA-MARTOR:receipt-registru*/, martori: martori.length ? martori : null /*PAZA-MARTOR:receipt-fixat*/ }) : null; + if (!(!!l && l.ok) /*PAZA-MARTOR:receipt-verificata*/) { + const motiv = !plic ? `the witness has it ${r.j.status.slice(0, 40)} and the answer carries no receipt${r.j.status === 'notarized' ? '' : ' yet (ask again after its next checkpoint)'}` + : capPrimit !== idc ? `the answer is the receipt of another head, not of ${idc}` : `the receipt does not hold here: [${l.rule}] ${l.error}`; + console.error(`NOT MEASURED: ${motiv}; nothing written${get('--out') ? ' to ' + get('--out') : ''}`); return 2; + } + scrie(r.j, get('--out')); + console.error(`status ${r.j.status}${r.j.status === 'notarized' ? '' : ' (not notarized yet: the chain cannot vouch for it until it is)'}; the receipt of seq ${l.head.seq} of this ledger, signed by ${l.checkpoint.witness}${l.witnessPinned ? ' (pinned)' : ' (NOT pinned: its signature was checked with the key the receipt names; pass --witness to pin the witness)'}`); + return r.j.status === 'notarized' ? 0 : 2; + } + if (cmd === 'verify-receipt') { + const martori = toate('--witness'); if (martori.some((x) => !eWitnessId(x))) throw new Folosire('--witness aere-witness:<40 hex>[,...]'); + const x = citeste(get('--receipt'), 'receipt'); + // raspunsul GET al unui cap inca `pending` (niciun cap de jurnal nu il acopera) nu e o chitanta GRESITA, e una care nu exista inca: NEMASURAT + if (obiect(x) && !esteChitanta(x) && x.status === 'pending' /*PAZA-MARTOR:cod-pending*/) { console.log(JSON.stringify({ ok: null, status: 'pending', id: x.id || null, note: 'not a receipt yet: no checkpoint covers this head; ask the witness again after its next checkpoint' }, null, 1)); return 2; } + const v = verificaChitanta(chitantaDin(x), { ledger: get('--ledger') ? citeste(get('--ledger'), 'ledger') : null, martori: martori.length ? martori : null }); + console.log(JSON.stringify(v.ok ? { ok: true, seq: v.head.seq, checkpoint: v.checkpoint, witnessPinned: v.witnessPinned, receivedAt: v.receivedAt, note: 'local checks only: the time the chain certifies is judged by agent-cli.mjs verify --anchors' + (v.witnessPinned ? '' : '; the witness was not pinned (--witness)') } : { ok: false, rule: v.rule, error: v.error }, null, 1)); + return v.ok ? 0 : 1; + } + if (cmd === 'verify-equivocation') { const v = verificaEchivocare(citeste(get('--proof'), 'proof')); console.log(JSON.stringify(v)); return v.ok ? 0 : 1; } + throw new Folosire('usage: martor-agenti.mjs request|submit|receipt|verify-receipt|verify-equivocation ... (see README.md)'); +} +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main(process.argv.slice(2)).then((c) => { process.exitCode = c; }, (e) => { console.error(`agent-witness: ${e.message}`); process.exitCode = e instanceof Folosire ? 2 : 1; }); +} diff --git a/agents/proba-martor-agenti.mjs b/agents/proba-martor-agenti.mjs new file mode 100644 index 0000000..3489fc0 --- /dev/null +++ b/agents/proba-martor-agenti.mjs @@ -0,0 +1,1756 @@ +// Proba martorului gazduit al agentilor (martor-agenti.mjs, martor-agenti-serviciu.mjs; B3, 2026-10-19). Fluxul intreg, cu chei nascute la +// rulare (agenti, martor, oameni) si un NOTAR-MOCK local (numit asa in fiecare rand): doi agenti, capete succesive, un cap care sare, un cap +// mai vechi si unul care nu continua ultimul (409), echivocarea prinsa, pastrata si intoarsa, refuzurile numite ale cererii (continutul +// registrului, semnatura, cheia, identitatea, sesiunea, momentul), limitele de ritm, capetele de jurnal semnate de martor cu dovada de +// includere si de consistenta, repornirea pe aceeasi stare, chitanta verificata FARA martor (local) si ca ancora (cu raspunsul verificatorului +// AIP-23 in forma lui), antedatarea de dupa cap prinsa cu ancora si nu fara ea, linia de comanda, usa gateway-ului (daca e in arbore), si plicul +// martorului judecat de verificatorul AIP-23 de referinta (AERE_VERIFY_PROOF; fara el randurile acelea sunt sarite pe nume si iesirea e 2). +// Sectiunea 9 (2026-10-19): cate un rand pentru fiecare din cele 18 constatari ale reviziei adversariale B3 (numele incep cu "9.R"), fiecare +// scris asa incat pe forma de dinainte de reparatie sa iasa RAU (nu sa opreasca proba): martorul fixat, lista intreaga a capetelor, cioburile +// si bugetul jurnalului, echivocarea pe drumul "deja vazut", argumentele, iesirea lui submit, vocabularul felului, plafonul dovezilor, paginarea, +// cota consumata prin retrimitere, limitatoarele, scrierile durabile si erorile publice. Un rand "9.P" e controlul unui paznic NOU pus de +// reparatia lui R (forma veche putea sa treaca de el din alt motiv); 9.R6 se masoara in controlul negativ, 9.R14 si 9.R15 in +// cloud-gateway/proba-desfasoara-martor-agenti.sh. +// Sectiunea 10 (2026-10-19, a doua revizie adversariala, 15 constatari confirmate): randurile "10.R2-", la fel scrise sa iasa RAU pe forma +// de dinainte (cf56bcda6): lista de capete semnata de martor (--witness-url fata de un proxy ostil, un impostor si o pagina veche reluata), dovezile +// de echivocare legate de ciobul capului contrazis, limitatorul la plafonul de chei, o lista cu elemente stricate, ancorele simple sub un martor +// fixat, arborele incremental si memoria unei stari cu un cap de jurnal pe frunza, rotirea care asteapta notarizarea, ciobul nascut intreg, ruta +// ciobului inghetat, lacatul cu pornirea masinii, codurile de iesire, bugetul citit inainte de cota agentului, Retry-After prin usa, si jumatatea +// "radacina" a paznicului de deschidere. Randurile "10.P2-" sunt controalele paznicilor noi; doua randuri 10.R2-15 si 10.P2-13 trec si pe +// forma veche (acolo comportamentul era bun, lipsea proba care sa-l poata inrosi). +// Sectiunea 11 (2026-10-19, a treia revizie adversariala, 5 constatari confirmate): randurile "11.R3-", RAU pe forma de dinainte (c34b34f1b): +// restul unei stergeri de ciob intrerupte (redenumit intai; un rest vechi, fara ciob.json sau frunze.ndjson, sters), dovada care traieste cel mai +// mult pe sesiune, steagurile date fara valoare, chitanta ultimului cap tinut de martor sub --witness-url, codurile lui receipt si submit. +// Sectiunea 12 (2026-10-19, a patra revizie adversariala, 6 constatari confirmate): randurile "12.R4-", RAU pe forma de dinainte (c5183bcb8), +// pe regula de proiectare a lui --witness-url (marturia martorului): notarizarea intarziata, un proxy care strica GET-ul unui cap, fiecare cap tinut +// cu chitanta lui, submit pe drumul principal, steagurile --flag=valoare si cele necunoscute, departajarea a doua dovezi si marcajul .de-sters. +// Sectiunea 13 (2026-10-19, a cincea revizie adversariala, 6 constatari confirmate): randurile "13.R5-", RAU pe forma de dinainte (b55038c2a): +// verify ca EVALUARE COMPLETA cu un singur verdict (un NEMASURAT nu mai ascunde o vina dovedita; o lista a altui martor, o ancora simpla, o pana a +// notarului sau a RPC-ului nu mai fac un FAILED), tipurile judecate inainte de folosire (nicio exceptie), si asteptarea pe 429 cu Retry-After. +// Sectiunea 14 (2026-10-19, verificarea tintita a reparatiilor R5, constatarile N1..N3): randurile "14.N", RAU pe forma de dinainte (786aa0038): +// o chitanta fara --rpc e o dovada NEMASURATA [rpc] judecata la locul ei (nu o iesire care ascunde ramura aratata de lista semnata), `receipt` scrie +// numai chitanta capului cerut, a registrului dat, cu semnatura martorului verificata, si regula (c) pe drumul "nimic nou" al lui submit. +// Fiecare afirmatie cu perechea ei negativa. +// node proba-martor-agenti.mjs offline (notar-mock): iesire 0 toate cum trebuia, 1 una rosie, 2 o parte nemasurata +// node proba-martor-agenti.mjs --testnet si notarizarea REALA pe testnetul public 28001 (cheie de proba: AERE_TESTNET_KEY_FILE, sau una +// nascuta si alimentata din robinet), plicul VALID cu finalitate post-cuantica, verify --anchors +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import net from 'node:net'; +import http from 'node:http'; +import crypto from 'node:crypto'; +import { spawn, spawnSync } from 'node:child_process'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { definePolicy } from './agent-policy.mjs'; +import { newAgentIdentity, openLedger, resumeLedger, verifyLedger, agentIdFromKey, sessionId } from './agent-ledger.mjs'; +import { NOTARI } from './agent-ancora.mjs'; +import * as M from './martor-agenti.mjs'; +import * as S from './martor-agenti-serviciu.mjs'; +import * as PK from '../proof-kinds/proof-kinds.mjs'; + +const { porneste, tur, notarizatorReal } = S; +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const TESTNET = process.argv.includes('--testnet'); +let ok = 0, rau = 0, sarit = 0; +const cer = (c, ce, d = '') => { console.log(` ${c ? 'OK ' : 'RAU'} ${ce}${c || !d ? '' : ' -- ' + String(d).slice(0, 300)}`); c ? ok++ : rau++; }; +const sari = (ce, dece) => { console.log(` -- ${ce} (SARIT, NEMASURAT: ${dece})`); sarit++; }; +// un rand care nu are voie sa opreasca proba: pe forma veche a componentei o functie lipsa sau o forma de raspuns alta da RAU, numit +const rand = async (ce, f) => { let r; try { r = await f(); } catch (e) { return cer(false, ce, 'EXCEPTIE: ' + (e && e.message)); } return cer(!!r[0], ce, r[1]); }; +const clon = (o) => JSON.parse(JSON.stringify(o)); +const sha = (s) => '0x' + crypto.createHash('sha256').update(Buffer.from(s, 'utf8')).digest('hex'); +const iso = (ms) => new Date(ms).toISOString(); +const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-martor-agenti-')); +const DEV_VERIFY = path.resolve(AICI, '..', '..', 'verificator-falcon', 'verify-proof.mjs'); +const VERIFY = process.env.AERE_VERIFY_PROOF !== undefined ? process.env.AERE_VERIFY_PROOF : (fs.existsSync(DEV_VERIFY) ? DEV_VERIFY : ''); +const resemneaza = (env, cheie) => { env.statementHash = sha(JSON.stringify(env.statement)); env.signature.signature = crypto.sign(null, Buffer.from(JSON.stringify(env.statement), 'utf8'), cheie).toString('base64'); return env; }; +const hx = () => crypto.randomBytes(32).toString('hex'); +// un registru "de forma" (numai seq si hash, cat cere martorul): pentru probele de volum, unde intrarile nu se judeca +const registruBrut = (id, n, policyHash = '0x' + hx()) => ({ agentId: id.agentId, session: sessionId(id.agentId, policyHash), policyHash, entries: Array.from({ length: n }, (_, seq) => ({ seq, hash: hx() })) }); +// fisierul frunzelor ciobului k (starea de dinainte de cioburi le tinea la radacina: proba se poate rula si pe forma veche a componentei) +const frunzeCiob = (dir, k = 0) => { const f = path.join(dir, 'cioburi', String(k), 'frunze.ndjson'); return fs.existsSync(f) || !fs.existsSync(path.join(dir, 'frunze.ndjson')) ? f : path.join(dir, 'frunze.ndjson'); }; +// `submit` si `verify --witness-url` vorbesc cu un martor din ACEST proces: un spawnSync ar bloca bucla care il serveste, deci copilul ruleaza asincron +const ruleazaAsincron = (args, env = {}) => new Promise((g) => { const c = spawn(process.execPath, args, { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...env } }); let stdout = '', stderr = ''; c.stdout.on('data', (x) => { stdout += x; }); c.stderr.on('data', (x) => { stderr += x; }); c.on('close', (status) => g({ status, stdout, stderr })); }); + +// ------------------------------------------------------------------------------------------------ actorii si registrele +const acum = Math.floor(Date.now() / 1000); +const BAZA = acum - 3000; // intrarile 0..2 ale lui A la BAZA..BAZA+10; capul seq 2 trimis la BAZA+1200 +const pol = (id) => definePolicy({ agentId: id.agentId, spend: { amount: '1000', windowSeconds: 3600 }, recipients: ['0xbbbb'] }); +const registru = (id, p, momente) => { let t = momente[0]; const L = openLedger({ identity: id, policy: p.policy, policyHash: p.policyHash, now: () => t }); for (const m of momente) { t = m; L.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); } return L; }; +const A = newAgentIdentity(), B = newAgentIdentity(), C = newAgentIdentity(), D = newAgentIdentity(), E = newAgentIdentity(); +const pA = pol(A), pB = pol(B), pC = pol(C), pD = pol(D), pE = pol(E); +const LA = registru(A, pA, [BAZA, BAZA + 5, BAZA + 10]); +const LA2 = registru(A, pA, [BAZA, BAZA + 6, BAZA + 11, BAZA + 12]); // alta RAMURA a aceluiasi agent, aceeasi sesiune +const LB = registru(B, pB, [BAZA + 20, BAZA + 25]), LC = registru(C, pC, [BAZA + 30, BAZA + 31]), LD = registru(D, pD, [BAZA + 40]), LE = registru(E, pE, [BAZA + 50]); +let wMs = (BAZA + 1) * 1000; // ceasul martorului (injectat) +const DIR = path.join(T, 'stare'); +const LIMITE = { postPeClient: { cereri: 40, ms: 600_000 }, postPeAgent: { cereri: 12, ms: 600_000 }, getPeClient: { cereri: 2000, ms: 600_000 } }; +let m = M.deschideMartor(DIR, { ceas: () => wMs }); +let srv = await porneste(m, { limite: LIMITE }); +const U = () => `http://127.0.0.1:${srv.port}`; +async function postLa(baza, corp, client = 'proba-principal', ct = 'application/json', antete = {}) { + const r = await fetch(baza + '/v1/agents/witness', { method: 'POST', headers: { 'content-type': ct, 'x-real-ip': client, ...antete }, body: typeof corp === 'string' ? corp : JSON.stringify(corp) }); + let j = null; try { j = await r.json(); } catch { j = null; } return { cod: r.status, j, antete: r.headers }; +} +const post = (corp, client, ct) => postLa(U(), corp, client, ct); +const get = async (cale, baza = U(), client = 'proba-citire') => { const r = await fetch(baza + cale, { headers: { 'x-real-ip': client } }); let j = null; try { j = await r.json(); } catch { j = null; } return { cod: r.status, j }; }; +const cerere = (L, id, o = {}) => M.cerereMartor(L.export(), id, { createdAt: iso(wMs), ...o }); +// raspunsul verificatorului AIP-23 in forma lui, pentru un plic dat (fara retea) +const verdict = (h, { fin = 'PASSED', t, sig = 'PASSED' } = {}) => ({ verdict: fin === 'PASSED' && sig === 'PASSED' ? 'VALID' : 'PARTIAL', statementHash: h, levels: [ + { level: 'form', state: 'PASSED', detail: 'kind=' + M.CHECKPOINT_KIND + '-attestation' }, { level: 'integrity', state: 'PASSED', detail: 'sha256(statement) == statementHash' }, + { level: 'signature', state: sig, detail: 'ml-dsa-65: verifies over the canonical statement text' }, + { level: 'finality', state: fin, detail: fin === 'PASSED' ? `post-quantum: the state of block 9, the parent of anchor 10 whose post-quantum certificate verified here, records this statementHash in AereNotary ${NOTARI[28001]} as first seen at ${iso(t * 1000)} (${t}); first seen in block 5` : 'notarized after the anchor; the next anchor will cover it' }] }); +const stub = (f) => async (_vp, fis) => f(JSON.parse(fs.readFileSync(fis, 'utf8'))); +const notarMock = (firstSeen) => async (env) => ({ ok: true, notarization: { chainId: 28001, notary: NOTARI[28001], firstSeen, txHash: '0x' + 'ab'.repeat(32), block: 123456, mock: 'local notary mock (offline probe)', statementHashSeen: env.statementHash } }); +// un verificator AIP-23 inlocuit, in forma raspunsului lui, pentru linia de comanda; momentul il da proba (STUB_F), nu plicul +const F5 = path.join(T, 'cli'); fs.mkdirSync(F5); +const fStub = path.join(F5, 'verify-stub.mjs'); +fs.writeFileSync(fStub, `import fs from 'node:fs'; const e = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); const t = Number(process.env.STUB_F); +console.log(JSON.stringify({ verdict: 'VALID', statementHash: e.statementHash, levels: [{ level: 'form', state: 'PASSED', detail: 'x' }, { level: 'integrity', state: 'PASSED', detail: 'x' }, { level: 'signature', state: 'PASSED', detail: 'x' }, + { level: 'finality', state: 'PASSED', detail: 'post-quantum: records this statementHash as first seen at ' + new Date(t * 1000).toISOString() + ' (' + t + ')' }] }));\n`); +const scrie = (n, o) => { const f = path.join(F5, n); fs.writeFileSync(f, JSON.stringify(o)); return f; }; +const CLI = path.join(AICI, 'agent-cli.mjs'); +// sectiunea 10: servere locale (proxy ostil, impostor) si paginile semnate de o cheie de martor data +const serverLocal = (f) => new Promise((g) => { const s = http.createServer((req, res) => { Promise.resolve(f(req, res, new URL(req.url, 'http://x'))).catch(() => { try { res.writeHead(500); res.end('{}'); } catch { /* deja raspuns */ } }); }); s.listen(0, '127.0.0.1', () => g({ url: `http://127.0.0.1:${s.address().port}`, close: () => new Promise((h) => s.close(() => h())) })); }); +const trimite = (res, cod, o, antete = {}) => { const b = Buffer.from(JSON.stringify(o)); res.writeHead(cod, { 'content-type': 'application/json', 'content-length': b.length, ...antete }); res.end(b); }; +// o pagina de capete semnata de o cheie de martor data (impostorul isi semneaza cinstit propriile pagini: cheia martorului fixat nu o are) +const KIND_PAGINA = M.PAGINA_KIND || 'aere-agent-witness-heads'; +const paginaSemnataDe = (k, st) => { const text = JSON.stringify(st); return { v: 1, kind: KIND_PAGINA + '-attestation', statement: st, statementHash: sha(text), signature: { scheme: 'ml-dsa-65', publicKey: k.publicKeyPem, signature: crypto.sign(null, Buffer.from(text, 'utf8'), k.privateKey).toString('base64') } }; }; +const paginaGoala = (k, agentId, session, from, heads = []) => ({ v: 1, kind: KIND_PAGINA, witness: k.witnessId, log: 'impostor', agentId, session, from, limit: 100, count: heads.length, latest: null, heads, next: null, equivocations: [], at: iso(Date.now()) }); + + +try { + // ---------------------------------------------------------------------------------------------- 1. cererea si judecata ei + const c0 = cerere(LA, A, { seq: 0 }); + const v0 = M.verificaCapSemnat(c0, { acumS: wMs / 1000 }); + cer(v0.ok && c0.kind === 'aere-agent-ledger-head-attestation' && c0.statement.hash === LA.export().entries[0].hash && c0.statement.previous === null && c0.signature.scheme === 'ml-dsa-65', + '1. cererea: capul registrului (seq, hash, sesiune, politica) semnat ML-DSA-65 de agent, ca plic AIP-23; martorul il primeste', JSON.stringify(v0).slice(0, 200)); + const cu = (f) => { const x = clon(c0); f(x); return M.verificaCapSemnat(x, { acumS: wMs / 1000 }); }; + const rS = cu((x) => { x.statement.createdAt = iso(wMs + 1000); x.statementHash = sha(JSON.stringify(x.statement)); }); + cer(!rS.ok && rS.rule === 'semnatura', '1. CONTROL: declaratia schimbata si hash-ul refacut, semnatura veche -> refuz "semnatura"', JSON.stringify(rS)); + const rI = cu((x) => { x.statement.seq = 1; }); + cer(!rI.ok && rI.rule === 'integritate', '1. CONTROL: declaratia schimbata fara hash refacut -> refuz "integritate"', JSON.stringify(rI)); + const ed = crypto.generateKeyPairSync('ed25519'); + const rK = cu((x) => { const aid = agentIdFromKey(ed.publicKey); x.statement.agentId = aid; x.statement.session = sessionId(aid, x.statement.policyHash); x.signature.publicKey = ed.publicKey.export({ type: 'spki', format: 'pem' }); resemneaza(x, ed.privateKey); }); + cer(!rK.ok && rK.rule === 'cheie', '1. CONTROL: un cap semnat cu o cheie Ed25519 (id si sesiune derivate din ea) -> refuz "cheie"', JSON.stringify(rK)); + const rId = cu((x) => { x.statement.agentId = B.agentId; x.statement.session = sessionId(B.agentId, x.statement.policyHash); resemneaza(x, A.privateKey); }); + cer(!rId.ok && rId.rule === 'identitate', '1. CONTROL: cheia lui A semneaza un cap cu id-ul lui B -> refuz "identitate"', JSON.stringify(rId)); + const rSe = cu((x) => { x.statement.session = 'ab'.repeat(16); resemneaza(x, A.privateKey); }); + cer(!rSe.ok && rSe.rule === 'sesiune', '1. CONTROL: o sesiune care nu e cea derivata din agent si politica -> refuz "sesiune"', JSON.stringify(rSe)); + const rC1 = cu((x) => { x.entries = LA.export().entries; }); + const rC2 = cu((x) => { x.statement.action = { kind: 'payment', amount: '10' }; resemneaza(x, A.privateKey); }); + cer(!rC1.ok && rC1.rule === 'continut' && !rC2.ok && rC2.rule === 'continut', '1. CONTROL: intrarile registrului langa cap, sau o actiune in declaratie (semnata) -> refuz "continut": martorul nu primeste niciodata continutul registrului', JSON.stringify([rC1, rC2])); + const rT = M.verificaCapSemnat(cerere(LA, A, { seq: 0, createdAt: iso(wMs - 400_000) }), { acumS: wMs / 1000 }); + cer(!rT.ok && rT.rule === 'timp', '1. CONTROL: un cap declarat cu 400 s inaintea ceasului martorului -> refuz "timp"', JSON.stringify(rT)); + // forma: un predecesor care nu e inaintea capului, sau un moment fara "T" si "Z", semnate cinstit (judecata fara ceas: numai forma ii poate prinde) + const rF1 = (() => { const x = clon(c0); x.statement.seq = 1; x.statement.previous = { seq: 1, hash: x.statement.hash }; return M.verificaCapSemnat(resemneaza(x, A.privateKey)); })(); + const rF2 = (() => { const x = clon(c0); x.statement.createdAt = x.statement.createdAt.replace('T', ' ').replace('Z', ''); return M.verificaCapSemnat(resemneaza(x, A.privateKey)); })(); + cer(!rF1.ok && rF1.rule === 'forma' && !rF2.ok && rF2.rule === 'forma', '1. CONTROL: forma capului: un predecesor care nu e inaintea capului, sau un moment fara "T" si "Z" (semnate cinstit) -> refuz "forma"', JSON.stringify([rF1, rF2]).slice(0, 200)); + let arunca = null; try { M.cerereMartor(LA.export(), A, { seq: 2, previous: { seq: 0, hash: LA2.export().entries[0].hash } }); } catch (e) { arunca = e.message; } + cer(/another branch/.test(String(arunca)), '1. CONTROL: agentul care ar numi ca predecesor un cap pe care registrul lui nu il are e oprit inainte de trimitere (alta ramura)', arunca); + + // ---------------------------------------------------------------------------------------------- 2. primirea, consistenta, echivocarea + const r0 = await post(c0); + cer(r0.cod === 202 && r0.j.status === 'pending' && r0.j.id === c0.statementHash && r0.j.leafIndex === 0 && r0.j.sandbox === M.SANDBOX, + '2. POST: primul cap al lui A (previous null) -> 202 pending, pozitia 0 in jurnal, "sandbox: testnet 28001; mainnet: at activation"', JSON.stringify(r0.j).slice(0, 300)); + const pr = r0.j.promise; + cer(pr && pr.statement.kind === 'aere-proof-of-time' && pr.statement.subjectHash === c0.statementHash && pr.statement.source === m.witnessId && Date.parse(pr.statement.at) === wMs && pr.signature.scheme === 'ml-dsa-65', + '2. promisiunea de la primire: plic AIP-23 `time` (capul a existat la momentul primirii, dupa martor), semnat ML-DSA-65 de martor', JSON.stringify(pr).slice(0, 200)); + const r0b = await post(c0); + cer(r0b.cod === 200 && r0b.j.id === c0.statementHash && r0b.j.status === 'pending', '2. acelasi cap trimis din nou -> 200, aceeasi intrare (fara o a doua frunza)', JSON.stringify(r0b.j).slice(0, 200)); + wMs = (BAZA + 1200) * 1000; // capul seq 2 se trimite la 20 de minute dupa ultima intrare (asta e fereastra pe care o inchide ancora) + const c2 = cerere(LA, A, { seq: 2, previous: 0 }); + const r2 = await post(c2); + cer(r2.cod === 202 && r2.j.leafIndex === 1, '2. un cap care SARE (seq 0 -> seq 2), numind ca predecesor ultimul cap martorizat -> 202', JSON.stringify(r2.j).slice(0, 200)); + const rb0 = await post(cerere(LB, B, { seq: 0 })), rb1 = await post(cerere(LB, B, { seq: 1, previous: 0 })); + cer(rb0.cod === 202 && rb1.cod === 202 && rb1.j.leafIndex === 3, '2. al doilea agent (B): capete succesive 0 si 1 -> 202, 202 (jurnalul e comun, capetele sunt pe agent si politica)', JSON.stringify([rb0.j, rb1.j]).slice(0, 200)); + const rV = await post(cerere(LA, A, { seq: 1, previous: 0 })); + cer(rV.cod === 409 && rV.j.error === 'stale_head' && rV.j.latest.seq === 2, '2. CONTROL: un cap mai VECHI decat ultimul martorizat (seq 1 dupa seq 2) -> 409 stale_head, cu ultimul cap', JSON.stringify(rV.j).slice(0, 200)); + LA.record({ kind: 'payment', to: '0xbbbb', amount: '10' }, { at: undefined }); // seq 3 al lui A, la ceasul registrului (BAZA + 10) + const rN = await post(cerere(LA, A, { seq: 3, previous: 0 })); + cer(rN.cod === 409 && rN.j.error === 'not_successor' && rN.j.latest.seq === 2, '2. CONTROL: un cap care nu numeste ca predecesor ultimul cap martorizat -> 409 not_successor, cu ultimul cap (ca 409-ul din C2SP tlog-witness)', JSON.stringify(rN.j).slice(0, 200)); + const rPrim = await post(cerere(LC, C, { seq: 1, previous: 0 })); + cer(rPrim.cod === 409 && rPrim.j.error === 'not_successor' && rPrim.j.latest === null, '2. CONTROL: primul cap al unui agent care numeste un predecesor nemartorizat -> 409 not_successor (latest null)', JSON.stringify(rPrim.j).slice(0, 200)); + const rE1 = await post(cerere(LA2, A, { seq: 2, previous: 0 })); + const vE1 = rE1.j && rE1.j.proof ? M.verificaEchivocare(rE1.j.proof) : { ok: false }; + cer(rE1.cod === 409 && rE1.j.error === 'equivocation' && vE1.ok && vE1.seq === 2, '2. ECHIVOCARE: alta ramura a lui A trimite seq 2 cu alt hash -> 409, cu dovada (doua capete semnate de agent, aceeasi pozitie), verificabila de oricine', JSON.stringify(rE1.j).slice(0, 300)); + const rE2 = await post(cerere(LA2, A, { seq: 3, previous: 2 })); + const vE2 = rE2.j && rE2.j.proof ? M.verificaEchivocare(rE2.j.proof) : { ok: false }; + cer(rE2.cod === 409 && rE2.j.error === 'equivocation' && vE2.ok && vE2.seq === 2, '2. ECHIVOCARE prin predecesor: seq 3 care numeste ca previous un hash al pozitiei 2 pe care agentul a semnat-o altfel -> 409, cu dovada', JSON.stringify(rE2.j).slice(0, 300)); + await rand('2. GET heads?session: capetele lui A (0, 2) in ordine; o singura dovada PASTRATA pentru agent si politica (a doua e intoarsa, nepastrata), ca id, iar dovada intreaga pe ruta ei', async () => { + const hA = await get(`/v1/agents/witness/${A.agentId}/heads?session=${LA.export().session}`); + const eq = hA.j && Array.isArray(hA.j.equivocations) ? hA.j.equivocations : []; + const dv = eq.length ? await get('/v1/agents/witness/equivocation/' + eq[0].id) : { cod: 0 }; + return [hA.cod === 200 && Array.isArray(hA.j.heads) && hA.j.heads.map((x) => x.seq).join(',') === '0,2' && eq.length === 1 && eq[0].a === undefined && rE1.j.kept === true && rE2.j.kept === false + && dv.cod === 200 && M.verificaEchivocare(dv.j.proof).ok, JSON.stringify([hA.j, dv.cod]).slice(0, 300)]; + }); + const falsa = clon(rE1.j.proof); falsa.b.statement.hash = falsa.a.statement.hash; + const edDov = clon(rE1.j.proof); edDov.a.signature.publicKey = ed.publicKey.export({ type: 'spki', format: 'pem' }); + cer(!M.verificaEchivocare(falsa).ok && !M.verificaEchivocare(edDov).ok && M.verificaEchivocare(edDov).rule === 'cheie', '2. CONTROL: o dovada cu un cap atins, sau cu o cheie Ed25519 in locul cheii agentului -> refuzata', JSON.stringify([M.verificaEchivocare(falsa), M.verificaEchivocare(edDov)])); + const rCt = await post({ ...cerere(LE, E, { seq: 0 }), ledger: LE.export() }, 'proba-e'); + const rMare = await post('{"x":"' + 'a'.repeat(20000) + '"}', 'proba-e'); + const rTip = await post(cerere(LE, E, { seq: 0 }), 'proba-e', 'text/plain'); + const sigE = cerere(LE, E, { seq: 0 }); sigE.statement.createdAt = iso(wMs + 2000); sigE.statementHash = sha(JSON.stringify(sigE.statement)); + const rSig = await post(sigE, 'proba-e'); + cer(rCt.cod === 400 && rCt.j.rule === 'continut' && rMare.cod === 413 && rTip.cod === 415 && rSig.cod === 403 && rSig.j.rule === 'semnatura', + '2. CONTROL prin HTTP: registrul in cerere -> 400 "continut"; un corp de 20 KB -> 413; alt tip de continut -> 415; semnatura care nu verifica -> 403 "semnatura"', JSON.stringify([rCt.cod, rCt.j && rCt.j.rule, rMare.cod, rTip.cod, rSig.cod, rSig.j && rSig.j.rule])); + const rD0 = await post(cerere(LD, D, { seq: 0 }), 'proba-d'); // capul lui D in jurnalul probei (pozitia 4) + // limita pe agent: pe un martor separat (jurnalul probei ramane cel numarat mai jos), capete NOI succesive ale aceluiasi agent + const mQ = M.deschideMartor(fs.mkdtempSync(path.join(T, 'q-')), { ceas: () => wMs }); const srvQ = await porneste(mQ, { limite: LIMITE }); + const UQ = `http://127.0.0.1:${srvQ.port}`; + const Q_ = newAgentIdentity(); const LQ = registruBrut(Q_, LIMITE.postPeAgent.cereri + 1); const coduriQ = []; + for (let s = 0; s <= LIMITE.postPeAgent.cereri; s++) coduriQ.push((await postLa(UQ, M.cerereMartor(LQ, Q_, { seq: s, previous: s ? s - 1 : null, createdAt: iso(wMs) }), 'proba-q')).cod); + cer(rD0.cod === 202 && coduriQ.slice(0, -1).every((c) => c === 202) && coduriQ[coduriQ.length - 1] === 429, `2. CONTROL ritm pe agent: ${LIMITE.postPeAgent.cereri} capete NOI ale aceluiasi agent trec, al ${LIMITE.postPeAgent.cereri + 1}-lea -> 429`, coduriQ.join(',')); + const F_ = newAgentIdentity(); const LF = registru(F_, pol(F_), [BAZA + 60]); const cF = cerere(LF, F_, { seq: 0 }); + const falsF = clon(cF); falsF.signature.signature = Buffer.from('x'.repeat(64)).toString('base64'); + const coduriFals = []; for (let i = 0; i < LIMITE.postPeAgent.cereri + 2; i++) coduriFals.push((await postLa(UQ, falsF, 'proba-strain-' + i)).cod); + const rF = { cod: (await postLa(UQ, cF, 'proba-f')).cod }; await srvQ.close(); mQ.inchide(); + // un agent care echivoca in bucla: fiecare refuz intoarce dovada lui, dar martorul pastreaza UNA pe agent si politica ("o dovada ajunge") + const G_ = newAgentIdentity(); const pG = pol(G_); const mG = M.deschideMartor(fs.mkdtempSync(path.join(T, 'g-')), { ceas: () => wMs }); + const g0 = mG.primeste(cerere(registru(G_, pG, [BAZA + 70]), G_, { seq: 0 })); const echG = []; + for (let i = 0; i < 10; i++) echG.push(mG.primeste(cerere(registru(G_, pG, [BAZA + 70]), G_, { seq: 0 }))); + await rand('2. CONTROL: zece echivocari ale aceluiasi agent -> zece 409 cu dovada fiecare, dar martorul pastreaza UNA (o dovada ajunge; discul nu se umple)', async () => { + const s = mG.capeteAgent(G_.agentId).sessions[0]; const pastrate = typeof s.equivocations === 'number' ? s.equivocations : s.equivocations.length; mG.inchide(); + return [g0.cod === 202 && echG.every((r) => r.cod === 409 && r.corp.error === 'equivocation' && M.verificaEchivocare(r.corp.proof).ok) && pastrate === 1 && echG.filter((r) => r.corp.kept).length === 1, JSON.stringify([g0.cod, echG.map((r) => r.cod).join(','), pastrate])]; + }); + cer(coduriFals.every((c) => c === 403) && rF.cod === 202, `2. CONTROL: ${LIMITE.postPeAgent.cereri + 2} cereri cu semnatura rea care numesc agentul F (de la straini) -> 403, si NU consuma cota lui F: capul lui adevarat -> 202`, coduriFals.join(',') + ' / ' + rF.cod); + const coduriF = []; for (let i = 0; i < LIMITE.postPeClient.cereri + 1; i++) coduriF.push((await post('{}', 'proba-inundatie')).cod); + cer(coduriF.slice(0, -1).every((c) => c === 400) && coduriF[coduriF.length - 1] === 429, `2. CONTROL ritm pe client: ${LIMITE.postPeClient.cereri} cereri ale aceluiasi client sunt judecate, a ${LIMITE.postPeClient.cereri + 1}-a -> 429 inainte de corp`, coduriF.slice(-3).join(',')); + + // ---------------------------------------------------------------------------------------------- 3. capul de jurnal, chitanta, consistenta + const g2a = await get('/v1/agents/witness/' + c2.statementHash); + cer(g2a.cod === 200 && g2a.j.status === 'pending' && !g2a.j.receipt, '3. inainte de un cap de jurnal: GET -> pending, fara chitanta', JSON.stringify(g2a.j).slice(0, 200)); + const t1 = await tur(m, { notarizeaza: null }); + const g2b = await get('/v1/agents/witness/' + c2.statementHash); + const plicNenot = g2b.j && g2b.j.receipt; + cer(t1.built === 0 && g2b.j.status === 'checkpointed' && plicNenot && plicNenot.statement.kind === M.CHECKPOINT_KIND && plicNenot.statement.treeSize === 5 && !plicNenot.notarization && plicNenot.witnessReceipt.leafIndex === 1, + '3. capul de jurnal 0 (5 capete) semnat de martor; chitanta lui A seq 2: pozitia 1, dovada de includere, inca nenotarizata (checkpointed)', JSON.stringify(g2b.j).slice(0, 300)); + const lc = M.verificaChitanta(plicNenot, { ledger: LA.export() }); + cer(lc.ok && lc.head.seq === 2 && lc.checkpoint.treeSize === 5, '3. chitanta verificata FARA martor: semnatura martorului, capul semnat de agent, frunza refacuta, includerea RFC 9162 pana la radacina, capul in registrul lui A', JSON.stringify(lc).slice(0, 200)); + const chit = (f) => { const x = clon(plicNenot); f(x); return M.verificaChitanta(x, { ledger: LA.export() }); }; + const kc = [['includere', chit((x) => { x.witnessReceipt.path[0] = '0x' + '00'.repeat(32); }), 'un element al drumului schimbat'], + ['includere', chit((x) => { x.witnessReceipt.leafIndex = 2; }), 'alta pozitie'], + ['frunza', chit((x) => { x.witnessReceipt.receivedAtMs += 1000; }), 'alt moment de primire'], + ['semnatura', chit((x) => { x.witnessReceipt.head.statement.createdAt = iso(wMs + 3000); x.witnessReceipt.head.statementHash = sha(JSON.stringify(x.witnessReceipt.head.statement)); }), 'capul primit schimbat (hash refacut, semnatura agentului veche)'], + ['punct-semnatura', chit((x) => { x.statement.treeSize = 4; x.statementHash = sha(JSON.stringify(x.statement)); }), 'capul de jurnal schimbat (hash refacut, semnatura martorului veche)'], + ['punct-integritate', chit((x) => { x.statement.treeSize = 4; }), 'capul de jurnal schimbat fara hash refacut'], + ['martor', chit((x) => { const k = crypto.generateKeyPairSync('ml-dsa-65'); x.signature.publicKey = k.publicKey.export({ type: 'spki', format: 'pem' }); x.signature.signature = crypto.sign(null, Buffer.from(JSON.stringify(x.statement)), k.privateKey).toString('base64'); }), 'capul de jurnal semnat de alta cheie decat a martorului numit'], + ['promisiune', chit((x) => { x.witnessReceipt.promise.statement.at = iso(wMs + 5000); }), 'promisiunea cu alt moment'], + ['registru', M.verificaChitanta(plicNenot, { ledger: LA2.export() }), 'judecata pe alta ramura a agentului'], + ['chitanta-forma', chit((x) => { delete x.witnessReceipt.leafHash; }), 'chitanta fara leafHash'], + ['punct-forma', chit((x) => { x.statement.treeSize = 0; x.statementHash = sha(JSON.stringify(x.statement)); }), 'capul de jurnal cu marimea 0 (hash refacut)']]; + for (const [regula, r, ce] of kc) cer(!r.ok && r.rule === regula, `3. CONTROL chitanta: ${ce} -> refuz "${regula}"`, JSON.stringify(r).slice(0, 200)); + // capul primit, cu sesiunea sau integritatea lui atinse, nu trece de chitanta: aceleasi garzi ca la primire + const rcSes = chit((x) => { x.witnessReceipt.head.statement.session = 'cd'.repeat(16); x.witnessReceipt.head.statementHash = sha(JSON.stringify(x.witnessReceipt.head.statement)); }); + const rcInt = chit((x) => { x.witnessReceipt.head.statement.seq = 7; }); + cer(!rcSes.ok && rcSes.rule === 'sesiune' && !rcInt.ok && rcInt.rule === 'integritate', '3. CONTROL chitanta: capul primit cu alta sesiune -> "sesiune"; capul primit atins fara hash refacut -> "integritate"', JSON.stringify([rcSes, rcInt]).slice(0, 200)); + // martorul nu e crezut nici pe propriul moment de primire: un jurnal facut de mana cu o frunza primita la 400 s dupa momentul declarat de cap + const km = M.cheiaMartorului(fs.mkdtempSync(path.join(T, 'km-'))); const cR = cerere(LE, E, { seq: 0 }); + const rcvR = Date.parse(cR.statement.createdAt) + 400_000; const fr = M.frunzaMartor(cR.statementHash, rcvR); + const pR = M.capDeJurnal(km, { treeSize: 1, root: fr, log: 'proba', chainId: 28001, maxMergeDelay: 1800 }); + const rPrimire = M.verificaChitanta({ ...pR, witnessReceipt: { format: M.RECEIPT_FORMAT, head: cR, receivedAtMs: rcvR, leafIndex: 0, treeSize: 1, leafHash: fr, path: [] } }); + cer(!rPrimire.ok && rPrimire.rule === 'primire', '3. CONTROL: o chitanta a unui martor care a scris in frunza o primire la 400 s de momentul capului -> refuz "primire" (regula martorului, verificata de verificator)', JSON.stringify(rPrimire)); + // notarizatorul real refuza orice lant decat 28001 INAINTE de cheie (un ethers fals care raspunde 2800 si nu poate face nicio cheie) + const ethFals = (lant) => ({ JsonRpcProvider: class { async getNetwork() { return { chainId: BigInt(lant) }; } }, parseEther: () => 0n, + Wallet: class { static createRandom() { throw new Error('the test never gets this far'); } constructor() { throw new Error('the test never gets this far'); } } }); + const kfx = path.join(T, 'nu-trebuie-creata.key'); + const n2800 = await notarizatorReal({ rpc: 'x', cheieFisier: kfx, ethers: ethFals(2800) })(plicNenot); + cer(!n2800.ok && /REFUSED.*testnet 28001 only/.test(n2800.motiv) && !fs.existsSync(kfx), '3. CONTROL: notarizatorul pe un RPC care serveste lantul 2800 -> REFUZ inainte de cheie (nicio cheie creata): sandbox 28001, mainnet la activare', JSON.stringify(n2800)); + // notarizarea: NOTAR-MOCK local (raspunde ca notarizeEnvelope, cu firstSeen la 60 s dupa primire) + const F = BAZA + 1260; + const mock = notarMock(F); + const t2 = await tur(m, { notarizeaza: mock }); + const g2c = await get('/v1/agents/witness/' + c2.statementHash); + const plic = g2c.j && g2c.j.receipt; + cer(t2.notarized === 0 && g2c.j.status === 'notarized' && plic.notarization && plic.notarization.firstSeen === F && plic.notarization.statementHashSeen === plic.statementHash, + '3. NOTAR-MOCK local: ultimul cap de jurnal notarizat (notarul a primit chiar statementHash-ul lui); GET -> notarized, chitanta cu notarizarea', JSON.stringify(g2c.j).slice(0, 300)); + wMs = (BAZA + 1300) * 1000; + LA.record({ kind: 'payment', to: '0xbbbb', amount: '1' }, { at: undefined }); + const t0LA = LA.export().entries.length; + const c4 = cerere(LA, A, { seq: t0LA - 1, previous: 2 }); + const r4 = await post(c4); + const t3 = await tur(m, { notarizeaza: mock }); + const cp = await get('/v1/agents/witness/checkpoint?from=5'); + const p0 = plic.statement, p1 = cp.j && cp.j.latest && cp.j.latest.statement; + const cons = cp.j && cp.j.consistency; + cer(r4.cod === 202 && t3.built === 1 && p1 && p1.treeSize === 6 && p1.previous === plic.statementHash && cons && M.verifyConsistency({ firstSize: 5, secondSize: 6, firstRoot: p0.root, secondRoot: p1.root, path: cons.path }), + '3. al doilea cap de jurnal (6 capete) numeste capul de dinainte si e CONSISTENT cu el (dovada RFC 9162, 2.1.4): jurnalul e numai adaugat', JSON.stringify(cp.j).slice(0, 300)); + const consRea = cons ? { firstSize: 5, secondSize: 6, firstRoot: '0x' + '11'.repeat(32), secondRoot: p1.root, path: cons.path } : null; + cer(!!consRea && !M.verifyConsistency(consRea), '3. CONTROL: consistenta fata de o radacina veche falsa -> nu verifica'); + // repornirea pe aceeasi stare: frunzele, capetele, echivocarile si capetele de jurnal raman; un cap nou continua + await srv.close(); m.inchide(); + const m2 = M.deschideMartor(DIR, { ceas: () => wMs }); + const s2 = m2.sanatate(); + LA.record({ kind: 'payment', to: '0xbbbb', amount: '1' }, { at: undefined }); + const rR = m2.primeste(cerere(LA, A, { seq: LA.export().entries.length - 1, previous: t0LA - 1 })); + cer(s2.leaves === 6 && s2.checkpoints === 2 && s2.equivocations === 1 && s2.witness === m.witnessId && rR.cod === 202 && rR.corp.leafIndex === 6, + '3. repornire: aceeasi cheie de martor, 6 frunze, 2 capete de jurnal, 1 echivocare pastrata; capul urmator al lui A continua jurnalul (pozitia 6)', JSON.stringify([s2, rR.cod]).slice(0, 300)); + let refuzConf = null; try { M.deschideMartor(DIR, { log: 'alt-jurnal', ceas: () => wMs }); } catch (e) { refuzConf = e.message; } + cer(/never changes its name/.test(String(refuzConf)), '3. CONTROL: aceeasi stare deschisa sub alt nume de jurnal -> refuz (un jurnal nu isi schimba numele, lantul, martorul sau intarzierea)', refuzConf); + m2.inchide(); + // un proces oprit in mijlocul scrierii unei frunze lasa un rand taiat la coada: la deschidere randul se taie (nu a primit niciodata raspuns) + await rand('3. CONTROL: un rand de frunza taiat la coada (proces oprit in mijlocul scrierii) -> taiat la deschidere, jurnalul ramane cel raspuns', async () => { + const fFr = frunzeCiob(DIR); const marime = fs.statSync(fFr).size; + fs.appendFileSync(fFr, '{"i":7,"h":"0x'); + const m3 = M.deschideMartor(DIR, { ceas: () => wMs }); const s3 = m3.sanatate(); m3.inchide(); + return [s3.leaves === 7 && fs.statSync(fFr).size === marime, JSON.stringify([s3.leaves, fs.statSync(fFr).size, marime])]; + }); + fs.writeFileSync(path.join(DIR, '.lacat'), String(process.ppid)); + let refuzLacat = null; try { M.deschideMartor(DIR, { ceas: () => wMs }); } catch (e) { refuzLacat = e.message; } + cer(/held by process/.test(String(refuzLacat)), '3. CONTROL: starea tinuta de alt proces viu (lacatul) -> refuz', refuzLacat); + fs.rmSync(path.join(DIR, '.lacat'), { force: true }); + m = M.deschideMartor(DIR, { ceas: () => wMs }); srv = await porneste(m, { limite: LIMITE }); + + // ---------------------------------------------------------------------------------------------- 4. chitanta ca ancora a registrului + // registrul lui A pana la capul seq 2, apoi o continuare CINSTITA (dupa primire) si una ANTEDATATA (in fereastra dintre intrarea 2 si cap) + const pana2 = { ...LA.export(), entries: LA.export().entries.slice(0, 3) }; + let tc = BAZA + 1210; const cinstit = resumeLedger({ identity: A, policy: pA.policy, policyHash: pA.policyHash, ledger: pana2, now: () => tc }); cinstit.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); + tc = BAZA + 100; const antedatat = resumeLedger({ identity: A, policy: pA.policy, policyHash: pA.policyHash, ledger: pana2, now: () => tc }); antedatat.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); + const ruleazaBun = stub((e) => verdict(e.statementHash, { t: F })); + const an = await M.ancoreDinChitante(cinstit.export(), [g2c.j], { verifyProof: 'stub', rpc: 'stub', ruleaza: ruleazaBun, martori: [m.witnessId] }); + cer(an.stare === 'OK' && an.anchors.length === 1 && an.anchors[0].seq === 2 && an.anchors[0].at === BAZA + 1200 && an.anchors[0].firstSeen === F, + '4. chitanta -> ancora {seq 2, hash, at = momentul semnat de agent}, cu firstSeen-ul certificat (raspunsul GET al martorului primit ca atare)', JSON.stringify(an).slice(0, 300)); + const vC = verifyLedger(cinstit.export(), { policy: pA.policy, anchors: an.anchors }); + cer(vC.ok && vC.time.lowerBound === 'witnessed', '4. registrul cinstit cu ancora martorului: ok, marginea de jos "witnessed"', JSON.stringify(vC).slice(0, 200)); + const vA0 = verifyLedger(antedatat.export(), { policy: pA.policy }); + cer(vA0.ok, '4. MARTORUL MASURII: fara ancora, intrarea antedatata (intre ultima intrare si capul trimis) trece', JSON.stringify(vA0).slice(0, 200)); + const vA = verifyLedger(antedatat.export(), { policy: pA.policy, anchors: an.anchors }); + cer(!vA.ok && /backdated/.test(vA.error || ''), '4. cu ancora martorului, intrarea antedatata e prinsa ("backdated"): ea contrazice capul semnat de agent', JSON.stringify(vA).slice(0, 200)); + const k4 = [ + ['FAILED', 'lant', stub(() => verdict('0x' + '22'.repeat(32), { t: F })), 'verificatorul a judecat alt statementHash'], + ['NEMASURAT', null, stub((e) => verdict(e.statementHash, { fin: 'PENDING', t: F })), 'finalitate in asteptare'], + ['FAILED', 'lant', stub((e) => verdict(e.statementHash, { fin: 'FAILED', t: F })), 'finalitate FAILED (radacina nu e pe lant)'], + ['NEMASURAT', null, async () => null, 'verificatorul nu raspunde'], + ['FAILED', 'lant', stub((e) => verdict(e.statementHash, { sig: 'FAILED', t: F })), 'semnatura martorului FAILED la verificatorul AIP-23'], + ['NEMASURAT', null, stub((e) => verdict(e.statementHash, { sig: 'UNMEASURED', t: F })), 'semnatura martorului UNMEASURED la verificatorul AIP-23 (nu PASSED)'], + ['FAILED', 'intarziere', stub((e) => verdict(e.statementHash, { t: BAZA + 1200 + 4000 })), 'capul de jurnal vazut de lant la 4000 s dupa primire (peste intarzierea maxima)'], + ['FAILED', 'primire', stub((e) => verdict(e.statementHash, { t: BAZA + 1200 - 400 })), 'lantul a vazut capul de jurnal INAINTE de primirea pe care o scrie martorul']]; + for (const [stare, regula, r, ce] of k4) { + await rand(`4. CONTROL: ${ce} -> ${stare}${regula ? ' "' + regula + '"' : ''}, nicio ancora`, async () => { + const x = await M.ancoreDinChitante(cinstit.export(), [g2c.j], { verifyProof: 'stub', rpc: 'stub', ruleaza: r }); + return [x.stare === stare && !x.anchors.length && (!regula || x.randuri[0].rule === regula), JSON.stringify(x).slice(0, 250)]; + }); + } + const xR = await M.ancoreDinChitante(LA2.export(), [g2c.j], { verifyProof: 'stub', rpc: 'stub', ruleaza: ruleazaBun }); + const xL = await M.ancoreDinChitante(cinstit.export(), [g2c.j], { verifyProof: 'stub', rpc: 'stub', ruleaza: ruleazaBun, chain: 2800 }); + const xN = await M.ancoreDinChitante(cinstit.export(), [g2b.j], { verifyProof: 'stub', rpc: 'stub', ruleaza: ruleazaBun }); + const g2cLant = clon(g2c.j); g2cLant.receipt.notarization.chainId = 2800; + const xNl = await M.ancoreDinChitante(cinstit.export(), [g2cLant], { verifyProof: 'stub', rpc: 'stub', ruleaza: ruleazaBun }); + cer(xR.stare === 'FAILED' && xR.randuri[0].rule === 'registru' && xL.stare === 'FAILED' && xL.randuri[0].rule === 'lant' && xN.stare === 'NEMASURAT' && xNl.stare === 'FAILED' && xNl.randuri[0].rule === 'lant', + '4. CONTROL: chitanta pe alta ramura -> FAILED "registru"; verificatorul cere lantul 2800 pentru un cap de jurnal de pe 28001 -> FAILED "lant"; o notarizare pe alt lant decat cel al capului de jurnal -> FAILED "lant"; chitanta inca nenotarizata -> NEMASURAT', JSON.stringify([xR.randuri, xL.randuri, xN.randuri, xNl.randuri]).slice(0, 300)); + + // ---------------------------------------------------------------------------------------------- 5. linia de comanda + const fPol = scrie('policy.json', pA), fCin = scrie('cinstit.json', cinstit.export()), fAnt = scrie('antedatat.json', antedatat.export()), fChit = scrie('receipt.json', g2c.j), fPend = scrie('pending.json', g2a.j); + const cli = (args, env = {}) => spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', env: { ...process.env, AERE_VERIFY_PROOF: '', STUB_F: String(F), ...env } }); + const v1 = cli(['verify', '--ledger', fCin, '--policy', fPol, '--anchors', fChit, '--rpc', 'http://127.0.0.1:9', '--verify-proof', fStub, '--witness', m.witnessId]); + const v2 = cli(['verify', '--ledger', fAnt, '--policy', fPol, '--anchors', fChit, '--rpc', 'http://127.0.0.1:9', '--verify-proof', fStub, '--witness', m.witnessId]); + const v3 = cli(['verify', '--ledger', fCin, '--policy', fPol, '--anchors', fChit, '--rpc', 'http://127.0.0.1:9']); + const v4 = cli(['verify', '--ledger', fCin, '--policy', fPol, '--anchors', fPend, '--rpc', 'http://127.0.0.1:9', '--verify-proof', fStub]); + cer(v1.status === 0 && /"lowerBound": "witnessed"/.test(v1.stdout), '5. agent-cli verify --anchors --witness : registrul cinstit -> 0, marginea de jos "witnessed"', v1.stdout + v1.stderr); + cer(v2.status === 1 && /backdated/.test(v2.stdout), '5. agent-cli verify: registrul antedatat cu aceeasi chitanta -> 1, "backdated"', v2.stdout.slice(0, 200) + v2.stderr); + cer(v3.status === 2 && /NOT MEASURED/.test(v3.stderr) && v4.status === 2 && /no receipt yet/.test(v4.stderr), '5. CONTROL: fara verificatorul AIP-23 -> 2 NOT MEASURED, pe nume; un raspuns pending (fara chitanta) -> 2, pe nume', `${v3.status} ${v3.stderr} | ${v4.status} ${v4.stderr}`); + const fEch = scrie('echivocare.json', rE1.j.proof), fEchRea = scrie('echivocare-rea.json', falsa); + const e1 = spawnSync(process.execPath, [path.join(AICI, 'martor-agenti.mjs'), 'verify-equivocation', '--proof', fEch], { encoding: 'utf8' }); + const e2 = spawnSync(process.execPath, [path.join(AICI, 'martor-agenti.mjs'), 'verify-equivocation', '--proof', fEchRea], { encoding: 'utf8' }); + const e3 = spawnSync(process.execPath, [path.join(AICI, 'martor-agenti.mjs'), 'verify-receipt', '--receipt', fChit, '--ledger', fCin], { encoding: 'utf8' }); + cer(e1.status === 0 && e2.status === 1 && e3.status === 0, '5. martor-agenti.mjs verify-equivocation (0 pe dovada martorului, 1 pe cea atinsa) si verify-receipt (0, numai local)', `${e1.status} ${e2.status} ${e3.status} ${e3.stderr}`); + const fKey = path.join(F5, 'agent.key.pem'); fs.writeFileSync(fKey, B.privateKey.export({ type: 'pkcs8', format: 'pem' })); + LB.record({ kind: 'payment', to: '0xbbbb', amount: '1' }, { at: undefined }); const fLB = scrie('lb.json', LB.export()); + wMs = Date.now(); // `submit` scrie capul la ceasul masinii; ceasul martorului il urmeaza + const sb = await ruleazaAsincron([path.join(AICI, 'martor-agenti.mjs'), 'submit', '--key', fKey, '--ledger', fLB, '--url', U()]); + const sbj = (() => { try { return JSON.parse(sb.stdout); } catch { return null; } })(); + cer(sb.status === 0 && sbj && sbj.status === 'pending' && sbj.seq === 2, '5. martor-agenti.mjs submit: ia ultimul cap martorizat al lui B ca predecesor, trimite seq 2 -> 202', sb.stdout.slice(0, 200) + sb.stderr); + const sbAlt = await ruleazaAsincron([path.join(AICI, 'martor-agenti.mjs'), 'submit', '--key', fKey, '--ledger', scrie('lb-ramura.json', registru(B, pB, [acum - 10, acum - 5, acum - 3, acum - 1]).export()), '--url', U()]); + cer(sbAlt.status === 1 && /another branch/.test(sbAlt.stderr), '5. CONTROL: submit cu o alta ramura a lui B se opreste inainte de trimitere (registrul nu are capul martorizat)', sbAlt.stdout + sbAlt.stderr); + // 9.R7: submit fara nimic nou (martorul tine deja chiar ultima intrare): 0, fara POST; un --seq mai vechi decat ultimul cap tinut: 1, numit + await rand('9.R7 submit fara nimic nou (martorul tine deja ultima intrare a registrului) -> 0 "nothing new", fara nicio frunza noua; --seq mai vechi decat ultimul cap tinut -> 1 "newer head"', async () => { + const inainte = m.sanatate().leaves; + const sb2 = await ruleazaAsincron([path.join(AICI, 'martor-agenti.mjs'), 'submit', '--key', fKey, '--ledger', fLB, '--url', U()]); + const sb3 = await ruleazaAsincron([path.join(AICI, 'martor-agenti.mjs'), 'submit', '--key', fKey, '--ledger', fLB, '--url', U(), '--seq', '1']); + return [sb2.status === 0 && /nothing new/.test(sb2.stderr) && m.sanatate().leaves === inainte && sb3.status === 1 && /newer head/.test(sb3.stderr), `${sb2.status} ${sb2.stderr.trim()} | ${sb3.status} ${sb3.stderr.trim()}`]; + }); + + // ---------------------------------------------------------------------------------------------- 6. usa gateway-ului (daca e in arbore) + // AERE_GATEWAY: controlul negativ ruleaza proba dintr-o copie si ii da gateway-ul arborelui (nu il planteaza) + const GW = process.env.AERE_GATEWAY || path.resolve(AICI, '..', '..', 'cloud-gateway', 'gateway.mjs'); + // gateway-ul Aere Cloud nu face parte din copia publicata a componentei: acolo randul e o INFORMATIE (nu un NEMASURAT), ca la conformanta + if (!fs.existsSync(GW)) console.log(` (6. usa gateway-ului Aere Cloud: gateway.mjs nu face parte din aceasta copie (${GW}); se masoara in depozitul de dezvoltare)`); + else { + const portLiber = () => new Promise((g) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => g(p)); }); }); + const pornesteGw = async (env) => { + const port = await portLiber(); const d = fs.mkdtempSync(path.join(T, 'gw-')); + const c = spawn(process.execPath, [GW], { env: { ...process.env, PORT: String(port), DIR_METRICA: d, RPC_URL: 'http://127.0.0.1:9', ...env }, stdio: ['ignore', 'pipe', 'pipe'] }); + let out = ''; c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; }); + for (let i = 0; i < 100 && !/aere-cloud-gateway pe/.test(out); i++) await new Promise((g) => setTimeout(g, 100)); + return { c, port, out: () => out }; + }; + const gw1 = await pornesteGw({ AERE_AGENTS_WITNESS_PORT: String(srv.port) }); + const gw0 = await pornesteGw({ AERE_AGENTS_WITNESS_PORT: '' }); + try { + LE.record({ kind: 'payment', to: '0xbbbb', amount: '1' }, { at: undefined }); wMs = Date.now(); + await rand('6. gateway cu AERE_AGENTS_WITNESS_PORT: POST /v1/agents/witness si GET heads ajung la martor, fara cheie API', async () => { + const viaGw = await fetch(`http://127.0.0.1:${gw1.port}/v1/agents/witness`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(M.cerereMartor(LE.export(), E, { seq: 0, createdAt: iso(wMs) })) }); + const jg = await viaGw.json().catch(() => null); + const hg = await get(`/v1/agents/witness/${E.agentId}/heads`, `http://127.0.0.1:${gw1.port}`); + return [viaGw.status === 202 && jg && jg.sandbox === M.SANDBOX && hg.cod === 200 && hg.j.sessions[0].count === 1, JSON.stringify([viaGw.status, jg, hg.cod]).slice(0, 300) + gw1.out().slice(0, 200)]; + }); + await rand('6. CONTROL: gateway fara variabila (usa nearmata): aceeasi ruta raspunde ca azi, 401 fara cheie', async () => { + const fara = await fetch(`http://127.0.0.1:${gw0.port}/v1/agents/witness`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }); + const jf = await fara.json().catch(() => null); + return [fara.status === 401 && jf && jf.error === 'missing_api_key', JSON.stringify([fara.status, jf]) + gw0.out().slice(0, 200)]; + }); + // 9.R13 (CORS): o pagina straina poate trimite POST-uri JSON din browserele vizitatorilor (nginx raspunde CORS deschis pe /v1/); un browser + // trimite intotdeauna Origin, un agent nu: usa refuza un POST cu Origin, inainte sa ajunga la martor + await rand('9.R13 gateway: un POST la /v1/agents/witness care poarta antetul Origin (un browser, pe drumul CORS) -> 403 inainte de martor; GET-ul cu Origin trece', async () => { + const inainte = m.sanatate().leaves; + LE.record({ kind: 'payment', to: '0xbbbb', amount: '1' }, { at: undefined }); wMs = Date.now(); + const rO = await fetch(`http://127.0.0.1:${gw1.port}/v1/agents/witness`, { method: 'POST', headers: { 'content-type': 'application/json', origin: 'https://pagina-straina.example' }, body: JSON.stringify(M.cerereMartor(LE.export(), E, { seq: 1, previous: 0, createdAt: iso(wMs) })) }); + const jO = await rO.json().catch(() => null); + const gO = await fetch(`http://127.0.0.1:${gw1.port}/v1/agents/witness/health`, { headers: { origin: 'https://pagina-straina.example' } }); + return [rO.status === 403 && jO && jO.error === 'browser_origin_refused' && m.sanatate().leaves === inainte && gO.status === 200, JSON.stringify([rO.status, jO, gO.status])]; + }); + } finally { gw1.c.kill(); gw0.c.kill(); } + } + + // ---------------------------------------------------------------------------------------------- 7. verificatorul AIP-23 de referinta + if (!VERIFY || !fs.existsSync(VERIFY)) { sari('7. plicul martorului VALID la verificatorul AIP-23 de referinta', 'AERE_VERIFY_PROOF nu numeste verify-proof.mjs'); sari('7. chitanta cu notarizarea NOTAR-MOCK, offline: PARTIAL la verificatorul AIP-23', 'idem'); sari('7. CONTROL: plicul atins INVALID la verificatorul AIP-23', 'idem'); } + else { + const vp = (o, ...a) => { const f = path.join(T, 'aip23-' + crypto.randomBytes(4).toString('hex') + '.json'); fs.writeFileSync(f, JSON.stringify(o)); const r = spawnSync(process.execPath, [VERIFY, f, '--json', ...a], { encoding: 'utf8' }); try { return { cod: r.status, ...JSON.parse(r.stdout) }; } catch { return { cod: r.status, verdict: '?', err: r.stderr.slice(0, 200) }; } }; + const n1 = vp(plicNenot); const sig = n1.levels && n1.levels.find((x) => x.level === 'signature'); + cer(n1.verdict === 'VALID' && n1.cod === 0 && sig && sig.state === 'PASSED', '7. capul de jurnal (chitanta checkpointed) la verificatorul AIP-23 de referinta: VALID, semnatura ML-DSA-65 a martorului PASSED, finalitatea ABSENT (nenotarizat)', JSON.stringify(n1).slice(0, 300)); + const n2 = vp(plic, '--offline'); const fin = n2.levels && n2.levels.find((x) => x.level === 'finality'); + cer(n2.verdict === 'PARTIAL' && fin && fin.state === 'UNMEASURED', '7. chitanta cu notarizarea NOTAR-MOCK, offline: PARTIAL, finalitatea UNMEASURED pe nume (o pretentie de notarizare nu e niciodata VALID fara lant)', JSON.stringify(n2).slice(0, 300)); + const atins = clon(plicNenot); atins.statement.treeSize = 7; + const n3 = vp(atins); const reSemnat = clon(plicNenot); reSemnat.statement.treeSize = 7; reSemnat.statementHash = sha(JSON.stringify(reSemnat.statement)); const n4 = vp(reSemnat); + cer(n3.verdict === 'INVALID' && n4.verdict === 'INVALID' && n4.levels.find((x) => x.level === 'signature').state === 'FAILED', '7. CONTROL: capul de jurnal atins -> INVALID (integritate); cu hash refacut -> INVALID (semnatura martorului)', JSON.stringify([n3.verdict, n4.verdict])); + } + + // ---------------------------------------------------------------------------------------------- 9. revizia adversariala B3 (2026-10-19) + await revizia({ m, U, plicNenot, g2c, F, fPol, cinstit }); + await revizia2(); + await revizia3(); + await revizia4(); + await revizia5(); + await revizia6(); + + // ---------------------------------------------------------------------------------------------- 8. testnetul 28001 (numai cu --testnet) + if (!TESTNET) console.log(' (8. notarizarea reala pe testnetul 28001 nu e ceruta: node proba-martor-agenti.mjs --testnet)'); + else await testnet(); +} finally { try { await srv.close(); } catch { /* deja inchis */ } try { m.inchide(); } catch { /* idem */ } fs.rmSync(T, { recursive: true, force: true }); } + +async function revizia({ m: mPrincipal, plicNenot, g2c }) { + // ---- 9.R1 / 9.R17: martorul FIXAT. Un al doilea martor, COERENT cu el insusi (cheia lui, ceasul lui), pe care il poate rula orice agent: + // ceasul la F - 2100, capul declarat la F - 2400 (|C - R| = 300), notarizat de agent la F. Fara fixare chitanta lui e primita; marginea e a lantului. + const acumR = Math.floor(Date.now() / 1000); const FR = acumR + 60; + const A1 = newAgentIdentity(); const pA1 = pol(A1); const T0 = acumR - 3000; + const L1 = registru(A1, pA1, [T0, T0 + 5, T0 + 10]), L1b = registru(A1, pA1, [T0, T0 + 6, T0 + 11]); // doua ramuri ale aceluiasi agent + const stubR = stub((e) => verdict(e.statementHash, { t: FR })); + const chitantaLa = async (dir, ceasMs, L, createdAtMs) => { + const mm = M.deschideMartor(dir, { ceas: () => ceasMs }); + const r = mm.primeste(M.cerereMartor(L.export(), A1, { seq: 2, createdAt: iso(createdAtMs) })); + await tur(mm, { notarizeaza: notarMock(FR) }); + const st = r.cod === 202 ? mm.stareaCapului(r.corp.id).corp : null; const w = mm.witnessId; mm.inchide(); + return { cod: r.cod, w, st, dir }; + }; + const gazduit = await chitantaLa(path.join(T, 'r1-gazduit'), acumR * 1000, L1, acumR * 1000); + const m0 = M.deschideMartor(gazduit.dir, { ceas: () => acumR * 1000 }); const eqR = m0.primeste(M.cerereMartor(L1b.export(), A1, { seq: 2, createdAt: iso(acumR * 1000) })); m0.inchide(); + const strain1 = await chitantaLa(path.join(T, 'r1-strain-1'), (FR - 2100) * 1000, L1, (FR - 2400) * 1000); + const strain2 = await chitantaLa(path.join(T, 'r1-strain-2'), (FR - 2100) * 1000, L1b, (FR - 2400) * 1000); + let tcR = FR - 2650; const ant1 = resumeLedger({ identity: A1, policy: pA1.policy, policyHash: pA1.policyHash, ledger: L1.export(), now: () => tcR }); ant1.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); + await rand('9.R1 un martor COERENT rulat de agent (alta cheie, ceasul lui la F-2100, capul declarat la F-2400): cu --witness fixat pe martorul gazduit chitanta lui -> FAILED "martor"; fara fixare e primita, dar ancora spune witnessPinned=false si marginea lantului F-2400, si randul numeste martorul', async () => { + const fix = await M.ancoreDinChitante(ant1.export(), [strain1.st], { verifyProof: 'stub', rpc: 'stub', ruleaza: stubR, martori: [gazduit.w] }); + const nefix = await M.ancoreDinChitante(ant1.export(), [strain1.st], { verifyProof: 'stub', rpc: 'stub', ruleaza: stubR }); + const a = nefix.anchors[0] || {}; + const cuGazduit = await M.ancoreDinChitante(ant1.export(), [gazduit.st], { verifyProof: 'stub', rpc: 'stub', ruleaza: stubR, martori: [gazduit.w] }); + const vG = cuGazduit.stare === 'OK' ? verifyLedger(ant1.export(), { policy: pA1.policy, anchors: cuGazduit.anchors }) : { ok: true }; + return [gazduit.cod === 202 && eqR.cod === 409 && strain1.cod === 202 && fix.stare === 'FAILED' && fix.randuri[0].rule === 'martor' + && nefix.stare === 'OK' && a.witness === strain1.w && a.witnessPinned === false && a.chainLowerBound === FR - 1800 - 600 && nefix.randuri[0].witness === strain1.w && /NOT pinned/.test(nefix.randuri[0].detail) + && cuGazduit.stare === 'OK' && cuGazduit.anchors[0].witnessPinned === true && !vG.ok && /backdated/.test(vG.error || ''), JSON.stringify([fix.randuri, nefix.randuri, a, cuGazduit.stare, vG.error]).slice(0, 400)]; + }); + await rand('9.R1 a doua ramura, refuzata de martorul gazduit ca echivocare (409), cu chitanta unui al doilea martor al agentului: fixat pe cel gazduit -> FAILED "martor"', async () => { + const r = await M.ancoreDinChitante(L1b.export(), [strain2.st], { verifyProof: 'stub', rpc: 'stub', ruleaza: stubR, martori: [gazduit.w] }); + return [eqR.cod === 409 && strain2.cod === 202 && r.stare === 'FAILED' && r.randuri[0].rule === 'martor', JSON.stringify(r.randuri)]; + }); + await rand('9.R17 agent-cli verify: chitanta martorului strain cu --witness -> 2 NOT MEASURED "[martor]" (nu e marturia martorului fixat, nici o vina dovedita a registrului; pana la a cincea revizie: 1); fara --witness -> 0, iar rezultatul numeste martorul (witnessReceipts) si spune lowerBound "UNPINNED"; verify-receipt --witness -> 1', async () => { + const fp = scrie('r17-policy.json', pA1), fl = scrie('r17-antedatat.json', ant1.export()), fr2 = scrie('r17-receipt.json', strain1.st); + const a = spawnSync(process.execPath, [CLI, 'verify', '--ledger', fl, '--policy', fp, '--anchors', fr2, '--rpc', 'http://127.0.0.1:9', '--verify-proof', fStub, '--witness', gazduit.w], { encoding: 'utf8', env: { ...process.env, STUB_F: String(FR) } }); + const b = spawnSync(process.execPath, [CLI, 'verify', '--ledger', fl, '--policy', fp, '--anchors', fr2, '--rpc', 'http://127.0.0.1:9', '--verify-proof', fStub], { encoding: 'utf8', env: { ...process.env, STUB_F: String(FR) } }); + const c = spawnSync(process.execPath, [path.join(AICI, 'martor-agenti.mjs'), 'verify-receipt', '--receipt', fr2, '--witness', gazduit.w], { encoding: 'utf8' }); + let jb = null; try { jb = JSON.parse(b.stdout); } catch { jb = null; } + return [a.status === 2 && /\[martor\]/.test(a.stderr) && /NOT MEASURED/.test(a.stderr) && b.status === 0 && jb && jb.witnessReceipts && jb.witnessReceipts[0].witness === strain1.w && jb.witnessReceipts[0].pinned === false && /UNPINNED/.test(jb.time.lowerBound) && c.status === 1, + `${a.status} ${a.stderr.slice(0, 120)} | ${b.status} ${b.stdout.slice(0, 160)} | ${c.status}`]; + }); + + // ---- 9.R2: o ramura despartita DUPA ultimul cap pentru care agentul da o chitanta; --witness-url cere lista intreaga a capetelor martorului + const X = newAgentIdentity(); const pX = pol(X); let tx = Math.floor(Date.now() / 1000) - 100; + const LX = openLedger({ identity: X, policy: pX.policy, policyHash: pX.policyHash, now: () => tx }); LX.record({ kind: 'payment', to: '0xbbbb', amount: '900' }); + const LXB = resumeLedger({ identity: X, policy: pX.policy, policyHash: pX.policyHash, ledger: LX.export(), now: () => tx }); // ramura B: aceeasi intrare 0 + let wX = Date.now(); const FX = Math.floor(wX / 1000) + 60; + const mX = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r2-')), { ceas: () => wX }); const srvX = await porneste(mX, { limite: LIMITE }); const UX = `http://127.0.0.1:${srvX.port}`; + const rx0 = mX.primeste(M.cerereMartor(LX.export(), X, { seq: 0, createdAt: iso(wX) })); + await tur(mX, { notarizeaza: notarMock(FX) }); + const chitX0 = mX.stareaCapului(rx0.corp && rx0.corp.id).corp; + tx += 120; wX += 120_000; + LX.record({ kind: 'payment', to: '0xbbbb', amount: '50' }); LX.record({ kind: 'payment', to: '0xbbbb', amount: '50' }); + const rx2 = mX.primeste(M.cerereMartor(LX.export(), X, { seq: 2, previous: 0, createdAt: iso(wX) })); + // capul cel mai nou notarizat si el: de la a treia revizie (R3-04) --witness-url cere chitanta ultimului cap tinut (o aduce de la martor) + await tur(mX, { notarizeaza: notarMock(FX) }); + LXB.record({ kind: 'payment', to: '0xbbbb', amount: '100' }); // ramura B: alta cheltuiala la seq 1 + const fpX = scrie('r2-policy.json', pX), frX = scrie('r2-receipt.json', chitX0), flB = scrie('r2-ramura-b.json', LXB.export()), flA = scrie('r2-ramura-a.json', LX.export()); + const cliX = (fl, extra) => ruleazaAsincron([CLI, 'verify', '--ledger', fl, '--policy', fpX, '--anchors', frX, '--rpc', 'http://127.0.0.1:9', '--verify-proof', fStub, ...extra], { STUB_F: String(FX), AERE_VERIFY_PROOF: '' }); + await rand('9.R2 ramura B (seq 1 = alta plata) cu chitanta capului 0, pe care martorul o are cinstit: fara --witness-url trece (limita scrisa); cu --witness-url -> 1 "[registru]" (martorul tine seq 2, care nu e in B); ramura A -> 0', async () => { + const faraUrl = await cliX(flB, []); const cuUrl = await cliX(flB, ['--witness-url', UX]); const aCinstit = await cliX(flA, ['--witness-url', UX]); + return [rx0.cod === 202 && rx2.cod === 202 && faraUrl.status === 0 && cuUrl.status === 1 && /\[registru\]/.test(cuUrl.stderr) && aCinstit.status === 0 && /"lowerBound": "witnessed"/.test(aCinstit.stdout), + `${faraUrl.status} | ${cuUrl.status} ${cuUrl.stderr.slice(0, 200)} | ${aCinstit.status} ${aCinstit.stderr.slice(0, 160)}`]; + }); + await rand('9.R2 CONTROL: --witness-url catre un martor care nu raspunde -> 2 NOT MEASURED (nu un verde); un martor care tine o dovada de echivocare pentru sesiune -> FAILED "echivocare"', async () => { + const jos = await cliX(flA, ['--witness-url', 'http://127.0.0.1:9']); + const vc = M.verificaCapeteMartorului(LX.export(), { agentId: X.agentId, session: LX.export().session, heads: [], equivocations: [{ id: '0x' + '12'.repeat(32), seq: 1 }] }); + return [jos.status === 2 && /NOT MEASURED/.test(jos.stderr) && vc.stare === 'FAILED' && vc.randuri.some((r) => r.rule === 'echivocare'), `${jos.status} ${jos.stderr.slice(0, 160)} | ${JSON.stringify(vc).slice(0, 200)}`]; + }); + await srvX.close(); mX.inchide(); + + // ---- 9.R3 / 9.R12: jurnalul in CIOBURI. Un ciob plin se ingheata si martorul trece la urmatorul (nu 503 pentru totdeauna); se tin doua cioburi. + const DR = fs.mkdtempSync(path.join(T, 'r3-')); let wR = Date.now(); + const P = newAgentIdentity(), Q = newAgentIdentity(), R = newAgentIdentity(), S_ = newAgentIdentity(); + const LP = registruBrut(P, 4), LQ0 = registruBrut(Q, 2), LR = registruBrut(R, 1), LS = registruBrut(S_, 1); + await rand('9.R3/R12 cioburile: plafon 2 frunze pe ciob; cinci capete -> toate 202 (niciun 503; notarizarea inainte de a doua rotire), ciobul 0 scos dupa al doilea ciob plin, capul lui -> 404, capul din ciobul 1 cu chitanta verificabila (jurnalul "/1"), continuitatea agentului peste cioburi, ciobul 2 numeste capul final al ciobului 1, discul fara ciobul 0', async () => { + let mR = M.deschideMartor(DR, { ceas: () => wR, maxFrunze: 2 }); + const c = (L, id, seq, prev) => mR.primeste(M.cerereMartor(L, id, { seq, previous: prev, createdAt: iso(wR) })); + const rP0 = c(LP, P, 0, null), rQ0 = c(LQ0, Q, 0, null), rP1 = c(LP, P, 1, 0), rR0 = c(LR, R, 0, null); + // a doua rotire scoate ciobul 0: capul lui de jurnal final trebuie sa fie notarizat intai (revizia B3 a doua, R2-07) + await tur(mR, { notarizeaza: notarMock(Math.floor(wR / 1000) + 60) }); + const rS0 = c(LS, S_, 0, null); + const h = mR.sanatate(); + const q0 = mR.stareaCapului(rQ0.corp.id); const p1 = mR.stareaCapului(rP1.corp.id); + const ch = p1.corp && p1.corp.receipt ? M.verificaChitanta(p1.corp.receipt) : { ok: false }; + const rP2 = c(LP, P, 2, 1); // P continua: ultimul lui cap tinut e in ciobul 1 (pastrat) + const rQ1 = c(LQ0, Q, 1, 0); // Q: singurul lui cap era in ciobul 0, scos: martorul nu il mai stie + const t = await tur(mR, { notarizeaza: notarMock(Math.floor(wR / 1000) + 60) }); + const cj = mR.capulJurnalului().corp; const cioburi = cj.shards || []; + mR.inchide(); + // stergerea ciobului scos e asincrona (zeci de mii de fisiere pe o stare reala): se asteapta, cel mult 10 s + for (let i = 0; i < 100 && fs.existsSync(path.join(DR, 'cioburi', '0')); i++) await new Promise((g) => setTimeout(g, 100)); + mR = M.deschideMartor(DR, { ceas: () => wR, maxFrunze: 2 }); const h2 = mR.sanatate(); mR.inchide(); + const coduri = [rP0, rQ0, rP1, rR0, rS0].map((r) => r.cod).join(','); + const s1 = cioburi.find((x) => x.shard === 1) || {}, s2 = cioburi.find((x) => x.shard === 2) || {}; + return [coduri === '202,202,202,202,202' && h.shard === 2 && h.maxLeaves === 2 && JSON.stringify(h.shardsKept) === '[1,2]' && q0.cod === 404 && p1.cod === 200 && ch.ok && ch.checkpoint.log === M.LOG_IMPLICIT + '/1' + && rP2.cod === 202 && rQ1.cod === 409 && rQ1.corp.error === 'not_successor' && rQ1.corp.latest === null && s2.previousShardFinal === s1.latest && !!s1.latest && t.error === null + && !fs.existsSync(path.join(DR, 'cioburi', '0')) && h2.shard === 2 && JSON.stringify(h2.shardsKept) === '[1,2]', JSON.stringify([coduri, h, q0.cod, p1.cod, ch.rule, rP2.cod, rQ1.corp, cioburi, t]).slice(0, 500)]; + }); + await rand('9.R12 bugetul jurnalului pe ora (comun tuturor): peste el un cap NOU -> 503 log_busy cu Retry-After; un cap deja vazut -> 200 (nu consuma bugetul); health spune leaves si maxLeaves', async () => { + const mB = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r12-')), { ceas: () => Date.now() }); + const srvB = await porneste(mB, { limite: { ...LIMITE, acceptatePeOra: { cereri: 3, ms: 3_600_000 } } }); const UB = `http://127.0.0.1:${srvB.port}`; + const capNou = () => { const id = newAgentIdentity(); return M.cerereMartor(registruBrut(id, 1), id, { seq: 0, createdAt: iso(Date.now()) }); }; + const primul = capNou(); const coduri = []; + coduri.push((await postLa(UB, primul, 'r12-a')).cod); for (let i = 0; i < 2; i++) coduri.push((await postLa(UB, capNou(), 'r12-b' + i)).cod); + const peste = await postLa(UB, capNou(), 'r12-c'); const din = await postLa(UB, primul, 'r12-d'); + const hb = await get('/v1/agents/witness/health', UB); + await srvB.close(); mB.inchide(); + return [coduri.join(',') === '202,202,202' && peste.cod === 503 && peste.j.error === 'log_busy' && peste.antete.get('retry-after') === '300' && din.cod === 200 && hb.j.leaves === 3 && hb.j.maxLeaves === M.PLAFON_FRUNZE + && hb.j.logBusy && hb.j.logBusy.refusedThisHour === 1, + JSON.stringify([coduri, peste.cod, peste.j, din.cod, hb.j && hb.j.maxLeaves])]; + }); + + // ---- 9.R4: acelasi seq si acelasi hash, dar cu un predecesor care contrazice un cap tinut: echivocare (nu "deja vazut") + await rand('9.R4 un cap la un seq deja tinut, cu acelasi hash, dar cu previous care contrazice capul tinut la seq 0 -> 409 equivocation, dovada pastrata, verificabila la pozitia 0', async () => { + const id = newAgentIdentity(); const L = registruBrut(id, 3); const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r4-')), { ceas: () => Date.now() }); + const h0 = M.cerereMartor(L, id, { seq: 0, createdAt: iso(Date.now()) }), h2 = M.cerereMartor(L, id, { seq: 2, previous: 0, createdAt: iso(Date.now()) }); + const c0 = mm.primeste(h0).cod, c2 = mm.primeste(h2).cod; + const st = { ...h2.statement, createdAt: iso(Date.now() + 1000), previous: { seq: 0, hash: 'ff'.repeat(32) } }; const text = JSON.stringify(st); + const fals = { v: 1, kind: h2.kind, statement: st, statementHash: sha(text), signature: { scheme: 'ml-dsa-65', publicKey: id.publicKey.export({ type: 'spki', format: 'pem' }), signature: crypto.sign(null, Buffer.from(text), id.privateKey).toString('base64') } }; + const r = mm.primeste(fals); const s = mm.sanatate(); mm.inchide(); + const v = r.corp && r.corp.proof ? M.verificaEchivocare(r.corp.proof) : { ok: false }; + return [c0 === 202 && c2 === 202 && r.cod === 409 && r.corp.error === 'equivocation' && v.ok && v.seq === 0 && s.equivocations === 1, JSON.stringify([c0, c2, r.cod, r.corp.error || r.corp.status, v, s.equivocations])]; + }); + + // ---- 9.R5: --chain si --max-merge-delay care nu sunt numere intregi pozitive: folosire gresita (2), nu o paza care cade deschisa + await rand('9.R5 agent-cli verify --chain 2800x -> 2 (folosire); --max-merge-delay 0 -> 2; ancoreDinChitante cu chain NaN -> refuz (biblioteca nu cade nici ea deschisa)', async () => { + const fl = scrie('r5-cinstit.json', registru(A1, pA1, [T0]).export()); + const a = spawnSync(process.execPath, [CLI, 'verify', '--ledger', fl, '--policy', scrie('r5-policy.json', pA1), '--anchors', scrie('r5-receipt.json', gazduit.st), '--rpc', 'http://127.0.0.1:9', '--verify-proof', fStub, '--chain', '2800x'], { encoding: 'utf8', env: { ...process.env, STUB_F: String(FR) } }); + const b = spawnSync(process.execPath, [CLI, 'verify', '--ledger', fl, '--policy', path.join(F5, 'r5-policy.json'), '--anchors', path.join(F5, 'r5-receipt.json'), '--rpc', 'http://127.0.0.1:9', '--verify-proof', fStub, '--max-merge-delay', '0'], { encoding: 'utf8', env: { ...process.env, STUB_F: String(FR) } }); + let lib = null; try { await M.ancoreDinChitante(L1.export(), [gazduit.st], { verifyProof: 'stub', rpc: 'stub', ruleaza: stubR, chain: Number('2800x') }); lib = 'primit'; } catch (e) { lib = e.message; } + return [a.status === 2 && /--chain must be a positive integer/.test(a.stderr) && b.status === 2 && /--max-merge-delay/.test(b.stderr) && /chain asked must be a positive integer/.test(String(lib)), `${a.status} ${a.stderr.slice(0, 120)} | ${b.status} | ${lib}`]; + }); + + // ---- 9.R8: vocabularul felului agent-witness-checkpoint, scris o singura data (proof-kinds) si folosit si de judecatorul martorului + await rand('9.R8 felul agent-witness-checkpoint: buildProof refuza treeSize "5", chainId "28001", maxMergeDelay 0, witness "x", log gol, createdAt "ieri"; verificaPunct refuza un plic facut de mana cu treeSize "5" ("punct-forma"); vocabularul e al felului din proof-kinds', async () => { + const baza = { root: '0x' + '5a'.repeat(32), log: 'aere-agent-witness/28001', witness: 'aere-witness:' + '7c'.repeat(20), chainId: 28001, treeSize: 3, maxMergeDelay: 1800, createdAt: '2026-10-19T09:00:00Z' }; + const rele = [['treeSize', '5'], ['chainId', '28001'], ['maxMergeDelay', 0], ['witness', 'x'], ['log', ''], ['createdAt', 'ieri']].filter(([c, v]) => { try { PK.buildProof('agent-witness-checkpoint', { ...baza, [c]: v }); return true; } catch { return false; } }); + const k = M.cheiaMartorului(fs.mkdtempSync(path.join(T, 'r8-'))); const bun = M.capDeJurnal(k, { treeSize: 3, root: baza.root, log: baza.log, chainId: 28001, maxMergeDelay: 1800 }); + const rau1 = clon(bun); rau1.statement.treeSize = '5'; rau1.statementHash = sha(JSON.stringify(rau1.statement)); rau1.signature.signature = crypto.sign(null, Buffer.from(JSON.stringify(rau1.statement)), k.privateKey).toString('base64'); + const vb = M.verificaPunct(bun), vr = M.verificaPunct(rau1); + return [rele.length === 0 && vb.ok && !vr.ok && vr.rule === 'punct-forma' && !!PK.SCHEME['agent-witness-checkpoint'].valori, JSON.stringify([rele.map(([c]) => c), vb, vr])]; + }); + + // ---- 9.R9: dovezile de echivocare pastrate sunt marginite in TOTAL (una pe agent si politica, si cel mult maxEchivocari) + await rand('9.R9 un agent cu 12 politici (sesiuni) si 3 echivocari pe fiecare, cu plafonul total 5: 36 de 409 cu dovada intoarsa, dar numai 5 pastrate (una pe sesiune), 5 fisiere pe disc, 5 si dupa redeschidere; GET heads intoarce id-uri, nu dovezi', async () => { + const DE = fs.mkdtempSync(path.join(T, 'r9-')); let mE = M.deschideMartor(DE, { ceas: () => Date.now(), maxEchivocari: 5 }); const id = newAgentIdentity(); let n409 = 0, kept = 0, sesiune0 = null; + for (let s = 0; s < 12; s++) { + const ph = '0x' + hx(); const ses = sessionId(id.agentId, ph); if (s === 0) sesiune0 = ses; + const L = (h) => ({ agentId: id.agentId, session: ses, policyHash: ph, entries: [{ seq: 0, hash: h }] }); + mE.primeste(M.cerereMartor(L(hx()), id, { createdAt: iso(Date.now()) })); + for (let k = 0; k < 3; k++) { const r = mE.primeste(M.cerereMartor(L(hx()), id, { createdAt: iso(Date.now()) })); if (r.cod === 409 && r.corp.error === 'equivocation' && M.verificaEchivocare(r.corp.proof).ok) n409++; if (r.corp.kept) kept++; } + } + const fis = ['echivocari', 'echivocari-2'].filter((d) => fs.existsSync(path.join(DE, d))).reduce((a, d) => a + fs.readdirSync(path.join(DE, d)).filter((f) => f.endsWith('.json')).length, 0); + const pag = mE.capeteAgent(id.agentId, sesiune0); mE.inchide(); + mE = M.deschideMartor(DE, { ceas: () => Date.now(), maxEchivocari: 5 }); const s2 = mE.sanatate(); mE.inchide(); + return [n409 === 36 && fis === 5 && s2.equivocations === 5 && pag.equivocations.length === 1 && pag.equivocations[0].a === undefined && !!pag.equivocations[0].id, JSON.stringify([n409, kept, fis, s2.equivocations, pag.equivocations])]; + }); + + // ---- 9.R10: GET .../heads e PAGINAT si nu citeste nimic de pe disc + await rand('9.R10 GET heads?session: 250 de capete -> pagini de 100 cu `next` (100, 200, null); limit=101 -> 400; fara session: sesiunile cu numarul capetelor; capul unui rand sters de pe disc nu opreste lista (nimic citit de pe disc)', async () => { + const id = newAgentIdentity(); const L = registruBrut(id, 250); const DP = fs.mkdtempSync(path.join(T, 'r10-')); + const mP = M.deschideMartor(DP, { ceas: () => Date.now() }); + for (let s = 0; s < 250; s++) { const r = mP.primeste(M.cerereMartor(L, id, { seq: s, previous: s ? s - 1 : null, createdAt: iso(Date.now()) })); if (r.cod !== 202) throw new Error('cap ' + s + ': ' + r.cod); } + const srvP = await porneste(mP, { limite: LIMITE }); const UP = `http://127.0.0.1:${srvP.port}`; + fs.rmSync(path.join(DP, 'cioburi', '0', 'capete', mP.capeteAgent(id.agentId, L.session).heads[5].id.slice(2) + '.json')); + const p1 = await get(`/v1/agents/witness/${id.agentId}/heads?session=${L.session}`, UP), p2 = await get(`/v1/agents/witness/${id.agentId}/heads?session=${L.session}&from=100`, UP), p3 = await get(`/v1/agents/witness/${id.agentId}/heads?session=${L.session}&from=200`, UP); + const rLim = await get(`/v1/agents/witness/${id.agentId}/heads?session=${L.session}&limit=101`, UP), rTot = await get(`/v1/agents/witness/${id.agentId}/heads`, UP); + await srvP.close(); mP.inchide(); + const n = (p) => (p.j && Array.isArray(p.j.heads) ? p.j.heads.length : -1); + return [p1.cod === 200 && n(p1) === 100 && p1.j.next === 100 && n(p2) === 100 && p2.j.next === 200 && n(p3) === 50 && p3.j.next === null && p3.j.latest.seq === 249 && rLim.cod === 400 && rTot.j.sessions[0].count === 250, + JSON.stringify([p1.cod, n(p1), p1.j && p1.j.next, n(p2), n(p3), rLim.cod, rTot.j && rTot.j.sessions && rTot.j.sessions[0]]).slice(0, 300)]; + }); + + // ---- 9.R11: retrimiterea capului PUBLIC al unui agent nu ii consuma cota (numai schimbarile de stare o consuma) + await rand('9.R11 un strain retrimite capul public al victimei de 2 x limita pe agent (de la clienti diferiti) -> 200 fiecare; capul urmator al victimei -> 202 (nu 429)', async () => { + const mV = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r11-')), { ceas: () => Date.now() }); const srvV = await porneste(mV, { limite: LIMITE }); const UV = `http://127.0.0.1:${srvV.port}`; + const V = newAgentIdentity(); const L = registruBrut(V, 2); + const h0 = M.cerereMartor(L, V, { seq: 0, createdAt: iso(Date.now()) }); const c0 = (await postLa(UV, h0, 'r11-victima')).cod; + const coduri = []; for (let i = 0; i < 2 * LIMITE.postPeAgent.cereri; i++) coduri.push((await postLa(UV, h0, 'r11-strain-' + i)).cod); + const c1 = (await postLa(UV, M.cerereMartor(L, V, { seq: 1, previous: 0, createdAt: iso(Date.now()) }), 'r11-victima')).cod; + await srvV.close(); mV.inchide(); + return [c0 === 202 && coduri.every((c) => c === 200) && c1 === 202, `${c0} ${[...new Set(coduri)].join(',')} x${coduri.length} ${c1}`]; + }); + + await rand('9.P11 CONTROL (paznicul nou al lui R11): pastrarea unei dovezi NOI e o schimbare de stare: un agent care si-a consumat cota cu capete noi, apoi echivoca -> 429, dovada nepastrata', async () => { + const mV = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r11b-')), { ceas: () => Date.now() }); const srvV = await porneste(mV, { limite: LIMITE }); const UV = `http://127.0.0.1:${srvV.port}`; + const V = newAgentIdentity(); const L = registruBrut(V, LIMITE.postPeAgent.cereri); + const coduri = []; for (let s = 0; s < LIMITE.postPeAgent.cereri; s++) coduri.push((await postLa(UV, M.cerereMartor(L, V, { seq: s, previous: s ? s - 1 : null, createdAt: iso(Date.now()) }), 'r11b')).cod); + const alt = { ...L, entries: [{ seq: 0, hash: hx() }] }; const e = await postLa(UV, M.cerereMartor(alt, V, { seq: 0, createdAt: iso(Date.now()) }), 'r11b'); + const h = mV.sanatate(); await srvV.close(); mV.inchide(); + return [coduri.every((c) => c === 202) && e.cod === 429 && h.equivocations === 0, JSON.stringify([coduri.join(','), e.cod, e.j && e.j.error, h.equivocations])]; + }); + + // ---- 9.R13: limitatoarele: IPv6 pe /64, chei curatate, plafon de chei + await rand('9.R13 cheia clientului: doua adrese din acelasi /64 -> aceeasi cheie, alt /64 -> alta, ::ffff:IPv4 -> IPv4; 41 de POST-uri din 41 de adrese ale aceluiasi /64 -> al 41-lea 429', async () => { + const k1 = S.cheieClient('2001:db8:1:2:aaaa::1'), k2 = S.cheieClient('2001:db8:1:2:bbbb:cccc:dddd:2'), k3 = S.cheieClient('2001:db8:1:3::1'), k4 = S.cheieClient('::ffff:198.51.100.7'); + const mL = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r13-')), { ceas: () => Date.now() }); const srvL = await porneste(mL, { limite: LIMITE }); const UL = `http://127.0.0.1:${srvL.port}`; + const coduri = []; for (let i = 0; i <= LIMITE.postPeClient.cereri; i++) coduri.push((await postLa(UL, '{}', `2001:db8:77:1:${(i + 1).toString(16)}::1`)).cod); + await srvL.close(); mL.inchide(); + return [k1 === k2 && k1 !== k3 && k4 === '198.51.100.7' && coduri.slice(0, -1).every((c) => c === 400) && coduri[coduri.length - 1] === 429, JSON.stringify([k1, k2, k3, k4, coduri.slice(-3)])]; + }); + await rand('9.R13 limitatorul: 1000 de chei noi cu plafonul 100 -> cel mult 100 tinute; dupa fereastra cheile expirate se curata si un client nou trece', async () => { + let t = 1_000_000; const l = S.limitator({ cereri: 5, ms: 1000 }, { maxChei: 100, ceas: () => t }); + let limitate = 0; for (let i = 0; i < 1000; i++) if (l('c' + i)) limitate++; + const marimeLa = l.marime(); t += 1001; const dupa = l('nou'); const marimeDupa = l.marime(); + return [marimeLa === 100 && dupa === false && marimeDupa === 1, JSON.stringify([marimeLa, limitate, dupa, marimeDupa])]; + }); + + // ---- 9.R16: scrierile durabile: o scriere SCURTA nu taie frunze, o scriere cazuta se da inapoi, o stare stricata in mijloc e refuzata + const origWrite = fs.writeSync; + const cuScriere = async (inlocuire, f) => { fs.writeSync = inlocuire; try { return await f(); } finally { fs.writeSync = origWrite; } }; + await rand('9.R16 fiecare fs.writeSync scrie numai jumatate (cum face un disc aproape plin): trei capete primite, si dupa redeschidere toate trei, cu chitante care verifica', async () => { + const DW = fs.mkdtempSync(path.join(T, 'r16a-')); const id = newAgentIdentity(); const L = registruBrut(id, 3); + const jumatate = function (fd, buf, ...r) { if (typeof buf === 'string') return origWrite.call(fs, fd, buf.slice(0, Math.max(1, Math.ceil(buf.length / 2)))); const off = r[0] ?? 0; const len = r[1] ?? (buf.length - off); return origWrite.call(fs, fd, buf, off, Math.max(1, Math.ceil(len / 2))); }; + const coduri = await cuScriere(jumatate, () => { const mm = M.deschideMartor(DW, { ceas: () => Date.now() }); const c = []; for (let s = 0; s < 3; s++) c.push(mm.primeste(M.cerereMartor(L, id, { seq: s, previous: s ? s - 1 : null, createdAt: iso(Date.now()) })).cod); mm.inchide(); return c; }); + const mm = M.deschideMartor(DW, { ceas: () => Date.now() }); await tur(mm, {}); const h = mm.sanatate(); const ids = mm.capeteAgent(id.agentId, L.session).heads.map((x) => x.id); + const toate = ids.every((i) => { const s = mm.stareaCapului(i).corp; return s.receipt && M.verificaChitanta(s.receipt).ok; }); mm.inchide(); + return [coduri.join(',') === '202,202,202' && h.leaves === 3 && ids.length === 3 && toate, JSON.stringify([coduri, h.leaves, ids.length, toate])]; + }); + await rand('9.R16 o scriere a frunzei care cade la jumatate (ENOSPC): fisierul se taie inapoi, capul nu e primit; dupa, capul urmator si o redeschidere -> jurnalul intreg', async () => { + const DW = fs.mkdtempSync(path.join(T, 'r16b-')); const id = newAgentIdentity(); const L = registruBrut(id, 3); let mm = M.deschideMartor(DW, { ceas: () => Date.now() }); + const c0 = mm.primeste(M.cerereMartor(L, id, { seq: 0, createdAt: iso(Date.now()) })).cod; + let pas = 0; + const cade = function (fd, buf, ...r) { const off = r[0] ?? 0; const inceput = typeof buf === 'string' ? buf.slice(0, 5) : Buffer.from(buf).subarray(off, off + 5).toString(); if (pas === 1) { pas = 2; throw Object.assign(new Error('ENOSPC: no space left on device, write'), { code: 'ENOSPC' }); } if (pas === 0 && inceput === '{"i":') { pas = 1; if (typeof buf === 'string') return origWrite.call(fs, fd, buf.slice(0, 20)); return origWrite.call(fs, fd, buf, off, 20); } return origWrite.call(fs, fd, buf, ...r); }; + const h1 = M.cerereMartor(L, id, { seq: 1, previous: 0, createdAt: iso(Date.now()) }); + let eroare = null; await cuScriere(cade, () => { try { mm.primeste(h1); } catch (e) { eroare = e.code || e.message; } }); + const dupaEsec = mm.stareaCapului(h1.statementHash).cod; + const c1 = mm.primeste(M.cerereMartor(L, id, { seq: 1, previous: 0, createdAt: iso(Date.now()) })).cod, c2 = mm.primeste(M.cerereMartor(L, id, { seq: 2, previous: 1, createdAt: iso(Date.now()) })).cod; mm.inchide(); + mm = M.deschideMartor(DW, { ceas: () => Date.now() }); const h = mm.sanatate(); mm.inchide(); + return [c0 === 202 && pas === 2 && eroare === 'ENOSPC' && dupaEsec === 404 && c1 === 202 && c2 === 202 && h.leaves === 3, JSON.stringify([c0, pas, eroare, dupaEsec, c1, c2, h.leaves])]; + }); + await rand('9.R16 o stare cu un rand de frunza stricat IN MIJLOC -> deschiderea refuza (nu taie frunze confirmate); un cap de jurnal semnat peste mai multe frunze decat sunt pe disc -> refuza', async () => { + const id = newAgentIdentity(); const L = registruBrut(id, 3); + const fa = (d) => { const mm = M.deschideMartor(d, { ceas: () => Date.now() }); for (let s = 0; s < 3; s++) mm.primeste(M.cerereMartor(L, id, { seq: s, previous: s ? s - 1 : null, createdAt: iso(Date.now()) })); mm.construiestePunct(); mm.inchide(); }; + const D1 = fs.mkdtempSync(path.join(T, 'r16c-')); fa(D1); const f1 = frunzeCiob(D1); const r1 = fs.readFileSync(f1, 'utf8').split('\n'); r1[1] = r1[1].slice(0, 30); fs.writeFileSync(f1, r1.join('\n')); + let e1 = null; try { M.deschideMartor(D1, { ceas: () => Date.now() }).inchide(); } catch (e) { e1 = e.message; } + const D2 = fs.mkdtempSync(path.join(T, 'r16d-')); fa(D2); const f2 = frunzeCiob(D2); const r2 = fs.readFileSync(f2, 'utf8').split('\n'); fs.writeFileSync(f2, r2.slice(0, 2).join('\n') + '\n'); + let e2 = null; try { M.deschideMartor(D2, { ceas: () => Date.now() }).inchide(); } catch (e) { e2 = e.message; } + return [/damaged and lines follow/.test(String(e1)) && /refusing to start/.test(String(e2)), `${e1} | ${e2}`]; + }); + // 9.R16 / 9.R18: un GET pe un cap al carui fisier lipseste: 500 cu un cod (fara cale), iar procesul ramane viu. Intr-un proces COPIL: pe forma + // veche exceptia nu era prinsa si oprea serviciul, deci oprea si proba + await rand('9.R16/R18 GET pe un cap al carui fisier lipseste de pe disc -> 500 {error, code: io_error}, fara nicio cale sau nume de dosar; serviciul raspunde mai departe (health 200)', async () => { + const DG = fs.mkdtempSync(path.join(T, 'r18-')); const fc = path.join(T, 'r18-copil.mjs'); + fs.writeFileSync(fc, `import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; +const M = await import(pathToFileURL(${JSON.stringify(path.join(AICI, 'martor-agenti.mjs'))}).href); const S = await import(pathToFileURL(${JSON.stringify(path.join(AICI, 'martor-agenti-serviciu.mjs'))}).href); +const L = await import(pathToFileURL(${JSON.stringify(path.join(AICI, 'agent-ledger.mjs'))}).href); +const dir = process.argv[2]; const m = M.deschideMartor(dir); const id = L.newAgentIdentity(); const ph = '0x' + 'ab'.repeat(32); +const led = { agentId: id.agentId, session: L.sessionId(id.agentId, ph), policyHash: ph, entries: [{ seq: 0, hash: 'cd'.repeat(32) }] }; +const r = m.primeste(M.cerereMartor(led, id, {})); const srv = await S.porneste(m, { port: 0 }); +for (const f of fs.readdirSync(dir, { recursive: true })) if (String(f).includes(r.corp.id.slice(2)) && String(f).endsWith('.json')) fs.rmSync(path.join(dir, String(f))); +const g = await fetch('http://127.0.0.1:' + srv.port + '/v1/agents/witness/' + r.corp.id); const t = await g.text(); +const h = await fetch('http://127.0.0.1:' + srv.port + '/v1/agents/witness/health'); +console.log(JSON.stringify({ cod: g.status, corp: t, sanatate: h.status })); await srv.close(); m.inchide();\n`); + const c = spawnSync(process.execPath, [fc, DG], { encoding: 'utf8', timeout: 60000 }); + let o = null; try { o = JSON.parse(c.stdout.trim().split('\n').pop()); } catch { o = null; } + const corp = o ? JSON.parse(o.corp) : null; + return [c.status === 0 && o && o.cod === 500 && corp.code === 'io_error' && !/[\\/]|capete|cioburi|ENOENT/.test(o.corp) && o.sanatate === 200, `cod proces ${c.status} | ${c.stdout.slice(0, 200)} | ${c.stderr.slice(0, 200)}`]; + }); + await rand('9.R18 o notarizare cazuta cu un mesaj care poarta o cale: in /health si in starea capului apare numai codul (unfunded; fara cod: notarization_failed), niciodata mesajul; codul notarizatorului real pe lantul 2800: chain_refused', async () => { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r18b-')), { ceas: () => Date.now() }); const id = newAgentIdentity(); + const r = mm.primeste(M.cerereMartor(registruBrut(id, 1), id, { createdAt: iso(Date.now()) })); + await tur(mm, { notarizeaza: async () => ({ ok: false, cod: 'unfunded', motiv: "unfunded: Cannot find module '/opt/aere/notar/node_modules/ethers' Require stack" }) }); + const h1 = mm.sanatate(); const s1 = mm.stareaCapului(r.corp.id).corp; + await tur(mm, { notarizeaza: async () => ({ ok: false, motiv: "ENOSPC: open '/opt/aere/martor-agenti/stare-28001/x.json'" }) }); + const h2 = mm.sanatate(); mm.inchide(); + const ethFals = { JsonRpcProvider: class { async getNetwork() { return { chainId: 2800n }; } }, parseEther: () => 0n, Wallet: class {} }; + const nr = await notarizatorReal({ rpc: 'x', cheieFisier: path.join(T, 'r18-nu.key'), ethers: ethFals })(plicNenot); + const tot = JSON.stringify([h1, s1, h2]); + return [h1.lastCheckpoint.lastError === 'unfunded' && s1.notarization.lastError === 'unfunded' && h2.lastCheckpoint.lastError === 'notarization_failed' && !/\/opt|Require|ENOSPC/.test(tot) && nr.cod === 'chain_refused', JSON.stringify([h1.lastCheckpoint, s1.notarization, h2.lastCheckpoint, nr.cod])]; + }); + // ---- 9.R6 (controlul negativ planteaza fiecare paznic numit) se masoara in control-negativ-martor-agenti.mjs (acoperirea markerilor) + void mPrincipal; void g2c; +} + +// ---------------------------------------------------------------------------------------------- 10. a doua revizie adversariala B3 (2026-10-19) +// Cate un rand pentru fiecare din cele 15 constatari confirmate (numele incep cu "10.R2-"), scris asa incat pe forma de dinainte (cf56bcda6) +// sa iasa RAU, nu sa opreasca proba; un rand "10.P2-" e controlul unui paznic nou (forma veche putea trece de el din alt motiv). +async function revizia2() { + const acumY = Math.floor(Date.now() / 1000); const FY = acumY + 60; + // ---- martorul REAL al sectiunii (ceasul lui se poate muta: pagina veche a lui R2-01) si serviciul lui + let wW = Date.now(); + const DW = fs.mkdtempSync(path.join(T, 'r2-w-')); const mW = M.deschideMartor(DW, { ceas: () => wW }); const srvW = await porneste(mW, { limite: LIMITE }); const UW = `http://127.0.0.1:${srvW.port}`; + const W = mW.witnessId; + try { + // ---- 10.R2-01: --witness --witness-url : numai lista SEMNATA de W, din ultimele 300 s, conteaza + const Y = newAgentIdentity(); const pY = pol(Y); let ty = acumY - 100; + const LY = openLedger({ identity: Y, policy: pY.policy, policyHash: pY.policyHash, now: () => ty }); LY.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); ty += 1; LY.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); + const bazaY = LY.export(); ty += 1; + const LYA = resumeLedger({ identity: Y, policy: pY.policy, policyHash: pY.policyHash, ledger: bazaY, now: () => ty }); LYA.record({ kind: 'payment', to: '0xbbbb', amount: '5' }); + const LYB = resumeLedger({ identity: Y, policy: pY.policy, policyHash: pY.policyHash, ledger: bazaY, now: () => ty }); LYB.record({ kind: 'payment', to: '0xbbbb', amount: '50' }); + const A_ = LYA.export(), B_ = LYB.export(); + const ry0 = mW.primeste(M.cerereMartor(A_, Y, { seq: 0, createdAt: iso(wW) })); + await tur(mW, { notarizeaza: notarMock(FY) }); + const chitY = mW.stareaCapului(ry0.corp && ry0.corp.id).corp; + const ry1 = mW.primeste(M.cerereMartor(A_, Y, { seq: 1, previous: 0, createdAt: iso(wW) })); + // pagina VECHE, semnata cinstit de W cu 600 s in urma (inainte de seq 2): ce poate relua un intermediar + wW = Date.now() - 600_000; const vechi = mW.capeteAgent(Y.agentId, A_.session); wW = Date.now(); + const ry2 = mW.primeste(M.cerereMartor(A_, Y, { seq: 2, previous: 1, createdAt: iso(wW) })); + await tur(mW, { notarizeaza: notarMock(FY) }); // ultimul cap tinut notarizat: --witness-url cere chitanta lui (R3-04) + const fpY = scrie('r2-01-policy.json', pY), frY = scrie('r2-01-receipt.json', chitY), faY = scrie('r2-01-a.json', A_), fbY = scrie('r2-01-b.json', B_); + const verY = (fl, url, extra = []) => ruleazaAsincron([CLI, 'verify', '--ledger', fl, '--policy', fpY, '--anchors', frY, '--rpc', 'http://127.0.0.1:9', '--verify-proof', fStub, '--witness', W, '--witness-url', url, ...extra], { STUB_F: String(FY), AERE_VERIFY_PROOF: '' }); + // proxy ostil in fata martorului real: copiaza tot si scoate seq 2 din lista (si din pagina semnata, cu hash-ul refacut: semnatura nu se poate reface) + const prox = await serverLocal(async (req, res, u) => { + const r = await fetch(UW + u.pathname + u.search, { headers: { 'x-real-ip': 'r2-01-proxy' } }); const j = await r.json(); + if (/\/heads$/.test(u.pathname) && j && Array.isArray(j.heads)) { + j.heads = j.heads.filter((h) => h.seq < 2); j.count = j.heads.length; const u2 = j.heads[j.heads.length - 1]; j.latest = u2 ? { seq: u2.seq, hash: u2.hash, id: u2.id } : null; + if (j.signedPage && j.signedPage.statement) { const st = j.signedPage.statement; st.heads = st.heads.filter((h) => h.seq < 2); st.count = st.heads.length; const u3 = st.heads[st.heads.length - 1]; st.latest = u3 ? { seq: u3.seq, hash: u3.hash } : null; j.signedPage.statementHash = sha(JSON.stringify(st)); } + } + trimite(res, r.status, j); + }); + // al doilea proxy: lasa pagina semnata NEATINSA si scoate seq 2 numai din campurile nesemnate de langa ea (heads, count, latest) + const prox2 = await serverLocal(async (req, res, u) => { + const r = await fetch(UW + u.pathname + u.search, { headers: { 'x-real-ip': 'r2-01-proxy2' } }); const j = await r.json(); + if (/\/heads$/.test(u.pathname) && j && Array.isArray(j.heads)) { j.heads = j.heads.filter((h) => h.seq < 2); j.count = j.heads.length; const u2 = j.heads[j.heads.length - 1]; j.latest = u2 ? { seq: u2.seq, hash: u2.hash, id: u2.id } : null; } + trimite(res, r.status, j); + }); + // impostor pur: spune in /health ca e W, si isi semneaza paginile cu cheia LUI (lista goala) + const kImp = M.cheiaMartorului(fs.mkdtempSync(path.join(T, 'r2-imp-'))); + const imp = await serverLocal(async (req, res, u) => (u.pathname.endsWith('/health') ? trimite(res, 200, { ok: true, witness: W }) + : trimite(res, 200, { agentId: B_.agentId, session: B_.session, heads: [], count: 0, latest: null, next: null, equivocations: [], signedPage: paginaSemnataDe(kImp, paginaGoala(kImp, B_.agentId, B_.session, Number(u.searchParams.get('from') || 0))) }))); + // reluarea paginii VECHI (semnate de W, fara seq 2) + // (numai lista e cea veche; restul rutelor merg la martorul real, deci si chitanta ultimului cap din lista veche, ceruta de la R3-04) + const rel = await serverLocal(async (req, res, u) => { + if (u.pathname.endsWith('/health')) return trimite(res, 200, { ok: true, witness: W }); + if (/\/heads$/.test(u.pathname)) return trimite(res, 200, vechi); + const r = await fetch(UW + u.pathname + u.search, { headers: { 'x-real-ip': 'r2-01-reluare' } }); return trimite(res, r.status, await r.json()); + }); + await rand('10.R2-01 --witness --witness-url: martorul real prinde ramura B (1) si trece A (0); un proxy ostil care scoate capul seq 2 din lista -> 2 NOT MEASURED (nu 0); unul care il scoate numai din campurile nesemnate -> 1 "[registru]" (conteaza pagina semnata); un impostor care se da W in /health -> 2 NOT MEASURED "[martor]" (lista lui e semnata de alta cheie: nu e marturia lui W; pana la a cincea revizie: 1); o pagina VECHE a lui W, reluata -> 2', async () => { + const a = await verY(faY, UW), b = await verY(fbY, UW), p = await verY(fbY, prox.url), p2 = await verY(fbY, prox2.url), i = await verY(fbY, imp.url), r = await verY(fbY, rel.url); + return [ry0.cod === 202 && ry1.cod === 202 && ry2.cod === 202 && a.status === 0 && b.status === 1 && /\[registru\]/.test(b.stderr) && p.status === 2 && /NOT MEASURED/.test(p.stderr) && p2.status === 1 && /\[registru\]/.test(p2.stderr) + && i.status === 2 && /\[martor\]/.test(i.stderr) && /NOT MEASURED/.test(i.stderr) && r.status === 2 && /NOT MEASURED/.test(r.stderr), + `A ${a.status} | B ${b.status} | proxy ${p.status} ${p.stderr.slice(0, 160)} | proxy nesemnat ${p2.status} ${p2.stderr.slice(0, 120)} | impostor ${i.status} ${i.stderr.slice(0, 160)} | reluare ${r.status} ${r.stderr.slice(0, 160)}`]; + }); + await rand('10.P2-01 CONTROL: paginile semnate de martor: o pagina cu un cap schimbat (hash refacut) -> "punct-semnatura"; fara hash refacut -> "pagina-integritate"; alt agent sau alt `from` decat cel cerut -> "pagina-forma"; pagina reala, acum -> ok; numarul semnat care nu se aduna -> NOT MEASURED', async () => { + const pg = mW.capeteAgent(Y.agentId, A_.session).signedPage; const o = { agentId: Y.agentId, session: A_.session, from: 0 }; + const bun = M.verificaPaginaCapetelor(pg, o); + const x1 = clon(pg); x1.statement.heads[0].hash = 'ee'.repeat(32); x1.statementHash = sha(JSON.stringify(x1.statement)); + const x2 = clon(pg); x2.statement.heads[0].hash = 'ee'.repeat(32); + const r1 = M.verificaPaginaCapetelor(x1, o), r2 = M.verificaPaginaCapetelor(x2, o), r3 = M.verificaPaginaCapetelor(pg, { ...o, agentId: B.agentId }), r4 = M.verificaPaginaCapetelor(pg, { ...o, from: 1 }); + const nep = await serverLocal(async (req, res, u) => { const st = { ...paginaGoala(kImp, A_.agentId, A_.session, 0), count: 5 }; trimite(res, 200, { signedPage: paginaSemnataDe(kImp, st) }); }); + const cn = await M.citesteCapeteMartorului(nep.url, A_.agentId, A_.session); await nep.close(); + return [bun.ok && r1.rule === 'punct-semnatura' && r2.rule === 'pagina-integritate' && r3.rule === 'pagina-forma' && r4.rule === 'pagina-forma' && !cn.ok && /ask again/.test(cn.motiv), JSON.stringify([bun.ok, r1.rule, r2.rule, r3.rule, r4.rule, cn.motiv])]; + }); + await prox.close(); await prox2.close(); await imp.close(); await rel.close(); + + // ---- 10.R2-15 (2): --witness --witness-url -> NOT MEASURED [martor] (lista e semnata de W, nu de cel fixat; din a cincea revizie, + // R5-02, o lista a altui martor nu e o dovada impotriva registrului, deci nu mai da FAILED) + await rand('10.R2-15 agent-cli verify --witness --witness-url -> 2 NOT MEASURED "[martor]" (lista e semnata de W, nu de martorul fixat)', async () => { + const r = await ruleazaAsincron([CLI, 'verify', '--ledger', faY, '--policy', fpY, '--witness', 'aere-witness:' + 'ee'.repeat(20), '--witness-url', UW], { AERE_VERIFY_PROOF: '' }); + return [r.status === 2 && /list: \[martor\]/.test(r.stderr) && /NOT MEASURED/.test(r.stderr), `${r.status} ${r.stderr.slice(0, 200)}`]; + }); + + // ---- 10.R2-04: o lista cu elemente care nu sunt capete: NEMASURAT (2), nu o exceptie si nu un verdict de ramura + await rand('10.R2-04 verificaCapeteMartorului pe heads [null], [5], [{}] -> NEMASURAT (nici exceptie, nici "FAIL [registru]")', async () => { + const v = [[null], [5], [{}]].map((heads) => M.verificaCapeteMartorului(A_, { agentId: A_.agentId, session: A_.session, heads, equivocations: [] }).stare); + return [v.every((s) => s === 'NEMASURAT'), JSON.stringify(v)]; + }); + const kRau = M.cheiaMartorului(fs.mkdtempSync(path.join(T, 'r2-04-'))); + const rele = [[null], [5], [{}], [{ seq: 0, hash: 'zz', id: '0x' + '11'.repeat(32) }]]; + await rand('10.R2-04 agent-cli verify --witness-url catre un martor care SEMNEAZA o lista cu elemente stricate ([null], [5], [{}], [{seq 0, hash "zz"}]) -> 2 NOT MEASURED de fiecare data', async () => { + const coduri = []; + for (const heads of rele) { + const s = await serverLocal(async (req, res, u) => (u.pathname.endsWith('/health') ? trimite(res, 200, { ok: true, witness: kRau.witnessId }) + : trimite(res, 200, { agentId: A_.agentId, session: A_.session, heads, count: heads.length, next: null, equivocations: [], signedPage: paginaSemnataDe(kRau, { ...paginaGoala(kRau, A_.agentId, A_.session, 0), heads, count: heads.length }) }))); + const r = await ruleazaAsincron([CLI, 'verify', '--ledger', faY, '--policy', fpY, '--witness-url', s.url], { AERE_VERIFY_PROOF: '' }); await s.close(); + coduri.push(r.status + (/Cannot read/.test(r.stderr) ? ' (exceptie)' : '')); + } + return [coduri.every((c) => c === '2'), coduri.join(',')]; + }); + + // ---- 10.R2-05: sub un martor fixat, o ancora simpla {seq, hash, at} nu e o chitanta; fara chitanta, marginea martorului nu e masurata + const P5 = newAgentIdentity(); const pP5 = pol(P5); const T5 = acumY - 3000; + const LP5 = registru(P5, pP5, [T5, T5 + 5, T5 + 10]); + const r5 = mW.primeste(M.cerereMartor(LP5.export(), P5, { seq: 2, createdAt: iso(wW) })); + await tur(mW, { notarizeaza: notarMock(FY) }); + const chit5 = mW.stareaCapului(r5.corp && r5.corp.id).corp; + let ta5 = acumY - 2000; const ant5 = resumeLedger({ identity: P5, policy: pP5.policy, policyHash: pP5.policyHash, ledger: LP5.export(), now: () => ta5 }); ant5.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); + const fp5 = scrie('r2-05-policy.json', pP5), fl5 = scrie('r2-05-antedatat.json', ant5.export()), fr5 = scrie('r2-05-receipt.json', chit5); + const fs5 = scrie('r2-05-simplu.json', [{ seq: 2, hash: LP5.export().entries[2].hash, at: T5 + 10 }]), fg5 = scrie('r2-05-gol.json', []); + const v5 = (anc, extra) => ruleazaAsincron([CLI, 'verify', '--ledger', fl5, '--policy', fp5, '--anchors', anc, '--rpc', 'http://127.0.0.1:9', '--chain', '28001', '--verify-proof', fStub, ...extra], { STUB_F: String(FY), AERE_VERIFY_PROOF: '' }); + await rand('10.R2-05 registrul antedatat: cu chitanta martorului fixat -> 1 "backdated"; cu o ancora SIMPLA facuta de agent si acelasi --witness -> 2 NOT MEASURED "[martor]" (nu 0 "witnessed"; pana la a cincea revizie: 1); cu un fisier de ancore gol si --witness -> 2; cu ancora simpla si --witness-url (martorul de acolo e cel fixat) -> 1 "backdated" (chitanta capului tinut, adusa, dovedeste antedatarea; ancora simpla nu o mai poate ascunde); fara nicio fixare ancora simpla trece ca pana azi (0)', async () => { + const c = await v5(fr5, ['--witness', W]), s = await v5(fs5, ['--witness', W]), g = await v5(fg5, ['--witness', W]), u = await v5(fs5, ['--witness-url', UW]), n = await v5(fs5, []); + return [r5.cod === 202 && c.status === 1 && /backdated/.test(c.stdout) && s.status === 2 && /anchors\[0\]: \[martor\] a plain anchor/.test(s.stderr) && /NOT MEASURED/.test(s.stderr) && g.status === 2 && /NOT MEASURED/.test(g.stderr) && u.status === 1 && /backdated/.test(u.stdout + u.stderr) && n.status === 0, + `chitanta ${c.status} | simpla ${s.status} ${s.stderr.slice(0, 120)} | gol ${g.status} ${g.stderr.slice(0, 120)} | url ${u.status} ${u.stderr.slice(-200)} | nefixat ${n.status}`]; + }); + + // ---- 10.R2-11 / 10.R2-12: codurile de iesire ale lui martor-agenti.mjs (receipt, verify-receipt) si valorile goale ale lui --witness + const MA_ = path.join(AICI, 'martor-agenti.mjs'); + const idY = ry0.corp && ry0.corp.id; + await rand('10.R2-11 receipt cu martorul raspunzand 503 sau 429 -> 2 NOT MEASURED (nu 1); 404 -> 1; verify-receipt pe raspunsul GET al unui cap PENDING -> 2', async () => { + const coduri = {}; + // (de la a sasea revizie, N2, receipt cere --ledger: raspunsul se scrie numai daca e chitanta unui cap al acelui registru) + const flR = scrie('r2-11-ledger.json', registruBrut(newAgentIdentity(), 1)); + for (const cod of [503, 429, 404]) { const s = await serverLocal(async (req, res) => trimite(res, cod, { error: cod === 404 ? 'unknown_head' : cod === 429 ? 'rate_limited' : 'agents_witness_unavailable' })); coduri[cod] = (await ruleazaAsincron([MA_, 'receipt', '--url', s.url, '--id', idY, '--ledger', flR])).status; await s.close(); } + const idP = newAgentIdentity(); const rp = mW.primeste(M.cerereMartor(registruBrut(idP, 1), idP, { createdAt: iso(wW) })); + const pend = mW.stareaCapului(rp.corp && rp.corp.id).corp; // un cap primit, inca neacoperit de niciun cap de jurnal: pending + const vr = spawnSync(process.execPath, [MA_, 'verify-receipt', '--receipt', scrie('r2-11-pending.json', pend)], { encoding: 'utf8' }); + return [rp.cod === 202 && pend.status === 'pending' && coduri[503] === 2 && coduri[429] === 2 && coduri[404] === 1 && vr.status === 2, JSON.stringify([coduri, pend.status, vr.status, vr.stdout.slice(0, 120)])]; + }); + const frChit = scrie('r2-12-receipt.json', chitY); + await rand('10.R2-12 verify-receipt --witness "" (o variabila goala), --witness "," si un --witness final fara valoare -> 2 (folosire gresita), nu 0; --witness -> 0; --witness -> 1', async () => { + const vr = (extra) => spawnSync(process.execPath, [MA_, 'verify-receipt', '--receipt', frChit, ...extra], { encoding: 'utf8' }).status; + const c = [vr(['--witness', '']), vr(['--witness', ',']), vr(['--witness']), vr(['--witness', W]), vr(['--witness', 'aere-witness:' + 'ee'.repeat(20)])]; + return [c.join(',') === '2,2,2,0,1', c.join(',')]; + }); + } finally { await srvW.close(); mW.inchide(); } + + // ---- 10.R2-02: plafonul dovezilor nu se poate umple ca sa fie impinsa afara (sau refuzata) echivocarea reala a altui agent + await rand('10.R2-02 maxFrunze 4, plafonul 8: un strain umple 8 dovezi (4 pe capetele ciobului 0, facute cand ciobul curent era 1, si 4 pe ciobul 1); dupa rotire, echivocarea reala a lui C e PASTRATA (kept) si apare in lista lui, /health numara nepastrarile (0 la plafon), iar dupa redeschidere dovada lui se reface si verifica', async () => { + const DQ = fs.mkdtempSync(path.join(T, 'r2-02-')); let mQ = M.deschideMartor(DQ, { maxFrunze: 4, maxEchivocari: 8, ceas: () => Date.now() }); + const J = newAgentIdentity(); const ses = Array.from({ length: 8 }, () => registruBrut(J, 1)); + const cap = (L, id, h) => mQ.primeste(M.cerereMartor({ ...L, entries: [{ seq: 0, hash: h || L.entries[0].hash }] }, id, { createdAt: iso(Date.now()) })); + const coduri = []; + for (let i = 0; i < 5; i++) coduri.push(cap(ses[i], J).cod); // J1..J4 umplu ciobul 0; J5 roteste in ciobul 1 + for (let i = 0; i < 4; i++) coduri.push(cap(ses[i], J, hx()).cod); // dovezile lui J1..J4 (ciobul curent: 1) + for (let i = 5; i < 8; i++) coduri.push(cap(ses[i], J).cod); // J6..J8: ciobul 1 plin + for (let i = 4; i < 8; i++) coduri.push(cap(ses[i], J, hx()).cod); // dovezile lui J5..J8 + const pline = mQ.sanatate().equivocations; + await tur(mQ, { notarizeaza: notarMock(Math.floor(Date.now() / 1000) + 60) }); // capul final al ciobului 0 notarizat: rotirea il poate scoate + const C2 = newAgentIdentity(); const LC2 = registruBrut(C2, 1); + const c0 = cap(LC2, C2).cod; const ce = cap(LC2, C2, hx()); + const lista = mQ.capeteAgent(C2.agentId, LC2.session); const h = mQ.sanatate(); + const vc = M.verificaCapeteMartorului(LC2, lista); + mQ.inchide(); mQ = M.deschideMartor(DQ, { maxFrunze: 4, maxEchivocari: 8, ceas: () => Date.now() }); + const h2 = mQ.sanatate(); const dv = ce.corp && ce.corp.proofId ? mQ.dovadaEchivocarii(ce.corp.proofId) : { cod: 0 }; mQ.inchide(); + return [pline === 8 && c0 === 202 && ce.cod === 409 && ce.corp.kept === true && lista.equivocations.length === 1 && vc.stare === 'FAILED' && h.equivocationsNotKept && h.equivocationsNotKept.cap === 0 + && h2.equivocations === h.equivocations && dv.cod === 200 && M.verificaEchivocare(dv.corp.proof).ok, + JSON.stringify([coduri.join(','), pline, c0, ce.cod, ce.corp && ce.corp.kept, lista.equivocations, vc.stare, h.equivocations, h.equivocationsNotKept, h2.equivocations, dv.cod])]; + }); + await rand('10.P2-02 CONTROL: plafonul dovezilor e marginea lor prin constructie: cu maxFrunze 4 implicit 8 (CIOBURI_PASTRATE x maxFrunze), pe starea reala 40.000', async () => { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r2-02b-')), { maxFrunze: 4 }); const h = mm.sanatate(); mm.inchide(); + return [h.maxEquivocations === 8 && M.ECHIVOCARI_TOTAL === M.CIOBURI_PASTRATE * M.PLAFON_FRUNZE && M.ECHIVOCARI_TOTAL === 40000, JSON.stringify([h.maxEquivocations, M.ECHIVOCARI_TOTAL])]; + }); + + // ---- 10.R2-03: la plafonul de chei al limitatorului, un client NOU nu e tinut afara (se scoate cheia cea mai veche) + await rand('10.R2-03 limitatorul la plafonul de chei (5): un client NOU nu e limitat, harta ramane la 5, cheia cea mai veche e scoasa (numarata); prin HTTP, dupa 5 /64-uri, un client nou primeste 200 la /health (nu 429), iar /health numara cheile scoase', async () => { + let t = 1_000_000; const l = S.limitator({ cereri: 5, ms: 600_000 }, { maxChei: 5, ceas: () => t }); + for (let i = 0; i < 5; i++) l('c' + i); + const nou = l('nou'); const marime = l.marime(); const scoase = typeof l.scoase === 'function' ? l.scoase() : null; const vechiInapoi = l('c0'); + const mL = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r2-03-')), { ceas: () => Date.now() }); const srvL = await porneste(mL, { limite: LIMITE, maxChei: 5 }); const UL = `http://127.0.0.1:${srvL.port}`; + for (let i = 0; i < 5; i++) await get('/v1/agents/witness/health', UL, `2001:db8:3:${i + 1}::1`); + const hn = await get('/v1/agents/witness/health', UL, '2001:db8:3:99::1'); + await srvL.close(); mL.inchide(); + const lim = hn.j && hn.j.limiters && hn.j.limiters.getPerClient; + return [nou === false && marime === 5 && scoase === 1 && vechiInapoi === false && hn.cod === 200 && !!lim && lim.keys <= 5 && lim.droppedAtCap >= 1, JSON.stringify([nou, marime, scoase, hn.cod, lim])]; + }); + + // ---- 10.R2-06: arborele incremental (fara memorie pe interval): aceleasi radacini si dovezi ca merkleTree, si memoria unei stari cu un cap de jurnal pe frunza + await rand('10.R2-06 arborele incremental al martorului = merkleTree (RFC 9162) pe toate marimile 1..70 (radacina, includerea fiecarei frunze, verificata de verifyInclusion) si pe toate perechile de marimi 1..40 (consistenta, verificata de verifyConsistency)', async () => { + const sursa = (fs.readFileSync(path.join(AICI, 'martor-agenti.mjs'), 'utf8').match(/from '(\.\.\/[^']+\/arbore-merkle\.mjs)'/) || [])[1] || '../verify-layer/arbore-merkle.mjs'; + const { merkleTree, verifyInclusion } = await import(pathToFileURL(path.resolve(AICI, sursa)).href); + const fr = Array.from({ length: 70 }, (_, i) => M.frunzaMartor(sha('f' + i), i)); const a = M.arboreIncremental(); let rele = 0, n = 0; + for (let s = 1; s <= 70; s++) { + a.adauga(fr[s - 1]); const r = merkleTree(fr.slice(0, s)); + if (a.root() !== r.root()) rele++; + for (let i = 0; i < s; i++) { n++; const p = a.inclusionProof(i); if (JSON.stringify(p) !== JSON.stringify(r.inclusionProof(i)) || !verifyInclusion(p)) rele++; } + if (s <= 40) for (let m1 = 1; m1 <= s; m1++) { n++; const c = a.consistencyProof(m1, s); if (JSON.stringify(c) !== JSON.stringify(r.consistencyProof(m1, s)) || !M.verifyConsistency(c)) rele++; } + } + return [rele === 0 && n > 3000, `${rele} diferente din ${n} comparatii`]; + }); + await rand('10.R2-06 o stare cu un cap de jurnal PE FRUNZA (250 de frunze, 250 de capete de jurnal), redeschisa intr-un proces nou, tine sub 1.500 de octeti de heap pe frunza (inainte de reparatie: ~2.300, memoria pe interval a arborelui)', async () => { + const fc = path.join(T, 'r2-06-copil.mjs'); + fs.writeFileSync(fc, `import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; +const M = await import(pathToFileURL(${JSON.stringify(path.join(AICI, 'martor-agenti.mjs'))}).href); const L = await import(pathToFileURL(${JSON.stringify(path.join(AICI, 'agent-ledger.mjs'))}).href); +const dir = process.argv[2]; const N = 250; fs.fsyncSync = () => {}; let t = Date.now(); const m = M.deschideMartor(dir, { ceas: () => t }); +const ids = Array.from({ length: 10 }, () => L.newAgentIdentity()); const ph = '0x' + 'ab'.repeat(32); +const leds = ids.map((id) => ({ id, led: { agentId: id.agentId, session: L.sessionId(id.agentId, ph), policyHash: ph, entries: [] } })); +for (let s = 0; s < N; s++) { const X = leds[s % 10]; const q = X.led.entries.length; X.led.entries.push({ seq: q, hash: (s + 1).toString(16).padStart(64, '0') }); t += 10; + const r = m.primeste(M.cerereMartor(X.led, X.id, { seq: q, previous: q ? q - 1 : null, createdAt: new Date(t).toISOString() })); if (r.cod !== 202) throw new Error('cap ' + s); m.construiestePunct(); } +m.inchide(); globalThis.gc(); globalThis.gc(); const h0 = process.memoryUsage().heapUsed; +const m2 = M.deschideMartor(dir, { ceas: () => t }); globalThis.gc(); globalThis.gc(); const h1 = process.memoryUsage().heapUsed; const s = m2.sanatate(); m2.inchide(); +console.log(JSON.stringify({ checkpoints: s.checkpoints, perLeaf: Math.round((h1 - h0) / N) }));\n`); + const c = spawnSync(process.execPath, ['--expose-gc', fc, fs.mkdtempSync(path.join(T, 'r2-06-'))], { encoding: 'utf8', timeout: 120000 }); + let o = null; try { o = JSON.parse(c.stdout.trim().split('\n').pop()); } catch { o = null; } + return [c.status === 0 && o && o.checkpoints === 250 && o.perLeaf < 1500, `cod ${c.status} | ${c.stdout.trim().slice(0, 200)} | ${c.stderr.slice(0, 200)}`]; + }); + + // ---- 10.R2-07: un ciob nu se scoate pana nu i s-a notarizat capul de jurnal final + await rand('10.R2-07 maxFrunze 2, notarizarea cade mereu: capul care ar scoate ciobul 0 (nenotarizat) -> 503 log_busy "notarization_behind" (nu 202), capul din ciobul 0 se serveste mai departe (200, nu 404), /health spune ca rotirea asteapta; dupa notarizare acelasi cap -> 202, iar chitanta luata dupa notarizare e "notarized"', async () => { + const mR7 = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r2-07-')), { maxFrunze: 2, ceas: () => Date.now() }); + const id = newAgentIdentity(); const L = registruBrut(id, 6); const c = (s) => mR7.primeste(M.cerereMartor(L, id, { seq: s, previous: s ? s - 1 : null, createdAt: iso(Date.now()) })); + const cade = async () => ({ ok: false, cod: 'unfunded', motiv: 'unfunded' }); + const r0 = c(0); c(1); await tur(mR7, { notarizeaza: cade }); c(2); await tur(mR7, { notarizeaza: cade }); c(3); + const r4 = c(4); const g0 = mR7.stareaCapului(r0.corp.id); const h = mR7.sanatate(); + await tur(mR7, { notarizeaza: notarMock(Math.floor(Date.now() / 1000) + 60) }); + const g0b = mR7.stareaCapului(r0.corp.id); const r4b = c(4); const g0c = mR7.stareaCapului(r0.corp.id); mR7.inchide(); + return [r4.cod === 503 && r4.corp.code === 'notarization_behind' && g0.cod === 200 && h.logBusy && h.logBusy.rotationWaitsForNotarization === true && g0b.corp.status === 'notarized' && r4b.cod === 202 && g0c.cod === 404, + JSON.stringify([r4.cod, r4.corp.code || r4.corp.status, g0.cod, h.logBusy, g0b.corp.status, r4b.cod, g0c.cod])]; + }); + await rand('10.P2-07 CONTROL: la deschidere, un ciob mai vechi decat cele pastrate al carui cap de jurnal final NU e notarizat ramane (cioburile 0, 1, 2), iar capul lui se serveste', async () => { + const DR7 = fs.mkdtempSync(path.join(T, 'r2-07b-')); let mm = M.deschideMartor(DR7, { maxFrunze: 2, faraLant: true, ceas: () => Date.now() }); + const id = newAgentIdentity(); const L = registruBrut(id, 5); const c = (s) => mm.primeste(M.cerereMartor(L, id, { seq: s, previous: s ? s - 1 : null, createdAt: iso(Date.now()) })); + const r0 = c(0); c(1); c(2); mm.construiestePunct(); + const salv = path.join(T, 'r2-07b-ciob0'); fs.cpSync(path.join(DR7, 'cioburi', '0'), salv, { recursive: true }); + c(3); c(4); mm.inchide(); + for (let i = 0; i < 100 && fs.existsSync(path.join(DR7, 'cioburi', '0')); i++) await new Promise((g) => setTimeout(g, 100)); + fs.cpSync(salv, path.join(DR7, 'cioburi', '0'), { recursive: true }); + mm = M.deschideMartor(DR7, { maxFrunze: 2, ceas: () => Date.now() }); const h = mm.sanatate(); const g0 = mm.stareaCapului(r0.corp.id); mm.inchide(); + return [JSON.stringify(h.shardsKept) === '[0,1,2]' && g0.cod === 200, JSON.stringify([h.shardsKept, g0.cod])]; + }); + + // ---- 10.R2-08: un ciob nou se naste intreg; un ciob fara ciob.json isi reface legatura + await rand('10.R2-08 rotirea cade la scrierea ciob.json a ciobului 1 (ENOSPC): niciun dosar cioburi/1 ramas pe jumatate; dupa repornire, ciobul 1 numeste capul final al ciobului 0 (previousShardFinal), si primul lui cap de jurnal la fel; un dosar .nou-* ramas se sterge la deschidere', async () => { + const DD = fs.mkdtempSync(path.join(T, 'r2-08-')); let mm = M.deschideMartor(DD, { maxFrunze: 2, ceas: () => Date.now() }); + const capNou = () => { const id = newAgentIdentity(); return mm.primeste(M.cerereMartor(registruBrut(id, 1), id, { createdAt: iso(Date.now()) })); }; + capNou(); capNou(); + const orig = fs.writeSync; let cazut = null; + fs.writeSync = function (fd, buf, ...r) { const s = typeof buf === 'string' ? buf : Buffer.from(buf).toString('utf8', r[0] ?? 0, Math.min((r[0] ?? 0) + 80, buf.length)); if (/aere-agent-witness-shard-1/.test(s) && /"k": 1/.test(s)) throw Object.assign(new Error('ENOSPC: no space left on device, write'), { code: 'ENOSPC' }); return orig.call(fs, fd, buf, ...r); }; + try { capNou(); } catch (e) { cazut = e.code; } finally { fs.writeSync = orig; } + const ramas = fs.existsSync(path.join(DD, 'cioburi', '1')); mm.inchide(); + fs.mkdirSync(path.join(DD, 'cioburi', '.nou-9-1-1', 'capete'), { recursive: true }); + mm = M.deschideMartor(DD, { maxFrunze: 2, ceas: () => Date.now() }); capNou(); mm.construiestePunct(); + const sh = mm.capulJurnalului().corp.shards; const p1 = mm.plicPunct({ k: 1, n: 0 }); mm.inchide(); + const s0 = sh.find((x) => x.shard === 0) || {}, s1 = sh.find((x) => x.shard === 1) || {}; + const nou = fs.readdirSync(path.join(DD, 'cioburi')).filter((f) => f.startsWith('.nou')); + return [cazut === 'ENOSPC' && ramas === false && !!s0.latest && s1.previousShardFinal === s0.latest && p1.statement.previous === s0.latest && nou.length === 0, + JSON.stringify([cazut, ramas, s0.latest, s1.previousShardFinal, p1.statement.previous, nou])]; + }); + await rand('10.P2-08 CONTROL: o stare in care ciobul 1 exista FARA ciob.json (o creare intrerupta a formei de dinainte) se deschide cu legatura refacuta din capul final al ciobului 0', async () => { + const DD = fs.mkdtempSync(path.join(T, 'r2-08b-')); let mm = M.deschideMartor(DD, { maxFrunze: 2, ceas: () => Date.now() }); + const capNou = () => { const id = newAgentIdentity(); return mm.primeste(M.cerereMartor(registruBrut(id, 1), id, { createdAt: iso(Date.now()) })); }; + capNou(); capNou(); capNou(); const fin0 = mm.capulJurnalului(null, 0).corp; mm.inchide(); + fs.rmSync(path.join(DD, 'cioburi', '1', 'ciob.json')); + mm = M.deschideMartor(DD, { maxFrunze: 2, ceas: () => Date.now() }); const sh = mm.capulJurnalului().corp.shards; mm.inchide(); + const s0 = sh.find((x) => x.shard === 0) || {}, s1 = sh.find((x) => x.shard === 1) || {}; + return [!!s0.latest && s1.previousShardFinal === s0.latest && fin0 && fin0.frozen === true, JSON.stringify([s0.latest, s1.previousShardFinal, fin0 && fin0.frozen])]; + }); + + // ---- 10.R2-09: consistenta ciobului inghetat pana la capul lui final, prin ruta + await rand('10.R2-09 un monitor care tine capul de jurnal (ciobul 0, marimea 3): dupa rotire GET /checkpoint?shard=0&from=3 da capul FINAL al ciobului 0 (marimea 4, previous = cel tinut) cu o dovada de consistenta care verifica fata de radacina tinuta; ?shard=7 -> 404, ?shard=x -> 400', async () => { + const m9 = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r2-09-')), { maxFrunze: 4, ceas: () => Date.now() }); const s9 = await porneste(m9, { limite: LIMITE }); const U9 = `http://127.0.0.1:${s9.port}`; + const capNou = () => { const id = newAgentIdentity(); return m9.primeste(M.cerereMartor(registruBrut(id, 1), id, { createdAt: iso(Date.now()) })); }; + for (let i = 0; i < 3; i++) capNou(); const tinut = m9.construiestePunct(); + capNou(); for (let i = 0; i < 2; i++) capNou(); m9.construiestePunct(); + const g = await get('/v1/agents/witness/checkpoint?shard=0&from=3', U9), g7 = await get('/v1/agents/witness/checkpoint?shard=7', U9), gx = await get('/v1/agents/witness/checkpoint?shard=x', U9); + await s9.close(); m9.inchide(); + const fin = g.j && g.j.latest && g.j.latest.statement; const cs = g.j && g.j.consistency; + const ok = g.cod === 200 && g.j.shard === 0 && g.j.frozen === true && fin && fin.treeSize === 4 && fin.previous === tinut.statementHash && cs + && M.verifyConsistency({ firstSize: 3, secondSize: 4, firstRoot: tinut.root, secondRoot: fin.root, path: cs.path }); + return [!!ok && g7.cod === 404 && gx.cod === 400, JSON.stringify([g.cod, g.j && g.j.log, g.j && g.j.shard, fin && fin.treeSize, !!cs, g7.cod, gx.cod])]; + }); + + // ---- 10.R2-10: lacatul ramas dupa o oprire necurata, cu pid-ul luat de alt proces viu + await rand('10.R2-10 lacatTinut: forma veche (numai pid, viu) -> tinut; scris inainte de ultima pornire a masinii -> netinut; acelasi boot, pid viu, alt moment de pornire al procesului -> netinut; acelasi moment -> tinut; pid mort -> netinut; "up:" la 100 s -> aceeasi pornire, la 1.000 s -> alta', async () => { + const t = (o, x = {}) => M.lacatTinut(typeof o === 'string' ? o : JSON.stringify(o), { propriu: 1, viu: () => true, pornire: () => '500', boot: 'linux:aaaa', ...x }).tinut; + const r = [t('123'), t({ pid: 123, boot: 'linux:bbbb', start: '500' }), t({ pid: 123, boot: 'linux:aaaa', start: '499' }), t({ pid: 123, boot: 'linux:aaaa', start: '500' }), t({ pid: 123, boot: 'linux:aaaa' }, { viu: () => false }), + t({ pid: 123, boot: 'up:1000', start: null }, { boot: 'up:1100' }), t({ pid: 123, boot: 'up:1000', start: null }, { boot: 'up:2000' })]; + return [r.join(',') === 'true,false,false,true,false,true,false', r.join(',')]; + }); + await rand('10.R2-10 deschideMartor pe o stare al carei lacat (pid-ul unui proces VIU, alt program) e scris inainte de ultima pornire a masinii -> deschisa; CONTROL: acelasi pid, pornirea de acum -> refuz "held by process"', async () => { + const DL = fs.mkdtempSync(path.join(T, 'r2-10-')); M.deschideMartor(DL).inchide(); + fs.writeFileSync(path.join(DL, '.lacat'), JSON.stringify({ pid: process.ppid, boot: 'up:1', start: null })); + let e1 = null; try { M.deschideMartor(DL).inchide(); } catch (e) { e1 = e.message; } + fs.writeFileSync(path.join(DL, '.lacat'), JSON.stringify({ pid: process.ppid, boot: M.idPornire(), start: M.pornireProces(process.ppid) })); + let e2 = null; try { M.deschideMartor(DL).inchide(); } catch (e) { e2 = e.message; } + return [e1 === null && /held by process/.test(String(e2)), `${e1} | ${e2}`]; + }); + + // ---- 10.R2-13: un cap refuzat de bugetul jurnalului nu consuma cota agentului + await rand('10.R2-13 cota pe agent 3, bugetul jurnalului plin: patru capete NOI -> 503,503,503,503 (nu 503,503,503,429); bugetul liber, acelasi agent -> 202', async () => { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r2-13-')), { ceas: () => Date.now() }); + const peAgent = S.limitator({ cereri: 3, ms: 600_000 }); let plin = true; const id = newAgentIdentity(); const L = registruBrut(id, 1); const coduri = []; + for (let i = 0; i < 4; i++) coduri.push(mm.primeste(M.cerereMartor(L, id, { createdAt: iso(Date.now() + i) }), { limitaAgent: peAgent, limitaGlobala: () => plin }).cod); + plin = false; const dupa = mm.primeste(M.cerereMartor(L, id, { createdAt: iso(Date.now() + 9) }), { limitaAgent: peAgent, limitaGlobala: () => plin }); mm.inchide(); + return [coduri.join(',') === '503,503,503,503' && dupa.cod === 202, `${coduri.join(',')} | ${dupa.cod}`]; + }); + await rand('10.P2-13 CONTROL prin serviciu (bugetul 2 pe ora, cota pe agent 1): X seq 0 -> 202, X seq 1 -> 429 (nu consuma bugetul), Y -> 202, Z -> 503 log_busy', async () => { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r2-13b-')), { ceas: () => Date.now() }); + const sv = await porneste(mm, { limite: { postPeClient: { cereri: 100, ms: 600_000 }, postPeAgent: { cereri: 1, ms: 600_000 }, getPeClient: { cereri: 100, ms: 600_000 }, acceptatePeOra: { cereri: 2, ms: 3_600_000 } } }); const UV = `http://127.0.0.1:${sv.port}`; + const X = newAgentIdentity(), Y2 = newAgentIdentity(), Z = newAgentIdentity(); const LX2 = registruBrut(X, 2); + const c = []; + c.push((await postLa(UV, M.cerereMartor(LX2, X, { seq: 0, createdAt: iso(Date.now()) }), 'r2-13')).cod); + c.push((await postLa(UV, M.cerereMartor(LX2, X, { seq: 1, previous: 0, createdAt: iso(Date.now()) }), 'r2-13')).cod); + c.push((await postLa(UV, M.cerereMartor(registruBrut(Y2, 1), Y2, { createdAt: iso(Date.now()) }), 'r2-13')).cod); + c.push((await postLa(UV, M.cerereMartor(registruBrut(Z, 1), Z, { createdAt: iso(Date.now()) }), 'r2-13')).cod); + await sv.close(); mm.inchide(); + return [c.join(',') === '202,429,202,503', c.join(',')]; + }); + + // ---- 10.R2-14: usa gateway-ului lasa sa treaca Retry-After al lui 503 log_busy + const GW2 = process.env.AERE_GATEWAY || path.resolve(AICI, '..', '..', 'cloud-gateway', 'gateway.mjs'); + if (!fs.existsSync(GW2)) console.log(` (10.R2-14 usa gateway-ului Aere Cloud: gateway.mjs nu face parte din aceasta copie; se masoara in depozitul de dezvoltare)`); + else { + await rand('10.R2-14 prin usa gateway-ului: 503 log_busy (bugetul jurnalului plin) poarta Retry-After: 300, ca direct la martor', async () => { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r2-14-')), { ceas: () => Date.now() }); + const sv = await porneste(mm, { limite: { ...LIMITE, acceptatePeOra: { cereri: 1, ms: 3_600_000 } } }); + const port = await new Promise((g) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => g(p)); }); }); + const c = spawn(process.execPath, [GW2], { env: { ...process.env, PORT: String(port), DIR_METRICA: fs.mkdtempSync(path.join(T, 'r2-14gw-')), RPC_URL: 'http://127.0.0.1:9', AERE_AGENTS_WITNESS_PORT: String(sv.port) }, stdio: ['ignore', 'pipe', 'pipe'] }); + let out = ''; c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; }); + try { + for (let i = 0; i < 100 && !/aere-cloud-gateway pe/.test(out); i++) await new Promise((g) => setTimeout(g, 100)); + const capNou = () => { const id = newAgentIdentity(); return JSON.stringify(M.cerereMartor(registruBrut(id, 1), id, { createdAt: iso(Date.now()) })); }; + const p1 = await fetch(`http://127.0.0.1:${port}/v1/agents/witness`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: capNou() }); + const p2 = await fetch(`http://127.0.0.1:${port}/v1/agents/witness`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: capNou() }); + const j2 = await p2.json().catch(() => null); + return [p1.status === 202 && p2.status === 503 && j2 && j2.error === 'log_busy' && p2.headers.get('retry-after') === '300', JSON.stringify([p1.status, p2.status, j2 && j2.error, p2.headers.get('retry-after')]) + out.slice(0, 120)]; + } finally { c.kill(); await sv.close(); mm.inchide(); } + }); + } + + // ---- 10.R2-15 (1): jumatatea "radacina" a paznicului de la deschidere, cu proba ei + await rand('10.R2-15 o stare cu o frunza din mijloc inlocuita de alt rand VALID (aceeasi pozitie, alt moment de primire, capul ei pe disc) sub un cap de jurnal semnat peste 3 frunze -> deschiderea refuza (radacina nu se reface)', async () => { + const DS = fs.mkdtempSync(path.join(T, 'r2-15-')); const mm = M.deschideMartor(DS, { ceas: () => Date.now() }); const id = newAgentIdentity(); const L = registruBrut(id, 3); + for (let s = 0; s < 3; s++) mm.primeste(M.cerereMartor(L, id, { seq: s, previous: s ? s - 1 : null, createdAt: iso(Date.now()) })); mm.construiestePunct(); mm.inchide(); + const f = frunzeCiob(DS); const r = fs.readFileSync(f, 'utf8').split('\n'); const o = JSON.parse(r[1]); o.r += 1; r[1] = JSON.stringify(o); fs.writeFileSync(f, r.join('\n')); + let e = null; try { M.deschideMartor(DS, { ceas: () => Date.now() }).inchide(); } catch (x) { e = x.message; } + return [/do not give it/.test(String(e)) && /refusing to start/.test(String(e)), String(e)]; + }); +} + +// ---------------------------------------------------------------------------------------------- 11. a treia revizie adversariala B3 (2026-10-19) +// Cate un rand pentru fiecare din cele 5 constatari confirmate ("11.R3-"), scris sa iasa RAU pe forma de dinainte (c34b34f1b); "11.P3-" +// e controlul unui paznic nou. +async function revizia3() { + const capNouIn = (mm) => { const id = newAgentIdentity(); return mm.primeste(M.cerereMartor(registruBrut(id, 1), id, { createdAt: iso(Date.now()) })); }; + const notarAcum = () => notarMock(Math.floor(Date.now() / 1000) + 60); + // o stare cu cioburile 0 si 1 pline si notarizate (maxFrunze 2), apoi capul care roteste in 2 si scoate ciobul 0, cu stergerea asincrona OPRITA + // (fs.rm nu face nimic): ce ramane pe disc cand procesul e oprit in timpul stergerii + const stareCuStergereOprita = async (DS) => { + const mm = M.deschideMartor(DS, { maxFrunze: 2, ceas: () => Date.now() }); + capNouIn(mm); capNouIn(mm); await tur(mm, { notarizeaza: notarAcum() }); capNouIn(mm); capNouIn(mm); await tur(mm, { notarizeaza: notarAcum() }); + const rm = fs.rm; fs.rm = (p, o, cb) => { if (typeof o === 'function') o(null); else if (cb) cb(null); }; + let r; try { r = capNouIn(mm); } finally { fs.rm = rm; } + mm.inchide(); + const dc = path.join(DS, 'cioburi'); + const rest = ['0', ...fs.readdirSync(dc).filter((f) => f.startsWith('.sters-0-'))].map((f) => path.join(dc, f)).find((d) => fs.existsSync(d)); + return { cod: r.cod, dc, rest }; + }; + const jumatateDinCapete = (d) => { const c = path.join(d, 'capete'); const l = fs.readdirSync(c); l.slice(0, Math.ceil(l.length / 2)).forEach((f) => fs.rmSync(path.join(c, f))); }; + // dupa redeschidere: ciobul curent se umple si o rotire noua (notarul merge) primeste 202, nu 503 pentru totdeauna + const merge = async (DS) => { + let e = null, mm = null; try { mm = M.deschideMartor(DS, { maxFrunze: 2, ceas: () => Date.now() }); } catch (x) { e = x.message; } + if (!mm) return { e }; + const h = mm.sanatate(); capNouIn(mm); await tur(mm, { notarizeaza: notarAcum() }); const r = capNouIn(mm); mm.inchide(); + return { e, shardsKept: h.shardsKept, cod: r.cod, code: r.corp && r.corp.code, sters: fs.readdirSync(path.join(DS, 'cioburi')).filter((f) => f.startsWith('.sters-0-')).length }; + }; + + // ---- 11.R3-01: un ciob scos de rotire nu mai poate ramane pe disc drept un ciob: se redenumeste intai, apoi se sterge + await rand('11.R3-01 o oprire in timpul stergerii asincrone a ciobului scos, cu restul in ORICE ordine (aici ciob.json si frunze.ndjson ramase, puncte/ sters, capete/ pe jumatate): la redeschidere restul nu e luat drept un ciob (cioburile 1 si 2), martorul porneste, iar rotirea urmatoare primeste 202', async () => { + const DS = fs.mkdtempSync(path.join(T, 'r3-01-')); const s = await stareCuStergereOprita(DS); + if (s.rest) { fs.rmSync(path.join(s.rest, 'puncte'), { recursive: true, force: true }); jumatateDinCapete(s.rest); } + const o = await merge(DS); + return [s.cod === 202 && !!s.rest && !o.e && JSON.stringify(o.shardsKept) === '[1,2]' && o.cod === 202 && o.sters === 0, JSON.stringify([s.cod, s.rest && path.basename(s.rest), o])]; + }); + await rand('11.P3-01 CONTROL: un REST al formei de dinainte, ramas pe loc in cioburi/0 fara marcaj (ciob.json si frunze.ndjson sterse primele, puncte/ gol, capete/ pe jumatate) -> sters la deschidere (cioburile 1 si 2), iar rotirea urmatoare primeste 202, nu 503 notarization_behind', async () => { + const DS = fs.mkdtempSync(path.join(T, 'r3-01b-')); const s = await stareCuStergereOprita(DS); + const d0 = path.join(s.dc, '0'); if (s.rest && s.rest !== d0) fs.renameSync(s.rest, d0); + for (const f of ['ciob.json', 'frunze.ndjson', '.de-sters']) fs.rmSync(path.join(d0, f), { force: true }); + fs.rmSync(path.join(d0, 'puncte'), { recursive: true, force: true }); fs.mkdirSync(path.join(d0, 'puncte')); jumatateDinCapete(d0); + const o = await merge(DS); + return [s.cod === 202 && !o.e && JSON.stringify(o.shardsKept) === '[1,2]' && o.cod === 202, JSON.stringify([s.cod, o])]; + }); + + // ---- 11.R3-02: o dovada contra unui cap din ciobul VECHI nu tine locul dovezii care conteaza + await rand('11.R3-02 agentul X echivoca intai contra capului lui din ciobul 0 (pastrata), apoi contra capului din ciobul 1 -> a doua ia locul primei (kept, replaced); dupa rotirea care scoate ciobul 0 sesiunea are tot o dovada, iar verificarea listei iese FAILED "echivocare"', async () => { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r3-02-')), { maxFrunze: 4, faraLant: true, ceas: () => Date.now() }); + const X = newAgentIdentity(); const L = registruBrut(X, 2); + const c = (led, seq, prev) => mm.primeste(M.cerereMartor(led, X, { seq, previous: prev, createdAt: iso(Date.now()) })); + const a0 = c(L, 0, null); for (let i = 0; i < 3; i++) capNouIn(mm); const a1 = c(L, 1, 0); + const b0 = c({ ...L, entries: [{ seq: 0, hash: hx() }] }, 0, null); + const b1 = c({ ...L, entries: [L.entries[0], { seq: 1, hash: hx() }] }, 1, 0); + for (let i = 0; i < 4; i++) capNouIn(mm); // ciobul 1 plin, apoi rotirea in 2 scoate ciobul 0 + const pag = mm.capeteAgent(X.agentId, L.session); const h = mm.sanatate(); mm.inchide(); + const vc = M.verificaCapeteMartorului(L, pag); + return [a0.cod === 202 && a1.cod === 202 && b0.cod === 409 && b0.corp.kept === true && b1.cod === 409 && b1.corp.kept === true && b1.corp.replaced === b0.corp.proofId + && JSON.stringify(h.shardsKept) === '[1,2]' && pag.equivocations.length === 1 && vc.stare === 'FAILED', + JSON.stringify([a0.cod, a1.cod, b0.cod, b0.corp.kept, b1.cod, b1.corp.kept, b1.corp.replaced === b0.corp.proofId, h.shardsKept, pag.equivocations.length, vc.stare])]; + }); + + // ---- 11.R3-03: un steag dat fara valoare, sau cu valoarea goala, e folosire gresita + const Q3 = newAgentIdentity(); const pQ3 = pol(Q3); const LQ3 = registru(Q3, pQ3, [Math.floor(Date.now() / 1000) - 100]); + const fp3 = scrie('r3-03-policy.json', pQ3), fl3 = scrie('r3-03-ledger.json', LQ3.export()); + const m3 = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r3-03-')), { ceas: () => Date.now() }); + const r3 = m3.primeste(M.cerereMartor(LQ3.export(), Q3, { createdAt: iso(Date.now()) })); await tur(m3, { notarizeaza: notarAcum() }); + const fr3 = scrie('r3-03-receipt.json', m3.stareaCapului(r3.corp.id).corp); m3.inchide(); + await rand('11.R3-03 agent-cli verify cu --witness-url sau --policy-hash puse ULTIMELE (fara valoare) si cu --revocations "" -> 2 (nu 0, cu paza scoasa); martor-agenti verify-receipt --ledger "" si --ledger pus ultimul -> 2; CONTROL: aceleasi comenzi cu valori bune -> 0', async () => { + const v = (...extra) => spawnSync(process.execPath, [CLI, 'verify', '--ledger', fl3, '--policy', fp3, ...extra], { encoding: 'utf8', env: { ...process.env, AERE_VERIFY_PROOF: '' } }).status; + const vr = (...extra) => spawnSync(process.execPath, [path.join(AICI, 'martor-agenti.mjs'), 'verify-receipt', '--receipt', fr3, ...extra], { encoding: 'utf8' }).status; + const c = [v('--witness-url'), v('--policy-hash'), v('--revocations', ''), vr('--ledger', ''), vr('--ledger'), v(), v('--policy-hash', pQ3.policyHash), vr('--ledger', fl3)]; + return [c.join(',') === '2,2,2,2,2,0,0,0', c.join(',')]; + }); + + // ---- 11.R3-04: sub --witness-url, marginea intrarilor de dupa ultimul cap tinut o da chitanta LUI, nu cea aleasa de agent + // un verificator AIP-23 inlocuit care ia momentul din notarizarea plicului (notarul-mock): fiecare chitanta cu momentul ei + const fStub2 = path.join(F5, 'verify-stub-notar.mjs'); + fs.writeFileSync(fStub2, `import fs from 'node:fs'; const e = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); const t = Number(e.notarization && e.notarization.firstSeen); +console.log(JSON.stringify({ verdict: 'VALID', statementHash: e.statementHash, levels: [{ level: 'form', state: 'PASSED', detail: 'x' }, { level: 'integrity', state: 'PASSED', detail: 'x' }, { level: 'signature', state: 'PASSED', detail: 'x' }, + { level: 'finality', state: 'PASSED', detail: 'post-quantum: records this statementHash as first seen at ' + new Date(t * 1000).toISOString() + ' (' + t + ')' }] }));\n`); + const acum4 = Math.floor(Date.now() / 1000); const T4 = acum4 - 4000, C2 = acum4 - 10; + let w4 = T4 * 1000; const m4 = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r3-04-')), { ceas: () => w4 }); const s4 = await porneste(m4, { limite: LIMITE }); const U4 = `http://127.0.0.1:${s4.port}`; + try { + const Z = newAgentIdentity(); const pZ = pol(Z); const LZ = registru(Z, pZ, [T4, T4 + 5, T4 + 10]); // capul seq 2 se semneaza abia la C2 + const z0 = m4.primeste(M.cerereMartor(LZ.export(), Z, { seq: 0, createdAt: iso(w4) })); await tur(m4, { notarizeaza: notarMock(T4 + 60) }); + w4 = C2 * 1000; const z2 = m4.primeste(M.cerereMartor(LZ.export(), Z, { seq: 2, previous: 0, createdAt: iso(w4) })); await tur(m4, { notarizeaza: notarMock(C2 + 60) }); + let ta = T4 + 30; const antZ = resumeLedger({ identity: Z, policy: pZ.policy, policyHash: pZ.policyHash, ledger: LZ.export(), now: () => ta }); antZ.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); + const fpZ = scrie('r3-04-policy.json', pZ), flZ = scrie('r3-04-antedatat.json', antZ.export()); + const fr0 = scrie('r3-04-r0.json', m4.stareaCapului(z0.corp.id).corp), fr02 = scrie('r3-04-r02.json', [m4.stareaCapului(z0.corp.id).corp, m4.stareaCapului(z2.corp.id).corp]); + const vZ = (anc) => ruleazaAsincron([CLI, 'verify', '--ledger', flZ, '--policy', fpZ, '--anchors', anc, '--rpc', 'http://127.0.0.1:9', '--chain', '28001', '--verify-proof', fStub2, '--witness', m4.witnessId, '--witness-url', U4], { AERE_VERIFY_PROOF: '' }); + await rand('11.R3-04 martorul fixat tine capetele seq 0 si seq 2 (ultimul); intrarea 3, scrisa dupa seq 2, se declara la T0+30; agentul preda NUMAI chitanta lui seq 0 -> 1 "backdated" (chitanta ultimului cap e adusa de la --witness-url), ca si cu ambele chitante; pe forma veche: 0', async () => { + const numai0 = await vZ(fr0), ambele = await vZ(fr02); + return [z0.cod === 202 && z2.cod === 202 && numai0.status === 1 && /backdated/.test(numai0.stdout) && /taken from --witness-url/.test(numai0.stderr) && ambele.status === 1 && /backdated/.test(ambele.stdout), + `numai seq 0: ${numai0.status} ${numai0.stdout.slice(0, 160)} ${numai0.stderr.slice(-200)} | ambele: ${ambele.status}`]; + }); + await rand('11.P3-04 CONTROL: ultimul cap tinut de martor inca nenotarizat (pending) si fara chitanta in --anchors -> 2 NOT MEASURED (intrarile de dupa el nu sunt marginite de martor), nu 0', async () => { + const V = newAgentIdentity(); const pV = pol(V); const LV = registru(V, pV, [C2 - 20, C2 - 10]); + const v0 = m4.primeste(M.cerereMartor(LV.export(), V, { seq: 0, createdAt: iso(w4) })); await tur(m4, { notarizeaza: notarMock(C2 + 60) }); + const v1 = m4.primeste(M.cerereMartor(LV.export(), V, { seq: 1, previous: 0, createdAt: iso(w4) })); + const r = await ruleazaAsincron([CLI, 'verify', '--ledger', scrie('r3-04b-ledger.json', LV.export()), '--policy', scrie('r3-04b-policy.json', pV), '--anchors', scrie('r3-04b-r0.json', m4.stareaCapului(v0.corp.id).corp), '--rpc', 'http://127.0.0.1:9', '--chain', '28001', '--verify-proof', fStub2, '--witness', m4.witnessId, '--witness-url', U4], { AERE_VERIFY_PROOF: '' }); + return [v0.cod === 202 && v1.cod === 202 && r.status === 2 && /not bounded by the witness/.test(r.stderr), `${v0.cod} ${v1.cod} | ${r.status} ${r.stderr.slice(-240)}`]; + }); + // un cap tinut care nu e in registrul aratat, desi ULTIMUL cap tinut (si chitanta lui, adusa de la R3-04) este: numai lista il prinde + await rand('11.P3-04b CONTROL: agentul semneaza la martor seq 0 cu un hash strain si seq 1, seq 2 din registrul B (predecesorii semnati se potrivesc, chitanta ultimului cap e buna pentru B) -> verify B --witness-url iese 1 "[registru]" pe seq 0 (lista o prinde, nu chitanta)', async () => { + const Yb = newAgentIdentity(); const pYb = pol(Yb); const LB3 = registru(Yb, pYb, [C2 - 30, C2 - 20, C2 - 15]); const E = LB3.export(); const h0 = hx(); + const amestec = (n) => ({ ...E, entries: [{ seq: 0, hash: h0 }, ...E.entries.slice(1, n)] }); + const c0 = m4.primeste(M.cerereMartor(amestec(1), Yb, { seq: 0, createdAt: iso(w4) })).cod; + const c1 = m4.primeste(M.cerereMartor(amestec(2), Yb, { seq: 1, previous: 0, createdAt: iso(w4) })).cod; + const c2 = m4.primeste(M.cerereMartor(amestec(3), Yb, { seq: 2, previous: 1, createdAt: iso(w4) })).cod; + await tur(m4, { notarizeaza: notarMock(C2 + 60) }); + const r = await ruleazaAsincron([CLI, 'verify', '--ledger', scrie('r3-04c-b.json', E), '--policy', scrie('r3-04c-policy.json', pYb), '--rpc', 'http://127.0.0.1:9', '--chain', '28001', '--verify-proof', fStub2, '--witness-url', U4], { AERE_VERIFY_PROOF: '' }); + return [c0 === 202 && c1 === 202 && c2 === 202 && r.status === 1 && /\[registru\] the witness holds head seq 0/.test(r.stderr), `${c0} ${c1} ${c2} | ${r.status} ${r.stderr.slice(0, 240)}`]; + }); + } finally { await s4.close(); m4.inchide(); } + + // ---- 11.R3-05: codurile lui receipt si submit pe raspunsuri care nu sunt ale martorului + const MA_ = path.join(AICI, 'martor-agenti.mjs'); + await rand('11.R3-05 receipt la o usa care raspunde 200 cu o pagina HTML -> 2 (nu 1 cu o exceptie); submit pe drumul "nimic nou" cu starea capului 503 -> 2, iar --out nu e scris cu eroarea; CONTROL: aceeasi stare 200 -> 0, --out scris', async () => { + const html = await serverLocal(async (req, res) => { const b = Buffer.from('edge'); res.writeHead(200, { 'content-type': 'text/html', 'content-length': b.length }); res.end(b); }); + // (de la a sasea revizie, N2, receipt cere --ledger) + const rh = await ruleazaAsincron([MA_, 'receipt', '--url', html.url, '--id', '0x' + '33'.repeat(32), '--ledger', scrie('r3-05-ledger-receipt.json', registruBrut(newAgentIdentity(), 1))]); await html.close(); + const id = newAgentIdentity(); const L = registruBrut(id, 1); const fk = path.join(F5, 'r3-05-agent.key.pem'); fs.writeFileSync(fk, id.privateKey.export({ type: 'pkcs8', format: 'pem' })); + const fl = scrie('r3-05-ledger.json', L); const idCap = '0x' + '22'.repeat(32); + const fals = (codStare) => serverLocal(async (req, res, u) => (/\/heads$/.test(u.pathname) ? trimite(res, 200, { latest: { seq: 0, hash: L.entries[0].hash, id: idCap }, heads: [], next: null }) + : codStare === 200 ? trimite(res, 200, { status: 'notarized', id: idCap }) : trimite(res, codStare, { error: 'agents_witness_unavailable', hint: 'temporary' }))); + const s503 = await fals(503); const fo1 = path.join(F5, 'r3-05-out-503.json'); const r1 = await ruleazaAsincron([MA_, 'submit', '--key', fk, '--ledger', fl, '--url', s503.url, '--out', fo1]); await s503.close(); + const s200 = await fals(200); const fo2 = path.join(F5, 'r3-05-out-200.json'); const r2 = await ruleazaAsincron([MA_, 'submit', '--key', fk, '--ledger', fl, '--url', s200.url, '--out', fo2]); await s200.close(); + return [rh.status === 2 && !/Cannot read/.test(rh.stderr) && r1.status === 2 && !fs.existsSync(fo1) && r2.status === 0 && fs.existsSync(fo2), + `receipt html ${rh.status} ${rh.stderr.slice(0, 120)} | submit 503 ${r1.status} out=${fs.existsSync(fo1)} | submit 200 ${r2.status} out=${fs.existsSync(fo2)}`]; + }); +} + +// ---------------------------------------------------------------------------------------------- 12. a patra revizie adversariala B3 (2026-10-19) +// Cate un rand pentru fiecare din cele 6 constatari confirmate ("12.R4-"), RAU pe forma de dinainte (c5183bcb8); "12.P4-" e controlul unui +// paznic nou. Regula de proiectare (agent-cli.mjs, README): sub --witness-url ce aduce verificatorul de la adresa e MARTURIA martorului. +async function revizia4() { + const fStub4 = path.join(F5, 'verify-stub-notar-4.mjs'); // verificatorul AIP-23 inlocuit: momentul din notarizarea plicului (notarul-mock) + fs.writeFileSync(fStub4, `import fs from 'node:fs'; const e = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); const t = Number(e.notarization && e.notarization.firstSeen); +console.log(JSON.stringify({ verdict: 'VALID', statementHash: e.statementHash, levels: [{ level: 'form', state: 'PASSED', detail: 'x' }, { level: 'integrity', state: 'PASSED', detail: 'x' }, { level: 'signature', state: 'PASSED', detail: 'x' }, + { level: 'finality', state: 'PASSED', detail: 'post-quantum: records this statementHash as first seen at ' + new Date(t * 1000).toISOString() + ' (' + t + ')' }] }));\n`); + const capNouIn = (mm) => { const id = newAgentIdentity(); return mm.primeste(M.cerereMartor(registruBrut(id, 1), id, { createdAt: iso(Date.now()) })); }; + const cade = async () => ({ ok: false, cod: 'unfunded', motiv: 'unfunded' }); + const verifica = (fl, fp, anc, extra) => ruleazaAsincron([CLI, 'verify', '--ledger', fl, '--policy', fp, ...(anc ? ['--anchors', anc] : []), '--rpc', 'http://127.0.0.1:9', '--chain', '28001', '--verify-proof', fStub4, ...extra], { AERE_VERIFY_PROOF: '' }); + const acum = Math.floor(Date.now() / 1000); + + // ---- 12.R4-01: o notarizare INTARZIATA a martorului (pana a notarului) nu face dintr-un registru cinstit un FAILED + { + const T0 = acum - 7000, T1 = acum - 3700; let w = T0 * 1000; + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r4-01-')), { ceas: () => w }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + try { + const Y = newAgentIdentity(); const pY = pol(Y); const L = registru(Y, pY, [T0, T0 + 5, T1 - 10]); + const h0 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 0, createdAt: iso(w) })); await tur(mm, { notarizeaza: notarMock(T0 + 30) }); + w = T1 * 1000; const h2 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 2, previous: 0, createdAt: iso(w) })); + await tur(mm, { notarizeaza: cade }); await tur(mm, { notarizeaza: notarMock(T1 + 3600) }); // primul tur cade: notarizat la T1+3600 + w = Date.now(); + const fp = scrie('r4-01-policy.json', pY), fl = scrie('r4-01-ledger.json', L.export()); + const f0 = scrie('r4-01-r0.json', mm.stareaCapului(h0.corp.id).corp), f02 = scrie('r4-01-r02.json', [mm.stareaCapului(h0.corp.id).corp, mm.stareaCapului(h2.corp.id).corp]); + await rand('12.R4-01 registru CINSTIT, ultimul cap tinut notarizat la 3600 s dupa primire (o pana a notarului): --witness W --witness-url cu chitanta lui seq 0 -> 2 NOT MEASURED, cu motivul [intarziere] (nu 1); aceeasi chitanta intarziata predata chiar de agent -> tot 2 [intarziere] (o intarziere a notarului nu e vina agentului; pana la a cincea revizie: 1); fara --witness-url -> 0', async () => { + const a1 = await verifica(fl, fp, f0, ['--witness', mm.witnessId, '--witness-url', UU]), a2 = await verifica(fl, fp, f02, ['--witness', mm.witnessId, '--witness-url', UU]), a0 = await verifica(fl, fp, f0, ['--witness', mm.witnessId]); + return [h0.cod === 202 && h2.cod === 202 && a1.status === 2 && /\[intarziere\]/.test(a1.stderr) && /NOT MEASURED/.test(a1.stderr) && a2.status === 2 && /\[intarziere\]/.test(a2.stderr) && a0.status === 0, + `adusa ${a1.status} ${a1.stderr.slice(-220)} | predata ${a2.status} | fara url ${a0.status}`]; + }); + } finally { await sv.close(); mm.inchide(); } + } + + // ---- 12.R4-02: un proxy care trece neatinse paginile semnate si strica numai GET-ul unui cap tinut: 2, nu 1, si nicio exceptie + { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r4-02-')), { ceas: () => Date.now() }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + try { + const Y = newAgentIdentity(); const pY = pol(Y); const L = registru(Y, pY, [acum - 30, acum - 20, acum - 10]); + const h0 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 0, createdAt: iso(Date.now()) })); const h2 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 2, previous: 0, createdAt: iso(Date.now()) })); + await tur(mm, { notarizeaza: notarMock(acum + 60) }); + const fp = scrie('r4-02-policy.json', pY), fl = scrie('r4-02-ledger.json', L.export()), f0 = scrie('r4-02-r0.json', mm.stareaCapului(h0.corp.id).corp); + const rupt = clon(mm.stareaCapului(h0.corp.id).corp); rupt.receipt.witnessReceipt = {}; const fRupt = scrie('r4-02-rupt.json', rupt); + const proxy = (strica) => serverLocal(async (req, res, u) => { + const r = await fetch(UU + u.pathname + u.search, { headers: { 'x-real-ip': 'r4-02-proxy' } }); let j = await r.json(); + if (u.pathname.endsWith(h2.corp.id)) j = strica(clon(j)); + trimite(res, r.status, j); + }); + const stricari = [['drum', (j) => { j.receipt.witnessReceipt.path = ['0x' + '00'.repeat(32)]; return j; }], ['fara-cap', (j) => { j.receipt.witnessReceipt = {}; return j; }], ['fara-chitanta', () => ({ status: 'notarized' })]]; + await rand('12.R4-02 proxy la --witness-url care strica numai GET-ul capului seq 2 (o dovada de includere stricata, o chitanta fara cap, un {status: notarized} fara chitanta) -> 2 de fiecare data, fara exceptie si fara mesaj contradictoriu; o chitanta fara cap predata de AGENT -> 2 NOT MEASURED "[chitanta-forma]" (o forma gresita nu e o vina dovedita; pana la a cincea revizie: 1), fara exceptie', async () => { + const coduri = []; + for (const [nume, f] of stricari) { const p = await proxy(f); const r = await verifica(fl, fp, f0, ['--witness', mm.witnessId, '--witness-url', p.url]); await p.close(); coduri.push(`${nume}:${r.status}${/Cannot read/.test(r.stderr) ? '(exceptie)' : ''}${/notarized, not notarized/.test(r.stderr) ? '(contradictie)' : ''}`); } + const ag = await verifica(fl, fp, fRupt, ['--witness', mm.witnessId, '--witness-url', UU]); + return [h0.cod === 202 && h2.cod === 202 && coduri.join(',') === 'drum:2,fara-cap:2,fara-chitanta:2' && ag.status === 2 && /\[chitanta-forma\]/.test(ag.stderr) && /NOT MEASURED/.test(ag.stderr) && !/Cannot read/.test(ag.stderr), + `${coduri.join(',')} | agent ${ag.status} ${ag.stderr.slice(-160)}`]; + }); + } finally { await sv.close(); mm.inchide(); } + } + + // ---- 12.R4-03: FIECARE cap tinut, nu doar ultimul: un cap intermediar fara chitanta nu mai lasa intrarile de dupa el antedatate + { + const T0 = acum - 4000, C2 = acum - 400, C4 = acum - 30; let w = T0 * 1000; + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r4-03-')), { ceas: () => w }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + try { + const Z = newAgentIdentity(); const pZ = pol(Z); const L = registru(Z, pZ, [T0, T0 + 5, T0 + 10]); + const h0 = mm.primeste(M.cerereMartor(L.export(), Z, { seq: 0, createdAt: iso(w) })); await tur(mm, { notarizeaza: notarMock(T0 + 60) }); + w = C2 * 1000; const h2 = mm.primeste(M.cerereMartor(L.export(), Z, { seq: 2, previous: 0, createdAt: iso(w) })); await tur(mm, { notarizeaza: notarMock(C2 + 60) }); + let ta = T0 + 30; const L3 = resumeLedger({ identity: Z, policy: pZ.policy, policyHash: pZ.policyHash, ledger: L.export(), now: () => ta }); L3.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); // intrarea 3, antedatata + ta = acum - 50; const L4 = resumeLedger({ identity: Z, policy: pZ.policy, policyHash: pZ.policyHash, ledger: L3.export(), now: () => ta }); L4.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); // intrarea 4, cinstita + w = C4 * 1000; const h4 = mm.primeste(M.cerereMartor(L4.export(), Z, { seq: 4, previous: 2, createdAt: iso(w) })); await tur(mm, { notarizeaza: notarMock(C4 + 60) }); + w = Date.now(); + const fp = scrie('r4-03-policy.json', pZ), fl = scrie('r4-03-ledger.json', L4.export()); + const st = (h) => mm.stareaCapului(h.corp.id).corp; + const f0 = scrie('r4-03-r0.json', st(h0)), fGol = scrie('r4-03-gol.json', []), f024 = scrie('r4-03-r024.json', [st(h0), st(h2), st(h4)]); + await rand('12.R4-03 martorul tine capetele 0, 2, 4; intrarea 3, scrisa dupa capul 2, se declara la T0+30: cu NUMAI chitanta lui 0 -> 1 "backdated" (chitantele lui 2 si 4 aduse de la --witness-url); cu --anchors [] -> 1; CONTROL: cu toate trei -> 1', async () => { + const a = await verifica(fl, fp, f0, ['--witness', mm.witnessId, '--witness-url', UU]), b = await verifica(fl, fp, fGol, ['--witness-url', UU]), c = await verifica(fl, fp, f024, ['--witness', mm.witnessId, '--witness-url', UU]); + return [h0.cod === 202 && h2.cod === 202 && h4.cod === 202 && a.status === 1 && /backdated/.test(a.stdout) && b.status === 1 && /backdated/.test(b.stdout) && c.status === 1 && /backdated/.test(c.stdout), + `numai 0: ${a.status} | gol: ${b.status} ${b.stderr.slice(-160)} | toate: ${c.status}`]; + }); + } finally { await sv.close(); mm.inchide(); } + } + + // ---- 12.P4-03b: o chitanta ADUSA, verificata ca a martorului, al carei cap tinut numeste un predecesor care nu e intrare a registrului: FAILED + { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r4-03b-')), { maxFrunze: 4, faraLant: true, ceas: () => Date.now() }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + try { + const Yb = newAgentIdentity(); const pYb = pol(Yb); const B3 = registru(Yb, pYb, [acum - 20, acum - 10]); const E = B3.export(); const h0 = hx(); + const c0 = mm.primeste(M.cerereMartor({ ...E, entries: [{ seq: 0, hash: h0 }] }, Yb, { seq: 0, createdAt: iso(Date.now()) })).cod; + for (let i = 0; i < 3; i++) capNouIn(mm); + const c1 = mm.primeste(M.cerereMartor({ ...E, entries: [{ seq: 0, hash: h0 }, E.entries[1]] }, Yb, { seq: 1, previous: 0, createdAt: iso(Date.now()) })).cod; + for (let i = 0; i < 4; i++) capNouIn(mm); // ciobul 1 plin, rotirea in 2 scoate ciobul 0: martorul mai tine numai seq 1 + await tur(mm, { notarizeaza: notarMock(acum + 60) }); + await rand('12.P4-03b CONTROL: martorul mai tine numai seq 1 al registrului B, cu predecesorul semnat seq 0 = un hash strain (ciobul lui scos): lista trece, iar chitanta lui seq 1, adusa si verificata, arata ramura -> 1 FAILED (vina registrului), nu 2', async () => { + const r = await verifica(scrie('r4-03b-b.json', E), scrie('r4-03b-policy.json', pYb), null, ['--witness-url', UU]); + return [c0 === 202 && c1 === 202 && r.status === 1 && /\[registru\]/.test(r.stderr) && /another branch/.test(r.stderr), `${c0} ${c1} | ${r.status} ${r.stderr.slice(-240)}`]; + }); + } finally { await sv.close(); mm.inchide(); } + } + + // ---- 12.R4-04: submit pe drumul principal (POST): un 200/202 care nu e starea capului trimis + const MA_ = path.join(AICI, 'martor-agenti.mjs'); + await rand('12.R4-04 submit cu POST-ul raspuns 200 sau 202 cu o pagina HTML, sau 202 JSON cu alta sesiune -> 2, iar --out nu e scris (nu 0 cu "null" in --out)', async () => { + const id = newAgentIdentity(); const L = registruBrut(id, 1); const fk = path.join(F5, 'r4-04-agent.key.pem'); fs.writeFileSync(fk, id.privateKey.export({ type: 'pkcs8', format: 'pem' })); const fl = scrie('r4-04-ledger.json', L); + const rez = []; + for (const [nume, cod, corp] of [['200-html', 200, null], ['202-html', 202, null], ['202-alta-sesiune', 202, { status: 'pending', id: '0x' + '44'.repeat(32), session: 'ab'.repeat(16), seq: 0 }]]) { + const s = await serverLocal(async (req, res, u) => { + if (/\/heads$/.test(u.pathname)) return trimite(res, 200, { latest: null, heads: [], next: null }); + if (corp) return trimite(res, cod, corp); + const b = Buffer.from('edge'); res.writeHead(cod, { 'content-type': 'text/html', 'content-length': b.length }); res.end(b); + }); + const fo = path.join(F5, `r4-04-out-${nume}.json`); const r = await ruleazaAsincron([MA_, 'submit', '--key', fk, '--ledger', fl, '--url', s.url, '--out', fo]); await s.close(); + rez.push(`${nume}:${r.status}:${fs.existsSync(fo) ? 'scris' : 'nescris'}`); + } + return [rez.join(',') === '200-html:2:nescris,202-html:2:nescris,202-alta-sesiune:2:nescris', rez.join(',')]; + }); + + // ---- 12.R4-05: --flag=valoare, greseli de tastare si steaguri date de doua ori: folosire gresita (2), in ambele linii de comanda + { + const Q4 = newAgentIdentity(); const pQ4 = pol(Q4); const LQ4 = registru(Q4, pQ4, [acum - 100]); + const fp = scrie('r4-05-policy.json', pQ4), fl = scrie('r4-05-ledger.json', LQ4.export()); + const m5 = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r4-05-')), { ceas: () => Date.now() }); const r5 = m5.primeste(M.cerereMartor(LQ4.export(), Q4, { createdAt: iso(Date.now()) })); await tur(m5, { notarizeaza: notarMock(acum + 60) }); + const fr = scrie('r4-05-receipt.json', m5.stareaCapului(r5.corp.id).corp); const W5 = m5.witnessId; m5.inchide(); + await rand('12.R4-05 agent-cli verify --witness-url=, --policy-hash=, --revocation (greseala de tastare), --ledger dat de doua ori; martor-agenti verify-receipt --witness=, --ledger=, --ledgr , --ledger de doua ori -> 2 de fiecare data (nu 0); CONTROL: formele bune -> 0', async () => { + const v = (...a) => spawnSync(process.execPath, [CLI, 'verify', ...a], { encoding: 'utf8', env: { ...process.env, AERE_VERIFY_PROOF: '' } }).status; + const vr = (...a) => spawnSync(process.execPath, [MA_, 'verify-receipt', '--receipt', fr, ...a], { encoding: 'utf8' }).status; + const c = [v('--ledger', fl, '--policy', fp, '--witness-url=http://127.0.0.1:9'), v('--ledger', fl, '--policy', fp, '--policy-hash=' + pQ4.policyHash), v('--ledger', fl, '--policy', fp, '--revocation', fp), + v('--ledger', fl, '--policy', fp, '--ledger', fl), vr('--witness=' + W5), vr('--ledger=' + fl), vr('--ledgr', fl), vr('--ledger', fl, '--ledger', fl), v('--ledger', fl, '--policy', fp, '--policy-hash', pQ4.policyHash), vr('--witness', W5, '--ledger', fl)]; + return [c.join(',') === '2,2,2,2,2,2,2,2,0,0', c.join(',')]; + }); + } + + // ---- 12.R4-06: departajarea de la deschidere intre doua dovezi ale unei sesiuni, si marcajul .de-sters, fiecare cu randul lui + await rand('12.R4-06 o inlocuire de dovada intrerupta (dovada veche pusa la loc pe disc langa cea noua): la redeschidere ramane cea NOUA (a capului din ciobul mai nou), iar cea veche e stearsa; dupa rotirea care scoate ciobul 0 sesiunea are tot o dovada', async () => { + const D6 = fs.mkdtempSync(path.join(T, 'r4-06a-')); let mm = M.deschideMartor(D6, { maxFrunze: 4, faraLant: true, ceas: () => Date.now() }); + const X = newAgentIdentity(); const L = registruBrut(X, 2); const c = (led, seq, prev) => mm.primeste(M.cerereMartor(led, X, { seq, previous: prev, createdAt: iso(Date.now()) })); + c(L, 0, null); for (let i = 0; i < 3; i++) capNouIn(mm); c(L, 1, 0); + const b0 = c({ ...L, entries: [{ seq: 0, hash: hx() }] }, 0, null); + const fis = (id) => ['echivocari-2', 'echivocari'].map((d) => path.join(D6, d, String(id).slice(2) + '.json')).find((f) => fs.existsSync(f)); + const f0 = fis(b0.corp.proofId); const copie0 = f0 ? fs.readFileSync(f0) : null; + const b1 = c({ ...L, entries: [L.entries[0], { seq: 1, hash: hx() }] }, 1, 0); mm.inchide(); + if (f0 && copie0) fs.writeFileSync(f0, copie0); // inlocuirea intrerupta: dovada veche ramasa pe disc + mm = M.deschideMartor(D6, { maxFrunze: 4, faraLant: true, ceas: () => Date.now() }); + const eq1 = mm.capeteAgent(X.agentId, L.session).equivocations; const veche = !!fis(b0.corp.proofId); + for (let i = 0; i < 4; i++) capNouIn(mm); const eq2 = mm.capeteAgent(X.agentId, L.session).equivocations; mm.inchide(); + return [b0.corp.kept === true && b1.corp.kept === true && eq1.length === 1 && eq1[0].id === b1.corp.proofId && !veche && eq2.length === 1, JSON.stringify([b1.corp.kept, eq1.map((q) => q.id === b1.corp.proofId), veche, eq2.length])]; + }); + await rand('12.R4-06 redenumirea ciobului scos cade (EPERM), deci dosarul e marcat .de-sters; stergerea e oprita, puncte/ si jumatate din capete/ sterse, ciob.json si frunze.ndjson ramase: la redeschidere restul e sters dupa marcaj (cioburile 1 si 2), iar rotirea urmatoare primeste 202', async () => { + const DS = fs.mkdtempSync(path.join(T, 'r4-06b-')); let mm = M.deschideMartor(DS, { maxFrunze: 2, ceas: () => Date.now() }); const notar = () => notarMock(Math.floor(Date.now() / 1000) + 60); + capNouIn(mm); capNouIn(mm); await tur(mm, { notarizeaza: notar() }); capNouIn(mm); capNouIn(mm); await tur(mm, { notarizeaza: notar() }); + const rm = fs.rm, ren = fs.renameSync; const d0 = path.join(DS, 'cioburi', '0'); + fs.rm = (p, o, cb) => { if (typeof o === 'function') o(null); else if (cb) cb(null); }; + fs.renameSync = function (a, b) { if (path.resolve(String(a)) === path.resolve(d0)) throw Object.assign(new Error('EPERM: operation not permitted, rename'), { code: 'EPERM' }); return ren.call(fs, a, b); }; + let r; try { r = capNouIn(mm); } finally { fs.rm = rm; fs.renameSync = ren; } + mm.inchide(); + const marcat = fs.existsSync(path.join(d0, '.de-sters')); + fs.rmSync(path.join(d0, 'puncte'), { recursive: true, force: true }); const cp = path.join(d0, 'capete'); const l = fs.existsSync(cp) ? fs.readdirSync(cp) : []; l.slice(0, Math.ceil(l.length / 2)).forEach((f) => fs.rmSync(path.join(cp, f))); + let e = null; try { mm = M.deschideMartor(DS, { maxFrunze: 2, ceas: () => Date.now() }); } catch (x) { e = x.message; mm = null; } + let h = null, r2 = null; if (mm) { h = mm.sanatate(); capNouIn(mm); await tur(mm, { notarizeaza: notar() }); r2 = capNouIn(mm); mm.inchide(); } + return [r.cod === 202 && marcat && !e && JSON.stringify(h.shardsKept) === '[1,2]' && r2.cod === 202, JSON.stringify([r.cod, marcat, e, h && h.shardsKept, r2 && r2.cod])]; + }); +} + +// ================================================================================================ 13. a cincea revizie adversariala B3 +// (2026-10-19, 6 constatari R5-01..R5-06): randurile "13.R5-", RAU pe forma de dinainte (b55038c2a). `verify` e acum o EVALUARE COMPLETA +// urmata de UN SINGUR verdict: toate dovezile se aduna, fiecare primeste o clasa (VERIFICATA, VINA-REGISTRULUI, NEMASURATA), iar verdictul e +// FAILED daca exista o vina dovedita, altfel NOT MEASURED daca o dovada ceruta e nemasurata, altfel OK. Randurile "13.P5-" sunt controalele +// claselor (o vina a agentului ramane vina, un proxy nu poate face un FAILED); ordinea in care vin dovezile o rastoarna controlul negativ, care cere +// atunci proba verde intreaga. +async function revizia5() { + const fStub5 = path.join(F5, 'verify-stub-notar-5.mjs'); // verificatorul AIP-23 inlocuit: momentul din notarizarea plicului (notarul-mock) + fs.writeFileSync(fStub5, `import fs from 'node:fs'; const e = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); const t = Number(e.notarization && e.notarization.firstSeen); +console.log(JSON.stringify({ verdict: 'VALID', statementHash: e.statementHash, levels: [{ level: 'form', state: 'PASSED', detail: 'x' }, { level: 'integrity', state: 'PASSED', detail: 'x' }, { level: 'signature', state: 'PASSED', detail: 'x' }, + { level: 'finality', state: 'PASSED', detail: 'post-quantum: records this statementHash as first seen at ' + new Date(t * 1000).toISOString() + ' (' + t + ')' }] }));\n`); + // un verificator AIP-23 care da finalitatea FAILED din cauza RPC-ului ales de VERIFICATOR (un antet servit gresit), ca verify-proof.mjs real (R5-03) + const fStubF = path.join(F5, 'verify-stub-finalitate-picata-5.mjs'); + fs.writeFileSync(fStubF, `import fs from 'node:fs'; const e = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); +console.log(JSON.stringify({ verdict: 'INVALID', statementHash: e.statementHash, levels: [{ level: 'form', state: 'PASSED', detail: 'x' }, { level: 'integrity', state: 'PASSED', detail: 'x' }, { level: 'signature', state: 'PASSED', detail: 'x' }, + { level: 'finality', state: 'FAILED', detail: 'the header the endpoint served for block 9 does not hash to the hash anchor 10s certificate signed' }] }));\n`); + const verifica = (fl, fp, anc, extra, { stub = fStub5, env = {} } = {}) => ruleazaAsincron([CLI, 'verify', '--ledger', fl, '--policy', fp, ...(anc ? ['--anchors', anc] : []), '--rpc', 'http://127.0.0.1:9', '--chain', '28001', '--verify-proof', stub, ...extra], { AERE_VERIFY_PROOF: '', ...env }); + const cade = async () => ({ ok: false, cod: 'unfunded', motiv: 'unfunded' }); + const acum = Math.floor(Date.now() / 1000); + const st = (mm, h) => mm.stareaCapului(h.corp.id).corp; + // fara exceptie: nicio linie "agent: " (asa iese main pe o exceptie, cu 1) si niciun TypeError al unei conversii + const faraExceptie = (r) => !/^agent: /m.test(r.stderr) && !/Cannot (read|convert)/.test(r.stderr); + const ultim = (r) => `${r.status} ${r.stderr.trim().split('\n').slice(-1)[0].slice(0, 170)}`; + const adauga = (id, p, L, t) => { const R = resumeLedger({ identity: id, policy: p.policy, policyHash: p.policyHash, ledger: L.export(), now: () => t }); R.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); return R; }; + + // ---- 13.R5-01: un NEMASURAT nu mai iese INAINTEA judecatii: o antedatare dovedita de chitantele verificate ramane FAILED oricate dovezi nemasurate + // stau langa ea (un cap proaspat in asteptare, un {"status":"pending"} in --anchors, copia veche a unei chitante); si o ramura aratata de lista, la fel + { + const T0 = acum - 4000, C2 = acum - 400, C4 = acum - 30; let w = T0 * 1000; + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r5-01-')), { ceas: () => w }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + try { + // Z: intrarea 3 scrisa dupa capul 2 si declarata la T0+30 (antedatata); Q: acelasi drum, cinstit; R: ramura, aratata verificatorului altfel + const Z = newAgentIdentity(), Q = newAgentIdentity(), R = newAgentIdentity(); const pZ = pol(Z), pQ = pol(Q), pR = pol(R); + const LZ = registru(Z, pZ, [T0, T0 + 5, T0 + 10]), LQ = registru(Q, pQ, [T0, T0 + 5, T0 + 10]), LR = registru(R, pR, [T0, T0 + 5, T0 + 10]); + const h0 = [[Z, LZ], [Q, LQ], [R, LR]].map(([id, L]) => mm.primeste(M.cerereMartor(L.export(), id, { seq: 0, createdAt: iso(w) }))); + await tur(mm, { notarizeaza: notarMock(T0 + 60) }); + w = C2 * 1000; const h2 = [[Z, LZ], [Q, LQ], [R, LR]].map(([id, L]) => mm.primeste(M.cerereMartor(L.export(), id, { seq: 2, previous: 0, createdAt: iso(w) }))); + await tur(mm, { notarizeaza: cade }); // capul de jurnal facut, notarizarea cazuta: chitantele lui seq 2 sunt "checkpointed" (copiile VECHI) + const vechi2 = h2.map((h) => clon(st(mm, h))); + await tur(mm, { notarizeaza: notarMock(C2 + 60) }); + const LZ4 = adauga(Z, pZ, adauga(Z, pZ, LZ, T0 + 30), acum - 50), LQ4 = adauga(Q, pQ, adauga(Q, pQ, LQ, C2 + 10), acum - 50), LR4 = adauga(R, pR, adauga(R, pR, LR, C2 + 10), acum - 50); + w = C4 * 1000; const h4 = [[Z, LZ4], [Q, LQ4], [R, LR4]].map(([id, L]) => mm.primeste(M.cerereMartor(L.export(), id, { seq: 4, previous: 2, createdAt: iso(w) }))); + w = Date.now(); // capetele seq 4: primite, inca fara cap de jurnal (pending) + const LRalt = registru(R, pR, [T0, T0 + 5, T0 + 12]); // registrul aratat verificatorului de R: alta intrare 2 decat cea pe care martorul o tine + const fpZ = scrie('r5-01-pZ.json', pZ), flZ = scrie('r5-01-lZ.json', LZ4.export()), fpQ = scrie('r5-01-pQ.json', pQ), flQ = scrie('r5-01-lQ.json', LQ4.export()); + const fpR = scrie('r5-01-pR.json', pR), flR = scrie('r5-01-lR.json', LRalt.export()); + const PEND = { status: 'pending' }; + const fZ0 = scrie('r5-01-z0.json', [st(mm, h0[0])]), fZ02p = scrie('r5-01-z02p.json', [st(mm, h0[0]), st(mm, h2[0]), PEND]), fZ0p = scrie('r5-01-z0p.json', [st(mm, h0[0]), PEND]); + const fQ0 = scrie('r5-01-q0.json', [st(mm, h0[1])]), fGol = scrie('r5-01-gol.json', []); + const W = mm.witnessId; + await rand('13.R5-01 capul seq 4 al lui Z inca in asteptare: --witness W --witness-url cu numai chitanta lui 0 -> 1 "backdated" (chitanta lui 2 adusa dovedeste antedatarea; pana la reparatie 2); --witness W fara --witness-url, chitantele 0 si 2 plus un {"status":"pending"} -> 1 (pana la reparatie 2); --witness-url, chitanta lui 0 plus un {"status":"pending"} -> 1; CONTROL cinstit (Q, acelasi drum) -> 2 [adusa] cat capul 4 asteapta', async () => { + const a = await verifica(flZ, fpZ, fZ0, ['--witness', W, '--witness-url', UU]), b = await verifica(flZ, fpZ, fZ02p, ['--witness', W]), c = await verifica(flZ, fpZ, fZ0p, ['--witness', W, '--witness-url', UU]); + const q = await verifica(flQ, fpQ, fQ0, ['--witness', W, '--witness-url', UU]); + return [h0.every((h) => h.cod === 202) && h2.every((h) => h.cod === 202) && h4.every((h) => h.cod === 202) && st(mm, h4[0]).status === 'pending' + && a.status === 1 && /backdated/.test(a.stdout) && b.status === 1 && /backdated/.test(b.stdout) && c.status === 1 && /backdated/.test(c.stdout) && q.status === 2 && /\[adusa\]/.test(q.stderr), + `pending ${a.status} | fara url + pending ${b.status} | url + pending ${c.status} | cinstit ${ultim(q)}`]; + }); + await rand('13.P5-01 CONTROL ramura aratata de lista semnata (martorul tine seq 2 si 4 ale lui R, verificatorului i se arata alta intrare 2), cu capul 4 inca in asteptare -> 1 [registru]', async () => { + const r = await verifica(flR, fpR, fGol, ['--witness', W, '--witness-url', UU]); + return [r.status === 1 && /\[registru\]/.test(r.stderr) && /another branch/.test(r.stderr), ultim(r)]; + }); + await tur(mm, { notarizeaza: notarMock(acum + 60) }); // capetele seq 4 notarizate + const fZ0v = scrie('r5-01-z0v.json', [st(mm, h0[0]), vechi2[0]]), fQ0v = scrie('r5-01-q0v.json', [st(mm, h0[1]), vechi2[1]]); + await rand('13.R5-01 copia VECHE (checkpointed) a chitantei capului 2, pe care martorul il are acum notarizat: Z cu chitanta lui 0 si copia veche -> 1 "backdated" (chitanta notarizata a lui 2 e adusa: numai o chitanta VERIFICATA scuteste aducerea; pana la reparatie 2); Q (cinstit) cu aceeasi forma -> 0 (copia veche nu mai e ceruta); CONTROL Q fara copie -> 0', async () => { + const z = await verifica(flZ, fpZ, fZ0v, ['--witness', W, '--witness-url', UU]), q = await verifica(flQ, fpQ, fQ0v, ['--witness', W, '--witness-url', UU]), q0 = await verifica(flQ, fpQ, fQ0, ['--witness', W, '--witness-url', UU]); + return [vechi2.every((x) => x.status === 'checkpointed') && z.status === 1 && /backdated/.test(z.stdout) && q.status === 0 && /superseded/.test(q.stderr) && q0.status === 0, + `z ${ultim(z)} | q ${ultim(q)} | q fara copie ${q0.status}`]; + }); + } finally { await sv.close(); mm.inchide(); } + } + + // ---- 13.R5-01 (ramura prin predecesor): martorul mai tine numai seq 1 si 2 ale lui B; capul 1 numeste ca predecesor semnat un seq 0 strain (ciobul + // lui scos), deci chitanta lui 1, adusa si verificata, arata ramura; capul 2 e inca in asteptare. Pe forma veche capul in asteptare iesea cu 2 inaintea + // judecatii chitantelor aduse + { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r5-01b-')), { maxFrunze: 4, faraLant: true, ceas: () => Date.now() }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + const capNouIn = (m2) => { const id = newAgentIdentity(); return m2.primeste(M.cerereMartor(registruBrut(id, 1), id, { createdAt: iso(Date.now()) })); }; + try { + const Yb = newAgentIdentity(); const pYb = pol(Yb); const E = registru(Yb, pYb, [acum - 20, acum - 10, acum - 5]).export(); const h0 = hx(); + const cu = (n) => ({ ...E, entries: [{ seq: 0, hash: h0 }, ...E.entries.slice(1, n)] }); + const c0 = mm.primeste(M.cerereMartor(cu(1), Yb, { seq: 0, createdAt: iso(Date.now()) })).cod; + for (let i = 0; i < 3; i++) capNouIn(mm); + const c1 = mm.primeste(M.cerereMartor(cu(2), Yb, { seq: 1, previous: 0, createdAt: iso(Date.now()) })).cod; + for (let i = 0; i < 4; i++) capNouIn(mm); // ciobul 1 plin, rotirea in 2 scoate ciobul 0: martorul mai tine numai seq 1 + await tur(mm, { notarizeaza: notarMock(acum + 60) }); + const c2 = mm.primeste(M.cerereMartor(cu(3), Yb, { seq: 2, previous: 1, createdAt: iso(Date.now()) })); // capul 2: pending + await rand('13.R5-01 ramura aratata de PREDECESORUL semnat al capului tinut seq 1 (seq 0 strain, ciobul lui scos), cu capul 2 inca in asteptare: --witness-url -> 1 [registru] (chitanta lui 1, adusa si verificata, arata ramura; pana la reparatie 2: capul in asteptare iesea inaintea ei)', async () => { + const r = await verifica(scrie('r5-01b-b.json', E), scrie('r5-01b-policy.json', pYb), null, ['--witness-url', UU]); + return [c0 === 202 && c1 === 202 && c2.cod === 202 && c2.corp.status === 'pending' && r.status === 1 && /\[registru\]/.test(r.stderr) && /another branch/.test(r.stderr), `${c0} ${c1} ${c2.cod} ${c2.corp && c2.corp.status} | ${ultim(r)}`]; + }); + } finally { await sv.close(); mm.inchide(); } + } + + // ---- 13.R5-02: o lista semnata de ALT martor decat cel fixat (alta adresa, un proxy fara cheie care re-semneaza pagina reala) si o ancora simpla + // langa chitante, sub un martor fixat, nu sunt dovezi impotriva registrului: NOT MEASURED (2), nu FAILED + { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r5-02-')), { ceas: () => Date.now() }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + const mX = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r5-02x-')), { ceas: () => Date.now() }); const svX = await porneste(mX, { limite: LIMITE }); const UX = `http://127.0.0.1:${svX.port}`; + const kImp = M.cheiaMartorului(fs.mkdtempSync(path.join(T, 'r5-02k-'))); + const resemn = await serverLocal(async (req, res, u) => { + const r = await fetch(UU + u.pathname + u.search, { headers: { 'x-real-ip': 'r5-02-proxy' } }); const j = await r.json(); + if (/\/heads$/.test(u.pathname) && j && j.signedPage) j.signedPage = paginaSemnataDe(kImp, { ...j.signedPage.statement, witness: kImp.witnessId }); + trimite(res, r.status, j); + }); + try { + const Y = newAgentIdentity(); const pY = pol(Y); const L = registru(Y, pY, [acum - 30]); + const h0 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 0, createdAt: iso(Date.now()) })); await tur(mm, { notarizeaza: notarMock(acum + 60) }); + const fp = scrie('r5-02-p.json', pY), fl = scrie('r5-02-l.json', L.export()), f0 = scrie('r5-02-r0.json', [st(mm, h0)]); + const fS = scrie('r5-02-rs.json', [st(mm, h0), { seq: 0, hash: L.export().entries[0].hash, at: acum - 30 }]), fS1 = scrie('r5-02-s.json', [{ seq: 0, hash: L.export().entries[0].hash, at: acum - 30 }]); + const W = mm.witnessId; + await rand('13.R5-02 registru CINSTIT, --witness W: --witness-url catre ALT martor (care nu tine nimic) -> 2 [martor] pe lista; un proxy fara cheia lui W care re-semneaza pagina reala cu cheia lui -> 2 [martor] (pana la reparatie 1 amandoua); CONTROL --witness-url W -> 0', async () => { + const x = await verifica(fl, fp, f0, ['--witness', W, '--witness-url', UX]), p = await verifica(fl, fp, f0, ['--witness', W, '--witness-url', resemn.url]), c = await verifica(fl, fp, f0, ['--witness', W, '--witness-url', UU]); + return [h0.cod === 202 && x.status === 2 && /list: \[martor\]/.test(x.stderr) && p.status === 2 && /list: \[martor\]/.test(p.stderr) && c.status === 0, `alt martor ${ultim(x)} | proxy ${ultim(p)} | W ${c.status}`]; + }); + await rand('13.R5-02 --witness W cu chitanta lui 0 si o ancora simpla ADEVARATA langa ea -> 2 [martor] pe ancora (pana la reparatie 1); numai ancora simpla -> 2; CONTROL fara nicio fixare, aceleasi doua -> 0', async () => { + const a = await verifica(fl, fp, fS, ['--witness', W]), b = await verifica(fl, fp, fS1, ['--witness', W]), c = await verifica(fl, fp, fS, []); + return [a.status === 2 && /anchors\[1\]: \[martor\] a plain anchor/.test(a.stderr) && b.status === 2 && /anchors\[0\]: \[martor\] a plain anchor/.test(b.stderr) && c.status === 0, `ancora + chitanta ${ultim(a)} | ancora ${ultim(b)} | nefixat ${c.status}`]; + }); + } finally { await resemn.close(); await sv.close(); await svX.close(); mm.inchide(); mX.inchide(); } + } + + // ---- 13.R5-03: o chitanta a martorului fixat, legata de intrarile registrului, care nu tine din vina notarului sau a RPC-ului ales de verificator + // (intarziere, finalitate FAILED) e NEMASURATA pe ORICE drum: predata de agent la fel ca adusa de la --witness-url + { + const T0 = acum - 7000, T1 = acum - 3700; let w = T0 * 1000; + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r5-03-')), { ceas: () => w }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + try { + const Y = newAgentIdentity(); const pY = pol(Y); const L = registru(Y, pY, [T0, T0 + 5, T1 - 10]); + const h0 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 0, createdAt: iso(w) })); await tur(mm, { notarizeaza: notarMock(T0 + 30) }); + w = T1 * 1000; const h2 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 2, previous: 0, createdAt: iso(w) })); + await tur(mm, { notarizeaza: cade }); await tur(mm, { notarizeaza: notarMock(T1 + 3600) }); // o pana a notarului: capul 2 notarizat la 3600 s + w = Date.now(); + const fp = scrie('r5-03-p.json', pY), fl = scrie('r5-03-l.json', L.export()), f02 = scrie('r5-03-r02.json', [st(mm, h0), st(mm, h2)]), f0 = scrie('r5-03-r0.json', [st(mm, h0)]); + const W = mm.witnessId; + await rand('13.R5-03 registru CINSTIT, capul 2 notarizat la 3600 s dupa primire: agentul preda chitantele 0 si 2 -> 2 [intarziere] cu --witness W si fara --witness-url, si tot 2 cu --witness-url (pana la reparatie 1 amandoua); chitanta lui 2 adusa -> 2', async () => { + const a = await verifica(fl, fp, f02, ['--witness', W]), b = await verifica(fl, fp, f02, ['--witness', W, '--witness-url', UU]), c = await verifica(fl, fp, f0, ['--witness', W, '--witness-url', UU]); + return [h0.cod === 202 && h2.cod === 202 && [a, b, c].every((r) => r.status === 2 && /\[intarziere\]/.test(r.stderr) && /NOT MEASURED/.test(r.stderr)), `predata ${ultim(a)} | predata, url ${b.status} | adusa ${c.status}`]; + }); + await rand('13.R5-03 finalitatea FAILED data de verificatorul AIP-23 din cauza RPC-ului ales de verificator: chitanta lui 0 predata -> 2 [lant] (pana la reparatie 1); adusa de la --witness-url -> 2', async () => { + const a = await verifica(fl, fp, f0, ['--witness', W], { stub: fStubF }), b = await verifica(fl, fp, scrie('r5-03-gol.json', []), ['--witness', W, '--witness-url', UU], { stub: fStubF }); + return [a.status === 2 && /\[lant\] finality FAILED/.test(a.stderr) && b.status === 2 && /\[lant\]/.test(b.stderr), `predata ${ultim(a)} | adusa ${ultim(b)}`]; + }); + } finally { await sv.close(); mm.inchide(); } + } + + // ---- 13.R5-04: regula (c): tipurile se judeca INAINTE de orice folosire; nicio forma de raspuns (un numar negativ, un obiect {"toString":0} in + // locul unui sir, in chitanta, in pagina listei, in `status`, in raspunsul lui submit) nu mai arunca: NOT MEASURED (2), cu motivul numit + { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r5-04-')), { ceas: () => Date.now() }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + try { + const Y = newAgentIdentity(); const pY = pol(Y); const L = registru(Y, pY, [acum - 30, acum - 20, acum - 10]); + const h0 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 0, createdAt: iso(Date.now()) })), h2 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 2, previous: 0, createdAt: iso(Date.now()) })); + await tur(mm, { notarizeaza: notarMock(acum + 60) }); + const fp = scrie('r5-04-p.json', pY), fl = scrie('r5-04-l.json', L.export()), f0 = scrie('r5-04-r0.json', [st(mm, h0)]); const W = mm.witnessId; + const OBJ = { toString: 0 }; // in JSON {"toString":0}: String() si un sablon de text pe el arunca TypeError + const proxy = (strica, stricaPagina = null) => serverLocal(async (req, res, u) => { + const r = await fetch(UU + u.pathname + u.search, { headers: { 'x-real-ip': 'r5-04-proxy' } }); let j = await r.json(); + if (strica && u.pathname.endsWith(h2.corp.id)) j = strica(clon(j)); + if (stricaPagina && /\/heads$/.test(u.pathname)) j = stricaPagina(clon(j)); + trimite(res, r.status, j); + }); + const cazuri = [['primire-negativa', (j) => { j.receipt.witnessReceipt.receivedAtMs = -1; return j; }, null, 'chitanta-forma'], ['leafHash-obiect', (j) => { j.receipt.witnessReceipt.leafHash = OBJ; return j; }, null, 'chitanta-forma'], + ['drum-obiect', (j) => { j.receipt.witnessReceipt.path = [OBJ]; return j; }, null, 'chitanta-forma'], ['statementHash-obiect', (j) => { j.receipt.statementHash = OBJ; return j; }, null, 'punct-integritate'], + ['lant-obiect', (j) => { j.receipt.notarization.chainId = OBJ; return j; }, null, 'lant'], ['status-obiect-fara-chitanta', () => ({ status: OBJ }), null, 'adusa'], + ['pagina-hash-obiect', null, (j) => { j.signedPage.statement.heads[0].hash = OBJ; return j; }, null]]; + await rand('13.R5-04 un proxy la --witness-url (W fixat, registru CINSTIT) pune receivedAtMs -1, un {"toString":0} in leafHash, in drum, in statementHash, in chainId-ul notarizarii, in status (fara chitanta) sau in hash-ul unui cap din pagina listei -> 2 de fiecare data, cu motivul numit si FARA exceptie (pana la reparatie 1, printr-o exceptie)', async () => { + const rez = []; + for (const [nume, f, fpag, regula] of cazuri) { + const p = await proxy(f, fpag); const r = await verifica(fl, fp, f0, ['--witness', W, '--witness-url', p.url]); await p.close(); + rez.push(`${nume}:${r.status}${faraExceptie(r) ? '' : '(exceptie)'}${regula && !r.stderr.includes('[' + regula + ']') ? '(fara [' + regula + '])' : ''}`); + } + return [h0.cod === 202 && h2.cod === 202 && rez.every((x) => /^[A-Za-z-]+:2$/.test(x)), rez.join(',')]; + }); + const neg = clon(st(mm, h0)); neg.receipt.witnessReceipt.receivedAtMs = -1; const fNeg = scrie('r5-04-neg.json', [neg]); + const fForme = scrie('r5-04-forme.json', [OBJ, { seq: OBJ, hash: L.export().entries[0].hash, at: acum - 30 }, 'text', 7, null]); + await rand('13.R5-04 agentul preda chitanta lui 0 cu receivedAtMs -1: --witness W -> 2 [chitanta-forma] fara exceptie (pana la reparatie 1, exceptie); cu --witness-url -> 0 (chitanta lui 0, adusa si verificata, tine locul copiei stricate); --anchors cu {"toString":0}, o ancora cu seq {"toString":0}, un sir, un numar, null -> 2 [forma] fara exceptie', async () => { + const a = await verifica(fl, fp, fNeg, ['--witness', W]), b = await verifica(fl, fp, fNeg, ['--witness', W, '--witness-url', UU]), c = await verifica(fl, fp, fForme, []); + return [a.status === 2 && /\[chitanta-forma\]/.test(a.stderr) && faraExceptie(a) && b.status === 0 && c.status === 2 && (c.stderr.match(/\[forma\]/g) || []).length === 5 && faraExceptie(c), + `predata ${ultim(a)} | predata, url ${ultim(b)} | forme ${ultim(c)}`]; + }); + } finally { await sv.close(); mm.inchide(); } + const MA_ = path.join(AICI, 'martor-agenti.mjs'); + await rand('13.R5-04 submit: un 202 JSON cu id-ul, sesiunea si pozitia capului trimis, dar status {"toString":0}, sau cu id {"toString":0} -> 2, --out nescris, fara exceptie (pana la reparatie 1: --out scris, apoi exceptie)', async () => { + const id = newAgentIdentity(); const L = registruBrut(id, 1); const fk = path.join(F5, 'r5-04-agent.key.pem'); fs.writeFileSync(fk, id.privateKey.export({ type: 'pkcs8', format: 'pem' })); const fl = scrie('r5-04-sub.json', L); + const rez = []; + for (const [nume, f] of [['status', (env) => ({ status: { toString: 0 }, id: env.statementHash, session: env.statement.session, seq: env.statement.seq })], ['id', (env) => ({ status: 'pending', id: { toString: 0 }, session: env.statement.session, seq: env.statement.seq })]]) { + const s = await serverLocal(async (req, res, u) => { + if (/\/heads$/.test(u.pathname)) return trimite(res, 200, { latest: null, heads: [], next: null }); + let b = ''; for await (const c of req) b += c; return trimite(res, 202, f(JSON.parse(b))); + }); + const fo = path.join(F5, `r5-04-out-${nume}.json`); const r = await ruleazaAsincron([MA_, 'submit', '--key', fk, '--ledger', fl, '--url', s.url, '--out', fo]); await s.close(); + rez.push(`${nume}:${r.status}:${fs.existsSync(fo) ? 'scris' : 'nescris'}${/Cannot (read|convert)/.test(r.stderr) ? '(exceptie)' : ''}`); + } + return [rez.join(',') === 'status:2:nescris,id:2:nescris', rez.join(',')]; + }); + } + + // ---- 13.R5-05: ordinea in care vin dovezile nu schimba verdictul. O chitanta a AGENTULUI cu semnatura martorului fixat INVALIDA, sau a martorului + // fixat pentru alt registru, e vina agentului (1) oricat de devreme vine un cap in asteptare; una cu includerea stricata (semnatura martorului + // valida) e NEMASURATA (2) singura sau langa un cap in asteptare, in ambele ordini + { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r5-05-')), { ceas: () => Date.now() }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + try { + const Y = newAgentIdentity(); const pY = pol(Y); const L = registru(Y, pY, [acum - 30, acum - 20]); + const Y2 = newAgentIdentity(); const pY2 = pol(Y2); const L2 = registru(Y2, pY2, [acum - 30]); + const h0 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 0, createdAt: iso(Date.now()) })), g0 = mm.primeste(M.cerereMartor(L2.export(), Y2, { seq: 0, createdAt: iso(Date.now()) })); + await tur(mm, { notarizeaza: notarMock(acum + 60) }); + const h1 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 1, previous: 0, createdAt: iso(Date.now()) })); // capul 1: pending + const fp = scrie('r5-05-p.json', pY), fl = scrie('r5-05-l.json', L.export()); const W = mm.witnessId; const PEND = { status: 'pending' }; + const inc = clon(st(mm, h0)); inc.receipt.witnessReceipt.path = ['0x' + '00'.repeat(32)]; // includerea stricata, semnatura martorului neatinsa + const fals = clon(st(mm, h0)); fals.receipt.statement.root = '0x' + '77'.repeat(32); fals.receipt.statementHash = sha(JSON.stringify(fals.receipt.statement)); // semnatura lui W nu mai verifica + const alt = st(mm, g0); // chitanta martorului fixat, a altui registru + const v = (anc, extra) => verifica(fl, fp, scrie(`r5-05-${anc.length}-${crypto.randomBytes(3).toString('hex')}.json`, anc), ['--witness', W, ...extra]); + await rand('13.R5-05 chitanta agentului cu includerea stricata (semnatura lui W valida): singura -> 2 [includere] (pana la reparatie 1); langa un {"status":"pending"}, in ambele ordini -> 2; cu --witness-url si capul 1 in asteptare -> 2', async () => { + const a = await v([inc], []), b = await v([inc, PEND], []), c = await v([PEND, inc], []), d = await v([inc], ['--witness-url', UU]); + return [h1.cod === 202 && [a, b, c, d].every((r) => r.status === 2 && /\[includere\]/.test(r.stderr)), `${ultim(a)} | ${b.status} | ${c.status} | ${ultim(d)}`]; + }); + await rand('13.P5-05 CONTROL vina agentului: chitanta lui 0 cu semnatura lui W INVALIDA (capul de jurnal schimbat, hash refacut) -> 1 [punct-semnatura] singura, langa un {"status":"pending"} in ambele ordini, si cu --witness-url (capul 1 in asteptare); chitanta lui W pentru ALT registru -> 1 [registru] la fel', async () => { + const r = [await v([fals], []), await v([fals, PEND], []), await v([PEND, fals], []), await v([fals], ['--witness-url', UU]), await v([alt], []), await v([PEND, alt], []), await v([alt], ['--witness-url', UU])]; + return [r.slice(0, 4).every((x) => x.status === 1 && /\[punct-semnatura\]/.test(x.stderr)) && r.slice(4).every((x) => x.status === 1 && /\[registru\]/.test(x.stderr)), r.map((x) => x.status).join(',') + ' | ' + ultim(r[3])]; + }); + // un proxy la --witness-url care serveste aceeasi chitanta falsificata (semnatura lui W invalida) pentru capul 0: nu e vina registrului + const pf = await serverLocal(async (req, res, u) => { const r = await fetch(UU + u.pathname + u.search, { headers: { 'x-real-ip': 'r5-05-proxy' } }); let j = await r.json(); if (u.pathname.endsWith(h0.corp.id)) j = { ...j, receipt: fals.receipt }; trimite(res, r.status, j); }); + await rand('13.P5-05 CONTROL un proxy fara cheia lui W serveste, la --witness-url, chitanta capului 0 cu semnatura lui W invalida -> 2 [punct-semnatura] (o adresa nu poate face un FAILED), nu 1', async () => { + const r = await verifica(fl, fp, scrie('r5-05-gol.json', []), ['--witness', W, '--witness-url', pf.url]); await pf.close(); + return [r.status === 2 && /\[punct-semnatura\]/.test(r.stderr), ultim(r)]; + }); + } finally { await sv.close(); mm.inchide(); } + } + + // ---- 13.R5-06: cate o chitanta adusa pe cap tinut, sub limita de citiri a serviciului: pe un 429 verificatorul asteapta cat spune Retry-After, + // intr-un buget comun (AERE_WITNESS_WAIT_MAX_S, implicit 900 s); peste buget, NOT MEASURED numit (HTTP 429 si cat s-a asteptat) + { + const LIM = { postPeClient: { cereri: 4000, ms: 600_000 }, postPeAgent: { cereri: 4000, ms: 600_000 }, getPeClient: { cereri: 4, ms: 1000 } }; + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r5-06-')), { ceas: () => Date.now() }); const sv = await porneste(mm, { limite: LIM }); const UU = `http://127.0.0.1:${sv.port}`; + try { + const N = 12; const Y = newAgentIdentity(); const pY = definePolicy({ agentId: Y.agentId, spend: { amount: '100000000', windowSeconds: 3600 }, recipients: ['0xbbbb'] }); + let t = acum - 200; const L = openLedger({ identity: Y, policy: pY.policy, policyHash: pY.policyHash, now: () => t }); + for (let i = 0; i < N; i++) { t = acum - 200 + i * 5; L.record({ kind: 'payment', to: '0xbbbb', amount: '1' }); } + const E = L.export(); let prev = null, primite = 0; + for (let i = 0; i < N; i++) { if (mm.primeste(M.cerereMartor(E, Y, { seq: i, previous: prev, createdAt: iso(Date.now()) })).cod === 202) primite++; prev = i; } + await tur(mm, { notarizeaza: notarMock(acum + 60) }); + const fp = scrie('r5-06-p.json', pY), fl = scrie('r5-06-l.json', E), fGol = scrie('r5-06-gol.json', []); + // un client care citeste de 12 ori: al cincilea raspuns e 429, cu Retry-After + let ra = null; for (let i = 0; i < 12; i++) { const r = await fetch(`${UU}/v1/agents/witness/health`, { headers: { 'x-real-ip': 'r5-06-numarator' } }); if (r.status === 429 && ra === null) ra = r.headers.get('retry-after'); await r.arrayBuffer(); } + await rand(`13.R5-06 serviciul cu 4 citiri pe client in 1 s, un registru CINSTIT cu ${N} capete tinute, --witness-url fara nicio chitanta a agentului -> 0, dupa ce verificatorul a asteptat cat a cerut Retry-After (pana la reparatie 2, "HTTP 429"); 429-ul unei citiri poarta Retry-After`, async () => { + const r = await verifica(fl, fp, fGol, ['--witness', mm.witnessId, '--witness-url', UU]); + return [primite === N && /^\d+$/.test(String(ra)) && Number(ra) === 1 && r.status === 0 && /rate-limited this verification \(HTTP 429\): \d+ s waited/.test(r.stderr), `primite ${primite}, Retry-After ${ra} | ${ultim(r)}`]; + }); + await rand('13.P5-06 CONTROL bugetul: acelasi registru cu AERE_WITNESS_WAIT_MAX_S=0 -> 2 NOT MEASURED numit (HTTP 429, Retry-After, bugetul); AERE_WITNESS_WAIT_MAX_S=abc -> 2 folosire gresita', async () => { + await new Promise((g) => setTimeout(g, 1100)); // o fereastra noua: lista intra, iar al patrulea cap adus primeste 429 + const r = await verifica(fl, fp, fGol, ['--witness', mm.witnessId, '--witness-url', UU], { env: { AERE_WITNESS_WAIT_MAX_S: '0' } }); + const u = await verifica(fl, fp, fGol, ['--witness', mm.witnessId, '--witness-url', UU], { env: { AERE_WITNESS_WAIT_MAX_S: 'abc' } }); + return [r.status === 2 && /HTTP 429 \(Retry-After \d+ s; 0 s already waited of a budget of 0 s/.test(r.stderr) && /NOT MEASURED/.test(r.stderr) && u.status === 2 && /AERE_WITNESS_WAIT_MAX_S must be/.test(u.stderr), `${ultim(r)} | ${ultim(u)}`]; + }); + } finally { await sv.close(); mm.inchide(); } + } +} + +// ================================================================================================ 14. a sasea revizie B3 +// (2026-10-19, verificarea tintita a reparatiilor R5: N1..N4): randurile "14.N", RAU pe forma de dinainte (786aa0038). N4 (cifrele din README) +// nu are rand: se masoara rulind proba si controlul. +async function revizia6() { + const fStub6 = path.join(F5, 'verify-stub-notar-6.mjs'); // verificatorul AIP-23 inlocuit: momentul din notarizarea plicului (notarul-mock) + fs.writeFileSync(fStub6, `import fs from 'node:fs'; const e = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); const t = Number(e.notarization && e.notarization.firstSeen); +console.log(JSON.stringify({ verdict: 'VALID', statementHash: e.statementHash, levels: [{ level: 'form', state: 'PASSED', detail: 'x' }, { level: 'integrity', state: 'PASSED', detail: 'x' }, { level: 'signature', state: 'PASSED', detail: 'x' }, + { level: 'finality', state: 'PASSED', detail: 'post-quantum: records this statementHash as first seen at ' + new Date(t * 1000).toISOString() + ' (' + t + ')' }] }));\n`); + const MA_ = path.join(AICI, 'martor-agenti.mjs'); + const acum = Math.floor(Date.now() / 1000); + const ultim = (r) => `${r.status} ${r.stderr.trim().split('\n').slice(-1)[0].slice(0, 170)}`; + const adauga = (id, p, L, t) => { const R = resumeLedger({ identity: id, policy: p.policy, policyHash: p.policyHash, ledger: L.export(), now: () => t }); R.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); return R; }; + + // ---- 14.N1: o chitanta din --anchors fara --rpc nu mai iese cu 2 (folosire gresita) INAINTEA listei semnate: e o dovada NEMASURATA [rpc], iar o ramura + // aratata de lista martorului fixat ramane FAILED (pana la reparatie, orice obiect cu forma de chitanta pus de agent in --anchors o ascundea) + { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r6-n1-')), { ceas: () => Date.now() }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + try { + const Y = newAgentIdentity(); const pY = pol(Y); const L0 = registru(Y, pY, [acum - 30]); + const A = adauga(Y, pY, L0, acum - 20), B = adauga(Y, pY, L0, acum - 15); // A: registrul tinut de martor; B: alta intrare 1, aratata verificatorului + const h0 = mm.primeste(M.cerereMartor(A.export(), Y, { seq: 0, createdAt: iso(Date.now()) })); + const h1 = mm.primeste(M.cerereMartor(A.export(), Y, { seq: 1, previous: 0, createdAt: iso(Date.now()) })); + await tur(mm, { notarizeaza: notarMock(acum + 60) }); + const W = mm.witnessId; const fp = scrie('r6-n1-p.json', pY), fA = scrie('r6-n1-a.json', A.export()), fB = scrie('r6-n1-b.json', B.export()); + const f0 = scrie('r6-n1-r0.json', [mm.stareaCapului(h0.corp.id).corp]), fForma = scrie('r6-n1-forma.json', [{ witnessReceipt: {} }]); + const v = (fl, anc, extra) => ruleazaAsincron([CLI, 'verify', '--ledger', fl, '--policy', fp, '--anchors', anc, '--chain', '28001', '--verify-proof', fStub6, ...extra], { AERE_VERIFY_PROOF: '' }); + const folosire = (r) => /^agent: .*needs --rpc/m.test(r.stderr); + await rand('14.N1 fara --rpc, ramura aratata de lista semnata (martorul tine seq 0 si 1 ale lui A, verificatorului i se arata B): --anchors [chitanta adevarata a lui 0] -> 1 [registru], chitanta NEMASURATA [rpc] (pana la reparatie 2, folosire gresita, inaintea listei); --anchors [{"witnessReceipt":{}}] -> 1; CONTROL registrul CINSTIT A cu chitanta lui 0, --witness W, fara --rpc -> 2 NOT MEASURED [rpc], nu folosire gresita; cu --rpc, ramura -> 1', async () => { + const a = await v(fB, f0, ['--witness', W, '--witness-url', UU]), b = await v(fB, fForma, ['--witness', W, '--witness-url', UU]), c = await v(fA, f0, ['--witness', W]); + const d = await v(fB, f0, ['--witness', W, '--witness-url', UU, '--rpc', 'http://127.0.0.1:9']); + return [h0.cod === 202 && h1.cod === 202 && a.status === 1 && /\[registru\]/.test(a.stderr) && /anchors\[0\]: \[rpc\]/.test(a.stderr) && !folosire(a) && b.status === 1 && /\[registru\]/.test(b.stderr) + && c.status === 2 && /anchors\[0\]: \[rpc\]/.test(c.stderr) && /^NOT MEASURED/m.test(c.stderr) && !folosire(c) && d.status === 1 && /\[registru\]/.test(d.stderr), + `ramura+chitanta ${ultim(a)} | ramura+forma ${b.status} | cinstit ${ultim(c)} | cu --rpc ${d.status}`]; + }); + } finally { await sv.close(); mm.inchide(); } + } + + // ---- 14.N2: `receipt` scrie --out NUMAI daca raspunsul e chitanta capului cerut (--id), a registrului dat (--ledger), cu semnatura martorului verificata + // (cu --witness, daca e dat); altfel 2 si nimic scris. Pana la reparatie un proxy fara cheie la --url dadea agentului cinstit chitanta adevarata a capului + // ALTUI agent sau una cu semnatura rupta, receipt iesea 0 si scria, iar fisierul predat iesea 1 la verify (din vina adresei) + { + const mm = M.deschideMartor(fs.mkdtempSync(path.join(T, 'r6-n2-')), { ceas: () => Date.now() }); const sv = await porneste(mm, { limite: LIMITE }); const UU = `http://127.0.0.1:${sv.port}`; + try { + const Y = newAgentIdentity(); const pY = pol(Y); const L = registru(Y, pY, [acum - 30, acum - 20]); + const Z = newAgentIdentity(); const pZ = pol(Z); const LZ = registru(Z, pZ, [acum - 30]); + const y0 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 0, createdAt: iso(Date.now()) })); + const y1 = mm.primeste(M.cerereMartor(L.export(), Y, { seq: 1, previous: 0, createdAt: iso(Date.now()) })); + const z0 = mm.primeste(M.cerereMartor(LZ.export(), Z, { seq: 0, createdAt: iso(Date.now()) })); + await tur(mm, { notarizeaza: notarMock(acum + 60) }); + const W = mm.witnessId; const id0 = y0.corp.id, id1 = y1.corp.id, idZ = z0.corp.id; + const fl = scrie('r6-n2-l.json', L.export()), fp = scrie('r6-n2-p.json', pY); + // proxy-ul de pe drumul agentului: trece totul, afara de GET-ul capului 1 al lui Y, pe care il inlocuieste + const proxy = (f) => serverLocal(async (req, res, u) => { const r = await fetch(UU + u.pathname + u.search, { headers: { 'x-real-ip': 'r6-n2-proxy' } }); let j = await r.json(); if (u.pathname.endsWith(id1)) j = await f(clon(j)); trimite(res, r.status, j); }); + const stare = async (id) => (await fetch(`${UU}/v1/agents/witness/${id}`, { headers: { 'x-real-ip': 'r6-n2-citire' } })).json(); + const rupta = async (j) => { const b = Buffer.from(j.receipt.signature.signature, 'base64'); b[10] ^= 0xff; j.receipt.signature.signature = b.toString('base64'); return j; }; + let n = 0; const iesire = () => path.join(F5, `r6-n2-out-${n++}.json`); + const receipt = (url, id, extra, out) => ruleazaAsincron([MA_, 'receipt', '--url', url, '--id', id, ...extra, '--out', out]); + await rand('14.N2 receipt printr-un proxy fara cheie la --url (fluxul din README, fara --witness): chitanta ADEVARATA a lui W pentru capul lui Z, semnatura lui W rupta intr-un octet, chitanta capului 0 al lui Y in locul capului 1 cerut -> 2 de fiecare data, cu motivul numit, nimic scris (pana la reparatie 0, scris, si predat iesea 1); CONTROL fara proxy -> 0, scris, "NOT pinned", iar verify --anchors --witness W --witness-url W -> 0', async () => { + const fc = iesire(); const c = await receipt(UU, id1, ['--ledger', fl], fc); + const vc = fs.existsSync(fc) ? await ruleazaAsincron([CLI, 'verify', '--ledger', fl, '--policy', fp, '--anchors', fc, '--rpc', 'http://127.0.0.1:9', '--chain', '28001', '--verify-proof', fStub6, '--witness', W, '--witness-url', UU], { AERE_VERIFY_PROOF: '' }) : { status: -1 }; + const rez = []; + for (const [nume, f, motiv] of [['strain', () => stare(idZ), /receipt of another head/], ['rupta', rupta, /\[punct-semnatura\]/], ['alt-cap', () => stare(id0), /receipt of another head/]]) { + const p = await proxy(f); const fo = iesire(); const r = await receipt(p.url, id1, ['--ledger', fl], fo); await p.close(); + rez.push(`${nume}:${r.status}:${fs.existsSync(fo) ? 'scris' : 'nescris'}${motiv.test(r.stderr) ? '' : '(fara motiv)'}${/Cannot (read|convert)/.test(r.stderr) ? '(exceptie)' : ''}`); + } + return [y0.cod === 202 && y1.cod === 202 && z0.cod === 202 && c.status === 0 && fs.existsSync(fc) && /NOT pinned/.test(c.stderr) && vc.status === 0 && rez.join(',') === 'strain:2:nescris,rupta:2:nescris,alt-cap:2:nescris', + `control ${ultim(c)} | verify ${vc.status} | ${rez.join(',')}`]; + }); + await rand('14.N2 receipt fata de --ledger si --witness: --id al capului lui Z cu registrul lui Y -> 2 [registru], nimic scris; --witness pe chitanta lui W -> 2 [martor], nimic scris; --witness W -> 0 "(pinned)", scris; fara --ledger -> 2 folosire gresita, nimic scris (pana la reparatie --ledger si --witness erau steaguri necunoscute)', async () => { + const fd = iesire(), d = await receipt(UU, idZ, ['--ledger', fl], fd); + const fe = iesire(), e = await receipt(UU, id1, ['--ledger', fl, '--witness', 'aere-witness:' + 'ee'.repeat(20)], fe); + const fw = iesire(), w = await receipt(UU, id1, ['--ledger', fl, '--witness', W], fw); + const ff = iesire(), f = await receipt(UU, id1, [], ff); + return [d.status === 2 && !fs.existsSync(fd) && /\[registru\]/.test(d.stderr) && e.status === 2 && !fs.existsSync(fe) && /\[martor\]/.test(e.stderr) && w.status === 0 && fs.existsSync(fw) && /\(pinned\)/.test(w.stderr) + && f.status === 2 && !fs.existsSync(ff) && /--ledger is required/.test(f.stderr), `alt agent ${ultim(d)} | alt martor ${ultim(e)} | fixat ${ultim(w)} | fara --ledger ${ultim(f)}`]; + }); + } finally { await sv.close(); mm.inchide(); } + } + + // ---- 14.N3: regula (c) pe drumul "nimic nou" al lui submit: `latest` (campul nesemnat al paginii) si starea capului tinut, cu valori de tip gresit, dau 2 + // cu --out nescris, niciodata o exceptie (pana la reparatie: un {"toString":0} in seq sau id arunca, iar un status {"toString":0} scria --out si arunca) + { + const id = newAgentIdentity(); const p = pol(id); const E = registru(id, p, [acum - 10]).export(); + const fk = path.join(F5, 'r6-n3-agent.key.pem'); fs.writeFileSync(fk, id.privateKey.export({ type: 'pkcs8', format: 'pem' })); const fl = scrie('r6-n3-l.json', E); + const X = '0x' + '11'.repeat(32), OBJ = { toString: 0 }, h0 = E.entries[0].hash; + const cazuri = [['control', { seq: 0, hash: h0, id: X }, { id: X, status: 'notarized' }], ['status-obiect', { seq: 0, hash: h0, id: X }, { id: X, status: OBJ }], + ['seq-obiect', { seq: OBJ, hash: h0, id: X }, null], ['id-obiect', { seq: 0, hash: h0, id: OBJ }, null], ['hash-obiect', { seq: 0, hash: OBJ, id: X }, null], ['latest-numar', 5, null]]; + await rand('14.N3 submit pe drumul "nimic nou": latest cu seq, id sau hash {"toString":0}, latest un numar, starea capului cu status {"toString":0} -> 2, --out nescris, fara exceptie (pana la reparatie 1, prin exceptie, iar pe status --out scris); CONTROL latest = intrarea 0 si starea "notarized" -> 0, --out scris', async () => { + const rez = []; + for (const [nume, latest, stareCap] of cazuri) { + const s = await serverLocal(async (req, res, u) => { + if (/\/heads$/.test(u.pathname)) return trimite(res, 200, { latest, heads: [], next: null }); + if (req.method === 'GET' && stareCap) return trimite(res, 200, stareCap); + return trimite(res, 500, { error: 'not in this scenario' }); + }); + const fo = path.join(F5, `r6-n3-out-${nume}.json`); const r = await ruleazaAsincron([MA_, 'submit', '--key', fk, '--ledger', fl, '--url', s.url, '--out', fo]); await s.close(); + rez.push(`${nume}:${r.status}:${fs.existsSync(fo) ? 'scris' : 'nescris'}${/Cannot (read|convert)/.test(r.stderr) ? '(exceptie)' : ''}`); + } + return [rez.join(',') === 'control:0:scris,status-obiect:2:nescris,seq-obiect:2:nescris,id-obiect:2:nescris,hash-obiect:2:nescris,latest-numar:2:nescris', rez.join(',')]; + }); + } +} + +async function testnet() { + const RPC = process.env.AERE_TESTNET_RPC || 'https://testnet-rpc.aere.network'; + if (!VERIFY || !fs.existsSync(VERIFY)) return sari('8. testnetul 28001', 'lipseste verificatorul AIP-23 (AERE_VERIFY_PROOF)'); + let kf = process.env.AERE_TESTNET_KEY_FILE, robinet = null; + if (!kf) { kf = path.join(T, 'cheie-proba-28001.key'); const r = path.resolve(AICI, '..', 'aere-quantum-shield', 'robinet.mjs'); robinet = fs.existsSync(r) ? r : null; if (!robinet) return sari('8. testnetul 28001', 'nici AERE_TESTNET_KEY_FILE, nici clientul robinetului (tools/aere-quantum-shield/robinet.mjs)'); } + const D8 = path.join(T, 'stare-28001'); wMs = Date.now(); + const mt = M.deschideMartor(D8, { ceas: () => Date.now() }); + try { + const id = newAgentIdentity(); const p = pol(id); let t = Math.floor(Date.now() / 1000) - 900; const L = openLedger({ identity: id, policy: p.policy, policyHash: p.policyHash, now: () => t }); + L.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); t += 5; L.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); + const inainte = L.export(); const cap = M.cerereMartor(inainte, id, { seq: 1 }); + const r = mt.primeste(cap); + cer(r.cod === 202, '8. testnet: capul unui registru real primit de martor (202)', JSON.stringify(r.corp).slice(0, 200)); + const tr = await tur(mt, { notarizeaza: notarizatorReal({ rpc: RPC, cheieFisier: kf, robinet }) }); + if (tr.notarized !== 0) { if (/unfunded|chain|RPC|fetch|ECONN|timeout|failed/i.test(String(tr.error))) return sari('8. testnetul 28001', 'notarizarea n-a reusit: ' + tr.error); return cer(false, '8. testnet: capul de jurnal notarizat pe AereNotary 28001', tr.error); } + const st = mt.stareaCapului(cap.statementHash).corp; + cer(st.status === 'notarized' && st.receipt.notarization.chainId === 28001 && st.receipt.notarization.notary === NOTARI[28001] && st.receipt.notarization.txHash, `8. testnet: capul de jurnal notarizat pe AereNotary 28001 (bloc ${st.receipt.notarization.block}, firstSeen ${st.receipt.notarization.firstSeen})`, JSON.stringify(st).slice(0, 300)); + let v = null; const start = Date.now(); + while (Date.now() - start < 420_000) { + const f = path.join(T, 'plic-28001.json'); fs.writeFileSync(f, JSON.stringify(st.receipt)); + const o = spawnSync(process.execPath, [VERIFY, f, '--json', '--rpc', RPC, '--chain', '28001'], { encoding: 'utf8', timeout: 300_000 }); + try { v = JSON.parse(o.stdout); } catch { v = null; } + if (v && v.verdict !== 'PARTIAL') break; + await new Promise((g) => setTimeout(g, 20_000)); + } + if (!v || v.verdict === 'PARTIAL') return sari('8. testnet: plicul martorului VALID cu finalitate post-cuantica', 'finalitatea nu a venit in 7 minute: ' + JSON.stringify(v && v.levels).slice(0, 200)); + const fin = v.levels.find((x) => x.level === 'finality'); + cer(v.verdict === 'VALID' && fin.state === 'PASSED' && v.levels.find((x) => x.level === 'signature').state === 'PASSED', `8. testnet: plicul martorului VALID la verificatorul AIP-23 de referinta: semnatura martorului si finalitatea post-cuantica PASSED (dupa ${Math.round((Date.now() - start) / 1000)} s)`, JSON.stringify(v).slice(0, 300)); + t = Math.floor(Date.now() / 1000); const cinstit = resumeLedger({ identity: id, policy: p.policy, policyHash: p.policyHash, ledger: inainte, now: () => t }); cinstit.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); + t = Math.floor(Date.parse(cap.statement.createdAt) / 1000) - 600; const antedatat = resumeLedger({ identity: id, policy: p.policy, policyHash: p.policyHash, ledger: inainte, now: () => t }); antedatat.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); + const F8 = path.join(T, 'cli-28001'); fs.mkdirSync(F8); + const sc = (n, o) => { const f = path.join(F8, n); fs.writeFileSync(f, JSON.stringify(o)); return f; }; + const fP = sc('policy.json', p), fR = sc('receipt.json', st); + const a1 = spawnSync(process.execPath, [CLI, 'verify', '--ledger', sc('cinstit.json', cinstit.export()), '--policy', fP, '--anchors', fR, '--rpc', RPC, '--chain', '28001', '--verify-proof', VERIFY, '--witness', mt.witnessId], { encoding: 'utf8', timeout: 300_000 }); + const a2 = spawnSync(process.execPath, [CLI, 'verify', '--ledger', sc('antedatat.json', antedatat.export()), '--policy', fP, '--anchors', fR, '--rpc', RPC, '--chain', '28001', '--verify-proof', VERIFY, '--witness', mt.witnessId], { encoding: 'utf8', timeout: 300_000 }); + cer(a1.status === 0 && /"lowerBound": "witnessed"/.test(a1.stdout), '8. testnet: agent-cli verify --anchors --witness pe lantul viu: registrul cinstit -> 0', a1.stdout.slice(0, 200) + a1.stderr); + cer(a2.status === 1 && /backdated/.test(a2.stdout), '8. testnet: registrul antedatat cu aceeasi chitanta -> 1 "backdated"', a2.stdout.slice(0, 200) + a2.stderr); + const DV = path.join(AICI, 'dovezi-martor-agenti-28001'); fs.mkdirSync(DV, { recursive: true }); + const zi = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + fs.writeFileSync(path.join(DV, `martor-agenti-${zi}.json`), JSON.stringify({ v: 1, kind: 'aere-agent-witness-evidence', network: 'eip155:28001', rpc: RPC, witness: mt.witnessId, policy: p, + ledgerBefore: inainte, head: cap, receipt: st.receipt, aip23: { verdict: v.verdict, levels: v.levels }, honest: cinstit.export(), backdated: antedatat.export(), + verdicts: { honestWithReceipt: a1.status, backdatedWithReceipt: a2.status } }, null, 1) + '\n'); + console.log(` (dovada in dovezi-martor-agenti-28001/martor-agenti-${zi}.json; nicio cheie privata)`); + } finally { mt.inchide(); } +} + +console.log(`\nmartor-agenti: ${ok}/${ok + rau} cum trebuia${sarit ? `, ${sarit} sarite (NEMASURAT)` : ''}`); +process.exitCode = rau ? 1 : sarit ? 2 : 0; +// un rand cazut intr-o exceptie poate lasa un server de proba deschis (pe forma veche a componentei, de pilda): procesul iese oricum, cu codul lui +setTimeout(() => process.exit(process.exitCode), 300); diff --git a/conformance/README.md b/conformance/README.md index 9700161..618bf02 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -116,9 +116,9 @@ issuer and for its status at once. ## The vectors -`vectori/` holds 631 vectors (622 offline, 9 that read testnet 28001; 22 MB, since every device, identity and Travel Rule vector carries its ML-DSA-65 keys and signatures), listed with their digests in `vectori/INDEX.json`: -envelope 67, payment-risk 11, payment-receipt 10, market-timeline 26, dataset 22, ai-trust-bundle 24, audit-log 19, audit-inclusion -10, audit-consistency 4, agent-ledger 18, identity-presentation 69, device-bundle 194, sdjwt-presentation 59, sdjwt-vc-presentation 32, travel-rule-binding 19, travel-rule-message 26, travel-rule-receipt 9, compliance-presentation 12. By expected verdict: 89 `VALID`, 70 `PARTIAL`, 472 `INVALID`. None is typed by hand +`vectori/` holds 634 vectors (625 offline, 9 that read testnet 28001; 22 MB, since every device, identity and Travel Rule vector carries its ML-DSA-65 keys and signatures), listed with their digests in `vectori/INDEX.json`: +envelope 70 (since 2026-10-19: 67 plus the three of the new kind `agent-witness-checkpoint`), payment-risk 11, payment-receipt 10, market-timeline 26, dataset 22, ai-trust-bundle 24, audit-log 19, audit-inclusion +10, audit-consistency 4, agent-ledger 18, identity-presentation 69, device-bundle 194, sdjwt-presentation 59, sdjwt-vc-presentation 32, travel-rule-binding 19, travel-rule-message 26, travel-rule-receipt 9, compliance-presentation 12. By expected verdict: 90 `VALID`, 70 `PARTIAL`, 474 `INVALID`. None is typed by hand (`node genereaza-vectori.mjs --verifica` regenerates them and compares byte for byte): - the 17 AIP-23 kinds come from their producers on fixed inputs (the same envelopes as the published AIP-23 kind vectors; `secret-access`, the audit entry of the secrets service, since 2026-10-18); the audit @@ -225,6 +225,18 @@ not implementations of this suite's contract). ## How the suite itself is checked +Measured on 2026-10-19 (UTC), after the AIP-23 kind `agent-witness-checkpoint` was added (the checkpoint of the hosted agents witness, +`agents/martor-agenti.mjs`; three envelope vectors: VALID, its statementHash cut short, one statement field changed; no rule changed). On +Node.js 24.14.1, offline, with the development reference verifier: `node proba-conformance.mjs` gives 12 rows green, 0 red, 1 not measured +(the 9 vectors that read testnet 28001): 241 rules of 15 verifiers, 634 vectors (`LA ZI`), the 18 kind envelopes are the AIP-23 kind +vectors, every INVALID vector (474) is its base with one operation (308) or one declared producer change (166), the published verifiers +pass 625 of 625 offline vectors, the independent Python implementation passes the 68 it claims and reports the other 566 not covered, +101,947 malformed inputs each got a verdict. Not run after this change: `--online` and `--control-negativ`. +Measured again the same day after the kind gained a vocabulary in `proof-kinds/` (the adversarial review of the hosted witness, finding 8; +the valid inputs build byte for byte the same envelopes): `genereaza-vectori.mjs --verifica` `LA ZI` (634 vectors), `proba-conformance.mjs` +12 green, 0 red, 1 not measured (the testnet vectors), and `--control-negativ` 320 green, 0 red, 3 not measured (the three rules whose +vectors need testnet 28001), 238 of 241 rules proven load-bearing. `--online` not run. + Measured on 2026-10-19 (UTC), after the SD-JWT verifier gained the rule `hybrid-half` (AIP-25 finding C1, D-535: with the keys of an AERE hybrid identity the ML-DSA-65 half is judged by default and the Ed25519 half only when the verifier asks for it, `classicalHalf`; until then the Ed25519 half was the default, so a token signed with it alone verified against the whole hybrid identity). Five vectors diff --git a/conformance/genereaza-vectori.mjs b/conformance/genereaza-vectori.mjs index 0650de9..ecb23e2 100644 --- a/conformance/genereaza-vectori.mjs +++ b/conformance/genereaza-vectori.mjs @@ -600,7 +600,9 @@ function intrariKinds() { settlement: { chainId: 2800, txHash: '0x' + '12'.repeat(32), blockHash: '0x' + '34'.repeat(32), from: '0xa', to: '0xb', amount: '100', asset: 'AERE', instruction: 'INSTR', createdAt: AT }, provenance: { subject: 'ARTIFACT', parents: ['SRC-1', 'SRC-2'], process: 'BUILD', actor: 'build-bot', createdAt: AT }, // 2026-10-18 (B8, cloud-gateway/pq-secrets): the same fixed input as the published kind vector (aere-proof-protocol/genereaza-vectori-kinds.mjs) - 'secret-access': { service: 'https://secrets.example', secret: DB, version: 2, operation: 'get', verdict: 'allowed', rules: [], grant: 'deployers', holder: 'aere-id:' + '0'.repeat(40), presenter: 'aere-id:' + '1'.repeat(40), presentation: 'BINDING', policy: 'POLICY', createdAt: AT } }; + 'secret-access': { service: 'https://secrets.example', secret: DB, version: 2, operation: 'get', verdict: 'allowed', rules: [], grant: 'deployers', holder: 'aere-id:' + '0'.repeat(40), presenter: 'aere-id:' + '1'.repeat(40), presentation: 'BINDING', policy: 'POLICY', createdAt: AT }, + // 2026-10-19 (B3, tools/agent-policy/martor-agenti.mjs): the same fixed input as the published kind vector + 'agent-witness-checkpoint': { root: '0x' + '5a'.repeat(32), previous: '0x' + '6b'.repeat(32), log: 'aere-agent-witness/28001', witness: 'aere-witness:' + '7c'.repeat(20), chainId: 28001, treeSize: 3, maxMergeDelay: 1800, createdAt: AT } }; } /** Every AIP-23 kind, from its producer, on the fixed inputs. */ export function plicuriKinds() { diff --git a/conformance/vectori/INDEX.json b/conformance/vectori/INDEX.json index efb1db2..f1b8c53 100644 --- a/conformance/vectori/INDEX.json +++ b/conformance/vectori/INDEX.json @@ -1,9 +1,9 @@ { "v": 1, "kind": "aere-conformance-vector-index", - "count": 631, + "count": 634, "artifacts": { - "envelope": 67, + "envelope": 70, "payment-risk": 11, "payment-receipt": 10, "market-timeline": 26, @@ -25,7 +25,7 @@ "dataset-consistency": 3, "dataset-sample": 2 }, - "offline": 622, + "offline": 625, "online": 9, "rules": { "agents.anchor": [ @@ -516,6 +516,7 @@ "envelope/settlement.form", "envelope/provenance.form", "envelope/secret-access.form", + "envelope/agent-witness-checkpoint.form", "envelope/ai.form", "envelope/agent-decision.form", "envelope/data.form-null", @@ -542,6 +543,7 @@ "envelope/settlement.integrity", "envelope/provenance.integrity", "envelope/secret-access.integrity", + "envelope/agent-witness-checkpoint.integrity", "envelope/ai.integrity", "envelope/agent-decision.integrity", "market-timeline/s1.proof-other-result" @@ -1325,6 +1327,27 @@ "requires": [], "sha256": "0x8c1f6f75345a65444348e01f9a95932cf62e0168b5e55fdb13934d306ad3556a" }, + { + "id": "envelope/agent-witness-checkpoint.valid", + "artifact": "envelope", + "verdict": "VALID", + "requires": [], + "sha256": "0x2362b0c00f7c393239807762d91d7a56d722c95acd3850e121cb5e7c12ff1fa3" + }, + { + "id": "envelope/agent-witness-checkpoint.form", + "artifact": "envelope", + "verdict": "INVALID", + "requires": [], + "sha256": "0x1155afce20406b5105abdbeec4ccc1082ebf6f69c5d5399eede84efa5c82e1d9" + }, + { + "id": "envelope/agent-witness-checkpoint.integrity", + "artifact": "envelope", + "verdict": "INVALID", + "requires": [], + "sha256": "0x81463e18a9109d6020b0dfdba2883252e5b4241a9ae02007a3bd8926bf4c8849" + }, { "id": "envelope/ai.valid", "artifact": "envelope", diff --git a/conformance/vectori/envelope/agent-witness-checkpoint.form.json b/conformance/vectori/envelope/agent-witness-checkpoint.form.json new file mode 100644 index 0000000..8a46a39 --- /dev/null +++ b/conformance/vectori/envelope/agent-witness-checkpoint.form.json @@ -0,0 +1,53 @@ +{ + "v": 1, + "kind": "aere-conformance-vector", + "id": "envelope/agent-witness-checkpoint.form", + "artifact": "envelope", + "title": "agent-witness-checkpoint: the statementHash is 63 hex digits", + "source": "derived from envelope/agent-witness-checkpoint.valid", + "requires": [], + "exercises": [ + "envelope.form", + "envelope.integrity" + ], + "base": "envelope/agent-witness-checkpoint.valid", + "mutation": { + "op": "set", + "path": [ + "envelope", + "statementHash" + ], + "value": "0x52c07602afdc773b6b2441b5899161a192b973ad4c9fcad52f6f4c1ef3ca2ac" + }, + "input": { + "envelope": { + "v": 1, + "kind": "aere-proof-of-agent-witness-checkpoint-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-agent-witness-checkpoint", + "root": "0x5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + "previous": "0x6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b", + "log": "aere-agent-witness/28001", + "witness": "aere-witness:7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c", + "chainId": 28001, + "treeSize": 3, + "maxMergeDelay": 1800, + "createdAt": "2026-09-26T00:00:00Z" + }, + "statementHash": "0x52c07602afdc773b6b2441b5899161a192b973ad4c9fcad52f6f4c1ef3ca2ac" + } + }, + "expected": { + "verdict": "INVALID", + "failed": [ + "envelope.form" + ], + "levels": { + "integrity": "FAILED", + "authenticity": "NOT_MEASURED", + "anchoring": "NOT_MEASURED", + "finality": "NOT_MEASURED" + } + } +} diff --git a/conformance/vectori/envelope/agent-witness-checkpoint.integrity.json b/conformance/vectori/envelope/agent-witness-checkpoint.integrity.json new file mode 100644 index 0000000..2ad38a8 --- /dev/null +++ b/conformance/vectori/envelope/agent-witness-checkpoint.integrity.json @@ -0,0 +1,54 @@ +{ + "v": 1, + "kind": "aere-conformance-vector", + "id": "envelope/agent-witness-checkpoint.integrity", + "artifact": "envelope", + "title": "agent-witness-checkpoint: statement.root changed after hashing", + "source": "derived from envelope/agent-witness-checkpoint.valid", + "requires": [], + "exercises": [ + "envelope.form", + "envelope.integrity" + ], + "base": "envelope/agent-witness-checkpoint.valid", + "mutation": { + "op": "set", + "path": [ + "envelope", + "statement", + "root" + ], + "value": "0x5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a0" + }, + "input": { + "envelope": { + "v": 1, + "kind": "aere-proof-of-agent-witness-checkpoint-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-agent-witness-checkpoint", + "root": "0x5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a0", + "previous": "0x6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b", + "log": "aere-agent-witness/28001", + "witness": "aere-witness:7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c", + "chainId": 28001, + "treeSize": 3, + "maxMergeDelay": 1800, + "createdAt": "2026-09-26T00:00:00Z" + }, + "statementHash": "0x52c07602afdc773b6b2441b5899161a192b973ad4c9fcad52f6f4c1ef3ca2aca" + } + }, + "expected": { + "verdict": "INVALID", + "failed": [ + "envelope.integrity" + ], + "levels": { + "integrity": "FAILED", + "authenticity": "NOT_MEASURED", + "anchoring": "NOT_MEASURED", + "finality": "NOT_MEASURED" + } + } +} diff --git a/conformance/vectori/envelope/agent-witness-checkpoint.valid.json b/conformance/vectori/envelope/agent-witness-checkpoint.valid.json new file mode 100644 index 0000000..d3bbf00 --- /dev/null +++ b/conformance/vectori/envelope/agent-witness-checkpoint.valid.json @@ -0,0 +1,44 @@ +{ + "v": 1, + "kind": "aere-conformance-vector", + "id": "envelope/agent-witness-checkpoint.valid", + "artifact": "envelope", + "title": "the agent-witness-checkpoint envelope from its producer", + "source": "producer on fixed inputs", + "requires": [], + "exercises": [ + "envelope.form", + "envelope.integrity" + ], + "base": null, + "mutation": null, + "input": { + "envelope": { + "v": 1, + "kind": "aere-proof-of-agent-witness-checkpoint-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-agent-witness-checkpoint", + "root": "0x5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + "previous": "0x6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b", + "log": "aere-agent-witness/28001", + "witness": "aere-witness:7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c", + "chainId": 28001, + "treeSize": 3, + "maxMergeDelay": 1800, + "createdAt": "2026-09-26T00:00:00Z" + }, + "statementHash": "0x52c07602afdc773b6b2441b5899161a192b973ad4c9fcad52f6f4c1ef3ca2aca" + } + }, + "expected": { + "verdict": "VALID", + "failed": [], + "levels": { + "integrity": "PASSED", + "authenticity": "NOT_MEASURED", + "anchoring": "NOT_MEASURED", + "finality": "NOT_MEASURED" + } + } +} diff --git a/control-plane/dovezi/retete-2026-10-18/scaneaza-local.mjs b/control-plane/dovezi/retete-2026-10-18/scaneaza-local.mjs index eba8c51..379bb74 100644 --- a/control-plane/dovezi/retete-2026-10-18/scaneaza-local.mjs +++ b/control-plane/dovezi/retete-2026-10-18/scaneaza-local.mjs @@ -1,8 +1,18 @@ // scaneaza-local.mjs : scanerul AERE de pregatire post-cuantica (cloud-gateway/readiness-service.mjs, functia scaneazaAdresa, // fara garda de adrese private: tinta e 127.0.0.1, aleasa de noi) pe serverul de proba al unei retete. Tipareste verdictul, rezumatul si // constatarile, exact ce ar vedea un client al scanerului pe serverul lui dupa aplicarea retetei. -import { pathToFileURL } from 'node:url'; +import fs from 'node:fs'; +import { pathToFileURL, fileURLToPath } from 'node:url'; const port = Number(process.argv[2] || 8445); -const { scaneazaAdresa } = await import(pathToFileURL('C:/Users/Jessica/Documents/AereNetwork/aerenew/cloud-gateway/readiness-service.mjs').href); +// 2026-10-19: scanerul se ia relativ la acest fisier, in ambele asezari (cloud-gateway/readiness-service.mjs in arborele de dezvoltare, +// readiness/readiness-service.mjs in copia publica aere-quantum), sau din AERE_READINESS_SERVICE; prima forma purta calea absoluta de pe +// laptopul de lucru, publicata asa in aere-quantum (D-541). +const CAND = [new URL('../../../readiness-service.mjs', import.meta.url), new URL('../../../readiness/readiness-service.mjs', import.meta.url)].find((u) => fs.existsSync(fileURLToPath(u))); +const SCANER = process.env.AERE_READINESS_SERVICE ? pathToFileURL(process.env.AERE_READINESS_SERVICE).href : CAND && CAND.href; +if (!SCANER) { console.error('scaneaza-local.mjs: scanerul de pregatire (readiness-service.mjs) nu se gaseste nici in arborele de dezvoltare, nici in copia publica; da calea in AERE_READINESS_SERVICE'); process.exit(2); } +const { scaneazaAdresa } = await import(SCANER).catch((e) => { + console.error('scaneaza-local.mjs: scanerul de pregatire nu s-a putut incarca (' + e.message + ')'); + process.exit(2); +}); const r = await scaneazaAdresa('proba.local', { address: '127.0.0.1', family: 4 }, { port, jurnal: false }); console.log(JSON.stringify({ score: r.score, verdict: r.verdict, summary: r.summary, findings: (r.findings || []).map((f) => f.id + ':' + f.severity), error: r.error, detail: r.detail }, null, 1).replace(/\n\s*/g, ' ')); diff --git a/crypto-inventory/README.md b/crypto-inventory/README.md index 8ab1391..72defb7 100644 --- a/crypto-inventory/README.md +++ b/crypto-inventory/README.md @@ -68,6 +68,13 @@ tool does not guess. At most 64 distinct variables are resolved per file; past t file are reported as `unknown`, and the file is counted (`files-over-resolution-limit` in the CBOM, a line in the text summary). +The TLS protocol version of a Node.js option (`minVersion`, `maxVersion`, `tls.DEFAULT_MIN_VERSION`, `tls.DEFAULT_MAX_VERSION`) is +resolved the same way since 0.4.1 (2026-10-19): a constant holding a version string is followed, and a constant assigned a +conditional between two version strings (`const min = strict ? 'TLSv1.3' : 'TLSv1.2'`) is reported with the WEAKER of the two, +since that is the version the code can end up allowing; `resolvedFrom` names the line and says it was a conditional. Any other +expression gives an `unknown` finding. Before 0.4.1 a version given through a variable produced no TLS finding at all, not even an +`unknown` one; it was found on our own TLS gateway, whose upstream minimum had moved into such a constant. + ## Cost on hostile input The scanner reads repositories it did not write, so its cost is kept linear in the size of each file, @@ -215,7 +222,7 @@ console.log(renderSummary(result)); ``` node test/proba.mjs # 68 probes: fixtures per language with the exact expected findings -node test/control-negativ.mjs # breaks the scanner in a copy, one rule at a time (35 mutations), and +node test/control-negativ.mjs # breaks the scanner in a copy, one rule at a time (38 mutations), and # requires the matching probe to fail while the rest of the suite still runs node test/proba-timp.mjs # 13 hostile inputs, each at two sizes: the cost must grow linearly node test/control-negativ-timp.mjs # puts each quadratic form back (and one Swift comment rescan) and requires the cost probe to turn red diff --git a/crypto-inventory/lib/context.mjs b/crypto-inventory/lib/context.mjs index e6b1c1b..2049d30 100644 --- a/crypto-inventory/lib/context.mjs +++ b/crypto-inventory/lib/context.mjs @@ -182,6 +182,27 @@ function rezolvaOData(ctx, nume) { return null; } +// 0.4.1 (2026-10-19, D-540): un TERNAR intre doua literale, `const X = ? 'A' : 'B'`, cu aceleasi conditii ca rezolva() +// (exact o atribuire in fisier, pe randul declaratiei, numele nu e parametru). Intoarce ambele valori: cine il cheama alege ce conteaza +// (pentru o versiune TLS, ramura cea mai slaba). Numai JavaScript/TypeScript; orice alta forma: null. +export function rezolvaTernarJs(ctx, nume) { + if (ctx.limbaj !== 'javascript' || !identificator(nume)) return null; + const c = ctx.code; + const e = escapeRe(nume); + const atribuiri = ctx.potriviri(new RegExp(`(?])`, 'g')); + // garda proprie, cu alt text decat cea din rezolvaOData: controlul negativ le ancoreaza separat + if (atribuiri.length > 1 || !atribuiri.length) return null; + const k = atribuiri[0].index; + const ls0 = c.lastIndexOf('\n', k) + 1; + const le0 = c.indexOf('\n', k) < 0 ? c.length : c.indexOf('\n', k); + const linie = c.slice(ls0, le0); + const parametru = new RegExp(`(?:function\\b[^(]{0,200}\\(|\\()([^()]{0,2000}(?|\\{)`).test(c) + || new RegExp(`\\bfor\\s*\\(\\s*(?:const|let|var)?\\s*${e}\\s+(?:of|in)\\b`).test(c) + || new RegExp(`(?`).test(c); + const m = new RegExp(`\\b(?:const|let|var)\\s+${e}\\s*=\\s*[^?\\n;]{1,200}\\?\\s*(['"])([^'"\\n$\\\\]*)\\1\\s*:\\s*(['"])([^'"\\n$\\\\]*)\\3\\s*;?\\s*$`).exec(linie); + return !parametru && m ? { valori: [m[2], m[4]], line: ctx.poz(k).line } : null; +} + // valoarea unui argument: literal, variabila rezolvata sau necunoscuta export function valoareArgument(ctx, arg) { if (!arg) return { fel: 'lipsa' }; diff --git a/crypto-inventory/lib/detect-js.mjs b/crypto-inventory/lib/detect-js.mjs index f5ac11c..8838782 100644 --- a/crypto-inventory/lib/detect-js.mjs +++ b/crypto-inventory/lib/detect-js.mjs @@ -1,5 +1,5 @@ // Detectorul JavaScript / TypeScript: node:crypto, WebCrypto, optiuni TLS, biblioteci din importuri. -import { argumente, imparteArgumente, valoareArgument, necunoscut, obiectulDin, cifruOpenssl, identificator, rezolva, escapeRe } from './context.mjs'; +import { argumente, imparteArgumente, valoareArgument, necunoscut, obiectulDin, cifruOpenssl, identificator, rezolva, rezolvaTernarJs, escapeRe } from './context.mjs'; import { hashCanonic, grupTls, jws, JWS_RE } from './catalog.mjs'; const MODP = { modp1: 768, modp2: 1024, modp5: 1536, modp14: 2048, modp15: 3072, modp16: 4096, modp17: 6144, modp18: 8192 }; @@ -210,6 +210,32 @@ function detecteazaTlsJs(ctx) { const rol = /min/i.test(m[1]) ? 'min' : 'max'; ctx.adauga(m.index, `tls ${m[1]}`, 'config', { grup: 'TLS', param: TLSV[m[3]], rol }, { bucata: m[0] }); } + // 0.4.1 (2026-10-19, D-540): versiunea data printr-o EXPRESIE, nu printr-un sir. Pana la 0.4.0 o constanta (`minVersion: v`) nu dadea + // nicio gasire TLS, nici macar una necunoscuta (gasit pe gateway-ul PQ al casei: `const minSus = ... ? 'TLSv1.3' : 'TLSv1.2'`). Acum, ca la + // ecdhCurve: o constanta se rezolva; un ternar intre doua literale de versiune da ramura cea mai SLABA (pentru o minima e podeaua care + // conteaza, pentru o maxima plafonul cel mai jos), spusa in resolvedFrom; orice alta expresie iese "unknown", nu tace. Un nume de tip + // TypeScript (`SecureVersion`, `string`) dupa doua puncte nu e o valoare. + for (const m of ctx.potriviri(/(?]))\s*/g)) { + const baza = m.index + m[0].length; + const expr = expresiePanaLaVirgula(ctx, baza); + if (!expr || /^(['"])/.test(expr) || /^(?:tls\s*\.\s*)?SecureVersion\b|^string\b/.test(expr)) continue; + const rol = /min/i.test(m[1]) ? 'min' : 'max'; + const v = valoareExpresie(ctx, expr); + if (v && TLSV[v.valoare]) { + ctx.adauga(m.index, `tls ${m[1]}`, 'config', { grup: 'TLS', param: TLSV[v.valoare], rol }, { bucata: `${m[1]}: ${v.valoare}`, rezolvatDin: v.din }); + continue; + } + const id = identificator(expr); + const ter = id && !v ? rezolvaTernarJs(ctx, id) : null; + if (ter && ter.valori.every((x) => TLSV[x])) { + const ORDINE = ['ssl3', '1.0', '1.1', '1.2', '1.3']; + const slaba = ter.valori.slice().sort((a, b) => ORDINE.indexOf(TLSV[a]) - ORDINE.indexOf(TLSV[b]))[0]; + ctx.adauga(m.index, `tls ${m[1]}`, 'config', { grup: 'TLS', param: TLSV[slaba], rol }, + { bucata: `${m[1]}: ${slaba}`, rezolvatDin: `${id} (line ${ter.line}, a conditional: ${ter.valori.join(' or ')}; the weaker one)` }); + continue; + } + ctx.adauga(m.index, `tls ${m[1]}`, 'config', necunoscut(expr, `tls ${m[1]}`), { bucata: m[0] + expr }); + } for (const m of ctx.potriviri(/(? 0) continue;", + tinta: 'pozitiv: js/tls.cjs', + martor: B, + }, + { + nume: 'ternarul reasignat e rezolvat la prima lui forma (D-540: garda unei singure atribuiri scoasa din rezolvaTernarJs)', + fisier: 'lib/context.mjs', + ancora: "if (atribuiri.length > 1 || !atribuiri.length) return null;", + cu: "if (!atribuiri.length) return null;", + tinta: 'pozitiv: js/tls.cjs', + martor: B, + }, + { + nume: 'ternarul versiunii TLS da ramura cea mai TARE, nu cea mai slaba (D-540)', + fisier: 'lib/detect-js.mjs', + ancora: "const slaba = ter.valori.slice().sort((a, b) => ORDINE.indexOf(TLSV[a]) - ORDINE.indexOf(TLSV[b]))[0];", + cu: "const slaba = ter.valori.slice().sort((a, b) => ORDINE.indexOf(TLSV[b]) - ORDINE.indexOf(TLSV[a]))[0];", + tinta: 'pozitiv: js/tls.cjs', + martor: B, + }, { nume: 'comentariile bloc Rust nu se mai imbrica (lexerul)', fisier: 'lib/lexer.mjs', diff --git a/crypto-inventory/test/echivalenta-0.1.0.mjs b/crypto-inventory/test/echivalenta-0.1.0.mjs index 229e41c..3508440 100644 --- a/crypto-inventory/test/echivalenta-0.1.0.mjs +++ b/crypto-inventory/test/echivalenta-0.1.0.mjs @@ -64,7 +64,11 @@ try { // 0.4.0: si regulile JVM adaugate pentru Kotlin, care se aplica si in Java (Android Keystore, Tink, OkHttp, PKIX, puntea libsignal). const ADAUGIRI = /"api":"(?:(?:ECKeyPair|IdentityKeyPair|ECPrivateKey|PrivateKey|KEMKeyPair)\.generate|Curve\.generateKeyPair|HKDF\.deriveSecrets|new (?:Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32|PreKeyBundle|KEMPublicKey|SealedSessionCipher)|import org\.signal\.libsignal|hkdf|Aes256GcmSiv\.new|PreKeyBundle\.new|processPreKeyBundle|import @signalapp\/libsignal-client(?:\/[^"]*)?|Native\.\w+|KeyGenParameterSpec\.Builder|KeyTemplates\.get|\w+KeyTemplates\.\w+|Predefined\w+Parameters\.\w+|\w+Config\.register|import com\.google\.crypto\.tink|ConnectionSpec\.\w+|CertificatePinner\.Builder|(?:CertPathValidator|TrustManagerFactory)\.getInstance|KeyStore\.getInstance\("AndroidCAStore"\)|X509TrustManager|checkServerTrusted)"/; const doarV = ka.filter((x) => !sb.has(x)), doarN0 = kb.filter((x) => !sa.has(x)); - const adaugate = doarN0.filter((x) => ADAUGIRI.test(x)), doarN = doarN0.filter((x) => !ADAUGIRI.test(x)); + // 0.4.1 (2026-10-19, D-540): versiunea TLS data printr-o variabila (rezolvata, sau "unknown"); 0.1.0 nu o vedea deloc. Numai forma noua: + // o gasire a acestei reguli care nu e rezolvata si nu e necunoscuta ar fi forma literala, deci diferenta adevarata. + const ADAUGIRE_TLS = (x) => /"api":"tls (?:minVersion|maxVersion|DEFAULT_MIN_VERSION|DEFAULT_MAX_VERSION)"/.test(x) && (/"resolvedFrom":/.test(x) || /"classification":"unknown"/.test(x)); + const eAdaugire = (x) => ADAUGIRI.test(x) || ADAUGIRE_TLS(x); + const adaugate = doarN0.filter(eAdaugire), doarN = doarN0.filter((x) => !eAdaugire(x)); if (adaugate.length) console.log(` (in afara echivalentei: ${adaugate.length} gasiri ale detectoarelor adaugate dupa 0.1.0, API-ul libsignal Java/Node si regulile JVM din 0.4.0)`); const ok = !doarV.length && !doarN.length && ka.length + adaugate.length === kb.length && JSON.stringify(a.libraries) === JSON.stringify(b.libraries); comparate += ka.length; diff --git a/crypto-inventory/test/fixturi/js/tls.cjs b/crypto-inventory/test/fixturi/js/tls.cjs index ffa636d..919ea8e 100644 --- a/crypto-inventory/test/fixturi/js/tls.cjs +++ b/crypto-inventory/test/fixturi/js/tls.cjs @@ -15,4 +15,17 @@ const a2 = tls.connect({ ecdhCurve: PQ }); // EXPECT: X25519MLKEM768 | quantum-s const a3 = tls.connect({ ecdhCurve: `${PQ}:X25519` }); // EXPECT: X25519MLKEM768 | quantum-safe ; X25519 | quantum-vulnerable const a4 = tls.connect({ ecdhCurve: GRUPURI[process.env.MOD] }); // EXPECT: tls ecdhCurve | unknown -module.exports = { server, agent, a2, a3, a4 }; +// 0.4.1 (2026-10-19, D-540): versiunea data printr-o variabila. Pana la 0.4.0 niciuna din cele patru nu dadea vreo gasire TLS. +const MIN_TLS = 'TLSv1.2'; +const a5 = tls.connect({ minVersion: MIN_TLS }); // EXPECT: TLSv1.2 | quantum-vulnerable +const modSus = process.env.MOD_SUS; +const minSus = modSus === 'hybrid-only' ? 'TLSv1.3' : 'TLSv1.2'; +const a6 = tls.connect({ minVersion: minSus }); // EXPECT: TLSv1.2 | quantum-vulnerable +const a7 = tls.connect({ minVersion: process.env.TLS_MIN }); // EXPECT: tls minVersion | unknown +function a8(v) { return tls.connect({ minVersion: v }); } // EXPECT: tls minVersion | unknown + +let minVar = modSus === 'x' ? 'TLSv1.3' : 'TLSv1.1'; +minVar = 'TLSv1.3'; +const a9 = tls.connect({ minVersion: minVar }); // EXPECT: tls minVersion | unknown + +module.exports = { server, agent, a2, a3, a4, a5, a6, a7, a8, a9 }; diff --git a/crypto-inventory/test/proba.mjs b/crypto-inventory/test/proba.mjs index bb8c6a2..408f548 100644 --- a/crypto-inventory/test/proba.mjs +++ b/crypto-inventory/test/proba.mjs @@ -19,7 +19,8 @@ const CA_JSON = process.argv.includes('--json'); // numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient // (0.4.0, 2026-10-18: 158 + 111 din fixturile Swift si Kotlin, numarate de compara-fixturi inainte de a fi scrise aici) -const MARCAJE_ASTEPTATE = 269; +// 0.4.1 (2026-10-19, D-540): + 5 in js/tls.cjs (versiunea TLS printr-o constanta, un ternar, o expresie, un parametru, un ternar reasignat) +const MARCAJE_ASTEPTATE = 274; const FISIERE_CU_MARCAJE = ['js/node-crypto.mjs', 'js/webcrypto.js', 'js/biblioteci.ts', 'js/tls.cjs', 'js/libsignal-client.ts', 'js/libsignal-client-import.ts', 'python/crypto_py.py', 'java/Crypto.java', 'java/Libsignal.java', 'java/LibsignalImport.java', 'go/crypto.go', 'rust/cripto.rs', 'rust/ambiguu.rs', 'rust/glob.rs', 'rust/libsignal.rs', 'negative/variabile.js', 'swift/libsignal.swift', 'swift/libsignal-import.swift', 'swift/cryptokit.swift', 'swift/securitate.swift', 'swift/biblioteci.swift', 'kotlin/Libsignal.kt', 'kotlin/Android.kt']; // fixturile negative: fiecare fisier de aici e un fisier de cod citit si analizat, cu zero gasiri (in afara de variabile.js, care are marcaje) diff --git a/proof-kinds/README.md b/proof-kinds/README.md index 0c43fa3..bf3d744 100644 --- a/proof-kinds/README.md +++ b/proof-kinds/README.md @@ -2,7 +2,7 @@ A generic builder of AERE Proof Protocol envelopes (AIP-23) for the standard proof kinds: `data`, `execution`, `identity`, `compliance`, `runtime`, `location`, `device`, `payment`, `ownership`, `time`, `block`, `authorization`, `settlement`, -`provenance` and, since 2026-10-18, `secret-access`. Every kind produces the same envelope (`statement` plus `statementHash`, the +`provenance`, since 2026-10-18 `secret-access` and since 2026-10-19 `agent-witness-checkpoint`. Every kind produces the same envelope (`statement` plus `statementHash`, the SHA-256 of the canonical statement text), so the same AIP-23 reference verifier checks all of them with no kind-specific verification code. @@ -25,15 +25,28 @@ review of the secrets service) it may also carry `requestHash` (the digest of th `claimedHolder` / `claimedPresenter` (ids a presentation claimed and nobody verified) and `judged` (on a refusal, each grant judged with the rules it refused on); envelopes without these fields are byte for byte what they were. +`agent-witness-checkpoint` is the checkpoint of the hosted agents witness (`agents/martor-agenti.mjs`), in the form of a transparency log +checkpoint (C2SP tlog-checkpoint: the log's name, its size, its RFC 9162 root): `root` and `previous` (the statementHash of the checkpoint +before it, absent on the first) are already 32-byte hashes and are refused in another form; `log`, `witness` (the id derived from the +witness's ML-DSA-65 key, which signs the envelope), `chainId` (where checkpoints are notarized), `treeSize` and `maxMergeDelay` (seconds +between receiving a head and covering it with a notarized checkpoint) have a vocabulary (below). The leaves of the tree are digests of the +signed ledger heads it received, nothing of any ledger. + **A kind may have a vocabulary** (`valori` in its schema: per field, a predicate and what it says), judged by `buildProof` on every -field present; `secret-access` is the only kind with one: `verdict` is `allowed`, `refused` or `failed`, `operation` one of the +field present; two kinds have one. `secret-access`: `verdict` is `allowed`, `refused` or `failed`, `operation` one of the operations of the secrets service (`SECRET_ACCESS.operations`, which the service requires to be its own list), `version` an integer of at least 1 or `null`, `rules` a list of rule names, ids `aere-id:` + 40 hex or `null`, `createdAt` an RFC 3339 time. An envelope with the verdict `banana` is no longer built. The reference verifier does not judge vocabularies (it judges the form and the hash of any kind), so an envelope written by hand with such values stays `VALID` there: the vocabulary is the builder's promise, not the -verifier's. `KINDS.json` of the published copy names the values of each field that has them. +verifier's. `agent-witness-checkpoint` (since 2026-10-19, the adversarial review of the hosted witness): `root` and `previous` 0x + 64 +lowercase hex, `log` 1 to 256 printable ASCII characters without spaces, `witness` `aere-witness:` + 40 lowercase hex, `chainId`, `treeSize` +and `maxMergeDelay` integers of at least 1, `createdAt` an RFC 3339 time; the vocabulary is one exported object (`AGENT_WITNESS_CHECKPOINT`) +that the witness's own judge of a checkpoint (`verificaPunct` in `agents/martor-agenti.mjs`) imports, so the builder and the judge cannot +drift apart. Until then a checkpoint with `treeSize` "5" or `maxMergeDelay` 0 was built, and `VALID` at the reference verifier, while the +witness refused it. `KINDS.json` of the published copy names the values of each field that has them. -Test: `node proba-proof-kinds.mjs` (28 checks: every kind built and found `VALID` by the reference verifier, seven negative +Test: `node proba-proof-kinds.mjs` (32 checks since 2026-10-19, after the review of the hosted witness: every kind built and found `VALID` by the reference verifier, eight negative controls such as content changed after hashing, a required field missing or a `secret-access` value outside its vocabulary, no raw -content in any envelope, bytes hashed as bytes, the new `secret-access` fields, the vocabulary held by that kind alone). It needs the AIP-23 reference verifier: `AERE_VERIFY_PROOF=` (as published at +content in any envelope, bytes hashed as bytes, the new `secret-access` fields, the two vocabularies held by those two kinds alone, eleven values outside the vocabulary of the witness checkpoint +refused, and that vocabulary being the very object the witness imports). It needs the AIP-23 reference verifier: `AERE_VERIFY_PROOF=` (as published 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); without it the test reports that it did not measure and exits with 2. Node.js 24. diff --git a/proof-kinds/proba-proof-kinds.mjs b/proof-kinds/proba-proof-kinds.mjs index d94a72b..7e329cd 100644 --- a/proof-kinds/proba-proof-kinds.mjs +++ b/proof-kinds/proba-proof-kinds.mjs @@ -5,7 +5,7 @@ import fs from 'node:fs'; import crypto from 'node:crypto'; import os from 'node:os'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; -import { buildProof, KINDS, SCHEME } from './proof-kinds.mjs'; +import { buildProof, KINDS, SCHEME, AGENT_WITNESS_CHECKPOINT } from './proof-kinds.mjs'; 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'); @@ -33,10 +33,12 @@ const INTRARI = { settlement: { chainId: 2800, txHash: '0x' + '12'.repeat(32), blockHash: '0x' + '34'.repeat(32), from: '0xa', to: '0xb', amount: '100', asset: 'AERE', instruction: BRUT, createdAt: '2026-09-28T09:00:00Z' }, provenance: { subject: BRUT, parents: [BRUT + '1', BRUT + '2'], process: BRUT + 'p', actor: 'build-bot', createdAt: '2026-09-28T09:00:00Z' }, 'secret-access': { service: 'https://secrets.example', secret: DB, version: 2, operation: 'get', verdict: 'allowed', rules: [], grant: 'deployers', holder: 'aere-id:' + '0'.repeat(40), presenter: 'aere-id:' + '1'.repeat(40), presentation: BRUT, policy: BRUT + 'pol', createdAt: '2026-10-18T09:00:00Z' }, + // 2026-10-19 (B3): capul de jurnal al martorului gazduit al agentilor (radacina si capul de dinainte sunt deja hash-uri de 32 de octeti) + 'agent-witness-checkpoint': { root: '0x' + '5a'.repeat(32), previous: '0x' + '6b'.repeat(32), log: 'aere-agent-witness/28001', witness: 'aere-witness:' + '7c'.repeat(20), chainId: 28001, treeSize: 3, maxMergeDelay: 1800, createdAt: '2026-10-19T09:00:00Z' }, }; try { - cer('schema acopera cele 15 feluri (10 + bloc + autorizare, decontare, provenienta + acces la secret)', KINDS.length >= 15 && ['authorization', 'settlement', 'provenance', 'secret-access'].every((k) => KINDS.includes(k))); + cer('schema acopera cele 16 feluri (10 + bloc + autorizare, decontare, provenienta + acces la secret + capul martorului agentilor)', KINDS.length >= 16 && ['authorization', 'settlement', 'provenance', 'secret-access', 'agent-witness-checkpoint'].every((k) => KINDS.includes(k))); for (const k of KINDS) { // fiecare fel se judeca separat: un fel care nu se mai poate construi e un RAU numit, nu o exceptie care opreste proba (2026-09-28) let plic; try { plic = buildProof(k, INTRARI[k]); } catch (e) { cer(`${k}: constructia a cazut (${e.message})`, false); continue; } @@ -80,8 +82,22 @@ try { const vechi = buildProof('secret-access', SA); cer('acces la secret: requestHash, claimedPresenter, judged si verdictul failed se construiesc; fara ele declaratia are exact campurile de dinainte, in ordinea lor', nou.statement.requestHash === '0x' + 'ab'.repeat(32) && nou.statement.verdict === 'failed' && Object.keys(vechi.statement).join(',') === 'v,kind,presentationHash,policyHash,service,secret,version,operation,verdict,rules,grant,holder,presenter,createdAt'); - cer('vocabularul e numai al felului secret-access (celelalte feluri se construiesc exact ca ieri)', KINDS.filter((k) => SCHEME[k].valori).join(',') === 'secret-access'); + cer('vocabularul e numai al felurilor secret-access si agent-witness-checkpoint (celelalte se construiesc exact ca ieri)', KINDS.filter((k) => SCHEME[k].valori).join(',') === 'secret-access,agent-witness-checkpoint'); + // 2026-10-19 (B3): capul martorului agentilor: radacina trebuie sa fie deja un hash de 32 de octeti (nu se hashuieste), primul cap nu are + // `previous`, iar ordinea campurilor e cea a constructorului (semnatura martorului e peste exact acest text) + const AW = INTRARI['agent-witness-checkpoint']; + const primul = buildProof('agent-witness-checkpoint', { ...AW, previous: undefined }); + cer('CONTROL: capul martorului cu radacina care nu e hash de 32 de octeti, sau fara maxMergeDelay -> eroare; primul cap fara previous; campurile in ordinea constructorului', + arunca(() => buildProof('agent-witness-checkpoint', { ...AW, root: 'radacina' })) && arunca(() => buildProof('agent-witness-checkpoint', { ...AW, maxMergeDelay: undefined })) + && !('previous' in primul.statement) && Object.keys(buildProof('agent-witness-checkpoint', AW).statement).join(',') === 'v,kind,root,previous,log,witness,chainId,treeSize,maxMergeDelay,createdAt'); + // 2026-10-19 (revizia adversariala B3, constatarea 8): VOCABULARUL capului martorului, scris o data (AGENT_WITNESS_CHECKPOINT) si importat si + // de judecatorul martorului (verificaPunct). Pana azi treeSize "5", maxMergeDelay 0 sau witness "x" se construiau si erau VALID la verificator. + const releAW = [['treeSize', '5'], ['treeSize', 0], ['chainId', '28001'], ['chainId', 0], ['maxMergeDelay', 0], ['maxMergeDelay', 1.5], ['witness', 'x'], ['witness', 'aere-witness:' + 'AB'.repeat(20)], + ['log', ''], ['log', 'cu spatiu'], ['createdAt', 'ieri']].filter(([c, v]) => !arunca(() => buildProof('agent-witness-checkpoint', { ...AW, [c]: v }))); + cer('CONTROL: capul martorului cu valori in afara vocabularului (treeSize "5" sau 0, chainId "28001" sau 0, maxMergeDelay 0 sau 1.5, witness "x" sau cu majuscule, log gol sau cu spatiu, createdAt "ieri") -> eroare la constructie' + (releAW.length ? ': construite ' + releAW.map(([c, v]) => c + '=' + JSON.stringify(v)).join(', ') : ''), releAW.length === 0); + cer('capul martorului: vocabularul e chiar obiectul exportat (AGENT_WITNESS_CHECKPOINT.valori), pe care il importa judecatorul martorului; valorile bune se construiesc octet cu octet ca ieri', + SCHEME['agent-witness-checkpoint'].valori === AGENT_WITNESS_CHECKPOINT.valori && Object.isFrozen(AGENT_WITNESS_CHECKPOINT.valori) && buildProof('agent-witness-checkpoint', AW).statementHash === '0x' + crypto.createHash('sha256').update(JSON.stringify({ v: 1, kind: 'aere-proof-of-agent-witness-checkpoint', root: AW.root, previous: AW.previous, log: AW.log, witness: AW.witness, chainId: AW.chainId, treeSize: AW.treeSize, maxMergeDelay: AW.maxMergeDelay, createdAt: AW.createdAt }), 'utf8').digest('hex')); } finally { fs.rmSync(T, { recursive: true, force: true }); } -const total = KINDS.length + 13; -console.log(`\nProof of everything: ${total - rele}/${total} cum trebuia (${KINDS.length} feluri + 7 controale negative + acoperire + Buffer + parinti + campurile noi + vocabularul)`); +const total = KINDS.length + 16; +console.log(`\nProof of everything: ${total - rele}/${total} cum trebuia (${KINDS.length} feluri + 9 controale negative + acoperire + Buffer + parinti + campurile noi + doua vocabulare + capul martorului)`); process.exit(rele ? 1 : 0); diff --git a/proof-kinds/proof-kinds.mjs b/proof-kinds/proof-kinds.mjs index f4a7f08..3ea29d0 100644 --- a/proof-kinds/proof-kinds.mjs +++ b/proof-kinds/proof-kinds.mjs @@ -29,6 +29,26 @@ const NUME_IN_SERVICIU = /^[a-z0-9][a-z0-9_-]{0,63}$/; const eRegula = (r) => typeof r === 'string' && /^[a-z][a-z0-9-]{0,63}$/.test(r); const eGrant = (g) => typeof g === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/.test(g); const eIdSauNull = (v) => v === null || (typeof v === 'string' && /^aere-id:[0-9a-f]{40}$/.test(v)); +const eRfc3339 = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/.test(v) && !Number.isNaN(Date.parse(v)); +const eHash32 = (v) => typeof v === 'string' && /^0x[0-9a-f]{64}$/.test(v); + +// 2026-10-19 (revizia adversariala B3, constatarea 8): VOCABULARUL capului de jurnal al martorului gazduit al agentilor, scris O SINGURA DATA +// aici. Pana azi constructorul felului copia campurile ca atare (treeSize '5', maxMergeDelay 0, witness 'x' se construiau, iar plicul era +// VALID la verificatorul AIP-23), in timp ce martorul (tools/agent-policy/martor-agenti.mjs, verificaPunct) judeca forma cu propria lui copie a +// regulilor. Acum verificaPunct importa exact acest obiect, deci cele doua nu mai pot diverge. Valorile bune nu se schimba: vectorii +// publicati raman octet cu octet aceiasi. +export const AGENT_WITNESS_CHECKPOINT = Object.freeze({ + valori: Object.freeze({ + root: [eHash32, 'a 32-byte hash, 0x + 64 lowercase hex (the RFC 9162 root of the log)'], + previous: [eHash32, 'the statementHash of the checkpoint before, 0x + 64 lowercase hex (absent on the first)'], + log: [(v) => typeof v === 'string' && v.length >= 1 && v.length <= 256 && /^[\x21-\x7e]+$/.test(v), '1 to 256 printable ASCII characters without spaces (the log name)'], + witness: [(v) => typeof v === 'string' && /^aere-witness:[0-9a-f]{40}$/.test(v), 'aere-witness: + 40 lowercase hex (derived from the ML-DSA-65 key of the witness)'], + chainId: [(v) => Number.isSafeInteger(v) && v >= 1, 'a positive integer (the chain the checkpoints are notarized on)'], + treeSize: [(v) => Number.isSafeInteger(v) && v >= 1, 'an integer >= 1 (the number of heads in the log)'], + maxMergeDelay: [(v) => Number.isSafeInteger(v) && v >= 1, 'an integer number of seconds >= 1 (the maximum merge delay)'], + createdAt: [eRfc3339, 'an RFC 3339 time'], + }), +}); // schema fiecarui fel: campuri de tip 'digest' (se hashuiesc), 'plain' (raman ca atare), 'bool', 'list'. Toate obligatorii daca in `req`. export const SCHEME = { @@ -81,6 +101,15 @@ export const SCHEME = { judged: [(v) => Array.isArray(v) && v.length <= 64 && v.every((x) => x && typeof x === 'object' && !Array.isArray(x) && Object.keys(x).sort().join(',') === 'grant,rules' && eGrant(x.grant) && Array.isArray(x.rules) && x.rules.length <= 64 && x.rules.every(eRegula)), 'a list of { grant, rules }, at most 64'], createdAt: [(v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/.test(v) && !Number.isNaN(Date.parse(v)), 'an RFC 3339 time'], } }, + // 2026-10-19 (roadmap pista B, B3, linia Agents): CAPUL DE JURNAL al martorului gazduit al agentilor (tools/agent-policy/martor-agenti.mjs), + // in forma unui checkpoint de jurnal transparent (C2SP tlog-checkpoint: numele jurnalului, marimea, radacina RFC 9162): marimea si radacina + // arborelui capetelor de registru primite, capul de dinainte (statementHash-ul lui), numele jurnalului, id-ul martorului (derivat din cheia + // lui ML-DSA-65, care semneaza plicul), lantul pe care se notarizeaza capetele (28001 in sandbox) si intarzierea maxima promisa intre primirea + // unui cap si acoperirea lui de un cap notarizat (secunde, ca MMD-ul din RFC 9162). Nimic din vreun registru: frunzele sunt digesturi. + // Felurile de dinainte nu se schimba cu nimic. + // 2026-10-19 (revizia B3): cu vocabularul de mai sus (AGENT_WITNESS_CHECKPOINT), importat si de judecatorul martorului. + 'agent-witness-checkpoint': { hash: ['root', 'previous'], plain: { log: 'log', witness: 'witness', chainId: 'chainId', treeSize: 'treeSize', maxMergeDelay: 'maxMergeDelay' }, req: ['root', 'log', 'witness', 'chainId', 'treeSize', 'maxMergeDelay'], + valori: AGENT_WITNESS_CHECKPOINT.valori }, }; export const KINDS = Object.keys(SCHEME);