Skip to content

relayer: config simplification - #1328

Open
dhfang wants to merge 17 commits into
developmentfrom
feat/connection-first-config
Open

relayer: config simplification#1328
dhfang wants to merge 17 commits into
developmentfrom
feat/connection-first-config

Conversation

@dhfang

@dhfang dhfang commented Aug 10, 2026

Copy link
Copy Markdown

Summary

  • Reworks the relayer config schema to be connection-first: one entry per IBC connection with two client ends (clientA/clientB).
  • Previously remote attestors were defined in the relayer's attestorSet config and local attestors were defined in the top level attestor configuration block. Now remote and local attestor definition is consolidated in a top level attestors config.
  • Removes the relayer's attestorSet config(threshold + attestor list + finality offset, per client end) and instead associates the attestors configured in the top level attestors block with their corresponding light client by associating their self-reported addresses with the addresses queried from the on-chain attestation light client. A misconfigured or stale quorum now fails at startup instead of silently drifting from what the chain actually enforces.
  • Adds live client counterparty validation to ibc config validate --live: confirms each connection's on-chain registered counterparty actually matches what's configured, in both directions.
  • The relayer used to rely on the finality offset configured in the attestorSet to determine when a packet commitment is finalized. This PR removes that as a concept and instead just waits till all the attestors report they can attest to the height of a commitment. This happens in the Relayer's CheckSendFinality/CheckTimeoutFinality/CheckWriteAckFinality pipeline stages where they now determine finality by calling LatestProvableHeight() on the attestor light client implementation of the ProofGenerator abstraction.

The refactors required to support the new configuration format are quite large - I added some comments in the diff to help reviewers parse the changes, but let me know if anyone prefers me to split into smaller PRs.

Closes FOU-1219, FOU-1221

dhfang added 13 commits August 7, 2026 11:48
…list

Replaces relayer.clients[]+routesToRelay[] (joined by alias, with
duplicated counterparty fields) with relayer.connections[], each holding
two client ends (clientA/clientB) whose counterparty is simply the other
end. Replaces attestor.attestations[] + embedded per-client AttestorEntry
objects with one top-level attestors[] list (local and remote together),
referenced by alias from attestorSet.attestors[].

Updates every downstream consumer of the old shape: pipeline opts,
dispatch routing, proofgen attestor resolution, relayer packet handling,
bootstrap's dual-mode gate, and the attestor service/local attestor.
Rewrites testdata/sample.yml and the config_test.go/relayer_test.go
table-driven cases to match relayer.connections[]/top-level attestors[].
Updates every other test file constructing the old ClientConfig/
RouteConfig/AttestationConfig literals inline.
ibc config validate --live now queries each connection's two chains'
routers (ICS02Client.getCounterparty) and confirms the on-chain
registered counterparty actually matches clientA/clientB, in both
directions -- catching a config that names two clients as counterparties
when the chains themselves disagree.

Adds chains.Client.GetCounterparty, regenerates its mock.
e2e/internal/harness/ibclink hand-mirrors link's config YAML shape to
generate configs for containerized relayer/attestor processes in e2e
tests. Since it's a separate Go module, the internal/config refactor
didn't surface this as a compile error -- it would have silently kept
writing the old clients[]+routesToRelay[]+attestor.attestations[] shape.

Updates the file-shape types and buildRelayerFileConfig to emit
relayer.connections[] and a unified top-level attestors[] list. Also
fixes a real gap the old code never needed: attestors referenced by an
explicit RelayerAttestorSet with type: remote were never declared in the
unified list (only locals were), which the real schema's cross-validation
now requires.
…solidate live validation and attestor resolution
@dhfang
dhfang requested a review from a team as a code owner August 10, 2026 06:15
@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown

Greptile Summary

The PR replaces static per-client attestor and finality configuration with startup-time on-chain quorum resolution, connection-first bidirectional configuration, and proof-generator-backed finality checks.

  • Resolves local and remote attestor identities against each light client’s on-chain attestation set.
  • Models relaying as reciprocal connections and adds live counterparty validation in both directions.
  • Uses LatestProvableHeight consistently across send, timeout, acknowledgement, and batch processing.
  • Adds the attestor Info RPC, updated configuration documentation, and extensive unit and integration coverage.

Confidence Score: 5/5

The PR appears safe to merge after the acknowledged branch rebase, with no concrete blocking or independently actionable non-blocking defects identified in the reviewed changes.

The connection model rejects ambiguous client ends, live validation checks both counterparties, startup quorum matching fails safely when configured attestors cannot satisfy on-chain requirements, and finality processors consistently use route-specific provable heights.

Important Files Changed

Filename Overview
link/internal/service/attestor/resolve.go Resolves unified local and remote attestor configuration, treating local setup errors strictly and unavailable remote candidates as best-effort omissions.
link/internal/relay/proofgen/attestation/resolve.go Matches resolved attestors by chain and signer address against each client’s on-chain quorum before constructing its proof generator.
link/internal/livevalidate/connections.go Validates both configured ends against their on-chain registered counterparty client identifiers.
link/internal/config/relayer.go Introduces bidirectional connection-first configuration with uniqueness validation for aliases and chain/client endpoints.
link/internal/relay/pipeline/pipeline.go Wires route-specific proof generators into send, timeout, and acknowledgement finality processors.
link/internal/relay/processors/check_send_finality.go Gates packet sends on whether their transaction height is within the route’s latest provable height.
link/internal/relay/processors/check_timeout_finality.go Gates timeout processing using the timestamp associated with the destination route’s latest provable height.
link/internal/relay/processors/check_write_ack_finality.go Gates acknowledgement relaying on whether the acknowledgement transaction is currently provable.
link/internal/bootstrap/bootstrap.go Integrates attestor resolution, quorum-backed proof-generator construction, and connection-first relayer startup.
link/internal/service/relayer/service.go Derives packet destination routing from the configured counterparty end of each connection.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  C[Connection config] --> V[Live counterparty validation]
  A[Configured attestors] --> I[Local identity or remote Info RPC]
  I --> Q[Match on-chain attestation set and threshold]
  V --> G[Build route proof generators]
  Q --> G
  G --> H[LatestProvableHeight]
  H --> S[Send finality]
  H --> T[Timeout finality]
  H --> W[Write-ack finality]
  S --> R[Relay pipeline]
  T --> R
  W --> R
Loading

Reviews (1): Last reviewed commit: "replace static finality offsets with Lat..." | Re-trigger Greptile

dhfang added 2 commits August 10, 2026 14:25
…n-first-config

# Conflicts:
#	e2e/internal/e2etest/traffic_setup.go
#	e2e/internal/harness/ibclink/attestor.go
#	link/docs/configuration.md
#	link/internal/chains/evm/client.go
#	link/internal/chains/evm/client_test.go
#	link/internal/relay/dispatch/dispatcher_test.go
#	link/internal/relay/pipeline/pipeline_test.go
#	link/internal/service/attestor/service_test.go
@dhfang dhfang changed the title relayer: live attestor-quorum resolution, connection-first config, finality checks via LatestProvableHeight relayer: config simplification Aug 10, 2026
@linear-code

linear-code Bot commented Aug 10, 2026

Copy link
Copy Markdown

FOU-1219

FOU-1221

// Dual mode: if .attestor config is provided, then we can run both relayer and attestor in the same process.
// This might be useful for PoC/testing environments or when an operator wants to run the relayer
// Attestors
local, remote, err := attestor.ResolveFromConfig(ctx, cfg.Attestors, clientSet, signers)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

We resolve both local and remote attestors up front now as opposed to the previous flow which was confusing. We previously initialized the attestor grpc service handlers, which internally resolved the local attestors. We'd pipe that attestor service into the ProofGenerators which retrieved local attestors from it and did their own remote attestor resolution as needed.

output = pipeline.ProcessConcurrently(ctx, stageConcurrency,
NewProcessorMW(deps.Storage, processors.NewCheckPacketCommitment(deps.Chains, deps.Storage)), output)

// wait for the send tx to finalize on the source chain

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

We pipe the ProofGenerators directly into the processors that handle waiting for tx finality so that they can query the LatestProvableHeight method to determine when a packet commitment can be relayed. This replaces static finality offsets configs.

