Skip to content

feat(gnovm): realm.Sub(subpath) sub-realm identities + NewBanker IsCurrent fix#5890

Draft
jaekwon wants to merge 9 commits into
masterfrom
realm-sub
Draft

feat(gnovm): realm.Sub(subpath) sub-realm identities + NewBanker IsCurrent fix#5890
jaekwon wants to merge 9 commits into
masterfrom
realm-sub

Conversation

@jaekwon

@jaekwon jaekwon commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Adds cur.Sub(subpath): a realm can mint a sub-realm identity token for an
internal actor (e.g. a DAO in a registry) — a realm value with synthesized
pkgpath "host:subpath" and a derived address. Downstream callees see it via
the normal cur.Previous() idiom after crossing; ":" is reserved so exact-match
pkgpath auth is never silently broadened. Includes Subpath()/DerivePkgSubAddr/
SplitPkgSubPath, a frozen-strict subpath grammar, and unsafe.* parity.

Security fix (independent, pre-existing base-v2 hole): NewBanker now requires
rlm.IsCurrent(). Without it a callee could build a RealmSend banker from
cur.Previous() and drain its caller (any realm draining any user who calls it).
Confirmed end-to-end against the real bank keeper; regression-tested.

Also removes the legacy cross1 migration sentinel.

Consensus-breaking (realm interface grows Sub/Subpath; gno0p9 extended in
place) — requires a coordinated node upgrade. Any network retaining state
should be swept for cross1 first.

Areas: GnoVM (uverse, preprocess, transpiler, gotypecheck), stdlib
(chain/banker, chain/address, execctx), tests (filetests, txtars, unit,
keeper), docs (interrealm-v2 §5.5, CONSTITUTION), ADRs. AI-assisted.

jaekwon added 9 commits July 1, 2026 22:29
cross1 was a temporary uverse sentinel easing the bare-cross ->
cross(rlm) migration. The migration is complete and per policy
(interrealm_v2.md) nothing should be using it. Remove the uverse def,
gno0p9 typechecker shim line, preprocessor recognizer/lowering cases,
and the transpiler ident rewrite; the compiler-synthesized .origin
lowering path is unchanged.

The legacy filetest becomes zrealm_cross1_removed.gno, an error test
asserting the name no longer resolves at both typecheck and
preprocess. migration_guide.md par.16 is rewritten as the direct
bare-cross -> cross(rlm) recipe; interrealm_v2.md marks its cross1
steps historical. ADR: gnovm/adr/prxxxx_remove_cross1.md.
A host realm can mint a sub-realm token for an internal actor
(cur.Sub("dao/42")): a realm value whose PkgPath is the synthesized
"host:subpath" (':' is reserved; explicitly rejected at
ValidateMemPackageAny) and whose Address derives from that synthesized
path. Downstream callees see the sub-identity through the ordinary
cur.Previous() idiom after crossing with the token; the host is
recoverable via chain.SplitPkgSubPath.

Mechanics:
- .grealm gains subpath/parent fields (transient, never persisted;
  legacy 3-field values read through zero-defaulting accessors). The
  isOriginRealmHIV persistence carve-out excludes sub-tokens.
- Sub() native: gas charged up front (mirroring chain.packageAddress
  calibration; uverse natives bypass the native gas table), subpath
  rules + derivation asserts, strict pointer-identity entry guard
  against the topmost crossing frame's Cur (sub-of-sub structurally
  blocked; deliberately NOT the relaxed IsCurrent path), ephemeral
  (/e/) hosts forbidden, caller-pkgpath check (no foreign minting).
- realmIsCurrentOnMachine accepts sub-tokens via the internal parent
  reference, so IsCurrent()/cross() work while the minting cur is
  live; classification predicates return the safe values naturally
  (IsCode true; IsUser/IsUserCall/IsUserRun/IsEphemeral false).
