Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
120 changes: 113 additions & 7 deletions src/chain/ResourceConstraintManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
pragma solidity ^0.8.0;

import "../precompiles/ArbOwner.sol";
import "../precompiles/ArbGasInfo.sol";
import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";

contract ResourceConstraintManager is AccessControlEnumerable {
ArbOwner internal constant ARB_OWNER = ArbOwner(address(0x70));
ArbGasInfo internal constant ARB_GAS_INFO = ArbGasInfo(address(0x6c));
Comment thread
gzeoneth marked this conversation as resolved.

// Constraint parameters boundaries
uint256 public constant MAX_CONSTRAINTS = 10;
Comment thread
TucksonDev marked this conversation as resolved.
Outdated
uint64 public constant MIN_GAS_TARGET_PER_SEC = 7_000_000;
uint64 public constant MAX_GAS_TARGET_PER_SEC = 100_000_000;
uint32 public constant MIN_ADJUSTMENT_WINDOW_SECS = 5;
uint32 public constant MAX_ADJUSTMENT_WINDOW_SECS = 86400;
uint64 public constant MAX_PRICING_EXPONENT = 8000; // scaled by 1000 to allow for fractional exponents

bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
uint256 public expiryTimestamp;
Expand All @@ -23,6 +29,7 @@
uint64 gasTargetPerSec, uint64 adjustmentWindowSecs, uint64 startingBacklogValue
);
error PricingExponentTooHigh(uint64 pricingExponent);
error DuplicateResourceKind(uint8 resourceKind);
error NotExpired();

