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): use ring buffer for ETH deposit and optimize storage #13868

Merged
merged 23 commits into from
Jun 16, 2023
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
17 changes: 8 additions & 9 deletions packages/eventindexer/contracts/taikol1/TaikoL1.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions packages/protocol/contracts/L1/TaikoConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ library TaikoConfig {
// Set it to 6M, since its the upper limit of the Alpha-2
// testnet's circuits.
blockMaxGasLimit: 6_000_000,
// Set it to 79 (+1 TaikoL2.anchor transaction = 80),
// and 80 is the upper limit of the Alpha-2 testnet's circuits.
maxTransactionsPerBlock: 79,
minEthDepositsPerBlock: 8,
maxEthDepositsPerBlock: 32,
maxEthDepositAmount: 10_000 ether,
minEthDepositAmount: 1 ether,
// Set it to 120KB, since 128KB is the upper size limit
// of a geth transaction, so using 120KB for the proposed
// transactions list calldata, 8K for the remaining tx fields.
Expand All @@ -38,8 +31,16 @@ library TaikoConfig {
// If block number is N, then only when N % 10 == 0, the real ZKP
// is needed. For mainnet, this must be 0 or 1.
realProofSkipSize: 10,
// Set it to 79 (+1 TaikoL2.anchor transaction = 80),
// and 80 is the upper limit of the Alpha-2 testnet's circuits.
maxTransactionsPerBlock: 79,
ethDepositMinCountPerBlock: 8,
ethDepositMaxCountPerBlock: 32,
ethDepositMaxAmount: 10_000 ether,
ethDepositMinAmount: 1 ether,
ethDepositGas: 21_000,
ethDepositMaxFee: 1 ether / 10,
ethDepositRingBufferSize: 1024,
txListCacheExpiry: 0,
relaySignalRoot: false
});
Expand Down
15 changes: 8 additions & 7 deletions packages/protocol/contracts/L1/TaikoData.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ library TaikoData {
uint256 realProofSkipSize;
uint256 ethDepositGas;
uint256 ethDepositMaxFee;
uint64 minEthDepositsPerBlock;
uint64 maxEthDepositsPerBlock;
uint96 maxEthDepositAmount;
uint96 minEthDepositAmount;
uint256 ethDepositRingBufferSize;
uint64 ethDepositMinCountPerBlock;
uint64 ethDepositMaxCountPerBlock;
uint96 ethDepositMaxAmount;
uint96 ethDepositMinAmount;
bool relaySignalRoot;
}

Expand Down Expand Up @@ -108,7 +109,6 @@ library TaikoData {
uint24 size;
}