- unsafe.{Current,Previous}Realm parity: execctx.GetRealm (and the
  test-stdlib mirror) now walk the presented-identity chain (crossing
  frame's Cur -> prev), so unsafe.* agrees with cur.Previous() for
  sub-tokens; legacy stage fallbacks unchanged at the chain root.
- gno0p9 typechecker shim extended in place with Sub (additive on the
  sealed realm interface; consensus-visible, ships with the same
  coordinated upgrade as the cross1 removal).
- chain.DerivePkgSubAddr/SplitPkgSubPath stdlib helpers (pure gno).
- banker.NewBanker forbids RealmIssue/OriginSend for sub-tokens;
  RealmSend works unchanged (rlm-as-capability, no native changes).

Tests: filetests for minting/classification, parity (heights 0/1),
derivation equivalence, sub-of-sub and staleness rejection, subpath
validation, persistence refusal, banker flows; keeper test for the
/e/ MsgRun ban; unit tests for the ':' reservation and legacy 3-field
shape guards. Golden updates: 5-field .grealm rendering
(zrealm_panic), allocation size (alloc_7a), earlier typecheck error
for non-realm implementors (zrealm_seal_realm), and the pinned
crossrealm38 multistore hash (realm interface TypeID shift — the
intended consensus change).

ADR: gnovm/adr/prxxxx_realm_sub.md (full design spec).

Note: cross(...) requires a bare realm-typed identifier, so the idiom
is two-step: sub := cur.Sub(x); f(cross(sub), ...).
The realm interface TypeID and gno0p9 typecheck shim grew with the
Sub method, shifting gas on realm-loading paths (~0.0001% on gc's
alloc scenario; ~3% headroom bumps on two gas-wanted values). Pure
gas-number updates; no test logic changed.
Quality pass over the sub-realm implementation (no behavior change to
the feature surface):

- Lazy field mint: primary curs mint the 3-field .grealm shape again;
  only Sub() builds the 5-field form. Readers of fields >=3 already
  go through realmSubpathOf/realmParentOf (zero-defaulting), so this
  is transparent — and it restores primary allocation to exactly the
  pre-feature cost. Reverts the alloc_7a/zrealm_panic goldens and two
  of the three txtar gas-golden changes back to master; the only
  remaining consensus-state deltas are the intended realm-interface
  TypeID apphash re-pin and one gas-wanted bump (the banker sub-token
  guard's pkgpath check on the OriginSend path).
- Dedup the crossing-frame walk: one Machine.topCrossingCur backs
  callingCurOrOrigin, topmostCrossingFrameCurHIV, and the new exported
  PresentedRealmAt (was heading toward four hand-synced copies).
- GetRealm / X_getRealm call PresentedRealmAt instead of duplicating
  the identity-chain loop; the prod legacy walk shrinks to the
  boundary switch and fails loudly rather than serving context-chain
  answers if the chain-length invariant ever breaks.
- Test mirror gates on live overrides (TestOverridden) instead of map
  length, so a popped helper's SetRealm no longer permanently disables
  the identity chain.
- Banker guard gates on bt first (int compare) and collapses to one
  fail-closed panic via bt.String().
- Sub native: drop the always-false nil-sv guard; hoist synthesized so
  the run-path assert (dead while the regex stays colon-free; kept as
  defense-in-depth) doesn't re-concat.
- Tests: zrealm_sub_stale now asserts the recoverable Sub-on-stale
  path + live cross (was unverified under a lone Error directive);
  zrealm_sub_stale_cross covers the fatal cross; native_gas_test
  guards OpCPUSubRealm* against packageAddress recalibration drift;
  drop cargo-culted _ int in the banker test; correct the nested-test
  comment about which guard fires.
- zrealm_sub_foreign.gno: the caller-pkgpath guard's foreign-minting
  rejection (borrow rule #1: crossrealm_b runs with m.Realm = itself,
  holding the caller's live cur — Sub refuses; surfaces as an abort).
  Adds the TrySubOn fixture to r/tests/vm/crossrealm_b. This was the
  one Sub() guard with no coverage.
- gno-interrealm-v2.md gains §5.5 (sub-realm identities: semantics,
  guards, opt-in matching idiom, tooling helpers, trust note).
- CONSTITUTION.md: one-line realization note on the DAO-address
  clause, pointing at §5.5.
… total cap, Subpath()

Pre-freeze hardening of the realm.Sub surface (the realm interface
enters the consensus-frozen gno0p9 shim; these are cheap now,
expensive-or-impossible later):

- Subpath grammar frozen strict: segment ("/" segment)*, segment =
  [a-z0-9]([a-z0-9_.-]*[a-z0-9])?. Excludes uppercase, whitespace,
  control bytes, non-ASCII (no NFC/NFD or RTL address ambiguity), ':',
  NUL, empty segments, edge punctuation, '..'/'.' traversal. Loosening
  later is additive; tightening later would strand funds at addresses
  derived from now-illegal subpaths, so the narrow set is chosen up
  front.
- Length cap moved to the *total* synthesized "host:subpath" (<= 256,
  the pkgpath limit), not the subpath alone — keeps downstream
  pkgpath-sized buffers valid instead of admitting ~513-byte PkgPath().
- Add realm.Subpath() ("" for primaries): the canonical "am I a sub,
  of what" accessor, derived from PkgPath() so it agrees with
  chain.SplitPkgSubPath for any value. First consumer is the banker
  ban. Deliberately NOT Host() realm (would let a callee holding
  cur.Previous() reconstruct a host-shaped value and bank over the
  host treasury).
- chain.assertValidSubpath mirrors the Go grammar byte-for-byte so
  DerivePkgSubAddr can never derive an address Sub would refuse.

Tests: TestIsValidSubpath (pinned accept/reject sets),
TestSubRealmPathErrorTotalCap, expanded zrealm_sub_validation filetest,
Subpath() assertions. Apphash re-pinned for the interface TypeID
growth. Docs: ADR + gno-interrealm-v2 §5.5 updated.
Pre-existing base-v2 authorization gap (independent of sub-realms,
surfaced while reviewing the Sub banker-compat claim): NewBanker had no
IsCurrent() check for RealmSend/RealmIssue, and the only spend gate is
SendCoins's pkgAddr==from. Because cur.Previous() hands every callee a
realm value bearing its CALLER's address (IsCurrent()==false), a callee
could build NewBanker(RealmSend, cur.Previous()) (pkgAddr=caller) and
SendCoins(caller, attacker, coins) — the gate passes and the bank
keeper moves the caller's coins. For a MsgCall that means any realm
could drain the user who called it; for realm-to-realm, a callee drains
its caller. Unbounded, no privileged position, no -send required;
confirmed by an end-to-end drain against the real bank keeper.

docs/resources/gno-security.md Class 2 ('never trust rlm.Address()
without IsCurrent()') already prescribes the fix; NewBanker was an
unfixed instance, and banker_security.txtar missed it (it only built
bankers from the realm's own cur, never from cur.Previous()).

Fix: require rlm.IsCurrent() at NewBanker construction (covers
OriginSend/RealmSend/RealmIssue; Readonly panics earlier). Construction
-time is correct — the banker value holds no realm handle, so use-time
re-check is impossible, and the legitimate pattern is
construct-from-live-cur-then-store/reuse (treasury wrappers,
banker_persistence). Live sub-tokens pass (IsCurrent is parent-anchored),
so the sub-token treasury flow is preserved.

Tests: banker_security.txtar TEST 6 (real-keeper drain via cur.Previous()
blocked; victim not drained), zrealm_banker_previous.gno (VM-level
rejection). crossrealm21/22/28 goldens shift (crossrealm_b gained the
TryBankerOnPrevious fixture — object-id/line only); apphash re-pinned
(stdlib source change). ADR banker section rewritten to document the
prerequisite fix rather than assuming the path sound.
…ve rejection

Close the coverage gaps the review flagged:
- zrealm_sub_foreign_present.gno + subhost fixture: sub-identity
  presented across a real package boundary — callee reads
  cur.Previous().PkgPath()/Address()/Subpath()/IsUserCall() through the
  cross (prior tests only crossed within one package or tested
  rejection).
- subrealm_run_parity.txtar: unsafe.* <-> cur.Previous() parity at
  MsgRun entry (StageRun, /e/ origin) — the counterpart to
  zrealm_sub1's MsgCall-entry parity the ADR requires for both.
- zrealm_sub_derive_reject.gno: chain.DerivePkgSubAddr REJECTS the same
  grammar cur.Sub does (+ colon-host derivation-safety assert), pinning
  the drift-prevention claim on the failure side (sub2 only covered the
  accepting path). Positive control: a run-shaped subpath hashes
  normally, never address-extracted.

Docs: ADR banker snippet updated to the shipped fail-closed
'bt != RealmSend' form (was a stale switch); decisions list corrected
to 'only RealmSend permitted for sub-tokens'. Deduped TestIsValidSubpath
invalid set.
@jaekwon jaekwon marked this pull request as draft July 2, 2026 22:25
@github-actions github-actions Bot added 📖 documentation Improvements or additions to documentation 🧾 package/realm Tag used for new Realms or Packages. 📦 🤖 gnovm Issues or PRs gnovm related 📦 ⛰️ gno.land Issues or PRs gno.land package related labels Jul 2, 2026
@Gno2D2

Gno2D2 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

🛠 PR Checks Summary

All Automated Checks passed. ✅

Manual Checks (for Reviewers):
  • IGNORE the bot requirements for this PR (force green CI check)
Read More

🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers.

✅ Automated Checks (for Contributors):

No automated checks match this pull request.

☑️ Contributor Actions:
  1. Fix any issues flagged by automated checks.
  2. Follow the Contributor Checklist to ensure your PR is ready for review.
    • Add new tests, or document why they are unnecessary.
    • Provide clear examples/screenshots, if necessary.
    • Update documentation, if required.
    • Ensure no breaking changes, or include BREAKING CHANGE notes.
    • Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
  1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.
📚 Resources:
Debug
Manual Checks
**IGNORE** the bot requirements for this PR (force green CI check)

If

🟢 Condition met
└── 🟢 On every pull request

Can be checked by

  • Any user with comment edit permission

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📖 documentation Improvements or additions to documentation 📦 ⛰️ gno.land Issues or PRs gno.land package related 📦 🤖 gnovm Issues or PRs gnovm related 🧾 package/realm Tag used for new Realms or Packages.

Projects

Development

Successfully merging this pull request may close these issues.

2 participants