Execution
Overview
The most important part of the PowerAgent operation. This section describes all steps performed by a Keeper and the Agent contract to execute the Job at a given time (or upon satisfying given conditions).
Whenever execution conditions are met, a Keeper is chosen to execute the task. Alternatively, a Keeper can be pre-selected for the next execution when a number of events happen (e.g. the job parameters or credits values change).
Keeper selection
For more information on Keeper selection, see <page>.
Specification
The chosen Keeper calls the function execute_44g58pv
of the Agent contract, passing required calldata dependent on the type of the Job and the config for the execution.
The function is heavily optimized for gas savings; inline assembly is used alongside with binary operations.
cfg
0x | name | binary
01 | FLAG_ACCEPT_MAX_BASE_FEE_LIMIT | 0...001
02 | FLAG_ACCRUE_REWARD | 0...010
This config is constructed by the Keeper for each execution separately. Setting the flags allows the keeper to override reversion when the block base gas fee exceeds the limit set by the job owner; the second boolean flag determines whether to accrue the reward for the execution on the PowerAgent contract or transfer it to the keeper worker address immediately.
calldata
Calldata passed by the keeper for the target function execution. Read this section for information on execution calldata.
Algorithm
function execute_44g58pv() external
{
//Hook called at the very start of the execution.
_beforeExecution();
//=========Assertions=========
_assertKeeperHasSufficientStake();
_assertJobIsActive();
_assertMinCvpDeposit();
_assertIntervalHasPassed();
_assertBaseFee();
_assertEoaMsgSender();
//=======Calldata load========
_assertCalldataNotMissing();
_assertSelectorCheck();
//=========Execution==========
//Try to execute
//if execution failed, store Job response in the memory for the purpose of constructing traceback.
//==========Payout==========
//If execution succeeded, pay compensation + reward
//Else, pay compensation (in RanDAO; in Flashbots realisation the payment is always in full)
//==========Events============
//If execution succeeded,
emit Execute(
jobKey,
jobAddress,
actualKeeperId,
gasUsed,
block.basefee,
tx.gasprice,
compensation,
bytes32(binJob)
);
//Hook called after successful execution
_afterExecutionSucceeded(
jobKey,
actualKeeperId,
binJob
);
//Else
//Hook called after execution reverted
_afterExecutionReverted(
jobKey,
//Type of calldata provided for execution
calldataSource,
actualKeeperId,
//Response from the Job contract stored if the execution reverted
executionResponse
);
}
Hooks
The hook functions are used to process execution calls by slashers (INSERT LINK TO SLASHING).
Their implementation varies depending on the realisation of the PowerAgent.
_beforeExecute
This hook is called at the very beginning of the execute
function. It executes only if the caller is a slasher.
The purpose of this hook is to ensure that by the time the slasher called this function, the slashing has been initiated and the task has been failed by the assigned keeper. If so, after the hook the execution continues, and the slasher becomes the executor of the task. Otherwise, a corresponding error is thrown, and the slasher is reverted.
_afterExecutionSucceeded
Called at the end if the execution was successful.
_afterExecutionReverted
Called at the end if the transaction was reverted by the Job contract.
Select a Keeper
Flashbots
At the current state of PPagentV2, the keeper to execute the task is selected by the Flashbots: the one offering the highest gas price is chosen, others are reverted with no cost.
RanDAO
Choosing a keeper by random seems like a smart idea.
In the RanDAO version, the choice is made by the following algorithm:
//get the randao realisation for the current block, stored in the block.difficulty
uint256 pseudoRandom = _getPseudoRandom();
//get the cardinality of the keeper set so that the index may be taken modulo it
uint256 totalActiveKeepers = activeKeepers.length();
//get the minimal CVP stake demanded of the keeper by the job
uint256 _jobMinKeeperCvp = jobMinKeeperCvp[jobKey_];
//obtain the index by taking the pseudorandom input, adding the job key so that variance in chosen keeper indices is observed within any single block, and mapping the result into the keeper index set by taking the modulo operation
//the resultant variable index is the index of the keeper selected for execution
uint256 index;
unchecked {
index = ((pseudoRandom + uint256(jobKey_)) % totalActiveKeepers);
}
//iterate over all the keepers, starting with the randomly selected one, until a keeper admissible for the job at hand is found or all gas has been expended
while (true) {
//effectively modulo operation
if (index >= totalActiveKeepers) {
index = 0;
}
//obtain the ID of the next keeper by the index selected
uint256 _nextExecutionKeeperId = activeKeepers.at(index);
//required stake is set to be the global minimal demanded CVP unless the job has a specification of its own
uint256 requiredStake = _jobMinKeeperCvp > 0 ? _jobMinKeeperCvp : minKeeperCvp;
//load the keeper object at the selected ID
Keeper memory keeper = keepers[_nextExecutionKeeperId];
//if the keeper is admissible, i.e. active and has sufficient stake, then assign him as the next keeper of the job and terminate the passage
if (keeper.isActive && keeper.cvpStake >= requiredStake) {
jobNextKeeperId[jobKey_] = _nextExecutionKeeperId;
keeperLocksByJob[_nextExecutionKeeperId].add(jobKey_);
emit KeeperJobLock(_nextExecutionKeeperId, jobKey_);
return;
}
index += 1;
}
}
Keeper assignment only takes place at either a corresponding external call by a job owner, after a successful job execution, after a job is registered; if a job changes its activity status or credit amount deposited, the keeper is assigned conditionally if the job has at least the minimal admissible amount of credits (specified by the RanDAO realisation config parameter rdConfig.jobMinCreditsFinney
).
Last updated
Was this helpful?