-
Notifications
You must be signed in to change notification settings - Fork 3
Initial work for workflow registry v2: limits #40
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
// SPDX-License-Identifier: BUSL 1.1 | ||
pragma solidity 0.8.26; | ||
|
||
import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; | ||
|
||
import {Ownable2StepMsgSender} from "../../shared/access/Ownable2StepMsgSender.sol"; | ||
|
||
contract WorkflowRegistry is Ownable2StepMsgSender, ITypeAndVersion { | ||
string public constant override typeAndVersion = "WorkflowRegistry 2.0.0-dev"; | ||
|
||
enum WorkflowStatus { | ||
ACTIVE, | ||
PAUSED | ||
} | ||
|
||
struct WorkflowMetadata { | ||
bytes32 workflowID; // Unique identifier from hash of owner address, WASM binary content, config content and secrets URL. | ||
bytes32 donLabel; // Label for the DON that is used when distributing the workflow across DONs. | ||
address owner; // ─────────╮ Workflow owner. | ||
uint64 created_at; // │ block.timestamp when the workflow was first registered. | ||
WorkflowStatus status; // ─╯ Current status of the workflow (active, paused). | ||
string workflowName; // Human readable string capped at 64 characters length. | ||
string binaryURL; // URL to the WASM binary. | ||
string configURL; // URL to the config. | ||
string secretsURL; // URL to the encrypted secrets. Workflow DON applies a default refresh period (e.g. daily). | ||
} | ||
|
||
constructor() { | ||
// Intialize with default limits for Config. | ||
s_cfg.defaultsPacked = _packDefaults(200, 500, 200); | ||
} | ||
|
||
// ================================================================ | ||
// | Limits Config | | ||
// ================================================================ | ||
|
||
/// @dev Instead of a big struct with mappings, we store | ||
/// defaults in a single 32-byte slot, and use nested mappings | ||
/// for overrides. | ||
struct Config { | ||
// Pack three uint32 defaults into a single 96-bit field: | ||
// Layout in bits: [maxUser(0..31) | maxDON(32..63) | maxUserDON(64..95)] | ||
uint96 defaultsPacked; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why pack it like this instead of specifying three separate uint32 fields that will also fit into a single storage slot (uint32 = 4 bytes, storage slot size uint256 = 32 bytes, total of 12 bytes in the slot, so you still have some space left)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. its one less SSTORE when you write. we need to check it often so it saves a little. Even though storage slot is packed together accessing them as separate fields is still separate... maybe it's not worth it :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. although... if we have them as separate, we might need separate setters too, otherwise, there's not much gained, and I don't know if this is worth having different setters. otherwise, it's also not worth doing |
||
// Struct to distinguish between unset and explicitly set zero values | ||
struct Value { | ||
uint32 value; | ||
bool enabled; | ||
} | ||
// 1) user-specific overrides: address -> limit | ||
mapping(address => Value) userOverride; | ||
// 2) DON-specific overrides: donLabel -> limit | ||
mapping(bytes32 => Value) donOverride; | ||
// 3) user+don override: user => (donLabel => limit) | ||
mapping(address => mapping(bytes32 => Value)) userDONOverride; | ||
} | ||
|
||
// Our single config instance | ||
Config private s_cfg; | ||
|
||
// ───────────────────────────────────────────────────────────────────────── | ||
// Limits Config - External Setters: Owner can set defaults and individual overrides | ||
// ───────────────────────────────────────────────────────────────────────── | ||
|
||
/// @notice Update the three default limits in a single call. | ||
function setDefaults(uint32 maxPerUser, uint32 maxPerDON, uint32 maxPerUserDON) external onlyOwner { | ||
s_cfg.defaultsPacked = _packDefaults(maxPerUser, maxPerDON, maxPerUserDON); | ||
} | ||
|
||
/// @notice Override the maximum # of workflows a specific user can register. | ||
function setUserOverride(address user, uint32 limit, bool enabled) external onlyOwner { | ||
if (enabled) { | ||
s_cfg.userOverride[user] = Config.Value(limit, true); | ||
} else { | ||
delete s_cfg.userOverride[user]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not 100% sure on this, but generally, deleting elements in mappings/arrays is avoided due to gas costs, so resetting values is more desirable. Since you're already using this
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. delete is only costly for large structures, but this is one slot size, so delete versus setting this to false cost the same storage write but has the slight benefit of the cleared slot refund. the only benefit potentially in allowing it is record I think. |
||
} | ||
} | ||
|
||
/// @notice Override the maximum # of workflows allowed for a specific DON label. | ||
/// @dev donLabel is a bytes32 value of the string, which should not exceed 32 ASCII characters. | ||
function setDONOverride(bytes32 donLabel, uint32 limit, bool enabled) external onlyOwner { | ||
if (enabled) { | ||
s_cfg.donOverride[donLabel] = Config.Value(limit, true); | ||
} else { | ||
delete s_cfg.donOverride[donLabel]; | ||
} | ||
} | ||
|
||
/// @notice Override the max # of workflows for a specific (user, DON) pair. | ||
function setUserDONOverride(address user, bytes32 donLabel, uint32 limit, bool enabled) external onlyOwner { | ||
if (enabled) { | ||
s_cfg.userDONOverride[user][donLabel] = Config.Value(limit, true); | ||
} else { | ||
delete s_cfg.userDONOverride[user][donLabel]; | ||
} | ||
} | ||
|
||
// ───────────────────────────────────────────────────────────────────────── | ||
// Limits Config - Public Getters that return the *effective* limit | ||
// (override if set, else default) | ||
// ───────────────────────────────────────────────────────────────────────── | ||
|
||
/// @notice Effective max # of workflows for a particular user. | ||
function getMaxWorkflowsPerUser( | ||
address user | ||
) public view returns (uint32) { | ||
Config.Value memory override = s_cfg.userOverride[user]; | ||
if (override.enabled) { | ||
return override.value; | ||
} | ||
// fallback to the default | ||
(uint32 defUser,,) = _unpackDefaults(s_cfg.defaultsPacked); | ||
return defUser; | ||
} | ||
|
||
/// @notice Effective max # of workflows for a particular DON. | ||
function getMaxWorkflowsPerDON( | ||
bytes32 donLabel | ||
) public view returns (uint32) { | ||
Config.Value memory override = s_cfg.donOverride[donLabel]; | ||
if (override.enabled) { | ||
return override.value; | ||
} | ||
// fallback to the default | ||
(, uint32 defDON,) = _unpackDefaults(s_cfg.defaultsPacked); | ||
return defDON; | ||
} | ||
|
||
/// @notice Effective max # of workflows for a (user, DON) combo. | ||
function getMaxWorkflowsPerUserDON(address user, bytes32 donLabel) public view returns (uint32) { | ||
Config.Value memory override = s_cfg.userDONOverride[user][donLabel]; | ||
if (override.enabled) { | ||
return override.value; | ||
} | ||
// fallback to the default | ||
(,, uint32 defUserDON) = _unpackDefaults(s_cfg.defaultsPacked); | ||
return defUserDON; | ||
} | ||
|
||
/// @notice Returns the three default limits: | ||
/// (maxWorkflowsPerUser, maxWorkflowsPerDon, maxWorkflowsPerUserDon) | ||
function getDefaults() external view returns (uint32 maxPerUser, uint32 maxPerDon, uint32 maxPerUserDon) { | ||
(maxPerUser, maxPerDon, maxPerUserDon) = _unpackDefaults(s_cfg.defaultsPacked); | ||
return (maxPerUser, maxPerDon, maxPerUserDon); | ||
} | ||
|
||
// ───────────────────────────────────────────────────────────────────────── | ||
// Limits Config - Internal Helpers: set/read defaults (packed into one 96-bit variable) | ||
// ───────────────────────────────────────────────────────────────────────── | ||
|
||
/// @dev Store 3 uint32 values in a single 96-bit field. | ||
function _packDefaults( | ||
uint32 maxPerUser, | ||
uint32 maxPerDON, | ||
uint32 maxPerUserDON | ||
) internal pure returns (uint96 packed) { | ||
// lower 32 bits: maxPerUser | ||
// middle 32 bits: maxPerDON | ||
// top 32 bits: maxPerUserDON | ||
packed = uint96(maxPerUser) | (uint96(maxPerDON) << 32) | (uint96(maxPerUserDON) << 64); | ||
return packed; | ||
} | ||
|
||
/// @dev Extract the 3 defaults from the packed value. | ||
function _unpackDefaults( | ||
uint96 packed | ||
) internal pure returns (uint32 maxPerUser, uint32 maxPerDON, uint32 maxPerUserDON) { | ||
maxPerUser = uint32(packed); | ||
maxPerDON = uint32(packed >> 32); | ||
maxPerUserDON = uint32(packed >> 64); | ||
return (maxPerUser, maxPerDON, maxPerUserDON); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// SPDX-License-Identifier: BUSL 1.1 | ||
pragma solidity 0.8.24; | ||
|
||
contract WorkflowRegistry_setDONOverride { | ||
function test_WhenCallerIsNOTTheOwner() external { | ||
// it should revert with OnlyCallableByOwner | ||
} | ||
|
||
modifier whenCallerISTheOwner() { | ||
_; | ||
} | ||
|
||
function test_WhenThereAreNoOverridesYet() external whenCallerISTheOwner { | ||
// it is the default value | ||
// call getMaxWorkflowsPerDon(donLabelA) returns the default (e.g. 500) | ||
} | ||
|
||
function test_WhenLimitIsSetTo0() external whenCallerISTheOwner { | ||
// it correctly sets the limit to 0 | ||
// call setDONOverride(donLabelA, 0) | ||
// getMaxWorkflowsPerDon(donLabelA) returns default (override cleared) | ||
} | ||
|
||
function test_WhenLimitIsSetToANormalPositiveValue() external whenCallerISTheOwner { | ||
// it correctly sets the limit | ||
// call setDONOverride(donLabelA, 123) | ||
// getMaxWorkflowsPerDon(donLabelA) returns 123 | ||
} | ||
|
||
function test_WhenLimitIsSetToUint32Max() external whenCallerISTheOwner { | ||
// it correctly sets the limit | ||
// call setDONOverride(donLabelA, 4294967295) | ||
// getMaxWorkflowsPerDon(donLabelA) returns 4294967295 | ||
} | ||
|
||
function test_WhenCallingTheFunctionMultipleTimes() external whenCallerISTheOwner { | ||
// it correctly sets the latest value | ||
// call setDONOverride(donLabelA, 10) | ||
// getMaxWorkflowsPerDon(donLabelA) returns 10 | ||
// call setDONOverride(donLabelA, 20) | ||
// getMaxWorkflowsPerDon(donLabelA) returns 20 (updated) | ||
} | ||
|
||
function test_WhenThereAreMultipleDONs() external whenCallerISTheOwner { | ||
// it should set the correct value for each DON | ||
// setDONOverride(donLabelA, 77) | ||
// getMaxWorkflowsPerDon(donLabelA) returns 77 | ||
// getMaxWorkflowsPerDon(donLabelB) returns default (500) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
WorkflowRegistry.setDONOverride | ||
├── when caller is NOT the owner | ||
│ └── it should revert with OnlyCallableByOwner | ||
└── when caller IS the owner | ||
├── when there are no overrides yet for a DON label | ||
│ └── it is the default value | ||
│ └── call getMaxWorkflowsPerDon(donLabelA) returns the default (e.g. 500) | ||
├── when limit is set to 0 | ||
│ └── it correctly sets the limit to 0 | ||
│ ├── call setDONOverride(donLabelA, 0) | ||
│ └── getMaxWorkflowsPerDon(donLabelA) returns default (override cleared) | ||
├── when limit is set to a normal positive value | ||
│ └── it correctly sets the limit | ||
│ ├── call setDONOverride(donLabelA, 123) | ||
│ └── getMaxWorkflowsPerDon(donLabelA) returns 123 | ||
├── when limit is set to uint32 max | ||
│ └── it correctly sets the limit | ||
│ ├── call setDONOverride(donLabelA, 4294967295) | ||
│ └── getMaxWorkflowsPerDon(donLabelA) returns 4294967295 | ||
├── when calling the function multiple times | ||
│ └── it correctly sets the latest value | ||
│ ├── call setDONOverride(donLabelA, 10) | ||
│ ├── getMaxWorkflowsPerDon(donLabelA) returns 10 | ||
│ ├── call setDONOverride(donLabelA, 20) | ||
│ └── getMaxWorkflowsPerDon(donLabelA) returns 20 (updated) | ||
└── when there are multiple DONs | ||
└── it should set the correct value for each DON | ||
├── setDONOverride(donLabelA, 77) | ||
├── getMaxWorkflowsPerDon(donLabelA) returns 77 | ||
└── getMaxWorkflowsPerDon(donLabelB) returns default (500) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
// SPDX-License-Identifier: BUSL 1.1 | ||
pragma solidity 0.8.24; | ||
|
||
contract WorkflowRegistry_setDefaults { | ||
function test_WhenTheCallerIsNOTTheOwner() external { | ||
// it should revert with OnlyCallableByOwner | ||
} | ||
|
||
modifier whenTheCallerISTheOwner() { | ||
_; | ||
} | ||
|
||
function test_WhenThereAreNoCallsMadeYet() external whenTheCallerISTheOwner { | ||
// it should be the constructor defaults (200, 500, 200) | ||
} | ||
|
||
function test_WhenItIsCalledWithATypicalValidUpdate() external whenTheCallerISTheOwner { | ||
// it should correctly set the updated values | ||
// call setDefaults(100, 200, 50) | ||
// getMaxWorkflowsPerUser(...) returns 100 | ||
// getMaxWorkflowsPerDon(...) returns 200 | ||
// getMaxWorkflowsPerUserDon(...) returns 50 | ||
// other mappings/overrides remain untouched | ||
} | ||
|
||
function test_WhenAllValuesAreZero() external whenTheCallerISTheOwner { | ||
// it should set 0 for all three values | ||
// call setDefaults(0, 0, 0) | ||
// getMaxWorkflowsPerUser(...) returns 0 | ||
// getMaxWorkflowsPerDon(...) returns 0 | ||
// getMaxWorkflowsPerUserDon(...) returns 0 | ||
} | ||
|
||
function test_WhenAllValuesAreAtUint32Max() external whenTheCallerISTheOwner { | ||
// it should set uint32 max for all three values | ||
// call setDefaults(4294967295, 4294967295, 4294967295) | ||
// getMaxWorkflowsPerUser(...) returns 4294967295 | ||
// getMaxWorkflowsPerDon(...) returns 4294967295 | ||
// getMaxWorkflowsPerUserDon(...) returns 4294967295 | ||
} | ||
|
||
function test_WhenCalledMultipleTimesInSequence() external whenTheCallerISTheOwner { | ||
// it should set to the most recent values | ||
// call setDefaults(A, B, C) | ||
// getters reflect (A, B, C) | ||
// call setDefaults(D, E, F) | ||
// getters now reflect (D, E, F) (overwriting previous) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
WorkflowRegistry.setDefaults | ||
├── when the caller is NOT the owner | ||
│ └── it should revert with OnlyCallableByOwner | ||
└── when the caller IS the owner | ||
├── when there are no calls made yet | ||
│ └── it should be the constructor defaults (200, 500, 200) | ||
├── when it is called with a typical valid update | ||
│ └── it should correctly set the updated values | ||
│ ├── call setDefaults(100, 200, 50) | ||
│ │ ├── getMaxWorkflowsPerUser(...) returns 100 | ||
│ │ ├── getMaxWorkflowsPerDon(...) returns 200 | ||
│ │ └── getMaxWorkflowsPerUserDon(...) returns 50 | ||
│ └── other mappings/overrides remain untouched | ||
├── when all values are zero | ||
│ └── it should set 0 for all three values | ||
│ └── call setDefaults(0, 0, 0) | ||
│ ├── getMaxWorkflowsPerUser(...) returns 0 | ||
│ ├── getMaxWorkflowsPerDon(...) returns 0 | ||
│ └── getMaxWorkflowsPerUserDon(...) returns 0 | ||
├── when all values are at uint32 max | ||
│ └── it should set uint32 max for all three values | ||
│ └── call setDefaults(4294967295, 4294967295, 4294967295) | ||
│ ├── getMaxWorkflowsPerUser(...) returns 4294967295 | ||
│ ├── getMaxWorkflowsPerDon(...) returns 4294967295 | ||
│ └── getMaxWorkflowsPerUserDon(...) returns 4294967295 | ||
└── when called multiple times in sequence | ||
└── it should set to the most recent values | ||
├── call setDefaults(A, B, C) | ||
│ └── getters reflect (A, B, C) | ||
└── call setDefaults(D, E, F) | ||
└── getters now reflect (D, E, F) (overwriting previous) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if we should set up a new profile for this and possibly also use pragma 0.8.26 if we do, especially as there will be duplicate functions when it comes to things like register
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For VRF, we set up a different profile for each version to benefit from having a newer Solidity version, so it's certainly possible. In this case, the Foundry profile could be called "workflow_v2".