// 2 slot
struct EthDeposit {
address recipient;
uint96 amount;
Expand All @@ -127,7 +127,8 @@ library TaikoData {
) forkChoiceIds;
mapping(address account => uint256 balance) taikoTokenBalances;
mapping(bytes32 txListHash => TxListInfo) txListInfo;
EthDeposit[] ethDeposits;
mapping(uint256 depositId_mod_ethDepositRingBufferSize => uint256)
ethDeposits;
// Never or rarely changed
// Slot 7: never or rarely changed
uint64 genesisHeight;
Expand All @@ -137,7 +138,7 @@ library TaikoData {
uint64 __reserved72;
// Slot 8
uint64 __reserved80;
uint64 __reserved81;
uint64 numEthDeposits;
uint64 numBlocks;
uint64 nextEthDepositToProcess;
// Slot 9
Expand Down
21 changes: 16 additions & 5 deletions packages/protocol/contracts/L1/TaikoL1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ contract TaikoL1 is
uint256[100] private __gap;

receive() external payable {
depositEtherToL2();
depositEtherToL2(address(0));
}

/**
Expand Down Expand Up @@ -143,10 +143,21 @@ contract TaikoL1 is
});
}

function depositEtherToL2() public payable {
LibEthDepositing.depositEtherToL2(
state, getConfig(), AddressResolver(this)
);
function depositEtherToL2(address recipient) public payable {
LibEthDepositing.depositEtherToL2({
state: state,
config: getConfig(),
resolver: AddressResolver(this),
recipient: recipient
});
}

function canDepositEthToL2(uint256 amount) public view returns (bool) {
return LibEthDepositing.canDepositEthToL2({
state: state,
config: getConfig(),
amount: amount
});
}

function getTaikoTokenBalance(address addr) public view returns (uint256) {
Expand Down
174 changes: 107 additions & 67 deletions packages/protocol/contracts/L1/libs/LibEthDepositing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,102 +25,130 @@ library LibEthDepositing {
function depositEtherToL2(
TaikoData.State storage state,
TaikoData.Config memory config,
AddressResolver resolver
AddressResolver resolver,
address recipient
)
internal
{
if (
msg.value < config.minEthDepositAmount
|| msg.value > config.maxEthDepositAmount
) {
if (!canDepositEthToL2(state, config, msg.value)) {
revert L1_INVALID_ETH_DEPOSIT();
}

TaikoData.EthDeposit memory deposit = TaikoData.EthDeposit({
recipient: msg.sender,
amount: uint96(msg.value),
id: uint64(state.ethDeposits.length)
});

address to = resolver.resolve("ether_vault", true);
if (to == address(0)) {
to = resolver.resolve("bridge", false);
}
to.sendEther(msg.value);

state.ethDeposits.push(deposit);
emit EthDeposited(deposit);
// Put the deposit and the end of the queue.
address _recipient = recipient == address(0) ? msg.sender : recipient;
uint256 slot = state.numEthDeposits % config.ethDepositRingBufferSize;
state.ethDeposits[slot] = _encodeEthDeposit(_recipient, msg.value);

emit EthDeposited(
TaikoData.EthDeposit({
recipient: _recipient,
amount: uint96(msg.value),
id: state.numEthDeposits
})
);

unchecked {
state.numEthDeposits++;
}
}

// When ethDepositMaxCountPerBlock is 32, the average gas cost per
// EthDeposit is about 2700 gas. We use 21000 so the proposer
// may earn a small profit if there are 32 deposits included
// in the block; if there are less EthDeposit to process, the
// proposer may suffer a loss so the proposer should simply wait
// for more EthDeposit be become available.
function processDeposits(
TaikoData.State storage state,
TaikoData.Config memory config,
address beneficiary
address feeRecipient
)
internal
returns (TaikoData.EthDeposit[] memory depositsProcessed)
returns (TaikoData.EthDeposit[] memory deposits)
{
// Allocate one extra slot for collecting fees on L2
depositsProcessed = new TaikoData.EthDeposit[](
config.maxEthDepositsPerBlock
);

uint256 j; // number of deposits to process on L2
if (
state.ethDeposits.length
>= state.nextEthDepositToProcess + config.minEthDepositsPerBlock
) {
unchecked {
// When maxEthDepositsPerBlock is 32, the average gas cost per
// EthDeposit is about 2700 gas. We use 21000 so the proposer
// may earn a small profit if there are 32 deposits included
// in the block; if there are less EthDeposit to process, the
// proposer may suffer a loss so the proposer should simply wait
// for more EthDeposit be become available.
uint96 feePerDeposit = uint96(
config.ethDepositMaxFee.min(
block.basefee * config.ethDepositGas
)
);
uint96 totalFee;
uint64 i = state.nextEthDepositToProcess;
while (
i < state.ethDeposits.length
&& state.nextEthDepositToProcess
+ config.maxEthDepositsPerBlock > i
) {
depositsProcessed[j] = state.ethDeposits[i];

if (depositsProcessed[j].amount > feePerDeposit) {
totalFee += feePerDeposit;
depositsProcessed[j].amount -= feePerDeposit;
} else {
totalFee += depositsProcessed[j].amount;
depositsProcessed[j].amount = 0;
}

uint256 numPending =
state.numEthDeposits - state.nextEthDepositToProcess;
if (numPending < config.ethDepositMinCountPerBlock) {
deposits = new TaikoData.EthDeposit[](0);
} else {
deposits = new TaikoData.EthDeposit[](
numPending.min(config.ethDepositMaxCountPerBlock)
);

uint96 fee = uint96(
config.ethDepositMaxFee.min(
block.basefee * config.ethDepositGas
)
);
uint64 j = state.nextEthDepositToProcess;
uint96 totalFee;
for (uint256 i; i < deposits.length;) {
uint256 data =
state.ethDeposits[j % config.ethDepositRingBufferSize];

deposits[i] = TaikoData.EthDeposit({
recipient: address(uint160(data >> 96)),
amount: uint96(data), // works
id: j
});

uint96 _fee =
deposits[i].amount > fee ? fee : deposits[i].amount;

unchecked {
deposits[i].amount -= _fee;
totalFee += _fee;
++i;
++j;
}
}
state.nextEthDepositToProcess = j;

// Fee collecting deposit
if (totalFee > 0) {
TaikoData.EthDeposit memory deposit = TaikoData.EthDeposit({
recipient: beneficiary,
amount: totalFee,
id: uint64(state.ethDeposits.length)
});
// This is the fee deposit
state.ethDeposits[state.numEthDeposits
% config.ethDepositRingBufferSize] =
_encodeEthDeposit(feeRecipient, totalFee);

state.ethDeposits.push(deposit);
}
// Advance cursor
state.nextEthDepositToProcess = i;
unchecked {
state.numEthDeposits++;
}
}
}

assembly {
mstore(depositsProcessed, j)
function canDepositEthToL2(
TaikoData.State storage state,
TaikoData.Config memory config,
uint256 amount
)
internal
view
returns (bool)
{
if (
amount < config.ethDepositMinAmount
|| amount > config.ethDepositMaxAmount
) {
return false;
}

unchecked {
uint256 numPending =
state.numEthDeposits - state.nextEthDepositToProcess;

// We need to make sure we always reverve one slot for the fee
// deposit
if (numPending >= config.ethDepositRingBufferSize - 1) {
return false;
}
}

return true;
}

function hashEthDeposits(TaikoData.EthDeposit[] memory deposits)
Expand All @@ -130,4 +158,16 @@ library LibEthDepositing {
{
return keccak256(abi.encode(deposits));
}

function _encodeEthDeposit(
address addr,
uint256 amount
)
private
pure
returns (uint256)
{
if (amount >= type(uint96).max) revert L1_INVALID_ETH_DEPOSIT();
return uint256(uint160(addr)) << 96 | amount;
}
}
2 changes: 1 addition & 1 deletion packages/protocol/contracts/L1/libs/LibUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ library LibUtils {
numBlocks: state.numBlocks,
lastVerifiedBlockId: state.lastVerifiedBlockId,
nextEthDepositToProcess: state.nextEthDepositToProcess,
numEthDeposits: uint64(state.ethDeposits.length)
numEthDeposits: state.numEthDeposits - state.nextEthDepositToProcess
});
}

Expand Down
16 changes: 10 additions & 6 deletions packages/protocol/contracts/L1/libs/LibVerifying.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,19 @@ library LibVerifying {
|| config.blockMaxGasLimit == 0
|| config.maxTransactionsPerBlock == 0
|| config.maxBytesPerTxList == 0
|| config.txListCacheExpiry > 30 * 24 hours
// EIP-4844 blob size up to 128K
|| config.maxBytesPerTxList > 128 * 1024
|| config.maxEthDepositsPerBlock == 0
|| config.maxEthDepositsPerBlock < config.minEthDepositsPerBlock
// EIP-4844 blob deleted after 30 days
|| config.txListCacheExpiry > 30 * 24 hours
|| config.ethDepositGas == 0 //
|| config.ethDepositMinCountPerBlock == 0
|| config.ethDepositMaxCountPerBlock
< config.ethDepositMinCountPerBlock || config.ethDepositGas == 0 //
|| config.ethDepositMinAmount == 0
|| config.ethDepositMaxAmount <= config.ethDepositMinAmount
|| config.ethDepositMaxAmount >= type(uint96).max
|| config.ethDepositMaxFee == 0
|| config.ethDepositMaxFee >= type(uint96).max
|| config.ethDepositMaxFee
>= type(uint96).max / config.ethDepositMaxCountPerBlock
|| config.ethDepositRingBufferSize <= 1
) revert L1_INVALID_CONFIG();

uint64 timeNow = uint64(block.timestamp);
Expand Down
Loading