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

Stake Root Simplifications [WIP] #741

Merged
merged 2 commits into from
Sep 16, 2024
Merged

Stake Root Simplifications [WIP] #741

merged 2 commits into from
Sep 16, 2024

Conversation

shotaronowhere
Copy link

This PR represents WIP simplification and extension of the stake root contracts. It is not in a finalized state as items such as test coverage, safe type casting, natspec comments, incentive compatibility for removals, modifications needed to charging, and agreement of the simplification approach are needed.

Simplifications

  • Removing checkpoint history data structures. Some of these data structures were used to track balance updates and introduced both logical and computational complexity as binary search was used in some cases. There's still a checkpoint history used for the total charge, but I think we can get rid of that with another approach (storage proof or synchronous charging).
  • The async nature of charging is another point of complexity related to the blacklist window. We should discuss what the goal is here for blacklisting in general, but in the context of charging, it looks like an unneccesary complication that impacts the happy path due to an unhappy path concern. As far as I see, the original implementation delays payment of the proof submitter in case some point of the zk-proving stack is compromised, and you don't want to compensate someone for producing a proof which does not effectively compute the stake root. On the otherhand, proof charging is rate limited and should this event happen, the contract is upgradable anyways so we could effectively pause the contract. If we are designing a contract that will be immutable in the future, then that changes the analysis. I don't think the the complexity of async charging is not worth the rare edge case it protects against.
  • Removal from the stake tree leaves strategies dirty

AVS' can withdraw the balances at any time and remove operator sets from the tree.

_addStrategiesAndMultipliers(IAVSDirectory.OperatorSet({avs: msg.sender, operatorSetId: operatorSetId}), strategiesAndMultipliers);
}

// natspec
Copy link
Contributor

Choose a reason for hiding this comment

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

delete


// natspec
/// @notice Initializes an operator set with a given set of strategies and multipliers
/// @dev This a convinience (gas saving) function for avs'
Copy link
Contributor

Choose a reason for hiding this comment

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

we use multiline comments stylistically

/**
 *
...
 */

