Skip to content

Source on-chain explicit-state base payload from LCP canonical store#74

Closed
ikehara wants to merge 2 commits into
feature/explicit-state-parallelizationfrom
fix/explicit-state-onchain-base-payload
Closed

Source on-chain explicit-state base payload from LCP canonical store#74
ikehara wants to merge 2 commits into
feature/explicit-state-parallelizationfrom
fix/explicit-state-onchain-base-payload

Conversation

@ikehara

@ikehara ikehara commented Jun 12, 2026

Copy link
Copy Markdown

Problem

queryOnChainCommittedExplicitStateBase rebuilds the ELC base payload from scratch via CreateInitialLightClientState(baseHeight). The rebuilt payload only byte-matches the LCP canonical store at the create-client height: after any committed in-enclave update, the stored canonical client_state is the enclave's round-trip encoding. In particular chain_info_json is parsed into a typed struct and re-serialized by serde inside the enclave, which changes key names (kebab-case → snake_case for fields without a rename), key order, and silently drops unknown keys.

As a result, every speculative batch anchored at the on-chain base after the first committed update is deterministically rejected by LCP's stitch-phase byte-equality verification:

BaseStateMismatch: invalid argument:
  descr=stored speculative base client_state mismatch: client_id=<id> height=<base height>

The activate-client path is unaffected (it uses the LCP canonical base verbatim), so the failure first surfaces on the handshake updateClient path. Reproduced deterministically on cosmos-arbitrum-ibc-lcp E2E CI and on a dev cluster handshake run.

Fix

Use the on-chain commitment only as the (height, state_id) witness and take the base payload bytes verbatim from the LCP canonical query:

  • The on-chain state_id is still threaded into the first unit's prev_state_id (bindFirstUnitToExplicitStateBase), so LCP keeps verifying that the speculative chain anchors at exactly the on-chain committed state.
  • When the LCP canonical latest height does not match the on-chain committed height (canonical drifted ahead of the on-chain commitment), fail fast with both heights in the error instead of letting LCP reject the batch with a byte-level mismatch. Rebasing from an earlier committed height needs LCP-side support for serving historical payloads either way (the canonical store only keeps the latest client_state), so this PR does not change that limitation — it only makes it explicit.

Tests

  • TestQueryOnChainExplicitStateBaseWithFallbackUsesCanonicalPayloadAtOnChainHeight: the returned base carries the canonical payload Anys verbatim and the on-chain state_id.
  • TestQueryOnChainExplicitStateBaseWithFallbackRejectsCanonicalHeightDrift: on-chain/canonical height drift is rejected with both heights in the error.
  • go build ./..., go vet ./relay/, go test ./... pass.

🤖 Generated with Claude Code

Kiyoshi Nakao added 2 commits June 12, 2026 16:13
queryOnChainCommittedExplicitStateBase rebuilt the ELC base payload from
scratch via CreateInitialLightClientState at the on-chain committed
height. The rebuilt payload only matches the LCP canonical store at the
create-client height: after any committed in-enclave update, the stored
canonical client_state is the enclave's round-trip encoding (notably
chain_info_json is re-serialized by serde with different key names,
order, and dropped unknown keys), so LCP's stitch-phase byte-equality
check rejected every speculative batch anchored at the on-chain base
with BaseStateMismatch (stored speculative base client_state mismatch).

Use the on-chain commitment only as the (height, state_id) witness and
take the payload bytes verbatim from the LCP canonical query. The
on-chain state_id is still threaded into the first unit's prev_state_id,
so LCP keeps verifying that the speculative chain anchors at exactly the
committed state. When the canonical latest height does not match the
on-chain committed height (canonical drifted ahead), fail fast with both
heights instead of letting LCP reject the batch with a byte-level
mismatch; serving historical payloads needs LCP-side support either way.
The explicit-state path anchors speculative batches at the on-chain
committed base, but the LCP canonical store routinely advances past the
on-chain commitment: updateELC commits canonical state at the stitch
regardless of whether the caller submits the resulting UpdateClient
message (e.g. a channel handshake step that turns out to need no proof
discards it). Once canonical is ahead, the speculative path cannot
anchor at the on-chain height — the canonical store only serves the
latest payload — and previously every subsequent update attempt failed
without a recovery path.

Type the drift detection as ExplicitStateBaseDriftError and recover in
updateELCForUpdateClient by falling back to the serial update path: the
origin prover builds headers trusted at the on-chain client state, and
the enclave anchors each serial update at its stored consensus for that
height (light clients accept updates from older trusted heights and
only move latest_height forward), producing messages that reconnect the
on-chain client regardless of how far canonical has advanced. The
fallback re-verifies the drifted span, so it is slower than the
speculative path, but it is stateless and heals the drift on the next
submission; subsequent updates use the speculative path again.
@ikehara

ikehara commented Jun 16, 2026

Copy link
Copy Markdown
Author

Superseded by upstream lcp change datachainlab/lcp#143 (commit c6fa4ce "enclave-api: drop inappropriate bincode checks from speculative base verify"), which removes the underlying byte-equality check this PR was working around.

state_id remains the CAS and absorbs canonicalize-equivalent differences in the supplied Anys; value-level divergence still surfaces as a state_id mismatch.

@ikehara ikehara closed this Jun 16, 2026
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.

1 participant