Skip to content

Commit

Permalink
feat: mechanics to capture conflations & replay them as test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
delehef authored and delehef committed Jan 24, 2024
1 parent 0e44a9f commit 2a32fb3
Show file tree
Hide file tree
Showing 22 changed files with 1,097 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright Consensys Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

package net.consensys.linea.blockcapture;

import java.nio.file.Path;
import java.util.List;

import com.google.gson.Gson;
import net.consensys.linea.blockcapture.reapers.Reaper;
import net.consensys.linea.zktracer.ZkBlockAwareOperationTracer;
import net.consensys.linea.zktracer.opcode.OpCode;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Transaction;
import org.hyperledger.besu.evm.account.Account;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.internal.Words;
import org.hyperledger.besu.evm.log.Log;
import org.hyperledger.besu.evm.worldstate.WorldUpdater;
import org.hyperledger.besu.evm.worldstate.WorldView;
import org.hyperledger.besu.plugin.data.BlockBody;
import org.hyperledger.besu.plugin.data.BlockHeader;

public class BlockCapturer implements ZkBlockAwareOperationTracer {
/**
* The {@link Reaper} will collect all the data that will need to be mimicked to replay the block.
*/
private final Reaper reaper = new Reaper();
/**
* This keeps a pointer to the initial state (i.e. ) to be used at the end of tracing to store the
* minimal required information to replay the conflation.
*/
private WorldUpdater worldUpdater;

/**
* Must be called **before** any tracing activity.
*
* @param worldUpdater the state of the world before the conflation is applied
*/
public void setWorld(WorldUpdater worldUpdater) {
this.worldUpdater = worldUpdater;
}

@Override
public void traceStartConflation(long numBlocksInConflation) {}

@Override
public void traceEndConflation() {}

@Override
public void traceStartBlock(BlockHeader blockHeader, BlockBody blockBody) {
this.reaper.enterBlock(blockHeader, blockBody);
}

@Override
public void traceStartTransaction(WorldView worldView, Transaction transaction) {
this.reaper.enterTransaction(transaction);
}

@Override
public void traceEndTransaction(
WorldView worldView,
Transaction tx,
boolean status,
Bytes output,
List<Log> logs,
long gasUsed,
long timeNs) {
this.reaper.exitTransaction(status);
}

/**
* This method only bothers with instruction putatively accessing the state as it was at the
* beginning of the conflation.
*
* @param frame the frame
*/
@Override
public void tracePreExecution(MessageFrame frame) {
final OpCode opCode = OpCode.of(frame.getCurrentOperation().getOpcode());

switch (opCode) {
// These access contracts potentially existing before the conflation played out.
case EXTCODESIZE, EXTCODECOPY, EXTCODEHASH -> {
if (frame.stackSize() > 0) {
final Address target = Words.toAddress(frame.getStackItem(0));
this.reaper.touchAddress(target);
}
}

// SLOAD may access storage cells whose value was set before the conflation execution.
case SLOAD -> {
if (frame.stackSize() > 0) {
final Account account = frame.getWorldUpdater().get(frame.getRecipientAddress());
final Address address = account.getAddress();
final UInt256 key = UInt256.fromBytes(frame.getStackItem(0));
this.reaper.touchStorage(address, key);
}
}

// These access contracts potentially existing before the conflation played out.
case CALL, CALLCODE, DELEGATECALL, STATICCALL -> {
if (frame.stackSize() > 1) {
final Address target = Words.toAddress(frame.getStackItem(1));
this.reaper.touchAddress(target);
}
}
}
}

public String toJson() {
Gson gson = new Gson();
return gson.toJson(this.reaper.collapse(this.worldUpdater));
}

/**
* Implementing this method here does not make sense as we only ever access its JSON result.
*
* @param filename
*/
@Override
public void writeToFile(Path filename) {
throw new UnsupportedOperationException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Consensys Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

package net.consensys.linea.blockcapture.reapers;

import java.util.ArrayDeque;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

import org.hyperledger.besu.datatypes.Address;

public class AddressReaper {
private final ArrayDeque<Set<Address>> reaped = new ArrayDeque<>();

public AddressReaper() {
// “Bedrock” address set for block-level gathering.
this.reaped.addLast(new HashSet<>());
}

public void enterTransaction() {
this.reaped.addLast(new HashSet<>());
}

public void exitTransaction(boolean success) {
if (!success) {
this.reaped.removeLast();
}
}

public void touch(final Address... addresses) {
for (Address address : addresses) {
this.reaped.peekLast().add(address);
}
}

public Set<Address> collapse() {
return this.reaped.stream().flatMap(Collection::stream).collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright Consensys Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

package net.consensys.linea.blockcapture.reapers;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.consensys.linea.blockcapture.snapshots.AccountSnapshot;
import net.consensys.linea.blockcapture.snapshots.BlockSnapshot;
import net.consensys.linea.blockcapture.snapshots.ConflationSnapshot;
import net.consensys.linea.blockcapture.snapshots.StorageSnapshot;
import org.apache.tuweni.units.bigints.UInt256;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Transaction;
import org.hyperledger.besu.evm.worldstate.WorldUpdater;
import org.hyperledger.besu.plugin.data.BlockBody;
import org.hyperledger.besu.plugin.data.BlockHeader;

/**
* The Reaper collect all the information from the state that will be accessed during the execution
* of a conflation.
*
* <p>This data can than be collapsed into a “replay” ({@link ConflationSnapshot}), i.e. the minimal
* required information to replay a conflation as if it were executed on the blockchain.
*/
public class Reaper {
/** Collect the reads from the state */
private final StateReaper state = new StateReaper();
/** Collect the addresses read from the state */
private final AddressReaper addresses = new AddressReaper();
/** Collect the blocks within a conflation */
private final List<BlockSnapshot> blocks = new ArrayList<>();

public void enterBlock(final BlockHeader header, final BlockBody body) {
this.blocks.add(
BlockSnapshot.of((org.hyperledger.besu.ethereum.core.BlockHeader) header, body));
this.addresses.touch(header.getCoinbase());
}

public void enterTransaction(Transaction tx) {
this.state.enterTransaction();
this.addresses.enterTransaction();

this.touchAddress(tx.getSender());
tx.getTo().ifPresent(this::touchAddress);
}

public void exitTransaction(boolean success) {
this.state.exitTransaction(success);
this.addresses.exitTransaction(success);
}

public void touchAddress(final Address address) {
this.addresses.touch(address);
}

public void touchStorage(final Address address, final UInt256 key) {
this.state.touch(address, key);
}

/**
* Uniquify and solidify the accumulated data, then return a {@link ConflationSnapshot}, which
* contains the smallest dataset required to exactly replay the conflation within a test framework
* without requiring access to the whole state.
*
* @param world the state before the conflation execution
* @return a minimal set of information required to replay the conflation within a test framework
*/
public ConflationSnapshot collapse(final WorldUpdater world) {
final List<AccountSnapshot> initialAccounts =
this.addresses.collapse().stream()
.flatMap(a -> AccountSnapshot.from(a, world).stream())
.toList();

final List<StorageSnapshot> initialStorage = new ArrayList<>();
for (Map.Entry<Address, Set<UInt256>> e : this.state.collapse().entrySet()) {
final Address address = e.getKey();

e.getValue().stream()
.flatMap(key -> StorageSnapshot.from(address, key, world).stream())
.forEach(initialStorage::add);
}

return new ConflationSnapshot(this.blocks, initialAccounts, initialStorage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright Consensys Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

package net.consensys.linea.blockcapture.reapers;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.tuweni.units.bigints.UInt256;
import org.hyperledger.besu.datatypes.Address;

/**
* This object gathers all non-reversed accesses to storage values during the execution of a
* conflation, then collapse them into a single mapping of the initial values in these slots.
*/
public class StateReaper {
private final Deque<HashMap<Address, Set<UInt256>>> transientStates = new ArrayDeque<>();

public void enterTransaction() {
this.transientStates.addLast(new HashMap<>());
}

public void exitTransaction(boolean success) {
if (!success) {
this.transientStates.removeLast();
}
}

public void touch(final Address address, final UInt256 key) {
this.transientStates.peekLast().computeIfAbsent(address, k -> new HashSet<>()).add(key);
}

public Map<Address, Set<UInt256>> collapse() {
final Map<Address, Set<UInt256>> r = new HashMap<>();

for (var txEntry : this.transientStates) {
for (Map.Entry<Address, Set<UInt256>> addressKeys : txEntry.entrySet()) {
final Address address = addressKeys.getKey();

r.computeIfAbsent(address, k -> new HashSet<>()).addAll(addressKeys.getValue());
}
}

return r;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Consensys Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

package net.consensys.linea.blockcapture.snapshots;

import java.util.List;

import org.hyperledger.besu.datatypes.AccessListEntry;

public record AccessListEntrySnapshot(String address, List<String> storageKeys) {
public static AccessListEntrySnapshot from(AccessListEntry e) {
return new AccessListEntrySnapshot(e.getAddressString(), e.getStorageKeysString());
}
}
Loading

0 comments on commit 2a32fb3

Please sign in to comment.