/// @dev This a convinience (gas saving) function for avs'
/// @param operatorSetId The ID of the operator set to initialize
/// @param strategiesAndMultipliers The strategies and multipliers to initialize the operator set with
function initOperatorSetInStakeTree(
Copy link
Contributor

Choose a reason for hiding this comment

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

hm.

Can we just make addStrategiesAndMultipliers payable and deposit if msg.value > 0?

@@ -26,170 +25,139 @@ contract StakeRootCompendium is StakeRootCompendiumStorage {

/// @inheritdoc IStakeRootCompendium
function depositForOperatorSet(IAVSDirectory.OperatorSet calldata operatorSet) external payable {
Copy link
Contributor

Choose a reason for hiding this comment

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

just deposit to be consistent with new internal funcs

uint256 multipliedDelegatedShares = delegatedShares[i] * multipliers[i] / 1 ether;
delegatedStake += multipliedDelegatedShares * totalMagnitudes[i];
slashableStake += multipliedDelegatedShares * allocatedMagnitudes[i];
delegatedStake += delegatedShares[i] * totalMagnitudes[i] / 1 ether * multipliers[i];
Copy link
Contributor

Choose a reason for hiding this comment

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

i think the previous saves more gas in the zkVM

Copy link
Contributor

Choose a reason for hiding this comment

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

not sure though, we should benchmark

Copy link
Author

Choose a reason for hiding this comment

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

ahh okay this was inadvertent

}
IAVSDirectory.OperatorSet memory substituteOperatorSet = operatorSets[operatorSets.length - 1];
// note: operatorSetIndex is 1 indexed so operatorSetOneBasedIndex - 1 is strictly non-negative
operatorSets[operatorSetOneBasedIndex - 1] = substituteOperatorSet;
Copy link
Contributor

Choose a reason for hiding this comment

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

i definitely prefer 0 based indexing haha

return latestSnapshottedCumulativeCharge + uint224(currentCharge * (latestCalculationTimestamp - latestSnapshotTimestamp) / proofInterval);
// update the index of the operator sets
operatorSetToOneBasedIndex[msg.sender][operatorSet.operatorSetId] = 0;
operatorSetToOneBasedIndex[msg.sender][substituteOperatorSet.operatorSetId] = operatorSetOneBasedIndex;
Copy link
Contributor

Choose a reason for hiding this comment

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

this should not be msg.sender, it should be the operatorSet.avs and substituteOperatorSet.avs respectively

Copy link
Author

Choose a reason for hiding this comment

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

yeah, definitely

);
// updates the deposit balance for the operator set and returns the penalty if the operator set has fallen below the minimum deposit balance
function _updateDepositInfo(IAVSDirectory.OperatorSet memory operatorSet) internal returns (uint256) {
uint256 depositBalance = getDepositBalance(operatorSet);
Copy link
Contributor

Choose a reason for hiding this comment

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

we should remove them from the operatorSet if they fall below the min balance here?

Copy link
Author

Choose a reason for hiding this comment

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

we can, i reserved that logic where it is explicitly relevant eg processing withdrawals. . .

let me ponder, can put it back in, just thinking if there was a good reason i took it out

uint224 operatorSetIndex = operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].latest();
operatorSets[operatorSetIndex] = substituteOperatorSet;
operatorSets.pop();
function _updateTotalStrategies(uint256 numStrategiesBefore, IAVSDirectory.OperatorSet memory operatorSet) internal {
Copy link
Contributor

@gpsanant gpsanant Sep 11, 2024

Choose a reason for hiding this comment

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

rename back to updateTotalCharge because we need to call this when charges change

Copy link
Author

Choose a reason for hiding this comment

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

will do

cumulativeContantChargeSnapshot.upperLookup(timestamp),
cumulativeLinearChargeSnapshot.upperLookup(timestamp)
function _removeOperatorSetFromStakeTree(IAVSDirectory.OperatorSet memory operatorSet) internal {
uint256 operatorSetOneBasedIndex = operatorSetToOneBasedIndex[operatorSet.avs][operatorSet.operatorSetId];
Copy link
Contributor

Choose a reason for hiding this comment

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

we need to update the totalCharge in this function in case this is a force removal that isn't a removal of strategies


// remove the strategies and multipliers for the operator set
_removeStrategiesAndMultipliers(operatorSet, strategies);
function removeOperatorSetFromStakeTree(uint32 operatorSetId) external {
Copy link
Contributor

Choose a reason for hiding this comment

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

hm why do we have this?

Copy link
Author

Choose a reason for hiding this comment

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

not necessary I guess, could remove it if it doesn't matter for AVS'

}

/// @inheritdoc IStakeRootCompendium
function setOperatorSetExtraData(uint32 operatorSetId, bytes32 extraData) external {
(bool exists,, uint224 index) = operatorSetToIndex[msg.sender][operatorSetId].latestCheckpoint();
Copy link
Contributor

Choose a reason for hiding this comment

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

seems correct to revert here imo

Copy link
Author

Choose a reason for hiding this comment

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

yeah i'll put that back, it's a setter so used very rarely

operatorExtraDatas[msg.sender][operatorSetId][operator] = extraData;
}

/// POSTING ROOTS AND BLACKLISTING

/// @inheritdoc IStakeRootCompendium
function verifyStakeRoot(
uint32 calculationTimestamp,
uint64 calculationBlockNumber,
Copy link
Contributor

@gpsanant gpsanant Sep 11, 2024

Choose a reason for hiding this comment

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

this should be a timestamp since everything is in timestamps in the slashing release

Copy link
Author

Choose a reason for hiding this comment

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

hmmmm this might be awkward since the zk proof is against a blockhash and blocks are sequential but calculationTimestamps can jump
BN: [ 0 ] [ 1 ] [ 2 ]
TS: 12 36 48

so there is a missing timestamp corresponding to timestamp 24. . . should a proof be skipped in this case?

);
require(
block.timestamp < stakeRootSubmissions[submissionIndex].blacklistableBefore,
"StakeRootCompendium.blacklistStakeRoot: stakeRoot cannot be blacklisted"
latestBlockNumberProven < calculationBlockNumber,
Copy link
Contributor

Choose a reason for hiding this comment

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

calculationBlockNumber = latestBlockNumberProven + proofInterval

And this can only be if this is not the first submission

Copy link
Author

Choose a reason for hiding this comment

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

what do you mean?

from the first require statement, we have calculationBlockNumber % proofInterval == 0

and from latestBlockNumberProven < calculationBlockNumber

we have basically that

latestBlockNumberProven = latestBlockNumberProven + n * proofInterval


do you mean proofs should be calculated strictly sequentially?

"StakeRootCompendium._postStakeRoot: calculationTimestamp must be greater than the last posted calculationTimestamp"
);
if (submission.stakeRoot == bytes32(0)) {
submission.stakeRoot = stakeRoot;
Copy link
Contributor

Choose a reason for hiding this comment

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

if this is not a proof post, the submission must be proven. confirmation must happen after proofs

Copy link
Author

Choose a reason for hiding this comment

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

I was kind of overloading this functionality for the desired feature that if the provers are offline, the same data structures can be used for AVS' which may choose to use a simpler confirmation rule (trust eigen labs)


or what is desired as a fallback if zkprovers are all offline?

Copy link
Author

Choose a reason for hiding this comment

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

but yeah, certainly can adjust this so confirmerss strictly confirm

operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].push(uint32(block.timestamp), operatorSetIndex);
function _setChargePerProof(uint64 _chargePerProofLinear, uint64 _chargePerProofConstant) internal {
_updateCumulativeCharges();
chargePerProofLinear = _chargePerProofLinear;
Copy link
Contributor

Choose a reason for hiding this comment

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

we need to update the total Charge here

Copy link
Author

Choose a reason for hiding this comment

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

yeah, i overlooked some total charging related logic, will put it in there

function _updateDepositInfo(IAVSDirectory.OperatorSet memory operatorSet) internal returns (uint256) {
uint256 depositBalance = getDepositBalance(operatorSet);
depositInfo[operatorSet.avs][operatorSet.operatorSetId].balance = uint96(depositBalance);
depositInfo[operatorSet.avs][operatorSet.operatorSetId].snapshotTimestamp = uint64(block.timestamp);
Copy link
Contributor

Choose a reason for hiding this comment

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

we need to update the cumulatives here

stakeRootSubmissions[submissionIndex].blacklisted = true;
// TODO: verify proof
_postStakeRoot(true, calculationBlockNumber, stakeRoot);
uint256 charge = totalChargeSnapshot.upperLookup(uint32(calculationBlockNumber));
Copy link
Contributor

Choose a reason for hiding this comment

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

should we just allow them to submit an index here to avoid lookup gas?

Copy link
Author

Choose a reason for hiding this comment

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

i need to put a bit more thought into the total charge, thats a component I overlooked a bit. . .

can't we avoid snapshots by having a view function for the totalCharge and doing a storage proof for that block? that's basically what the zk proof is doing anyways right? could add that to the proof? and to avoid zk tech stack failure enforce a MAX_TOTAL_CHARGE = 3 eth

alternatively to KISS we can do a little bit more accounting to have an O(1) lookup, I think there's a simple way to do it

@@ -23,8 +23,6 @@ abstract contract StakeRootCompendiumStorage is IStakeRootCompendium, OwnableUpg
uint32 public constant MAX_NUM_OPERATOR_SETS = 2048;
/// @notice the maximum number of strategies that each operator set in the StakeTree can use to weight their operator stakes
uint32 public constant MAX_NUM_STRATEGIES = 20;
/// @notice the placeholder index used for operator sets that are removed from the StakeTree
uint32 public constant REMOVED_INDEX = type(uint32).max;

/// @notice the minimum balance that must be maintained for an operatorSet
uint256 public constant MIN_DEPOSIT_BALANCE = 0.001 ether;
Copy link
Contributor

Choose a reason for hiding this comment

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

can we make this an immutable?

Copy link
Author

Choose a reason for hiding this comment

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

MIN_DEPOSIT_BALANCE? yeah

stakeRootSubmissions[submissionIndex].blacklisted = true;
// TODO: verify proof
_postStakeRoot(true, calculationBlockNumber, stakeRoot);
uint256 charge = totalChargeSnapshot.upperLookup(uint32(calculationBlockNumber));
Copy link
Contributor

Choose a reason for hiding this comment

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

set latestBlockNumberProven

src/contracts/core/StakeRootCompendium.sol Fixed Show fixed Hide fixed
src/contracts/core/StakeRootCompendium.sol Fixed Show fixed Hide fixed
src/contracts/core/StakeRootCompendium.sol Fixed Show fixed Hide fixed
src/contracts/core/StakeRootCompendium.sol Fixed Show fixed Hide fixed
src/contracts/core/StakeRootCompendium.sol Fixed Show fixed Hide fixed
Comment on lines +415 to +433
function _isInStakeTree(IAVSDirectory.OperatorSet memory operatorSet) internal view returns (bool) {
(bool exists,,uint224 index) = operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].latestCheckpoint();
return exists && index != REMOVED_INDEX;
}

Check warning

Code scanning / Slither

Unused return

StakeRootCompendium._isInStakeTree(IAVSDirectory.OperatorSet) (src/contracts/core/StakeRootCompendium.sol#415-418) ignores return value by (exists,None,index) = operatorSetToIndex[operatorSet.avs][operatorSet.operatorSetId].latestCheckpoint() (src/contracts/core/StakeRootCompendium.sol#416)
Comment on lines 307 to 333
function _updateTotalStrategies(uint256 _countStrategiesBefore, uint256 _countStrategiesAfter) internal {
totalStrategies = totalStrategies - _countStrategiesBefore + _countStrategiesAfter;
_updateChargePerProof();
}

Check warning

Code scanning / Slither

Costly operations inside a loop

StakeRootCompendium._updateTotalStrategies(uint256,uint256) (src/contracts/core/StakeRootCompendium.sol#307-310) has costly operations inside a loop: - totalStrategies = totalStrategies - _countStrategiesBefore + _countStrategiesAfter (src/contracts/core/StakeRootCompendium.sol#308)
bytes32 _imageId
) StakeRootCompendiumStorage(_delegationManager, _avsDirectory, _maxTotalCharge, _minBalanceThreshold, _minProofsDuration, _verifier, _imageId) {}

function initialize(address _owner, address _rootConfirmer, uint32 _proofIntervalSeconds, uint96 _chargePerStrategy, uint96 _chargePerOperatorSet) public initializer {

Check notice

Code scanning / Slither

Missing zero address validation

StakeRootCompendium.initialize(address,address,uint32,uint96,uint96)._rootConfirmer (src/contracts/core/StakeRootCompendium.sol#23) lacks a zero-check on : - rootConfirmer = _rootConfirmer (src/contracts/core/StakeRootCompendium.sol#27)
if (endIndex > stakeRootSubmissions.length) {
endIndex = stakeRootSubmissions.length;
}
function setProofIntervalSeconds(uint32 proofIntervalSeconds) public onlyOwner {

Check notice

Code scanning / Slither

Local variable shadowing

StakeRootCompendium.setProofIntervalSeconds(uint32).proofIntervalSeconds (src/contracts/core/StakeRootCompendium.sol#251) shadows: - StakeRootCompendiumStorage.proofIntervalSeconds (src/contracts/core/StakeRootCompendiumStorage.sol#46) (state variable) - IStakeRootCompendium.proofIntervalSeconds() (src/contracts/interfaces/IStakeRootCompendium.sol#53) (function)
// total charge is the charge per strategy per proof times the number of strategies at the time of proof
totalCharge += totalChargeSnapshot.upperLookup(stakeRootSubmissions[i].calculationTimestamp);
}
function setRootConfirmer(address _rootConfirmer) public onlyOwner {

Check notice

Code scanning / Slither

Missing zero address validation

StakeRootCompendium.setRootConfirmer(address)._rootConfirmer (src/contracts/core/StakeRootCompendium.sol#261) lacks a zero-check on : - rootConfirmer = _rootConfirmer (src/contracts/core/StakeRootCompendium.sol#262)
Comment on lines +335 to +343
function _updateChargePerProof() internal {
// note if totalStrategies is 0, the charge per proof will be 0, and provers should not post a proof
uint256 chargePerProof = operatorSets.length * chargePerOperatorSet + totalStrategies * chargePerStrategy;
require(chargePerProof <= MAX_TOTAL_CHARGE, "StakeRootCompendium._updateChargePerProof: charge per proof exceeds max total charge");
chargePerProofHistory.push(
uint32(block.timestamp),
uint224(chargePerProof)
);
}

Check warning

Code scanning / Slither

Unused return

StakeRootCompendium._updateChargePerProof() (src/contracts/core/StakeRootCompendium.sol#335-343) ignores return value by chargePerProofHistory.push(uint32(block.timestamp),uint224(chargePerProof)) (src/contracts/core/StakeRootCompendium.sol#339-342)
bytes32 public immutable imageId;

/// @notice the interval in seconds at which proofs can be posted
uint32 public proofIntervalSeconds;

Check failure

Code scanning / Slither

Uninitialized state variables

StakeRootCompendiumStorage.proofIntervalSeconds (src/contracts/core/StakeRootCompendiumStorage.sol#46) is never initialized. It is used in: - StakeRootCompendium.verifyStakeRoot(uint256,bytes32,address,uint256,IStakeRootCompendium.Proof) (src/contracts/core/StakeRootCompendium.sol#196-231) - StakeRootCompendium.canWithdrawDepositBalance(IAVSDirectory.OperatorSet) (src/contracts/core/StakeRootCompendium.sol#273-275) - StakeRootCompendium._totalCharge() (src/contracts/core/StakeRootCompendium.sol#345-357)
bytes32 public immutable imageId;

/// @notice the interval in seconds at which proofs can be posted
uint32 public proofIntervalSeconds;

Check warning

Code scanning / Slither

State variables that could be declared constant

StakeRootCompendiumStorage.proofIntervalSeconds (src/contracts/core/StakeRootCompendiumStorage.sol#46) should be constant
@gpsanant gpsanant merged commit 7023144 into stake-root Sep 16, 2024
4 of 10 checks passed
@gpsanant gpsanant deleted the shotaro-stake-root branch September 16, 2024 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants