Skip to main content

Timeboost: API Reference

Auctioneer API Reference

Overview

The auctioneer exposes a namespace called auctioneer with methods for managing bids in the express lane auction system. This API allows bidders to participate in auctions for controlling express lane access in each round.

Methods

auctioneer_submitBid

Submit a bid to become an express lane controller for an upcoming round.

Parameters
ParameterTypeDescription
chainIdbigIntChain ID of the target chain
expressLaneControlleraddressHex string of desired express lane controller address (0x-prefixed)
auctionContractAddressaddressHex string of auction contract address (0x-prefixed)
rounduint64Round number (0-indexed) for target round
amountbigIntBid amount in wei of deposit ERC-20 token
signaturebytesEthereum signature over bid data
Signature Format

The signature must be created over the following packed values:

keccak256(abi.encodePacked(
domainValue, // uint16 domain separator
chainId, // uint64
round, // uint64
amount, // uint256
expressLaneController // address
))

See signature verification implementation:

416:478:prototype/contracts/src/ExpressLaneAuction.sol
    function verifySignature(
address signer,
bytes memory message,
bytes memory signature
) public pure returns (bool) {
bytes32 messageHash = getMessageHash(message);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
address recoveredSigner = recoverSigner(ethSignedMessageHash, signature);
return recoveredSigner == signer;
}

// Function to hash the message
function getMessageHash(bytes memory message) internal pure returns (bytes32) {
return keccak256(message);
}

// Function to recreate the Ethereum signed message hash
function getEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32) {
/*
Signature is produced from the Keccak256 hash of the concatenation of
"\x19Ethereum Signed Message:\n" with the length of the message and the message itself.
Here, "\x19" is the control character used to indicate that the string is a signed message.
"\n" is a new line character.
*/
return keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)
);
}

// Function to recover the signer from the signature
function recoverSigner(
bytes32 _ethSignedMessageHash, bytes memory _signature
) internal pure returns (address) {
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);
return ecrecover(_ethSignedMessageHash, v, r, s);
}