constructor(address admin, address manager, uint256 _expiryTimestamp) {
Expand Down Expand Up @@ -50,18 +57,23 @@
) external onlyRole(MANAGER_ROLE) {
// If zero constraints are provided, the chain uses the single-constraint pricing model
uint256 nConstraints = constraints.length;
if (nConstraints > 10) {
if (nConstraints > MAX_CONSTRAINTS) {
revert TooManyConstraints();
}
uint64 pricingExponent = 0;
for (uint256 i = 0; i < nConstraints; ++i) {
uint64 gasTargetPerSec = constraints[i][0];
uint64 adjustmentWindowSecs = constraints[i][1];
uint64 startingBacklogValue = constraints[i][2];
if (gasTargetPerSec < 7_000_000 || gasTargetPerSec > 100_000_000) {
if (
gasTargetPerSec < MIN_GAS_TARGET_PER_SEC || gasTargetPerSec > MAX_GAS_TARGET_PER_SEC
) {
revert InvalidTarget(gasTargetPerSec, adjustmentWindowSecs, startingBacklogValue);
}
if (adjustmentWindowSecs < 5 || adjustmentWindowSecs > 86400) {
if (
adjustmentWindowSecs < MIN_ADJUSTMENT_WINDOW_SECS
|| adjustmentWindowSecs > MAX_ADJUSTMENT_WINDOW_SECS
) {
revert InvalidPeriod(gasTargetPerSec, adjustmentWindowSecs, startingBacklogValue);
}
// we scale by 1000 to improve precision in calculating the exponent
Expand All @@ -74,15 +86,109 @@
(startingBacklogValue * 1000) / (gasTargetPerSec * adjustmentWindowSecs);
}

// this calculated pricing exponent will by used by nitro to calculate the gas price
// this calculated pricing exponent will be used by nitro to calculate the gas price
// we check that the pricing exponent is below some reasonable number to avoid setting the gas price astronomically high
// as long as the gas price is not so high that no-one at all can send a transaction the chain will be able to function
// eg. these constraints can be changed again, or the sec council can send admin transactions
// with min base fee of 0.02, exponent of 8 (scaled by 1000) corresponds to a gas price of ~60 Gwei
if (pricingExponent > 8000) {
if (pricingExponent > MAX_PRICING_EXPONENT) {
revert PricingExponentTooHigh(pricingExponent);
}

ARB_OWNER.setGasPricingConstraints(constraints);
}

/// @notice Sets the list of multi-gas pricing constraints for the multi-dimensional multi-constraint pricing model.
/// See ArbOwner.setMultiGasPricingConstraints interface for more information.
/// @param constraints Array of ResourceConstraint structs, each containing:
/// - resources: list of (ResourceKind, weight) pairs
/// - adjustmentWindowSecs: time window (seconds) over which the price will rise by a factor of e if demand is 2x the target (uint32, seconds)
/// - targetPerSec: target gas usage per second for this constraint (uint64, gas/sec)
/// - backlog: initial backlog value for this constraint (uint64, gas units)
function setMultiGasPricingConstraints(
ArbMultiGasConstraintsTypes.ResourceConstraint[] calldata constraints
) external onlyRole(MANAGER_ROLE) {
// If zero constraints are provided, the chain uses the single-constraint pricing model
// Starting from ArbOS 60, there's no limit to the number of constraints to set
uint256 nConstraints = constraints.length;

// We calculate the implied pricing exponent for each resource kind
uint8 numResourceKinds = uint8(type(ArbMultiGasConstraintsTypes.ResourceKind).max) + 1;
uint64[] memory pricingExponents = new uint64[](numResourceKinds);
for (uint256 i = 0; i < nConstraints; ++i) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe also check weight != 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think it doesn't matter if a resource has a weight of 0, if I understand the nitro code correctly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not necessary, but setting weight to 0 is probably a mistake (can be omitted instead) so good to check here

uint64 targetPerSec = constraints[i].targetPerSec;
uint32 adjustmentWindowSecs = constraints[i].adjustmentWindowSecs;
uint64 startingBacklogValue = constraints[i].backlog;
if (targetPerSec < MIN_GAS_TARGET_PER_SEC || targetPerSec > MAX_GAS_TARGET_PER_SEC) {
revert InvalidTarget(targetPerSec, adjustmentWindowSecs, startingBacklogValue);
}
if (
adjustmentWindowSecs < MIN_ADJUSTMENT_WINDOW_SECS
|| adjustmentWindowSecs > MAX_ADJUSTMENT_WINDOW_SECS
Comment thread
TucksonDev marked this conversation as resolved.
) {
revert InvalidPeriod(targetPerSec, adjustmentWindowSecs, startingBacklogValue);
}
{
// Check for duplicate resource kinds within this constraint
// Using bit comparison for efficient calculation (supports up to 256 kinds)
uint256 seenKinds;
Comment thread Fixed
uint256 nResources = constraints[i].resources.length;
for (uint256 j = 0; j < nResources; ++j) {
uint8 kind = uint8(constraints[i].resources[j].resource);
// Shifting 1 by the resource kind value
// (example: kind = 1, kindBit = ...0010)
// (example: kind = 2, kindBit = ...0100)
uint256 kindBit = 1 << kind;
// Bitwise AND comparison
if ((seenKinds & kindBit) != 0) {
revert DuplicateResourceKind(kind);
}
// Bitwise OR to add kind to seenKinds
seenKinds = seenKinds | kindBit;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we can check final lastResourceKind is a valid ResourceKind too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since the struct property is ResourceKind resource;, it should revert if we ever pass a kind that is out of range there (if I understood your comment correctly)

if (startingBacklogValue > 0) {
// Find the maximum weight among all resources in this constraint
uint64 maxWeight = 0;
uint256 nResources = constraints[i].resources.length;
for (uint256 j = 0; j < nResources; ++j) {
uint64 weight = constraints[i].resources[j].weight;
if (weight > maxWeight) {
maxWeight = weight;
}
}

if (maxWeight > 0) {
// Neither of these values can be zero due to the earlier checks, so this division is safe
uint256 divisor =
uint256(adjustmentWindowSecs) * uint256(targetPerSec) * uint256(maxWeight);

// Calculate per-resource-kind exponent contribution
// we scale by 1000 to improve precision in calculating the exponent
// since this division will round down, it's always possible for the real exponent to be up to
// the number of constraints greater than the value we measure
// Operation is performed in uint256 to avoid overflow, but the result is guaranteed to fit in uint64 due to the earlier check
for (uint256 j = 0; j < nResources; ++j) {
uint8 kind = uint8(constraints[i].resources[j].resource);
uint64 weight = constraints[i].resources[j].weight;
pricingExponents[kind] +=
uint64(uint256(startingBacklogValue) * uint256(weight) * 1000 / divisor);
}
}
}
}

// this calculated pricing exponent will be used by nitro to calculate the gas price
// we check that the pricing exponent is below some reasonable number to avoid setting the gas price astronomically high
// as long as the gas price is not so high that no-one at all can send a transaction the chain will be able to function
// eg. these constraints can be changed again, or the sec council can send admin transactions
// with min base fee of 0.02, exponent of 8 (scaled by 1000) corresponds to a gas price of ~60 Gwei
for (uint8 k = 0; k < numResourceKinds; ++k) {
if (pricingExponents[k] > MAX_PRICING_EXPONENT) {
revert PricingExponentTooHigh(pricingExponents[k]);
}
}

ARB_OWNER.setMultiGasPricingConstraints(constraints);
}
}
2 changes: 1 addition & 1 deletion src/precompiles
Loading
Loading