// MatchAttestors resolves self's on-chain attestation set and returns the
// attestors that watch counterparty's chain and whose self-reported address
// appears in it, erroring if too few match to meet the on-chain threshold.
func MatchAttestors(

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

We no longer specify in the configs which attestors belong to which light client in the relayer configs. It is replaced with this logic that associates the attestors with their light clients on startup. This lets us get rid of the attestorSet config block entirely. The attestorSet config block was confusing to dog fooders because you configure it under the client for one chain but it actually should be attesting to the counterparty client's chain.

Comment thread proto/link/attestor.proto
rpc LatestHeight(LatestHeightRequest) returns (LatestHeightResponse);

// Returns identity information about a configured attestor.
rpc Info(InfoRequest) returns (InfoResponse);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added an info endpoint to the attestor so that the relayer can discover the attestor's chain id and address on startup.

continue
}

matched = append(matched, a)

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.

might want to dedup here. with two configured attestors with the same attestor key this could "reach quorum" according to this code (misconfiguration probably, but it would create some confusing errors later).

Comment thread link/cmd/ibc/attestors.go
if attestation, ok := cfg.AttestationByName(token); ok {
alias = attestation.Signer
if attestor, ok := cfg.AttestorByName(token); ok {
alias = attestor.Signer

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.

Given:

attestors:

  • name: attestor-1
    type: remote
    grpc: attestor.example.com:3000

And:

ibc deploy client
--chain 1
--counterparty-chain 8453
--attestors attestor-1

The failure is:

  1. The CLI finds attestor-1 in attestors[].
  2. link/cmd/ibc/attestors.go:13 tries to resolve its signer.
  3. The resolver fails to find a signer and returns the original string "attestor-1".
  4. The EVM driver receives that string and rejects it as invalid attestor address "attestor-1".

// wait for the send tx to finalize on the source chain
// wait for the send tx's packet event to be at or before what the
// destination client can currently prove
checkSendFinality, err := processors.NewCheckSendFinality(deps.Chains, deps.ProofGenerators, route)

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.

🤖: These per-transfer finality checks duplicate the proof-height/timestamp gates already enforced by BatchRecvPacket, BatchTimeoutPacket, and BatchAckPacket. This adds redundant RPCs and leaves TxHeight plus the finalized-time fields with no independent runtime purpose.

AutoRelay AutoRelayConfig `yaml:"autoRelay,omitempty"`
// AutoRelay configures auto-relay for packets flowing FROM this end's
// chain TOWARD the counterparty end.
AutoRelay AutoRelayConfig `yaml:"autoRelay,omitempty"`

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.

🤖: autoRelay.enabled and lookback have no runtime readers. In particular, enabled: false does not disable anything, so the config accepts and documents values that silently have no effect.

// relays, in both directions. ClientA's counterparty is simply ClientB (and
// vice versa).
type ConnectionConfig struct {
Alias string `yaml:"alias"`

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.

the connection alias is required and uniqueness-validated, but ConnectionByAlias has no production callers and is otherwise only used in diagnostics atm. should we remove it?

return New(attestorsSpecs)
}

// New Service constructor. Attestors should have unique aliases

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.

code smell? Alias() is identical to Name() for both implementations and only used to key Service. can we drop it?

clients := make(map[string]string, 2*len(cfg.Connections))
type localAttestor struct{ chainID, keyFile string }
locals := make(map[string]localAttestor)
sourceClients := make(map[string]struct{}, 2*len(cfg.Connections))

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.

🤖: sourceClients exists only to validate cfg.Routes; those routes never reach the generated YAML, while Connections already configure both directions. The harness shape therefore suggests route-level control that the relayer config no longer has.

}

remote = append(remote, a)
default:

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.

🤖: The unsupported-type fallback here and the missing-gRPC check in resolveRemote duplicate authoritative config validation. Production callers cannot reach either state, so maintaining the same schema policy in this resolver risks drift.

}

// Locals returns the subset of attestors this process runs itself.
func (a Attestors) Locals() []AttestorConfig {

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.

looking at how this is used, it's unclear if we need this?

attestorIDs := env.Attestors()
attestorSets := make(map[string]*ibclink.RelayerAttestorSet, len(attestorIDs))
for _, id := range attestorIDs {
declaredAttestors := map[string]bool{}

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.

🤖: Environment.Attestors() enumerates a map whose IDs were already uniqueness-validated, so duplicates cannot reach this loop. declaredAttestors only rechecks an invariant already guaranteed structurally.

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.

2 participants