Skip to content

Commit

Permalink
Fix issue related to chain cloning (#564)
Browse files Browse the repository at this point in the history
* preliminary commit

* optimization bug fix

* add base block context

* fix cheatcode bugs, add prevrandao, and deprecate difficulty

* revert version number

* fix optimization mode bug

* update solc version in CI

* update solc version again

* fix context management

* fix context bug and improve logging while shrinking

* enable fork mode when --rpc-url is specified

* update documentation

* last update

* i genuinely hate prettier

* update docs
  • Loading branch information
anishnaik authored Feb 5, 2025
1 parent ea4cf89 commit 3cef3de
Show file tree
Hide file tree
Showing 32 changed files with 278 additions and 105 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ jobs:
- name: Install solc
run: |
solc-select use 0.8.17 --always-install
solc-select use 0.8.28 --always-install
- name: Test
run: go test ./...
Expand Down
3 changes: 2 additions & 1 deletion chain/block_context.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package chain

import (
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"math/big"
)

// newTestChainBlockContext obtains a new vm.BlockContext that is tailored to provide data from a TestChain.
Expand Down
26 changes: 17 additions & 9 deletions chain/standard_cheat_code_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,27 +172,35 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract,
},
)

// Difficulty: Updates difficulty
// TODO: Make changes to difficulty permanent and make it revert for post-Paris EVM versions
// Difficulty: Updates difficulty. Since we do not allow users to choose the fork that
// they are using (for now), and we are using a post-Paris fork, the difficulty cheatcode is a no-op.
contract.addMethod(
"difficulty", abi.Arguments{{Type: typeUint256}}, abi.Arguments{},
func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) {
// Maintain our changes until the transaction exits.
spoofedDifficulty := inputs[0].(*big.Int)
spoofedDifficultyHash := common.BigToHash(spoofedDifficulty)
return nil, nil
},
)

// Prevrandao: Updates random.
contract.addMethod(
"prevrandao", abi.Arguments{{Type: typeBytes32}}, abi.Arguments{},
func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) {
// Store our original random
originalRandom := tracer.chain.pendingBlockContext.Random

// In newer evm versions, block.difficulty uses opRandom instead of opDifficulty.
tracer.chain.pendingBlockContext.Random = &spoofedDifficultyHash
// Update the pending block context random
newRandom := inputs[0].([32]byte)
newRandomHash := common.BytesToHash(newRandom[:])
tracer.chain.pendingBlockContext.Random = &newRandomHash

// Restore the original random when top frame exits
tracer.CurrentCallFrame().onTopFrameExitRestoreHooks.Push(func() {
tracer.chain.pendingBlockContext.Random = originalRandom
})
return nil, nil
},
)

// TODO: Add prevrandao cheatcode

