Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 211 additions & 26 deletions relay/explicit_state_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"math/big"
"net"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -538,20 +540,6 @@ func makeExplicitStateSourceHeaderUnitStream(
return ch
}

type fakeOriginProverWithInitialState struct {
fakeOriginProver
requestedHeights []ibcexported.Height
}

func (p *fakeOriginProverWithInitialState) CreateInitialLightClientState(_ context.Context, height ibcexported.Height) (ibcexported.ClientState, ibcexported.ConsensusState, error) {
p.requestedHeights = append(p.requestedHeights, height)
h, ok := height.(clienttypes.Height)
if !ok {
return nil, nil, fmt.Errorf("unexpected height type: %T", height)
}
return &lcptypes.ClientState{LatestHeight: h}, &lcptypes.ConsensusState{StateId: []byte(fmt.Sprintf("elc-state-%d", h.RevisionHeight))}, nil
}

type fakeOnChainLCPChain struct {
core.FinalityAwareChain
queryHeight ibcexported.Height
Expand Down Expand Up @@ -1156,41 +1144,238 @@ func TestQueryLCPCanonicalExplicitStateBase(t *testing.T) {
}
}

func TestQueryOnChainExplicitStateBaseWithFallbackUsesOnChainCommittedHeight(t *testing.T) {
func newCanonicalQueryConn(t *testing.T, svc elc.QueryServer) *grpc.ClientConn {
t.Helper()
lis := bufconn.Listen(1024 * 1024)
server := grpc.NewServer()
elc.RegisterQueryServer(server, svc)
t.Cleanup(server.Stop)
go func() {
if err := server.Serve(lis); err != nil {
panic(err)
}
}()
conn, err := grpc.DialContext(
context.Background(),
"bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatalf("grpc.DialContext() error = %v", err)
}
t.Cleanup(func() { _ = conn.Close() })
return conn
}

// explicitStateFixedCanonicalServer serves a canonical state pinned at a fixed
// height, with payload bytes that are distinguishable from anything the
// relayer could rebuild locally.
type explicitStateFixedCanonicalServer struct {
elc.UnimplementedQueryServer
clientID string
clientState *codectypes.Any
consensusState *codectypes.Any
}

func (s *explicitStateFixedCanonicalServer) Client(_ context.Context, req *elc.QueryClientRequest) (*elc.QueryClientResponse, error) {
if req.ClientId != s.clientID {
return &elc.QueryClientResponse{Found: false}, nil
}
return &elc.QueryClientResponse{
Found: true,
ClientState: s.clientState,
ConsensusState: s.consensusState,
}, nil
}

func TestQueryOnChainExplicitStateBaseWithFallbackUsesCanonicalPayloadAtOnChainHeight(t *testing.T) {
if err := ylog.InitLogger("error", "text", "null", false); err != nil {
t.Fatalf("InitLogger() error = %v", err)
}
interfaceRegistry := codectypes.NewInterfaceRegistry()
std.RegisterInterfaces(interfaceRegistry)
lcptypes.RegisterInterfaces(interfaceRegistry)
coreCodec := codec.NewProtoCodec(interfaceRegistry)

onChainHeight := clienttypes.Height{RevisionHeight: 9}
originProver := &fakeOriginProverWithInitialState{}
canonicalClientStateAny, err := clienttypes.PackClientState(&lcptypes.ClientState{LatestHeight: onChainHeight})
if err != nil {
t.Fatalf("PackClientState() error = %v", err)
}
canonicalConsensusStateAny, err := clienttypes.PackConsensusState(&lcptypes.ConsensusState{StateId: []byte("canonical-state-9")})
if err != nil {
t.Fatalf("PackConsensusState() error = %v", err)
}
conn := newCanonicalQueryConn(t, &explicitStateFixedCanonicalServer{
clientID: "07-tendermint-11",
clientState: canonicalClientStateAny,
consensusState: canonicalConsensusStateAny,
})

chain := &fakeOnChainLCPChain{
queryHeight: clienttypes.Height{RevisionHeight: 100},
clientStateHeight: onChainHeight,
stateID: []byte("on-chain-state-9"),
}
base, err := (&Prover{
codec: coreCodec,
originProver: originProver,
codec: coreCodec,
lcpServiceClient: NewLCPServiceClient(conn),
}).queryOnChainExplicitStateBaseWithFallback(context.Background(), chain, "07-tendermint-11")
if err != nil {
t.Fatalf("queryOnChainExplicitStateBaseWithFallback() error = %v", err)
}
if base == nil || base.Height.RevisionHeight != onChainHeight.RevisionHeight {
t.Fatalf("unexpected base height: %#v", base)
}
if len(originProver.requestedHeights) != 1 || originProver.requestedHeights[0].GetRevisionHeight() != onChainHeight.RevisionHeight {
t.Fatalf("expected origin prover to build base at on-chain height, got %#v", originProver.requestedHeights)
}
if len(chain.consensusQueries) != 1 || chain.consensusQueries[0].GetRevisionHeight() != onChainHeight.RevisionHeight {
t.Fatalf("expected consensus query at on-chain height, got %#v", chain.consensusQueries)
}
var clientState ibcexported.ClientState
if err := coreCodec.UnpackAny(base.ClientState, &clientState); err != nil {
t.Fatalf("failed to unpack base client_state: %v", err)
if base.ClientState.TypeUrl != canonicalClientStateAny.TypeUrl ||
!bytes.Equal(base.ClientState.Value, canonicalClientStateAny.Value) {
t.Fatalf("base client_state must be the canonical payload verbatim: %#v", base.ClientState)
}
if base.ConsensusState.TypeUrl != canonicalConsensusStateAny.TypeUrl ||
!bytes.Equal(base.ConsensusState.Value, canonicalConsensusStateAny.Value) {
t.Fatalf("base consensus_state must be the canonical payload verbatim: %#v", base.ConsensusState)
}
if !bytes.Equal(base.StateId, []byte("on-chain-state-9")) {
t.Fatalf("base state_id must come from the on-chain commitment: %#v", base.StateId)
}
}

func TestQueryOnChainExplicitStateBaseWithFallbackRejectsCanonicalHeightDrift(t *testing.T) {
if err := ylog.InitLogger("error", "text", "null", false); err != nil {
t.Fatalf("InitLogger() error = %v", err)
}
interfaceRegistry := codectypes.NewInterfaceRegistry()
std.RegisterInterfaces(interfaceRegistry)
lcptypes.RegisterInterfaces(interfaceRegistry)
coreCodec := codec.NewProtoCodec(interfaceRegistry)

canonicalHeight := clienttypes.Height{RevisionHeight: 12}
canonicalClientStateAny, err := clienttypes.PackClientState(&lcptypes.ClientState{LatestHeight: canonicalHeight})
if err != nil {
t.Fatalf("PackClientState() error = %v", err)
}
canonicalConsensusStateAny, err := clienttypes.PackConsensusState(&lcptypes.ConsensusState{StateId: []byte("canonical-state-12")})
if err != nil {
t.Fatalf("PackConsensusState() error = %v", err)
}
conn := newCanonicalQueryConn(t, &explicitStateFixedCanonicalServer{
clientID: "07-tendermint-11",
clientState: canonicalClientStateAny,
consensusState: canonicalConsensusStateAny,
})

chain := &fakeOnChainLCPChain{
queryHeight: clienttypes.Height{RevisionHeight: 100},
clientStateHeight: clienttypes.Height{RevisionHeight: 9},
stateID: []byte("on-chain-state-9"),
}
_, err = (&Prover{
codec: coreCodec,
lcpServiceClient: NewLCPServiceClient(conn),
}).queryOnChainExplicitStateBaseWithFallback(context.Background(), chain, "07-tendermint-11")
if err == nil {
t.Fatalf("expected drift between on-chain and canonical heights to be rejected")
}
var driftErr *ExplicitStateBaseDriftError
if !errors.As(err, &driftErr) {
t.Fatalf("expected ExplicitStateBaseDriftError, got: %v", err)
}
if driftErr.OnChainHeight.RevisionHeight != 9 || driftErr.CanonicalHeight.RevisionHeight != 12 {
t.Fatalf("unexpected drift heights: %#v", driftErr)
}
if !strings.Contains(err.Error(), "on_chain_height=0-9") || !strings.Contains(err.Error(), "lcp_canonical_height=0-12") {
t.Fatalf("expected error to carry both heights, got: %v", err)
}
}

func TestUpdateELCForUpdateClientFallsBackToSerialOnExplicitStateBaseDrift(t *testing.T) {
if err := ylog.InitLogger("error", "text", "null", false); err != nil {
t.Fatalf("InitLogger() error = %v", err)
}

lis := bufconn.Listen(1024 * 1024)
server := grpc.NewServer()
svc := &explicitStateParityTestServer{}
elc.RegisterQueryServer(server, svc)
elc.RegisterMsgServer(server, svc)
defer server.Stop()
go func() {
if err := server.Serve(lis); err != nil {
panic(err)
}
}()

conn, err := grpc.DialContext(
context.Background(),
"bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatalf("grpc.DialContext() error = %v", err)
}
defer conn.Close()

interfaceRegistry := codectypes.NewInterfaceRegistry()
std.RegisterInterfaces(interfaceRegistry)
lcptypes.RegisterInterfaces(interfaceRegistry)
coreCodec := codec.NewProtoCodec(interfaceRegistry)

headers := []core.Header{
&tmclienttypes.Header{TrustedHeight: clienttypes.Height{RevisionHeight: 10}},
&tmclienttypes.Header{TrustedHeight: clienttypes.Height{RevisionHeight: 11}},
}
// On-chain LCP client at 9 while the canonical query (parity server)
// reports 10 — a canonical-ahead drift the speculative path cannot
// anchor at.
driftChain := &fakeOnChainLCPChain{
queryHeight: clienttypes.Height{RevisionHeight: 100},
clientStateHeight: clienttypes.Height{RevisionHeight: 9},
stateID: []byte("on-chain-state-9"),
}
if got := clientState.GetLatestHeight(); got.GetRevisionHeight() != onChainHeight.RevisionHeight {
t.Fatalf("unexpected packed base client_state height: %v", got)

results, err := (&Prover{
config: ProverConfig{
ElcClientId: "07-tendermint-11",
EnableExplicitStateUpdateClient: true,
},
codec: coreCodec,
originProver: fakeOriginProver{
headers: headers,
explicitStateChunks: mustExplicitStateSourceUnitsWithBaseStatesFromHeaders(t, headers...),
},
lcpServiceClient: NewLCPServiceClient(conn),
activeEnclaveKey: &enclave.EnclaveKeyInfo{
KeyInfo: &enclave.EnclaveKeyInfo_Ias{
Ias: &enclave.IASEnclaveKeyInfo{
EnclaveKeyAddress: common.HexToAddress("0x1111111111111111111111111111111111111111").Bytes(),
},
},
},
}).updateELCForUpdateClient(
context.Background(),
driftChain,
headers[len(headers)-1],
)
if err != nil {
t.Fatalf("updateELCForUpdateClient() error = %v", err)
}
if svc.batchCalls != 0 {
t.Fatalf("expected no speculative batch on drift, got %d", svc.batchCalls)
}
if svc.updateCalls != len(headers) {
t.Fatalf("expected serial fallback to update per header, got %d calls", svc.updateCalls)
}
if len(results) != len(headers) {
t.Fatalf("unexpected result count: %d", len(results))
}
}

Expand Down
71 changes: 45 additions & 26 deletions relay/explicit_state_update_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ func isExplicitStateBaseStateMismatchError(err error) bool {
strings.Contains(msg, "stored speculative base")
}

// ExplicitStateBaseDriftError indicates that the LCP canonical state has
// advanced past the on-chain committed state, so the explicit-state path
// cannot anchor a speculative batch at the on-chain base.
type ExplicitStateBaseDriftError struct {
OnChainHeight clienttypes.Height
CanonicalHeight clienttypes.Height
}

func (e *ExplicitStateBaseDriftError) Error() string {
return fmt.Sprintf(
"LCP canonical state is ahead of the on-chain committed state: on_chain_height=%v lcp_canonical_height=%v",
e.OnChainHeight, e.CanonicalHeight,
)
}

func (pr *Prover) queryLCPCanonicalExplicitStateBase(ctx context.Context, elcClientID string) (*ExplicitStateBase, error) {
res, err := pr.lcpServiceClient.Client(ctx, &elc.QueryClientRequest{
ClientId: elcClientID,
Expand Down Expand Up @@ -98,7 +113,7 @@ func (pr *Prover) queryLCPCanonicalExplicitStateBase(ctx context.Context, elcCli
// does not host an LCP client (e.g. test/mock configurations); query
// failures are returned as errors instead of silently weakening the base.
func (pr *Prover) queryOnChainExplicitStateBaseWithFallback(ctx context.Context, dstChain core.FinalityAwareChain, elcClientID string) (*ExplicitStateBase, error) {
base, ok, err := pr.queryOnChainCommittedExplicitStateBase(ctx, dstChain)
base, ok, err := pr.queryOnChainCommittedExplicitStateBase(ctx, dstChain, elcClientID)
if err != nil {
return nil, err
}
Expand All @@ -113,7 +128,15 @@ func (pr *Prover) queryOnChainExplicitStateBaseWithFallback(ctx context.Context,
return pr.queryLCPCanonicalExplicitStateBase(ctx, elcClientID)
}

func (pr *Prover) queryOnChainCommittedExplicitStateBase(ctx context.Context, dstChain core.FinalityAwareChain) (*ExplicitStateBase, bool, error) {
// queryOnChainCommittedExplicitStateBase anchors the explicit-state base at
// the on-chain committed (height, state_id) and sources the base payload
// bytes from the LCP canonical store. The payload must be the canonical
// bytes verbatim: LCP's stitch-phase verification compares the supplied
// base against the stored canonical state byte-for-byte, and any payload
// re-encoded outside the enclave (e.g. rebuilt from chain queries) diverges
// from the enclave round-trip encoding. The on-chain state_id is kept as
// the witness binding the canonical payload to the committed state.
func (pr *Prover) queryOnChainCommittedExplicitStateBase(ctx context.Context, dstChain core.FinalityAwareChain, elcClientID string) (*ExplicitStateBase, bool, error) {
if dstChain == nil {
return nil, false, nil
}
Expand Down Expand Up @@ -158,41 +181,37 @@ func (pr *Prover) queryOnChainCommittedExplicitStateBase(ctx context.Context, ds
if len(lcpConsensusState.StateId) == 0 {
return nil, true, fmt.Errorf("on-chain LCP consensus_state state_id is empty for explicit-state base: query_height=%v base_height=%v", queryHeight, baseHeight)
}
baseClientState, baseConsensusState, err := pr.originProver.CreateInitialLightClientState(ctx, baseHeight)
canonicalBase, err := pr.queryLCPCanonicalExplicitStateBase(ctx, elcClientID)
if err != nil {
return nil, true, fmt.Errorf("failed to build ELC explicit-state base from on-chain committed height: base_height=%v %w", baseHeight, err)
}
if baseClientState == nil {
return nil, true, fmt.Errorf("built ELC explicit-state base client_state is nil: base_height=%v", baseHeight)
}
if baseConsensusState == nil {
return nil, true, fmt.Errorf("built ELC explicit-state base consensus_state is nil: base_height=%v", baseHeight)
}
if latest := baseClientState.GetLatestHeight(); latest.GetRevisionNumber() != baseHeight.GetRevisionNumber() ||
latest.GetRevisionHeight() != baseHeight.GetRevisionHeight() {
return nil, true, fmt.Errorf("built ELC explicit-state base height mismatch: base_height=%v client_state_latest_height=%v", baseHeight, latest)
}
baseClientStateAny, err := clienttypes.PackClientState(baseClientState)
if err != nil {
return nil, true, fmt.Errorf("failed to pack ELC explicit-state base client_state: base_height=%v %w", baseHeight, err)
}
baseConsensusStateAny, err := clienttypes.PackConsensusState(baseConsensusState)
if err != nil {
return nil, true, fmt.Errorf("failed to pack ELC explicit-state base consensus_state: base_height=%v %w", baseHeight, err)
return nil, true, fmt.Errorf("failed to query LCP canonical payload for on-chain committed explicit-state base: base_height=%v %w", baseHeight, err)
}
if canonicalBase.Height.GetRevisionNumber() != baseHeight.GetRevisionNumber() ||
canonicalBase.Height.GetRevisionHeight() != baseHeight.GetRevisionHeight() {
// The canonical store only serves the latest payload, so a speculative
// batch cannot be anchored at an earlier committed height (canonical
// drifted ahead of the on-chain commitment, e.g. a prior updateELC
// committed at the stitch but its UpdateClient message was never
// submitted). Return a typed error so the caller can recover via the
// serial path, which the enclave anchors at its stored consensus for
// the on-chain height.
return nil, true, &ExplicitStateBaseDriftError{
OnChainHeight: baseHeight,
CanonicalHeight: canonicalBase.Height,
}
}
pr.getLogger().InfoContext(
ctx,
"queried on-chain committed explicit-state base",
"query_height", queryHeight.String(),
"base_height", baseHeight.String(),
"on_chain_state_id", fmt.Sprintf("0x%x", lcpConsensusState.StateId),
"client_state_type", baseClientStateAny.TypeUrl,
"consensus_state_type", baseConsensusStateAny.TypeUrl,
"client_state_type", canonicalBase.ClientState.TypeUrl,
"consensus_state_type", canonicalBase.ConsensusState.TypeUrl,
)
return &ExplicitStateBase{
Height: baseHeight,
ClientState: cloneExplicitStateAny(baseClientStateAny),
ConsensusState: cloneExplicitStateAny(baseConsensusStateAny),
ClientState: canonicalBase.ClientState,
ConsensusState: canonicalBase.ConsensusState,
StateId: append([]byte(nil), lcpConsensusState.StateId...),
}, true, nil
}
Expand Down
Loading
Loading