Skip to content

Commit

Permalink
Clean up auctioneer part 1
Browse files Browse the repository at this point in the history
  • Loading branch information
terencechain committed Jul 25, 2024
1 parent fc81996 commit dcb5525
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 151 deletions.
2 changes: 1 addition & 1 deletion system_tests/seqfeed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ func TestSequencerFeed_ExpressLaneAuction(t *testing.T) {
waitTime = roundDuration - time.Duration(now.Second())*time.Second - time.Duration(now.Nanosecond())
time.Sleep(waitTime)

currRound := timeboost.CurrentRound(time.Unix(int64(info.OffsetTimestamp), 0), roundDuration)
currRound := timeboost.currentRound(time.Unix(int64(info.OffsetTimestamp), 0), roundDuration)
t.Log("curr round", currRound)
if currRound != winnerRound {
now = time.Now()
Expand Down
189 changes: 148 additions & 41 deletions timeboost/auctioneer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,26 @@ 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/log"
"github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen"
"github.com/pkg/errors"
"golang.org/x/crypto/sha3"
)

// domainValue is the Keccak256 hash of the string "TIMEBOOST_BID".
// This variable represents a fixed domain identifier used in the express lane auction.
var domainValue = []byte{

This comment has been minimized.

Copy link
@rauljordan

rauljordan Jul 25, 2024

Contributor

why not compute it on init() of the package?

0xc7, 0xf4, 0x5f, 0x6f, 0x1b, 0x1e, 0x1d, 0xfc,
0x22, 0xe1, 0xb9, 0xf6, 0x9c, 0xda, 0x8e, 0x4e,
0x86, 0xf4, 0x84, 0x81, 0xf0, 0xc5, 0xe0, 0x19,
0x7c, 0x3f, 0x09, 0x1b, 0x89, 0xe8, 0xeb, 0x12,
}

type AuctioneerOpt func(*Auctioneer)

// Auctioneer is a struct that represents an autonomous auctioneer.
// It is responsible for receiving bids, validating them, and resolving auctions.
// Spec: https://github.com/OffchainLabs/timeboost-design/tree/main
type Auctioneer struct {
txOpts *bind.TransactOpts
chainId []uint64 // Auctioneer could handle auctions on multiple chains.
Expand All @@ -30,18 +42,15 @@ type Auctioneer struct {
roundDuration time.Duration
auctionClosingDuration time.Duration
reserveSubmissionDuration time.Duration
auctionContractAddr common.Address
reservePriceLock sync.RWMutex
reservePrice *big.Int
minReservePriceLock sync.RWMutex
minReservePrice *big.Int // TODO(Terence): Do we need to keep min reserve price? assuming contract will automatically update reserve price.
}

// NewAuctioneer creates a new autonomous auctioneer struct.
func NewAuctioneer(
txOpts *bind.TransactOpts,
chainId []uint64,
client Client,
auctionContractAddr common.Address,
auctionContract *express_lane_auctiongen.ExpressLaneAuction,
opts ...AuctioneerOpt,
) (*Auctioneer, error) {
Expand All @@ -54,33 +63,23 @@ func NewAuctioneer(
auctionClosingDuration := time.Duration(roundTimingInfo.AuctionClosingSeconds) * time.Second
reserveSubmissionDuration := time.Duration(roundTimingInfo.ReserveSubmissionSeconds) * time.Second

minReservePrice, err := auctionContract.MinReservePrice(&bind.CallOpts{})
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,
client: client,
auctionContract: auctionContract,
bidsReceiver: make(chan *Bid, 10_000),
bidsReceiver: make(chan *Bid, 10_000), // TODO(Terence): Is 10000 enough? Make this configurable?
bidCache: newBidCache(),
initialRoundTimestamp: initialTimestamp,
auctionContractAddr: auctionContractAddr,
roundDuration: roundDuration,
auctionClosingDuration: auctionClosingDuration,
reserveSubmissionDuration: reserveSubmissionDuration,
reservePrice: reservePrice,
minReservePrice: minReservePrice,
domainValue: domainValue,
}
for _, o := range opts {
Expand All @@ -89,18 +88,20 @@ func NewAuctioneer(
return am, nil
}

func (a *Auctioneer) ReceiveBid(ctx context.Context, b *Bid) error {
validated, err := a.newValidatedBid(b)
// ReceiveBid validates and adds a bid to the bid cache.
func (a *Auctioneer) receiveBid(ctx context.Context, b *Bid) error {
vb, err := a.validateBid(b)
if err != nil {
return fmt.Errorf("could not validate bid: %v", err)
return errors.Wrap(err, "could not validate bid")
}
a.bidCache.add(validated)
a.bidCache.add(vb)
return nil
}

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

// Listen for sequencer health in the background and close upcoming auctions if so.
go a.checkSequencerHealth(ctx)
Expand All @@ -118,27 +119,22 @@ func (a *Auctioneer) Start(ctx context.Context) {
if err := a.resolveAuction(ctx); err != nil {
log.Error("Could not resolve auction for round", "error", err)
}
// Clear the bid cache.
a.bidCache = newBidCache()
}
}
}

// resolveAuction resolves the auction by calling the smart contract with the top two bids.
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 := a.bidCache.topTwoBids()
first := result.firstPlace
second := result.secondPlace
var tx *types.Transaction
var err error
hasSingleBid := first != nil && second == nil
hasBothBids := first != nil && second != nil
noBids := first == nil && second == nil

// TODO: Retry a given number of times in case of flakey connection.
switch {
case hasBothBids:
case first != nil && second != nil: // Both bids are present
tx, err = a.auctionContract.ResolveMultiBidAuction(
a.txOpts,
express_lane_auctiongen.Bid{
Expand All @@ -152,9 +148,9 @@ func (a *Auctioneer) resolveAuction(ctx context.Context) error {
Signature: second.signature,
},
)
log.Info("Resolving auctions, received two bids", "round", upcomingRound)
case hasSingleBid:
log.Info("Resolving auctions, received single bids", "round", upcomingRound)
log.Info("Resolving auction with two bids", "round", upcomingRound)

case first != nil: // Single bid is present
tx, err = a.auctionContract.ResolveSingleBidAuction(
a.txOpts,
express_lane_auctiongen.Bid{
Expand All @@ -163,23 +159,32 @@ func (a *Auctioneer) resolveAuction(ctx context.Context) error {
Signature: first.signature,
},
)
case noBids:
// TODO: Cancel the upcoming auction.
log.Info("No bids received for auction resolution")
log.Info("Resolving auction with single bid", "round", upcomingRound)

case second == nil: // No bids received
log.Info("No bids received for auction resolution", "round", upcomingRound)
return nil
}

if err != nil {
log.Error("Error resolving auction", "error", err)
return err
}

receipt, err := bind.WaitMined(ctx, a.client, tx)
if err != nil {
log.Error("Error waiting for transaction to be mined", "error", err)
return err
}
if receipt.Status != types.ReceiptStatusSuccessful {
return errors.New("deposit failed")

if tx == nil || receipt == nil || receipt.Status != types.ReceiptStatusSuccessful {
if tx != nil {
log.Error("Transaction failed or did not finalize successfully", "txHash", tx.Hash().Hex())
}
return errors.New("transaction failed or did not finalize successfully")
}
// Clear the bid cache.
a.bidCache = newBidCache()

log.Info("Auction resolved successfully", "txHash", tx.Hash().Hex())
return nil
}

Expand All @@ -189,11 +194,113 @@ func (a *Auctioneer) checkSequencerHealth(ctx context.Context) {

}

// TODO(Terence): Set reserve price from the contract.

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

func (a *Auctioneer) validateBid(bid *Bid) (*validatedBid, error) {
// Check basic integrity.
if bid == nil {
return nil, errors.Wrap(ErrMalformedData, "nil bid")
}
if bid.Bidder == (common.Address{}) {
return nil, errors.Wrap(ErrMalformedData, "empty bidder address")
}
if bid.ExpressLaneController == (common.Address{}) {
return nil, errors.Wrap(ErrMalformedData, "empty express lane controller address")
}

// Check if the chain ID is valid.
chainIdOk := false
for _, id := range a.chainId {
if bid.ChainId == id {
chainIdOk = true
break
}
}
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 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.Wrapf(ErrInsufficientBid, "reserve price %s, bid %s", reservePrice, bid.Amount)
}

// Validate the signature.
packedBidBytes, err := encodeBidValues(
a.domainValue,
new(big.Int).SetUint64(bid.ChainId),
bid.AuctionContractAddress,
bid.Round,
bid.Amount,
bid.ExpressLaneController,
)
if err != nil {
return nil, ErrMalformedData
}
if len(bid.Signature) != 65 {
return nil, errors.Wrap(ErrMalformedData, "signature length is not 65")
}
// Recover the public key.
prefixed := crypto.Keccak256(append([]byte("\x19Ethereum Signed Message:\n112"), packedBidBytes...))
sigItem := make([]byte, len(bid.Signature))
copy(sigItem, bid.Signature)
if sigItem[len(sigItem)-1] >= 27 {
sigItem[len(sigItem)-1] -= 27
}
pubkey, err := crypto.SigToPub(prefixed, sigItem)
if err != nil {
return nil, ErrMalformedData
}
if !verifySignature(pubkey, packedBidBytes, sigItem) {
return nil, ErrWrongSignature
}
// Validate if the user if a depositor in the contract and has enough balance for the bid.
// TODO: Retry some number of times if flakey connection.
// 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 := a.auctionContract.BalanceOf(&bind.CallOpts{}, bid.Bidder)
if err != nil {
return nil, err
}
if depositBal.Cmp(new(big.Int)) == 0 {
return nil, ErrNotDepositor
}
if depositBal.Cmp(bid.Amount) < 0 {
return nil, errors.Wrapf(ErrInsufficientBalance, "onchain balance %#x, bid amount %#x", depositBal, bid.Amount)
}
return &validatedBid{
expressLaneController: bid.ExpressLaneController,
amount: bid.Amount,
signature: bid.Signature,
}, nil
}

// CurrentRound returns the current round number.
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) {
// auctionClosed returns the time since auction was closed and whether the auction is closed.
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: 2 additions & 7 deletions timeboost/bidder_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ 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 @@ -24,7 +23,7 @@ type Client interface {
}

type auctioneerConnection interface {
ReceiveBid(ctx context.Context, bid *Bid) error
receiveBid(ctx context.Context, bid *Bid) error
}

type BidderClient struct {
Expand Down Expand Up @@ -70,10 +69,6 @@ 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 Down Expand Up @@ -132,7 +127,7 @@ func (bd *BidderClient) Bid(
return nil, err
}
newBid.Signature = sig
if err = bd.auctioneer.ReceiveBid(ctx, newBid); err != nil {
if err = bd.auctioneer.receiveBid(ctx, newBid); err != nil {
return nil, err
}
return newBid, nil
Expand Down
Loading

0 comments on commit dcb5525

Please sign in to comment.