Wanderland

Wanderland Core: Merkle DAG

The trace chain is a Merkle DAG. Every crossing carries a backreference hash to the crossing that caused it. Mutating any node invalidates every descendant's signature.

Not a classical balanced Merkle tree with a single root hash over all leaves. A linked Merkle structure: every node is a commitment to its ancestry, and walking any path from a leaf to the root re-verifies the whole prefix.

Two Hash Fields Per Crossing

Every crossing record carries:

That is the entire construction. No external trace ID, no separate chain ledger, no roll-up root.

Signing a Single Crossing

lib/wanderland/crossing.rbCrossing#sign

def sign(storage)
  return self unless storage && @has_identity
  begin
    Activities::PKI.generate(storage, @identity)
    @signature = Activities::PKI.sign(storage, @identity, canonical)
  rescue
    # Signing failed — crossing goes unsigned
  end
  self
end

def canonical
  JSON.generate(to_h.reject { |k, _| k == "signature" }.sort.to_h)
end

Three deterministic steps, same as ever:

Because trace is part of the signable payload, the parent hash is bound into this crossing's signature.

Signing is soft: if the boundary has no identity, or if key material can't be generated, the crossing goes unsigned rather than raising. Context#verify_chain surfaces that as sig_valid: false without breaking the chain walk. Production deployments provision real keys into :pki: at boot via resolver tags (!SSM, !Vault); dev/test auto-generates ephemeral keys on first sign.

Linear Links

lib/wanderland/crossing.rbCrossing#link

def link(prev)
  @trace = prev&.respond_to?(:signature) ? prev.signature : prev&.dig("signature")
  self
end

The walker calls link on every crossing before signing, passing the previous event from Context. Root crossings are linked against nil and get a nil trace.

crossing 0:  sig = "aaa"                       <- root, trace = nil
crossing 1:  trace = "aaa",  sig = "bbb"       <- binds aaa into bbb
crossing 2:  trace = "bbb",  sig = "ccc"       <- binds bbb into ccc

Tamper with crossing 1's payload: its sig no longer verifies. Re-sign crossing 1 to fix that: now its sig differs from "bbb", so crossing 2's trace is wrong. To repair the chain you would have to re-sign every descendant, which requires every descendant's private key. That is the tamper-evidence.

Forks

The current build is linear — every crossing links to the single previous event on the Context stack. Fork encoding (parent_sig:child_index) is parked and returns alongside the reactivity model (trigger registry + wave executor), where a single write can fire multiple boundaries in one wave.

When it lands, the scheme is the same as the old prototype: a child trace is parent_sig:N, rpartition(":") recovers the parent sig and child index, and verify_link accepts either an exact match or the parent portion for fork children. Convergence (two parents into one child) stays out of band — a boundary that needs to reference multiple ancestors embeds their sigs in its payload.

Until then: one trace per crossing, pure linear DAG, one predecessor per node.

Composing a Whole Chain

lib/wanderland/boundary.rb — the walker composes the chain inline as boundaries execute. There is no standalone sign_chain function anymore; every crossing is linked and signed at the moment Boundary.execute returns it.

# Inside Boundary.execute, per slot:
crossing = Wanderland::Crossing.new(boundary: reg, signal: signal, from: caller)
crossing.link(ctx&.events&.last)   # trace = prev.signature (or nil for root)
crossing.sign(storage)              # canonical JSON, signed by the boundary's identity
ctx.append(crossing)                # Context verifies the signature on append

Three things fall out of this for free:

Verifying

lib/wanderland/context.rbContext#verify_chain

def verify_chain
  results = @events do |crossing, i|
    sig_valid = @verified == true
    link_valid = if i == 0
                   true
                 else
                   prev = @events[i - 1]
                   prev_sig = prev.respond_to?(:signature) ? prev.signature : prev["signature"]
                   trace = crossing.respond_to?(:trace) ? crossing.trace : crossing["trace"]
                   trace == prev_sig
                 end
    { "index" => i, "boundary" => crossing["boundary"],
      "identity" => crossing["identity"],
      "sig_valid" => sig_valid, "link_valid" => link_valid }
  end
  { "valid" => results.all? { |r| r["sig_valid"] && r["link_valid"] }, "results" => results }
end

Two independent checks per crossing:

The chain is valid: true only if every crossing passes both. Unsigned crossings (no identity, signing failed, or forged) show sig_valid: false without breaking the walk — the chain is valid where it can be verified, transparent where it can't.

The User-to-Boundary Chain

The chain is not internal bookkeeping. It spans the full request lifecycle.

user JWT signs   -> crossing 0 (identity: :sessions:user-xyz)
boundary signs   -> crossing 1 (identity: :boundaries:auth_gate,   trace: sig_0)
boundary signs   -> crossing 2 (identity: :boundaries:enforce_denials, trace: sig_1)
boundary signs   -> crossing 3 (identity: :boundaries:echo,        trace: sig_2)
engine signs     -> crossing N (identity: :boundaries:format,      trace: sig_{N-1})
engine signs     -> crossing N+1 (identity: engine:seal,           trace: sig_N)

The chain answers four questions cryptographically:

Question Answered by
Who initiated? root crossing's identity (and, when present, its JWT sig)
What executed? each crossing's boundary field, bound into that crossing's signature
In what order? trace links, verified left-to-right by Context#verify_chain
Nothing tampered? every sig_valid: true and every link_valid: true

Identity naming is still settling. In the current build, boundary identities use the boundary:name / engine:name scheme; the case task-bac15977 (Crossing ↔ 4-address convergence) will rename identityfrom_addr to align with the crossings-architecture four-address model.

Roll-Up to a Single Root

There is no pre-computed root hash over the whole chain. When an external system needs one — compliance export, cross-org attestation, a notary handoff — the seal boundary produces it as just another crossing.

... normal chain ...
crossing N:    sig = "zzz"
crossing N+1:  boundary  = "seal"
               identity  = "engine:seal"
               trace     = "zzz"
               payload:
                 chain_valid:  true
                 chain_depth:  12
                 sealed_sigs:  ["ddd", "fff", "zzz", ...]   # sorted leaves
                 sealed_at:    "2026-04-16T18:20:00Z"
               signature = "root"         # commitment over the whole chain

The seal's signature is a commitment over both its payload (which lists every leaf sig, sorted) and its trace (which is itself a commitment to the linear prefix). One sig binds the entire DAG.

Live implementation — lib/wanderland/boundaries/seal.rb:

class Seal
  include Wanderland::Boundary

  IDENTITY = Wanderland::Identity.new(
    id: "engine:seal", name: "Chain Seal",
    roles: [:engine], type: :service, scopes: [:seal]
  ).freeze

  boundary :seal,
    identity: IDENTITY,
    capabilities: [:seal],
    when_shape: { "always" => true }

  def call(input)
    context = input["context"]
    verification = context.verify_chain
    signatures = context.events
      .map { |c| c.respond_to?(:signature) ? c.signature : c["signature"] }
      .compact

    Signal.ok(
      chain_valid:  verification["valid"],
      chain_depth:  context.size,
      sealed_sigs:  signatures.sort,
      sealed_at:    Time.now.utc.iso8601
    )
  end
end

Key changes from the prototype:

Sorted sig list in canonical form means reproduction is deterministic across hosts. No new primitive — the engine signs the seal crossing on the way out, same as any other.

What It Isn't

Not a classical Merkle tree:

What it is:

Think blockchain without the blocks — or Git commit ancestry with authenticated signatures on every link.

Where It Is Exercised

Removed in commit a92dbfd (kept here for historical context):

Source