Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use EIP712 signing scheme for bids #2808

Open
wants to merge 1 commit into
base: express-lane-timeboost-early-submission-grace
Choose a base branch
from
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
6 changes: 3 additions & 3 deletions timeboost/bid_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ func (bc *bidCache) topTwoBids() *auctionResult {
result.secondPlace = result.firstPlace
result.firstPlace = bid
} else if bid.Amount.Cmp(result.firstPlace.Amount) == 0 {
if bid.BigIntHash().Cmp(result.firstPlace.BigIntHash()) > 0 {
if bid.bigIntHash().Cmp(result.firstPlace.bigIntHash()) > 0 {
result.secondPlace = result.firstPlace
result.firstPlace = bid
} else if result.secondPlace == nil || bid.BigIntHash().Cmp(result.secondPlace.BigIntHash()) > 0 {
} else if result.secondPlace == nil || bid.bigIntHash().Cmp(result.secondPlace.bigIntHash()) > 0 {
result.secondPlace = bid
}
} else if result.secondPlace == nil || bid.Amount.Cmp(result.secondPlace.Amount) > 0 {
result.secondPlace = bid
} else if bid.Amount.Cmp(result.secondPlace.Amount) == 0 {
if bid.BigIntHash().Cmp(result.secondPlace.BigIntHash()) > 0 {
if bid.bigIntHash().Cmp(result.secondPlace.bigIntHash()) > 0 {
result.secondPlace = bid
}
}
Expand Down
87 changes: 51 additions & 36 deletions timeboost/bid_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,25 @@ func BidValidatorConfigAddOptions(prefix string, f *pflag.FlagSet) {
type BidValidator struct {
stopwaiter.StopWaiter
sync.RWMutex
chainId *big.Int
stack *node.Node
producerCfg *pubsub.ProducerConfig
producer *pubsub.Producer[*JsonValidatedBid, error]
redisClient redis.UniversalClient
domainValue []byte
client *ethclient.Client
auctionContract *express_lane_auctiongen.ExpressLaneAuction
auctionContractAddr common.Address
bidsReceiver chan *Bid
initialRoundTimestamp time.Time
roundDuration time.Duration
auctionClosingDuration time.Duration
reserveSubmissionDuration time.Duration
reservePriceLock sync.RWMutex
reservePrice *big.Int
bidsPerSenderInRound map[common.Address]uint8
maxBidsPerSenderInRound uint8
chainId *big.Int
stack *node.Node
producerCfg *pubsub.ProducerConfig
producer *pubsub.Producer[*JsonValidatedBid, error]
redisClient redis.UniversalClient
domainValue []byte
client *ethclient.Client
auctionContract *express_lane_auctiongen.ExpressLaneAuction
auctionContractAddr common.Address
auctionContractDomainSeparator [32]byte
bidsReceiver chan *Bid
initialRoundTimestamp time.Time
roundDuration time.Duration
auctionClosingDuration time.Duration
reserveSubmissionDuration time.Duration
reservePriceLock sync.RWMutex
reservePrice *big.Int
bidsPerSenderInRound map[common.Address]uint8
maxBidsPerSenderInRound uint8
}

func NewBidValidator(
Expand Down Expand Up @@ -128,23 +129,32 @@ func NewBidValidator(
if err != nil {
return nil, err
}

domainSeparator, err := auctionContract.DomainSeparator(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}

bidValidator := &BidValidator{
chainId: chainId,
client: sequencerClient,
redisClient: redisClient,
stack: stack,
auctionContract: auctionContract,
auctionContractAddr: auctionContractAddr,
bidsReceiver: make(chan *Bid, 10_000),
initialRoundTimestamp: initialTimestamp,
roundDuration: roundDuration,
auctionClosingDuration: auctionClosingDuration,
reserveSubmissionDuration: reserveSubmissionDuration,
reservePrice: reservePrice,
domainValue: domainValue,
bidsPerSenderInRound: make(map[common.Address]uint8),
maxBidsPerSenderInRound: 5, // 5 max bids per sender address in a round.
producerCfg: &cfg.ProducerConfig,
chainId: chainId,
client: sequencerClient,
redisClient: redisClient,
stack: stack,
auctionContract: auctionContract,
auctionContractAddr: auctionContractAddr,
auctionContractDomainSeparator: domainSeparator,
bidsReceiver: make(chan *Bid, 10_000),
initialRoundTimestamp: initialTimestamp,
roundDuration: roundDuration,
auctionClosingDuration: auctionClosingDuration,
reserveSubmissionDuration: reserveSubmissionDuration,
reservePrice: reservePrice,
domainValue: domainValue,
bidsPerSenderInRound: make(map[common.Address]uint8),
maxBidsPerSenderInRound: 5, // 5 max bids per sender address in a round.
producerCfg: &cfg.ProducerConfig,
}
api := &BidValidatorAPI{bidValidator}
valAPIs := []rpc.API{{
Expand Down Expand Up @@ -313,10 +323,10 @@ func (bv *BidValidator) validateBid(
}

// Validate the signature.
packedBidBytes := bid.ToMessageBytes()
if len(bid.Signature) != 65 {
return nil, errors.Wrap(ErrMalformedData, "signature length is not 65")
}

// Recover the public key.
sigItem := make([]byte, len(bid.Signature))
copy(sigItem, bid.Signature)
Expand All @@ -327,7 +337,12 @@ func (bv *BidValidator) validateBid(
if sigItem[len(sigItem)-1] >= 27 {
sigItem[len(sigItem)-1] -= 27
}
pubkey, err := crypto.SigToPub(buildEthereumSignedMessage(packedBidBytes), sigItem)

bidHash, err := bid.ToEIP712Hash(bv.auctionContractDomainSeparator)
if err != nil {
return nil, err
}
pubkey, err := crypto.SigToPub(bidHash[:], sigItem)
if err != nil {
return nil, ErrMalformedData
}
Expand Down
39 changes: 18 additions & 21 deletions timeboost/bid_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package timeboost

import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"testing"
"time"
Expand Down Expand Up @@ -131,14 +129,15 @@ func TestBidValidator_validateBid_perRoundBidLimitReached(t *testing.T) {
}
auctionContractAddr := common.Address{'a'}
bv := BidValidator{
chainId: big.NewInt(1),
initialRoundTimestamp: time.Now().Add(-time.Second),
reservePrice: big.NewInt(2),
roundDuration: time.Minute,
auctionClosingDuration: 45 * time.Second,
bidsPerSenderInRound: make(map[common.Address]uint8),
maxBidsPerSenderInRound: 5,
auctionContractAddr: auctionContractAddr,
chainId: big.NewInt(1),
initialRoundTimestamp: time.Now().Add(-time.Second),
reservePrice: big.NewInt(2),
roundDuration: time.Minute,
auctionClosingDuration: 45 * time.Second,
bidsPerSenderInRound: make(map[common.Address]uint8),
maxBidsPerSenderInRound: 5,
auctionContractAddr: auctionContractAddr,
auctionContractDomainSeparator: common.Hash{},
}
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
Expand All @@ -150,7 +149,11 @@ func TestBidValidator_validateBid_perRoundBidLimitReached(t *testing.T) {
Amount: big.NewInt(3),
Signature: []byte{'a'},
}
signature, err := buildSignature(privateKey, bid.ToMessageBytes())

bidHash, err := bid.ToEIP712Hash(bv.auctionContractDomainSeparator)
require.NoError(t, err)

signature, err := crypto.Sign(bidHash[:], privateKey)
require.NoError(t, err)

bid.Signature = signature
Expand All @@ -163,15 +166,6 @@ func TestBidValidator_validateBid_perRoundBidLimitReached(t *testing.T) {

}

func buildSignature(privateKey *ecdsa.PrivateKey, data []byte) ([]byte, error) {
prefixedData := crypto.Keccak256(append([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(data))), data...))
signature, err := crypto.Sign(prefixedData, privateKey)
if err != nil {
return nil, err
}
return signature, nil
}

func buildValidBid(t *testing.T, auctionContractAddr common.Address) *Bid {
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
Expand All @@ -184,7 +178,10 @@ func buildValidBid(t *testing.T, auctionContractAddr common.Address) *Bid {
Signature: []byte{'a'},
}

signature, err := buildSignature(privateKey, bid.ToMessageBytes())
bidHash, err := bid.ToEIP712Hash(common.Hash{})
require.NoError(t, err)

signature, err := crypto.Sign(bidHash[:], privateKey)
require.NoError(t, err)

bid.Signature = signature
Expand Down
22 changes: 15 additions & 7 deletions timeboost/bidder_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
Expand Down Expand Up @@ -195,20 +194,33 @@ func (bd *BidderClient) Bid(
if (expressLaneController == common.Address{}) {
expressLaneController = bd.txOpts.From
}

domainSeparator, err := bd.auctionContract.DomainSeparator(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}
newBid := &Bid{
ChainId: bd.chainId,
ExpressLaneController: expressLaneController,
AuctionContractAddress: bd.auctionContractAddress,
Round: CurrentRound(bd.initialRoundTimestamp, bd.roundDuration) + 1,
Amount: amount,
Signature: nil,
}
sig, err := bd.signer(buildEthereumSignedMessage(newBid.ToMessageBytes()))
bidHash, err := newBid.ToEIP712Hash(domainSeparator)
if err != nil {
return nil, err
}

sig, err := bd.signer(bidHash.Bytes())
if err != nil {
return nil, err
}
sig[64] += 27

newBid.Signature = sig

promise := bd.submitBid(newBid)
if _, err := promise.Await(ctx); err != nil {
return nil, err
Expand All @@ -222,7 +234,3 @@ func (bd *BidderClient) submitBid(bid *Bid) containers.PromiseInterface[struct{}
return struct{}{}, err
})
}

func buildEthereumSignedMessage(msg []byte) []byte {
return crypto.Keccak256(append([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(msg))), msg...))
}
57 changes: 41 additions & 16 deletions timeboost/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)

type Bid struct {
Expand All @@ -33,19 +34,40 @@ func (b *Bid) ToJson() *JsonBid {
}
}

func (b *Bid) ToMessageBytes() []byte {
buf := new(bytes.Buffer)
// Encode uint256 values - each occupies 32 bytes
buf.Write(domainValue)
buf.Write(padBigInt(b.ChainId))
buf.Write(b.AuctionContractAddress[:])
roundBuf := make([]byte, 8)
binary.BigEndian.PutUint64(roundBuf, b.Round)
buf.Write(roundBuf)
buf.Write(padBigInt(b.Amount))
buf.Write(b.ExpressLaneController[:])
func (b *Bid) ToEIP712Hash(domainSeparator [32]byte) (common.Hash, error) {
types := apitypes.Types{
"Bid": []apitypes.Type{
{Name: "round", Type: "uint64"},
{Name: "expressLaneController", Type: "address"},
{Name: "amount", Type: "uint256"},
},
}

message := apitypes.TypedDataMessage{
"round": big.NewInt(0).SetUint64(b.Round),
"expressLaneController": [20]byte(b.ExpressLaneController),
"amount": b.Amount,
}

return buf.Bytes()
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "Bid",
Message: message,
Domain: apitypes.TypedDataDomain{Salt: "Unused; domain separator fetched from method on contract. This must be nonempty for validation."},
}

messageHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
if err != nil {
return common.Hash{}, err
}

bidHash := crypto.Keccak256Hash(
[]byte("\x19\x01"),
domainSeparator[:],
messageHash,
)

return bidHash, nil
}

type JsonBid struct {
Expand All @@ -72,17 +94,20 @@ type ValidatedBid struct {
// The hash is equivalent to the following Solidity implementation:
//
// uint256(keccak256(abi.encodePacked(bidder, bidBytes)))
func (v *ValidatedBid) BigIntHash() *big.Int {
bidBytes := v.BidBytes()
//
// This is only used for breaking ties amongst equivalent bids and not used for
// Bid signing, which uses EIP 712 as the hashing scheme.
func (v *ValidatedBid) bigIntHash() *big.Int {
bidBytes := v.bidBytes()
bidder := v.Bidder.Bytes()

return new(big.Int).SetBytes(crypto.Keccak256Hash(bidder, bidBytes).Bytes())
}

// BidBytes returns the byte representation equivalent to the Solidity implementation of
// bidBytes returns the byte representation equivalent to the Solidity implementation of
//
// abi.encodePacked(BID_DOMAIN, block.chainid, address(this), _round, _amount, _expressLaneController)
func (v *ValidatedBid) BidBytes() []byte {
func (v *ValidatedBid) bidBytes() []byte {
var buffer bytes.Buffer

buffer.Write(domainValue)
Expand Down
Loading