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

fix(protocol): limit reward per gas in weight calculation #14098

Merged
merged 10 commits into from
Jul 5, 2023
168 changes: 103 additions & 65 deletions packages/protocol/contracts/L1/ProverPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,19 @@ import { Proxied } from "../common/Proxied.sol";

contract ProverPool is EssentialContract, IProverPool {
using LibMath for uint256;
// 8 bytes or 1 uint64

struct Prover {
uint64 stakedAmount;
uint16 rewardPerGas;
uint16 currentCapacity;
uint64 weight;
uint32 rewardPerGas;
uint32 currentCapacity;
}

// Make sure we only use one slot
struct Staker {
uint64 exitRequestedAt;
uint64 exitAmount;
uint16 maxCapacity;
uint8 proverId; // 0 to indicate the staker is not a top prover
uint32 maxCapacity;
uint32 proverId; // 0 to indicate the staker is not a top prover
}

// Given that we only have 32 slots for the top provers, if the protocol
Expand All @@ -37,15 +35,15 @@ contract ProverPool is EssentialContract, IProverPool {
// provide a capacity of at least 3600/32=112.
uint32 public constant MAX_CAPACITY_LOWER_BOUND = 128;
uint64 public constant EXIT_PERIOD = 1 weeks;
uint32 public constant SLASH_POINTS = 25; // basis points or 0.25%
uint64 public constant SLASH_POINTS = 25; // basis points or 0.25%
uint64 public constant MIN_STAKE_PER_CAPACITY = 10_000;
uint64 public constant MIN_SLASH_AMOUNT = 1e8; // 1 token
uint256 public constant MAX_NUM_PROVERS = 32;
uint256 public constant MIN_CHANGE_DELAY = 1 hours;

// Reserve more slots than necessary
Prover[1024] public provers; // provers[0] is never used
mapping(uint256 id => address prover) public idToProver;
mapping(uint256 id => address prover) public proverIdToAddress;
// Save the weights only when: stake / unstaked / slashed
mapping(address staker => Staker) public stakers;

Expand All @@ -57,8 +55,8 @@ contract ProverPool is EssentialContract, IProverPool {
event Staked(
address indexed addr,
uint64 amount,
uint16 rewardPerGas,
uint16 currentCapacity
uint32 rewardPerGas,
uint32 currentCapacity
);

error CHANGE_TOO_FREQUENT();
Expand All @@ -80,43 +78,28 @@ contract ProverPool is EssentialContract, IProverPool {

function assignProver(
uint64 blockId,
uint32 /*feePerGas*/
uint32 feePerGas
)
external
onlyFromProtocol
returns (address prover, uint32 rewardPerGas)
{
unchecked {
uint256[MAX_NUM_PROVERS] memory weights;
uint256 totalWeight;
Prover memory _prover;

for (uint8 i; i < MAX_NUM_PROVERS; ++i) {
_prover = provers[i + 1];
if (_prover.currentCapacity != 0) {
weights[i] = _prover.weight;
totalWeight += _prover.weight;
}
}
(
uint256[MAX_NUM_PROVERS] memory weights,
uint32[MAX_NUM_PROVERS] memory erpg
) = getProverWeights(feePerGas);

if (totalWeight == 0) {
return (address(0), 0);
}

// Pick a prover using a pseudo random number
bytes32 rand =
keccak256(abi.encode(blockhash(block.number - 1), blockId));
uint256 r = uint256(rand) % totalWeight + 1;
uint256 z;
uint8 id;
uint256 id = _selectProver(rand, weights);

while (z < r && id < MAX_NUM_PROVERS) {
z += weights[id++];
if (id == 0) {
return (address(0), 0);
} else {
provers[id].currentCapacity -= 1;
return (proverIdToAddress[id], erpg[id - 1]);
}
provers[id].currentCapacity -= 1;

// Note that prover ID is 1 bigger than its index
return (idToProver[id], _prover.rewardPerGas);
}
}

Expand All @@ -135,17 +118,16 @@ contract ProverPool is EssentialContract, IProverPool {
// Slashes a prover
function slashProver(address addr) external onlyFromProtocol {
(Staker memory staker, Prover memory prover) = getStaker(addr);
unchecked {
// if the exit is mature, we do not count it in the total slash-able
// amount
uint256 slashableAmount = staker.exitRequestedAt > 0
&& block.timestamp <= staker.exitRequestedAt + EXIT_PERIOD
? prover.stakedAmount + staker.exitAmount
: prover.stakedAmount;

// if the exit is mature, we do not count it in the total slash-able
// amount
uint256 slashableAmount = staker.exitRequestedAt > 0
&& block.timestamp <= staker.exitRequestedAt + EXIT_PERIOD
? prover.stakedAmount + staker.exitAmount
: prover.stakedAmount;

if (slashableAmount == 0) return;
if (slashableAmount == 0) return;

unchecked {
uint64 amountToSlash = uint64(
(slashableAmount * SLASH_POINTS / 10_000).max(MIN_SLASH_AMOUNT)
.min(slashableAmount)
Expand All @@ -160,11 +142,8 @@ contract ProverPool is EssentialContract, IProverPool {

if (prover.stakedAmount > _additional) {
provers[staker.proverId].stakedAmount -= _additional;
provers[staker.proverId].weight =
_calcWeight(prover.stakedAmount, prover.rewardPerGas);
} else {
provers[staker.proverId].stakedAmount = 0;
provers[staker.proverId].weight = 0;
}
}
emit Slashed(addr, amountToSlash);
Expand All @@ -173,8 +152,8 @@ contract ProverPool is EssentialContract, IProverPool {

function stake(
uint64 amount,
uint16 rewardPerGas,
uint16 maxCapacity
uint32 rewardPerGas,
uint32 maxCapacity
)
external
nonReentrant
Expand Down Expand Up @@ -233,15 +212,42 @@ contract ProverPool is EssentialContract, IProverPool {
_stakers = new address[](MAX_NUM_PROVERS);
for (uint256 i; i < MAX_NUM_PROVERS; ++i) {
_provers[i] = provers[i + 1];
_stakers[i] = idToProver[i + 1];
_stakers[i] = proverIdToAddress[i + 1];
}
}

function getProverWeights(uint32 feePerGas)
public
view
returns (
uint256[MAX_NUM_PROVERS] memory weights,
uint32[MAX_NUM_PROVERS] memory erpg
)
{
Prover memory _prover;
unchecked {
for (uint32 i; i < MAX_NUM_PROVERS; ++i) {
_prover = provers[i + 1];
if (_prover.currentCapacity != 0) {
// Keep the effective rewardPerGas in [75-125%] of feePerGas
if (_prover.rewardPerGas > feePerGas * 125 / 100) {
erpg[i] = feePerGas * 125 / 100;
} else if (_prover.rewardPerGas < feePerGas * 75 / 100) {
erpg[i] = feePerGas * 75 / 100;
} else {
erpg[i] = _prover.rewardPerGas;
}
weights[i] = _calcWeight(_prover.stakedAmount, erpg[i]);
}
}
}
}

function _stake(
address addr,
uint64 amount,
uint16 rewardPerGas,
uint16 maxCapacity
uint32 rewardPerGas,
uint32 maxCapacity
)
private
{
Expand Down Expand Up @@ -271,8 +277,8 @@ contract ProverPool is EssentialContract, IProverPool {
staker.maxCapacity = maxCapacity;

// Find the prover id
uint8 proverId = 1;
for (uint8 i = 2; i <= MAX_NUM_PROVERS;) {
uint32 proverId = 1;
for (uint32 i = 2; i <= MAX_NUM_PROVERS;) {
if (provers[proverId].stakedAmount > provers[i].stakedAmount) {
proverId = i;
}
Expand All @@ -286,20 +292,19 @@ contract ProverPool is EssentialContract, IProverPool {
}

// Force the replaced prover to exit
address replaced = idToProver[proverId];
address replaced = proverIdToAddress[proverId];
if (replaced != address(0)) {
_withdraw(replaced);
_exit(replaced, false);
}
idToProver[proverId] = addr;
proverIdToAddress[proverId] = addr;
staker.proverId = proverId;

// Insert the prover in the top prover list
provers[proverId] = Prover({
stakedAmount: amount,
rewardPerGas: rewardPerGas,
currentCapacity: maxCapacity,
weight: _calcWeight(amount, rewardPerGas)
currentCapacity: maxCapacity
});

emit Staked(addr, amount, rewardPerGas, maxCapacity);
Expand All @@ -326,9 +331,9 @@ contract ProverPool is EssentialContract, IProverPool {

// Delete the prover but make it non-zero for cheaper rewrites
// by keep rewardPerGas = 1
provers[staker.proverId] = Prover(0, 1, 0, 0);
provers[staker.proverId] = Prover(0, 1, 0);

delete idToProver[staker.proverId];
delete proverIdToAddress[staker.proverId];

emit Exited(addr, staker.exitAmount);
}
Expand All @@ -355,15 +360,48 @@ contract ProverPool is EssentialContract, IProverPool {
// Calculates the user weight's when it stakes/unstakes/slashed
function _calcWeight(
uint64 stakedAmount,
uint16 rewardPerGas
uint32 rewardPerGas
)
private
pure
returns (uint64 weight)
{
unchecked {
if (rewardPerGas == 0) {
return 0;
}

weight = stakedAmount / rewardPerGas / rewardPerGas;
if (weight == 0) {
weight = 1;
}
}
}

function _selectProver(
bytes32 rand,
uint256[MAX_NUM_PROVERS] memory weights
)
private
pure
returns (uint64)
returns (uint256 proverId)
{
assert(rewardPerGas > 0);
unchecked {
return stakedAmount / rewardPerGas / rewardPerGas;
uint256 totalWeight;
for (uint256 i; i < MAX_NUM_PROVERS; ++i) {
totalWeight += weights[i];
}
if (totalWeight == 0) return 0;

uint256 r = uint256(rand) % totalWeight;
uint256 accumulatedWeight;
for (uint256 i; i < MAX_NUM_PROVERS; ++i) {
accumulatedWeight += weights[i];
if (r < accumulatedWeight) {
return i + 1;
}
}
assert(false); // shall not reach here
}
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/protocol/contracts/L1/TaikoConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ library TaikoConfig {
ethDepositGas: 21_000,
ethDepositMaxFee: 1 ether / 10,
// Group 5: tokenomics
rewardPerGasRange: 2000, // 20%
rewardOpenMultipler: 200, // percentage
rewardOpenMaxCount: 2000
});
Expand Down
1 change: 0 additions & 1 deletion packages/protocol/contracts/L1/TaikoData.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ library TaikoData {
uint256 ethDepositGas;
uint256 ethDepositMaxFee;
// Group 5: tokenomics
uint32 rewardPerGasRange;
uint8 rewardOpenMultipler;
uint256 rewardOpenMaxCount;
}
Expand Down
13 changes: 2 additions & 11 deletions packages/protocol/contracts/L1/libs/LibProposing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,9 @@ library LibProposing {
++state.numOpenBlocks;
} else {
blk.assignedProver = assignedProver;

// Cap the reward to a range of [80%, 120%] * blk.feePerGas, if
// rewardPerGasRange is set to 20% (2000 bp)
uint32 diff = blk.feePerGas * config.rewardPerGasRange / 10_000;
blk.rewardPerGas = uint32(
uint256(rewardPerGas).min(state.feePerGas + diff).max(
state.feePerGas - diff
)
);

blk.rewardPerGas = rewardPerGas;
blk.proofWindow = uint16(
uint256(state.avgProofDelay * 3).min(config.proofMaxWindow).max(
uint256(state.avgProofDelay * 2).min(config.proofMaxWindow).max(
config.proofMinWindow
)
);
Expand Down
2 changes: 0 additions & 2 deletions packages/protocol/contracts/L1/libs/LibVerifying.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ library LibVerifying {
|| config.ethDepositMaxFee >= type(uint96).max
|| config.ethDepositMaxFee
>= type(uint96).max / config.ethDepositMaxCountPerBlock
|| config.rewardPerGasRange == 0
|| config.rewardPerGasRange >= 10_000
|| config.rewardOpenMultipler < 100
) revert L1_INVALID_CONFIG();

Expand Down
Loading