Skip to content

Port onto @anyone-protocol/ao-client (D15/D16/D17) - #5

Open
jim-toth wants to merge 6 commits into
masterfrom
dev-hyper-aos-migration
Open

Port onto @anyone-protocol/ao-client (D15/D16/D17)#5
jim-toth wants to merge 6 commits into
masterfrom
dev-hyper-aos-migration

Conversation

@jim-toth

Copy link
Copy Markdown
Contributor

Reference port for W4. Replaces @permaweb/aoconnect 0.0.59 and the local send-aos-message.ts wrapper with the shared client: writes are ANS-104 DataItems signed with the controller key and pushed to our own HyperBEAM node, reads are plain unsigned GETs against the process's own device.

D17 — explicit endpoints. CU_URL/GATEWAY_URL/GRAPHQL_URL are gone, replaced by a single HB_URL with NO default (nodeUrlFromEnv fails closed). Two of the three pointed at ar-io.net, a third party sitting in the critical path. The nginx edge whitelists /~meta@1.0 and ^/{contract-pid}, which covers both the ~process@1.0/now/... reads and the ~process@1.0/push writes, so the public hostname serves all three operations.

Reads are reworked to the D4 design: the View-State dryrun becomes a dump view read. Measured on a node holding the real migrated registry (7932 verified / 2940 claimable / 1088 hardware) before choosing:

  • Read latency is dominated by materializing now and scales with STATE size, not response size — every read of this process costs ~2.7s whether it returns 226 bytes or 1MB. The same contract with empty state answers in ~30ms.
  • The fingerprints view returns HTTP 414 between 100 and 200 ids (~8KB of URL), so a bulk check would have to chunk at ~150.

So chunking is far more expensive, not less: ~7000 fingerprints is ~47 requests x 2.7s ~= 127s against a single 2.9s dump. The comment on getOperatorRegistryState records this so it does not get "optimized" into 47 round-trips later.

Two traps found and avoided while porting:

  • Base-addressed whole-map reads (now/state/verified/serialize~json@1.0) are NOT usable by consumers: they leak HyperBEAM message metadata (device, commitments) as extra map entries and coerce booleans to the STRING "true". The lua views return clean, correctly-typed JSON. Note the native contract's own comment claims whole maps "come FREE from base-addressing" — they do not, in this shape.
  • Lua still serializes an EMPTY table as a JSON array, so the legacynet normalization loop is still load-bearing. A seeded-empty process dumps {"claimable":[],"verified":[],...}.

The state interface moves to the native D26 field names (claimable/verified/ blocked/registrationCredits/verifiedHardware) with the legacy mapping documented on it, including that addresses are now EIP-55 rather than legacynet ALLCAPS.

Deletes the vendored src/util/arbundles-lite (an old copy of Irys arbundles) in favour of the maintained @dha-team/arbundles that ao-client peer-depends on, plus create-ethereum-data-item-signer.ts which only existed to feed aoconnect.

Contract rejections are now actually detected. A rejected write returns HTTP 200 with the reason only in its own slot output, so the old code reported success for writes that changed nothing; ao-client confirms the slot and raises AoContractError, which the service reports as a business outcome rather than an outage.

Integration spec: 5/5 green against a local node holding the real 1MB migrated registry, including a duplicate-credit case that asserts the rejection path (contract reason "Registration Credit already added!" surfaced from the slot). Typecheck is net -1 error against baseline and introduces none.

jim-toth added 6 commits July 27, 2026 15:18
Reference port for W4. Replaces @permaweb/aoconnect 0.0.59 and the local
send-aos-message.ts wrapper with the shared client: writes are ANS-104 DataItems
signed with the controller key and pushed to our own HyperBEAM node, reads are
plain unsigned GETs against the process's own device.

D17 — explicit endpoints. CU_URL/GATEWAY_URL/GRAPHQL_URL are gone, replaced by a
single HB_URL with NO default (nodeUrlFromEnv fails closed). Two of the three
pointed at ar-io.net, a third party sitting in the critical path. The nginx edge
whitelists /~meta@1.0 and ^/{contract-pid}, which covers both the
~process@1.0/now/... reads and the ~process@1.0/push writes, so the public
hostname serves all three operations.

Reads are reworked to the D4 design: the View-State dryrun becomes a `dump` view
read. Measured on a node holding the real migrated registry (7932 verified /
2940 claimable / 1088 hardware) before choosing:

  - Read latency is dominated by materializing `now` and scales with STATE size,
    not response size — every read of this process costs ~2.7s whether it returns
    226 bytes or 1MB. The same contract with empty state answers in ~30ms.
  - The `fingerprints` view returns HTTP 414 between 100 and 200 ids (~8KB of
    URL), so a bulk check would have to chunk at ~150.

So chunking is far more expensive, not less: ~7000 fingerprints is ~47 requests
x 2.7s ~= 127s against a single 2.9s `dump`. The comment on getOperatorRegistryState
records this so it does not get "optimized" into 47 round-trips later.

Two traps found and avoided while porting:

  - Base-addressed whole-map reads (now/state/verified/serialize~json@1.0) are
    NOT usable by consumers: they leak HyperBEAM message metadata (`device`,
    `commitments`) as extra map entries and coerce booleans to the STRING
    "true". The lua views return clean, correctly-typed JSON. Note the native
    contract's own comment claims whole maps "come FREE from base-addressing" —
    they do not, in this shape.
  - Lua still serializes an EMPTY table as a JSON array, so the legacynet
    normalization loop is still load-bearing. A seeded-empty process dumps
    {"claimable":[],"verified":[],...}.

