diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/instructionprocessing/callTests/sixtyThreeSixtyFourths/SixtyThreeSixtyFourthsTests.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/instructionprocessing/callTests/sixtyThreeSixtyFourths/SixtyThreeSixtyFourthsTests.java new file mode 100644 index 0000000000..c5adaea02e --- /dev/null +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/instructionprocessing/callTests/sixtyThreeSixtyFourths/SixtyThreeSixtyFourthsTests.java @@ -0,0 +1,404 @@ +/* + * 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.zktracer.instructionprocessing.callTests.sixtyThreeSixtyFourths; + +import static com.google.common.base.Preconditions.checkArgument; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.GAS_CONST_G_CALL_VALUE; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.GAS_CONST_G_NEW_ACCOUNT; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.GAS_CONST_G_WARM_ACCESS; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PRC_BLAKE2F_SIZE; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.WORD_SIZE; +import static net.consensys.linea.zktracer.module.hub.signals.TracedException.OUT_OF_GAS_EXCEPTION; +import static net.consensys.linea.zktracer.opcode.OpCode.CALL; +import static net.consensys.linea.zktracer.opcode.OpCode.MLOAD; +import static net.consensys.linea.zktracer.opcode.OpCode.POP; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.generateModexpInput; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.getBLAKE2FCost; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.getECADDCost; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.getMODEXPCost; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.getPrecompileCost; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.prepareBlake2F; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.prepareModexp; +import static org.hyperledger.besu.datatypes.Address.ALTBN128_ADD; +import static org.hyperledger.besu.datatypes.Address.ALTBN128_MUL; +import static org.hyperledger.besu.datatypes.Address.ALTBN128_PAIRING; +import static org.hyperledger.besu.datatypes.Address.BLAKE2B_F_COMPRESSION; +import static org.hyperledger.besu.datatypes.Address.ECREC; +import static org.hyperledger.besu.datatypes.Address.ID; +import static org.hyperledger.besu.datatypes.Address.MODEXP; +import static org.hyperledger.besu.datatypes.Address.RIPEMD160; +import static org.hyperledger.besu.datatypes.Address.SHA256; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import net.consensys.linea.testing.BytecodeCompiler; +import net.consensys.linea.testing.BytecodeRunner; +import net.consensys.linea.testing.ToyAccount; +import net.consensys.linea.zktracer.module.constants.GlobalConstants; +import net.consensys.linea.zktracer.module.oob.OobOperation; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/* + * 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 + */ + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class SixtyThreeSixtyFourthsTests { + + /* + Cases to cover: + + * value = 0 + If precompileGasCost >= 2300 then we are interested in: + * value = 1, targetAddressExists = false + * value = 1, targetAddressExists = true + + Note: BLAKE2F and MODEXP requires a non-zero non-trivial input to have a cost greater than 2300. + Other precompiles only require a proper call data size at most. + + See https://github.com/Consensys/linea-tracer/issues/1153 for additional documentation. + */ + + final Bytes INFINITE_GAS = Bytes.fromHexString("ff".repeat(32)); + + // BLAKE2F specific parameters + final int rLeadingByte = 0x09; + final int r = rLeadingByte << 8; + + // MODEXP specific parameters + final int bbs = 2; + final int ebs = 6; + final int mbs = 128; + final Bytes modexpInput = generateModexpInput(bbs, mbs, ebs); + final int exponentLog = + OobOperation.computeExponentLog(modexpInput, 96 + bbs + ebs + mbs, bbs, ebs); + final Address codeOwnerAddress = Address.fromHexString("0xC0DE"); + // codeOwnerAccount owns the bytecode that will be given as input to MODEXP through EXTCODECOPY + final ToyAccount codeOwnerAccount = + ToyAccount.builder() + .balance(Wei.of(0)) + .nonce(1) + .address(codeOwnerAddress) + .code(modexpInput) + .build(); + final List additionalAccounts = List.of(codeOwnerAccount); + + // Cost of preCallProgram in different scenarios: + // (address, transfersValue) -> gasCost + final Map> preCallProgramGasMap = + Stream.of( + ECREC, + SHA256, + RIPEMD160, + ID, + MODEXP, + ALTBN128_ADD, + ALTBN128_MUL, + ALTBN128_PAIRING, + BLAKE2B_F_COMPRESSION) + .collect( + Collectors.toMap( + address -> address, + address -> + new HashMap<>() { + { + put( + false, + BytecodeRunner.of(preCallProgram(address, false, false, 0)) + .runOnlyForGasCost( + address == MODEXP ? additionalAccounts : List.of())); + put( + true, + BytecodeRunner.of(preCallProgram(address, false, true, 0)) + .runOnlyForGasCost( + address == MODEXP ? additionalAccounts : List.of())); + } + })); + + // Note: transferValue = false and cds = 0 as we are interested only in the cost of the + // corresponding PUSHes here + + /** + * Parameterized test for the ECADD precompile, that has a fixed cost of 150. + * + * @param gasLimit the gas limit for the transaction. It is either as much as needed for the ECADD + * call or slightly less. + * @param insufficientGasForPrecompileExpected flag indicating if insufficient gas for ECADD is + * expected. + */ + @ParameterizedTest + @MethodSource("fixedCostEcAddTestSource") + void fixedCostEcAddTest(long gasLimit, boolean insufficientGasForPrecompileExpected) { + // Whenever transferValue = true, gas is enough + // so we only test the case in which transferValue = false + + final BytecodeCompiler program = BytecodeCompiler.newProgram(); + + program.immediate(preCallProgram(ALTBN128_ADD, false, false, 0)).op(CALL); + + final BytecodeRunner bytecodeRunner = BytecodeRunner.of(program); + bytecodeRunner.run(gasLimit); + + assertNotEquals( + OUT_OF_GAS_EXCEPTION, + bytecodeRunner.getHub().previousTraceSection().commonValues.tracedException()); + + final boolean insufficientGasForPrecompileActual = + bytecodeRunner.getHub().oob().operations().stream() + .anyMatch(OobOperation::isInsufficientGasForPrecompile); + + assertEquals(insufficientGasForPrecompileExpected, insufficientGasForPrecompileActual); + } + + Stream fixedCostEcAddTestSource() { + List arguments = new ArrayList<>(); + final long targetCalleeGas = getECADDCost(); + for (int cornerCase : List.of(0, -1)) { + final long gasLimit = + getGasLimit( + targetCalleeGas + cornerCase, + false, + false, + preCallProgramGasMap.get(ALTBN128_ADD).get(false)); + arguments.add(Arguments.of(gasLimit, cornerCase == -1)); + } + return arguments.stream(); + } + + /** + * Parameterized test for precompile calls where the cost is greater than or equal to the stipend + * (every precompile except ECADD, as long as cds and inputs are properly selected). + * + * @param address the address of the precompile contract. + * @param gasLimit the gas limit for the transaction. It is either as much as needed for the + * precompile call or slightly less. + * @param insufficientGasForPrecompileExpected flag indicating if insufficient gas for precompile + * call is expected. + * @param transfersValue flag indicating if the call to the precompile transfers value. + * @param targetAddressExists flag indicating if the precompile target address exists at the + * moment of the final call. + * @param cds the call data size. + */ + @ParameterizedTest + @MethodSource("costGEQStipendTest") + void costGEQStipendTest( + Address address, + long gasLimit, + boolean insufficientGasForPrecompileExpected, + boolean transfersValue, + boolean targetAddressExists, + int cds) { + final BytecodeCompiler program = BytecodeCompiler.newProgram(); + program.immediate(preCallProgram(address, transfersValue, targetAddressExists, cds)).op(CALL); + + final BytecodeRunner bytecodeRunner = BytecodeRunner.of(program); + bytecodeRunner.run(gasLimit, address == MODEXP ? additionalAccounts : List.of()); + + assertNotEquals( + OUT_OF_GAS_EXCEPTION, + bytecodeRunner.getHub().previousTraceSection().commonValues.tracedException()); + + final boolean insufficientGasForPrecompileActual = + bytecodeRunner.getHub().oob().operations().stream() + .anyMatch(OobOperation::isInsufficientGasForPrecompile); + + assertEquals(insufficientGasForPrecompileExpected, insufficientGasForPrecompileActual); + } + + Stream costGEQStipendTest() { + List arguments = new ArrayList<>(); + for (Address address : + List.of( + ECREC, + SHA256, + RIPEMD160, + ID, + MODEXP, + ALTBN128_MUL, + ALTBN128_PAIRING, + BLAKE2B_F_COMPRESSION)) { + final int cds = getCallDataSize(address); + final long targetCalleeGas = + address == BLAKE2B_F_COMPRESSION + ? getBLAKE2FCost(r) + : address == MODEXP + ? getMODEXPCost(bbs, mbs, exponentLog) + : getPrecompileCost(address, cds); + for (int cornerCase : List.of(0, -1)) { + for (boolean transfersValue : List.of(true, false)) { + for (boolean targetAddressExists : List.of(true, false)) { + if (!transfersValue && targetAddressExists) { + continue; // no need to test this case + } + final long gasLimit = + getGasLimit( + targetCalleeGas + cornerCase, + transfersValue, + targetAddressExists, + preCallProgramGasMap.get(address).get(targetAddressExists)); + arguments.add( + Arguments.of( + address, gasLimit, cornerCase == -1, transfersValue, targetAddressExists, cds)); + } + } + } + } + return arguments.stream(); + } + + // Support methods + Bytes preCallProgram( + Address address, boolean transfersValue, boolean targetAddressExists, int cds) { + return BytecodeCompiler.newProgram() + .immediate(expandMemoryTo2048Words()) + .immediate(targetAddressExists ? successfullySummonIntoExistence(address) : Bytes.EMPTY) + .immediate( + address == MODEXP ? prepareModexp(modexpInput, 0, codeOwnerAddress) : Bytes.EMPTY) + .immediate(address == BLAKE2B_F_COMPRESSION ? prepareBlake2F(rLeadingByte, 2) : Bytes.EMPTY) + .immediate(pushCallArguments(INFINITE_GAS, address, cds, transfersValue)) + .compile(); + } + + Bytes expandMemoryTo2048Words() { + return expandMemoryTo(2048); + } + + Bytes expandMemoryTo(int words) { + checkArgument(words >= 1); + return BytecodeCompiler.newProgram().push((words - 1) * WORD_SIZE).op(MLOAD).op(POP).compile(); + } + + Bytes successfullySummonIntoExistence(Address address) { + return call( + INFINITE_GAS, + address, + address == BLAKE2B_F_COMPRESSION + ? PRC_BLAKE2F_SIZE + : 0, // For BLAKE2F we need a meaningful cds for the call to succeed + true); + } + + Bytes call(Bytes gas, Address address, int cds, boolean transfersValue) { + return BytecodeCompiler.newProgram() + .immediate(pushCallArguments(gas, address, cds, transfersValue)) + .op(CALL) + .compile(); + } + + Bytes pushCallArguments(Bytes gas, Address address, int cds, boolean transfersValue) { + return BytecodeCompiler.newProgram() + .push(0) // returnAtCapacity + .push(0) // returnAtOffset + .push(cds) // callDataSize + .push(0) // callDataOffset + .push(transfersValue ? 1 : 0) // value + .push(address) // address + .push(gas) // gas + .compile(); + } + + /** + * Calculates the gas limit for a transaction to a contract calling a callee (in this class, a + * precompile), based on targetCallGas, transfersValue, targetAddressExists, and + * preCallProgramGas. + * + * @param targetCalleeGas the gas given to the callee. + * @param transfersValue flag indicating if the call to callee transfers value. + * @param targetAddressExists flag indicating if the target address exists. + * @param preCallProgramGas the gas cost of the preCallProgram. + * @return the calculated gas limit for the transaction. + */ + long getGasLimit( + long targetCalleeGas, + boolean transfersValue, + boolean targetAddressExists, + long preCallProgramGas) { + /* gasLimit = preCallProgramGasCost + gasPreCall + / let x = gasPreCall - gasUpFront = 64 * k + l + / with: + / x !≡ 63 % 64 => (x - 63) % 64 != 0 + / k = floor(x / 64) + / l = x % 64 + / 63 * k + l + stipend = targetCalleeGas + / find gasLimit going backwards + */ + final long stipend = transfersValue ? GlobalConstants.GAS_CONST_G_CALL_STIPEND : 0; + checkArgument(targetCalleeGas >= stipend); + final long l = (targetCalleeGas - stipend) % 63; + final long k = (targetCalleeGas - stipend - l) / 63; + checkArgument(targetCalleeGas == 63 * k + l + stipend); + final long gasUpfront = getUpfrontGasCost(transfersValue, targetAddressExists); + final long gasPreCall = 64 * k + l + gasUpfront; + return preCallProgramGas + gasPreCall; // gasLimit + } + + /** + * Calculates the upfront gas cost of a CALL-type instruction based on whether it transfers value + * and if the target address exists. It assumed target address is warm and there is no memory + * expansion. + * + * @param transfersValue flag indicating if the call transfers value. + * @param targetAddressExists flag indicating if the target address exists. + * @return the upfront gas cost for the call. + */ + long getUpfrontGasCost(boolean transfersValue, boolean targetAddressExists) { + // GAS_CONST_G_WARM_ACCESS = 100 + // GAS_CONST_G_COLD_ACCOUNT_ACCESS = 2600 + // GAS_CONST_G_CALL_VALUE = 9000 + // GAS_CONST_G_NEW_ACCOUNT = 25000 + return GAS_CONST_G_WARM_ACCESS + + (transfersValue + ? GAS_CONST_G_CALL_VALUE + (targetAddressExists ? 0 : GAS_CONST_G_NEW_ACCOUNT) + : 0); + } + + int getCallDataSize(Address address) { + if (address == SHA256 || address == RIPEMD160 || address == ID) { + return 1024 * WORD_SIZE; // Ensures cost is greater than stipend + } else if (address == MODEXP) { + return 96 + bbs + ebs + mbs; // Ensures cost is greater than stipend with non-zero non-trivial + } else if (address == BLAKE2B_F_COMPRESSION) { + return PRC_BLAKE2F_SIZE; // Ensures cost is greater than stipend with non-zero non-trivial + // input + } else { + return 0; + } + } +} diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/precompiles/LowGasStipendPrecompileCallTests.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/precompiles/LowGasStipendPrecompileCallTests.java index f684235a26..0950434a6a 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/precompiles/LowGasStipendPrecompileCallTests.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/precompiles/LowGasStipendPrecompileCallTests.java @@ -15,12 +15,16 @@ package net.consensys.linea.zktracer.precompiles; -import static net.consensys.linea.zktracer.instructionprocessing.callTests.Utilities.populateMemory; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.GAS_CONST_G_CALL_STIPEND; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PRC_BLAKE2F_SIZE; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PRC_ECPAIRING_SIZE; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.WORD_SIZE; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.generateModexpInput; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.getExpectedReturnAtCapacity; import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.getPrecompileCost; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.prepareBlake2F; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.prepareModexp; +import static net.consensys.linea.zktracer.precompiles.PrecompileUtils.prepareSha256Ripemd160Id; import static org.hyperledger.besu.datatypes.Address.ALTBN128_ADD; import static org.hyperledger.besu.datatypes.Address.ALTBN128_MUL; import static org.hyperledger.besu.datatypes.Address.ALTBN128_PAIRING; @@ -47,7 +51,6 @@ import net.consensys.linea.zktracer.module.oob.OobOperation; import net.consensys.linea.zktracer.opcode.OpCode; import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Wei; import org.junit.jupiter.params.ParameterizedTest; @@ -83,12 +86,12 @@ enum GasCase { int r = 0; // MODEXP specific parameters - int bbs = 0; - int ebs = 0; - int mbs = 0; - List additionalAccounts = new ArrayList<>(); - Address codeOwnerAddress = Address.fromHexString("0xC0DE"); - int exponentLog = 0; + int bbs; // Defined within the test case + int ebs; // Defined within the test case + int mbs; // Defined within the test case + int exponentLog; // Computed within the test case + final Address codeOwnerAddress = Address.fromHexString("0xC0DE"); + List additionalAccounts = new ArrayList<>(); // Re-assigned within the test case /** * Parameterized test for low gas stipend precompile call. In this family of tests we call every @@ -131,7 +134,7 @@ void lowGasStipendPrecompileCallTest( rLeadingByte = modexpCostGT200OrBlake2fRoundsGT0 ? 0x12 : 0; r = rLeadingByte << 8; callDataSize = PRC_BLAKE2F_SIZE; - prepareBlake2F(program, rLeadingByte, callDataOffset); + prepareBlake2F(program, rLeadingByte, callDataOffset + 2); } else if (precompileAddress == ALTBN128_PAIRING) { callDataSize = PRC_ECPAIRING_SIZE; } else if ((precompileAddress == SHA256 @@ -145,7 +148,21 @@ void lowGasStipendPrecompileCallTest( ebs = modexpCostGT200OrBlake2fRoundsGT0 ? 6 : 3; mbs = modexpCostGT200OrBlake2fRoundsGT0 ? 25 : 4; callDataSize = 96 + bbs + ebs + mbs; - prepareModexp(bbs, mbs, ebs, callDataOffset, callDataSize, program); + + final Bytes modexpInput = generateModexpInput(bbs, mbs, ebs); + exponentLog = OobOperation.computeExponentLog(modexpInput, callDataSize, bbs, ebs); + // codeOwnerAccount owns the bytecode that will be given as input to MODEXP through + // EXTCODECOPY + final ToyAccount codeOwnerAccount = + ToyAccount.builder() + .balance(Wei.of(0)) + .nonce(1) + .address(codeOwnerAddress) + .code(modexpInput) + .build(); + additionalAccounts = List.of(codeOwnerAccount); + + prepareModexp(program, modexpInput, callDataOffset, codeOwnerAddress); } else { // ECADD, ECMUL, ECRECOVER cases callDataSize = 1; // This is an arbitrary value @@ -188,7 +205,7 @@ void lowGasStipendPrecompileCallTest( .push(gas) // gas .op(OpCode.CALL); final BytecodeRunner bytecodeRunner = BytecodeRunner.of(program); - bytecodeRunner.run(61_000_000L, additionalAccounts); + bytecodeRunner.run(61_000_000L, precompileAddress == MODEXP ? additionalAccounts : List.of()); final Hub hub = bytecodeRunner.getHub(); // Here we check if OOB detects the insufficient gas for the precompile call @@ -245,82 +262,6 @@ static Stream lowGasStipendPrecompileCallTestSource() { } // Support methods - private void prepareBlake2F(BytecodeCompiler program, int rLeadingByte, int callDataOffset) { - program - .push(rLeadingByte) // For simplicity, we only set the first byte of r - .push(callDataOffset + 2) // offset - // Writing rLeadingByte at this offset - // allows to have r = 0x00000000 or r = 0x00001200 - .op(OpCode.MSTORE8); - } - - private void prepareSha256Ripemd160Id(BytecodeCompiler program, int nWords, int callDataOffset) { - populateMemory(program, nWords, callDataOffset); - } - - private void prepareModexp( - int bbs, int mbs, int ebs, int targetOffset, int callDataSize, BytecodeCompiler program) { - final Bytes32 bbsPadded = Bytes32.leftPad(Bytes.of(bbs)); - final Bytes32 ebsPadded = Bytes32.leftPad(Bytes.of(ebs)); - final Bytes32 mbsPadded = Bytes32.leftPad(Bytes.of(mbs)); - final Bytes bem = - Bytes.fromHexString("0x" + "aa".repeat(bbs) + "ff".repeat(ebs) + "bb".repeat(mbs)); - final Bytes modexpInput = Bytes.concatenate(bbsPadded, ebsPadded, mbsPadded, bem); - - // codeOwnerAccount owns the bytecode that will be given as input to MODEXP through EXTCODECOPY - final ToyAccount codeOwnerAccount = - ToyAccount.builder() - .balance(Wei.of(0)) - .nonce(1) - .address(codeOwnerAddress) - .code(modexpInput) - .build(); - additionalAccounts = List.of(codeOwnerAccount); - - // This is computed here for convenience, and it is used for pricing the MODEXP precompile - exponentLog = Math.max(OobOperation.computeExponentLog(modexpInput, callDataSize, bbs, ebs), 1); - - // Copy to targetOffset the code of codeOwnerAccount - program - .push(codeOwnerAddress) - .op(OpCode.EXTCODESIZE) // size - .push(0) // offset - .push(targetOffset) // targetOffset - .push(codeOwnerAddress) // address - .op(OpCode.EXTCODECOPY); - } - - /** - * Computes the expected returnAtCapacity based on the precompile address, and callDataSize in the - * case of ID. - * - * @param precompileAddress the address of the precompile contract. - * @param callDataSize the call data size. Beyond the case of ID, this value is ignored. - * @param mbs the modulo byte size. Beyond the case of MODEXP, this value is ignored. - * @return the computed return at capacity. - */ - private static int getExpectedReturnAtCapacity( - Address precompileAddress, int callDataSize, int mbs) { - final int returnAtCapacity; - if (precompileAddress == ECREC - || precompileAddress == SHA256 - || precompileAddress == RIPEMD160 - || precompileAddress == ALTBN128_PAIRING) { - returnAtCapacity = WORD_SIZE; - } else if (precompileAddress == ALTBN128_ADD - || precompileAddress == ALTBN128_MUL - || precompileAddress == BLAKE2B_F_COMPRESSION) { - returnAtCapacity = 2 * WORD_SIZE; - } else if (precompileAddress == MODEXP) { - returnAtCapacity = mbs; - } else if (precompileAddress == ID) { - returnAtCapacity = callDataSize; - } else { - throw new IllegalArgumentException("Unknown precompile address"); - } - return returnAtCapacity; - } - /** * Computes the gas based on the gas parameter and precompile cost. * diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/precompiles/PrecompileUtils.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/precompiles/PrecompileUtils.java index f72953168d..d81f9d3bb5 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/precompiles/PrecompileUtils.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/precompiles/PrecompileUtils.java @@ -15,6 +15,7 @@ package net.consensys.linea.zktracer.precompiles; +import static net.consensys.linea.zktracer.instructionprocessing.callTests.Utilities.populateMemory; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.GAS_CONST_BLAKE2_PER_ROUND; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.GAS_CONST_ECADD; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.GAS_CONST_ECMUL; @@ -34,6 +35,10 @@ import static net.consensys.linea.zktracer.module.oob.Trace.G_QUADDIVISOR; import static org.hyperledger.besu.datatypes.Address.*; +import net.consensys.linea.testing.BytecodeCompiler; +import net.consensys.linea.zktracer.opcode.OpCode; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; import org.hyperledger.besu.datatypes.Address; public class PrecompileUtils { @@ -80,45 +85,135 @@ public static int getPrecompileCost( } } - private static int getECRECCost() { + public static int getPrecompileCost(Address precompileAddress, int cds) { + return getPrecompileCost(precompileAddress, cds, 0, 0, 0, 0); + } + + private static int words(int sizeInBytes) { + return (sizeInBytes + WORD_SIZE_MO) / WORD_SIZE; + } + + public static int getECRECCost() { return GAS_CONST_ECRECOVER; } - private static int getSHA256Cost(int cds) { - final int words = (cds + WORD_SIZE_MO) / WORD_SIZE; - return GAS_CONST_SHA2 + words * GAS_CONST_SHA2_WORD; + public static int getSHA256Cost(int cds) { + return GAS_CONST_SHA2 + words(cds) * GAS_CONST_SHA2_WORD; } - private static int getRIPEMD160Cost(int cds) { - final int words = (cds + WORD_SIZE_MO) / WORD_SIZE; - return GAS_CONST_RIPEMD + words * GAS_CONST_RIPEMD_WORD; + public static int getRIPEMD160Cost(int cds) { + return GAS_CONST_RIPEMD + words(cds) * GAS_CONST_RIPEMD_WORD; } - private static int getIDCost(int cds) { - final int words = (cds + WORD_SIZE_MO) / WORD_SIZE; - return GAS_CONST_IDENTITY + words * GAS_CONST_IDENTITY_WORD; + public static int getIDCost(int cds) { + return GAS_CONST_IDENTITY + words(cds) * GAS_CONST_IDENTITY_WORD; } - static int getMODEXPCost(int bbs, int mbs, int exponentLog) { + public static int getMODEXPCost(int bbs, int mbs, int exponentLog) { final int fOfMax = ((Math.max(bbs, mbs) + 7) / 8) * ((Math.max(bbs, mbs) + 7) / 8); final int bigNumerator = fOfMax * Math.max(exponentLog, 1); final int bigQuotient = bigNumerator / G_QUADDIVISOR; return Math.max(GAS_CONST_MODEXP, bigQuotient); } - private static int getECADDCost() { + public static int getECADDCost() { return GAS_CONST_ECADD; } - private static int getECMULCost() { + public static int getECMULCost() { return GAS_CONST_ECMUL; } - private static int getECPAIRINGCost(int cds) { + public static int getECPAIRINGCost(int cds) { return GAS_CONST_ECPAIRING + GAS_CONST_ECPAIRING_PAIR * (cds / PRC_ECPAIRING_SIZE); } - private static int getBLAKE2FCost(int r) { + public static int getBLAKE2FCost(int r) { return GAS_CONST_BLAKE2_PER_ROUND * r; } + + // Methods to prepare inputs for certain precompiles + + // BLAKE2F + static void prepareBlake2F(BytecodeCompiler program, int rLeadingByte, int offset) { + program + .push(rLeadingByte) // For simplicity, we only set the first byte of r + .push(offset) // offset + .op(OpCode.MSTORE8); + } + + public static Bytes prepareBlake2F(int rLeadingByte, int offset) { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + prepareBlake2F(program, rLeadingByte, offset); + return program.compile(); + } + + // SHA256, RIPEMD160, ID + static void prepareSha256Ripemd160Id(BytecodeCompiler program, int nWords, int offset) { + populateMemory(program, nWords, offset); + } + + static Bytes prepareSha256Ripemd160Id(int nWords, int offset) { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + prepareSha256Ripemd160Id(program, nWords, offset); + return program.compile(); + } + + // MODEXP + public static Bytes generateModexpInput(int bbs, int mbs, int ebs) { + final Bytes32 bbsPadded = Bytes32.leftPad(Bytes.of(bbs)); + final Bytes32 ebsPadded = Bytes32.leftPad(Bytes.of(ebs)); + final Bytes32 mbsPadded = Bytes32.leftPad(Bytes.of(mbs)); + final Bytes bem = + Bytes.fromHexString("0x" + "aa".repeat(bbs) + "ff".repeat(ebs) + "bb".repeat(mbs)); + return Bytes.concatenate(bbsPadded, ebsPadded, mbsPadded, bem); + } + + static void prepareModexp( + BytecodeCompiler program, Bytes modexpInput, int targetOffset, Address codeOwnerAddress) { + // Copy to targetOffset the code of codeOwnerAccount + program + .push(codeOwnerAddress) + .op(OpCode.EXTCODESIZE) // size + .push(0) // offset + .push(targetOffset) // targetOffset + .push(codeOwnerAddress) // address + .op(OpCode.EXTCODECOPY); + } + + public static Bytes prepareModexp(Bytes modexpInput, int targetOffset, Address codeOwnerAddress) { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + prepareModexp(program, modexpInput, targetOffset, codeOwnerAddress); + return program.compile(); + } + + /** + * Computes the expected returnAtCapacity based on the precompile address, and callDataSize in the + * case of ID. + * + * @param precompileAddress the address of the precompile contract. + * @param callDataSize the call data size. Beyond the case of ID, this value is ignored. + * @param mbs the modulo byte size. Beyond the case of MODEXP, this value is ignored. + * @return the computed return at capacity. + */ + static int getExpectedReturnAtCapacity(Address precompileAddress, int callDataSize, int mbs) { + final int returnAtCapacity; + if (precompileAddress == ECREC + || precompileAddress == SHA256 + || precompileAddress == RIPEMD160 + || precompileAddress == ALTBN128_PAIRING) { + returnAtCapacity = WORD_SIZE; + } else if (precompileAddress == ALTBN128_ADD + || precompileAddress == ALTBN128_MUL + || precompileAddress == BLAKE2B_F_COMPRESSION) { + returnAtCapacity = 2 * WORD_SIZE; + } else if (precompileAddress == MODEXP) { + returnAtCapacity = mbs; + } else if (precompileAddress == ID) { + returnAtCapacity = callDataSize; + } else { + throw new IllegalArgumentException("Unknown precompile address"); + } + return returnAtCapacity; + } }