Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
121 changes: 114 additions & 7 deletions src/chain/ResourceConstraintManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
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_SINGLE_GAS_CONSTRAINTS = 10;
uint256 public constant MAX_MULTI_GAS_CONSTRAINTS = 100;
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 @@ -22,6 +29,7 @@ contract ResourceConstraintManager is AccessControlEnumerable {
error InvalidTarget(
uint64 gasTargetPerSec, uint64 adjustmentWindowSecs, uint64 startingBacklogValue
);
error InvalidResources(uint8 resourceKind);
error PricingExponentTooHigh(uint64 pricingExponent);
error NotExpired();

Expand Down Expand Up @@ -50,18 +58,23 @@ contract ResourceConstraintManager is AccessControlEnumerable {
) 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_SINGLE_GAS_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 +87,109 @@ contract ResourceConstraintManager is AccessControlEnumerable {
(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. Must be sorted by ResourceKind and contain no duplicate ResourceKinds. (see ArbMultiGasConstraintsTypes for struct definitions)
/// - 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
// Each constraint adds a small amount of overhead to the gas cost of each transaction and block, so we limit the number of constraints that can be set
uint256 nConstraints = constraints.length;
if (nConstraints > MAX_MULTI_GAS_CONSTRAINTS) {
revert TooManyConstraints();
}

// 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 unsorted or duplicate resource kinds within this constraint
// The check is performed here instead of in the loop below (for calculating pricing exponents)
// to prevent bypassing the check when setting a starting backlog value of zero
// (in that case, nitro would only store the last of the duplicated entries)
uint8 lastResourceKind = 0;
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.

let's set this to 1 to disallow ResourceKindUnknown(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've just removed the (j>0) guard so it also checks during the first iteration, which will revert if kind = 0

uint256 nResources = constraints[i].resources.length;
for (uint256 j = 0; j < nResources; ++j) {
uint8 kind = uint8(constraints[i].resources[j].resource);
// check that resource kinds are sorted and contain no duplicates
if (j > 0 && kind <= lastResourceKind) {
revert InvalidResources(kind);
}
lastResourceKind = kind;
}
}
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);
}
}
Loading
Loading