Skip to content
Merged
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
4 changes: 2 additions & 2 deletions cli/besu/doc.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0

// Package besu provides header parsing, storage slot derivation and ABI
// encoding for Besu light-client payloads. Consensus verification belongs to
// Package besu provides header parsing and sealing, storage slot derivation
// and ABI encoding for Besu light-client payloads. Consensus verification belongs to
// the contract, simulated by the transaction submitter.
//
// Wire types and codecs are generated upstream. The contract stores only
Expand Down
85 changes: 77 additions & 8 deletions cli/besu/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package besu

import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
Expand Down Expand Up @@ -73,17 +74,13 @@ func ParseSealedHeader(h *types.Header) (*ParsedHeader, error) {
return nil, fmt.Errorf("invalid number %v", h.Number)
}

var extraItems []rlp.RawValue
if err := rlp.DecodeBytes(h.Extra, &extraItems); err != nil {
return nil, fmt.Errorf("extra data list: %w", err)
}

if len(extraItems) != extraDataItemCount {
return nil, fmt.Errorf("%d extra data items, want %d", len(extraItems), extraDataItemCount)
extraItems, err := decodeExtra(h)
if err != nil {
return nil, err
}

header := &ParsedHeader{Height: h.Number.Uint64(), Timestamp: h.Time, StateRoot: h.Root}
if err := rlp.DecodeBytes(extraItems[extraIdxValidators], &header.Validators); err != nil {
if err = rlp.DecodeBytes(extraItems[extraIdxValidators], &header.Validators); err != nil {
return nil, fmt.Errorf("validators: %w", err)
}

Expand All @@ -101,6 +98,78 @@ func ParseSealedHeader(h *types.Header) (*ParsedHeader, error) {
return header, nil
}

// SealHeader sets h's validators and replaces its commit seals with seals
// by signers under consensus, then parses the result. It seals headers a
// chain never produced, such as misbehavior evidence. Every signer must be a
// distinct one of validators.
func SealHeader(
h *types.Header,
consensus Consensus,
validators []common.Address,
signers []*ecdsa.PrivateKey,
) (*ParsedHeader, error) {
if h == nil {
return nil, errors.New("nil header")
}

extraItems, err := decodeExtra(h)
if err != nil {
return nil, err
}

// The contract requires validators in ascending order.
sorted := slices.SortedFunc(slices.Values(validators), common.Address.Cmp)
if extraItems[extraIdxValidators], err = rlp.EncodeToBytes(sorted); err != nil {
return nil, fmt.Errorf("encode validators: %w", err)
}

// The digest covers the validators, so it follows them.
digest, err := commitSealDigest(h, extraItems, consensus)
if err != nil {
return nil, err
}

seals := make([][]byte, len(signers))
signed := make([]common.Address, 0, len(signers))
for i, key := range signers {
signer := crypto.PubkeyToAddress(key.PublicKey)
if !slices.Contains(sorted, signer) {
return nil, fmt.Errorf("signer %s is not a validator", signer)
}
if slices.Contains(signed, signer) {
return nil, fmt.Errorf("signer %s repeats", signer)
}
signed = append(signed, signer)
if seals[i], err = crypto.Sign(digest, key); err != nil {
return nil, fmt.Errorf("sign commit seal %d: %w", i, err)
}
}

if extraItems[extraIdxCommitSeals], err = rlp.EncodeToBytes(seals); err != nil {
return nil, fmt.Errorf("encode commit seals: %w", err)
}

sealed, err := withExtraData(h, extraItems)
if err != nil {
return nil, err
}

return ParseSealedHeader(sealed)
}

func decodeExtra(h *types.Header) ([]rlp.RawValue, error) {
var extraItems []rlp.RawValue
if err := rlp.DecodeBytes(h.Extra, &extraItems); err != nil {
return nil, fmt.Errorf("extra data list: %w", err)
}

if len(extraItems) != extraDataItemCount {
return nil, fmt.Errorf("%d extra data items, want %d", len(extraItems), extraDataItemCount)
}

return extraItems, nil
}

// normalizeCommitSeals returns h with only the commit seals the contract
// accepts: canonical, from one of validators, one per signer, in strictly
// ascending order of signer. The contract rejects the whole header for any
Expand Down
36 changes: 36 additions & 0 deletions cli/besu/header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,42 @@ func TestParseSealedHeaderRequiresAValidatorSeal(t *testing.T) {
assert.Empty(t, parsed.Consensus)
}

// A header resealed by a quorum of new validators parses to their consensus
// with those validators in ascending order and one seal per signer. A signer
// outside the validators, or repeated, is rejected.
func TestSealHeader(t *testing.T) {
for _, consensus := range besu.Consensuses {
var header types.Header
require.NoError(t, rlp.DecodeBytes(besutest.MustFixture(t, consensus).AdjacentUpdate.HeaderRLP, &header))
header.Root = common.HexToHash("0x01")

keys := make([]*ecdsa.PrivateKey, 4)
validators := make([]common.Address, len(keys))
for i := range keys {
key, err := crypto.GenerateKey()
require.NoError(t, err)
keys[i], validators[i] = key, crypto.PubkeyToAddress(key.PublicKey)
}

sealed, err := besu.SealHeader(&header, consensus, validators, keys[:3])
require.NoError(t, err)
assert.Equal(t, consensus, sealed.Consensus)
assert.Equal(t, slices.SortedFunc(slices.Values(validators), common.Address.Cmp), sealed.Validators)
assert.Equal(t, header.Root, sealed.StateRoot)
var resealed types.Header
require.NoError(t, rlp.DecodeBytes(sealed.RLP, &resealed))
_, seals := splitExtra(t, resealed.Extra)
assert.Len(t, seals, 3)

outsider, err := crypto.GenerateKey()
require.NoError(t, err)
_, err = besu.SealHeader(&header, consensus, validators, []*ecdsa.PrivateKey{keys[0], outsider})
require.ErrorContains(t, err, "is not a validator")
_, err = besu.SealHeader(&header, consensus, validators, []*ecdsa.PrivateKey{keys[0], keys[1], keys[0]})
require.ErrorContains(t, err, "repeats")
}
}

// Validator seals under two consensuses leave the chain's consensus unknown,
// so parsing fails rather than pick one.
func TestParseSealedHeaderRejectsSealsUnderTwoConsensuses(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This repository-level surface hosts one black-box acceptance package. Its tests

The root package covers ICS20 transfer, ICS27 GMP, IFT (burn/mint on top of GMP) relay behavior, timeout refunds, error acknowledgements, pending-packet status, Relayer and node recovery, attestor quorum loss and recovery, cross-route handling, and relaying through an attached RPC that `Environment` does not own. These are all acceptance criteria and run together by default.

Besu light clients are covered through a two-chain Besu mesh, which requires the Besu provider and therefore skips in fast mode. `TestTransferBesuQBFT_AutoRelay` exercises client updates from sealed headers, membership proofs and acknowledgement; `TestTransferBesuQBFT_TimeoutRefund` exercises the non-membership proof of a timeout, letting the packet expire in real time; `TestTransferBesuIBFT2_AutoRelay` repeats the transfer on IBFT 2.0 chains. `TestDeployBesuQBFTIBFT2Connection` deploys a besu connection between a QBFT and an IBFT 2.0 chain with `ibc deploy client besu` alone, so each client detects a different consensus, and relays an IFT transfer over it; `TestDeployMixedBesuQBFTAttestationConnection` does the same for the production shape, a besu client on one end and an attestation client on the other, relaying a round trip so each client type proves both a receive and an acknowledgement. `TestTransferBesuQBFT_ValidatorTurnover` and `TestTransferBesuIBFT2_ValidatorTurnover` run the source chain with three follower nodes and, before any packet is relayed, vote the followers in as validators and three of the four genesis validators out, so the relayer must step the destination's client across the set changes to relay a transfer. Their clients are deployed at a trust level of one half through `NewBesuClient.TrustLevel`, since at the default two thirds the client cannot follow the removals. `ManagedBesu.Followers` adds the follower nodes and `Chain.Validators` votes through them. Client expiry is not exercised end-to-end: it would need the Besu clock moved past the trusting period.
Besu light clients are covered through a two-chain Besu mesh, which requires the Besu provider and therefore skips in fast mode. `TestTransferBesuQBFT_AutoRelay` exercises client updates from sealed headers, membership proofs and acknowledgement; `TestTransferBesuQBFT_TimeoutRefund` exercises the non-membership proof of a timeout, letting the packet expire in real time; `TestTransferBesuIBFT2_AutoRelay` repeats the transfer on IBFT 2.0 chains. `TestDeployBesuQBFTIBFT2Connection` deploys a besu connection between a QBFT and an IBFT 2.0 chain with `ibc deploy client besu` alone, so each client detects a different consensus, and relays an IFT transfer over it; `TestDeployMixedBesuQBFTAttestationConnection` does the same for the production shape, a besu client on one end and an attestation client on the other, relaying a round trip so each client type proves both a receive and an acknowledgement. `TestTransferBesuQBFT_ValidatorTurnover` and `TestTransferBesuIBFT2_ValidatorTurnover` run the source chain with three follower nodes and, before any packet is relayed, vote the followers in as validators and three of the four genesis validators out, so the relayer must step the destination's client across the set changes to relay a transfer. Their clients are deployed at a trust level of one half through `NewBesuClient.TrustLevel`, since at the default two thirds the client cannot follow the removals. `ManagedBesu.Followers` adds the follower nodes and `Chain.Validators` votes through them. `TestBesuQBFTClient_DoubleSignFreeze` and `TestBesuQBFTClient_TimeNonMonotonicityFreeze` submit headers sealed by a quorum of the source validators as misbehaviour evidence, one pair for each kind the client detects, and require the frozen client to reject updates and leave the next transfer pending. Client expiry is not exercised end-to-end: it would need the Besu clock moved past the trusting period.

## Running the acceptance tests

Expand Down
Loading
Loading