Skip to content

Espresso 3b: TEE batcher (re-hosted) - #459

Open
QuentinI wants to merge 58 commits into
celo-rebase-18from
espresso/batcher
Open

QuentinI wants to merge 58 commits into
celo-rebase-18from
espresso/batcher

Conversation

@QuentinI

@QuentinI QuentinI commented Jun 17, 2026 •

Copy link
Copy Markdown

Based on #448

Pulls in the Espresso/TEE batcher .

  • In op-node: adds EspressoBatch type and marshaling logic for it. This is the datastructure that ends up posted to Espresso.
  • In espresso package: adds the CLI flags and interfaces for the streamer.
  • In the batcher: adds NSM helper in op-batcher/enclave/attestation.go and modifies the driver to add an Espresso path. Bulk of the changes is in espresso_-prefixed files.

This is #447, re-hosted from an in-repo branch (now properly stacked).

Comment thread op-batcher/batcher/service.go Outdated
Comment thread espresso/cli.go
Comment thread op-batcher/batcher/espresso_active.go Outdated
Comment thread op-batcher/batcher/espresso.go Outdated
@QuentinI
QuentinI force-pushed the espresso/batcher branch 2 times, most recently from 6cf6a1f to eb6ff32 Compare June 18, 2026 16:40
Comment thread op-batcher/batcher/espresso_active.go Outdated
Comment thread op-batcher/batcher/espresso_service.go Outdated
Comment thread op-batcher/batcher/driver.go
Comment thread op-batcher/batcher/espresso.go Outdated
Comment thread op-batcher/batcher/driver.go Outdated
Comment thread op-node/rollup/derive/espresso_batch.go Outdated
// Sign represents the interface for signing things via eth_sign.
func (s *SignerClient) Sign(ctx context.Context, address common.Address, data []byte) ([]byte, error) {
var result hexutil.Bytes
if err := s.client.CallContext(ctx, &result, "eth_sign", address, data); err != nil {

@piersy piersy Jul 15, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This eth_sign call can't work against op-signer, and I don't think op-signer should be extended to make it work either.

op-signer doesn't serve eth_sign. Its server registers only two namespaces — eth (eth_signTransaction) and opsigner (signBlockPayload, signBlockPayloadV2): https://github.com/ethereum-optimism/infra/blob/main/op-signer/service/service.go#L73-L82. There is no arbitrary-data signing method. And this SignerClient can only talk to op-signer in the first place: NewSignerClient dials with op-signer's mutual-TLS and then handshakes with a health_status ping before returning, so pointing it at a plain geth or another HSM front-end fails at construction. So the call here errors method-not-found at runtime against a real op-signer.

The reason op-signer has no such method is deliberate, and it's why I'd argue against adding one. An HSM-backed signer must never sign raw bytes the caller hands it. If it did, a compromised batcher could pass a 32-byte value that is really the sighash of an L1 transaction spending the funded key, or a block payload for equivocation, and the HSM would sign it. That's why every op-signer method reconstructs the thing being signed server-side from typed arguments and binds a domain tag and chain id into the hash — see BlockPayloadArgs (domain, chainId, payloadBytes) and Message().ToSigningHash(). The client never sends a bare hash. Adding an eth_sign that signs any digest would remove exactly that protection for a key that also signs L1 transactions.

There's a second, backend-independent problem: eth_sign applies the EIP-191 prefix ("\x19Ethereum Signed Message:\n32" || hash), but the verify side recovers over the raw digest (crypto.SigToPub(batchHash, sig) in op-node/rollup/derive/espresso_batch.go):

batchHash := crypto.Keccak256(batchData)
signerKey, err := crypto.SigToPub(batchHash, signatureData)
So even a signer that did serve eth_sign would recover the wrong address and every batch would be rejected.

If remote HSM signing of Espresso batches is a requirement, the right shape is a purpose-built op-signer method modeled on signBlockPayload: the client sends typed args (a fixed domain tag, the L2 chain id / namespace, and the batch commitment), op-signer reconstructs the domain-separated digest and signs it with the HSM key, and op-node verifies the same digest. That also resolves the separate domain-separation gap (the batch digest is currently a bare keccak256(rlp(batch)) with no namespace binding). It is a change in the op-signer repo, so it can't land from this PR alone — until it exists, only the local private-key ChainSigner actually works. I'd suggest dropping this eth_sign helper and the clientSigner branch here rather than shipping a path that can't sign or verify.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Jean is going to look and respond here as he did this work.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the thorough writeup. The analysis is right about op-signer, and the fact that it reads as targeting op-signer at all is a documentation gap on our side, so let me fill in the missing context first.

This Sign call doesn't talk to op-signer. The --signer.endpoint it's deployed against is espresso-kms-signer (https://github.com/EspressoSystems/espresso-kms-signer), a small AWS KMS signing sidecar we built specifically to speak the signer protocol this batcher uses: health_status, eth_signTransaction, and eth_sign (https://github.com/EspressoSystems/espresso-kms-signer/blob/758e4d0/src/rpc.rs#L26-L46). It runs as an ECS sidecar next to op-batcher-tee and has done full batch-posting cycles on our kms test devnets (batches accepted on L1 and on HotShot). On the client side, NewSignerClient isn't actually op-signer-specific: mTLS only kicks in when tlsConfig.Enabled is set (https://github.com/celo-org/optimism/blob/c828d61cd9/op-service/signer/client.go#L31-L65) (plain HTTP otherwise), and the handshake is just a health_status call into a Go string, which the sidecar answers. That genericness is what let us add KMS signing with no batcher code changes.

On EIP-191: agreed that a standard eth_sign would break recovery, but the sidecar's eth_sign is deliberately non-standard; it signs the raw 32-byte digest with no message prefix and returns r||s||v with v ∈ {0,1}, (i.e. go-ethereum's crypto). Sign convention, which makes it semantically identical to the privateKeySigner path in this PR (crypto.Sign(hash, privKey) (https://github.com/celo-org/optimism/blob/c828d61cd9/op-service/crypto/espresso.go#L161)). Both verify the same way under SigToPub. And it's pinned rather than hoped-for: the sidecar's fixture generator (https://github.com/EspressoSystems/espresso-kms-signer/blob/758e4d0/tests/fixtures/gen/main.go#L158-L178) is a Go program that imports op-service/signer itself and records the exact JSON-RPC params bytes geth's RPC client marshals (including the base64 []byte encoding), which CI replays against the production handler; there's also a localstack test that runs the real KMS path end-to-end and asserts the recovered address. That said, you're completely right that calling a method eth_sign while breaking eth_sign semantics is asking for exactly this confusion. I'd be happy to rename it in a follow-up, and we'll add a doc comment on SignerClient.Sign pointing at the sidecar and its semantics either way. (Small note: the espresso_batch.go verify code you linked has since moved into espresso-streamers digest recovery (https://github.com/EspressoSystems/espresso-streamers/blob/1884a718fbf7/op/derivation/espresso_batch.go#L102-L104).)

On "an HSM signer should never sign raw caller-supplied bytes", no pushback on the principle, and I'd rather be precise about what it costs us here. A raw-digest endpoint does mean the sidecar's eth_signTransaction guards (chainId, from, to-allowlist) only protect against a buggy caller, not a malicious one; anyone who can reach the endpoint can get a signature over an arbitrary digest, including an L1 tx sighash. What bounds the damage is that this key was never a batch-eligibility authority: batch acceptance in TEE mode requires an EIP-712 commitment signature from the ephemeral key generated inside the Nitro enclave and verified on-chain via the TEE verifier; the sidecar never touches that key. So a compromised sidecar (or its host) can spend the batcher address's gas funds and inject noise into our own HotShot namespace, but it can't make derivation accept a batch the enclave didn't produce. That's our documented trust model: the sidecar is trusted for availability, not integrity.

You're also right about the missing domain separation, and that one stands on its own: the namespace lives in the transaction envelope outside the signed bytes, so the signature binds neither chain nor namespace, under the local key just as much as the remote one. Fixing it means changing the digest every verifier reconstructs, so it's a coordinated change across the batcher and espresso-streamers with a migration story for payloads already in the stream, and we'll file it as a tracked issue rather than fold it into this PR.

Where I'd push back is on the remedy. A typed, domain-separated signing method modeled on signBlockPayload is the right end state, but it belongs in espresso-kms-signer (op-signer isn't in this deployment), and it should land together with the digest-scheme change since both alter what verifiers recover over. Dropping clientSigner in the meantime wouldn't remove the capability this comment worries about; it would move the key from KMS hardware into batcher memory, which is a strict downgrade for the same attack surface. So my proposal: keep clientSigner/Sign as-is here, add a comment linking the sidecar and its non-standard semantics, and file two linked follow-ups, one for the typed domain-separated method (including the eth_sign rename/retirement) and one for the digest-scheme migration (which I will discuss with the team). Happy to talk through the typed-method design if you have opinions on the shape!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Any thoughts or remarks @piersy?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi @jjeangal, I hadn't realised this was talking to the kms signer.

So yes, agreed on the followups 👍

Comment thread op-node/rollup/derive/espresso_batch.go Outdated
Comment thread op-node/rollup/derive/espresso_batch.go Outdated
Comment thread op-batcher/batcher/espresso.go Outdated
Comment thread op-batcher/batcher/espresso.go Outdated
@lukeiannucci
lukeiannucci force-pushed the espresso/batcher branch 3 times, most recently from 05288e9 to 78e33c9 Compare July 20, 2026 18:31
philippecamacho and others added 20 commits October 13, 2026 17:14
…ero LocalSafeL2 cannot floor the queue at genesis and re-enqueue derived history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ocalSafeL2 cannot disable the stale-batch re-anchor floor and republish derived blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ent sync-status failure cannot empty the channel manager while leaving the streamer at its old cursor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
isBatcherActive ran its CodeAt existence check on the caller's context,
which is the publishing loop's lifetime killCtx, while every other RPC in
the publish gate (the BatchAuthenticator reads below it and l1Tip in the
fallback-auth gate) is bounded by NetworkTimeout. A stalled L1 RPC could
therefore wedge the publishing loop on every publish tick and block
graceful shutdown until the stop context force-cancels killCtx.

Give CodeAt its own NetworkTimeout window, matching the reads below.

Addresses the Codex P2 review comment on PR #459.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CLI value was never read; all runtime paths use
RollupConfig.BatchAuthenticatorAddress as the single source of truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A zero or negative --espresso.poll-interval previously passed Check()
and panicked later in time.NewTicker inside espressoBatchLoadingLoop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tdown

registerBatcher runs while StartBatchSubmitting holds l.mutex, and
StopBatchSubmitting needs that mutex to cancel killCtx, so an unbounded
receipt wait (txmgr.send-timeout defaults to 0) wedged graceful shutdown.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The between-attempts backoff in espressoVerifyTransactionWorker was a
plain time.Sleep, so a worker mid-backoff stalled StopBatchSubmitting's
wg.Wait() for the full configured retry delay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…p-l1-url

The Espresso L1 URL was parsed and validated but never dialed: light-client
calls always went through the batcher's L1 client, so pointing the flag at a
different RPC was silently ignored. Now a dedicated client is dialed when the
URL differs from --l1-eth-rpc (reusing the batcher's client when it matches),
and closed in Stop. --espresso.rollup-l1-url only ever logged a warning, so
it is removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…2 chain ID

The CLI value was parsed and validated but never read: both runtime
sites (transaction publishing and streamer construction) already derive
the namespace from RollupConfig.L2ChainID, which the proof side pins too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror the zero-field guards in computeSyncActions and nextBlockRange:
a transiently zeroed sync status during clearState would re-anchor the
streamer at genesis and re-serve derived history. Retry the clear instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght-l2

The comments promised a RollupConfig.EspressoOriginBatchPos() fallback
that never existed; zero means no anchor floor (anchor at the current
local-safe head). No default can be derived from the rollup config since
espresso_time is a timestamp whose L2 height is not statically known, so
handoffs from the fallback batcher must set the flag explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two comments point at a flag that was never registered. The note in
espresso/cli.go claims op-batcher/flags/flags.go registers
--espresso.fallback-auth-lead-time, and initEspresso's doc claims the
fallback batcher reads a FallbackAuthLeadTime field on BatcherConfig.
Neither the flag nor the field exists on any branch; both comments date
from the TEE flag import and describe a knob the fallback batcher landed
without.
The test's per-endpoint call counters, the two restarted-server flags and
the recorded shutdown error are all written by httptest handler
goroutines (or by throttlingLoop, for closeApp) and read directly by the
test goroutine, so the package could not run under -race at all: five
reports on every run.

Make each of them atomic. The slices.Contains check over the counters
becomes a small anyUncalled helper, since atomic.Int64 elements can't be
compared by value.
* espresso: delete dead supporting code

Remove code with no callers: espresso/ethclient.go (duplicates
batcherL1Adapter; FetchEspressoBatcherAddress belongs to the caff node in
another repo), opcrypto.Verify and its test, and the
AllowEmptyAttestationService escape hatch, which nothing sets, so Check()
now requires the attestation service URL unconditionally. Also drop a doc
reference to DebouncingHandler, which does not exist.

* espresso: gofmt cli.go after removing the config field
espresso: replace hand-copied bindings with generated op-service/bindings (#513)

* Delete espresso/bindings, an abigen copy that had drifted from the contracts.
* Add op-service/bindings: ABI-only bindings for BatchAuthenticator and SystemConfig, one package per contract, generated from the contracts-bedrock forge artifacts by gen.sh and kept current by the check-bindings CI job.
* Add CI job to detect BatchAuthenticator + SystemConfig bindings drifts

Closes #504.
* shrink txmgr seam, delete SignerFactoryFromConfig and test file

* Revert deletion SignerFactoryFromConfig and test file

SignerFactoryFromConfig had no callers because NewConfig had been moved
onto the fork's ChainSignerFactoryFromConfig to populate
Config.ChainSigner.
Not only don't we want to delete upstream functions, we also want to
remove the modification of the upstream NewConfig in a later commit.

* op-service/txmgr: go back to upstream's SignerFactoryFromConfig

NewConfig was switched onto the fork's ChainSignerFactoryFromConfig so that
Config.ChainSigner could be populated for SimpleTxManager.Sign. Both of those are
gone now, and txmgr only ever uses SignTransaction, which upstream's SignerFactory
already provides.

Switching back makes op-service/txmgr free of Espresso entirely, and gives
SignerFactoryFromConfig its caller back - it only looked like a redundant copy of
ChainSignerFactoryFromConfig because txmgr had been moved off it.

ChainSignerFactoryFromConfig stays: the batcher needs Sign over an arbitrary hash
for the Espresso transaction envelope, which a SignerFn cannot provide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* op-batcher: guard the Espresso ChainSigner against signing-config drift

initChainSigner derives its own signer from cfg.TxMgrConfig, mirroring the HD-path
resolution in txmgr.NewConfig. Nothing checked that the two agreed. If they ever
diverged, Espresso batches would be signed by a key that is not the registered
batcher, the streamer would drop all of them, and L1 submission would carry on.

Check that both derivations resolve to the same address and fail startup if not.

Move the call from initTxManager to initEspresso, next to initKeyPair, so
initTxManager carries no Espresso logic and both signing identities are set up in
one place.

Also note on ChainSignerFactoryFromConfig that upstream's SignerFactoryFromConfig
is not redundant with it: SignerFactory yields a SignerFn, which cannot sign an
arbitrary hash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Maximilian Langenfeld <15726643+ezdac@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… once at startup (#523)

op-batcher: use one BatchAuthenticator reader (#523)

Replace the scattered BatchAuthenticator calls with a single
batchAuthenticatorReader, bound once at startup and shared by
registerBatcher, resolveTEEVerifierAddress and the publish gate.

- Read the fallback batcher from the SystemConfig the BatchAuthenticator
  itself names, cached after the first read (closes #503).
- Bound every read by NetworkTimeout. Rely on the bindings' ErrNoCode for
  a contract that is not deployed yet; registerBatcher keeps an explicit
  CodeAt before sending.
- Reject a zero SystemConfig or TEE verifier address.
- Refuse to start an Espresso batcher with no BatchAuthenticator.
- Throttle the gate's skip and error warnings, and reset the wrong-key
  warning when the batcher's mode goes inactive.
- Test the reader, both gates (results, call counts, warnings,
  throttling) and the callers' error paths.
…527)

* publish op-batcher enclave images from this repo

* op-batcher: redact credential flags and URL secrets in enclave argument log

* op-batcher: proxy --throttle.additional-endpoints through socat in enclave entrypoint

* op-batcher: redact URLs in enclave entrypoint processing logs

* op-batcher: reject https for socat-proxied internal URLs in enclave entrypoint

* op-service/signer: honor proxy environment variables in TLS signer client

* only on dispatch + pin enclaver to prod v0.5.0

* harden enclave arg delivery and CI image tagging

---------

Co-authored-by: Jean Gal <jgatbkk@gmail.com>
)

* publish op-batcher enclave images from this repo

* op-batcher: redact credential flags and URL secrets in enclave argument log

* op-batcher: proxy --throttle.additional-endpoints through socat in enclave entrypoint

* op-batcher: redact URLs in enclave entrypoint processing logs

* op-batcher: reject https for socat-proxied internal URLs in enclave entrypoint

* op-service/signer: honor proxy environment variables in TLS signer client

* only on dispatch + pin enclaver to prod v0.5.0

* op-batcher: expose enclave Prometheus metrics via enclaver ingress

Add a :7300 ingress port to the enclaver manifest and launch op-batcher
with metrics enabled on 0.0.0.0:7300 so the enclaver ingress can bridge
Prometheus metrics out of the Nitro enclave to the parent.

The parent-side bridge (tcp<->vsock) lives in EspressoSystems/tee-image-builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* op-batcher: force enclave metrics onto the fixed 7300 ingress

Review on #536: the previous "skip if caller set --metrics.enabled" guard
let a caller-supplied --metrics.port/addr bind somewhere the enclaver ingress
(fixed at 7300) doesn't bridge, so scrapes failed silently. Drop the guard and
append enabled/addr/port last so they win via urfave/cli's last-value-wins,
guaranteeing the batcher always serves on 0.0.0.0:7300.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* trim comment

---------

Co-authored-by: Keyao Shen <shenkeyao@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 158237f8bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +50 to +52
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-,enable=${{ github.event_name == 'push' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Generate an image tag for tag-ref dispatches

When this workflow is manually dispatched with a tag ref—which GitHub explicitly supports for workflow_dispatch—neither the branch nor PR rule generates a Docker tag, while the SHA rule is disabled because github.event_name is workflow_dispatch, not push. The build step still enables push, so it has no named GHCR image target to publish. Add type=ref,event=tag or enable the SHA tag for manual dispatches. GitHub workflow dispatch reference metadata-action ref semantics

Useful? React with 👍 / 👎.

* op-service: add ChainSigner interface and eth_sign helper

Bring in op-service/crypto/espresso.go (ChainSigner interface unifying
SignTransaction and arbitrary-data signing) and op-service/signer/espresso.go
(SignerClient.Sign wrapper around eth_sign).

Required by the Espresso batcher to sign batch-authentication payloads with
either a remote signer or a local private key, in addition to the existing
transaction-signing path.

Co-authored-by: OpenCode <noreply@opencode.ai>

* op-node: add EspressoBatch type and converters

Introduce op-node/rollup/derive/espresso_batch.go defining EspressoBatch
(a SingularBatch with block number, L1 info deposit transaction, and
signer address attached), along with BlockToEspressoBatch and the
unmarshaler used by the streamer.

Also pulls in the github.com/EspressoSystems/espresso-network/sdks/go
dependency, which provides the Espresso transaction and namespace types.

Consumed by the Espresso batcher (next commits) to convert L2 blocks into
batches submitted to Espresso, and to round-trip those batches back through
the streamer.

Co-authored-by: OpenCode <noreply@opencode.ai>

* espresso: add TEE batcher CLI flags, streamer interface, and L1 adapter

Bring in the parts of the espresso/ shared package that are TEE-only:

- espresso/cli.go: full CLI flag set for --espresso.enabled, query
  service URLs, light-client/L1 endpoints, batch-authenticator address,
  receipt-verification tuning, namespace/origin-height parameters used
  to construct the espresso-streamer. (The --espresso.fallback-auth-lead-time
  flag lives in op-batcher/flags/flags.go and was added by the fallback PR.)
- espresso/interface.go: EspressoStreamer[B] interface that wraps
  github.com/EspressoSystems/espresso-streamers/op.BatchStreamer.
- espresso/ethclient.go: AdaptL1BlockRefClient adapter (used by cli.go to
  construct the streamer) and FetchEspressoBatcherAddress helper.

Also adds the EspressoSystems/espresso-network/sdks/go and
EspressoSystems/espresso-streamers Go module dependencies.

The regenerated BatchAuthenticator bindings already live in the
fallback PR's espresso/bindings/.

Co-authored-by: OpenCode <noreply@opencode.ai>

* op-batcher: add Nitro NSM attestation helper

Bring in op-batcher/enclave/attestation.go: a thin wrapper around the
hf/nsm library that obtains an AWS Nitro NSM attestation document over
a given public key. Used by the Espresso batcher (next commit) to attach
a TEE attestation to its registration with the BatchAuthenticator.

Adds the github.com/hf/nsm dependency. Builds on all platforms; NSM
device access is only attempted at runtime when invoked from inside
a Nitro enclave.

Co-authored-by: OpenCode <noreply@opencode.ai>

* op-batcher: integrate TEE batcher and Espresso submission loop

Add the Espresso TEE batcher write-path on top of the fallback batcher:

- op-batcher/batcher/espresso.go: Espresso submission loop (peeks the
  channel manager, converts each L2 block to an EspressoBatch, submits
  it to Espresso, waits for inclusion, and then posts the batch txs to
  L1 with TEE-attested BatchAuthenticator.authenticateBatchInfo calls).
- op-batcher/batcher/espresso_service.go: EspressoBatcherConfig,
  initEspresso (Espresso client / light-client construction, optional
  TEE attestation gathering), and the initChainSigner hook that wraps
  the txmgr into a opcrypto.ChainSigner.
- op-batcher/batcher/espresso_helpers_test.go and
  espresso_transaction_submitter_test.go: unit tests for the helpers and
  the TEE transaction submitter.

Extends the existing fallback wiring:

- op-batcher/batcher/espresso_driver.go: adds EspressoDriverSetup
  fields (Client/LightClient/ChainSigner/SequencerAddress/Attestation),
  batcherL1Adapter, setupEspressoStreamer, startEspressoLoops,
  resetEspressoStreamer; extends dispatchAuthenticatedSendTx with the
  TEE branch (always authenticates when Espresso.Enabled).
- op-batcher/batcher/espresso_active.go: adds isBatcherActive
  (queries BatchAuthenticator.activeIsEspresso to gate publishing
  against this batcher's role).
- op-batcher/batcher/driver.go: extends DriverSetup with the Espresso
  EspressoDriverSetup field; adds espressoSubmitter / espressoStreamer /
  teeVerifierAddress / degradedLog fields on BatchSubmitter; calls
  setupEspressoStreamer in NewBatchSubmitter; branches
  StartBatchSubmitting on Espresso.Enabled to call startEspressoLoops;
  calls resetEspressoStreamer in clearState.
- op-batcher/batcher/service.go: BatcherConfig.Espresso field;
  EspressoClient / EspressoLightClient / ChainSigner / Attestation
  runtime fields; initEspresso / initChainSigner /
  applyEspressoDriverSetup call-outs.
- op-batcher/batcher/config.go: thread Espresso espresso.CLIConfig
  through CLIConfig.
- op-batcher/flags/flags.go: register espresso.CLIFlags (TEE-only
  flags; the --espresso.fallback-auth-lead-time flag added by the
  fallback PR continues to live in op-batcher/flags/flags.go).

Also adds op-service/log/repeat_state.go (RepeatStateLogger) and its
test, used by the Espresso submission loop's tick-driven warnings.
A safeTestRecorder helper is inlined into the test to avoid pulling
in the unrelated debouncer.

Adds the github.com/hf/nitrite dependency (transitively required by
hf/nsm for attestation document parsing).

Co-authored-by: OpenCode <noreply@opencode.ai>

* op-batcher: route TEE auth through ordered tx queue

The TEE batcher's Espresso submission path called Txmgr.Send directly for both
the authenticateBatchInfo tx and the batch inbox tx, and ran inside authGroup,
so it bypassed MaxPendingTransactions, assigned nonces nondeterministically
(violating Holocene's in-order L1 inclusion requirement), and never checked
whether the auth tx reverted — a reverted authenticateBatchInfo emits no event,
so the verifier would silently drop the batch.

Submit both txs through the ordered queue.Send path on the publishing-loop
goroutine (auth first, batch second) so the auth tx takes the lower nonce and is
mined first, and both stay under MaxPendingTransactions. A watcher goroutine
(tracked by authGroup) collects both receipts, fails the pair if the auth tx
reverted, runs the lookback-window check, and emits a single synthetic receipt.

This is the same fix already applied to the fallback path; extract the shared
submission + receipt-watching flow into submitAuthenticatedBatch /
watchAuthReceipts so both paths reuse it and differ only in how the
authenticateBatchInfo calldata is built (TEE-attested signature vs empty
signature). Rename fallback_auth.go to espresso_auth.go to reflect the shared
scope, and restructure the tests: one suite drives the shared flow directly,
plus per-path tests asserting the distinguishing auth calldata (empty sig vs a
recoverable EIP-712 signature).

Co-authored-by: OpenCode <noreply@opencode.ai>

* op-node: add EspressoBatch roundtrip test, fix derive_test vet failure

Port TestBatchRoundtrip from the integration branch and fold it into
espresso_batch_test.go. It is the only test covering ToEspressoTransaction
and the batcher->derivation serialization path; it asserts the decoded batch
matches the original and that the recovered signer is the batcher.

Also drop the decodedBlock.ExecutionWitness() comparison in
TestEspressoBatchConversion: that method does not exist on the op-geth
types.Block pinned in the rebase-18 base, so go vet of the derive_test
package failed to build. EspressoBatch/ToBlock carries no execution witness.

Co-authored-by: OpenCode <noreply@opencode.ai>

* verify batches while converting to a block

* start throttling loop after espresso/non espresso loops

* make use of unsafeBytesUpdated

* initChainSigner fallback for non espresso mode

* lint and make use of isBatcherActive

* request to clear state from BlockLoader

* safety check for namespace and nil l1 deposit

* remove duplicate files between espresso streamer and upstream

* address comments

* remove uneeded comments

* update unsafe bytes every 100 blocks. matching non espresso path

* always set the prev sync status

* log at warn level if batcher isnt active

* check the batcher address before publishing to avoid reverts

* copy espresso.go over from our fork

* proper version of streamer

* address comment, remove uneeded methods and compile time assertion

* use sync status interface for streamer

* empty commit to trigger CI

* point streamer to v2 (espresso-streamers PR 36) and adapt the batcher

Addresses the stall found in the PR 459 review of peekNextBatch: the
old streamer could discard a good batch when the channel manager tip
and the streamer position described different blocks, stalling it for
good. The v2 streamer owns the tip tracking: position and tip hash move
together, repositioning is explicit via SetBatchPosition, and Reset /
SetProperHead / Refresh / Update are gone.

- pin espresso-streamers to 22c396aa (PR 36, op streamer v2)
- construct the streamer in StartBatchSubmitting (needs a context and
  a synced L2 node to resolve its anchor), anchor it at the safe L2
  head, and run it from its own poll loops via Start/Stop
- drop peekNextBatch and the cachedSyncStatus provider; the loading
  loop now just Peeks batches extending the tracked tip and advances
  after the channel manager accepts them
- re-anchor instead of skipping on ToBlock/AddL2Block failures
- drop the dead BatchStreamerFromCLIConfig (no callers, v1-only API)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Check resolveTEEVerifierAddress first so that the streamer is not left running after an error.

* Use LocalSafeL2 instead of SafeL2 so that Channel Manager and Streamer reset on the same heads.

* Roll back running state on Espresso setup failure so that a failed start does not block later start attempts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Anchor the streamer at the local-safe head during construction so that startup does not depend on the L2 endpoint serving the historical origin height.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Floor every streamer anchor and sync comparison at the caffeination point so that the Espresso batcher never treats pre-activation blocks as its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Bound every raw RPC read on the Espresso startup path with the network timeout so that a stalled endpoint cannot hang StartBatchSubmitting while it holds the start mutex.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Re-anchor the streamer instead of queueing batches at or below the local-safe head so that already-derived blocks are not resubmitted after a restart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Gate startup on the caffeination point being local-safe instead of anchoring on a cached unsafe floor block, whose reorg could permanently wedge fork selection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Retry on zeroed sync-status fields in nextBlockRange so a transient zero LocalSafeL2 cannot floor the queue at genesis and re-enqueue derived history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Skip the drain tick when the sync status is out of sync so a zeroed LocalSafeL2 cannot disable the stale-batch re-anchor floor and republish derived blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fetch the streamer re-anchor target before clearing state so a transient sync-status failure cannot empty the channel manager while leaving the streamer at its old cursor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* op-batcher: bound the active-check CodeAt call with NetworkTimeout

isBatcherActive ran its CodeAt existence check on the caller's context,
which is the publishing loop's lifetime killCtx, while every other RPC in
the publish gate (the BatchAuthenticator reads below it and l1Tip in the
fallback-auth gate) is bounded by NetworkTimeout. A stalled L1 RPC could
therefore wedge the publishing loop on every publish tick and block
graceful shutdown until the stop context force-cancels killCtx.

Give CodeAt its own NetworkTimeout window, matching the reads below.

Addresses the Codex P2 review comment on PR #459.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Empty commit to trigger CI

* op-batcher: remove dead --espresso.batch-authenticator-addr flag

The CLI value was never read; all runtime paths use
RollupConfig.BatchAuthenticatorAddress as the single source of truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* espresso: reject non-positive poll intervals in config validation

A zero or negative --espresso.poll-interval previously passed Check()
and panicked later in time.NewTicker inside espressoBatchLoadingLoop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* op-batcher: bound the registration send so startup can't deadlock shutdown

registerBatcher runs while StartBatchSubmitting holds l.mutex, and
StopBatchSubmitting needs that mutex to cancel killCtx, so an unbounded
receipt wait (txmgr.send-timeout defaults to 0) wedged graceful shutdown.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* op-batcher: make the verify-receipt retry delay cancellation-aware

The between-attempts backoff in espressoVerifyTransactionWorker was a
plain time.Sleep, so a worker mid-backoff stalled StopBatchSubmitting's
wg.Wait() for the full configured retry delay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* op-batcher: dial --espresso.l1-url for light-client reads, drop rollup-l1-url

The Espresso L1 URL was parsed and validated but never dialed: light-client
calls always went through the batcher's L1 client, so pointing the flag at a
different RPC was silently ignored. Now a dedicated client is dialed when the
URL differs from --l1-eth-rpc (reusing the batcher's client when it matches),
and closed in Stop. --espresso.rollup-l1-url only ever logged a warning, so
it is removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* op-batcher: remove dead --espresso.namespace flag, namespace is the L2 chain ID

The CLI value was parsed and validated but never read: both runtime
sites (transaction publishing and streamer construction) already derive
the namespace from RollupConfig.L2ChainID, which the proof side pins too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* op-batcher: reject zeroed LocalSafeL2 as the streamer re-anchor target

Mirror the zero-field guards in computeSyncActions and nextBlockRange:
a transiently zeroed sync status during clearState would re-anchor the
streamer at genesis and re-serve derived history. Retry the clear instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* op-batcher: document the real zero semantics of --espresso.origin-height-l2

The comments promised a RollupConfig.EspressoOriginBatchPos() fallback
that never existed; zero means no anchor floor (anchor at the current
local-safe head). No default can be derived from the rollup config since
espresso_time is a timestamp whose L2 height is not statically known, so
handoffs from the fallback batcher must set the flag explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* espresso: drop stale references to --espresso.fallback-auth-lead-time

Two comments point at a flag that was never registered. The note in
espresso/cli.go claims op-batcher/flags/flags.go registers
--espresso.fallback-auth-lead-time, and initEspresso's doc claims the
fallback batcher reads a FallbackAuthLeadTime field on BatcherConfig.
Neither the flag nor the field exists on any branch; both comments date
from the TEE flag import and describe a knob the fallback batcher landed
without.

* op-batcher: fix the data races in TestBatchSubmitter_ThrottlingEndpoints

The test's per-endpoint call counters, the two restarted-server flags and
the recorded shutdown error are all written by httptest handler
goroutines (or by throttlingLoop, for closeApp) and read directly by the
test goroutine, so the package could not run under -race at all: five
reports on every run.

Make each of them atomic. The slices.Contains check over the counters
becomes a small anyUncalled helper, since atomic.Int64 elements can't be
compared by value.

* espresso: delete dead supporting code (#512)

* espresso: delete dead supporting code

Remove code with no callers: espresso/ethclient.go (duplicates
batcherL1Adapter; FetchEspressoBatcherAddress belongs to the caff node in
another repo), opcrypto.Verify and its test, and the
AllowEmptyAttestationService escape hatch, which nothing sets, so Check()
now requires the attestation service URL unconditionally. Also drop a doc
reference to DebouncingHandler, which does not exist.

* espresso: gofmt cli.go after removing the config field

* espresso: deduplicate the generated contract bindings (#513)

espresso: replace hand-copied bindings with generated op-service/bindings (#513)

* Delete espresso/bindings, an abigen copy that had drifted from the contracts.
* Add op-service/bindings: ABI-only bindings for BatchAuthenticator and SystemConfig, one package per contract, generated from the contracts-bedrock forge artifacts by gen.sh and kept current by the check-bindings CI job.
* Add CI job to detect BatchAuthenticator + SystemConfig bindings drifts

Closes #504.

* espresso: remove the txmgr seam, keep upstream's signer factory (#520)

* shrink txmgr seam, delete SignerFactoryFromConfig and test file

* Revert deletion SignerFactoryFromConfig and test file

SignerFactoryFromConfig had no callers because NewConfig had been moved
onto the fork's ChainSignerFactoryFromConfig to populate
Config.ChainSigner.
Not only don't we want to delete upstream functions, we also want to
remove the modification of the upstream NewConfig in a later commit.

* op-service/txmgr: go back to upstream's SignerFactoryFromConfig

NewConfig was switched onto the fork's ChainSignerFactoryFromConfig so that
Config.ChainSigner could be populated for SimpleTxManager.Sign. Both of those are
gone now, and txmgr only ever uses SignTransaction, which upstream's SignerFactory
already provides.

Switching back makes op-service/txmgr free of Espresso entirely, and gives
SignerFactoryFromConfig its caller back - it only looked like a redundant copy of
ChainSignerFactoryFromConfig because txmgr had been moved off it.

ChainSignerFactoryFromConfig stays: the batcher needs Sign over an arbitrary hash
for the Espresso transaction envelope, which a SignerFn cannot provide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* op-batcher: guard the Espresso ChainSigner against signing-config drift

initChainSigner derives its own signer from cfg.TxMgrConfig, mirroring the HD-path
resolution in txmgr.NewConfig. Nothing checked that the two agreed. If they ever
diverged, Espresso batches would be signed by a key that is not the registered
batcher, the streamer would drop all of them, and L1 submission would carry on.

Check that both derivations resolve to the same address and fail startup if not.

Move the call from initTxManager to initEspresso, next to initKeyPair, so
initTxManager carries no Espresso logic and both signing identities are set up in
one place.

Also note on ChainSignerFactoryFromConfig that upstream's SignerFactoryFromConfig
is not redundant with it: SignerFactory yields a SignerFn, which cannot sign an
arbitrary hash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Maximilian Langenfeld <15726643+ezdac@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* op-service: make the RPC pre-flight check proxy-aware

* op-service: resolve the proxy for ws/wss URLs in the pre-flight check

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* op-service: pin the non-local availability test to no proxy

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* op-service: address review on the proxy-aware pre-flight check

* op-batcher: use one BatchAuthenticator reader, bound and code-checked once at startup (#523)

op-batcher: use one BatchAuthenticator reader (#523)

Replace the scattered BatchAuthenticator calls with a single
batchAuthenticatorReader, bound once at startup and shared by
registerBatcher, resolveTEEVerifierAddress and the publish gate.

- Read the fallback batcher from the SystemConfig the BatchAuthenticator
  itself names, cached after the first read (closes #503).
- Bound every read by NetworkTimeout. Rely on the bindings' ErrNoCode for
  a contract that is not deployed yet; registerBatcher keeps an explicit
  CodeAt before sending.
- Reject a zero SystemConfig or TEE verifier address.
- Refuse to start an Espresso batcher with no BatchAuthenticator.
- Throttle the gate's skip and error warnings, and reset the wrong-key
  warning when the batcher's mode goes inactive.
- Test the reader, both gates (results, call counts, warnings,
  throttling) and the callers' error paths.

* Add enclave tools / docker building for TEE image to celo repository (#527)

* publish op-batcher enclave images from this repo

* op-batcher: redact credential flags and URL secrets in enclave argument log

* op-batcher: proxy --throttle.additional-endpoints through socat in enclave entrypoint

* op-batcher: redact URLs in enclave entrypoint processing logs

* op-batcher: reject https for socat-proxied internal URLs in enclave entrypoint

* op-service/signer: honor proxy environment variables in TLS signer client

* only on dispatch + pin enclaver to prod v0.5.0

* harden enclave arg delivery and CI image tagging

---------

Co-authored-by: Jean Gal <jgatbkk@gmail.com>

* op-batcher: expose enclave Prometheus metrics via enclaver ingress (#536)

* publish op-batcher enclave images from this repo

* op-batcher: redact credential flags and URL secrets in enclave argument log

* op-batcher: proxy --throttle.additional-endpoints through socat in enclave entrypoint

* op-batcher: redact URLs in enclave entrypoint processing logs

* op-batcher: reject https for socat-proxied internal URLs in enclave entrypoint

* op-service/signer: honor proxy environment variables in TLS signer client

* only on dispatch + pin enclaver to prod v0.5.0

* op-batcher: expose enclave Prometheus metrics via enclaver ingress

Add a :7300 ingress port to the enclaver manifest and launch op-batcher
with metrics enabled on 0.0.0.0:7300 so the enclaver ingress can bridge
Prometheus metrics out of the Nitro enclave to the parent.

The parent-side bridge (tcp<->vsock) lives in EspressoSystems/tee-image-builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* op-batcher: force enclave metrics onto the fixed 7300 ingress

Review on #536: the previous "skip if caller set --metrics.enabled" guard
let a caller-supplied --metrics.port/addr bind somewhere the enclaver ingress
(fixed at 7300) doesn't bridge, so scrapes failed silently. Drop the guard and
append enabled/addr/port last so they win via urfave/cli's last-value-wins,
guaranteeing the batcher always serves on 0.0.0.0:7300.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* trim comment

---------

Co-authored-by: Keyao Shen <shenkeyao@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* add reviewer requested dial tests

---------

Co-authored-by: Artemii Gerasimovich <artemii@espressosys.com>
Co-authored-by: OpenCode <noreply@opencode.ai>
Co-authored-by: Luke Iannucci <luke.iannucci@outlook.com>
Co-authored-by: Philippe Camacho <philippe@espressosys.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Paul Lange <palango@gmx.de>
Co-authored-by: Paul Lange <palango@users.noreply.github.com>
Co-authored-by: Jean Gal <45081726+jjeangal@users.noreply.github.com>
Co-authored-by: Maximilian Langenfeld <15726643+ezdac@users.noreply.github.com>
Co-authored-by: Jean Gal <jgatbkk@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ccef597df0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


# Re-populate the arguments passed through the environment
if [ -n "$ENCLAVE_BATCHER_ARGS" ]; then
eval set -- "$ENCLAVE_BATCHER_ARGS"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parse enclave arguments without shell evaluation

When ENCLAVE_BATCHER_ARGS contains shell metacharacters—whether from templated deployment configuration or another external source—eval executes them inside the attested enclave instead of treating them as batcher arguments. This permits command substitution, redirection, or background processes before op-batcher starts, undermining the guarantee that the measured image is running only the configured batcher; pass arguments through the existing NUL-delimited channel or another non-evaluating array encoding.

Useful? React with 👍 / 👎.

@jjeangal
jjeangal removed this pull request from stack #522 October 20, 2026 14:27
Sneh1999 and others added 2 commits October 21, 2026 17:31
* fix: Add network timeout ctx to Espresso client

* fix: address Espresso client timeout review feedback

* op-batcher: fix Espresso timeout wiring and add regression coverage

Wrap the SDK client during initialization and preserve nil when disabled.
Cover SDK deadlines, caller cancellation, adapter contexts, and streamer
stop/start cleanup. Simplify client interfaces, naming, and comments.

* fix(op-batcher): apply Espresso timeouts at call sites

Preserve caller cancellation and bound SDK requests, streamer setup,
and Peek calls. Add regression coverage for timeout recovery and
streamer lifecycle cleanup.

* fix(op-batcher): bound Espresso transaction signing by network timeout

---------

Co-authored-by: Jean Gal <jgatbkk@gmail.com>
The publish gate decides before every batch transaction whether this batcher may post, but its outcome was only visible in throttled logs. With the Espresso and fallback batchers in separate pods, a chain where
neither is active went unnoticed.

Closes #553.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ezdac
ezdac added this pull request to stack #565 October 22, 2026 13:09
…538)

The streamer, the transaction submitter and the resolved TEE verifier address are per-start state, but only the streamer was dropped on stop, so the submitter and verifier address outlived the run that created
them. Bundle the three into an espressoSession that is built in full, published to l.espresso only once a start succeeds, and dropped as a whole on stop. A stale or half-built session is now unrepresentable
rather than handled, and a failed start leaves nothing to roll back.

Also drops the uncalled BatcherService.EspressoStreamer accessor.

Closes #502.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a7cc8950c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

echo "=== Enclave Environment Debug Info ==="
echo "PATH: $PATH"
echo "Working directory: $(pwd)"
echo "Proxy: ${http_proxy:-not set}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Redact credentials before logging the proxy URL

When http_proxy contains standard URL userinfo such as http://user:password@proxy:10000, this startup message writes those credentials verbatim to the enclave logs; line 67 exposes the same value again through HTTPS_PROXY. Since the script already redacts credentials from other logged URLs, apply equivalent redaction here (or log only the proxy host and port).

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
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.

8 participants