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

Updated auctioneer with research spec #2521

Merged
merged 1 commit into from
Jul 24, 2024
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
2 changes: 1 addition & 1 deletion system_tests/seqfeed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func TestSequencerFeed_ExpressLaneAuction(t *testing.T) {
auctionContractOpts := builderSeq.L1Info.GetDefaultTransactOpts("AuctionContract", ctx)
chainId, err := l1client.ChainID(ctx)
Require(t, err)
auctioneer, err := timeboost.NewAuctioneer(&auctionContractOpts, chainId, builderSeq.L1.Client, auctionAddr, auctionContract)
auctioneer, err := timeboost.NewAuctioneer(&auctionContractOpts, []uint64{chainId.Uint64()}, builderSeq.L1.Client, auctionAddr, auctionContract)
Require(t, err)

go auctioneer.Start(ctx)
Expand Down
65 changes: 42 additions & 23 deletions timeboost/auctioneer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
"github.com/ethereum/go-ethereum/log"
"github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen"
"github.com/pkg/errors"
"golang.org/x/crypto/sha3"
)

type AuctioneerOpt func(*Auctioneer)

type Auctioneer struct {
txOpts *bind.TransactOpts
chainId *big.Int
chainId []uint64 // Auctioneer could handle auctions on multiple chains.
domainValue []byte
client Client
auctionContract *express_lane_auctiongen.ExpressLaneAuction
bidsReceiver chan *Bid
Expand All @@ -31,11 +33,13 @@
auctionContractAddr common.Address
reservePriceLock sync.RWMutex
reservePrice *big.Int
minReservePriceLock sync.RWMutex

Check failure on line 36 in timeboost/auctioneer.go

View workflow job for this annotation

GitHub Actions / Go Tests (defaults)

field `minReservePriceLock` is unused (unused)

Check failure on line 36 in timeboost/auctioneer.go

View workflow job for this annotation

GitHub Actions / Go Tests (race)

field `minReservePriceLock` is unused (unused)

Check failure on line 36 in timeboost/auctioneer.go

View workflow job for this annotation

GitHub Actions / Go Tests (stylus)

field `minReservePriceLock` is unused (unused)

Check failure on line 36 in timeboost/auctioneer.go

View workflow job for this annotation

GitHub Actions / Go Tests (long)

field `minReservePriceLock` is unused (unused)

Check failure on line 36 in timeboost/auctioneer.go

View workflow job for this annotation

GitHub Actions / Go Tests (challenge)

field `minReservePriceLock` is unused (unused)
minReservePrice *big.Int // TODO(Terence): Do we need to keep min reserve price? assuming contract will automatically update reserve price.
}

func NewAuctioneer(
txOpts *bind.TransactOpts,
chainId *big.Int,
chainId []uint64,
client Client,
auctionContractAddr common.Address,
auctionContract *express_lane_auctiongen.ExpressLaneAuction,
Expand All @@ -54,6 +58,15 @@
if err != nil {
return nil, err
}
reservePrice, err := auctionContract.ReservePrice(&bind.CallOpts{})
if err != nil {
return nil, err
}

hash := sha3.NewLegacyKeccak256()
hash.Write([]byte("TIMEBOOST_BID"))
domainValue := hash.Sum(nil)

am := &Auctioneer{
txOpts: txOpts,
chainId: chainId,
Expand All @@ -64,55 +77,57 @@
initialRoundTimestamp: initialTimestamp,
auctionContractAddr: auctionContractAddr,
roundDuration: roundDuration,
reservePrice: minReservePrice,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a bug. We mixed min reserve price with actual reserve price

auctionClosingDuration: auctionClosingDuration,
reserveSubmissionDuration: reserveSubmissionDuration,
reservePrice: reservePrice,
minReservePrice: minReservePrice,
domainValue: domainValue,
}
for _, o := range opts {
o(am)
}
return am, nil
}

func (am *Auctioneer) ReceiveBid(ctx context.Context, b *Bid) error {
validated, err := am.newValidatedBid(b)
func (a *Auctioneer) ReceiveBid(ctx context.Context, b *Bid) error {
validated, err := a.newValidatedBid(b)
if err != nil {
return fmt.Errorf("could not validate bid: %v", err)

Check failure on line 95 in timeboost/auctioneer.go

View workflow job for this annotation

GitHub Actions / Go Tests (defaults)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)

Check failure on line 95 in timeboost/auctioneer.go

View workflow job for this annotation

GitHub Actions / Go Tests (race)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)

Check failure on line 95 in timeboost/auctioneer.go

View workflow job for this annotation

GitHub Actions / Go Tests (stylus)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)

Check failure on line 95 in timeboost/auctioneer.go

View workflow job for this annotation

GitHub Actions / Go Tests (long)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)

Check failure on line 95 in timeboost/auctioneer.go

View workflow job for this annotation

GitHub Actions / Go Tests (challenge)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}
am.bidCache.add(validated)
a.bidCache.add(validated)
return nil
}

func (am *Auctioneer) Start(ctx context.Context) {
func (a *Auctioneer) Start(ctx context.Context) {
// Receive bids in the background.
go receiveAsync(ctx, am.bidsReceiver, am.ReceiveBid)
go receiveAsync(ctx, a.bidsReceiver, a.ReceiveBid)

// Listen for sequencer health in the background and close upcoming auctions if so.
go am.checkSequencerHealth(ctx)
go a.checkSequencerHealth(ctx)

// Work on closing auctions.
ticker := newAuctionCloseTicker(am.roundDuration, am.auctionClosingDuration)
ticker := newAuctionCloseTicker(a.roundDuration, a.auctionClosingDuration)
go ticker.start()
for {
select {
case <-ctx.Done():
log.Error("Context closed, autonomous auctioneer shutting down")
return
case auctionClosingTime := <-ticker.c:
log.Info("New auction closing time reached", "closingTime", auctionClosingTime, "totalBids", am.bidCache.size())
if err := am.resolveAuction(ctx); err != nil {
log.Info("New auction closing time reached", "closingTime", auctionClosingTime, "totalBids", a.bidCache.size())
if err := a.resolveAuction(ctx); err != nil {
log.Error("Could not resolve auction for round", "error", err)
}
}
}
}

func (am *Auctioneer) resolveAuction(ctx context.Context) error {
upcomingRound := CurrentRound(am.initialRoundTimestamp, am.roundDuration) + 1
func (a *Auctioneer) resolveAuction(ctx context.Context) error {
upcomingRound := CurrentRound(a.initialRoundTimestamp, a.roundDuration) + 1
// If we have no winner, then we can cancel the auction.
// Auctioneer can also subscribe to sequencer feed and
// close auction if sequencer is down.
result := am.bidCache.topTwoBids()
result := a.bidCache.topTwoBids()
first := result.firstPlace
second := result.secondPlace
var tx *types.Transaction
Expand All @@ -124,9 +139,8 @@
// TODO: Retry a given number of times in case of flakey connection.
switch {
case hasBothBids:
fmt.Printf("First express lane controller: %#x\n", first.expressLaneController)
tx, err = am.auctionContract.ResolveMultiBidAuction(
am.txOpts,
tx, err = a.auctionContract.ResolveMultiBidAuction(
a.txOpts,
express_lane_auctiongen.Bid{
ExpressLaneController: first.expressLaneController,
Amount: first.amount,
Expand All @@ -141,8 +155,8 @@
log.Info("Resolving auctions, received two bids", "round", upcomingRound)
case hasSingleBid:
log.Info("Resolving auctions, received single bids", "round", upcomingRound)
tx, err = am.auctionContract.ResolveSingleBidAuction(
am.txOpts,
tx, err = a.auctionContract.ResolveSingleBidAuction(
a.txOpts,
express_lane_auctiongen.Bid{
ExpressLaneController: first.expressLaneController,
Amount: first.amount,
Expand All @@ -157,24 +171,29 @@
if err != nil {
return err
}
receipt, err := bind.WaitMined(ctx, am.client, tx)
receipt, err := bind.WaitMined(ctx, a.client, tx)
if err != nil {
return err
}
if receipt.Status != types.ReceiptStatusSuccessful {
return errors.New("deposit failed")
}
// Clear the bid cache.
am.bidCache = newBidCache()
a.bidCache = newBidCache()
return nil
}

// TODO: Implement. If sequencer is down for some time, cancel the upcoming auction by calling
// the cancel method on the smart contract.
func (am *Auctioneer) checkSequencerHealth(ctx context.Context) {
func (a *Auctioneer) checkSequencerHealth(ctx context.Context) {

}

func CurrentRound(initialRoundTimestamp time.Time, roundDuration time.Duration) uint64 {
return uint64(time.Since(initialRoundTimestamp) / roundDuration)
}

func AuctionClosed(initialRoundTimestamp time.Time, roundDuration time.Duration, auctionClosingDuration time.Duration) (time.Duration, bool) {
d := time.Since(initialRoundTimestamp) % roundDuration
return d, d > auctionClosingDuration
}
9 changes: 9 additions & 0 deletions timeboost/bidder_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/ethereum/go-ethereum/crypto/secp256k1"
"github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen"
"github.com/pkg/errors"
"golang.org/x/crypto/sha3"
)

type Client interface {
Expand All @@ -37,6 +38,7 @@ type BidderClient struct {
auctioneer auctioneerConnection
initialRoundTimestamp time.Time
roundDuration time.Duration
domainValue []byte
}

// TODO: Provide a safer option.
Expand Down Expand Up @@ -67,6 +69,11 @@ func NewBidderClient(
}
initialTimestamp := time.Unix(int64(roundTimingInfo.OffsetTimestamp), 0)
roundDuration := time.Duration(roundTimingInfo.RoundDurationSeconds) * time.Second

hash := sha3.NewLegacyKeccak256()
hash.Write([]byte("TIMEBOOST_BID"))
domainValue := hash.Sum(nil)

return &BidderClient{
chainId: chainId.Uint64(),
name: name,
Expand All @@ -78,6 +85,7 @@ func NewBidderClient(
auctioneer: auctioneer,
initialRoundTimestamp: initialTimestamp,
roundDuration: roundDuration,
domainValue: domainValue,
}, nil
}

Expand Down Expand Up @@ -109,6 +117,7 @@ func (bd *BidderClient) Bid(
Signature: nil,
}
packedBidBytes, err := encodeBidValues(
bd.domainValue,
new(big.Int).SetUint64(newBid.ChainId),
bd.auctionContractAddress,
newBid.Round,
Expand Down
52 changes: 36 additions & 16 deletions timeboost/bids.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"crypto/ecdsa"
"encoding/binary"
"fmt"
"math/big"
"sync"

Expand All @@ -21,6 +22,7 @@ var (
ErrWrongSignature = errors.New("wrong signature")
ErrBadRoundNumber = errors.New("bad round number")
ErrInsufficientBalance = errors.New("insufficient balance")
ErrInsufficientBid = errors.New("insufficient bid")
)

type Bid struct {
Expand All @@ -39,13 +41,13 @@ type validatedBid struct {
signature []byte
}

func (am *Auctioneer) fetchReservePrice() *big.Int {
am.reservePriceLock.RLock()
defer am.reservePriceLock.RUnlock()
return new(big.Int).Set(am.reservePrice)
func (a *Auctioneer) fetchReservePrice() *big.Int {
a.reservePriceLock.RLock()
defer a.reservePriceLock.RUnlock()
return new(big.Int).Set(a.reservePrice)
}

func (am *Auctioneer) newValidatedBid(bid *Bid) (*validatedBid, error) {
func (a *Auctioneer) newValidatedBid(bid *Bid) (*validatedBid, error) {
// Check basic integrity.
if bid == nil {
return nil, errors.Wrap(ErrMalformedData, "nil bid")
Expand All @@ -56,24 +58,41 @@ func (am *Auctioneer) newValidatedBid(bid *Bid) (*validatedBid, error) {
if bid.ExpressLaneController == (common.Address{}) {
return nil, errors.Wrap(ErrMalformedData, "empty express lane controller address")
}
// Verify chain id.
if new(big.Int).SetUint64(bid.ChainId).Cmp(am.chainId) != 0 {
return nil, errors.Wrapf(ErrWrongChainId, "wanted %#x, got %#x", am.chainId, bid.ChainId)

// Check if the chain ID is valid.
chainIdOk := false
for _, id := range a.chainId {
if bid.ChainId == id {
chainIdOk = true
break
}
}
// Check if for upcoming round.
upcomingRound := CurrentRound(am.initialRoundTimestamp, am.roundDuration) + 1
if !chainIdOk {
return nil, errors.Wrapf(ErrWrongChainId, "can not aucution for chain id: %d", bid.ChainId)
}

// Check if the bid is intended for upcoming round.
upcomingRound := CurrentRound(a.initialRoundTimestamp, a.roundDuration) + 1
if bid.Round != upcomingRound {
return nil, errors.Wrapf(ErrBadRoundNumber, "wanted %d, got %d", upcomingRound, bid.Round)
}
// Check bid amount.
reservePrice := am.fetchReservePrice()

// Check if the auction is closed.
if d, closed := AuctionClosed(a.initialRoundTimestamp, a.roundDuration, a.auctionClosingDuration); closed {
return nil, fmt.Errorf("auction is closed, %d seconds into the round", d)
}

// Check bid is higher than reserve price.
reservePrice := a.fetchReservePrice()
if bid.Amount.Cmp(reservePrice) == -1 {
return nil, errors.Wrap(ErrMalformedData, "expected bid to be at least of reserve price magnitude")
return nil, errors.Wrapf(ErrInsufficientBid, "reserve price %s, bid %s", reservePrice, bid.Amount)
}

// Validate the signature.
packedBidBytes, err := encodeBidValues(
a.domainValue,
new(big.Int).SetUint64(bid.ChainId),
am.auctionContractAddr,
bid.AuctionContractAddress,
bid.Round,
bid.Amount,
bid.ExpressLaneController,
Expand Down Expand Up @@ -103,7 +122,7 @@ func (am *Auctioneer) newValidatedBid(bid *Bid) (*validatedBid, error) {
// TODO: Validate reserve price against amount of bid.
// TODO: No need to do anything expensive if the bid coming is in invalid.
// Cache this if the received time of the bid is too soon. Include the arrival timestamp.
depositBal, err := am.auctionContract.BalanceOf(&bind.CallOpts{}, bid.Bidder)
depositBal, err := a.auctionContract.BalanceOf(&bind.CallOpts{}, bid.Bidder)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -180,10 +199,11 @@ func padBigInt(bi *big.Int) []byte {
return padded
}

func encodeBidValues(chainId *big.Int, auctionContractAddress common.Address, round uint64, amount *big.Int, expressLaneController common.Address) ([]byte, error) {
func encodeBidValues(domainValue []byte, chainId *big.Int, auctionContractAddress common.Address, round uint64, amount *big.Int, expressLaneController common.Address) ([]byte, error) {
buf := new(bytes.Buffer)

// Encode uint256 values - each occupies 32 bytes
buf.Write(domainValue)
buf.Write(padBigInt(chainId))
buf.Write(auctionContractAddress[:])
roundBuf := make([]byte, 8)
Expand Down
4 changes: 2 additions & 2 deletions timeboost/bids_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestResolveAuction(t *testing.T) {

// Set up a new auction master instance that can validate bids.
am, err := NewAuctioneer(
testSetup.accounts[0].txOpts, testSetup.chainId, testSetup.backend.Client(), testSetup.expressLaneAuctionAddr, testSetup.expressLaneAuction,
testSetup.accounts[0].txOpts, []uint64{testSetup.chainId.Uint64()}, testSetup.backend.Client(), testSetup.expressLaneAuctionAddr, testSetup.expressLaneAuction,
)
require.NoError(t, err)

Expand Down Expand Up @@ -76,7 +76,7 @@ func TestReceiveBid_OK(t *testing.T) {

// Set up a new auction master instance that can validate bids.
am, err := NewAuctioneer(
testSetup.accounts[1].txOpts, testSetup.chainId, testSetup.backend.Client(), testSetup.expressLaneAuctionAddr, testSetup.expressLaneAuction,
testSetup.accounts[1].txOpts, []uint64{testSetup.chainId.Uint64()}, testSetup.backend.Client(), testSetup.expressLaneAuctionAddr, testSetup.expressLaneAuction,
)
require.NoError(t, err)

Expand Down
Loading