Skip to content

feat(l1): introduce SettlementLayer and EthMessagingClient abstractions#3738

Open
brbrr wants to merge 10 commits into
mainfrom
feat/l1-settlement-geth
Open

feat(l1): introduce SettlementLayer and EthMessagingClient abstractions#3738
brbrr wants to merge 10 commits into
mainfrom
feat/l1-settlement-geth

Conversation

@brbrr

@brbrr brbrr commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

This is the first part of #3727 (deprecated) that adds an abstraction layer between the Geth L1 client and Juno, separating the settlement layer and the actual RPC client.

@brbrr brbrr self-assigned this Jun 18, 2026
@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.38144% with 40 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.46%. Comparing base (2095502) to head (1c464ae).

Files with missing lines Patch % Lines
node/node.go 10.34% 26 Missing ⚠️
l1/geth_settlement.go 88.07% 11 Missing and 2 partials ⚠️
node/migration.go 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3738      +/-   ##
==========================================
+ Coverage   75.39%   75.46%   +0.07%     
==========================================
  Files         438      437       -1     
  Lines       39463    39509      +46     
==========================================
+ Hits        29753    29817      +64     
+ Misses       7637     7618      -19     
- Partials     2073     2074       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@brbrr brbrr marked this pull request as ready for review June 19, 2026 11:44
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @brbrr's task in 5m 10s —— View job


PR Review: L1 SettlementLayer abstraction

I reviewed the diff against origin/main (and the branch base, since the branch predates current main). The packages build cleanly, and the refactor in l1.go preserves the existing sync/catch-up logic 1:1 — only the types change (SubscriberSettlementLayer, contract.StarknetLogStateUpdateStateUpdate). Nicely documented throughout.

Reviewed:

  • l1/geth_settlement.go (new adapter + forwarder)
  • l1/settlement.go + l1/l1.go
  • node/node.go, node/migration.go, node/metrics.go wiring
  • rpc/rpccore + RPC GetMessageStatus receipt consumption
  • tests + stale-reference / dead-code sweep

This is a clean, well-structured refactor. Logic is correct; I found no blockers — one test-coverage gap worth addressing and two nits.

Important

  • Forwarder is untestedgethStateUpdateForwarder / WatchStateUpdate is the only concurrency-sensitive code in the PR (goroutine lifecycle, closeOnce shutdown, non-blocking Err() delivery, sink back-pressure) and has no direct coverage. The HTTP test server can't exercise WS subscriptions, and the l1_*_test.go suites mock WatchStateUpdate at the interface boundary, so the real forwarder never runs. Codecov's 54 missing lines in this file are largely this block. Details + a suggested 4-case test left inline. The logic itself looks correct on inspection — shutdown is once-guarded, no send-after-close, no double-writer on sink, and Unsubscribe unblocks run via <-w.closed — it just isn't pinned down by a test.

Nits

  • rpc/rpccore/rpccore.go:38EthMessagingClient alias is added but unused in this PR (it's for the next commit per the description). Fine as a forward-declaration, but it's currently dead code; consider landing it together with its first consumer so it doesn't linger unused.
  • node/node.go newGethSettlement dials with a fresh context.Background()+1m timeout, so in node/migration.go:fetchL1HeadIfMissing the migration's ctx cancellation won't cancel the dial. Harmless given the bounded dial timeout, but threading the caller's ctx would be more consistent.

Verified (no action needed)

  • Partial gethReceiptToEth copy (only Logs) is safe — GetMessageStatus (rpc/v10/l1.go) reads only Logs[].Topics/Data, all of which are converted.
  • No stale references to the removed Subscriber / EthSubscriber / EthReceiptAdapter / Client.L1() remain.
  • Concurrent settlement RPC calls (prometheus gauge scrapes vs. the sync loop) are safe — listener is set once before hand-off and go-ethereum's rpc.Client is concurrency-safe.

Comment thread l1/geth_settlement.go Outdated
Comment on lines +276 to +328
// gethStateUpdateForwarder decodes contract.StarknetLogStateUpdate events
// into l1.StateUpdate as they arrive from the underlying log subscription.
type gethStateUpdateForwarder struct {
inner event.Subscription
sink chan<- *StateUpdate
raw chan *contract.StarknetLogStateUpdate
errCh chan error
closed chan struct{}
closeOnce sync.Once
}

func (w *gethStateUpdateForwarder) Err() <-chan error { return w.errCh }

func (w *gethStateUpdateForwarder) Unsubscribe() {
w.shutdown(nil)
w.inner.Unsubscribe()
}

func (w *gethStateUpdateForwarder) shutdown(cause error) {
w.closeOnce.Do(func() {
close(w.closed)
if cause != nil {
select {
case w.errCh <- cause:
default:
}
}
close(w.errCh)
})
}

func (w *gethStateUpdateForwarder) run() {
defer w.shutdown(nil)
for {
select {
case <-w.closed:
return
case err, ok := <-w.inner.Err():
if !ok {
return
}
w.shutdown(err)
return
case ev := <-w.raw:
su := stateUpdateFromGethContract(ev)
select {
case w.sink <- su:
case <-w.closed:
return
}
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important (test gap): gethStateUpdateForwarder and GethSettlement.WatchStateUpdate are the most concurrency-sensitive code in this PR — goroutine lifecycle, closeOnce shutdown, non-blocking error delivery, sink back-pressure — yet they have no direct test. The new HTTP-based gethJSONRPCServer can only exercise the unary calls; WatchStateUpdate needs a WS endpoint, so nothing here is covered (Codecov flags 54 missing lines, mostly this block).

The l1_*_test.go suites mock WatchStateUpdate at the SettlementLayer boundary, so the real forwarder logic is never run.

A focused unit test driving a fake event.Subscription through the four paths would lock this down:

  1. normal: event on raw → decoded StateUpdate on sink
  2. inner errors → Err() delivers the cause exactly once, then closes
  3. Unsubscribe()run goroutine exits and inner.Unsubscribe() is called (assert no leak via goleak)
  4. sink blocked + Unsubscribe() concurrently → run unblocks via <-w.closed and returns

Fix this →

@brbrr brbrr changed the title Feat/l1 settlement geth feat(l1): introduce SettlementLayer and EthMessagingClient abstractions Jun 19, 2026
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @brbrr's task in 3m 41s —— View job


PR Review: L1 SettlementLayer abstraction — follow-up

  • Read PR context & prior review
  • Review new l1/geth_forwarder_test.go
  • Re-review l1/geth_settlement.go forwarder logic
  • Verify the two prior nits were addressed
  • Build affected packages

Re-reviewed after the address feedback commit (fcd716d9). All three items from the prior review are resolved, and ./l1/..., ./node/..., ./rpc/rpccore/... build cleanly.

Resolved

  • Forwarder test gap (the one "Important")l1/geth_forwarder_test.go now drives gethStateUpdateForwarder directly through a fake event.Subscription, covering exactly the four paths flagged: normal forward + decode, inner-error Err() delivery-then-close, Unsubscribe() goroutine exit (asserts inner.Unsubscribe() ran), and sink-stall + Unsubscribe() unblocking via <-w.closed. The fake's closeOnce-equivalent (CompareAndSwap) and the buffered errCh/raw match the production wiring, so the assertions exercise the real logic rather than a re-implementation. Good coverage of the concurrency-sensitive block.
  • Unused EthMessagingClient alias — removed from rpc/rpccore/rpccore.go (deferred to the commit that introduces its first consumer).
  • ctx threading into newGethSettlementnode/migration.go now passes the migration's ctx, and node/node.go layers the 1-minute dial timeout on the caller's context. node.New still passes context.Background(), which is correct — there's no node-lifetime context at construction time, and that's documented.

Nits (non-blocking)

  • l1/geth_forwarder_test.go:144 TestGethStateUpdateForwarder_SinkStalledThenUnsubscribe — because raw is buffered (cap 1), after Unsubscribe() closes w.closed the run loop's top-level select may pick the <-w.closed arm before ever dequeuing from raw, so the intended "blocked on w.sink <- su" path isn't guaranteed to execute on every run. The test still passes either way (it asserts only shutdown + unsubscribe), so this is a determinism nit, not a correctness issue — the stall arm is already covered deterministically by inspection. If you want to pin it, have the test wait for the goroutine to reach the send (e.g. an unbuffered raw so the w.raw <- send only completes once run has dequeued) before calling Unsubscribe().

Verdict

No blockers. The previously-flagged test gap is closed and both nits are addressed — this is good to merge from my side (note: I cannot approve PRs). I was unable to run go test ./l1/ -race in this environment (command requires approval), but the forwarder logic and test assertions check out on inspection and the packages compile.
• branch feat/l1-settlement-geth

@rodrodros rodrodros left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR looks more complicated than it should, let's discuss it more on Monday

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @brbrr's task in 4m 21s —— View job


PR Review: SettlementLayer & EthMessagingClient abstractions

  • Read new abstractions (settlement.go, geth_settlement.go)
  • Review forwarder concurrency / resource leaks
  • Review l1.go rewiring & node wiring
  • Review tests for meaningful assertions
  • Build affected packages (./l1/... ./node/... ./rpc/... — OK)

I reviewed this against the branch base (ada24e91e, since the branch predates current origin/main). This is a clean, well-structured refactor and I found no correctness, concurrency, or resource-leak blockers. The l1.Client sync/catch-up logic is preserved 1:1 — only types change (SubscriberSettlementLayer, contract.StarknetLogStateUpdateStateUpdate).

Verified (no action needed)

  • Forwarder concurrency (geth_settlement.go:276-328) — shutdown is closeOnce-guarded; no send-after-close (errCh send is non-blocking then closed exactly once); run unblocks a stalled sink <- su via <-w.closed; Unsubscribe exits the goroutine and forwards to inner. The new geth_forwarder_test.go drives all four paths (normal forward, inner-error delivery+close, unsubscribe exit, sink-stall+unsubscribe) directly against a fake event.Subscription — the previously-flagged test gap is closed.
  • SetListener data race — the field is read unlocked from every RPC method, but node.go calls SetListener once before any goroutine starts and the contract is documented (geth_settlement.go:111-122). Single-shot wiring is respected.
  • Resource lifecyclesettlement.Close() is deferred in Client.Run/CatchUpL1Head; the migration path (node/migration.go) builds a listener-less settlement and closes it the same way. No leak.
  • FinalisedHeight not-found maps ethereum.NotFoundeth.ErrNotFound, distinguishing "no finality yet" from transport failure — covered by test.
  • gethReceiptToEth copies only Logs, which is all starknet_getMessageStatus reads (rpc/v10/l1.go) — safe, and documented as such.
  • No stale references to the removed Subscriber / EthSubscriber / EthReceiptAdapter / Client.L1() remain.
  • Tests: meaningful assertions, no flaky time.Sleep (the two time.Sleep matches are only failure-message strings), race-sensitive forwarder tests included.

Nits (non-blocking)

  • gethReceiptToEth is a latent footgun — it silently drops every receipt field except Logs. Correct today, but a future L1Client consumer reading e.g. Status or BlockNumber off the receipt would get a zero value with no compile-time signal. The doc comment mitigates this; consider it a known trade-off.
  • PR title scope — the title says "and EthMessagingClient abstractions", but per the description that type is deferred to a later commit and is not present in this diff. Title is slightly ahead of the contents (purely cosmetic).
  • SinkStalledThenUnsubscribe determinism (geth_forwarder_test.go:144) — because raw is buffered (cap 1), run may take the <-w.closed arm before dequeuing, so the intended "blocked on send" path isn't guaranteed every run. Test still passes (it only asserts shutdown + unsubscribe); an unbuffered raw would pin the stall path deterministically. Already noted in a prior review.

Note: I could not run go test ./l1/ -race in this environment (command requires approval), but the package builds cleanly and the forwarder logic + assertions check out on inspection.

Re @rodrodros's "more complicated than it should be" — that's a design-altitude question (worth the Monday discussion) rather than a correctness one; from a bug/race/leak standpoint the implementation is sound.
• branch feat/l1-settlement-geth

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @brbrr's task in 3m 47s —— View job


PR Review: SettlementLayer & EthMessagingClient abstractions

  • Read new abstractions (settlement.go, geth_settlement.go)
  • Review forwarder concurrency / resource leaks
  • Review l1.go rewiring & node wiring
  • Review tests
  • Build affected packages (./l1/... ./node/... ./rpc/... — OK)

Re-reviewed after the latest Simplify StateUpdate production commit (4de2d8887), against the branch base ada24e91e (the branch predates current origin/main, so there's no merge base with it). No correctness, concurrency, or resource-leak blockers.

The simplification is a real improvement

The hand-rolled gethStateUpdateForwarder (manual closeOnce / closed channel / shutdown(cause) plumbing) is gone, replaced by forwardStateUpdates built on go-ethereum's event.NewSubscription (geth_settlement.go:273-295). This is the right call — event.NewSubscription already owns once-only close, idempotent Unsubscribe, error delivery, and goroutine teardown, so the adapter is now just type translation + the stall-vs-quit select. Net −34 lines and the concurrency-sensitive surface shrank to something the runtime library guarantees. This directly addresses @rodrodros's "more complicated than it should be" note. Lifecycle re-verified:

  • defer inner.Unsubscribe() runs on every producer exit (error, quit, or stall+quit) → no upstream subscription leak.
  • sink <- ... is guarded by <-quit, so a stalled sink can't wedge the goroutine after Unsubscribe.
  • Unsubscribe() blocks until the producer returns → deterministic teardown, no send-after-close.

Verified (no action needed)

  • l1.Client logic preserved 1:1 (l1.go) — only types change (SubscriberSettlementLayer, contract.StarknetLogStateUpdateStateUpdate); applyStateUpdate, reorg-clear, setL1Head finality gating all unchanged.
  • SetListener race contract — read unlocked from RPC methods, but set once in node.go before hand-off and documented (geth_settlement.go:110-121). Single-shot wiring respected.
  • Resource lifecyclesettlement.Close() deferred in Run/CatchUpL1Head; migration path builds a listener-less settlement and closes it the same way.
  • ctx threadingnewGethSettlement layers a 1-min dial timeout on the caller's ctx (node.go:648); migration passes its ctx; node.New correctly uses context.Background() (no node-lifetime ctx at construction).
  • gethReceiptToEth copies only Logs, which is all starknet_getMessageStatus reads — safe and documented.
  • rpccore.L1Client is now documented and the previously-unused EthMessagingClient alias was removed (deferred to its first consumer).
  • Forwarder tests rewired to drive forwardStateUpdates directly through a fake event.Subscription, covering all four paths (normal forward, inner-error delivery+close, unsubscribe exit, sink-stall+unsubscribe).

Nits (non-blocking)

  • SinkStalledThenUnsubscribe determinism (geth_forwarder_test.go:143) — raw is buffered (cap 1), so the producer's outer select may take <-quit before dequeuing, meaning the intended "blocked on sink <- ..." path isn't guaranteed each run. Test still passes (it only asserts shutdown + unsubscribe). An unbuffered raw would pin the stall path. Carried over from prior reviews.
  • gethReceiptToEth latent footgun — silently drops every receipt field except Logs; a future L1Client consumer reading e.g. Status/BlockNumber would get a zero value with no compile-time signal. Mitigated by the doc comment; known trade-off.
  • PR title scopeEthMessagingClient is deferred to a later commit and isn't in this diff; title is slightly ahead of contents (cosmetic).

Note: build is clean, but I couldn't run go test ./l1/ -race in this environment (command requires approval). The forwarder logic and test assertions check out on inspection.
• branch feat/l1-settlement-geth

Comment thread l1/l1_pkg_test.go Outdated
Comment thread l1/l1_pkg_test.go
Comment thread l1/l1.go Outdated
Comment thread node/migration.go
Comment on lines 112 to 114
if err != nil {
return fmt.Errorf("creating a new L1 client: %w", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts: I guess the message is ok, since the err will explain it is a "dial" problem. Also, it is clearer to the user using the term L1 client.

Comment thread l1/geth_settlement.go Outdated
Comment on lines +104 to +108
// WithSettlementLogger attaches a logger forwarded to the settlement.
// Surfaces transport-level warnings at debug level.
func WithSettlementLogger(l log.StructuredLogger) GethSettlementOption {
return func(o *gethSettlementOptions) { o.logger = l }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this planning to be used in the next PR?
Because the log is never used, and the comment Surfaces transport-level warnings at debug level. is not correct at the moment.

Comment thread l1/geth_settlement.go Outdated
Comment thread l1/geth_settlement.go Outdated
Comment thread l1/geth_settlement.go Outdated
Comment on lines +110 to +121
// SetListener swaps the event listener after construction. The metrics
// listener captures the settlement in a closure (so it can read gauges
// off it), which means the listener can only be built AFTER the
// settlement exists — hence a post-construction setter rather than a
// constructor option.
//
// Concurrency contract: SetListener MUST be called before the
// settlement is handed off to any goroutine (i.e. before l1.NewClient
// in node.go). The field is read unlocked from every RPC method; a
// concurrent SetListener would be a data race. The single-shot wiring
// in node.go satisfies this — don't introduce a second caller.
func (s *GethSettlement) SetListener(l EventListener) { s.listener = l }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you documented why this exists, but this is a VERY weird pattern IMO.

I'm not sure how to solve this, but is this really necessary?

Comment thread l1/geth_settlement.go Outdated
Comment thread l1/geth_settlement.go
Copilot AI review requested due to automatic review settings July 2, 2026 11:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new L1 abstraction boundary by splitting the “settlement layer” (watch/filter state update events, heights, chain ID, receipts) from the higher-level l1.Client sync loop, and wires the new adapter into node startup/migrations and RPC.

Changes:

  • Add l1.SettlementLayer + l1.StateUpdate as chain-neutral types consumed by l1.Client, and implement a go-ethereum-backed adapter (l1.GethSettlement).
  • Remove the old geth-specific EthSubscriber and the RPC EthReceiptAdapter, wiring GethSettlement directly into RPC handlers for starknet_getMessageStatus.
  • Refactor L1 metrics so the listener is shared across l1.Client and the settlement adapter, and split settlement polling gauges into a separate registrar.

Reviewed changes

Copilot reviewed 18 out of 21 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
rpc/rpccore/rpccore.go Adds/clarifies the L1Client interface documentation used by RPC for message status.
rpc/rpccore/eth_receipt_adapter.go Removes the receipt adapter now that the settlement client returns Juno eth types directly.
rpc/rpccore/eth_receipt_adapter_test.go Removes adapter tests (adapter deleted).
node/node.go Constructs GethSettlement, attaches metrics listener/options, passes settlement to both l1.Client and RPC.
node/migration.go Uses GethSettlement + l1.Client to fetch L1 head during migration without metrics listener.
node/metrics.go Splits L1 metrics into listener vs settlement polling gauges; hooks into SettlementLayer.
node/metrics_test.go Updates tests to use MockSettlementLayer and the split metric registration flow.
mocks/mock_subscriber.go Deletes mock for the removed Subscriber interface.
mocks/mock_settlement_layer.go Adds mock for the new SettlementLayer interface.
l1/settlement.go Introduces SettlementLayer and chain-neutral StateUpdate.
l1/l1.go Refactors l1.Client to depend on SettlementLayer and StateUpdate instead of geth types.
l1/l1_test.go Updates internal tests to use MockSettlementLayer and StateUpdate.
l1/l1_pkg_test.go Moves tests to l1_test and uses exported test helpers to avoid import cycles.
l1/export_test.go Exposes package-private hooks to external tests (SetSettlement, cache accessors, retry loops).
l1/geth/contract/starknet.go Adds generated contract binding used by GethSettlement.
l1/geth/contract/starknet_filter.go Adds helper to fully drain filtered logs from abigen’s FilterLogs.
l1/geth_settlement.go Adds go-ethereum-backed GethSettlement implementing SettlementLayer (+ receipt lookup).
l1/geth_settlement_test.go Adds unit tests for GethSettlement via a minimal JSON-RPC test server.
l1/geth_forwarder_test.go Adds tests for forwarding/teardown behavior of the state update forwarder.
l1/eth/subscription.go Removes the old eth.Subscription interface (now expected to live at the l1 boundary).
l1/eth_subscriber.go Deletes the old go-ethereum-backed subscriber implementation (replaced by GethSettlement).
Files not reviewed (1)
  • mocks/mock_settlement_layer.go: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread l1/settlement.go
Comment thread l1/geth_settlement.go
Comment thread node/node.go Outdated
Comment thread node/migration.go
@brbrr brbrr force-pushed the feat/l1-settlement-geth branch from c30556e to 1c464ae Compare July 2, 2026 12:18
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @brbrr's task in 3m 48s —— View job


PR Review: SettlementLayer abstraction

  • Gather context (diff vs origin/main, prior reviews)
  • Review new abstractions (settlement.go, subscription.go, geth_settlement.go)
  • Review forwarder concurrency / resource leaks
  • Review l1.go rewiring + node/migration/metrics wiring
  • Review tests
  • Build affected packages (./l1/... ./node/... ./rpc/...OK)

Re-reviewed the current tip (1c464ae0c) against origin/main. This remains a clean refactor with no correctness, concurrency, or resource-leak blockers. The l1.Client sync/catch-up logic is preserved 1:1 — only the types change (SubscriberSettlementLayer, contract.StarknetLogStateUpdateStateUpdate).

Verified (no action needed)

  • Forwarder lifecycle (geth_settlement.go:249-271) — the hand-rolled closeOnce/shutdown plumbing is gone, replaced by event.NewSubscription. defer inner.Unsubscribe() runs on every producer exit (error / quit / stall+quit) → no upstream leak; the sink <- … send is guarded by <-quit so a stalled sink can't wedge the goroutine; Unsubscribe() blocks until the producer returns → deterministic teardown, no send-after-close.
  • Resubscribe path (l1.go:243-267) — because event.NewSubscription.Unsubscribe() blocks until the producer returns, the old forwarder has fully stopped before the new one is created, so there's never a second concurrent writer on the shared updateChan. Safe.
  • Subscription interface now lives in l1/subscription.go (addresses @RafaelGranza's "shouldn't live in eth" and Copilot's "undefined identifier" note — both resolved).
  • SetListener/listener race — the settlement listener is read unlocked from RPC methods but set once at construction (WithSettlementListener) before hand-off, and documented. Single-shot wiring respected.
  • Resource lifecyclesettlement.Close() is deferred in Run/CatchUpL1Head; the migration path builds a listener-less settlement and closes it the same way. No leak. Metrics gauges (registerL1SettlementMetrics) that outlive Close() just return 0 on error — no panic.
  • ctx threadingnewGethSettlement now layers a 1-min dial timeout on the caller's ctx (node.go:683); migration passes its own ctx; node.New correctly uses context.Background().
  • gethReceiptToEth copies only Logs, which is all starknet_getMessageStatus reads (rpc/v10/l1.go) — safe and documented.
  • Error wraps now consistently start with a verb-ing form (dialing, binding, getting, filtering, subscribing) — addresses @RafaelGranza / @rodrodros's format note. The unused EthMessagingClient alias and the log-field/"transport warnings" comment flagged in earlier reviews are gone from the current diff.
  • Forwarder tests (geth_forwarder_test.go) drive forwardStateUpdates directly through a fake event.Subscription, covering all four paths (normal forward, inner-error delivery+close, unsubscribe exit, sink-stall+unsubscribe).

Nits (non-blocking, all cosmetic — no inline comments posted)

  • Constructor doc vs. behavior (geth_settlement.go:51-54) — NewGethSettlement's comment says "ws/wss is required" / "transport is selected by URL scheme", but the constructor doesn't enforce a scheme (only WatchStateUpdate needs ws; unary calls work over HTTP). Enforcement actually lives in node.go:newGethSettlement. Consider softening the constructor comment to "ws/wss required for WatchStateUpdate" so it doesn't over-claim. (Raised by Copilot.)
  • Error-wrap wordingnode/node.go:639 still wraps as "create L1 client" and node/migration.go:113 as "creating a new L1 client", but the failing op is building the settlement adapter (node.go:690 already uses the clearer "set up L1 settlement client"). Aligning the other two would make startup/migration failures clearer. (Raised by Copilot.)
  • SinkStalledThenUnsubscribe determinism (geth_forwarder_test.go:142) — raw is buffered (cap 1), so the producer's outer select may take <-quit before dequeuing; the intended "blocked on sink <- …" path isn't guaranteed each run (test still passes — it only asserts shutdown + unsubscribe). An unbuffered raw would pin the stall path. Carried over from prior reviews.
  • gethReceiptToEth latent footgun — silently drops every receipt field except Logs; a future L1Client consumer reading Status/BlockNumber would get a zero value with no compile-time signal. Mitigated by the doc comment; known trade-off.

Re @rodrodros's "more complicated than it should be" — the event.NewSubscription rewrite (net −34 lines vs. the hand-rolled forwarder) directly shrinks the concurrency surface; from a bug/race/leak standpoint the implementation is sound. Whether the abstraction boundary itself is worth it is a design-altitude call, not a correctness one.

Verdict: No blockers. Good to merge from a correctness standpoint (note: I can't approve PRs). I couldn't run go test ./l1/ -race here (requires approval), but the package builds cleanly and the forwarder logic + assertions check out on inspection.
• branch feat/l1-settlement-geth

@brbrr brbrr deployed to Development July 2, 2026 12:26 — with GitHub Actions Active
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.

4 participants