The state interface moves to the native D26 field names (claimable/verified/
blocked/registrationCredits/verifiedHardware) with the legacy mapping documented
on it, including that addresses are now EIP-55 rather than legacynet ALLCAPS.

Deletes the vendored src/util/arbundles-lite (an old copy of Irys arbundles) in
favour of the maintained @dha-team/arbundles that ao-client peer-depends on, plus
create-ethereum-data-item-signer.ts which only existed to feed aoconnect.

Contract rejections are now actually detected. A rejected write returns HTTP 200
with the reason only in its own slot output, so the old code reported success for
writes that changed nothing; ao-client confirms the slot and raises
AoContractError, which the service reports as a business outcome rather than an
outage.

Integration spec: 5/5 green against a local node holding the real 1MB migrated
registry, including a duplicate-credit case that asserts the rejection path
(contract reason "Registration Credit already added!" surfaced from the slot).
Typecheck is net -1 error against baseline and introduces none.
Declared as a direct dependency but never imported — the Arweave interaction all
goes through the bundling service, which used the Turbo SDK.

No effect on `npm audit`: arweave stays in the tree transitively via
@ardrive/turbo-sdk and @dha-team/arbundles. This is declaration hygiene, so the
manifest says what the service actually uses.
ArDrive left Arweave, and the SDK was doing very little here — sign a data item,
POST it — while costing a great deal.

What it dragged in, all of it now gone from the tree:

  * @permaweb/aoconnect 0.0.57, bundled inside the SDK. Older than any pin this
    migration removed, and it carries third-party default endpoints. So the
    "aoconnect removed from every service" claim was true of our code and false
    of the dependency tree.
  * The entire Solana payment stack — @solana/spl-token, @solana/buffer-layout-utils,
    bigint-buffer — inherited purely to upload to Arweave.

`npm audit` drops from 66 to 53.

The wire format is the one publish-module.ts already proved: POST the raw signed
item to `<bundler>/~bundler@1.0/tx`. That path is served both by up.arweave.net
— it is HyperBEAM's own default `bundler_ans104` target, see dev_arweave.erl
post_tx/4 — and by our own node. So moving to self-hosted bundling later is a
BUNDLER_NODE change, not a rewrite; the commented-out line above it in each HCL
records the two prerequisites (edge-allow /~bundler@1.0/tx, faff-allow-list the
signer).

BUNDLER_GATEWAY and BUNDLER_NETWORK are dropped — both existed only to configure
Turbo.

@noble/curves is now declared, which is unrelated to bundling but was exposed by
this change. hardware-verification.service.ts imports it and never declared it;
it resolved to a hoisted copy that the Turbo SDK happened to provide. Removing
the SDK left only ethers' nested 1.2.0, which lacks the `nist.js` subpath, so the
build broke. The pin is ^1.9.x because that line has both subpaths the code uses
— `nist.js` (v2 path) and `abstract/utils` (v1 path, dropped in v2). This was a
latent break waiting on any unrelated dependency change.
Non-breaking advisory fixes only; no direct dependency changed, lockfile only.

Production vulnerabilities 33 -> 16 (high 11 -> 3). Tracked with `npm audit --omit=dev`: the
all-inclusive headline moves the wrong way because the tree now resolves fully
and the growth is dev-only tooling (jest, eslint, the Nest CLI) that never ships.

Build clean.
Clears all 3 production criticals. These came from @dha-team/arbundles, which
pins the EOL @ethersproject v5 line; arbundles 1.0.4 is already latest and the
advisory range is `*`, so there was no upgrade to take — npm's suggested "fix"
is a downgrade to 1.0.3.

But the advisories are version-exact and both packages already ship fixes:

  elliptic     critical hits <=6.6.0, patched in 6.6.1
  secp256k1    high hits exactly 5.0.0, patched in 5.0.1

Both were installed twice — the patched version hoisted at the root, and a
vulnerable copy nested under arbundles that its ethersproject chain resolved to.
An override collapses them onto the patched ones, so this needs no change to
arbundles or to ethers (our own code is already on v6.17).

Verified rather than assumed: these are the ECDSA signing primitives, so a clean
build proves nothing. The integration specs were run against a real node holding
real migrated state, exercising ANS-104 signed writes end to end — operator
registry 5/5 (registration credits, operator certificates) and relay rewards 3/3
(settles a round, reads the snapshot back from the settle slot).

One low remains in elliptic with range <=6.6.1, i.e. unfixed at latest.
Clears the last production highs. @dha-team/arbundles pulls
@ethersproject/providers 5.7.2, which pins ws 7.4.6 — inside the advisory range
7.0.0 - 7.5.10 for two DoS reports. Same nested-vs-hoisted shape as elliptic and
secp256k1: a safe ws 8.21.0 sits hoisted for ethers v6 while the vulnerable 7.4.6
sits nested.

The override is SCOPED to @ethersproject/providers so ethers v6 keeps ws 8;
forcing ws 7 globally would break it. 7.5.13 is the patched 7.x line, so
ethersproject stays on a compatible major.

This also clears the aggregate advisories on @dha-team/arbundles and
@ethersproject/providers themselves, which were flagged only by way of their
vulnerable leaves. With elliptic, secp256k1 and now ws all patched, nothing is
left — so the cluster was never actually blocked on arbundles leaving
ethersproject v5, as previously recorded.

Worth stating plainly: these are DoS bugs in WebSocket frame handling, and
nothing on this path opens a WebSocket — arbundles pulls @ethersproject/providers
for wallet and provider types, and no WebSocketProvider is ever constructed. This
is noise reduction, not risk reduction.

Verified against a real node holding real migrated state: the operator-registry
integration spec passes 5/5 including ANS-104 signed writes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant