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

feat(protocol): implement batch block auction #13836

Merged
merged 56 commits into from Jun 11, 2023

Conversation

adaki2004
Copy link
Contributor

@adaki2004 adaki2004 commented May 29, 2023

So this is the implementation of the Auction mechanism. Not tested and by any means it is ready !
I'd like to first challenge the design by you, then create tests when we think it is good to go.

You can see interfaces introduced in TaikoL1.sol, but some desc.:

  • bidForBatch() : Bids for a batch with the following struct:
    struct Bid {
        uint256 batchId; 
        address prover;
        uint64 deposit;
        uint64 feePerGas;
    }
  • batchId can be determined for a block by calling: blockIdToBatchId(uint256 blockId)

  • Batches currently a range of 100 and this: id(0): 1..100, id(1): 101..200, etc.

  • isBlockProvableBy(uint256 blockId, address prover) : It return if a block is proveable by that specified address. It can return true in 2 cases :
    1: address owns the winning bid (auction bid window elapsed, but proofTime window not yet),
    2: an address tho does not own the winning bid, but the proof window (currently set to 2 horus) elapsed

  • isBidAcceptable(TaikoData.Bid calldata bid) : A helper for the clients, to know if their wannabe bids can be accepted based on the criteria (either bc there is not yet a bid, or 90% better then the current winning bid)
    - isBatchAuctionable(uint256 batchId): Again a helper, to see if auction is ongoing (either bc. there is no bid, or there is at least one bid but the time for auction window not yet elapsed)

Open questions:

  1. Reward distribution mechanism is: (see _markBlockVerified())
  • If the winning bidder submits a proof per given block, it gets back his deposit / block + the reward.
  • If 'anoyne' submitted a block (so outside of proof window), it gets the reward + the half of the deposit/ block.
    Is it OK ? Shall i modify something ?
  1. Currently the proposer fee is tied to averageRewardPerBlock() (because i cannot tie it to average fee per gas, because at proposal only the gasLimit is available).
  • Is it OK ? If so, what should happen when there is no data available for the average ?
  • If not ok, what would be the preferred way ?

@@ -114,6 +123,13 @@ library TaikoData {
uint96 amount;
}

struct Bid {
uint256 minFeePerGasInWei;
Copy link
Contributor

Choose a reason for hiding this comment

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

feePerGas shall be fine

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should have a Auction struct and a Bid struct

// 2 bytes32
struct Bid {
   uint64 batchId; 
   address prover;
   uint16 proofWindow; // allow bider to use a smaller window to win a bid?
   uint64 deposit;
   uint64 feePerGas;
}

// still 2 bytes32
struct Auction {
   Bid bid;
   uint64 startedAt;
}

Copy link
Contributor Author

@adaki2004 adaki2004 May 30, 2023

Choose a reason for hiding this comment

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

proofWindow: It is very confusing. Is it auction window or proof window ? (Based on the comment the former). If so, why would we allow bidder to set itself a smaller auction window ? I'd skip this var.

Copy link
Contributor

Choose a reason for hiding this comment

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

Two reasons to have the proving window there:

  • the proving window should be adjusted automatically after a block is verified, but we do not to want to proving window for a specific block or a batch to be dynamic, so better to keep a copy there when the auction started.
  • The proving window is another parameter to the bid comparison function, this allows us to favor bids that promises with earlier proofs to win if we want.

Copy link
Contributor Author

@adaki2004 adaki2004 May 31, 2023

Choose a reason for hiding this comment

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

Ok, i have a config.worstCaseProofWindowInSec by default so that after that everyone could submit a proof regardless of the auciton winner. I can put this proofWindow and use it as 'scoring' algo.

@@ -126,6 +142,7 @@ library TaikoData {
) forkChoiceIds;
mapping(address account => uint256 balance) taikoTokenBalances;
mapping(bytes32 txListHash => TxListInfo) txListInfo;
mapping(uint256 batchStartBlockId => Bid bid) bids;
Copy link
Contributor

@dantaik dantaik May 30, 2023

Choose a reason for hiding this comment

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

Use a batch ID here is better I think, batchId = (blockId - 1) / batch_size

* being paid as a reward for proving the block.
*/

function bidForBatch(uint256 startBlockId, uint256 minFeePerGasInWei)
Copy link
Contributor

Choose a reason for hiding this comment

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

function bidForBlocks(Bid calldata bid) {

}

uint256 batchStartBlockId,
uint256 minFeePerGasInWei
) internal {
// If it verified already -> Revert, otherwise it would be possible
Copy link
Contributor

Choose a reason for hiding this comment

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

The block ID => batch mapping should consider the fact the genesis block is at 0, and the first batch will cover block #1 to block #100, assuming batch size is 100.

funciton blockIdToBatch(uint blockId) {
   return (blockId - 1)/batch_size;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Block proposal and batch auction can be entirely independent: we can have a million blocks proposed and we are still auctioning the first 100K blocks, or we can have more block batch auctioned but a few blocks proposed. Decoupling these two will make things easier.

Since proofs cannot be submitted without its batch auction complete. When someone bid for a batch, we do not need to check if lastVerifiedBlockId is smaller/larger than the first block in the batch. We just need to check if the auction has started/ended.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please see this comment from Brecht:
#13813 (comment)
The auctions can be for block ids %n, and so also n the number of auctions that need to be run for a sequential range of blocks, that way we make the system more parallelizable because now the prover doesn't have to be able to prove all sequential blocks himself, the work can be shared between up to n provers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

kép

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok after discussion (Discord) i'll proceed with Daniel's solution.

Copy link
Contributor

Choose a reason for hiding this comment

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

Lets expect provers to have their own ways of distributing blocks of a batch to its workers. They may have their own auction offchain.

// If there is an existing bid already
// we compare this bid to the existing one first, to see if its a
// lower fee accepted.
if (currentBid.bidder != address(0)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Always use a ring buffer please, this check is then incorrect -- we need to check if batchId matches, bidder (call it prover please) will not be reset to address(0).

Copy link
Contributor Author

@adaki2004 adaki2004 May 30, 2023

Choose a reason for hiding this comment

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

When we use Brecht's proposal, there is no need the same ring buffer, because a batchId (aka batchStartBlockId ) is already kind of 'ring'.

See comment above here.

UPDATE: Agreed in Discord we continue using Daniel's consecutive batch proposal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we cannot use ring buffer if we are allowing auctioning for future blocks independently of the amount of proposed blocks. So in you example:
Block proposal and batch auction can be entirely independent: we can have a million blocks proposed and we are still auctioning the first 100K blocks, or we can have more block batch auctioned but a few blocks proposed. Decoupling these two will make things easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So if we have 10.000 block proposed, but Alice wants to bid for the proving rights of the 100.000th block, what kind of ring buffer we could use ? We simply have this below , and that's it.

funciton blockIdToBatch(uint blockId) {
   return (blockId - 1)/batch_size;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

They are not sharing the same ring buffer, so why it is not possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I dont get the idea of ring with batchId s. So if anyone could bid to future proofs then there is no 'limit' of what could be the size of the ring, therefore no need to have a ring IMO.

// allow to fail in case they have a bad onTokenReceived
// so they cant be outbid
}
emit BidDepositRefunded(
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need to emit this event.

revert L1_BID_NOT_ACCEPTABLE();
} else {
// otherwise we have a new high bid, and can refund the previous claimer deposit
try tkoToken.transfer(currentBid.bidder, currentBid.deposit) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

We should use mint/burn here...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Curious on why ? The current and prev. tokenomics just kept track this reward balance, i think it is simple than mint/burn.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fine for bids, but for reward and penalty, we should use mint/burn.

Copy link
Contributor Author

@adaki2004 adaki2004 May 31, 2023

Choose a reason for hiding this comment

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

After playing around with it, i prefer:

  • Having a "deposit pool" (same as with current system) and keep track of viastate.taikoTokenBalances[msg.sender]
  • Everything goes through it (proposing fee deducted from it, deposit placed from it, and refunded, etc.) except the burn when penalty. It is much more gas efficient and people (provers) could claim / withdraw as they wish, just like with the current system.
    Would not throw away a good, working system.


// if a current bid exists, and the delay after the current bid exists
// has passed, bidding is over.
if (!isBiddingOpenForBlock(config, currentBid)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

isBatchAuctionable(uint64 batchId)?


// isBidAcceptable determines is checking if the bid is acceptable based on the defined
// criteria
function isBidAcceptable(
Copy link
Contributor

Choose a reason for hiding this comment

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

We also need to expose a public function

isBidAcceptable(Bid) {}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, i'd expose the external interfaces via TaikoL1 because these functions need state/config.

@dantaik
Copy link
Contributor

dantaik commented May 30, 2023

@adaki2004 Do you think we can get this done by the end of this week?

@adaki2004
Copy link
Contributor Author

@adaki2004 Do you think we can get this done by the end of this week?

I'm almost sure with testing + simulation it is a no. Excluding test + simulaiton, yes.

@adaki2004
Copy link
Contributor Author

@cyberhorsey @davidtaikocha
Wanted to give a TLDR version, how the auction works - so might help with client impl.

Notes:

  • SystemProver, OracleProver + proof time window stays the same as current version. SystemProver proofs can be verified, OracleProver serves as a failsafe but it has to be 'confirmed' with a normal proof then can be verified.
  • Auction work in batches (current default config set to 100 blocks). BatchId starts from 1 and represents 1..100th blocks, 2: 101..200th, etc.
  • Off-chain sw can map a blockId to a batchId by calling L1.batchForBlock(blockId)
  • Deposits, block fee (proposer) and block reward (prover) is distributed with the taikoTokenBalances - same as now. So participants have to withdraw TKO into their contract balance
  • A batch is auctionable if:
  1. batchId not 0
  2. batchId cannot be smaller then the last verified block's batch (obviously, since it is auctioned already!)
currentVerifiedBatchId =
            batchForBlock(config, state.lastVerifiedBlockId + 1);
batchId < currentVerifiedBatchId
  1. if previous auction has already stared
batchId > state.numAuctions + 1
  1. If we dont have to keep all the auctions info in order to prove/verify blocks
    ```
    batchId >= currentVerifiedBatchId + config.auctionRingBufferSize
    || batchId
    >= currentProposedBatchId + config.auctonMaxAheadOfProposals

5. If auction started and auction still going (so `block.timestamp <= auction.startedAt + config.auctionWindow`)

For this, you can check `_isBatchAuctionable()` function. 

Okay, suppose auction is ongoing, there is a bid formal validity check, saying bid in invalid in case:
       ```
 if (
            bid.prover != address(0) // auto-fill
                || bid.blockMaxGasLimit != 0 // auto-fill
                || bid.feePerGas == 0 || bid.proofWindow == 0 || bid.deposit == 0
                || bid.proofWindow
                    > state.avgProofWindow * config.auctionProofWindowMultiplier
                || bid.deposit
                    < state.feePerGas
                        * (config.blockFeeBaseGas + config.blockMaxGasLimit)
                        * config.auctionDepositMultipler
        ) {
            revert L1_BID_INVALID();
        }

If a bid is OK formally, there is a check if better 'in-value', checked by the isBidBetter() function.

        return _adjustBidPropertyScore(ONE * b.feePerGas / a.feePerGas, 1)
            * _adjustBidPropertyScore(ONE * a.deposit / b.deposit, 2)
            * _adjustBidPropertyScore(ONE * b.proofWindow / a.proofWindow, 4)
            >= ONE * ONE * ONE * 110 / 100;

There are public APIs which can be queried:

function bidForBatch(uint64 batchId, TaikoData.Bid memory bid)
function batchForBlock(uint256 blockId) public pure returns (uint256)
function isBidBetter(TaikoData.Bid memory newBid, TaikoData.Bid memory oldBid)
function getAuctions(uint256 startBatchId,uint256 count)public view returns (uint256 currentTime, TaikoData.Auction[] memory auctions)
function isBatchAuctionable(uint256 batchId)public view returns (bool)
function isBlockProvableBy( uint256 blockId,address prover) public view returns (bool, TaikoData.Auction memory)

Basic workflow:

  1. Check / query in client which batch (e.g.: batchId: X) is auctionable. (isBatchAuctionable())
  2. If yes, can query auction getAuctions(X, 1)
  3. Can see the current auction with bid, craft a bid and check if isBidBetter() or can even do those calcs off-chain so can be sure if bid is better - so it is an optional step
  4. Place (hopefully winning) bid with bidForBatch()
  5. Wait until auction ends (startedAt+auctionWindow) and query isBlockProvableBy() when proof is ready. It will return TRUE in case the prover is the winning prover within proof time window OR everyone outside the time window
  6. Submit proof.

@cyberhorsey
Copy link
Contributor

Would another public helper, to extend isBidBetter, be helpful, like minimumNextBidForBatch?

@adaki2004
Copy link
Contributor Author

Would another public helper, to extend isBidBetter, be helpful, like minimumNextBidForBatch?

I might just use this calc comparison off-chain, because it is a scoring system, so depending on multiple factors.
What i could imagine to have is like a : minimumNextBidForBatchDepositOnly, minimumNextBidForBatchProofWindowOnly, but this is not "representative" in a way of: we want to provers act rational and not just 'undercut' based on the params, they need to determine what is profitable/worth for them, so just construct a bid and see if it is better.

return _adjustBidPropertyScore(ONE * b.feePerGas / a.feePerGas, 1)
            * _adjustBidPropertyScore(ONE * a.deposit / b.deposit, 2)
            * _adjustBidPropertyScore(ONE * b.proofWindow / a.proofWindow, 4)
            >= ONE * ONE * ONE * 110 / 100;
    }

@dantaik dantaik merged commit 8e6c281 into alpha-4-base-initial-impl Jun 11, 2023
5 of 13 checks passed
@dantaik dantaik deleted the batch_auction_dani branch June 11, 2023 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants