-
Notifications
You must be signed in to change notification settings - Fork 92
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
migrate Prover functionality from core repo #4
Closed
Closed
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
16fef83
migrate Prover functionality from core repo
ChaoticWalrus a312607
Merge branch 'master' of https://github.com/Layr-Labs/eigenlayer-midd…
ChaoticWalrus cdcf576
clean up trash and add the folder to gitignore
ChaoticWalrus eb1136d
Merge branch 'master' of https://github.com/Layr-Labs/eigenlayer-midd…
ChaoticWalrus e6703d2
Merge branch 'master' of https://github.com/Layr-Labs/eigenlayer-midd…
ChaoticWalrus a91ec69
fix script to use correct remapping and remove double forward-slash b…
ChaoticWalrus d762ddc
fix duplicate run issue
ChaoticWalrus a2ebaf5
Merge branch 'master' of https://github.com/Layr-Labs/eigenlayer-midd…
ChaoticWalrus be6479c
try using more NONDET summaries
ChaoticWalrus 8eea40f
Merge branch 'master' of https://github.com/Layr-Labs/eigenlayer-midd…
ChaoticWalrus 4f7e77d
temporarily delete submodule in order to force upgrade
ChaoticWalrus 3a83c4e
remove other submodules
ChaoticWalrus 0001b43
forge install: eigenlayer-contracts
ChaoticWalrus dcca0b6
chore: switch remappings to use more of the eigenlayer-contracts subm…
ChaoticWalrus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
name: Certora Prover | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
- release-v* | ||
- formal-verification | ||
pull_request: {} | ||
workflow_dispatch: {} | ||
|
||
jobs: | ||
list-scripts: | ||
runs-on: ubuntu-latest | ||
outputs: | ||
matrix: ${{ steps.set-matrix.outputs.matrix }} | ||
steps: | ||
- uses: actions/checkout@v2 | ||
- id: set-matrix | ||
run: echo ::set-output name=matrix::$(ls certora/scripts/*.sh | grep -v '\WnoCI\W' | jq -Rsc 'split("\n")[:-1]') | ||
verify: | ||
runs-on: ubuntu-latest | ||
needs: list-scripts | ||
steps: | ||
- uses: actions/checkout@v3 | ||
with: | ||
submodules: recursive | ||
- name: Install Foundry | ||
uses: foundry-rs/foundry-toolchain@v1 | ||
with: | ||
version: nightly | ||
- name: Install forge dependencies | ||
run: forge install | ||
- name: Install python | ||
uses: actions/setup-python@v2 | ||
with: | ||
python-version: '3.10' | ||
cache: 'pip' | ||
- name: Install java | ||
uses: actions/setup-java@v2 | ||
with: | ||
distribution: temurin | ||
java-version: '17' | ||
- name: Install certora | ||
run: pip install certora-cli | ||
- name: Install solc | ||
run: | | ||
wget https://github.com/ethereum/solidity/releases/download/v0.8.12/solc-static-linux | ||
sudo mv solc-static-linux /usr/local/bin/solc | ||
chmod +x /usr/local/bin/solc | ||
- name: Verify rule ${{ matrix.params }} | ||
run: | | ||
touch certora/applyHarness.patch | ||
make -C certora munged | ||
bash ${{ matrix.params }} | ||
env: | ||
CERTORAKEY: ${{ secrets.CERTORAKEY }} | ||
|
||
strategy: | ||
fail-fast: false | ||
max-parallel: 4 | ||
matrix: | ||
params: ${{ fromJson(needs.list-scripts.outputs.matrix) }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,3 +11,9 @@ node_modules/ | |
|
||
# Dotenv file | ||
.env | ||
|
||
# Certora Outputs | ||
.certora_internal/ | ||
.certora_recent_jobs.json | ||
|
||
*.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,3 @@ | ||
[submodule "lib/forge-std"] | ||
path = lib/forge-std | ||
url = https://github.com/foundry-rs/forge-std | ||
[submodule "lib/eigenlayer-contracts"] | ||
path = lib/eigenlayer-contracts | ||
url = https://github.com/Layr-labs/eigenlayer-contracts | ||
[submodule "lib/ds-test"] | ||
path = lib/ds-test | ||
url = https://github.com/dapphub/ds-test | ||
url = https://github.com/Layr-Labs/eigenlayer-contracts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
munged |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
default: help | ||
|
||
PATCH = applyHarness.patch | ||
CONTRACTS_DIR = ../src | ||
MUNGED_DIR = munged | ||
|
||
help: | ||
@echo "usage:" | ||
@echo " make clean: remove all generated files (those ignored by git)" | ||
@echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)" | ||
@echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)" | ||
|
||
munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) | ||
rm -rf $@ | ||
cp -r $(CONTRACTS_DIR) $@ | ||
patch -p0 -d $@ < $(PATCH) | ||
|
||
record: | ||
diff -druN $(CONTRACTS_DIR) $(MUNGED_DIR) | sed 's+../contracts/++g' | sed 's+munged/++g' > $(PATCH) | ||
|
||
refresh: munged record | ||
|
||
clean: | ||
git clean -fdX | ||
touch $(PATCH) |
Empty file.
54 changes: 54 additions & 0 deletions
54
certora/harnesses/BLSRegistryCoordinatorWithIndicesHarness.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity =0.8.12; | ||
|
||
import "../munged/BLSRegistryCoordinatorWithIndices.sol"; | ||
|
||
contract BLSRegistryCoordinatorWithIndicesHarness is BLSRegistryCoordinatorWithIndices { | ||
constructor( | ||
ISlasher _slasher, | ||
IServiceManager _serviceManager, | ||
IStakeRegistry _stakeRegistry, | ||
IBLSPubkeyRegistry _blsPubkeyRegistry, | ||
IIndexRegistry _indexRegistry | ||
) | ||
BLSRegistryCoordinatorWithIndices(_slasher, _serviceManager, _stakeRegistry, _blsPubkeyRegistry, _indexRegistry) | ||
{} | ||
|
||
// @notice function based upon `BitmapUtils.bytesArrayToBitmap`, used to determine if an array contains any duplicates | ||
function bytesArrayContainsDuplicates(bytes memory bytesArray) public pure returns (bool) { | ||
// sanity-check on input. a too-long input would fail later on due to having duplicate entry(s) | ||
if (bytesArray.length > 256) { | ||
return false; | ||
} | ||
|
||
// initialize the empty bitmap, to be built inside the loop | ||
uint256 bitmap; | ||
// initialize an empty uint256 to be used as a bitmask inside the loop | ||
uint256 bitMask; | ||
|
||
// loop through each byte in the array to construct the bitmap | ||
for (uint256 i = 0; i < bytesArray.length; ++i) { | ||
// construct a single-bit mask from the numerical value of the next byte out of the array | ||
bitMask = uint256(1 << uint8(bytesArray[i])); | ||
// check that the entry is not a repeat | ||
if (bitmap & bitMask != 0) { | ||
return false; | ||
} | ||
// add the entry to the bitmap | ||
bitmap = (bitmap | bitMask); | ||
} | ||
|
||
// if the loop is completed without returning early, then the array contains no duplicates | ||
return true; | ||
} | ||
|
||
// @notice verifies that a bytes array is a (non-strict) subset of a bitmap | ||
function bytesArrayIsSubsetOfBitmap(uint256 referenceBitmap, bytes memory arrayWhichShouldBeASubsetOfTheReference) public pure returns (bool) { | ||
uint256 arrayWhichShouldBeASubsetOfTheReferenceBitmap = BitmapUtils.bytesArrayToBitmap(arrayWhichShouldBeASubsetOfTheReference); | ||
if (referenceBitmap | arrayWhichShouldBeASubsetOfTheReferenceBitmap == referenceBitmap) { | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
certora/scripts/verifyBLSRegistryCoordinatorWithIndices.sh
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
if [[ "$2" ]] | ||
then | ||
RULE="--rule $2" | ||
fi | ||
|
||
solc-select use 0.8.12 | ||
|
||
certoraRun certora/harnesses/BLSRegistryCoordinatorWithIndicesHarness.sol \ | ||
lib/eigenlayer-contracts/lib/openzeppelin-contracts/contracts/mocks/ERC1271WalletMock.sol \ | ||
certora/munged/StakeRegistry.sol certora/munged/BLSPubkeyRegistry.sol certora/munged/IndexRegistry.sol \ | ||
lib/eigenlayer-contracts/src/contracts/core/Slasher.sol \ | ||
--verify BLSRegistryCoordinatorWithIndicesHarness:certora/specs/BLSRegistryCoordinatorWithIndices.spec \ | ||
--optimistic_loop \ | ||
--optimistic_hashing \ | ||
--prover_args '-optimisticFallback true -recursionEntryLimit 2 ' \ | ||
$RULE \ | ||
--loop_iter 2 \ | ||
--packages @openzeppelin=lib/eigenlayer-contracts/lib/openzeppelin-contracts @openzeppelin-upgrades=lib/eigenlayer-contracts/lib/openzeppelin-contracts-upgradeable eigenlayer-contracts=lib/eigenlayer-contracts \ | ||
--msg "BLSRegistryCoordinatorWithIndices $1 $2" \ | ||
|
||
# TODO: import a ServiceManager contract |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
|
||
methods { | ||
//// External Calls | ||
// external calls to StakeRegistry | ||
function _.quorumCount() external => DISPATCHER(true); | ||
function _.getCurrentTotalStakeForQuorum(uint8 quorumNumber) external => DISPATCHER(true); | ||
function _.getCurrentOperatorStakeForQuorum(bytes32 operatorId, uint8 quorumNumber) external => DISPATCHER(true); | ||
// function _.registerOperator(address, bytes32, bytes) external => DISPATCHER(true); | ||
function _.registerOperator(address, bytes32, bytes) external => NONDET; | ||
function _.deregisterOperator(bytes32, bytes) external => DISPATCHER(true); | ||
|
||
// external calls to Slasher | ||
function _.contractCanSlashOperatorUntilBlock(address, address) external => DISPATCHER(true); | ||
|
||
// external calls to BLSPubkeyRegistry | ||
// function _.registerOperator(address, bytes, BN254.G1Point) external => DISPATCHER(true); | ||
function _.registerOperator(address, bytes, BN254.G1Point) external => NONDET; | ||
function _.deregisterOperator(address, bytes, BN254.G1Point) external => DISPATCHER(true); | ||
|
||
// external calls to ServiceManager | ||
function _.latestServeUntilBlock() external => DISPATCHER(true); | ||
function _.recordLastStakeUpdateAndRevokeSlashingAbility(address, uint256) external => DISPATCHER(true); | ||
|
||
// external calls to IndexRegistry | ||
// function _.registerOperator(bytes32, bytes) external => DISPATCHER(true); | ||
function _.registerOperator(bytes32, bytes) external => NONDET; | ||
function _.deregisterOperator(bytes32, bytes, bytes32[]) external => DISPATCHER(true); | ||
|
||
// external calls to ERC1271 (can import OpenZeppelin mock implementation) | ||
// isValidSignature(bytes32 hash, bytes memory signature) returns (bytes4 magicValue) => DISPATCHER(true) | ||
// function _.isValidSignature(bytes32, bytes) external => DISPATCHER(true); | ||
function _.isValidSignature(bytes32, bytes) external => NONDET; | ||
|
||
// external calls to BLSPubkeyCompendium | ||
function _.pubkeyHashToOperator(bytes32) external => DISPATCHER(true); | ||
|
||
//envfree functions | ||
function OPERATOR_CHURN_APPROVAL_TYPEHASH() external returns (bytes32) envfree; | ||
function slasher() external returns (address) envfree; | ||
function serviceManager() external returns (address) envfree; | ||
function blsPubkeyRegistry() external returns (address) envfree; | ||
function stakeRegistry() external returns (address) envfree; | ||
function indexRegistry() external returns (address) envfree; | ||
function registries(uint256) external returns (address) envfree; | ||
function churnApprover() external returns (address) envfree; | ||
function isChurnApproverSaltUsed(bytes32) external returns (bool) envfree; | ||
function getOperatorSetParams(uint8 quorumNumber) external returns (IBLSRegistryCoordinatorWithIndices.OperatorSetParam) envfree; | ||
function getOperator(address operator) external returns (IRegistryCoordinator.Operator) envfree; | ||
function getOperatorId(address operator) external returns (bytes32) envfree; | ||
function getOperatorStatus(address operator) external returns (IRegistryCoordinator.OperatorStatus) envfree; | ||
function getQuorumBitmapIndicesByOperatorIdsAtBlockNumber(uint32 blockNumber, bytes32[] operatorIds) | ||
external returns (uint32[]) envfree; | ||
function getQuorumBitmapByOperatorIdAtBlockNumberByIndex(bytes32 operatorId, uint32 blockNumber, uint256 index) external returns (uint192) envfree; | ||
function getQuorumBitmapUpdateByOperatorIdByIndex(bytes32 operatorId, uint256 index) | ||
external returns (IRegistryCoordinator.QuorumBitmapUpdate) envfree; | ||
function getCurrentQuorumBitmapByOperatorId(bytes32 operatorId) external returns (uint192) envfree; | ||
function getQuorumBitmapUpdateByOperatorIdLength(bytes32 operatorId) external returns (uint256) envfree; | ||
function numRegistries() external returns (uint256) envfree; | ||
function calculateOperatorChurnApprovalDigestHash( | ||
bytes32 registeringOperatorId, | ||
IBLSRegistryCoordinatorWithIndices.OperatorKickParam[] operatorKickParams, | ||
bytes32 salt, | ||
uint256 expiry | ||
) external returns (bytes32) envfree; | ||
|
||
// harnessed functions | ||
function bytesArrayContainsDuplicates(bytes bytesArray) external returns (bool) envfree; | ||
function bytesArrayIsSubsetOfBitmap(uint256 referenceBitmap, bytes arrayWhichShouldBeASubsetOfTheReference) external returns (bool) envfree; | ||
} | ||
|
||
// If my Operator status is REGISTERED ⇔ my quorum bitmap MUST BE nonzero | ||
invariant registeredOperatorsHaveNonzeroBitmaps(address operator) | ||
getOperatorStatus(operator) == IRegistryCoordinator.OperatorStatus.REGISTERED <=> | ||
getCurrentQuorumBitmapByOperatorId(getOperatorId(operator)) != 0; | ||
|
||
// if two operators have different addresses, then they have different IDs | ||
// excludes the case in which the operator is not registered, since then they can both have ID zero (the default) | ||
invariant operatorIdIsUnique(address operator1, address operator2) | ||
operator1 != operator2 => | ||
(getOperatorStatus(operator1) == IRegistryCoordinator.OperatorStatus.REGISTERED => getOperatorId(operator1) != getOperatorId(operator2)); | ||
|
||
definition methodCanModifyBitmap(method f) returns bool = | ||
f.selector == sig:registerOperatorWithCoordinator(bytes, bytes).selector | ||
|| f.selector == sig:registerOperatorWithCoordinator(bytes, BN254.G1Point, string).selector | ||
|| f.selector == sig:registerOperatorWithCoordinator( | ||
bytes, | ||
BN254.G1Point, | ||
string, | ||
IBLSRegistryCoordinatorWithIndices.OperatorKickParam[], | ||
ISignatureUtils.SignatureWithSaltAndExpiry | ||
).selector | ||
|| f.selector == sig:deregisterOperatorWithCoordinator(bytes, bytes).selector | ||
|| f.selector == sig:deregisterOperatorWithCoordinator(bytes, BN254.G1Point, bytes32[]).selector; | ||
|
||
definition methodCanAddToBitmap(method f) returns bool = | ||
f.selector == sig:registerOperatorWithCoordinator(bytes, bytes).selector | ||
|| f.selector == sig:registerOperatorWithCoordinator(bytes, BN254.G1Point, string).selector | ||
|| f.selector == sig:registerOperatorWithCoordinator( | ||
bytes, | ||
BN254.G1Point, | ||
string, | ||
IBLSRegistryCoordinatorWithIndices.OperatorKickParam[], | ||
ISignatureUtils.SignatureWithSaltAndExpiry | ||
).selector; | ||
|
||
// `registerOperatorWithCoordinator` with kick params also meets this definition due to the 'churn' mechanism | ||
definition methodCanRemoveFromBitmap(method f) returns bool = | ||
f.selector == sig:registerOperatorWithCoordinator( | ||
bytes, | ||
BN254.G1Point, | ||
string, | ||
IBLSRegistryCoordinatorWithIndices.OperatorKickParam[], | ||
ISignatureUtils.SignatureWithSaltAndExpiry | ||
).selector | ||
|| f.selector == sig:deregisterOperatorWithCoordinator(bytes, bytes).selector | ||
|| f.selector == sig:deregisterOperatorWithCoordinator(bytes, BN254.G1Point, bytes32[]).selector; | ||
|
||
// verify that quorumNumbers provided as an input to deregister operator MUST BE a subset of the operator’s current quorums | ||
rule canOnlyDeregisterFromExistingQuorums(address operator) { | ||
requireInvariant registeredOperatorsHaveNonzeroBitmaps(operator); | ||
|
||
// TODO: store this status, verify that all calls to `deregisterOperatorWithCoordinator` *fail* if the operator is not registered first! | ||
require(getOperatorStatus(operator) == IRegistryCoordinator.OperatorStatus.REGISTERED); | ||
|
||
uint256 bitmapBefore = getCurrentQuorumBitmapByOperatorId(getOperatorId(operator)); | ||
|
||
bytes quorumNumbers; | ||
BN254.G1Point pubkey; | ||
bytes32[] operatorIdsToSwap; | ||
env e; | ||
|
||
deregisterOperatorWithCoordinator(e, quorumNumbers, pubkey, operatorIdsToSwap); | ||
|
||
// if deregistration is successful, verify that `quorumNumbers` input was proper | ||
if (getOperatorStatus(operator) != IRegistryCoordinator.OperatorStatus.REGISTERED) { | ||
assert(bytesArrayIsSubsetOfBitmap(bitmapBefore, quorumNumbers)); | ||
} else { | ||
assert(true); | ||
} | ||
} | ||
|
||
/* TODO: this is a Work In Progress | ||
rule canOnlyModifyBitmapWithSpecificFunctions(address operator) { | ||
requireInvariant registeredOperatorsHaveNonzeroBitmaps(operator); | ||
uint256 bitmapBefore = getCurrentQuorumBitmapByOperatorId(getOperatorId(operator)); | ||
// prepare to perform arbitrary function call | ||
method f; | ||
env e; | ||
// TODO: need to ensure that if the function can modify the bitmap, then we are using the operator as an input | ||
if (!methodCanModifyBitmap(f)) { | ||
// perform arbitrary function call | ||
calldataarg arg; | ||
f(e, arg); | ||
uint256 bitmapAfter = getCurrentQuorumBitmapByOperatorId(getOperatorId(operator)); | ||
assert(bitmapAfter == bitmapBefore); | ||
} else if ( | ||
f.selector == sig:registerOperatorWithCoordinator(bytes, bytes).selector | ||
) { | ||
if (e.msg.sender != operator) { | ||
uint256 bitmapAfter = getCurrentQuorumBitmapByOperatorId(getOperatorId(operator)); | ||
assert(bitmapAfter == bitmapBefore); | ||
} | ||
} | ||
|
||
// if method did not remove from bitmap, it must have added | ||
if (bitmapAfter & bitmapBefore == bitmapBefore) { | ||
assert(methodCanAddToBitmap(f)); | ||
} else { | ||
assert(methodCanRemoveFromBitmap(f)); | ||
} | ||
} | ||
} | ||
*/ |
Submodule ds-test
deleted from
e28215
Submodule forge-std
deleted from
f73c73
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
@openzeppelin-upgrades/=lib/eigenlayer-contracts/lib/openzeppelin-contracts-upgradeable/ | ||
@openzeppelin/=lib/eigenlayer-contracts/lib/openzeppelin-contracts/ | ||
ds-test/=lib/ds-test/src/ | ||
ds-test/=lib/eigenlayer-contracts/lib/ds-test/src/ | ||
eigenlayer-contracts/=lib/eigenlayer-contracts/ | ||
forge-std/=lib/forge-std/src/ | ||
forge-std/=lib/eigenlayer-contracts/lib/forge-std/src/ | ||
openzeppelin-contracts-upgradeable/=lib/eigenlayer-contracts/lib/openzeppelin-contracts-upgradeable/ | ||
openzeppelin-contracts/=lib/eigenlayer-contracts/lib/openzeppelin-contracts/ |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I noticed in the action trace you shared this command wasn't found