Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ public int spillage(Trace trace) {
private final Rom rom = new Rom(romLex);
private final RlpTxn rlpTxn;
private final Mmio mmio;
private final TxnData<? extends TxnDataOperation> txnData = setTxnData();
@Getter final TxnData<? extends TxnDataOperation> txnData = setTxnData();
private final RlpTxnRcpt rlpTxnRcpt = new RlpTxnRcpt();
private final LogInfo logInfo = new LogInfo(rlpTxnRcpt);
private final LogData logData = new LogData(rlpTxnRcpt);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import java.math.BigInteger;

import lombok.Getter;
import net.consensys.linea.zktracer.Fork;
import net.consensys.linea.zktracer.module.txndata.cancun.CancunTxnData;
import net.consensys.linea.zktracer.module.txndata.cancun.CancunTxnDataOperation;
Expand All @@ -43,6 +44,13 @@ public class UserTransaction extends CancunTxnDataOperation {
public final ProcessableBlockHeader blockHeader;
public final Fork fork;

public enum DominantCost {
FLOOR_COST_DOMINATES,
EXECUTION_COST_DOMINATES
}

@Getter private DominantCost dominantCost;

public UserTransaction(
final CancunTxnData txnData, final TransactionProcessingMetadata txnMetadata) {
super(txnData, USER);
Expand Down Expand Up @@ -223,6 +231,10 @@ private void comparingEffectiveRefundToFloorCostComputationRow(long consumedGasA
Bytes.ofUnsignedLong(txn.getFloorCostPrague()));

rows.add(comparingEffectiveRefundsVsFloorCost);
dominantCost =
comparingEffectiveRefundsVsFloorCost.result()
? DominantCost.FLOOR_COST_DOMINATES
: DominantCost.EXECUTION_COST_DOMINATES;
}

private void detectingEmptyPayloadComputationRow() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright ConsenSys 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.zktracer.forkSpecific.prague.floorprice;

import static net.consensys.linea.zktracer.Fork.isPostPrague;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

import com.google.common.base.Preconditions;
import net.consensys.linea.UnitTestWatcher;
import net.consensys.linea.reporting.TracerTestBase;
import net.consensys.linea.testing.BytecodeCompiler;
import net.consensys.linea.testing.BytecodeRunner;
import net.consensys.linea.zktracer.module.txndata.cancun.transactions.UserTransaction;
import net.consensys.linea.zktracer.opcode.OpCode;
import org.apache.tuweni.bytes.Bytes;
import org.hyperledger.besu.datatypes.AccessListEntry;
import org.hyperledger.besu.datatypes.Address;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

@ExtendWith(UnitTestWatcher.class)
public class NontrivialExecutionTests extends TracerTestBase {

static final Bytes callData = Bytes.fromHexString("0000");

/**
* The 'to' address has byte code which can be padded with low-cost opcodes. This allows us to aim
* for the threshold where the floor price is overtaken by the execution cost.
*/
@ParameterizedTest
@MethodSource("adjustableByteCodeTestSource")
void adjustableByteCodeTest(
Bytes byteCode,
boolean provideAccessList,
UserTransaction.DominantCost dominantCostPrediction,
TestInfo testInfo) {
BytecodeRunner bytecodeRunner = BytecodeRunner.of(byteCode);
AccessListEntry accessListEntry =
new AccessListEntry(Address.fromHexString("0xABCD"), List.of());
List<AccessListEntry> accessList = provideAccessList ? List.of(accessListEntry) : List.of();
bytecodeRunner.run(callData, accessList, chainConfig, testInfo);
// Test blocks contain 4 transactions: 2 system transactions, 1 user transaction (the one we
// created) and 1 noop transaction.
if (isPostPrague(fork)) {
UserTransaction userTransaction =
(UserTransaction) bytecodeRunner.getHub().txnData().operations().get(2);
Preconditions.checkArgument(userTransaction.getDominantCost() == dominantCostPrediction);
}
}

static Stream<Arguments> adjustableByteCodeTestSource() {
List<Arguments> arguments = new ArrayList<>();

arguments.add(
Arguments.of(
buildProgram(
TransactionCategory.MESSAGE_CALL,
UserTransaction.DominantCost.FLOOR_COST_DOMINATES),
false,
UserTransaction.DominantCost.FLOOR_COST_DOMINATES));
arguments.add(
Arguments.of(
buildProgram(
TransactionCategory.MESSAGE_CALL,
UserTransaction.DominantCost.EXECUTION_COST_DOMINATES),
false,
UserTransaction.DominantCost.EXECUTION_COST_DOMINATES));

return arguments.stream();
}

/**
* The 'init code' is a sequence of JUMPDEST. Every byte contributes
*
* <ul>
* <li>40 to the floor cost
* <li>16 to the upfront gas cost
* <li>1 to the execution cost
* </ul>
*
* Furthermore, every word of the init code contributes 2 to the upfront gas cost.
*
* <p>The threshold is given by the following equation (ics = init_code_size):
*
* <pre><b>21_000 + 40*ics > 21_000 + 32_000 + 16*ics + 2*⌈ics/32⌉ + 1*ics</b></pre>
*
* which puts the threshold somewhere around 1390-1400 bytes. This allows us to aim for the
* threshold where the floor price is overtaken by the execution cost.
*/
@ParameterizedTest
@MethodSource("adjustableInitCodeTestSource")
void adjustableInitCodeTest(
Bytes initCode, UserTransaction.DominantCost dominantCostPrediction, TestInfo testInfo) {
BytecodeRunner bytecodeRunner = BytecodeRunner.of(initCode);
bytecodeRunner.runInitCode(chainConfig, testInfo);
// Test blocks contain 4 transactions: 2 system transactions, 1 user transaction (the one we
// created) and 1 noop transaction.
if (isPostPrague(fork)) {
UserTransaction userTransaction =
(UserTransaction) bytecodeRunner.getHub().txnData().operations().get(2);
Preconditions.checkArgument(userTransaction.getDominantCost() == dominantCostPrediction);
}
}

static Stream<Arguments> adjustableInitCodeTestSource() {
List<Arguments> arguments = new ArrayList<>();

arguments.add(
Arguments.of(
buildProgram(
TransactionCategory.DEPLOYMENT, UserTransaction.DominantCost.FLOOR_COST_DOMINATES),
UserTransaction.DominantCost.FLOOR_COST_DOMINATES));
arguments.add(
Arguments.of(
buildProgram(
TransactionCategory.DEPLOYMENT,
UserTransaction.DominantCost.EXECUTION_COST_DOMINATES),
UserTransaction.DominantCost.EXECUTION_COST_DOMINATES));

return arguments.stream();
}

// Support enums and methods
enum TransactionCategory {
DEPLOYMENT,
MESSAGE_CALL
}

private static Bytes buildProgram(
TransactionCategory transactionCategory, UserTransaction.DominantCost dominantCost) {
return switch (transactionCategory) {
case MESSAGE_CALL -> {
/**
* Given the {@link callData} the floor cost is 21_000 + 2*10 and the execution cost is
* 21_000 + 2*4 + 1*codesize. The threshold is thus reached when floor cost > execution cost
* i.e., 21_000 + 2*10 > 21_000 + 2*4 + 1*codesize that is codesize < 2*(10-4) = 12
*/
BytecodeCompiler program = BytecodeCompiler.newProgram(chainConfig);
program.op(
OpCode.JUMPDEST,
dominantCost == UserTransaction.DominantCost.EXECUTION_COST_DOMINATES ? 12 : 11);
yield program.compile();
}
case DEPLOYMENT -> {
/**
* Given the {@link callData} the floor cost is 21_000 + init_code_size*40 (all bytes will
* be nonzero) and the execution cost is 21_000 + 32_000 + 2*4 + 2*1 + code_execution_cost.
*/
BytecodeCompiler program = BytecodeCompiler.newProgram(chainConfig);
program.op(
OpCode.JUMPDEST,
dominantCost == UserTransaction.DominantCost.EXECUTION_COST_DOMINATES ? 1395 : 1396);
yield program.compile();
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright ConsenSys 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.zktracer.forkSpecific.prague.floorprice;

import static net.consensys.linea.testing.BytecodeRunner.DEFAULT_GAS_LIMIT;
import static net.consensys.linea.zktracer.Fork.isPostPrague;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

import com.google.common.base.Preconditions;
import net.consensys.linea.UnitTestWatcher;
import net.consensys.linea.reporting.TracerTestBase;
import net.consensys.linea.testing.BytecodeCompiler;
import net.consensys.linea.testing.ToyAccount;
import net.consensys.linea.testing.ToyExecutionEnvironmentV2;
import net.consensys.linea.testing.ToyTransaction;
import net.consensys.linea.testing.TransactionProcessingResultValidator;
import net.consensys.linea.zktracer.module.txndata.cancun.transactions.UserTransaction;
import net.consensys.linea.zktracer.module.txndata.cancun.transactions.UserTransaction.DominantCost;
import net.consensys.linea.zktracer.opcode.OpCode;
import net.consensys.linea.zktracer.types.AddressUtils;
import org.apache.tuweni.bytes.Bytes;
import org.hyperledger.besu.crypto.KeyPair;
import org.hyperledger.besu.crypto.SECP256K1;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

@ExtendWith(UnitTestWatcher.class)
public class RefundTests extends TracerTestBase {

final Bytes runtimeCode =
BytecodeCompiler.newProgram(chainConfig)
.push(0) // value
.push(1) // key
.op(OpCode.SSTORE) // reset storage slot 1 to 0
.op(OpCode.JUMPDEST, 10)
.compile(); // We assume this is at most 32 bytes

final Bytes initCode =
BytecodeCompiler.newProgram(chainConfig)
.push(0xff) // value
.push(1) // key
.op(OpCode.SSTORE) // the contract is initialised with a storage slot 1 set to 0xff
.push(runtimeCode) // value
.push(0) // offset
.op(OpCode.MSTORE)
.push(runtimeCode.size()) // size
.push(32 - runtimeCode.size()) // offset
.op(OpCode.RETURN)
.compile();

@ParameterizedTest
@MethodSource("refundTestSource")
void refundTest(Bytes callData, DominantCost dominantCostPrediction, TestInfo testInfo) {
final KeyPair keyPair = new SECP256K1().generateKeyPair();
final Address senderAddress =
Address.extract(Hash.hash(keyPair.getPublicKey().getEncodedBytes()));

final ToyAccount senderAccount =
ToyAccount.builder().balance(Wei.fromEth(1)).nonce(0).address(senderAddress).build();

// Deploy contract with slot 1 set to 1
final Transaction deploymentTransaction =
ToyTransaction.builder()
.payload(initCode)
.gasLimit(DEFAULT_GAS_LIMIT)
.sender(senderAccount)
.value(Wei.of(272)) // 256 + 16, easier for debugging
.keyPair(keyPair)
.gasPrice(Wei.of(8))
.build();

// Compute the address of the deployed contract
final Address deploymentAddress = AddressUtils.effectiveToAddress(deploymentTransaction);

// Call the deployment contract which resets slot 1 to 0
final Transaction callTransaction =
ToyTransaction.builder()
.payload(callData)
.gasLimit(DEFAULT_GAS_LIMIT)
.sender(senderAccount)
.toAddress(deploymentAddress)
.value(Wei.ZERO)
.keyPair(keyPair)
.gasPrice(Wei.of(8))
.nonce(senderAccount.getNonce() + 1)
.build();

ToyExecutionEnvironmentV2 toyExecutionEnvironmentV2 =
ToyExecutionEnvironmentV2.builder(chainConfig, testInfo)
.accounts(List.of(senderAccount))
.transactions(List.of(deploymentTransaction, callTransaction))
.transactionProcessingResultValidator(
TransactionProcessingResultValidator.EMPTY_VALIDATOR)
.build();
toyExecutionEnvironmentV2.run();

// Test blocks contain 5 transactions: 2 system transactions, 2 user transaction (the deployment
// transaction and the one we
// created) and 1 noop transaction.
if (isPostPrague(fork)) {
UserTransaction userTransaction =
(UserTransaction) toyExecutionEnvironmentV2.getHub().txnData().operations().get(3);
Preconditions.checkArgument(userTransaction.getDominantCost() == dominantCostPrediction);
}
}

static Stream<Arguments> refundTestSource() {
/*
SSTORE cost: 2_100 + 2_900 = 5_000
execution cost: 21_000 + 3 + 3 + 5_000 + 10*1 + 16*cds = 26_016 + 16*cds
refund: 4800
refund bound: execution cost / 5 ≥ 5_203 + 5*cds > 4_800
we get full refund

execution cost after refunds: 26_016 + 16*cds - 4_800 = 21_216 + 16*cds

floor price: 21_000 + 40*cds

threshold: 21_000 + 40*cds > 21_216 + 16*cds <=> 24*cds > 216 <=> cds > 9
*/
List<Arguments> arguments = new ArrayList<>();
arguments.add(
Arguments.of(Bytes.fromHexString("11".repeat(9)), DominantCost.EXECUTION_COST_DOMINATES));
arguments.add(
Arguments.of(Bytes.fromHexString("11".repeat(10)), DominantCost.FLOOR_COST_DOMINATES));
return arguments.stream();
}
}
Loading
Loading