Skip to content
Open
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
20 changes: 20 additions & 0 deletions core/vm/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,13 +350,22 @@ var PrecompiledContractsOsaka = PrecompiledContracts{
common.BytesToAddress([]byte{0x1, 0x00}): &p256Verify{eip7951: true},
}

var PrecompiledContractsPasteur = func() PrecompiledContracts {
precompiles := maps.Clone(PrecompiledContractsOsaka)
// Note: 0x66 (blsSignatureVerify) is a generic BLS verification primitive
// and does not require unique pubkeys. Uniqueness is enforced by callers.
precompiles[common.BytesToAddress([]byte{0x67})] = &cometBFTLightBlockValidatePasteur{}
return precompiles
}()

// PrecompiledContractsP256Verify contains the precompiled Ethereum
// contract specified in EIP-7212. This is exported for testing purposes.
var PrecompiledContractsP256Verify = PrecompiledContracts{
common.BytesToAddress([]byte{0x1, 0x00}): &p256Verify{},
}

var (
PrecompiledAddressesPasteur []common.Address
PrecompiledAddressesOsaka []common.Address
PrecompiledAddressesPrague []common.Address
PrecompiledAddressesHaber []common.Address
Expand Down Expand Up @@ -417,6 +426,9 @@ func init() {
for k := range PrecompiledContractsPrague {
PrecompiledAddressesPrague = append(PrecompiledAddressesPrague, k)
}
for k := range PrecompiledContractsPasteur {
PrecompiledAddressesPasteur = append(PrecompiledAddressesPasteur, k)
}
for k := range PrecompiledContractsOsaka {
PrecompiledAddressesOsaka = append(PrecompiledAddressesOsaka, k)
}
Expand All @@ -426,6 +438,8 @@ func activePrecompiledContracts(rules params.Rules) PrecompiledContracts {
switch {
case rules.IsVerkle:
return PrecompiledContractsVerkle
case rules.IsPasteur:
return PrecompiledContractsPasteur
case rules.IsOsaka:
return PrecompiledContractsOsaka
case rules.IsPrague:
Expand Down Expand Up @@ -467,6 +481,8 @@ func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts {
// ActivePrecompiles returns the precompile addresses enabled with the current configuration.
func ActivePrecompiles(rules params.Rules) []common.Address {
switch {
case rules.IsPasteur:
return PrecompiledAddressesPasteur
case rules.IsOsaka:
return PrecompiledAddressesOsaka
case rules.IsPrague:
Expand Down Expand Up @@ -1623,6 +1639,10 @@ func (c *blsSignatureVerify) RequiredGas(input []byte) uint64 {
// Run input:
// msg | signature | [{bls pubkey}] |
// 32 bytes | 96 bytes | [{48 bytes}] |
//
// Note: as a generic BLS signature verification primitive, this precompile
// does not require public keys to be unique. Callers that need uniqueness
// (e.g. validator set decoding) must enforce it themselves.
func (c *blsSignatureVerify) Run(input []byte) ([]byte, error) {
msgAndSigLength := msgHashLength + signatureLength
inputLen := uint64(len(input))
Expand Down
25 changes: 21 additions & 4 deletions core/vm/contracts_lightclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/url"
"strings"

"github.com/cometbft/cometbft/types"
"github.com/cosmos/iavl"
"github.com/tendermint/tendermint/crypto/merkle"
"github.com/tendermint/tendermint/crypto/secp256k1"
Expand Down Expand Up @@ -391,14 +392,18 @@ func (c *cometBFTLightBlockValidate) RequiredGas(input []byte) uint64 {
return params.CometBFTLightBlockValidateGas
}

func (c *cometBFTLightBlockValidate) run(input []byte, isHertz bool) (result []byte, err error) {
func (c *cometBFTLightBlockValidate) run(input []byte, isHertz bool, requireUniqueValidators bool) (result []byte, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("internal error: %v\n", r)
}
}()

cs, block, err := v2.DecodeLightBlockValidationInput(input)
var (
cs *v2.ConsensusState
block *types.LightBlock
)
cs, block, err = v2.DecodeLightBlockValidationInput(input, requireUniqueValidators)
if err != nil {
return nil, err
}
Expand All @@ -418,7 +423,7 @@ func (c *cometBFTLightBlockValidate) run(input []byte, isHertz bool) (result []b
}

func (c *cometBFTLightBlockValidate) Run(input []byte) (result []byte, err error) {
return c.run(input, false)
return c.run(input, false, false)
}

func (c *cometBFTLightBlockValidate) Name() string {
Expand All @@ -430,13 +435,25 @@ type cometBFTLightBlockValidateHertz struct {
}

func (c *cometBFTLightBlockValidateHertz) Run(input []byte) (result []byte, err error) {
return c.run(input, true)
return c.run(input, true, false)
}

func (c *cometBFTLightBlockValidateHertz) Name() string {
return "COMET_BFT_LIGHT_BLOCK_VALIDATE_HERTZ"
}

type cometBFTLightBlockValidatePasteur struct {
cometBFTLightBlockValidate
}

func (c *cometBFTLightBlockValidatePasteur) Run(input []byte) (result []byte, err error) {
return c.run(input, true, true)
}

func (c *cometBFTLightBlockValidatePasteur) Name() string {
return "COMET_BFT_LIGHT_BLOCK_VALIDATE_PASTEUR"
}

// secp256k1SignatureRecover implemented as a native contract.
type secp256k1SignatureRecover struct{}

Expand Down
37 changes: 37 additions & 0 deletions core/vm/contracts_lightclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import (
"encoding/hex"
"testing"

"github.com/cometbft/cometbft/types"
"github.com/cosmos/iavl"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto/merkle"
cmn "github.com/tendermint/tendermint/libs/common"

v1 "github.com/ethereum/go-ethereum/core/vm/lightclient/v1"
v2 "github.com/ethereum/go-ethereum/core/vm/lightclient/v2"
)

const (
Expand Down Expand Up @@ -374,6 +378,39 @@ func TestCometBFTLightBlockValidateHertz(t *testing.T) {
require.Equal(t, expectOutputStr, hex.EncodeToString(res))
}

func TestCometBFTLightBlockValidateRejectsDuplicateTrustedValidatorsAtPasteur(t *testing.T) {
inputStr := "000000000000000000000000000000000000000000000000000000000000018c677265656e6669656c645f393030302d3132310000000000000000000000000000000000000000013c350cd55b99dc6c2b7da9bef5410fbfb869fede858e7b95bf7ca294e228bb40e33f6e876d63791ebd05ff617a1b4f4ad1aa2ce65e3c3a9cdfb33e0ffa7e8423000000000098968015154514f68ce65a0d9eecc578c0ab12da0a2a28a0805521b5b7ae56eb3fb24555efbfe59e1622bfe9f7be8c9022e9b3f2442739c1ce870b9adee169afe60f674edd7c86451c5363d89052fde8351895eeea166ce5373c36e31b518ed191d0c599aa0f5b0000000000989680432f6c4908a9aa5f3444421f466b11645235c99b831b2a2de9e504d7ea299e52a202ce529808618eb3bfc0addf13d8c5f2df821d81e18f9bc61583510b322d067d46323b0a572635c06a049c0a2a929e3c8184a50cf6a8b95708c25834ade456f399015a0000000000989680864cb9828254d712f8e59b164fc6a9402dc4e6c59065e38cff24f5323c8c5da888a0f97e5ee4ba1e11b0674b0a0d06204c1dfa247c370cd4be3e799fc4f6f48d977ac7ca0aeb060adb030a02080b1213677265656e6669656c645f393030302d3132311802220c08b2d7f3a10610e8d2adb3032a480a20ec6ecb5db4ffb17fabe40c60ca7b8441e9c5d77585d0831186f3c37aa16e9c15122408011220a2ab9e1eb9ea52812f413526e424b326aff2f258a56e00d690db9f805b60fe7e32200f40aeff672e8309b7b0aefbb9a1ae3d4299b5c445b7d54e8ff398488467f0053a20e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85542203c350cd55b99dc6c2b7da9bef5410fbfb869fede858e7b95bf7ca294e228bb404a203c350cd55b99dc6c2b7da9bef5410fbfb869fede858e7b95bf7ca294e228bb405220294d8fbd0b94b767a7eba9840f299a3586da7fe6b5dead3b7eecba193c400f935a20bc50557c12d7392b0d07d75df0b61232d48f86a74fdea6d1485d9be6317d268c6220e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8556a20e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85572146699336aa109d1beab3946198c8e59f3b2cbd92f7a4065e3cd89e315ca39d87dee92835b98f8b8ec0861d6d9bb2c60156df5d375b3ceb1fbe71af6a244907d62548a694165caa660fec7a9b4e7b9198191361c71be0b128a0308021a480a20726abd0fdbfb6f779b0483e6e4b4b6f12241f6ea2bf374233ab1a316692b6415122408011220159f10ff15a8b58fc67a92ffd7f33c8cd407d4ce81b04ca79177dfd00ca19a67226808021214050cff76cc632760ba9db796c046004c900967361a0c08b3d7f3a10610808cadba03224080713027ffb776a702d78fd0406205c629ba473e1f8d6af646190f6eb9262cd67d69be90d10e597b91e06d7298eb6fa4b8f1eb7752ebf352a1f51560294548042268080212146699336aa109d1beab3946198c8e59f3b2cbd92f1a0c08b3d7f3a10610b087c1c00322405e2ddb70acfe4904438be3d9f4206c0ace905ac4fc306a42cfc9e86268950a0fbfd6ec5f526d3e41a3ef52bf9f9f358e3cb4c3feac76c762fa3651c1244fe004226808021214c55765fd2d0570e869f6ac22e7f2916a35ea300d1a0c08b3d7f3a10610f0b3d492032240ca17898bd22232fc9374e1188636ee321a396444a5b1a79f7628e4a11f265734b2ab50caf21e8092c55d701248e82b2f011426cb35ba22043b497a6b4661930612a0050aa8010a14050cff76cc632760ba9db796c046004c9009673612220a20e33f6e876d63791ebd05ff617a1b4f4ad1aa2ce65e3c3a9cdfb33e0ffa7e84231880ade2042080a6bbf6ffffffffff012a30a0805521b5b7ae56eb3fb24555efbfe59e1622bfe9f7be8c9022e9b3f2442739c1ce870b9adee169afe60f674edd7c86321415154514f68ce65a0d9eecc578c0ab12da0a2a283a14ee7a2a6a44d427f6949eeb8f12ea9fbb2501da880aa2010a146699336aa109d1beab3946198c8e59f3b2cbd92f12220a20451c5363d89052fde8351895eeea166ce5373c36e31b518ed191d0c599aa0f5b1880ade2042080ade2042a30831b2a2de9e504d7ea299e52a202ce529808618eb3bfc0addf13d8c5f2df821d81e18f9bc61583510b322d067d46323b3214432f6c4908a9aa5f3444421f466b11645235c99b3a14a0a7769429468054e19059af4867da0a495567e50aa2010a14c55765fd2d0570e869f6ac22e7f2916a35ea300d12220a200a572635c06a049c0a2a929e3c8184a50cf6a8b95708c25834ade456f399015a1880ade2042080ade2042a309065e38cff24f5323c8c5da888a0f97e5ee4ba1e11b0674b0a0d06204c1dfa247c370cd4be3e799fc4f6f48d977ac7ca3214864cb9828254d712f8e59b164fc6a9402dc4e6c53a143139916d97df0c589312b89950b6ab9795f34d1a12a8010a14050cff76cc632760ba9db796c046004c9009673612220a20e33f6e876d63791ebd05ff617a1b4f4ad1aa2ce65e3c3a9cdfb33e0ffa7e84231880ade2042080a6bbf6ffffffffff012a30a0805521b5b7ae56eb3fb24555efbfe59e1622bfe9f7be8c9022e9b3f2442739c1ce870b9adee169afe60f674edd7c86321415154514f68ce65a0d9eecc578c0ab12da0a2a283a14ee7a2a6a44d427f6949eeb8f12ea9fbb2501da88"

input, err := hex.DecodeString(inputStr)
require.NoError(t, err)

cs, block, err := v2.DecodeLightBlockValidationInput(input, false)
require.NoError(t, err)

duplicate := cs.ValidatorSet.Validators[0].Copy()
cs.ValidatorSet = &types.ValidatorSet{Validators: []*types.Validator{duplicate.Copy(), duplicate.Copy(), duplicate.Copy()}}
csBytes, err := cs.EncodeConsensusState()
require.NoError(t, err)

blockProto, err := block.ToProto()
require.NoError(t, err)
blockBytes, err := blockProto.Marshal()
require.NoError(t, err)

mutatedInput := make([]byte, 32)
binary.BigEndian.PutUint64(mutatedInput[24:], uint64(len(csBytes)))
mutatedInput = append(mutatedInput, csBytes...)
mutatedInput = append(mutatedInput, blockBytes...)

pasteurPrecompile := ActivePrecompiledContracts(params.Rules{IsOsaka: true, IsMendel: true, IsPasteur: true})[common.BytesToAddress([]byte{0x67})]
_, err = pasteurPrecompile.Run(mutatedInput)
require.ErrorContains(t, err, "duplicate validator")

legacyPrecompile := ActivePrecompiledContracts(params.Rules{IsOsaka: true})[common.BytesToAddress([]byte{0x67})]
_, err = legacyPrecompile.Run(mutatedInput)
require.NoError(t, err)
}

func TestSecp256k1SignatureRecover(t *testing.T) {
// local key
{
Expand Down
20 changes: 20 additions & 0 deletions core/vm/contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
)

// precompiledTest defines the input/output pairs for precompiled contract tests.
Expand Down Expand Up @@ -353,6 +354,25 @@ func TestPrecompiledBLS12381MapG2(t *testing.T) { testJson("blsMapG2", "f10

func TestPrecompiledBlsSignatureVerify(t *testing.T) { testJson("blsSignatureVerify", "66", t) }

func TestActivePrecompiledContractsUsesPasteurVariants(t *testing.T) {
rules := params.Rules{IsOsaka: true, IsMendel: true, IsPasteur: true}
precompiles := ActivePrecompiledContracts(rules)

requirePrecompile := func(addr byte) PrecompiledContract {
t.Helper()

precompile, ok := precompiles[common.BytesToAddress([]byte{addr})]
if !ok {
t.Fatalf("missing precompile 0x%02x", addr)
}
return precompile
}

if got := requirePrecompile(0x67).Name(); got != "COMET_BFT_LIGHT_BLOCK_VALIDATE_PASTEUR" {
t.Fatalf("unexpected Pasteur 0x67 precompile: %s", got)
}
}

func TestPrecompiledPointEvaluation(t *testing.T) { testJson("pointEvaluation", "0a", t) }

func BenchmarkPrecompiledPointEvaluation(b *testing.B) { benchJson("pointEvaluation", "0a", b) }
Expand Down
91 changes: 88 additions & 3 deletions core/vm/lightclient/v2/lightclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,81 @@ type ConsensusState struct {
ValidatorSet *types.ValidatorSet
}

type validatorDuplicateTracker struct {
field string
seen map[string]int
ignoreZero bool
}

func newValidatorDuplicateTracker(field string, size int, ignoreZero bool) *validatorDuplicateTracker {
return &validatorDuplicateTracker{
field: field,
seen: make(map[string]int, size),
ignoreZero: ignoreZero,
}
}

func (t *validatorDuplicateTracker) check(idx int, value []byte) error {
if t.ignoreZero {
// Optional bridge fields may be omitted in source validators or zero-filled by
// fixed-width decoding. Both forms mean "unset" and should not count as duplicates.
if len(value) == 0 || isZeroBytes(value) {
return nil
}
}

key := string(value)
if firstIdx, ok := t.seen[key]; ok {
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.

are bls key and RelayerAddress allowed to stay unset? represent by zero value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. Those two fields are allowed to be unset upstream. In our fixed-width decode path, "unset" is represented as zero-filled bytes, so I treat empty/nil and all-zero as the same unset state and exclude only
that case from duplicate detection.

return fmt.Errorf("duplicate validator %s #%d and #%d: %X", t.field, firstIdx, idx, value)
}
t.seen[key] = idx
return nil
}

func isZeroBytes(value []byte) bool {
if len(value) == 0 {
return false
}
for _, b := range value {
if b != 0 {
return false
}
}
return true
}

func validateUniqueValidatorSet(validatorSet *types.ValidatorSet) error {
if validatorSet == nil {
return nil
}

size := len(validatorSet.Validators)
addresses := newValidatorDuplicateTracker("address", size, false)
pubKeys := newValidatorDuplicateTracker("pubkey", size, false)
blsKeys := newValidatorDuplicateTracker("bls key", size, true)
relayerAddresses := newValidatorDuplicateTracker("relayer address", size, true)

for idx, validator := range validatorSet.Validators {
if validator == nil || validator.PubKey == nil {
return fmt.Errorf("invalid validator #%d", idx)
}
if err := addresses.check(idx, validator.Address); err != nil {
return err
}
if err := pubKeys.check(idx, validator.PubKey.Bytes()); err != nil {
return err
}
if err := blsKeys.check(idx, validator.BlsKey); err != nil {
return err
}
if err := relayerAddresses.check(idx, validator.RelayerAddress); err != nil {
return err
}
}

return nil
}

// output:
// | chainID | height | nextValidatorSetHash | [{validator pubkey, voting power, relayer address, relayer bls pubkey}] |
// | 32 bytes | 8 bytes | 32 bytes | [{32 bytes, 8 bytes, 20 bytes, 48 bytes}] |
Expand Down Expand Up @@ -137,7 +212,7 @@ func (cs *ConsensusState) ApplyLightBlock(block *types.LightBlock, isHertz bool)
// input:
// | chainID | height | nextValidatorSetHash | [{validator pubkey, voting power, relayer address, relayer bls pubkey}] |
// | 32 bytes | 8 bytes | 32 bytes | [{32 bytes, 8 bytes, 20 bytes, 48 bytes}] |
func DecodeConsensusState(input []byte) (ConsensusState, error) {
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.

how about

  1. func DecodeConsensusState(input []byte) (ConsensusState, error)
    --> func DecodeConsensusState(input []byte, requireUniqueValidators bool) (ConsensusState, error)
  2. delete DecodeConsensusStateWithValidation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

func DecodeConsensusState(input []byte, requireUniqueValidators bool) (ConsensusState, error) {
minimumLength := chainIDLength + heightLength + validatorSetHashLength
inputLen := uint64(len(input))
if inputLen <= minimumLength || (inputLen-minimumLength)%singleValidatorBytesLength != 0 {
Expand Down Expand Up @@ -192,14 +267,19 @@ func DecodeConsensusState(input []byte) (ConsensusState, error) {
Validators: validatorSet,
},
}
if requireUniqueValidators {
if err := validateUniqueValidatorSet(consensusState.ValidatorSet); err != nil {
return ConsensusState{}, err
}
}

return consensusState, nil
}

// input:
// consensus state length | consensus state | light block |
// 32 bytes | | |
func DecodeLightBlockValidationInput(input []byte) (*ConsensusState, *types.LightBlock, error) {
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.

how about

  1. func DecodeLightBlockValidationInput(input []byte) (*ConsensusState, *types.LightBlock, error)
    --> func DecodeLightBlockValidationInput(input []byte, requireUniqueValidators bool) (*ConsensusState, *types.LightBlock, error)
  2. delete DecodeLightBlockValidationInputWithValidation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

func DecodeLightBlockValidationInput(input []byte, requireUniqueValidators bool) (*ConsensusState, *types.LightBlock, error) {
if uint64(len(input)) <= consensusStateLengthBytesLength {
return nil, nil, errors.New("invalid input")
}
Expand All @@ -214,7 +294,7 @@ func DecodeLightBlockValidationInput(input []byte) (*ConsensusState, *types.Ligh
return nil, nil, fmt.Errorf("expected payload size %d, actual size: %d", consensusStateLengthBytesLength+csLen, len(input))
}

cs, err := DecodeConsensusState(input[consensusStateLengthBytesLength : consensusStateLengthBytesLength+csLen])
cs, err := DecodeConsensusState(input[consensusStateLengthBytesLength:consensusStateLengthBytesLength+csLen], requireUniqueValidators)
if err != nil {
return nil, nil, err
}
Expand All @@ -228,6 +308,11 @@ func DecodeLightBlockValidationInput(input []byte) (*ConsensusState, *types.Ligh
if err != nil {
return nil, nil, err
}
if requireUniqueValidators {
if err := validateUniqueValidatorSet(block.ValidatorSet); err != nil {
return nil, nil, err
}
}

return &cs, block, nil
}
Expand Down
Loading
Loading