// Coinbase: Updates the block coinbase. Note that this _permanently_ updates the coinbase for the remainder of the
// chain's lifecycle
contract.addMethod(
Expand Down
61 changes: 39 additions & 22 deletions chain/test_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package chain
import (
"errors"
"fmt"
"math/big"

"github.com/crytic/medusa/chain/state"
"golang.org/x/net/context"
"math/big"

"github.com/crytic/medusa/chain/config"
"github.com/ethereum/go-ethereum/core/rawdb"
Expand Down Expand Up @@ -211,7 +212,6 @@ func newTestChainWithStateFactory(

// Convert our genesis block (go-ethereum type) to a test chain block.
testChainGenesisBlock := types.NewBlock(genesisBlock.Header())

// Create our state database over-top our database.
stateDatabase := gethState.NewDatabaseWithConfig(db, dbConfig)

Expand Down Expand Up @@ -288,8 +288,9 @@ func (t *TestChain) Clone(onCreateFunc func(chain *TestChain) error) (*TestChain
// did originally.
for i := 1; i < len(t.blocks); i++ {
// First create a new pending block to commit
blockHeader := t.blocks[i].Header
_, err = targetChain.PendingBlockCreateWithParameters(blockHeader.Number.Uint64(), blockHeader.Time, &blockHeader.GasLimit)
block := t.blocks[i]
blockHeader := block.Header
_, err = targetChain.PendingBlockCreateWithBaseBlockContext(block.BaseContext, &blockHeader.GasLimit)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -578,11 +579,10 @@ func (t *TestChain) PendingBlockCreate() (*types.Block, error) {
return t.PendingBlockCreateWithParameters(blockNumber, timestamp, nil)
}

// PendingBlockCreateWithParameters constructs an empty block which is pending addition to the chain, using the block
// properties provided. Note that there are no constraints on the next block number or timestamp. Because of cheatcode
// usage, the next block can go back in time.
// Returns the constructed block, or an error if one occurred.
func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTime uint64, blockGasLimit *uint64) (*types.Block, error) {
// PendingBlockCreateWithBaseBlockContext constructs an empty block which is pending addition to the chain, using the
// provided base block context. The base block context holds information such as the block number, timestamp, and base fee
// that should be used to initialize the block.
func (t *TestChain) PendingBlockCreateWithBaseBlockContext(baseBlockContext *types.BaseBlockContext, blockGasLimit *uint64) (*types.Block, error) {
// If we already have a pending block, return an error.
if t.pendingBlock != nil {
return nil, fmt.Errorf("could not create a new pending block for chain, as a block is already pending")
Expand All @@ -593,41 +593,43 @@ func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTi
blockGasLimit = &t.BlockGasLimit
}

// Obtain our parent block hash to reference in our new block.
parentBlockHash := t.Head().Hash

// Note we do not perform any block number or timestamp validation since cheatcodes can permanently update the
// block number or timestamp which could violate the invariants of a blockchain (e.g. block.number is strictly
// increasing)

// Obtain our parent block hash to reference in our new block.
parentBlockHash := t.Head().Hash

// Create a block header for this block:
// - State root hash reflects the state after applying block updates (no transactions, so unchanged from last block)
// - Other hashes will populate as we apply transactions
// - Bloom is aggregated for each transaction in the block (for now empty).
// - TODO: Difficulty should be revisited/checked.
// - GasUsed is aggregated for each transaction in the block (for now zero).
// - Mix digest is only useful for randomness, so we just fake randomness by using the previous block hash.
// - TODO: BaseFee should be revisited/checked.
// - We don't care too much about difficulty and mix digest so setting them to random things
header := &gethTypes.Header{
ParentHash: parentBlockHash,
UncleHash: gethTypes.EmptyUncleHash,
Coinbase: t.Head().Header.Coinbase,
Root: t.Head().Header.Root,
TxHash: gethTypes.EmptyRootHash,
ReceiptHash: gethTypes.EmptyRootHash,
Bloom: gethTypes.Bloom{},
Difficulty: common.Big0,
Number: big.NewInt(int64(blockNumber)),
GasLimit: *blockGasLimit,
GasUsed: 0,
Time: blockTime,
Extra: []byte{},
MixDigest: parentBlockHash,
Nonce: gethTypes.BlockNonce{},
BaseFee: new(big.Int).Set(t.Head().Header.BaseFee),
Coinbase: baseBlockContext.Coinbase,
Difficulty: common.Big0,
Number: new(big.Int).Set(baseBlockContext.Number),
Time: baseBlockContext.Time,
MixDigest: parentBlockHash,
BaseFee: new(big.Int).Set(baseBlockContext.BaseFee),
}

// Create a new block for our test node
// Create a new block for our test chain
t.pendingBlock = types.NewBlock(header)

// Set the block hash
// Note that this block hash may change if cheatcodes that update the block header are used (e.g. warp)
t.pendingBlock.Hash = t.pendingBlock.Header.Hash()

// Emit our event for the pending block being created
Expand All @@ -643,6 +645,21 @@ func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTi
return t.pendingBlock, nil
}

// PendingBlockCreateWithParameters constructs an empty block which is pending addition to the chain, using the block number
// and timestamp provided. Returns the constructed block, or an error if one occurred.
func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTime uint64, blockGasLimit *uint64) (*types.Block, error) {
// We will create a base block context with the provided parameters in addition to using the current head block.
// All values that are not the block number and timestamp are taken from the current head block.
baseBlockContext := types.NewBaseBlockContext(
blockNumber,
blockTime,
t.Head().Header.BaseFee,
t.Head().Header.Coinbase,
)

return t.PendingBlockCreateWithBaseBlockContext(baseBlockContext, blockGasLimit)
}

// PendingBlockAddTx takes a message (internal txs) and adds it to the current pending block, updating the header
// with relevant execution information. If a pending block was not created, an error is returned.
// Returns an error if one occurred.
Expand Down
33 changes: 33 additions & 0 deletions chain/types/base_block_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package types

import (
"math/big"

"github.com/ethereum/go-ethereum/common"
)

// BaseBlockContext stores block-level information (e.g. block.number or block.timestamp) when the block is first
// created. We need to store these values because cheatcodes like warp or roll will directly modify the block header.
// We use these values during the cloning process to ensure that execution semantics are maintained while still
// allowing the cheatcodes to function as expected. We could expand this struct to hold additional values
// (e.g. difficulty) but we will ere to add values only as necessary.
type BaseBlockContext struct {
// Number represents the block number of the block when it was first created.
Number *big.Int
// Time represents the timestamp of the block when it was first created.
Time uint64
// BaseFee represents the base fee of the block when it was first created.
BaseFee *big.Int
// Coinbase represents the coinbase of the block when it was first created.
Coinbase common.Address
}

// NewBaseBlockContext returns a new BaseBlockContext with the provided parameters.
func NewBaseBlockContext(number uint64, time uint64, baseFee *big.Int, coinbase common.Address) *BaseBlockContext {
return &BaseBlockContext{
Number: new(big.Int).SetUint64(number),
Time: time,
BaseFee: new(big.Int).Set(baseFee),
Coinbase: coinbase,
}
}
16 changes: 14 additions & 2 deletions chain/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import (

// Block represents a rudimentary block structure generated by sending messages to a test chain.
type Block struct {
// hash represents the block hash for this block.
// Hash represents the block hash for this block.
Hash common.Hash

// header represents the block header for this current block.
// Header represents the block header for this current block.
Header *types.Header

// Messages represent internal EVM core.Message objects. Messages are derived from transactions after validation
Expand All @@ -22,6 +22,12 @@ type Block struct {

// MessageResults represents the results recorded while executing transactions.
MessageResults []*MessageResults

// BaseContext stores the initial (base) block context before the execution of any transactions
// within the block. Since transactions that use cheatcodes can affect the block header
// permanently, we need to store the original values so that we can maintain execution
// semantics and allow for the chain to be clone-able.
BaseContext *BaseBlockContext
}

// NewBlock returns a new Block with the provided parameters.
Expand All @@ -32,6 +38,12 @@ func NewBlock(header *types.Header) *Block {
Header: header,
Messages: make([]*core.Message, 0),
MessageResults: make([]*MessageResults, 0),
BaseContext: NewBaseBlockContext(
header.Number.Uint64(),
header.Time,
header.BaseFee,
header.Coinbase,
),
}
return block
}
7 changes: 5 additions & 2 deletions cmd/fuzz_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config.
if err != nil {
return err
}

err = projectConfig.Compilation.SetTarget(newTarget)
if err != nil {
return err
Expand Down Expand Up @@ -226,10 +225,14 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config.

// Update RPC url
if cmd.Flags().Changed("rpc-url") {
projectConfig.Fuzzing.TestChainConfig.ForkConfig.RpcUrl, err = cmd.Flags().GetString("rpc-url")
rpcUrl, err := cmd.Flags().GetString("rpc-url")
if err != nil {
return err
}

// Enable on-chain fuzzing with the given URL
projectConfig.Fuzzing.TestChainConfig.ForkConfig.ForkModeEnabled = true
projectConfig.Fuzzing.TestChainConfig.ForkConfig.RpcUrl = rpcUrl
}

// Update RPC block
Expand Down
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package cmd

import (
"os"

"github.com/crytic/medusa/logging"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"os"
)

const version = "1.0.0"
Expand Down
2 changes: 1 addition & 1 deletion compilation/types/slither.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (s *SlitherConfig) validateArgs() error {
// getArgs returns the arguments to be provided to slither, or an error if one occurs.
// The slither target is provided as an input argument.
func (s *SlitherConfig) getArgs(target string) ([]string, error) {
// By default we do not re-compile, use the echidna printer, and output in json format
// By default, we do not re-compile, use the echidna printer, and output in json format
args := []string{target, "--ignore-compile", "--print", "echidna", "--json", "-"}

// Add remaining args
Expand Down
2 changes: 2 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Testing Configuration](project_configuration/testing_config.md)
- [Chain Configuration](project_configuration/chain_config.md)
- [Compilation Configuration](project_configuration/compilation_config.md)
- [Slither Configuration](project_configuration/slither_config.md)
- [Logging Configuration](project_configuration/logging_config.md)

# Command Line Interface (CLI)
Expand Down Expand Up @@ -43,6 +44,7 @@
- [roll](./cheatcodes/roll.md)
- [fee](./cheatcodes/fee.md)
- [difficulty](./cheatcodes/difficulty.md)
- [prevrandao](./cheatcodes/prevrandao.md)
- [chainId](./cheatcodes/chain_id.md)
- [store](./cheatcodes/store.md)
- [load](./cheatcodes/load.md)
Expand Down
5 changes: 4 additions & 1 deletion docs/src/cheatcodes/cheatcodes_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ interface StdCheats {
// Set block.basefee
function fee(uint256) external;
// Set block.difficulty and block.prevrandao
// Set block.difficulty (deprecated in `medusa`)
function difficulty(uint256) external;
// Set block.prevrandao
function prevrandao(bytes32) external;
// Set block.chainid
function chainId(uint256) external;
Expand Down
17 changes: 2 additions & 15 deletions docs/src/cheatcodes/difficulty.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,8 @@

## Description

The `difficulty` cheatcode will set the `block.difficulty` and the `block.prevrandao` value. At the moment, both values
are changed since the cheatcode does not check what EVM version is running.

Note that this behavior will change in the future.

## Example

```solidity
// Obtain our cheat code contract reference.
IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
// Change value and verify.
cheats.difficulty(x);
assert(block.difficulty == x);
```
The `difficulty` cheatcode has been deprecated in `medusa`. Since `medusa` uses a post-Paris EVM version, the cheatcode
will not update the `block.difficulty` and instead calling it will be a no-op.

## Function Signature

Expand Down
22 changes: 22 additions & 0 deletions docs/src/cheatcodes/prevrandao.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# `prevrandao`

## Description

The `prevrandao` cheatcode updates the `block.prevrandao`.

## Example

```solidity
// Obtain our cheat code contract reference.
IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
// Change value and verify.
cheats.prevrandao(bytes32(uint256(42)));
assert(block.prevrandao == 42);
```

## Function Signature

```solidity
function prevrandao(bytes32) external;
```
Loading

0 comments on commit 3cef3de

Please sign in to comment.