// Helper function to split the signature into r, s and v
function splitSignature(bytes memory sig)
internal
pure
returns (
bytes32 r,
bytes32 s,
uint8 v
)
{
require(sig.length == 65, "invalid signature length");

assembly {
// First 32 bytes, after the length prefix
r := mload(add(sig, 32))
// Second 32 bytes
s := mload(add(sig, 64))
// Final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}

// If the signature is valid (and not malleable), v should be 27 or 28
// However, as of EIP-155, v = 27 or 28 + chainId * 2 + 8
if (v < 27) v += 27;
return (r, s, v);
}
Example Usage
timeboost_sendExpressLaneTransaction
{
"type": "object",
"properties": {
"chainId": {
"type": "bigInt",
"description": "chain id of the target chain"
},
"round": {
"type": "uint64",
"description": "round number (0-indexed) for the round the transaction is submitted for"
},
"auctionContractAddress": {
"type": "address",
"description": "hex string of the auction contract address that the bid corresponds to"
},
"sequenceNumber": {
"type": "uint64",
"description": "the per-round nonce of express lane submissions. Each submission to the express lane during a round increases this sequence number by one, and if submissions are received out of order, the sequencer will queue them for processing in order. This is reset to 0 at each round"
},
"transaction": {
"type": "bytes",
"description": "hex string of the RLP encoded transaction payload that submitter wishes to be sequenced through the express lane"
},
"options": {
"type": "ArbitrumConditionalOptions",
"description": "conditional options for Arbitrum transactions, supported by normal sequencer endpoint https://github.com/OffchainLabs/go-ethereum/blob/48de2030c7a6fa8689bc0a0212ebca2a0c73e3ad/arbitrum_types/txoptions.go#L71"
},
"signature": {
"type": "bytes",
"description": "Ethereum signature over the bytes encoding of (keccak256(TIMEBOOST_BID), padTo32Bytes(chainId), auctionContractAddress, uint64ToBytes(round), uint64ToBytes(sequenceNumber), transaction)"
}
},
}
bid := &Bid{
chainId: chainId,
address: bidderAddr,
round: currentRound + 1,
amount: big.NewInt(1000),
}

// Sign the bid
packedBidBytes, _ := encodeBidValues(
domainValue,
new(big.Int).SetUint64(bid.chainId),
new(big.Int).SetUint64(bid.round),
bid.amount,
)
signature, _ := sign(packedBidBytes, privateKey)
bid.signature = signature

// Submit bid
err := auctioneer.SubmitBid(ctx, bid)
Validation

The auctioneer performs the following validation:

  • All bid fields must be non-nil
  • Auction contract address must be valid
  • Express lane controller address must be defined
  • Chain ID must match target chain
  • Bid must be for an upcoming round
  • Bidding must be open when bid is received
  • Bid must meet minimum reserve price
  • Signature must be valid and recover correct sender
  • Sender must be a depositor with sufficient balance
Error Responses
Error CodeDescription
MALFORMED_DATAInvalid input data, missing fields, or deserialization failure
NOT_DEPOSITORAddress is not an active depositor in auction contract
WRONG_CHAIN_IDChain ID does not match target chain
WRONG_SIGNATUREInvalid or unverifiable signature
BAD_ROUND_NUMBERInvalid round number (e.g. past round)
INSUFFICIENT_BALANCEBid amount exceeds depositor's balance
RESERVE_PRICE_NOT_METBid below minimum reserve price
Error Prevention
  1. Always verify bidder has sufficient deposit before submitting bid:

212:225:prototype/bidder_client.go

func (bd *BidderClient) Deposit(ctx context.Context, amount *big.Int) error {
tx, err := bd.auctionContract.SubmitDeposit(bd.txOpts, amount)
if err != nil {
return err
}
receipt, err := bind.WaitMined(ctx, bd.client, tx)
if err != nil {
return err
}
if receipt.Status != types.ReceiptStatusSuccessful {
return errors.New("deposit failed")
}
return nil
}
  1. Ensure bid is for upcoming round using current round calculation:
    function currentRound() public view returns (uint64) {
if (initialTimestamp > block.timestamp) {
return type(uint64).max;
}
return uint64((block.timestamp - initialTimestamp) / roundDuration);
}
  1. Validate signature format matches expected encoding:

134:136:prototype/bids.go

func verifySignature(pubkey *ecdsa.PublicKey, message []byte, sig []byte) bool {
hash := crypto.Keccak256(message)
prefixed := crypto.Keccak256([]byte("\x19Ethereum Signed Message:\n32"), hash)

Events

The auction contract emits events for important state changes:

DepositSubmitted

    /// @param amount    the amount in wei of the deposit
event DepositSubmitted(address indexed bidder, uint256 amount);

AuctionResolved

47:52:prototype/contracts/src/ExpressLaneAuction.sol

    event AuctionResolved(
uint256 winningBidAmount,
uint256 secondPlaceBidAmount,
address indexed winningBidder,
uint256 indexed winnerRound
);

ExpressLaneControlDelegated

57:61:prototype/contracts/src/ExpressLaneAuction.sol

    event ExpressLaneControlDelegated(
address indexed from,
address indexed to,
uint64 round
);

Testing

See example test implementation for bid submission and validation:

12:68:prototype/bids_test.go

func TestWinningBidderBecomesExpressLaneController(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

testSetup := setupAuctionTest(t, ctx)

// Set up two different bidders.
alice := setupBidderClient(t, ctx, "alice", testSetup.accounts[0], testSetup)
bob := setupBidderClient(t, ctx, "bob", testSetup.accounts[1], testSetup)
require.NoError(t, alice.Deposit(ctx, big.NewInt(5)))
require.NoError(t, bob.Deposit(ctx, big.NewInt(5)))

// Set up a new auctioneer instance that can validate bids.
am, err := NewAuctioneer(
testSetup.accounts[2].txOpts, testSetup.chainId, testSetup.backend.Client(), testSetup.auctionContract,
)
require.NoError(t, err)
alice.auctioneer = am
bob.auctioneer = am

// Form two new bids for the round, with Alice being the bigger one.
aliceBid, err := alice.Bid(ctx, big.NewInt(2))
require.NoError(t, err)
bobBid, err := bob.Bid(ctx, big.NewInt(1))
require.NoError(t, err)
_, _ = aliceBid, bobBid

// Resolve the auction.
require.NoError(t, am.resolveAuctions(ctx))

// Expect Alice to have become the next express lane controller.
upcomingRound := CurrentRound(am.initialRoundTimestamp, am.roundDuration) + 1
controller, err := testSetup.auctionContract.ExpressLaneControllerByRound(&bind.CallOpts{}, big.NewInt(int64(upcomingRound)))
require.NoError(t, err)
require.Equal(t, alice.txOpts.From, controller)
}

func TestSubmitBid_OK(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

testSetup := setupAuctionTest(t, ctx)

// Make a deposit as a bidder into the contract.
bc := setupBidderClient(t, ctx, "alice", testSetup.accounts[0], testSetup)
require.NoError(t, bc.Deposit(ctx, big.NewInt(5)))

// Set up a new auctioneer instance that can validate bids.
am, err := NewAuctioneer(
testSetup.accounts[1].txOpts, testSetup.chainId, testSetup.backend.Client(), testSetup.auctionContract,
)
require.NoError(t, err)
bc.auctioneer = am

// Form a new bid with an amount.
newBid, err := bc.Bid(ctx, big.NewInt(5))
require.NoError(t, err)

Sequencer API Reference

Overview

The sequencer exposes a namespace called timeboost with methods for submitting transactions through the express lane system. This API allows express lane controllers to submit prioritized transactions during their assigned rounds.

Methods

timeboost_sendExpressLaneTransaction

Submit a transaction to be processed through the express lane with priority sequencing.

Parameters
ParameterTypeDescription
chainIdbigIntChain ID of the target chain
rounduint64Round number (0-indexed) for target round
auctionContractAddressaddressHex string of auction contract address (0x-prefixed)
sequenceNumberuint64Per-round nonce for express lane submissions
transactionbytesRLP encoded transaction payload
optionsArbitrumConditionalOptionsConditional options for Arbitrum transactions
signaturebytesEthereum signature over transaction data
Signature Format

The signature must be created over the following packed values:

keccak256(abi.encodePacked(
TIMEBOOST_BID, // domain separator
chainId, // uint64
auctionContractAddress, // address
round, // uint64
sequenceNumber, // uint64
transaction // bytes
))
Example Usage
{
"type": "object",
"properties": {
"chainId": {
"type": "bigInt",
"description": "chain id of the target chain"
},
"round": {
"type": "uint64",
"description": "round number (0-indexed) for the round the transaction is submitted for"
},
"auctionContractAddress": {
"type": "address",
"description": "hex string of the auction contract address that the bid corresponds to"
},
"sequenceNumber": {
"type": "uint64",
"description": "the per-round nonce of express lane submissions. Each submission to the express lane during a round increases this sequence number by one, and if submissions are received out of order, the sequencer will queue them for processing in order. This is reset to 0 at each round"
},
"transaction": {
"type": "bytes",
"description": "hex string of the RLP encoded transaction payload that submitter wishes to be sequenced through the express lane"
},
"options": {
"type": "ArbitrumConditionalOptions",
"description": "conditional options for Arbitrum transactions, supported by normal sequencer endpoint https://github.com/OffchainLabs/go-ethereum/blob/48de2030c7a6fa8689bc0a0212ebca2a0c73e3ad/arbitrum_types/txoptions.go#L71"
},
"signature": {
"type": "bytes",
"description": "Ethereum signature over the bytes encoding of (keccak256(TIMEBOOST_BID), padTo32Bytes(chainId), auctionContractAddress, uint64ToBytes(round), uint64ToBytes(sequenceNumber), transaction)"
}
}
}
tx := &ExpressLaneTx{
chainId: chainId,
round: currentRound,
sequenceNumber: 0,
transaction: encodedTx,
options: &ArbitrumTxOptions{
// Set desired options
},
}

// Sign the transaction data
signature := signExpressLaneTx(
tx.chainId,
tx.round,
tx.sequenceNumber,
tx.transaction,
controllerPrivKey,
)
tx.signature = signature

// Submit to sequencer
err := sequencer.SendExpressLaneTransaction(ctx, tx)
Validation

The sequencer performs the following validation:

  • All fields must be non-nil
  • Chain ID must match target chain
  • Auction contract address must be valid
  • Current round must have an express lane controller
  • Transaction must be for current round
  • Signature must be valid and recover correct sender
  • Sender must be current express lane controller
  • Sequence number must be in order (queued if not)
Error Responses
Error CodeDescription
MALFORMED_DATAInvalid input data, missing fields, or deserialization failure
WRONG_CHAIN_IDChain ID does not match target chain
WRONG_SIGNATUREInvalid or unverifiable signature
BAD_ROUND_NUMBERInvalid round number (e.g. past round)
NOT_EXPRESS_LANE_CONTROLLERSender is not the express lane controller
NO_ONCHAIN_CONTROLLERNo defined controller for the round
Error Prevention
  1. Check if you are the controller for the current round:

The sequencer will perform the following validation:

  • check the fields are not nil
  • check if the chain id is correct
  • check if the auction contract address is correct
  1. Track sequence numbers to ensure in-order submission:
// Example sequence number tracking
type ExpressLaneController struct {
currentSequenceNumber uint64
mutex sync.Mutex
}

func (c *ExpressLaneController) NextSequenceNumber() uint64 {
c.mutex.Lock()
defer c.mutex.Unlock()
seq := c.currentSequenceNumber
c.currentSequenceNumber++
return seq
}
  1. Verify transaction round matches current round:
function currentRound() public view returns (uint64) {
if (initialTimestamp > block.timestamp) {
return type(uint64).max;
}
return uint64((block.timestamp - initialTimestamp) / roundDuration);
}

Events

The sequencer emits events for express lane transaction processing:

ExpressLaneTxProcessed

event ExpressLaneTxProcessed(
address indexed controller,
uint64 indexed round,
uint64 sequenceNumber,
bytes32 txHash
);

ExpressLaneTxQueued

event ExpressLaneTxQueued(
address indexed controller,
uint64 indexed round,
uint64 sequenceNumber,
bytes32 txHash
);

Testing

See example test implementation for express lane transaction submission:

Basic Express Lane Controller Test

func TestWinningBidderBecomesExpressLaneController(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

testSetup := setupAuctionTest(t, ctx)

// Set up bidders
alice := setupBidderClient(t, ctx, "alice", testSetup.accounts[0], testSetup)
bob := setupBidderClient(t, ctx, "bob", testSetup.accounts[1], testSetup)
require.NoError(t, alice.Deposit(ctx, big.NewInt(5)))
require.NoError(t, bob.Deposit(ctx, big.NewInt(5)))

// Set up auctioneer
am, err := NewAuctioneer(
testSetup.accounts[2].txOpts,
testSetup.chainId,
testSetup.backend.Client(),
testSetup.auctionContract,
)
require.NoError(t, err)
alice.auctioneer = am
bob.auctioneer = am

// Submit bids
aliceBid, err := alice.Bid(ctx, big.NewInt(2))
require.NoError(t, err)
bobBid, err := bob.Bid(ctx, big.NewInt(1))
require.NoError(t, err)

// Resolve auction
require.NoError(t, am.resolveAuctions(ctx))

// Verify winner
upcomingRound := CurrentRound(am.initialRoundTimestamp, am.roundDuration) + 1
controller, err := testSetup.auctionContract.ExpressLaneControllerByRound(
&bind.CallOpts{},
big.NewInt(int64(upcomingRound)),
)
require.NoError(t, err)
require.Equal(t, alice.txOpts.From, controller)
}