Skip to content
Draft
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ format:

protoVer=0.17.1
protoImageName=ghcr.io/cosmos/proto-builder:$(protoVer)
protoImage=$(DOCKER) run --rm -v $(CURDIR):/workspace --workdir /workspace $(protoImageName)
protoImage=$(DOCKER) run --rm -e GOTOOLCHAIN=auto -v $(CURDIR):/workspace --workdir /workspace $(protoImageName)

#? proto-all: Format, lint and generate Protobuf files
proto-all: proto-format proto-lint proto-gen
Expand Down
3 changes: 3 additions & 0 deletions modules/core/exported/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const (
// LocalhostClientID is the sentinel client ID for the localhost client.
LocalhostClientID string = Localhost

// Attestations is used to indicate that the light client is an attestor-based client.
Attestations string = "10-attestations"

// Active is a status type of a client. An active client is allowed to be used.
Active Status = "Active"

Expand Down
31 changes: 31 additions & 0 deletions modules/light-clients/10-attestations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 10-attestations Light Client

An attestor-based IBC light client that verifies state using quorum-signed ECDSA attestations from a fixed set of trusted signers.

## Trust Model

- A fixed set of ECDSA attestors (EOA addresses) is configured at client creation
- Updates require `minRequiredSigs` unique signatures from the attestor set

## State

**Client State**
- `attestorAddresses` — trusted attestor set
- `minRequiredSigs` — quorum threshold
- `latestHeight` — highest trusted height
- `isFrozen` — halts all operations when true

**Consensus State** (per height)
- `timestamp` — trusted UNIX timestamp in nanoseconds

## Proofs

All proofs contain an `AttestationProof` with:
- `attestationData` — the attested payload (encoded `StateAttestation` or `PacketAttestation`)
- `signatures` — 65-byte ECDSA signatures over `sha256(attestationData)`

## Limitations

- No client recovery supported
- No client upgrades supported
- No attestor updates or rotation supported
34 changes: 34 additions & 0 deletions modules/light-clients/10-attestations/attestation_proof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package attestations

import (
errorsmod "cosmossdk.io/errors"

clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types"
"github.com/cosmos/ibc-go/v10/modules/core/exported"
)

var _ exported.ClientMessage = (*AttestationProof)(nil)

// ClientType defines that the AttestationProof is for Attestations.
func (AttestationProof) ClientType() string {
return exported.Attestations
}

// ValidateBasic ensures that the attestation data and signatures are initialized.
func (ap AttestationProof) ValidateBasic() error {
if len(ap.AttestationData) == 0 {
return errorsmod.Wrap(clienttypes.ErrInvalidHeader, "attestation data cannot be empty")
}

if len(ap.Signatures) == 0 {
return errorsmod.Wrap(clienttypes.ErrInvalidHeader, "signatures cannot be empty")
}

for i, sig := range ap.Signatures {
if len(sig) != SignatureLength {
return errorsmod.Wrapf(clienttypes.ErrInvalidHeader, "signature %d has invalid length: expected %d, got %d", i, SignatureLength, len(sig))
}
}

return nil
}
58 changes: 58 additions & 0 deletions modules/light-clients/10-attestations/attestation_proof_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package attestations_test

import (
attestations "github.com/cosmos/ibc-go/v10/modules/light-clients/10-attestations"
)

func (s *AttestationsTestSuite) TestAttestationProofValidateBasic() {
testCases := []struct {
name string
attestationProof attestations.AttestationProof
expErr string
}{
{
"valid proof",
attestations.AttestationProof{
AttestationData: []byte("valid data"),
Signatures: [][]byte{make([]byte, 65)},
},
"",
},
{
"empty attestation data",
attestations.AttestationProof{
AttestationData: []byte{},
Signatures: [][]byte{make([]byte, 65)},
},
"attestation data cannot be empty",
},
{
"empty signatures",
attestations.AttestationProof{
AttestationData: []byte("valid data"),
Signatures: [][]byte{},
},
"signatures cannot be empty",
},
{
"invalid signature length",
attestations.AttestationProof{
AttestationData: []byte("valid data"),
Signatures: [][]byte{make([]byte, 64)},
},
"signature 0 has invalid length",
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
err := tc.attestationProof.ValidateBasic()
if tc.expErr != "" {
s.Require().Error(err)
s.Require().ErrorContains(err, tc.expErr)
} else {
s.Require().NoError(err)
}
})
}
}
Loading
Loading