From f571822b0b8bc32013ca2a7cac7ce9c1dba45251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Bojarski?= <54240434+letypequividelespoubelles@users.noreply.github.com> Date: Wed, 22 May 2024 16:37:15 +0530 Subject: [PATCH 01/39] feat(rlpaddr): add keccak result selector column (#724) * feat(rlpaddr): add keccak result selector column * fix: constraints update --- .../module/constants/GlobalConstants.java | 1 + .../zktracer/module/rlpaddr/RlpAddr.java | 43 ++++++++++++----- .../linea/zktracer/module/rlpaddr/Trace.java | 48 ++++++++++++++----- zkevm-constraints | 2 +- 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/constants/GlobalConstants.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/constants/GlobalConstants.java index 8b56c9680a..e5b1d43f3f 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/constants/GlobalConstants.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/constants/GlobalConstants.java @@ -58,6 +58,7 @@ public class GlobalConstants { public static final int EVM_INST_COINBASE = 0x41; public static final int EVM_INST_CREATE = 0xf0; public static final int EVM_INST_CREATE2 = 0xf5; + public static final int CREATE2_SHIFT = 0xff; public static final int EVM_INST_DELEGATECALL = 0xf4; public static final int EVM_INST_DIFFICULTY = 0x44; public static final int EVM_INST_DIV = 0x4; diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlpaddr/RlpAddr.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlpaddr/RlpAddr.java index 31abd0f93e..b822269bab 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlpaddr/RlpAddr.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlpaddr/RlpAddr.java @@ -41,6 +41,7 @@ import net.consensys.linea.zktracer.ColumnHeader; import net.consensys.linea.zktracer.container.stacked.list.StackedList; import net.consensys.linea.zktracer.module.Module; +import net.consensys.linea.zktracer.module.constants.GlobalConstants; import net.consensys.linea.zktracer.module.hub.Hub; import net.consensys.linea.zktracer.module.rlputils.ByteCountAndPowerOutput; import net.consensys.linea.zktracer.module.trm.Trm; @@ -56,7 +57,7 @@ @RequiredArgsConstructor public class RlpAddr implements Module { - private static final Bytes CREATE2_SHIFT = Bytes.minimalBytes(Trace.CREATE2_SHIFT); + private static final Bytes CREATE2_SHIFT = Bytes.minimalBytes(GlobalConstants.CREATE2_SHIFT); private static final Bytes INT_SHORT = Bytes.ofUnsignedShort(RLP_PREFIX_INT_SHORT); private static final UnsignedByte BYTES_LLARGE = UnsignedByte.of(LLARGE); @@ -156,15 +157,28 @@ private void traceCreate2(int stamp, RlpAddrChunk chunk, Trace trace) { case 0 -> { trace.limb( rightPadTo(Bytes.concatenate(CREATE2_SHIFT, chunk.address().slice(0, 4)), LLARGE)); - trace.nBytes(UnsignedByte.of(5)); + trace.nBytes(UnsignedByte.of(5)).selectorKeccakRes(true); } - case 1 -> trace.limb(chunk.address().slice(4, LLARGE)).nBytes(BYTES_LLARGE); - case 2 -> trace.limb(chunk.salt().orElseThrow().slice(0, LLARGE)).nBytes(BYTES_LLARGE); - case 3 -> trace.limb(chunk.salt().orElseThrow().slice(LLARGE, LLARGE)).nBytes(BYTES_LLARGE); - case 4 -> trace.limb(chunk.keccak().orElseThrow().slice(0, LLARGE)).nBytes(BYTES_LLARGE); + case 1 -> trace + .limb(chunk.address().slice(4, LLARGE)) + .nBytes(BYTES_LLARGE) + .selectorKeccakRes(false); + case 2 -> trace + .limb(chunk.salt().orElseThrow().slice(0, LLARGE)) + .nBytes(BYTES_LLARGE) + .selectorKeccakRes(false); + case 3 -> trace + .limb(chunk.salt().orElseThrow().slice(LLARGE, LLARGE)) + .nBytes(BYTES_LLARGE) + .selectorKeccakRes(false); + case 4 -> trace + .limb(chunk.keccak().orElseThrow().slice(0, LLARGE)) + .nBytes(BYTES_LLARGE) + .selectorKeccakRes(false); case 5 -> trace .limb(chunk.keccak().orElseThrow().slice(LLARGE, LLARGE)) - .nBytes(BYTES_LLARGE); + .nBytes(BYTES_LLARGE) + .selectorKeccakRes(false); } // Columns unused for Recipe2 @@ -256,7 +270,8 @@ private void traceCreate(int stamp, RlpAddrChunk chunk, Trace trace) { .lc(false) .limb(Bytes.EMPTY) .nBytes(UnsignedByte.ZERO) - .index(UnsignedByte.ZERO); + .index(UnsignedByte.ZERO) + .selectorKeccakRes(ct == 0); case 4 -> trace .lc(true) .limb( @@ -267,7 +282,8 @@ private void traceCreate(int stamp, RlpAddrChunk chunk, Trace trace) { .add(BigInteger.valueOf(size_rlp_nonce))), LLARGE)) .nBytes(UnsignedByte.of(1)) - .index(UnsignedByte.ZERO); + .index(UnsignedByte.ZERO) + .selectorKeccakRes(false); case 5 -> trace .lc(true) .limb( @@ -276,17 +292,20 @@ private void traceCreate(int stamp, RlpAddrChunk chunk, Trace trace) { bigIntegerToBytes(BigInteger.valueOf(148)), chunk.address().slice(0, 4)), LLARGE)) .nBytes(UnsignedByte.of(5)) - .index(UnsignedByte.of(1)); + .index(UnsignedByte.of(1)) + .selectorKeccakRes(false); case 6 -> trace .lc(true) .limb(chunk.address().slice(4, LLARGE)) .nBytes(UnsignedByte.of(LLARGE)) - .index(UnsignedByte.of(2)); + .index(UnsignedByte.of(2)) + .selectorKeccakRes(false); case 7 -> trace .lc(true) .limb(rightPadTo(rlpNonce, LLARGE)) .nBytes(UnsignedByte.of(size_rlp_nonce)) - .index(UnsignedByte.of(3)); + .index(UnsignedByte.of(3)) + .selectorKeccakRes(false); } // Column not used fo recipe 1: diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlpaddr/Trace.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlpaddr/Trace.java index 1bf5b7a279..c94da5178b 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlpaddr/Trace.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlpaddr/Trace.java @@ -30,7 +30,6 @@ * Please DO NOT ATTEMPT TO MODIFY this code directly. */ public class Trace { - public static final int CREATE2_SHIFT = 0xff; public static final int MAX_CT_CREATE = 0x7; public static final int MAX_CT_CREATE2 = 0x5; @@ -61,6 +60,7 @@ public class Trace { private final MappedByteBuffer recipe2; private final MappedByteBuffer saltHi; private final MappedByteBuffer saltLo; + private final MappedByteBuffer selectorKeccakRes; private final MappedByteBuffer stamp; private final MappedByteBuffer tinyNonZeroNonce; @@ -90,6 +90,7 @@ static List headers(int length) { new ColumnHeader("rlpaddr.RECIPE_2", 1, length), new ColumnHeader("rlpaddr.SALT_HI", 32, length), new ColumnHeader("rlpaddr.SALT_LO", 32, length), + new ColumnHeader("rlpaddr.SELECTOR_KECCAK_RES", 1, length), new ColumnHeader("rlpaddr.STAMP", 4, length), new ColumnHeader("rlpaddr.TINY_NON_ZERO_NONCE", 1, length)); } @@ -119,8 +120,9 @@ public Trace(List buffers) { this.recipe2 = buffers.get(21); this.saltHi = buffers.get(22); this.saltLo = buffers.get(23); - this.stamp = buffers.get(24); - this.tinyNonZeroNonce = buffers.get(25); + this.selectorKeccakRes = buffers.get(24); + this.stamp = buffers.get(25); + this.tinyNonZeroNonce = buffers.get(26); } public int size() { @@ -336,10 +338,10 @@ public Trace limb(final Bytes b) { } public Trace nBytes(final UnsignedByte b) { - if (filled.get(25)) { + if (filled.get(26)) { throw new IllegalStateException("rlpaddr.nBYTES already set"); } else { - filled.set(25); + filled.set(26); } nBytes.put(b.toByte()); @@ -463,23 +465,35 @@ public Trace saltLo(final Bytes b) { return this; } - public Trace stamp(final int b) { + public Trace selectorKeccakRes(final Boolean b) { if (filled.get(23)) { - throw new IllegalStateException("rlpaddr.STAMP already set"); + throw new IllegalStateException("rlpaddr.SELECTOR_KECCAK_RES already set"); } else { filled.set(23); } + selectorKeccakRes.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace stamp(final int b) { + if (filled.get(24)) { + throw new IllegalStateException("rlpaddr.STAMP already set"); + } else { + filled.set(24); + } + stamp.putInt(b); return this; } public Trace tinyNonZeroNonce(final Boolean b) { - if (filled.get(24)) { + if (filled.get(25)) { throw new IllegalStateException("rlpaddr.TINY_NON_ZERO_NONCE already set"); } else { - filled.set(24); + filled.set(25); } tinyNonZeroNonce.put((byte) (b ? 1 : 0)); @@ -548,7 +562,7 @@ public Trace validateRow() { throw new IllegalStateException("rlpaddr.LIMB has not been filled"); } - if (!filled.get(25)) { + if (!filled.get(26)) { throw new IllegalStateException("rlpaddr.nBYTES has not been filled"); } @@ -585,10 +599,14 @@ public Trace validateRow() { } if (!filled.get(23)) { - throw new IllegalStateException("rlpaddr.STAMP has not been filled"); + throw new IllegalStateException("rlpaddr.SELECTOR_KECCAK_RES has not been filled"); } if (!filled.get(24)) { + throw new IllegalStateException("rlpaddr.STAMP has not been filled"); + } + + if (!filled.get(25)) { throw new IllegalStateException("rlpaddr.TINY_NON_ZERO_NONCE has not been filled"); } @@ -659,7 +677,7 @@ public Trace fillAndValidateRow() { limb.position(limb.position() + 32); } - if (!filled.get(25)) { + if (!filled.get(26)) { nBytes.position(nBytes.position() + 1); } @@ -696,10 +714,14 @@ public Trace fillAndValidateRow() { } if (!filled.get(23)) { - stamp.position(stamp.position() + 4); + selectorKeccakRes.position(selectorKeccakRes.position() + 1); } if (!filled.get(24)) { + stamp.position(stamp.position() + 4); + } + + if (!filled.get(25)) { tinyNonZeroNonce.position(tinyNonZeroNonce.position() + 1); } diff --git a/zkevm-constraints b/zkevm-constraints index e201957fa2..04f4804aa8 160000 --- a/zkevm-constraints +++ b/zkevm-constraints @@ -1 +1 @@ -Subproject commit e201957fa29b518739a02a4958e1ccee5f5f1764 +Subproject commit 04f4804aa8da843cfc910286b3759398803e2e35 From 3e777a453f4dc60f8ddb512631d52600284aaeea Mon Sep 17 00:00:00 2001 From: Lorenzo Gentile Date: Fri, 24 May 2024 19:13:49 +0200 Subject: [PATCH 02/39] fix(shfreftable): turned MSHP into an UnsignedByte (#726) --- .../consensys/linea/zktracer/module/tables/shf/ShfRt.java | 2 +- .../consensys/linea/zktracer/module/tables/shf/Trace.java | 8 ++++---- zkevm-constraints | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/tables/shf/ShfRt.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/tables/shf/ShfRt.java index e4606627b5..bd7bc1fbae 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/tables/shf/ShfRt.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/tables/shf/ShfRt.java @@ -53,7 +53,7 @@ public void commit(List buffers) { trace .byte1(UnsignedByte.of(a)) .las(unsignedByteA.shiftLeft(8 - uShp)) - .mshp((short) uShp) + .mshp(UnsignedByte.of(uShp)) .rap(unsignedByteA.shiftRight(uShp)) .ones(UnsignedByte.of((Bytes.fromHexString("0xFF").shiftRight(uShp)).not().toInt())) .iomf(true) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/tables/shf/Trace.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/tables/shf/Trace.java index a3de92152a..41b3513a03 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/tables/shf/Trace.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/tables/shf/Trace.java @@ -45,7 +45,7 @@ static List headers(int length) { new ColumnHeader("shfreftable.BYTE1", 1, length), new ColumnHeader("shfreftable.IOMF", 1, length), new ColumnHeader("shfreftable.LAS", 1, length), - new ColumnHeader("shfreftable.MSHP", 2, length), + new ColumnHeader("shfreftable.MSHP", 1, length), new ColumnHeader("shfreftable.ONES", 1, length), new ColumnHeader("shfreftable.RAP", 1, length)); } @@ -103,14 +103,14 @@ public Trace las(final UnsignedByte b) { return this; } - public Trace mshp(final short b) { + public Trace mshp(final UnsignedByte b) { if (filled.get(3)) { throw new IllegalStateException("shfreftable.MSHP already set"); } else { filled.set(3); } - mshp.putShort(b); + mshp.put(b.toByte()); return this; } @@ -184,7 +184,7 @@ public Trace fillAndValidateRow() { } if (!filled.get(3)) { - mshp.position(mshp.position() + 2); + mshp.position(mshp.position() + 1); } if (!filled.get(4)) { diff --git a/zkevm-constraints b/zkevm-constraints index 04f4804aa8..6ef21aecd2 160000 --- a/zkevm-constraints +++ b/zkevm-constraints @@ -1 +1 @@ -Subproject commit 04f4804aa8da843cfc910286b3759398803e2e35 +Subproject commit 6ef21aecd28e078b99d9425c00aa3556be55bf6c From 2d3510e97e5610662dea0f84511a5860b4493450 Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Mon, 27 May 2024 07:26:01 +0300 Subject: [PATCH 03/39] feat(shakira-data): implement SHAKIRA_DATA module (#655) * feat(shakira-data): add initial implementation * feat: connect ShakiraData module to RipeMD and SHA256 precompile calls * feat: refine implementation according to the spec * feat(shakira): connect to keccak call and fix inconsistencies in the implementation * feat(shakira): improve keccak triggering and fix issues * feat(shakira): change phase flag mechanism * feat(shakiradata): wip * feat(shakiradata): reimplement module * fix: cleaning + from Stack to Deque * fix: typo * style: cleaning * style(ecpairing): make it more logical (OB's comment) * fix(trace-gradle): rebase issue for txndata * feat: no memory allocation * feat(shakira): needs two padding row * feat(shakira): ass result selector for shakira * feat(shakira): stack arg in consistent order --------- Co-authored-by: Francois Bojarski --- .../container/stacked/set/StackedSet.java | 2 +- .../Blake2fModexpDataOperation.java | 20 +- .../linea/zktracer/module/hub/Hub.java | 25 +- .../hub/precompiles/PrecompileInvocation.java | 6 +- .../hub/transients/OperationAncillaries.java | 29 +- .../linea/zktracer/module/limits/Keccak.java | 71 ++- .../limits/precompiles/Blake2fRounds.java | 90 ++-- .../precompiles/EcAddEffectiveCall.java | 12 +- .../precompiles/EcMulEffectiveCall.java | 13 +- .../EcPairingCallEffectiveCall.java | 87 ++-- .../precompiles/EcRecoverEffectiveCall.java | 32 +- .../precompiles/ModexpEffectiveCall.java | 4 +- ...Rip160Blocks.java => RipeMd160Blocks.java} | 106 ++-- .../limits/precompiles/Sha256Blocks.java | 98 ++-- .../module/shakiradata/ShakiraData.java | 112 ++++ .../shakiradata/ShakiraDataComparator.java | 25 + .../shakiradata/ShakiraDataOperation.java | 167 ++++++ .../shakiradata/ShakiraPrecompileType.java | 22 + .../zktracer/module/shakiradata/Trace.java | 491 ++++++++++++++++++ .../linea/zktracer/module/wcp/Wcp.java | 6 +- gradle/trace-files.gradle | 2 +- zkevm-constraints | 2 +- 22 files changed, 1145 insertions(+), 277 deletions(-) rename arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/{Rip160Blocks.java => RipeMd160Blocks.java} (51%) create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraData.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraDataComparator.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraDataOperation.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraPrecompileType.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/Trace.java diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/container/stacked/set/StackedSet.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/container/stacked/set/StackedSet.java index 5fb0ea2689..01e72d1bd9 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/container/stacked/set/StackedSet.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/container/stacked/set/StackedSet.java @@ -37,7 +37,7 @@ * @param the type of elements stored in the set */ public class StackedSet implements StackedContainer, java.util.Set { - private final Deque> sets = new ArrayDeque<>(); + public final Deque> sets = new ArrayDeque<>(); private final Map occurrences = new HashMap<>(); @Override diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpDataOperation.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpDataOperation.java index f87c648d99..8961d50559 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpDataOperation.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpDataOperation.java @@ -57,14 +57,20 @@ public class Blake2fModexpDataOperation extends ModuleOperation { private static final Map PHASE_INFO_MAP = Map.of( - PHASE_MODEXP_BASE, new PhaseInfo(PHASE_MODEXP_BASE, Trace.INDEX_MAX_MODEXP_BASE), + PHASE_MODEXP_BASE, + new PhaseInfo(PHASE_MODEXP_BASE, Trace.INDEX_MAX_MODEXP_BASE), PHASE_MODEXP_EXPONENT, - new PhaseInfo(PHASE_MODEXP_EXPONENT, Trace.INDEX_MAX_MODEXP_EXPONENT), - PHASE_MODEXP_MODULUS, new PhaseInfo(PHASE_MODEXP_MODULUS, Trace.INDEX_MAX_MODEXP_MODULUS), - PHASE_MODEXP_RESULT, new PhaseInfo(PHASE_MODEXP_RESULT, Trace.INDEX_MAX_MODEXP_RESULT), - PHASE_BLAKE_DATA, new PhaseInfo(PHASE_BLAKE_DATA, Trace.INDEX_MAX_BLAKE_DATA), - PHASE_BLAKE_PARAMS, new PhaseInfo(PHASE_BLAKE_PARAMS, Trace.INDEX_MAX_BLAKE_PARAMS), - PHASE_BLAKE_RESULT, new PhaseInfo(PHASE_BLAKE_RESULT, Trace.INDEX_MAX_BLAKE_RESULT)); + new PhaseInfo(PHASE_MODEXP_EXPONENT, Trace.INDEX_MAX_MODEXP_EXPONENT), + PHASE_MODEXP_MODULUS, + new PhaseInfo(PHASE_MODEXP_MODULUS, Trace.INDEX_MAX_MODEXP_MODULUS), + PHASE_MODEXP_RESULT, + new PhaseInfo(PHASE_MODEXP_RESULT, Trace.INDEX_MAX_MODEXP_RESULT), + PHASE_BLAKE_DATA, + new PhaseInfo(PHASE_BLAKE_DATA, Trace.INDEX_MAX_BLAKE_DATA), + PHASE_BLAKE_PARAMS, + new PhaseInfo(PHASE_BLAKE_PARAMS, Trace.INDEX_MAX_BLAKE_PARAMS), + PHASE_BLAKE_RESULT, + new PhaseInfo(PHASE_BLAKE_RESULT, Trace.INDEX_MAX_BLAKE_RESULT)); @EqualsAndHashCode.Include public final long hubStampPlusOne; @Getter private long prevHubStamp; diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java index 7a894db706..ebd22d7e4b 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java @@ -61,7 +61,7 @@ import net.consensys.linea.zktracer.module.limits.precompiles.EcPairingMillerLoop; import net.consensys.linea.zktracer.module.limits.precompiles.EcRecoverEffectiveCall; import net.consensys.linea.zktracer.module.limits.precompiles.ModexpEffectiveCall; -import net.consensys.linea.zktracer.module.limits.precompiles.Rip160Blocks; +import net.consensys.linea.zktracer.module.limits.precompiles.RipeMd160Blocks; import net.consensys.linea.zktracer.module.limits.precompiles.Sha256Blocks; import net.consensys.linea.zktracer.module.logdata.LogData; import net.consensys.linea.zktracer.module.loginfo.LogInfo; @@ -75,6 +75,7 @@ import net.consensys.linea.zktracer.module.rlptxrcpt.RlpTxrcpt; import net.consensys.linea.zktracer.module.rom.Rom; import net.consensys.linea.zktracer.module.romlex.RomLex; +import net.consensys.linea.zktracer.module.shakiradata.ShakiraData; import net.consensys.linea.zktracer.module.shf.Shf; import net.consensys.linea.zktracer.module.stp.Stp; import net.consensys.linea.zktracer.module.tables.bin.BinRt; @@ -223,13 +224,16 @@ public void addTraceSection(TraceSection section) { @Getter private final RomLex romLex; private final TxnData txnData; - + private final ShakiraData shakiraData = new ShakiraData(this.wcp); private final ModexpEffectiveCall modexpEffectiveCall; private final Stp stp = new Stp(this, wcp, mod); private final L2Block l2Block; private final List modules; - /* Those modules are not traced, we just compute the number of calls to those precompile to meet the prover limits */ + /* + * Those modules are not traced, we just compute the number of calls to those + * precompile to meet the prover limits + */ private final List precompileLimitModules; private final List refTableModules; @@ -270,9 +274,9 @@ public Hub(final Address l2l1ContractAddress, final Bytes l2l1Topic) { this.precompileLimitModules = List.of( - new Sha256Blocks(this), + new Sha256Blocks(this, shakiraData), ecRec, - new Rip160Blocks(this), + new RipeMd160Blocks(this, shakiraData), this.modexpEffectiveCall, new EcAddEffectiveCall(this), new EcMulEffectiveCall(this), @@ -281,7 +285,7 @@ public Hub(final Address l2l1ContractAddress, final Bytes l2l1Topic) { new Blake2fRounds(this, this.blake2fModexpData), // Block level limits l2Block, - new Keccak(this, ecRec, l2Block), + new Keccak(this, ecRec, l2Block, shakiraData), new L2L1Logs(l2Block)); this.refTableModules = List.of(new BinRt(), new InstructionDecoder(), new ShfRt()); @@ -309,6 +313,7 @@ public Hub(final Address l2l1ContractAddress, final Bytes l2l1Topic) { this.rlpTxn, this.rom, this.romLex, + this.shakiraData, this.shf, this.stp, this.trm, @@ -333,7 +338,7 @@ public List getModulesToTrace() { this.blake2fModexpData, this.blockdata, this.blockhash, - // this.ecData, // TODO: not yet + // this.ecData, // TODO: not yet this.ext, this.euc, this.exp, @@ -349,6 +354,7 @@ public List getModulesToTrace() { this.rlpTxrcpt, this.rom, this.romLex, + this.shakiraData, this.shf, this.stp, this.trm, @@ -860,7 +866,8 @@ public void traceContextEnter(MessageFrame frame) { final boolean shouldCopyTxCallData = !isDeployment && !frame.getInputData().isEmpty() && currentTx.requiresEvmExecution(); - // TODO simplify this, the same bedRock context ( = root context ??) seems to be generated in + // TODO simplify this, the same bedRock context ( = root context ??) seems to be + // generated in // both case if (shouldCopyTxCallData) { this.callStack.newMantleAndBedrock( @@ -885,7 +892,7 @@ public void traceContextEnter(MessageFrame frame) { } else { this.callStack.newBedrock( this.state.stamps().hub(), - // this.transients.tx().transaction().getSender(), + // this.transients.tx().transaction().getSender(), toAddress, CallFrameType.BEDROCK, new Bytecode( diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/precompiles/PrecompileInvocation.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/precompiles/PrecompileInvocation.java index abdcbc39be..5fc3eb272d 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/precompiles/PrecompileInvocation.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/precompiles/PrecompileInvocation.java @@ -24,7 +24,7 @@ import net.consensys.linea.zktracer.module.limits.precompiles.EcPairingCallEffectiveCall; import net.consensys.linea.zktracer.module.limits.precompiles.EcRecoverEffectiveCall; import net.consensys.linea.zktracer.module.limits.precompiles.ModexpEffectiveCall; -import net.consensys.linea.zktracer.module.limits.precompiles.Rip160Blocks; +import net.consensys.linea.zktracer.module.limits.precompiles.RipeMd160Blocks; import net.consensys.linea.zktracer.module.limits.precompiles.Sha256Blocks; import net.consensys.linea.zktracer.types.MemorySpan; import net.consensys.linea.zktracer.types.Precompile; @@ -77,7 +77,7 @@ public static PrecompileInvocation of(final Hub hub, Precompile p) { switch (p) { case EC_RECOVER -> !EcRecoverEffectiveCall.hasEnoughGas(hub); case SHA2_256 -> !Sha256Blocks.hasEnoughGas(hub); - case RIPEMD_160 -> !Rip160Blocks.hasEnoughGas(hub); + case RIPEMD_160 -> !RipeMd160Blocks.hasEnoughGas(hub); case IDENTITY -> switch (hub.opCode()) { case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { final Address target = Words.toAddress(hub.messageFrame().getStackItem(1)); @@ -120,7 +120,7 @@ && switch (p) { : switch (p) { case EC_RECOVER -> EcRecoverEffectiveCall.gasCost(); case SHA2_256 -> Sha256Blocks.gasCost(hub); - case RIPEMD_160 -> Rip160Blocks.gasCost(hub); + case RIPEMD_160 -> RipeMd160Blocks.gasCost(hub); case IDENTITY -> switch (hub.opCode()) { case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { final Address target = Words.toAddress(hub.messageFrame().getStackItem(1)); diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/transients/OperationAncillaries.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/transients/OperationAncillaries.java index fb7a212414..8d3bd6d819 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/transients/OperationAncillaries.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/transients/OperationAncillaries.java @@ -57,23 +57,22 @@ private static Bytes maybeShadowReadMemory(final MemorySpan span, final MessageF public long gasAllowanceForCall() { final OpCode opCode = hub.opCode(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final long gas = Words.clampedToLong(hub.messageFrame().getStackItem(0)); - EWord value = EWord.ZERO; - if (opCode == OpCode.CALL || opCode == OpCode.CALLCODE) { - value = EWord.of(hub.messageFrame().getStackItem(2)); - } - final long stipend = value.isZero() ? 0 : GasConstants.G_CALL_STIPEND.cost(); - final long upfrontCost = Hub.GAS_PROJECTOR.of(hub.messageFrame(), opCode).total(); - return stipend - + Math.max( - Words.unsignedMin( - allButOneSixtyFourth(hub.messageFrame().getRemainingGas() - upfrontCost), gas), - 0); + if (opCode.isCall()) { + final long gas = Words.clampedToLong(hub.messageFrame().getStackItem(0)); + EWord value = EWord.ZERO; + if (opCode == OpCode.CALL || opCode == OpCode.CALLCODE) { + value = EWord.of(hub.messageFrame().getStackItem(2)); } - default -> throw new IllegalStateException("not a CALL"); + final long stipend = value.isZero() ? 0 : GasConstants.G_CALL_STIPEND.cost(); + final long upfrontCost = Hub.GAS_PROJECTOR.of(hub.messageFrame(), opCode).total(); + return stipend + + Math.max( + Words.unsignedMin( + allButOneSixtyFourth(hub.messageFrame().getRemainingGas() - upfrontCost), gas), + 0); } + + throw new IllegalStateException("not a CALL"); } /** diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/Keccak.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/Keccak.java index d0f214e388..9e752e05aa 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/Keccak.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/Keccak.java @@ -15,20 +15,24 @@ package net.consensys.linea.zktracer.module.limits; -import static org.hyperledger.besu.evm.internal.Words.clampedToLong; - import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; import com.google.common.base.Preconditions; +import lombok.Getter; import lombok.RequiredArgsConstructor; import net.consensys.linea.zktracer.ColumnHeader; import net.consensys.linea.zktracer.module.Module; import net.consensys.linea.zktracer.module.hub.Hub; +import net.consensys.linea.zktracer.module.hub.signals.PlatformController; import net.consensys.linea.zktracer.module.limits.precompiles.EcRecoverEffectiveCall; +import net.consensys.linea.zktracer.module.shakiradata.ShakiraData; +import net.consensys.linea.zktracer.module.shakiradata.ShakiraDataOperation; +import net.consensys.linea.zktracer.module.shakiradata.ShakiraPrecompileType; import net.consensys.linea.zktracer.opcode.OpCode; +import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.internal.Words; @@ -39,13 +43,18 @@ public class Keccak implements Module { private static final int L1_MSG_INDICES_BYTES = 8; private static final int L1_TIMESTAMPS_BYTES = 8; private static final int PUBKEY_BYTES = 64; + private static final int KECCAK_BIT_RATE = 1088; + private static final int KECCAK_BYTE_RATE = KECCAK_BIT_RATE / 8; // TODO: find correct name private final Hub hub; private final EcRecoverEffectiveCall ecRec; private final L2Block l2Block; - private final Deque> deployedCodesizes = new ArrayDeque<>(); + @Getter private final ShakiraData shakiraData; + + private final Deque> deployedCodeSizes = new ArrayDeque<>(); private final Deque> sha3Sizes = new ArrayDeque<>(); + private final Deque> create2Sizes = new ArrayDeque<>(); @Override public String moduleKey() { @@ -54,22 +63,20 @@ public String moduleKey() { @Override public void enterTransaction() { - this.deployedCodesizes.push(new ArrayList<>(5)); - this.sha3Sizes.push(new ArrayList<>(100)); + this.deployedCodeSizes.push(new ArrayList<>()); + this.sha3Sizes.push(new ArrayList<>()); + this.create2Sizes.push(new ArrayList<>()); } @Override public void popTransaction() { - this.deployedCodesizes.pop(); + this.deployedCodeSizes.pop(); this.sha3Sizes.pop(); - } - - public static int numKeccak(int x) { - return (x + 136) / 136; + this.create2Sizes.pop(); } private static int numKeccak(long x) { - final long r = (x + 136) / 136; + final long r = (x + KECCAK_BYTE_RATE - 1) / KECCAK_BYTE_RATE; Preconditions.checkState(r < Integer.MAX_VALUE, "demented KECCAK"); return (int) r; } @@ -78,18 +85,40 @@ private static int numKeccak(long x) { public void tracePreOpcode(final MessageFrame frame) { final OpCode opCode = this.hub.opCode(); - // Capture calls to SHA3 - if (opCode == OpCode.SHA3) { - if (frame.stackSize() > 1) { - final long sha3Size = Words.clampedToLong(frame.getStackItem(1)); - this.sha3Sizes.peek().add(sha3Size); + final PlatformController pch = this.hub.pch(); + + if (pch.exceptions().none()) { + // Capture calls to SHA3. + if (opCode == OpCode.SHA3) { + callShakira(frame, 0, 1, this.sha3Sizes); + } + + // Capture contract deployment + // TODO: compute the gas cost if we are under deployment. + if (opCode == OpCode.RETURN && hub.currentFrame().underDeployment()) { + callShakira(frame, 0, 1, this.deployedCodeSizes); + } + + if (opCode == OpCode.CREATE2 && pch.aborts().none()) { + callShakira(frame, 1, 2, this.create2Sizes); } } + } + + private void callShakira( + final MessageFrame frame, + final int codeOffsetStackItemOffset, + final int codeSizeStackItemOffset, + final Deque> codeSizes) { + final long codeSize = Words.clampedToLong(frame.getStackItem(codeSizeStackItemOffset)); + codeSizes.peek().add(codeSize); + + if (codeSize != 0) { + final long codeOffset = Words.clampedToLong(frame.getStackItem(codeOffsetStackItemOffset)); + final Bytes byteCode = frame.shadowReadMemory(codeOffset, codeSize); - // Capture contract deployment - if (opCode == OpCode.RETURN && hub.currentFrame().underDeployment() && frame.stackSize() > 1) { - final long codeSize = clampedToLong(frame.getStackItem(1)); - this.deployedCodesizes.peek().add(codeSize); + this.shakiraData.call( + new ShakiraDataOperation(hub.stamp(), ShakiraPrecompileType.KECCAK, byteCode)); } } @@ -107,7 +136,7 @@ public int lineCount() { // accurate? If this is actually the same data then we should not need to // prove it twice. If the second time the data is hashed with a few extra // bytes this should be accounted for : numKeccak(l) + numKeccak(l + extra) - + this.deployedCodesizes.stream().flatMap(List::stream).mapToInt(Keccak::numKeccak).sum() + + this.deployedCodeSizes.stream().flatMap(List::stream).mapToInt(Keccak::numKeccak).sum() // From ecRecover precompiles, // This accounts for the keccak of the recovered public keys to derive the // addresses. This also accounts for the transactions signatures diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Blake2fRounds.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Blake2fRounds.java index 7f0c06bdad..910d61866b 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Blake2fRounds.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Blake2fRounds.java @@ -67,18 +67,15 @@ public static boolean isHubFailure(final Hub hub) { final OpCode opCode = hub.opCode(); final MessageFrame frame = hub.messageFrame(); - return switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.BLAKE2B_F_COMPRESSION)) { - final long length = hub.transients().op().callDataSegment().length(); - yield length != BLAKE2f_INPUT_SIZE; - } else { - yield false; - } + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.BLAKE2B_F_COMPRESSION)) { + final long length = hub.transients().op().callDataSegment().length(); + return length != BLAKE2f_INPUT_SIZE; } - default -> false; - }; + } + + return false; } public static boolean isRamFailure(final Hub hub) { @@ -89,25 +86,22 @@ public static boolean isRamFailure(final Hub hub) { return false; } - return switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.BLAKE2B_F_COMPRESSION)) { - final long offset = hub.transients().op().callDataSegment().offset(); - final int f = - frame.shadowReadMemory(offset, BLAKE2f_INPUT_SIZE).get(BLAKE2f_INPUT_SIZE - 1); - final int r = - frame - .shadowReadMemory(offset, BLAKE2f_INPUT_SIZE) - .slice(0, 4) - .toInt(); // The number of round is equal to the gas to pay - yield !((f == 0 || f == 1) && hub.transients().op().gasAllowanceForCall() >= r); - } else { - yield false; - } + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.BLAKE2B_F_COMPRESSION)) { + final long offset = hub.transients().op().callDataSegment().offset(); + final int f = + frame.shadowReadMemory(offset, BLAKE2f_INPUT_SIZE).get(BLAKE2f_INPUT_SIZE - 1); + final int r = + frame + .shadowReadMemory(offset, BLAKE2f_INPUT_SIZE) + .slice(0, 4) + .toInt(); // The number of round is equal to the gas to pay + return !((f == 0 || f == 1) && hub.transients().op().gasAllowanceForCall() >= r); } - default -> false; - }; + } + + return false; } public static long gasCost(final Hub hub) { @@ -135,23 +129,21 @@ public static long gasCost(final Hub hub) { public static PrecompileMetadata metadata(final Hub hub) { final OpCode opCode = hub.opCode(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(hub.messageFrame().getStackItem(1)); - if (target.equals(Address.BLAKE2B_F_COMPRESSION)) { - final long length = hub.transients().op().callDataSegment().length(); - - if (length == BLAKE2f_INPUT_SIZE) { - final int f = hub.transients().op().callData().get(BLAKE2f_INPUT_SIZE - 1); - if (f == 0 || f == 1) { - final int r = - hub.transients() - .op() - .callData() - .slice(0, 4) - .toInt(); // The number of round is equal to the gas to pay - return new Blake2fMetadata(r, f); - } + if (opCode.isCall()) { + final Address target = Words.toAddress(hub.messageFrame().getStackItem(1)); + if (target.equals(Address.BLAKE2B_F_COMPRESSION)) { + final long length = hub.transients().op().callDataSegment().length(); + + if (length == BLAKE2f_INPUT_SIZE) { + final int f = hub.transients().op().callData().get(BLAKE2f_INPUT_SIZE - 1); + if (f == 0 || f == 1) { + final int r = + hub.transients() + .op() + .callData() + .slice(0, 4) + .toInt(); // The number of round is equal to the gas to pay + return new Blake2fMetadata(r, f); } } } @@ -164,7 +156,7 @@ public static PrecompileMetadata metadata(final Hub hub) { public void tracePreOpcode(MessageFrame frame) { final OpCode opCode = hub.opCode(); - if (opCode.isAnyOf(OpCode.CALL, OpCode.STATICCALL, OpCode.DELEGATECALL, OpCode.CALLCODE)) { + if (opCode.isCall()) { final Address target = Words.toAddress(frame.getStackItem(1)); if (target.equals(Address.BLAKE2B_F_COMPRESSION)) { @@ -200,8 +192,8 @@ public void tracePreOpcode(MessageFrame frame) { @Override public int lineCount() { int r = 0; - for (int i = 0; i < this.counts.size(); i++) { - r += this.counts.get(i); + for (Integer count : this.counts) { + r += count; } return r; } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcAddEffectiveCall.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcAddEffectiveCall.java index 5228e85a6e..f41a8e5f34 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcAddEffectiveCall.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcAddEffectiveCall.java @@ -59,13 +59,11 @@ public void popTransaction() { public void tracePreOpcode(MessageFrame frame) { final OpCode opCode = hub.opCode(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.ALTBN128_ADD) - && hub.transients().op().gasAllowanceForCall() >= PRECOMPILE_GAS_FEE) { - this.counts.push(this.counts.pop() + 1); - } + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.ALTBN128_ADD) + && hub.transients().op().gasAllowanceForCall() >= PRECOMPILE_GAS_FEE) { + this.counts.push(this.counts.pop() + 1); } } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcMulEffectiveCall.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcMulEffectiveCall.java index d776fc28c7..1e56c489eb 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcMulEffectiveCall.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcMulEffectiveCall.java @@ -59,15 +59,12 @@ public void popTransaction() { public void tracePreOpcode(MessageFrame frame) { final OpCode opCode = hub.opCode(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.ALTBN128_MUL) - && hub.transients().op().gasAllowanceForCall() >= PRECOMPILE_GAS_FEE) { - this.counts.push(this.counts.pop() + 1); - } + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.ALTBN128_MUL) + && hub.transients().op().gasAllowanceForCall() >= PRECOMPILE_GAS_FEE) { + this.counts.push(this.counts.pop() + 1); } - default -> {} } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcPairingCallEffectiveCall.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcPairingCallEffectiveCall.java index 5f1d5f3303..984d3b9ad1 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcPairingCallEffectiveCall.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcPairingCallEffectiveCall.java @@ -62,21 +62,18 @@ public static boolean isHubFailure(final Hub hub) { final OpCode opCode = hub.opCode(); final MessageFrame frame = hub.messageFrame(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.ALTBN128_PAIRING)) { - long length = hub.transients().op().callDataSegment().length(); - if (length % 192 != 0) { - return true; - } - final long pairingCount = length / ECPAIRING_NB_BYTES_PER_MILLER_LOOP; - - return hub.transients().op().gasAllowanceForCall() - < PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_MILLER_LOOP_GAS_FEE * pairingCount; + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.ALTBN128_PAIRING)) { + long length = hub.transients().op().callDataSegment().length(); + if (length % 192 != 0) { + return true; } + final long pairingCount = length / ECPAIRING_NB_BYTES_PER_MILLER_LOOP; + + return hub.transients().op().gasAllowanceForCall() + < PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_MILLER_LOOP_GAS_FEE * pairingCount; } - default -> {} } return false; @@ -104,18 +101,16 @@ public static long gasCost(final Hub hub) { final OpCode opCode = hub.opCode(); final MessageFrame frame = hub.messageFrame(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.ALTBN128_PAIRING)) { - final long length = hub.transients().op().callDataSegment().length(); - final long nMillerLoop = (length / ECPAIRING_NB_BYTES_PER_MILLER_LOOP); - if (nMillerLoop * ECPAIRING_NB_BYTES_PER_MILLER_LOOP != length) { - return 0; - } - - return PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_MILLER_LOOP_GAS_FEE * nMillerLoop; + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.ALTBN128_PAIRING)) { + final long length = hub.transients().op().callDataSegment().length(); + final long nMillerLoop = (length / ECPAIRING_NB_BYTES_PER_MILLER_LOOP); + if (nMillerLoop * ECPAIRING_NB_BYTES_PER_MILLER_LOOP != length) { + return 0; } + + return PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_MILLER_LOOP_GAS_FEE * nMillerLoop; } } @@ -126,37 +121,35 @@ public static long gasCost(final Hub hub) { public void tracePreOpcode(MessageFrame frame) { final OpCode opCode = hub.opCode(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.ALTBN128_PAIRING)) { - long length = hub.transients().op().callDataSegment().length(); - - final long nMillerLoop = (length / ECPAIRING_NB_BYTES_PER_MILLER_LOOP); - if (nMillerLoop * ECPAIRING_NB_BYTES_PER_MILLER_LOOP != length) { - log.warn("[ECPairing] Argument is not a right size: " + length); - return; - } - - if (hub.transients().op().gasAllowanceForCall() - >= PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_MILLER_LOOP_GAS_FEE * nMillerLoop) { - final EcPairingLimit lastEcpairingLimit = this.counts.pop(); - this.counts.push( - new EcPairingLimit( - lastEcpairingLimit.nPrecompileCall() + 1, - lastEcpairingLimit.nMillerLoop() + nMillerLoop)); - } + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.ALTBN128_PAIRING)) { + long length = hub.transients().op().callDataSegment().length(); + + if (length % ECPAIRING_NB_BYTES_PER_MILLER_LOOP != 0) { + log.warn("[ECPairing] Argument is not a right size: " + length); + return; + } + + final long nMillerLoop = (length / ECPAIRING_NB_BYTES_PER_MILLER_LOOP); + + if (hub.transients().op().gasAllowanceForCall() + >= PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_MILLER_LOOP_GAS_FEE * nMillerLoop) { + final EcPairingLimit lastEcpairingLimit = this.counts.pop(); + this.counts.push( + new EcPairingLimit( + lastEcpairingLimit.nPrecompileCall() + 1, + lastEcpairingLimit.nMillerLoop() + nMillerLoop)); } } - default -> {} } } @Override public int lineCount() { int r = 0; - for (int i = 0; i < this.counts.size(); i++) { - r += this.counts.get(i).nPrecompileCall(); + for (EcPairingLimit count : this.counts) { + r += count.nPrecompileCall(); } return r; } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcRecoverEffectiveCall.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcRecoverEffectiveCall.java index 0f22e70af0..6e4b13f022 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcRecoverEffectiveCall.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/EcRecoverEffectiveCall.java @@ -79,23 +79,21 @@ public static boolean isValid(final Hub hub) { final OpCode opCode = hub.opCode(); final MessageFrame frame = hub.messageFrame(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.ECREC)) { - final MemorySpan callData = hub.transients().op().callDataSegment(); - final Bytes inputData = frame.shadowReadMemory(callData.offset(), callData.length()); - final BigInteger v = slice(inputData, EWORD_SIZE, EWORD_SIZE).toUnsignedBigInteger(); - final BigInteger r = slice(inputData, EWORD_SIZE * 2, EWORD_SIZE).toUnsignedBigInteger(); - final BigInteger s = slice(inputData, EWORD_SIZE * 3, EWORD_SIZE).toUnsignedBigInteger(); - // TODO: exclude case without valid signature - return hasEnoughGas(hub) - && (v.equals(BigInteger.valueOf(27)) || v.equals(BigInteger.valueOf(28))) - && !r.equals(BigInteger.ZERO) - && r.compareTo(SECP_256_K1N) < 0 - && !s.equals(BigInteger.ZERO) - && s.compareTo(SECP_256_K1N) < 0; - } + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.ECREC)) { + final MemorySpan callData = hub.transients().op().callDataSegment(); + final Bytes inputData = frame.shadowReadMemory(callData.offset(), callData.length()); + final BigInteger v = slice(inputData, EWORD_SIZE, EWORD_SIZE).toUnsignedBigInteger(); + final BigInteger r = slice(inputData, EWORD_SIZE * 2, EWORD_SIZE).toUnsignedBigInteger(); + final BigInteger s = slice(inputData, EWORD_SIZE * 3, EWORD_SIZE).toUnsignedBigInteger(); + // TODO: exclude case without valid signature + return hasEnoughGas(hub) + && (v.equals(BigInteger.valueOf(27)) || v.equals(BigInteger.valueOf(28))) + && !r.equals(BigInteger.ZERO) + && r.compareTo(SECP_256_K1N) < 0 + && !s.equals(BigInteger.ZERO) + && s.compareTo(SECP_256_K1N) < 0; } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/ModexpEffectiveCall.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/ModexpEffectiveCall.java index c3eb716cd9..f003c59203 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/ModexpEffectiveCall.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/ModexpEffectiveCall.java @@ -70,7 +70,7 @@ public void popTransaction() { public void tracePreOpcode(MessageFrame frame) { final OpCode opCode = hub.opCode(); - if (opCode.isAnyOf(OpCode.CALL, OpCode.STATICCALL, OpCode.DELEGATECALL, OpCode.CALLCODE)) { + if (opCode.isCall()) { final Address target = Words.toAddress(frame.getStackItem(1)); if (target.equals(Address.MODEXP)) { final Bytes inputData = hub.transients().op().callData(); @@ -150,7 +150,7 @@ public static long gasCost(final Hub hub) { final OpCode opCode = hub.opCode(); final MessageFrame frame = hub.messageFrame(); - if (opCode.isAnyOf(OpCode.CALL, OpCode.STATICCALL, OpCode.DELEGATECALL, OpCode.CALLCODE)) { + if (opCode.isCall()) { final Address target = Words.toAddress(frame.getStackItem(1)); if (target.equals(Address.MODEXP)) { final Bytes inputData = hub.transients().op().callData(); diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Rip160Blocks.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/RipeMd160Blocks.java similarity index 51% rename from arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Rip160Blocks.java rename to arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/RipeMd160Blocks.java index 56d6449cf6..b617499282 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Rip160Blocks.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/RipeMd160Blocks.java @@ -15,41 +15,57 @@ package net.consensys.linea.zktracer.module.limits.precompiles; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.WORD_SIZE; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.WORD_SIZE_MO; + import java.nio.MappedByteBuffer; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.List; -import java.util.Stack; +import lombok.Getter; import lombok.RequiredArgsConstructor; import net.consensys.linea.zktracer.ColumnHeader; import net.consensys.linea.zktracer.module.Module; import net.consensys.linea.zktracer.module.hub.Hub; +import net.consensys.linea.zktracer.module.shakiradata.ShakiraData; +import net.consensys.linea.zktracer.module.shakiradata.ShakiraDataOperation; +import net.consensys.linea.zktracer.module.shakiradata.ShakiraPrecompileType; import net.consensys.linea.zktracer.opcode.OpCode; +import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.internal.Words; @RequiredArgsConstructor -public final class Rip160Blocks implements Module { +public final class RipeMd160Blocks implements Module { + private static final int PRECOMPILE_BASE_GAS_FEE = 600; + private static final int PRECOMPILE_GAS_FEE_PER_EWORD = 120; + private static final int RIPEMD160_BLOCKSIZE = 64 * 8; + // If the length is > 2⁶4, we just use the lower 64 bits. + private static final int RIPEMD160_LENGTH_APPEND = 64; + private static final int RIPEMD160_ND_PADDED_ONE = 1; + private final Hub hub; - private final Stack counts = new Stack<>(); + private final Deque counts = new ArrayDeque<>(); @Override public String moduleKey() { return "PRECOMPILE_RIPEMD_BLOCKS"; } - private static final int PRECOMPILE_BASE_GAS_FEE = 600; - private static final int PRECOMPILE_GAS_FEE_PER_EWORD = 120; - private static final int RIPEMD160_BLOCKSIZE = 64 * 8; - // If the length is > 2⁶4, we just use the lower 64 bits. - private static final int RIPEMD160_LENGTH_APPEND = 64; - private static final int RIPEMD160_ND_PADDED_ONE = 1; + @Getter private final ShakiraData shakiraData; @Override - public void enterTransaction() { + public void traceStartConflation(final long blockCount) { counts.push(0); } + @Override + public void enterTransaction() { + counts.push(counts.getFirst()); + } + @Override public void popTransaction() { counts.pop(); @@ -62,14 +78,12 @@ public static boolean hasEnoughGas(final Hub hub) { public static long gasCost(final Hub hub) { final OpCode opCode = hub.opCode(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(hub.messageFrame().getStackItem(1)); - if (target.equals(Address.RIPEMD160)) { - final long dataByteLength = hub.transients().op().callDataSegment().length(); - final long wordCount = (dataByteLength + 31) / 32; - return PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_GAS_FEE_PER_EWORD * wordCount; - } + if (opCode.isCall()) { + final Address target = Words.toAddress(hub.messageFrame().getStackItem(1)); + if (target.equals(Address.RIPEMD160)) { + final long dataByteLength = hub.transients().op().callDataSegment().length(); + final long wordCount = (dataByteLength + WORD_SIZE_MO) / WORD_SIZE; + return PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_GAS_FEE_PER_EWORD * wordCount; } } @@ -80,41 +94,41 @@ public static long gasCost(final Hub hub) { public void tracePreOpcode(MessageFrame frame) { final OpCode opCode = hub.opCode(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.RIPEMD160)) { - final long dataByteLength = hub.transients().op().callDataSegment().length(); - if (dataByteLength == 0) { - return; - } // skip trivial hash TODO: check the prover does skip it - final int blockCount = - (int) - (dataByteLength * 8 - + RIPEMD160_ND_PADDED_ONE - + RIPEMD160_LENGTH_APPEND - + (RIPEMD160_BLOCKSIZE - 1)) - / RIPEMD160_BLOCKSIZE; - - final long wordCount = (dataByteLength + 31) / 32; - final long gasNeeded = PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_GAS_FEE_PER_EWORD * wordCount; - - if (hub.transients().op().gasAllowanceForCall() >= gasNeeded) { - this.counts.push(this.counts.pop() + blockCount); - } + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.RIPEMD160)) { + final long dataByteLength = hub.transients().op().callDataSegment().length(); + + if (dataByteLength == 0) { + return; + } // skip trivial hash TODO: check the prover does skip it + + final int blockCount = + (int) + (dataByteLength * 8 + + RIPEMD160_ND_PADDED_ONE + + RIPEMD160_LENGTH_APPEND + + (RIPEMD160_BLOCKSIZE - 1)) + / RIPEMD160_BLOCKSIZE; + + final long wordCount = (dataByteLength + WORD_SIZE_MO) / WORD_SIZE; + final long gasNeeded = PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_GAS_FEE_PER_EWORD * wordCount; + + final Bytes inputData = hub.transients().op().callData(); + + if (hub.transients().op().gasAllowanceForCall() >= gasNeeded) { + this.shakiraData.call( + new ShakiraDataOperation(hub.stamp(), ShakiraPrecompileType.RIPEMD, inputData)); + + this.counts.push(this.counts.pop() + blockCount); } } - default -> {} } } @Override public int lineCount() { - int r = 0; - for (int i = 0; i < this.counts.size(); i++) { - r += this.counts.get(i); - } - return r; + return this.counts.getFirst(); } @Override diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Sha256Blocks.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Sha256Blocks.java index 1bbf27957f..6851236829 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Sha256Blocks.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Sha256Blocks.java @@ -15,28 +15,30 @@ package net.consensys.linea.zktracer.module.limits.precompiles; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.WORD_SIZE; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.WORD_SIZE_MO; + import java.nio.MappedByteBuffer; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.List; -import java.util.Stack; +import lombok.Getter; import lombok.RequiredArgsConstructor; import net.consensys.linea.zktracer.ColumnHeader; import net.consensys.linea.zktracer.module.Module; import net.consensys.linea.zktracer.module.hub.Hub; +import net.consensys.linea.zktracer.module.shakiradata.ShakiraData; +import net.consensys.linea.zktracer.module.shakiradata.ShakiraDataOperation; +import net.consensys.linea.zktracer.module.shakiradata.ShakiraPrecompileType; import net.consensys.linea.zktracer.opcode.OpCode; +import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.internal.Words; @RequiredArgsConstructor public final class Sha256Blocks implements Module { - private final Hub hub; - private final Stack counts = new Stack<>(); - - @Override - public String moduleKey() { - return "PRECOMPILE_SHA2_BLOCKS"; - } private static final int PRECOMPILE_BASE_GAS_FEE = 60; private static final int PRECOMPILE_GAS_FEE_PER_EWORD = 12; @@ -45,11 +47,26 @@ public String moduleKey() { private static final int SHA256_PADDING_LENGTH = 64; private static final int SHA256_NB_PADDED_ONE = 1; + private final Hub hub; + private final Deque counts = new ArrayDeque<>(); + + @Getter private final ShakiraData shakiraData; + @Override - public void enterTransaction() { + public String moduleKey() { + return "PRECOMPILE_SHA2_BLOCKS"; + } + + @Override + public void traceStartConflation(final long blockCount) { counts.push(0); } + @Override + public void enterTransaction() { + counts.push(counts.getFirst()); + } + @Override public void popTransaction() { counts.pop(); @@ -63,16 +80,15 @@ public static long gasCost(final Hub hub) { final OpCode opCode = hub.opCode(); final MessageFrame frame = hub.messageFrame(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.SHA256)) { - final long dataByteLength = hub.transients().op().callDataSegment().length(); - final long wordCount = (dataByteLength + 31) / 32; - return PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_GAS_FEE_PER_EWORD * wordCount; - } + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.SHA256)) { + final long dataByteLength = hub.transients().op().callDataSegment().length(); + final long wordCount = (dataByteLength + WORD_SIZE_MO) / WORD_SIZE; + return PRECOMPILE_BASE_GAS_FEE + PRECOMPILE_GAS_FEE_PER_EWORD * wordCount; } } + return 0; } @@ -80,38 +96,36 @@ public static long gasCost(final Hub hub) { public void tracePreOpcode(MessageFrame frame) { final OpCode opCode = hub.opCode(); - switch (opCode) { - case CALL, STATICCALL, DELEGATECALL, CALLCODE -> { - final Address target = Words.toAddress(frame.getStackItem(1)); - if (target.equals(Address.SHA256)) { - final long dataByteLength = hub.transients().op().callDataSegment().length(); - if (dataByteLength == 0) { - return; - } - final int blockCount = - (int) - (dataByteLength * 8 - + SHA256_NB_PADDED_ONE - + SHA256_PADDING_LENGTH - + (SHA256_BLOCKSIZE - 1)) - / SHA256_BLOCKSIZE; - - if (hasEnoughGas(this.hub)) { - this.counts.push(this.counts.pop() + blockCount); - } + if (opCode.isCall()) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (target.equals(Address.SHA256)) { + final long dataByteLength = hub.transients().op().callDataSegment().length(); + if (dataByteLength == 0) { + return; + } + final int blockCount = + (int) + (dataByteLength * 8 + + SHA256_NB_PADDED_ONE + + SHA256_PADDING_LENGTH + + (SHA256_BLOCKSIZE - 1)) + / SHA256_BLOCKSIZE; + + final Bytes inputData = hub.transients().op().callData(); + + if (hasEnoughGas(this.hub)) { + this.shakiraData.call( + new ShakiraDataOperation(hub.stamp(), ShakiraPrecompileType.SHA256, inputData)); + + this.counts.push(this.counts.pop() + blockCount); } } - default -> {} } } @Override public int lineCount() { - int r = 0; - for (int i = 0; i < this.counts.size(); i++) { - r += this.counts.get(i); - } - return r; + return counts.getFirst(); } @Override diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraData.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraData.java new file mode 100644 index 0000000000..03f62ce17a --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraData.java @@ -0,0 +1,112 @@ +/* + * 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.module.shakiradata; + +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.LLARGE; + +import java.nio.MappedByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import lombok.RequiredArgsConstructor; +import net.consensys.linea.zktracer.ColumnHeader; +import net.consensys.linea.zktracer.container.stacked.set.StackedSet; +import net.consensys.linea.zktracer.module.Module; +import net.consensys.linea.zktracer.module.wcp.Wcp; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.evm.log.Log; +import org.hyperledger.besu.evm.worldstate.WorldView; + +@RequiredArgsConstructor +public class ShakiraData implements Module { + private final Wcp wcp; + private final StackedSet operations = new StackedSet<>(); + private final List sortedOperations = new ArrayList<>(); + private int numberOfOperationsAtStartTx = 0; + private final ShakiraDataComparator comparator = new ShakiraDataComparator(); + + @Override + public String moduleKey() { + return "SHAKIRA_DATA"; + } + + @Override + public void enterTransaction() { + this.operations.enter(); + } + + @Override + public void popTransaction() { + this.sortedOperations.removeAll(this.operations.sets.getLast()); + this.operations.pop(); + } + + @Override + public int lineCount() { + return this.operations.lineCount() + + 1; /*because the lookup HUB -> SHAKIRA requires at least two padding rows. TODO: should be done by Corset */ + } + + @Override + public List columnsHeaders() { + return Trace.headers(this.lineCount()); + } + + public void call(final ShakiraDataOperation operation) { + this.operations.add(operation); + this.wcp.callGT(operation.lastNBytes(), 0); + this.wcp.callLEQ(operation.lastNBytes(), LLARGE); + } + + @Override + public void traceStartTx(WorldView worldView, Transaction tx) { + this.numberOfOperationsAtStartTx = operations.size(); + } + + @Override + public void traceEndTx( + WorldView worldView, + Transaction tx, + boolean isSuccessful, + Bytes output, + List logs, + long gasUsed) { + final List newOperations = + new ArrayList<>(this.operations.sets.getLast()); + newOperations.sort(comparator); + this.sortedOperations.addAll(newOperations); + final int numberOfOperationsAtEndTx = sortedOperations.size(); + for (int i = numberOfOperationsAtStartTx; i < numberOfOperationsAtEndTx; i++) { + final long previousID = i == 0 ? 0 : sortedOperations.get(i - 1).ID(); + this.wcp.callLT(previousID, sortedOperations.get(i).ID()); + } + } + + @Override + public void commit(List buffers) { + final Trace trace = new Trace(buffers); + + /* WARN: do not remove, the lookup HUB -> SHAKIRA requires at least two padding rows. TODO: should be done by Corset*/ + trace.fillAndValidateRow(); + + int stamp = 0; + for (ShakiraDataOperation operation : sortedOperations) { + stamp++; + operation.trace(trace, stamp); + } + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraDataComparator.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraDataComparator.java new file mode 100644 index 0000000000..5a515185b9 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraDataComparator.java @@ -0,0 +1,25 @@ +/* + * 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.module.shakiradata; + +import java.util.Comparator; + +class ShakiraDataComparator implements Comparator { + @Override + public int compare(ShakiraDataOperation o1, ShakiraDataOperation o2) { + return (int) (o1.ID() - o2.ID()); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraDataOperation.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraDataOperation.java new file mode 100644 index 0000000000..6426bec648 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraDataOperation.java @@ -0,0 +1,167 @@ +/* + * 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.module.shakiradata; + +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.LLARGE; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.LLARGEMO; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_KECCAK_DATA; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_KECCAK_RESULT; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_RIPEMD_DATA; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_RIPEMD_RESULT; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_SHA2_DATA; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_SHA2_RESULT; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.WORD_SIZE; +import static net.consensys.linea.zktracer.module.shakiradata.ShakiraPrecompileType.KECCAK; +import static net.consensys.linea.zktracer.module.shakiradata.ShakiraPrecompileType.RIPEMD; +import static net.consensys.linea.zktracer.module.shakiradata.ShakiraPrecompileType.SHA256; +import static net.consensys.linea.zktracer.module.shakiradata.Trace.INDEX_MAX_RESULT; +import static net.consensys.linea.zktracer.types.Utils.rightPadTo; + +import lombok.Getter; +import lombok.experimental.Accessors; +import net.consensys.linea.zktracer.container.ModuleOperation; +import net.consensys.linea.zktracer.types.UnsignedByte; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.crypto.Hash; + +@Accessors(fluent = true) +public class ShakiraDataOperation extends ModuleOperation { + + private final ShakiraPrecompileType precompileType; + private final Bytes hashInput; + @Getter private final long ID; + private final int inputSize; + @Getter private final short lastNBytes; + private final int indexMaxData; + private Bytes32 result; + + public ShakiraDataOperation( + final long hubStamp, final ShakiraPrecompileType precompileType, final Bytes hashInput) { + this.precompileType = precompileType; + this.ID = hubStamp + 1; + this.hashInput = hashInput; + this.inputSize = hashInput.size(); + this.lastNBytes = (short) (inputSize % LLARGE == 0 ? LLARGE : inputSize % LLARGE); + // this.indexMaxData = Math.ceilDiv(inputSize, LLARGE) - 1; + this.indexMaxData = (inputSize + LLARGEMO) / LLARGE - 1; + } + + @Override + protected int computeLineCount() { + return indexMaxData + 1 + INDEX_MAX_RESULT + 1; + } + + void trace(Trace trace, final int stamp) { + + this.result = Bytes32.leftPad(this.computeResult()); + traceData(trace, stamp); + traceResult(trace, stamp); + } + + private void traceData(Trace trace, final int stamp) { + final boolean isShaData = precompileType == SHA256; + final boolean isKecData = precompileType == KECCAK; + final boolean isRipData = precompileType == RIPEMD; + final UnsignedByte phase = + switch (precompileType) { + case SHA256 -> UnsignedByte.of(PHASE_SHA2_DATA); + case KECCAK -> UnsignedByte.of(PHASE_KECCAK_DATA); + case RIPEMD -> UnsignedByte.of(PHASE_RIPEMD_DATA); + }; + + for (int ct = 0; ct <= indexMaxData; ct++) { + final boolean lastDataRow = ct == indexMaxData; + trace + .ripshaStamp(stamp) + .id(ID) + .phase(phase) + .index(ct) + .indexMax(indexMaxData) + .limb( + lastDataRow + ? rightPadTo(hashInput.slice(ct * LLARGE), LLARGE) + : hashInput.slice(ct * LLARGE, LLARGE)) + .nBytes(lastDataRow ? lastNBytes : LLARGE) + .nBytesAcc(lastDataRow ? inputSize : (long) LLARGE * (ct + 1)) + .totalSize(inputSize) + .isSha2Data(isShaData) + .isKeccakData(isKecData) + .isRipemdData(isRipData) + .isSha2Result(false) + .isKeccakResult(false) + .isRipemdResult(false) + .selectorKeccakResHi(false) + .selectorSha2ResHi(false) + .selectorRipemdResHi(false) + .validateRow(); + } + } + + private void traceResult(Trace trace, final int stamp) { + final boolean isShaResult = precompileType == SHA256; + final boolean isKecResult = precompileType == KECCAK; + final boolean isRipResult = precompileType == RIPEMD; + final UnsignedByte phase = + switch (precompileType) { + case SHA256 -> UnsignedByte.of(PHASE_SHA2_RESULT); + case KECCAK -> UnsignedByte.of(PHASE_KECCAK_RESULT); + case RIPEMD -> UnsignedByte.of(PHASE_RIPEMD_RESULT); + }; + + for (int ct = 0; ct <= INDEX_MAX_RESULT; ct++) { + trace + .ripshaStamp(stamp) + .id(ID) + .phase(phase) + .index(ct) + .indexMax(INDEX_MAX_RESULT) + .isSha2Data(false) + .isKeccakData(false) + .isRipemdData(false) + .isSha2Result(isShaResult) + .isKeccakResult(isKecResult) + .isRipemdResult(isRipResult) + .nBytes((short) LLARGE) + .totalSize(WORD_SIZE); + + switch (ct) { + case 0 -> trace + .limb(result.slice(0, LLARGE)) + .nBytesAcc(LLARGE) + .selectorKeccakResHi(precompileType == KECCAK) + .selectorSha2ResHi(precompileType == SHA256) + .selectorRipemdResHi(precompileType == RIPEMD) + .validateRow(); + case 1 -> trace + .limb(result.slice(LLARGE, LLARGE)) + .nBytesAcc(WORD_SIZE) + .selectorKeccakResHi(false) + .selectorSha2ResHi(false) + .selectorRipemdResHi(false) + .validateRow(); + } + } + } + + private Bytes computeResult() { + return switch (precompileType) { + case SHA256 -> Hash.sha256(hashInput); + case KECCAK -> Hash.keccak256(hashInput); + case RIPEMD -> Hash.ripemd160(hashInput); + }; + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraPrecompileType.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraPrecompileType.java new file mode 100644 index 0000000000..a9502c06b3 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/ShakiraPrecompileType.java @@ -0,0 +1,22 @@ +/* + * 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.module.shakiradata; + +public enum ShakiraPrecompileType { + SHA256, + KECCAK, + RIPEMD +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/Trace.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/Trace.java new file mode 100644 index 0000000000..d9dd29c8fc --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shakiradata/Trace.java @@ -0,0 +1,491 @@ +/* + * 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.module.shakiradata; + +import java.nio.MappedByteBuffer; +import java.util.BitSet; +import java.util.List; + +import net.consensys.linea.zktracer.ColumnHeader; +import net.consensys.linea.zktracer.types.UnsignedByte; +import org.apache.tuweni.bytes.Bytes; + +/** + * WARNING: This code is generated automatically. + * + *

Any modifications to this code may be overwritten and could lead to unexpected behavior. + * Please DO NOT ATTEMPT TO MODIFY this code directly. + */ +public class Trace { + public static final int INDEX_MAX_RESULT = 0x1; + + private final BitSet filled = new BitSet(); + private int currentLine = 0; + + private final MappedByteBuffer id; + private final MappedByteBuffer index; + private final MappedByteBuffer indexMax; + private final MappedByteBuffer isKeccakData; + private final MappedByteBuffer isKeccakResult; + private final MappedByteBuffer isRipemdData; + private final MappedByteBuffer isRipemdResult; + private final MappedByteBuffer isSha2Data; + private final MappedByteBuffer isSha2Result; + private final MappedByteBuffer limb; + private final MappedByteBuffer nBytes; + private final MappedByteBuffer nBytesAcc; + private final MappedByteBuffer phase; + private final MappedByteBuffer ripshaStamp; + private final MappedByteBuffer selectorKeccakResHi; + private final MappedByteBuffer selectorRipemdResHi; + private final MappedByteBuffer selectorSha2ResHi; + private final MappedByteBuffer totalSize; + + static List headers(int length) { + return List.of( + new ColumnHeader("shakiradata.ID", 8, length), + new ColumnHeader("shakiradata.INDEX", 8, length), + new ColumnHeader("shakiradata.INDEX_MAX", 8, length), + new ColumnHeader("shakiradata.IS_KECCAK_DATA", 1, length), + new ColumnHeader("shakiradata.IS_KECCAK_RESULT", 1, length), + new ColumnHeader("shakiradata.IS_RIPEMD_DATA", 1, length), + new ColumnHeader("shakiradata.IS_RIPEMD_RESULT", 1, length), + new ColumnHeader("shakiradata.IS_SHA2_DATA", 1, length), + new ColumnHeader("shakiradata.IS_SHA2_RESULT", 1, length), + new ColumnHeader("shakiradata.LIMB", 32, length), + new ColumnHeader("shakiradata.nBYTES", 2, length), + new ColumnHeader("shakiradata.nBYTES_ACC", 8, length), + new ColumnHeader("shakiradata.PHASE", 1, length), + new ColumnHeader("shakiradata.RIPSHA_STAMP", 8, length), + new ColumnHeader("shakiradata.SELECTOR_KECCAK_RES_HI", 1, length), + new ColumnHeader("shakiradata.SELECTOR_RIPEMD_RES_HI", 1, length), + new ColumnHeader("shakiradata.SELECTOR_SHA2_RES_HI", 1, length), + new ColumnHeader("shakiradata.TOTAL_SIZE", 8, length)); + } + + public Trace(List buffers) { + this.id = buffers.get(0); + this.index = buffers.get(1); + this.indexMax = buffers.get(2); + this.isKeccakData = buffers.get(3); + this.isKeccakResult = buffers.get(4); + this.isRipemdData = buffers.get(5); + this.isRipemdResult = buffers.get(6); + this.isSha2Data = buffers.get(7); + this.isSha2Result = buffers.get(8); + this.limb = buffers.get(9); + this.nBytes = buffers.get(10); + this.nBytesAcc = buffers.get(11); + this.phase = buffers.get(12); + this.ripshaStamp = buffers.get(13); + this.selectorKeccakResHi = buffers.get(14); + this.selectorRipemdResHi = buffers.get(15); + this.selectorSha2ResHi = buffers.get(16); + this.totalSize = buffers.get(17); + } + + public int size() { + if (!filled.isEmpty()) { + throw new RuntimeException("Cannot measure a trace with a non-validated row."); + } + + return this.currentLine; + } + + public Trace id(final long b) { + if (filled.get(0)) { + throw new IllegalStateException("shakiradata.ID already set"); + } else { + filled.set(0); + } + + id.putLong(b); + + return this; + } + + public Trace index(final long b) { + if (filled.get(1)) { + throw new IllegalStateException("shakiradata.INDEX already set"); + } else { + filled.set(1); + } + + index.putLong(b); + + return this; + } + + public Trace indexMax(final long b) { + if (filled.get(2)) { + throw new IllegalStateException("shakiradata.INDEX_MAX already set"); + } else { + filled.set(2); + } + + indexMax.putLong(b); + + return this; + } + + public Trace isKeccakData(final Boolean b) { + if (filled.get(3)) { + throw new IllegalStateException("shakiradata.IS_KECCAK_DATA already set"); + } else { + filled.set(3); + } + + isKeccakData.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace isKeccakResult(final Boolean b) { + if (filled.get(4)) { + throw new IllegalStateException("shakiradata.IS_KECCAK_RESULT already set"); + } else { + filled.set(4); + } + + isKeccakResult.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace isRipemdData(final Boolean b) { + if (filled.get(5)) { + throw new IllegalStateException("shakiradata.IS_RIPEMD_DATA already set"); + } else { + filled.set(5); + } + + isRipemdData.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace isRipemdResult(final Boolean b) { + if (filled.get(6)) { + throw new IllegalStateException("shakiradata.IS_RIPEMD_RESULT already set"); + } else { + filled.set(6); + } + + isRipemdResult.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace isSha2Data(final Boolean b) { + if (filled.get(7)) { + throw new IllegalStateException("shakiradata.IS_SHA2_DATA already set"); + } else { + filled.set(7); + } + + isSha2Data.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace isSha2Result(final Boolean b) { + if (filled.get(8)) { + throw new IllegalStateException("shakiradata.IS_SHA2_RESULT already set"); + } else { + filled.set(8); + } + + isSha2Result.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace limb(final Bytes b) { + if (filled.get(9)) { + throw new IllegalStateException("shakiradata.LIMB already set"); + } else { + filled.set(9); + } + + final byte[] bs = b.toArrayUnsafe(); + for (int i = bs.length; i < 32; i++) { + limb.put((byte) 0); + } + limb.put(b.toArrayUnsafe()); + + return this; + } + + public Trace nBytes(final short b) { + if (filled.get(16)) { + throw new IllegalStateException("shakiradata.nBYTES already set"); + } else { + filled.set(16); + } + + nBytes.putShort(b); + + return this; + } + + public Trace nBytesAcc(final long b) { + if (filled.get(17)) { + throw new IllegalStateException("shakiradata.nBYTES_ACC already set"); + } else { + filled.set(17); + } + + nBytesAcc.putLong(b); + + return this; + } + + public Trace phase(final UnsignedByte b) { + if (filled.get(10)) { + throw new IllegalStateException("shakiradata.PHASE already set"); + } else { + filled.set(10); + } + + phase.put(b.toByte()); + + return this; + } + + public Trace ripshaStamp(final long b) { + if (filled.get(11)) { + throw new IllegalStateException("shakiradata.RIPSHA_STAMP already set"); + } else { + filled.set(11); + } + + ripshaStamp.putLong(b); + + return this; + } + + public Trace selectorKeccakResHi(final Boolean b) { + if (filled.get(12)) { + throw new IllegalStateException("shakiradata.SELECTOR_KECCAK_RES_HI already set"); + } else { + filled.set(12); + } + + selectorKeccakResHi.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace selectorRipemdResHi(final Boolean b) { + if (filled.get(13)) { + throw new IllegalStateException("shakiradata.SELECTOR_RIPEMD_RES_HI already set"); + } else { + filled.set(13); + } + + selectorRipemdResHi.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace selectorSha2ResHi(final Boolean b) { + if (filled.get(14)) { + throw new IllegalStateException("shakiradata.SELECTOR_SHA2_RES_HI already set"); + } else { + filled.set(14); + } + + selectorSha2ResHi.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace totalSize(final long b) { + if (filled.get(15)) { + throw new IllegalStateException("shakiradata.TOTAL_SIZE already set"); + } else { + filled.set(15); + } + + totalSize.putLong(b); + + return this; + } + + public Trace validateRow() { + if (!filled.get(0)) { + throw new IllegalStateException("shakiradata.ID has not been filled"); + } + + if (!filled.get(1)) { + throw new IllegalStateException("shakiradata.INDEX has not been filled"); + } + + if (!filled.get(2)) { + throw new IllegalStateException("shakiradata.INDEX_MAX has not been filled"); + } + + if (!filled.get(3)) { + throw new IllegalStateException("shakiradata.IS_KECCAK_DATA has not been filled"); + } + + if (!filled.get(4)) { + throw new IllegalStateException("shakiradata.IS_KECCAK_RESULT has not been filled"); + } + + if (!filled.get(5)) { + throw new IllegalStateException("shakiradata.IS_RIPEMD_DATA has not been filled"); + } + + if (!filled.get(6)) { + throw new IllegalStateException("shakiradata.IS_RIPEMD_RESULT has not been filled"); + } + + if (!filled.get(7)) { + throw new IllegalStateException("shakiradata.IS_SHA2_DATA has not been filled"); + } + + if (!filled.get(8)) { + throw new IllegalStateException("shakiradata.IS_SHA2_RESULT has not been filled"); + } + + if (!filled.get(9)) { + throw new IllegalStateException("shakiradata.LIMB has not been filled"); + } + + if (!filled.get(16)) { + throw new IllegalStateException("shakiradata.nBYTES has not been filled"); + } + + if (!filled.get(17)) { + throw new IllegalStateException("shakiradata.nBYTES_ACC has not been filled"); + } + + if (!filled.get(10)) { + throw new IllegalStateException("shakiradata.PHASE has not been filled"); + } + + if (!filled.get(11)) { + throw new IllegalStateException("shakiradata.RIPSHA_STAMP has not been filled"); + } + + if (!filled.get(12)) { + throw new IllegalStateException("shakiradata.SELECTOR_KECCAK_RES_HI has not been filled"); + } + + if (!filled.get(13)) { + throw new IllegalStateException("shakiradata.SELECTOR_RIPEMD_RES_HI has not been filled"); + } + + if (!filled.get(14)) { + throw new IllegalStateException("shakiradata.SELECTOR_SHA2_RES_HI has not been filled"); + } + + if (!filled.get(15)) { + throw new IllegalStateException("shakiradata.TOTAL_SIZE has not been filled"); + } + + filled.clear(); + this.currentLine++; + + return this; + } + + public Trace fillAndValidateRow() { + if (!filled.get(0)) { + id.position(id.position() + 8); + } + + if (!filled.get(1)) { + index.position(index.position() + 8); + } + + if (!filled.get(2)) { + indexMax.position(indexMax.position() + 8); + } + + if (!filled.get(3)) { + isKeccakData.position(isKeccakData.position() + 1); + } + + if (!filled.get(4)) { + isKeccakResult.position(isKeccakResult.position() + 1); + } + + if (!filled.get(5)) { + isRipemdData.position(isRipemdData.position() + 1); + } + + if (!filled.get(6)) { + isRipemdResult.position(isRipemdResult.position() + 1); + } + + if (!filled.get(7)) { + isSha2Data.position(isSha2Data.position() + 1); + } + + if (!filled.get(8)) { + isSha2Result.position(isSha2Result.position() + 1); + } + + if (!filled.get(9)) { + limb.position(limb.position() + 32); + } + + if (!filled.get(16)) { + nBytes.position(nBytes.position() + 2); + } + + if (!filled.get(17)) { + nBytesAcc.position(nBytesAcc.position() + 8); + } + + if (!filled.get(10)) { + phase.position(phase.position() + 1); + } + + if (!filled.get(11)) { + ripshaStamp.position(ripshaStamp.position() + 8); + } + + if (!filled.get(12)) { + selectorKeccakResHi.position(selectorKeccakResHi.position() + 1); + } + + if (!filled.get(13)) { + selectorRipemdResHi.position(selectorRipemdResHi.position() + 1); + } + + if (!filled.get(14)) { + selectorSha2ResHi.position(selectorSha2ResHi.position() + 1); + } + + if (!filled.get(15)) { + totalSize.position(totalSize.position() + 8); + } + + filled.clear(); + this.currentLine++; + + return this; + } + + public void build() { + if (!filled.isEmpty()) { + throw new IllegalStateException("Cannot build trace with a non-validated row."); + } + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/wcp/Wcp.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/wcp/Wcp.java index daed8f2aaf..c6922bdf86 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/wcp/Wcp.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/wcp/Wcp.java @@ -119,7 +119,7 @@ public boolean callLT(final Bytes arg1, final Bytes arg2) { return this.callLT(Bytes32.leftPad(arg1), Bytes32.leftPad(arg2)); } - public boolean callLT(final int arg1, final int arg2) { + public boolean callLT(final long arg1, final long arg2) { return this.callLT(Bytes.ofUnsignedLong(arg1), Bytes.ofUnsignedLong(arg2)); } @@ -159,6 +159,10 @@ public boolean callLEQ(final Bytes32 arg1, final Bytes32 arg2) { return arg1.compareTo(arg2) <= 0; } + public boolean callLEQ(final long arg1, final long arg2) { + return this.callLEQ(Bytes.ofUnsignedLong(arg1), Bytes.ofUnsignedLong(arg2)); + } + public boolean callLEQ(final Bytes arg1, final Bytes arg2) { return this.callLEQ(Bytes32.leftPad(arg1), Bytes32.leftPad(arg2)); } diff --git a/gradle/trace-files.gradle b/gradle/trace-files.gradle index f4b62a98c3..f7408b1a81 100644 --- a/gradle/trace-files.gradle +++ b/gradle/trace-files.gradle @@ -81,7 +81,7 @@ tasks.register('binreftable', TraceFilesTask) { // // Put here modules following the conventional MODULE/columns.lisp, MODULE/constants.lisp naming scheme. // -['mmu', 'blake2fmodexpdata', 'blockdata', 'oob', 'exp', 'rlptxrcpt', 'rlpaddr'].each {moduleName -> +['mmu', 'blake2fmodexpdata', 'blockdata', 'oob', 'exp', 'rlptxrcpt', 'rlpaddr', 'shakiradata'].each {moduleName -> tasks.register(moduleName, TraceFilesTask) { group "Trace files generation" dependsOn corsetExists diff --git a/zkevm-constraints b/zkevm-constraints index 6ef21aecd2..d119b57db9 160000 --- a/zkevm-constraints +++ b/zkevm-constraints @@ -1 +1 @@ -Subproject commit 6ef21aecd28e078b99d9425c00aa3556be55bf6c +Subproject commit d119b57db9ce0657de2e931a0bc6e7855e4bd840 From c10e0da8baa834a7f5046fe861a656bbc63e7196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Bojarski?= <54240434+letypequividelespoubelles@users.noreply.github.com> Date: Mon, 27 May 2024 19:57:03 +0530 Subject: [PATCH 04/39] feat(romlex): no more empty bytecode and add codehash columns (#733) * feat(romlex): no more empty bytecode and add codehash columns * feat(constraints): update * build(constraints): update --- .../zktracer/module/romlex/RomChunk.java | 12 +-- .../linea/zktracer/module/romlex/RomLex.java | 83 ++++++++++----- .../linea/zktracer/module/romlex/Trace.java | 100 ++++++++++++++---- zkevm-constraints | 2 +- 4 files changed, 139 insertions(+), 58 deletions(-) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomChunk.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomChunk.java index 2a0ec1dc30..637c2d2540 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomChunk.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomChunk.java @@ -21,6 +21,7 @@ import static net.consensys.linea.zktracer.module.constants.GlobalConstants.EVM_INST_PUSH32; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.LLARGE; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.LLARGEMO; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.WORD_SIZE; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.WORD_SIZE_MO; import static net.consensys.linea.zktracer.types.Utils.rightPadTo; @@ -41,12 +42,9 @@ public final class RomChunk extends ModuleOperation { private static final UnsignedByte UB_LLARGE = UnsignedByte.of(LLARGE); private static final UnsignedByte UB_LLARGE_MO = UnsignedByte.of(LLARGEMO); private static final UnsignedByte UB_EVW_WORD_MO = UnsignedByte.of(WORD_SIZE_MO); - private static final int PUSH_1 = 0x60; - private static final int PUSH_32 = 0x7f; - private static final UnsignedByte INVALID = UnsignedByte.of(0xFE); - private static final int JUMPDEST = 0x5b; + private static final UnsignedByte INVALID = UnsignedByte.of(EVM_INST_INVALID); - @EqualsAndHashCode.Include private final ContractMetadata metadata; + @Getter @EqualsAndHashCode.Include private final ContractMetadata metadata; private final boolean readFromTheState; private final boolean commitToTheState; private final Bytes byteCode; @@ -69,7 +67,7 @@ public void trace(Trace trace, int cfi, int cfiInfty) { for (int i = 0; i < chunkRowSize; i++) { boolean codeSizeReached = i >= codeSize; - int sliceNumber = i / 16; + int sliceNumber = i / LLARGE; // Fill Generic columns trace @@ -171,7 +169,7 @@ public void trace(Trace trace, int cfi, int cfiInfty) { @Override protected int computeLineCount() { // WARN this is the line count used by the ROM, not by the ROMLEX - final int nPaddingRow = 32; + final int nPaddingRow = WORD_SIZE; final int codeSize = this.byteCode.size(); final int nbSlice = (codeSize + (LLARGE - 1)) / LLARGE; diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomLex.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomLex.java index 961c0e719a..ae53f3ba30 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomLex.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomLex.java @@ -15,6 +15,7 @@ package net.consensys.linea.zktracer.module.romlex; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.LLARGE; import static net.consensys.linea.zktracer.types.AddressUtils.getCreate2Address; import static net.consensys.linea.zktracer.types.AddressUtils.getCreateAddress; @@ -30,10 +31,10 @@ import net.consensys.linea.zktracer.ColumnHeader; import net.consensys.linea.zktracer.container.stacked.set.StackedSet; import net.consensys.linea.zktracer.module.Module; -import net.consensys.linea.zktracer.module.constants.GlobalConstants; import net.consensys.linea.zktracer.module.hub.Hub; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Transaction; import org.hyperledger.besu.evm.account.AccountState; import org.hyperledger.besu.evm.frame.MessageFrame; @@ -105,7 +106,7 @@ public Optional getChunkByMetadata(final ContractMetadata metadata) { @Override public void traceStartTx(WorldView worldView, Transaction tx) { // Contract creation with InitCode - if (tx.getInit().isPresent() && !tx.getInit().orElseThrow().isEmpty()) { + if (tx.getInit().isPresent() && !tx.getInit().get().isEmpty()) { final Address calledAddress = Address.contractAddress(tx.getSender(), tx.getNonce()); final RomChunk chunk = new RomChunk( @@ -121,20 +122,24 @@ public void traceStartTx(WorldView worldView, Transaction tx) { .map(AccountState::getCode) .ifPresent( code -> { - final Address calledAddress = tx.getTo().get(); - int depNumber = hub.transients().conflation().deploymentInfo().number(calledAddress); - boolean depStatus = - hub.transients().conflation().deploymentInfo().isDeploying(calledAddress); - - final RomChunk chunk = - new RomChunk( - ContractMetadata.make(calledAddress, depNumber, depStatus), - true, - false, - code); - - this.chunks.add(chunk); - this.addressRomChunkMap.put(calledAddress, chunk); + if (!code.isEmpty()) { + + final Address calledAddress = tx.getTo().get(); + int depNumber = + hub.transients().conflation().deploymentInfo().number(calledAddress); + boolean depStatus = + hub.transients().conflation().deploymentInfo().isDeploying(calledAddress); + + final RomChunk chunk = + new RomChunk( + ContractMetadata.make(calledAddress, depNumber, depStatus), + true, + false, + code); + + this.chunks.add(chunk); + this.addressRomChunkMap.put(calledAddress, chunk); + } }); } @@ -142,8 +147,10 @@ public void traceStartTx(WorldView worldView, Transaction tx) { public void tracePreOpcode(MessageFrame frame) { switch (this.hub.opCode()) { case CREATE -> { - this.address = getCreateAddress(frame); this.byteCode = this.hub.transients().op().callData(); + if (!this.byteCode.isEmpty()) { + this.address = getCreateAddress(frame); + } } case CREATE2 -> { this.byteCode = this.hub.transients().op().callData(); @@ -153,9 +160,14 @@ public void tracePreOpcode(MessageFrame frame) { } case RETURN -> { final Bytes code = hub.transients().op().returnData(); + + if (code.isEmpty()) { + return; + } + final boolean depStatus = hub.transients().conflation().deploymentInfo().isDeploying(frame.getContractAddress()); - if (!code.isEmpty() && depStatus) { + if (depStatus) { int depNumber = hub.transients().conflation().deploymentInfo().number(frame.getContractAddress()); final ContractMetadata contractMetadata = @@ -163,7 +175,7 @@ public void tracePreOpcode(MessageFrame frame) { final RomChunk chunk = new RomChunk(contractMetadata, true, false, code); this.chunks.add(chunk); - this.addressRomChunkMap.put(frame.getContractAddress(), chunk); + this.addressRomChunkMap.put(contractMetadata.address(), chunk); } } @@ -174,13 +186,21 @@ public void tracePreOpcode(MessageFrame frame) { final int depNumber = hub.transients().conflation().deploymentInfo().number(frame.getContractAddress()); - final RomChunk chunk = - new RomChunk( - ContractMetadata.make(calledAddress, depNumber, depStatus), true, false, byteCode); Optional.ofNullable(frame.getWorldUpdater().get(calledAddress)) .map(AccountState::getCode) - .ifPresent(byteCode -> this.chunks.add(chunk)); - this.addressRomChunkMap.put(calledAddress, chunk); + .ifPresent( + byteCode -> { + if (!byteCode.isEmpty()) { + final RomChunk chunk = + new RomChunk( + ContractMetadata.make(calledAddress, depNumber, depStatus), + true, + false, + byteCode); + chunks.add(chunk); + this.addressRomChunkMap.put(calledAddress, chunk); + } + }); } case EXTCODECOPY -> { @@ -193,12 +213,13 @@ public void tracePreOpcode(MessageFrame frame) { } final int depNumber = hub.transients().conflation().deploymentInfo().number(frame.getContractAddress()); + Optional.ofNullable(frame.getWorldUpdater().get(calledAddress)) .map(AccountState::getCode) .ifPresent( byteCode -> { if (!byteCode.isEmpty()) { - RomChunk chunk = + final RomChunk chunk = new RomChunk( ContractMetadata.make(calledAddress, depNumber, false), true, @@ -215,6 +236,9 @@ public void tracePreOpcode(MessageFrame frame) { @Override public void tracePostOpcode(MessageFrame frame) { + if (byteCode.isEmpty()) { + return; + } switch (hub.opCode()) { case CREATE, CREATE2 -> { final int depNumber = hub.transients().conflation().deploymentInfo().number(this.address); @@ -231,18 +255,23 @@ public void tracePostOpcode(MessageFrame frame) { } } + // This is the tracing for ROMLEX module private void traceChunk( - final RomChunk chunk, int cfi, int codeFragmentIndexInfinity, Trace trace) { + final RomChunk chunk, final int cfi, final int codeFragmentIndexInfinity, Trace trace) { + final Hash codeHash = + chunk.metadata().underDeployment() ? Hash.EMPTY : Hash.hash(chunk.byteCode()); trace .codeFragmentIndex(cfi) .codeFragmentIndexInfty(codeFragmentIndexInfinity) .codeSize(chunk.byteCode().size()) .addressHi(chunk.metadata().address().slice(0, 4).toLong()) - .addressLo(chunk.metadata().address().slice(4, GlobalConstants.LLARGE)) + .addressLo(chunk.metadata().address().slice(4, LLARGE)) .commitToState(chunk.commitToTheState()) .deploymentNumber(chunk.metadata().deploymentNumber()) .deploymentStatus(chunk.metadata().underDeployment()) .readFromState(chunk.readFromTheState()) + .codeHashHi(codeHash.slice(0, LLARGE)) + .codeHashLo(codeHash.slice(LLARGE, LLARGE)) .validateRow(); } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/Trace.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/Trace.java index 702a049dcb..29643aee75 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/Trace.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/Trace.java @@ -37,6 +37,8 @@ public class Trace { private final MappedByteBuffer addressLo; private final MappedByteBuffer codeFragmentIndex; private final MappedByteBuffer codeFragmentIndexInfty; + private final MappedByteBuffer codeHashHi; + private final MappedByteBuffer codeHashLo; private final MappedByteBuffer codeSize; private final MappedByteBuffer commitToState; private final MappedByteBuffer deploymentNumber; @@ -49,6 +51,8 @@ static List headers(int length) { new ColumnHeader("romlex.ADDRESS_LO", 32, length), new ColumnHeader("romlex.CODE_FRAGMENT_INDEX", 8, length), new ColumnHeader("romlex.CODE_FRAGMENT_INDEX_INFTY", 8, length), + new ColumnHeader("romlex.CODE_HASH_HI", 32, length), + new ColumnHeader("romlex.CODE_HASH_LO", 32, length), new ColumnHeader("romlex.CODE_SIZE", 8, length), new ColumnHeader("romlex.COMMIT_TO_STATE", 1, length), new ColumnHeader("romlex.DEPLOYMENT_NUMBER", 4, length), @@ -61,11 +65,13 @@ public Trace(List buffers) { this.addressLo = buffers.get(1); this.codeFragmentIndex = buffers.get(2); this.codeFragmentIndexInfty = buffers.get(3); - this.codeSize = buffers.get(4); - this.commitToState = buffers.get(5); - this.deploymentNumber = buffers.get(6); - this.deploymentStatus = buffers.get(7); - this.readFromState = buffers.get(8); + this.codeHashHi = buffers.get(4); + this.codeHashLo = buffers.get(5); + this.codeSize = buffers.get(6); + this.commitToState = buffers.get(7); + this.deploymentNumber = buffers.get(8); + this.deploymentStatus = buffers.get(9); + this.readFromState = buffers.get(10); } public int size() { @@ -128,23 +134,55 @@ public Trace codeFragmentIndexInfty(final long b) { return this; } - public Trace codeSize(final long b) { + public Trace codeHashHi(final Bytes b) { if (filled.get(4)) { - throw new IllegalStateException("romlex.CODE_SIZE already set"); + throw new IllegalStateException("romlex.CODE_HASH_HI already set"); } else { filled.set(4); } + final byte[] bs = b.toArrayUnsafe(); + for (int i = bs.length; i < 32; i++) { + codeHashHi.put((byte) 0); + } + codeHashHi.put(b.toArrayUnsafe()); + + return this; + } + + public Trace codeHashLo(final Bytes b) { + if (filled.get(5)) { + throw new IllegalStateException("romlex.CODE_HASH_LO already set"); + } else { + filled.set(5); + } + + final byte[] bs = b.toArrayUnsafe(); + for (int i = bs.length; i < 32; i++) { + codeHashLo.put((byte) 0); + } + codeHashLo.put(b.toArrayUnsafe()); + + return this; + } + + public Trace codeSize(final long b) { + if (filled.get(6)) { + throw new IllegalStateException("romlex.CODE_SIZE already set"); + } else { + filled.set(6); + } + codeSize.putLong(b); return this; } public Trace commitToState(final Boolean b) { - if (filled.get(5)) { + if (filled.get(7)) { throw new IllegalStateException("romlex.COMMIT_TO_STATE already set"); } else { - filled.set(5); + filled.set(7); } commitToState.put((byte) (b ? 1 : 0)); @@ -153,10 +191,10 @@ public Trace commitToState(final Boolean b) { } public Trace deploymentNumber(final int b) { - if (filled.get(6)) { + if (filled.get(8)) { throw new IllegalStateException("romlex.DEPLOYMENT_NUMBER already set"); } else { - filled.set(6); + filled.set(8); } deploymentNumber.putInt(b); @@ -165,10 +203,10 @@ public Trace deploymentNumber(final int b) { } public Trace deploymentStatus(final Boolean b) { - if (filled.get(7)) { + if (filled.get(9)) { throw new IllegalStateException("romlex.DEPLOYMENT_STATUS already set"); } else { - filled.set(7); + filled.set(9); } deploymentStatus.put((byte) (b ? 1 : 0)); @@ -177,10 +215,10 @@ public Trace deploymentStatus(final Boolean b) { } public Trace readFromState(final Boolean b) { - if (filled.get(8)) { + if (filled.get(10)) { throw new IllegalStateException("romlex.READ_FROM_STATE already set"); } else { - filled.set(8); + filled.set(10); } readFromState.put((byte) (b ? 1 : 0)); @@ -206,22 +244,30 @@ public Trace validateRow() { } if (!filled.get(4)) { - throw new IllegalStateException("romlex.CODE_SIZE has not been filled"); + throw new IllegalStateException("romlex.CODE_HASH_HI has not been filled"); } if (!filled.get(5)) { - throw new IllegalStateException("romlex.COMMIT_TO_STATE has not been filled"); + throw new IllegalStateException("romlex.CODE_HASH_LO has not been filled"); } if (!filled.get(6)) { - throw new IllegalStateException("romlex.DEPLOYMENT_NUMBER has not been filled"); + throw new IllegalStateException("romlex.CODE_SIZE has not been filled"); } if (!filled.get(7)) { - throw new IllegalStateException("romlex.DEPLOYMENT_STATUS has not been filled"); + throw new IllegalStateException("romlex.COMMIT_TO_STATE has not been filled"); } if (!filled.get(8)) { + throw new IllegalStateException("romlex.DEPLOYMENT_NUMBER has not been filled"); + } + + if (!filled.get(9)) { + throw new IllegalStateException("romlex.DEPLOYMENT_STATUS has not been filled"); + } + + if (!filled.get(10)) { throw new IllegalStateException("romlex.READ_FROM_STATE has not been filled"); } @@ -249,22 +295,30 @@ public Trace fillAndValidateRow() { } if (!filled.get(4)) { - codeSize.position(codeSize.position() + 8); + codeHashHi.position(codeHashHi.position() + 32); } if (!filled.get(5)) { - commitToState.position(commitToState.position() + 1); + codeHashLo.position(codeHashLo.position() + 32); } if (!filled.get(6)) { - deploymentNumber.position(deploymentNumber.position() + 4); + codeSize.position(codeSize.position() + 8); } if (!filled.get(7)) { - deploymentStatus.position(deploymentStatus.position() + 1); + commitToState.position(commitToState.position() + 1); } if (!filled.get(8)) { + deploymentNumber.position(deploymentNumber.position() + 4); + } + + if (!filled.get(9)) { + deploymentStatus.position(deploymentStatus.position() + 1); + } + + if (!filled.get(10)) { readFromState.position(readFromState.position() + 1); } diff --git a/zkevm-constraints b/zkevm-constraints index d119b57db9..fc5593deaa 160000 --- a/zkevm-constraints +++ b/zkevm-constraints @@ -1 +1 @@ -Subproject commit d119b57db9ce0657de2e931a0bc6e7855e4bd840 +Subproject commit fc5593deaac98fd758f5b3646623e352095fc36c From 66b13c3b5be1001b34c7f153eb94c64ab8c7dbd8 Mon Sep 17 00:00:00 2001 From: Lorenzo Gentile Date: Mon, 27 May 2024 17:27:43 +0200 Subject: [PATCH 05/39] fix(mxp): used constants (#735) --- .../linea/zktracer/module/mxp/MxpData.java | 85 ++++++++++--------- .../linea/zktracer/module/mxp/Trace.java | 4 + gradle/trace-files.gradle | 4 +- zkevm-constraints | 2 +- 4 files changed, 50 insertions(+), 45 deletions(-) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/MxpData.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/MxpData.java index 6d8f213450..0654dba5df 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/MxpData.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/MxpData.java @@ -16,6 +16,9 @@ package net.consensys.linea.zktracer.module.mxp; import static net.consensys.linea.zktracer.module.Util.max; +import static net.consensys.linea.zktracer.module.mxp.Trace.CT_MAX_NON_TRIVIAL; +import static net.consensys.linea.zktracer.module.mxp.Trace.CT_MAX_NON_TRIVIAL_BUT_MXPX; +import static net.consensys.linea.zktracer.module.mxp.Trace.CT_MAX_TRIVIAL; import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; import static org.hyperledger.besu.evm.internal.Words.clampedAdd; import static org.hyperledger.besu.evm.internal.Words.clampedMultiply; @@ -47,8 +50,6 @@ public class MxpData extends ModuleOperation { public static final BigInteger TWO_POW_128 = BigInteger.ONE.shiftLeft(128); public static final BigInteger TWO_POW_32 = BigInteger.ONE.shiftLeft(32); - // constants from protocol_params.go - private final OpCodeData opCodeData; private final int contextNumber; private EWord offset1 = EWord.ZERO; @@ -114,7 +115,7 @@ public MxpData(final MessageFrame frame, final Hub hub) { @Override protected int computeLineCount() { - return this.maxCt(); + return this.ctMax(); } void compute() { @@ -130,15 +131,15 @@ void compute() { } private void setInitializeByteArrays() { - byte1 = new UnsignedByte[maxCt()]; - byte2 = new UnsignedByte[maxCt()]; - byte3 = new UnsignedByte[maxCt()]; - byte4 = new UnsignedByte[maxCt()]; - byteA = new UnsignedByte[maxCt()]; - byteW = new UnsignedByte[maxCt()]; - byteQ = new UnsignedByte[maxCt()]; - byteQQ = new UnsignedByte[maxCt()]; - byteR = new UnsignedByte[maxCt()]; + byte1 = new UnsignedByte[ctMax()]; + byte2 = new UnsignedByte[ctMax()]; + byte3 = new UnsignedByte[ctMax()]; + byte4 = new UnsignedByte[ctMax()]; + byteA = new UnsignedByte[ctMax()]; + byteW = new UnsignedByte[ctMax()]; + byteQ = new UnsignedByte[ctMax()]; + byteQQ = new UnsignedByte[ctMax()]; + byteR = new UnsignedByte[ctMax()]; Arrays.fill(byte1, UnsignedByte.of(0)); Arrays.fill(byte2, UnsignedByte.of(0)); Arrays.fill(byte3, UnsignedByte.of(0)); @@ -380,11 +381,11 @@ private mxpExecutionPath getMxpExecutionPath() { return mxpExecutionPath.NON_TRIVIAL; } - public int maxCt() { + public int ctMax() { return switch (this.getMxpExecutionPath()) { - case TRIVIAL -> 1; - case NON_TRIVIAL_BUT_MXPX -> 17; - case NON_TRIVIAL -> 4; + case TRIVIAL -> CT_MAX_TRIVIAL + 1; + case NON_TRIVIAL_BUT_MXPX -> CT_MAX_NON_TRIVIAL_BUT_MXPX + 1; + case NON_TRIVIAL -> CT_MAX_NON_TRIVIAL + 1; }; } @@ -413,7 +414,7 @@ protected void setAccQAndByteQQNonTrivialCase() { } protected void setBytes() { - int maxCt = maxCt(); + int ctMax = ctMax(); Bytes32 b1 = UInt256.valueOf(acc1); Bytes32 b2 = UInt256.valueOf(acc2); Bytes32 b3 = UInt256.valueOf(acc3); @@ -421,14 +422,14 @@ protected void setBytes() { Bytes32 bA = UInt256.valueOf(accA); Bytes32 bW = UInt256.valueOf(accW); Bytes32 bQ = UInt256.valueOf(accQ); - for (int i = 0; i < maxCt; i++) { - byte1[i] = UnsignedByte.of(b1.get(b1.size() - 1 - maxCt + i)); - byte2[i] = UnsignedByte.of(b2.get(b2.size() - 1 - maxCt + i)); - byte3[i] = UnsignedByte.of(b3.get(b3.size() - 1 - maxCt + i)); - byte4[i] = UnsignedByte.of(b4.get(b4.size() - 1 - maxCt + i)); - byteA[i] = UnsignedByte.of(bA.get(bA.size() - 1 - maxCt + i)); - byteW[i] = UnsignedByte.of(bW.get(bW.size() - 1 - maxCt + i)); - byteQ[i] = UnsignedByte.of(bQ.get(bQ.size() - 1 - maxCt + i)); + for (int i = 0; i < ctMax; i++) { + byte1[i] = UnsignedByte.of(b1.get(b1.size() - 1 - ctMax + i)); + byte2[i] = UnsignedByte.of(b2.get(b2.size() - 1 - ctMax + i)); + byte3[i] = UnsignedByte.of(b3.get(b3.size() - 1 - ctMax + i)); + byte4[i] = UnsignedByte.of(b4.get(b4.size() - 1 - ctMax + i)); + byteA[i] = UnsignedByte.of(bA.get(bA.size() - 1 - ctMax + i)); + byteW[i] = UnsignedByte.of(bW.get(bW.size() - 1 - ctMax + i)); + byteQ[i] = UnsignedByte.of(bQ.get(bQ.size() - 1 - ctMax + i)); } } @@ -494,10 +495,10 @@ final void trace(int stamp, Trace trace) { final EWord eSize1 = EWord.of(this.size1); final EWord eSize2 = EWord.of(this.size2); - int maxCt = this.maxCt(); - int maxCtComplement = 32 - maxCt; + int ctMax = this.ctMax(); + int ctMaxComplement = 32 - ctMax; - for (int i = 0; i < maxCt; i++) { + for (int i = 0; i < ctMax; i++) { trace .stamp(stamp) .cn(Bytes.ofUnsignedLong(this.getContextNumber())) @@ -534,20 +535,20 @@ final void trace(int stamp, Trace trace) { .maxOffset2(bigIntegerToBytes(this.getMaxOffset2())) .maxOffset(bigIntegerToBytes(this.getMaxOffset())) .comp(this.isComp()) - .acc1(acc1Bytes32.slice(maxCtComplement, 1 + i)) - .acc2(acc2Bytes32.slice(maxCtComplement, 1 + i)) - .acc3(acc3Bytes32.slice(maxCtComplement, 1 + i)) - .acc4(acc4Bytes32.slice(maxCtComplement, 1 + i)) - .accA(accABytes32.slice(maxCtComplement, 1 + i)) - .accW(accWBytes32.slice(maxCtComplement, 1 + i)) - .accQ(accQBytes32.slice(maxCtComplement, 1 + i)) - .byte1(UnsignedByte.of(acc1Bytes32.get(maxCtComplement + i))) - .byte2(UnsignedByte.of(acc2Bytes32.get(maxCtComplement + i))) - .byte3(UnsignedByte.of(acc3Bytes32.get(maxCtComplement + i))) - .byte4(UnsignedByte.of(acc4Bytes32.get(maxCtComplement + i))) - .byteA(UnsignedByte.of(accABytes32.get(maxCtComplement + i))) - .byteW(UnsignedByte.of(accWBytes32.get(maxCtComplement + i))) - .byteQ(UnsignedByte.of(accQBytes32.get(maxCtComplement + i))) + .acc1(acc1Bytes32.slice(ctMaxComplement, 1 + i)) + .acc2(acc2Bytes32.slice(ctMaxComplement, 1 + i)) + .acc3(acc3Bytes32.slice(ctMaxComplement, 1 + i)) + .acc4(acc4Bytes32.slice(ctMaxComplement, 1 + i)) + .accA(accABytes32.slice(ctMaxComplement, 1 + i)) + .accW(accWBytes32.slice(ctMaxComplement, 1 + i)) + .accQ(accQBytes32.slice(ctMaxComplement, 1 + i)) + .byte1(UnsignedByte.of(acc1Bytes32.get(ctMaxComplement + i))) + .byte2(UnsignedByte.of(acc2Bytes32.get(ctMaxComplement + i))) + .byte3(UnsignedByte.of(acc3Bytes32.get(ctMaxComplement + i))) + .byte4(UnsignedByte.of(acc4Bytes32.get(ctMaxComplement + i))) + .byteA(UnsignedByte.of(accABytes32.get(ctMaxComplement + i))) + .byteW(UnsignedByte.of(accWBytes32.get(ctMaxComplement + i))) + .byteQ(UnsignedByte.of(accQBytes32.get(ctMaxComplement + i))) .byteQq(UnsignedByte.of(this.getByteQQ()[i].toInteger())) .byteR(UnsignedByte.of(this.getByteR()[i].toInteger())) .words(Bytes.ofUnsignedLong(this.getWords())) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/Trace.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/Trace.java index 0dc2211f35..6400b46c42 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/Trace.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/Trace.java @@ -30,6 +30,10 @@ * Please DO NOT ATTEMPT TO MODIFY this code directly. */ public class Trace { + public static final int CT_MAX_NON_TRIVIAL = 0x3; + public static final int CT_MAX_NON_TRIVIAL_BUT_MXPX = 0x10; + public static final int CT_MAX_TRIVIAL = 0x0; + public static final long TWO_POW_32 = 0x100000000L; private final BitSet filled = new BitSet(); private int currentLine = 0; diff --git a/gradle/trace-files.gradle b/gradle/trace-files.gradle index f7408b1a81..af85ca6be3 100644 --- a/gradle/trace-files.gradle +++ b/gradle/trace-files.gradle @@ -68,7 +68,7 @@ tasks.register('binreftable', TraceFilesTask) { // // Put here modules following the conventional MODULE/columns.lisp naming scheme. // -['bin', 'blockhash', 'ecdata', 'euc', 'mmio', 'mxp', 'shf', 'wcp', 'rlptxn', 'rom', 'romlex', 'trm', 'stp', 'loginfo', 'logdata'].each {moduleName -> +['bin', 'blockhash', 'ecdata', 'euc', 'mmio', 'shf', 'wcp', 'rlptxn', 'rom', 'romlex', 'trm', 'stp', 'loginfo', 'logdata'].each {moduleName -> tasks.register(moduleName, TraceFilesTask) { group "Trace files generation" dependsOn corsetExists @@ -81,7 +81,7 @@ tasks.register('binreftable', TraceFilesTask) { // // Put here modules following the conventional MODULE/columns.lisp, MODULE/constants.lisp naming scheme. // -['mmu', 'blake2fmodexpdata', 'blockdata', 'oob', 'exp', 'rlptxrcpt', 'rlpaddr', 'shakiradata'].each {moduleName -> +['mmu', 'blake2fmodexpdata', 'blockdata', 'oob', 'exp', 'rlptxrcpt', 'rlpaddr', 'shakiradata', 'mxp'].each {moduleName -> tasks.register(moduleName, TraceFilesTask) { group "Trace files generation" dependsOn corsetExists diff --git a/zkevm-constraints b/zkevm-constraints index fc5593deaa..e4a3e05df4 160000 --- a/zkevm-constraints +++ b/zkevm-constraints @@ -1 +1 @@ -Subproject commit fc5593deaac98fd758f5b3646623e352095fc36c +Subproject commit e4a3e05df4ff8f92b0cdf2afaed83ea6a856ff3f From 0759971617c621567e52701a7867be7b1696b166 Mon Sep 17 00:00:00 2001 From: Lorenzo Gentile Date: Tue, 28 May 2024 15:24:25 +0200 Subject: [PATCH 06/39] feat(ecdata): implemented and tested ecrecover precompile (#732) * feat(ecdata): updated trace file and partially Operation * feat(ecdata): managed ecrecover case and common columns * fix(ecdata): fixed bugs related to assemble method and lookups * feat(ecdata): fixed BYTE_DELTA * feat(ecdata): computed recoveredAddress and made tests parametric * feat(ecdata): printed recovered address for debugging purposes * feat(ecdata): completed test of ecrecover * feat(ecdata): added code for id debugging * fix(ecdata): solved bug related to usage of Bytes.of * fix(ecdata): update constraints submodule reference * fix(shfreftable): turned MSHP into an UnsignedByte * fix(ecdata): previousId --------- Co-authored-by: Francois Bojarski --- .../linea/zktracer/module/ecdata/EcData.java | 49 +- .../module/ecdata/EcDataOperation.java | 529 ++++++---- .../linea/zktracer/module/ecdata/Trace.java | 963 +++++++++++++----- .../linea/zktracer/module/hub/Hub.java | 4 +- .../consensys/linea/zktracer/types/EWord.java | 16 + .../module/{ => ecdata}/EcDataTest.java | 202 +++- .../zktracer/testing/BytecodeRunner.java | 22 +- .../testing/ToyExecutionEnvironment.java | 5 + gradle/trace-files.gradle | 4 +- 9 files changed, 1315 insertions(+), 479 deletions(-) rename arithmetization/src/test/java/net/consensys/linea/zktracer/module/{ => ecdata}/EcDataTest.java (57%) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/EcData.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/EcData.java index 204595374c..dfdb9a05d4 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/EcData.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/EcData.java @@ -16,9 +16,11 @@ package net.consensys.linea.zktracer.module.ecdata; import java.nio.MappedByteBuffer; +import java.util.Comparator; import java.util.List; import java.util.Set; +import lombok.Getter; import lombok.RequiredArgsConstructor; import net.consensys.linea.zktracer.ColumnHeader; import net.consensys.linea.zktracer.container.stacked.set.StackedSet; @@ -35,13 +37,13 @@ @RequiredArgsConstructor public class EcData implements Module { public static final Set

EC_PRECOMPILES = - Set.of(Address.ECREC, Address.ALTBN128_ADD, Address.ALTBN128_MUL, Address.ALTBN128_PAIRING); + Set.of(Address.ECREC); // TODO: add later Address.ALTBN128_ADD, Address.ALTBN128_MUL, + // Address.ALTBN128_PAIRING); - private final StackedSet operations = new StackedSet<>(); + @Getter private final StackedSet operations = new StackedSet<>(); private final Hub hub; private final Wcp wcp; private final Ext ext; - private int previousContextNumber = 0; @Override public String moduleKey() { @@ -60,28 +62,20 @@ public void popTransaction() { @Override public void tracePreOpcode(MessageFrame frame) { - final Address to = Words.toAddress(frame.getStackItem(1)); - if (!EC_PRECOMPILES.contains(to)) { + final Address target = Words.toAddress(frame.getStackItem(1)); + if (!EC_PRECOMPILES.contains(target)) { return; } final MemorySpan callDataSource = hub.transients().op().callDataSegment(); - if (to.equals(Address.ALTBN128_PAIRING) + if (target.equals(Address.ALTBN128_PAIRING) && (callDataSource.isEmpty() || callDataSource.length() % 192 != 0)) { return; } - final Bytes input = hub.transients().op().callData(); - + final Bytes data = hub.transients().op().callData(); this.operations.add( - EcDataOperation.of( - this.wcp, - this.ext, - to, - input, - this.hub.currentFrame().contextNumber(), - this.previousContextNumber)); - this.previousContextNumber = this.hub.currentFrame().contextNumber(); + EcDataOperation.of(this.wcp, this.ext, 1 + this.hub.stamp(), target.get(19), data)); } @Override @@ -97,8 +91,27 @@ public List columnsHeaders() { @Override public void commit(List buffers) { final Trace trace = new Trace(buffers); - for (EcDataOperation op : this.operations) { - op.trace(trace); + int stamp = 0; + long previousId = 0; + + List sortedOperations = + this.operations.stream().sorted(Comparator.comparingLong(EcDataOperation::id)).toList(); + + for (EcDataOperation op : sortedOperations) { + /* + System.out.println( + "(tracing time) previousId: " + + Integer.toHexString(op.previousId()) + + " -> id: " + + Integer.toHexString(op.id()) + + " , byteDelta: " + + Arrays.stream(op.byteDelta()).map(b -> Integer.toHexString(b.toInteger())).toList() + + " , diff: " + + Integer.toHexString(op.id() - op.previousId() - 1)); + */ + stamp++; + op.trace(trace, stamp, previousId); + previousId = op.id(); } } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/EcDataOperation.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/EcDataOperation.java index 7530d8f7cf..afd9e4d02f 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/EcDataOperation.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/EcDataOperation.java @@ -15,227 +15,348 @@ package net.consensys.linea.zktracer.module.ecdata; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_ECADD_DATA; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_ECADD_RESULT; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_ECMUL_DATA; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_ECMUL_RESULT; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_ECPAIRING_DATA; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_ECPAIRING_RESULT; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_ECRECOVER_DATA; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_ECRECOVER_RESULT; +import static net.consensys.linea.zktracer.module.ecdata.Trace.ECADD; +import static net.consensys.linea.zktracer.module.ecdata.Trace.ECMUL; +import static net.consensys.linea.zktracer.module.ecdata.Trace.ECPAIRING; +import static net.consensys.linea.zktracer.module.ecdata.Trace.ECRECOVER; +import static net.consensys.linea.zktracer.module.ecdata.Trace.INDEX_MAX_ECADD_DATA; +import static net.consensys.linea.zktracer.module.ecdata.Trace.INDEX_MAX_ECADD_RESULT; +import static net.consensys.linea.zktracer.module.ecdata.Trace.INDEX_MAX_ECMUL_DATA; +import static net.consensys.linea.zktracer.module.ecdata.Trace.INDEX_MAX_ECMUL_RESULT; +import static net.consensys.linea.zktracer.module.ecdata.Trace.INDEX_MAX_ECPAIRING_DATA_MIN; +import static net.consensys.linea.zktracer.module.ecdata.Trace.INDEX_MAX_ECPAIRING_RESULT; +import static net.consensys.linea.zktracer.module.ecdata.Trace.INDEX_MAX_ECRECOVER_DATA; +import static net.consensys.linea.zktracer.module.ecdata.Trace.INDEX_MAX_ECRECOVER_RESULT; +import static net.consensys.linea.zktracer.module.ecdata.Trace.P_BN_HI; +import static net.consensys.linea.zktracer.module.ecdata.Trace.P_BN_LO; +import static net.consensys.linea.zktracer.module.ecdata.Trace.SECP256K1N_HI; +import static net.consensys.linea.zktracer.module.ecdata.Trace.SECP256K1N_LO; +import static net.consensys.linea.zktracer.module.ecdata.Trace.TOTAL_SIZE_ECADD_DATA; +import static net.consensys.linea.zktracer.module.ecdata.Trace.TOTAL_SIZE_ECADD_RESULT; +import static net.consensys.linea.zktracer.module.ecdata.Trace.TOTAL_SIZE_ECMUL_DATA; +import static net.consensys.linea.zktracer.module.ecdata.Trace.TOTAL_SIZE_ECMUL_RESULT; +import static net.consensys.linea.zktracer.module.ecdata.Trace.TOTAL_SIZE_ECPAIRING_DATA_MIN; +import static net.consensys.linea.zktracer.module.ecdata.Trace.TOTAL_SIZE_ECPAIRING_RESULT; +import static net.consensys.linea.zktracer.module.ecdata.Trace.TOTAL_SIZE_ECRECOVER_DATA; +import static net.consensys.linea.zktracer.module.ecdata.Trace.TOTAL_SIZE_ECRECOVER_RESULT; import static net.consensys.linea.zktracer.types.Containers.repeat; import static net.consensys.linea.zktracer.types.Utils.leftPadTo; import java.util.List; +import java.util.Optional; import java.util.Set; import com.google.common.base.Preconditions; +import lombok.Getter; import lombok.experimental.Accessors; -import net.consensys.linea.zktracer.CurveOperations; import net.consensys.linea.zktracer.container.ModuleOperation; import net.consensys.linea.zktracer.module.ext.Ext; import net.consensys.linea.zktracer.module.wcp.Wcp; import net.consensys.linea.zktracer.opcode.OpCode; import net.consensys.linea.zktracer.types.EWord; +import net.consensys.linea.zktracer.types.UnsignedByte; import org.apache.tuweni.bytes.Bytes; -import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.crypto.Hash; +import org.hyperledger.besu.crypto.SECP256K1; +import org.hyperledger.besu.crypto.SECPPublicKey; +import org.hyperledger.besu.crypto.SECPSignature; @Accessors(fluent = true) public class EcDataOperation extends ModuleOperation { - private static final int EC_RECOVER = 1; - private static final int EC_ADD = 6; - private static final int EC_MUL = 7; - private static final int EC_PAIRING = 8; - private static final Set EC_TYPES = Set.of(EC_RECOVER, EC_ADD, EC_MUL, EC_PAIRING); - - private static final EWord P = - EWord.ofHexString("0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47"); - private static final EWord Q = - EWord.ofHexString("0x30644E72E131A029B85045B68181585D2833E84879B9709143E1F593F0000001"); - private static final EWord SECP256_K1N = - EWord.ofHexString("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"); + private static final Set EC_TYPES = Set.of(ECRECOVER, ECADD, ECMUL, ECPAIRING); + private static final EWord P_BN = EWord.of(P_BN_HI, P_BN_LO); + public static final EWord SECP256K1N = EWord.of(SECP256K1N_HI, SECP256K1N_LO); + public static final int nBYTES_OF_DELTA_BYTES = 4; // TODO: from Corset ? private final Wcp wcp; private final Ext ext; - private final int contextNumber; - private final int contextNumberDelta; - private final Bytes input; + @Getter private final long id; + private final Bytes data; private final int ecType; private final int nRows; - /** -1 if no switch off (i.e. preliminary checks passed) */ - private int hurdleSwitchOffRow; + private final int nRowsData; + private final int nRowsResult; - private final List comparisons; - private final List equalities; - private final List squares; - private final List cubes; - private final List limbs; + @Getter private final List limb; + private final List hurdle; + @Getter private boolean internalChecksPassed; // WCP interaction - private final List wcpArg1; - private final List wcpArg2; - private final List wcpInst; + private final List wcpFlag; + private final List wcpArg1Hi; + private final List wcpArg1Lo; + private final List wcpArg2Hi; + private final List wcpArg2Lo; private final List wcpRes; + private final List wcpInst; // EXT interaction - private final List extArg1; - private final List extArg2; - private final List extArg3; + private final List extFlag; + private final List extArg1Hi; + private final List extArg1Lo; + private final List extArg2Hi; + private final List extArg2Lo; + private final List extArg3Hi; + private final List extArg3Lo; + private final List extResHi; + private final List extResLo; private final List extInst; - private final List extRes; + + private Bytes returnData; + @Getter private boolean successBit; + private boolean circuitSelectorEcrecover; // pairing-specific - private int thisIsNotOnG2Index; - private final int nPairings; - - private static int addressToEcType(final Address target) { - if (target.equals(Address.ECREC)) { - return EC_RECOVER; - } else if (target.equals(Address.ALTBN128_ADD)) { - return EC_ADD; - } else if (target.equals(Address.ALTBN128_MUL)) { - return EC_MUL; - } else if (target.equals(Address.ALTBN128_PAIRING)) { - return EC_PAIRING; + private final int totalPairings; + + private int getTotalSize(int ecType, boolean isData) { + if (isData) { + return switch (ecType) { + case ECRECOVER -> TOTAL_SIZE_ECRECOVER_DATA; + case ECADD -> TOTAL_SIZE_ECADD_DATA; + case ECMUL -> TOTAL_SIZE_ECMUL_DATA; + case ECPAIRING -> TOTAL_SIZE_ECPAIRING_DATA_MIN * this.totalPairings; + default -> throw new IllegalArgumentException("invalid EC type"); + }; } else { - throw new IllegalArgumentException("invalid EC address"); + return switch (ecType) { + case ECRECOVER -> successBit ? TOTAL_SIZE_ECRECOVER_RESULT : 0; + case ECADD -> successBit ? TOTAL_SIZE_ECADD_RESULT : 0; + case ECMUL -> successBit ? TOTAL_SIZE_ECMUL_RESULT : 0; + case ECPAIRING -> successBit ? TOTAL_SIZE_ECPAIRING_RESULT : 0; + default -> throw new IllegalArgumentException("invalid EC type"); + }; } } - private static int ecTypeToNRows(int ecType, final Bytes input) { - return switch (ecType) { - case EC_RECOVER, EC_ADD -> 8; - case EC_MUL -> 6; - case EC_PAIRING -> input.size() / 16; - default -> throw new IllegalArgumentException("invalid EC type"); - }; + private static short getPhase(int ecType, boolean isData) { + if (isData) { + return switch (ecType) { + case ECRECOVER -> PHASE_ECRECOVER_DATA; + case ECADD -> PHASE_ECADD_DATA; + case ECMUL -> PHASE_ECMUL_DATA; + case ECPAIRING -> PHASE_ECPAIRING_DATA; + default -> throw new IllegalArgumentException("invalid EC type"); + }; + } else { + return switch (ecType) { + case ECRECOVER -> PHASE_ECRECOVER_RESULT; + case ECADD -> PHASE_ECADD_RESULT; + case ECMUL -> PHASE_ECMUL_RESULT; + case ECPAIRING -> PHASE_ECPAIRING_RESULT; + default -> throw new IllegalArgumentException("invalid EC type"); + }; + } + } + + private int getIndexMax(int ecType, boolean isData) { + if (isData) { + return switch (ecType) { + case ECRECOVER -> INDEX_MAX_ECRECOVER_DATA; + case ECADD -> INDEX_MAX_ECADD_DATA; + case ECMUL -> INDEX_MAX_ECMUL_DATA; + case ECPAIRING -> (INDEX_MAX_ECPAIRING_DATA_MIN + 1) * this.totalPairings - 1; + default -> throw new IllegalArgumentException("invalid EC type"); + }; + } else { + return switch (ecType) { + case ECRECOVER -> INDEX_MAX_ECRECOVER_RESULT; + case ECADD -> INDEX_MAX_ECADD_RESULT; + case ECMUL -> INDEX_MAX_ECMUL_RESULT; + case ECPAIRING -> INDEX_MAX_ECPAIRING_RESULT; + default -> throw new IllegalArgumentException("invalid EC type"); + }; + } } - private EcDataOperation( - Wcp wcp, Ext ext, int contextNumber, int previousContextNumber, int ecType, Bytes input) { + private EcDataOperation(Wcp wcp, Ext ext, int id, int ecType, Bytes data) { Preconditions.checkArgument(EC_TYPES.contains(ecType), "invalid EC type"); - final int nRows = ecTypeToNRows(ecType, input); - final int minInputLength = ecType == EC_MUL ? 96 : 128; - if (input.size() < minInputLength) { - this.input = leftPadTo(input, minInputLength); + final int minInputLength = ecType == ECMUL ? 96 : 128; + if (data.size() < minInputLength) { + this.data = leftPadTo(data, minInputLength); } else { - this.input = input; + this.data = data; } - this.contextNumber = contextNumber; - this.contextNumberDelta = contextNumber - previousContextNumber; this.ecType = ecType; - this.nRows = nRows; - this.thisIsNotOnG2Index = -1; - if (ecType == EC_PAIRING) { - this.nPairings = input.size() / 192; + + if (ecType == ECPAIRING) { + this.totalPairings = data.size() / 192; } else { - this.nPairings = 0; + this.totalPairings = 0; } - this.comparisons = repeat(false, nRows / 2); - this.equalities = repeat(false, nRows); - this.squares = repeat(Bytes.EMPTY, nRows / 2); - this.cubes = repeat(Bytes.EMPTY, nRows / 2); - this.limbs = repeat(Bytes.EMPTY, nRows / 2); - - this.wcpArg1 = repeat(Bytes.EMPTY, nRows); - this.wcpArg2 = repeat(Bytes.EMPTY, nRows); - this.wcpInst = repeat(OpCode.INVALID, nRows); - this.wcpRes = repeat(false, nRows); - - this.extArg1 = repeat(Bytes.EMPTY, nRows); - this.extArg2 = repeat(Bytes.EMPTY, nRows); - this.extArg3 = repeat(Bytes.EMPTY, nRows); - this.extInst = repeat(OpCode.INVALID, nRows); - this.extRes = repeat(Bytes.EMPTY, nRows); + this.nRowsData = getIndexMax(ecType, true) + 1; + this.nRowsResult = getIndexMax(ecType, false) + 1; + this.nRows = this.nRowsData + this.nRowsResult; + this.id = id; + + /* + System.out.println( + "(ecdataoperation filling time) previousId: " + + Integer.toHexString(this.previousId) + + " -> id: " + + Integer.toHexString(this.id) + + " , byteDelta: " + + Arrays.stream(this.byteDelta).map(b -> Integer.toHexString(b.toInteger())).toList() + + " , diff: " + + Integer.toHexString(this.id - this.previousId - 1)); + + System.out.println( + "previousId: " + + this.previousId + + " -> id: " + + this.id + + " , byteDelta: " + + Arrays.stream(this.byteDelta).map(b -> b.toInteger()).toList() + + " , diff: " + (this.id - this.previousId - 1)); + */ + + this.limb = repeat(Bytes.EMPTY, this.nRows); + this.hurdle = repeat(false, this.nRows); + + this.wcpFlag = repeat(false, this.nRows); + this.wcpArg1Hi = repeat(Bytes.EMPTY, this.nRows); + this.wcpArg1Lo = repeat(Bytes.EMPTY, this.nRows); + this.wcpArg2Hi = repeat(Bytes.EMPTY, this.nRows); + this.wcpArg2Lo = repeat(Bytes.EMPTY, this.nRows); + this.wcpRes = repeat(false, this.nRows); + this.wcpInst = repeat(OpCode.INVALID, this.nRows); + + this.extFlag = repeat(false, this.nRows); + this.extArg1Hi = repeat(Bytes.EMPTY, this.nRows); + this.extArg1Lo = repeat(Bytes.EMPTY, this.nRows); + this.extArg2Hi = repeat(Bytes.EMPTY, this.nRows); + this.extArg2Lo = repeat(Bytes.EMPTY, this.nRows); + this.extArg3Hi = repeat(Bytes.EMPTY, this.nRows); + this.extArg3Lo = repeat(Bytes.EMPTY, this.nRows); + this.extResHi = repeat(Bytes.EMPTY, this.nRows); + this.extResLo = repeat(Bytes.EMPTY, this.nRows); + this.extInst = repeat(OpCode.INVALID, this.nRows); this.wcp = wcp; this.ext = ext; - } - public static EcDataOperation of( - Wcp wcp, - Ext ext, - final Address to, - Bytes input, - int currentContextNumber, - int previousContextNumber) { - final int ecType = addressToEcType(to); - - EcDataOperation r = - new EcDataOperation(wcp, ext, currentContextNumber, previousContextNumber, ecType, input); switch (ecType) { - case EC_RECOVER -> r.handleRecover(); - case EC_ADD -> r.handleAdd(); - case EC_MUL -> r.handleMul(); - case EC_PAIRING -> r.handlePairing(); + case ECRECOVER -> handleRecover(); + // case ECADD -> handleAdd(); + // case ECMUL -> handleMul(); + // case ECPAIRING -> handlePairing(); } - return r; } - private boolean preliminaryChecksPassed() { - return this.hurdleSwitchOffRow == -1; + public static EcDataOperation of(Wcp wcp, Ext ext, int id, final int ecType, Bytes data) { + + EcDataOperation ecDataRes = new EcDataOperation(wcp, ext, id, ecType, data); + switch (ecType) { + case ECRECOVER -> ecDataRes.handleRecover(); + // case ECADD -> ecDataRes.handleAdd(); + // case ECMUL -> ecDataRes.handleMul(); + // case ECPAIRING -> ecDataRes.handlePairing(); + } + return ecDataRes; } - private boolean callWcp(int i, OpCode inst, Bytes arg1, Bytes arg2) { - final boolean r = - switch (inst) { + private boolean callWcp(int i, OpCode wcpInst, EWord arg1, EWord arg2) { + final boolean wcpRes = + switch (wcpInst) { case LT -> this.wcp.callLT(arg1, arg2); case EQ -> this.wcp.callEQ(arg1, arg2); - default -> throw new IllegalStateException("Unexpected value: " + inst); + default -> throw new IllegalStateException("Unexpected value: " + wcpInst); }; - this.wcpArg1.set(i, arg1); - this.wcpArg2.set(i, arg2); - this.wcpInst.set(i, inst); - this.wcpRes.set(i, r); - return r; - } - - private Bytes callExt(int i, OpCode opCode, Bytes arg1, Bytes arg2, Bytes arg3) { - final Bytes result = ext.call(opCode, arg1, arg2, arg3); - this.extArg1.set(i, arg1); - this.extArg2.set(i, arg2); - this.extArg3.set(i, arg3); - this.extInst.set(i, opCode); - this.extRes.set(i, result); - - return result; + this.wcpFlag.set(i, true); + this.wcpArg1Hi.set(i, arg1.hi()); + this.wcpArg1Lo.set(i, arg1.lo()); + this.wcpArg2Hi.set(i, arg2.hi()); + this.wcpArg2Lo.set(i, arg2.lo()); + this.wcpRes.set(i, wcpRes); + this.wcpInst.set(i, wcpInst); + return wcpRes; } - private void fillHurdle() { - for (int i = 0; i < this.nRows; i++) { - boolean check = false; - switch (i % 4) { - case 1 -> { - check = true; - } - case 0, 2 -> { - check = this.comparisons.get(i / 2); - } - case 3 -> { - check = this.equalities.get(i); - } - } - if (!check) { - this.hurdleSwitchOffRow = i; - return; - } - } - - this.hurdleSwitchOffRow = -1; + private EWord callExt(int i, OpCode extInst, EWord arg1, EWord arg2, EWord arg3) { + final EWord extRes = EWord.of(ext.call(extInst, arg1, arg2, arg3)); + + this.extFlag.set(i, true); + this.extArg1Hi.set(i, arg1.hi()); + this.extArg1Lo.set(i, arg1.lo()); + this.extArg2Hi.set(i, arg2.hi()); + this.extArg2Lo.set(i, arg2.lo()); + this.extArg3Hi.set(i, arg3.hi()); + this.extArg3Lo.set(i, arg3.lo()); + this.extResHi.set(i, extRes.hi()); + this.extResLo.set(i, extRes.lo()); + this.extInst.set(i, extInst); + return extRes; } void handleRecover() { - final Bytes v = this.input.slice(32, 32); - final Bytes r = this.input.slice(64, 32); - final Bytes s = this.input.slice(96, 32); - - this.comparisons.set(0, this.callWcp(0, OpCode.LT, r, SECP256_K1N)); // r < secp256k1N - this.comparisons.set(2, this.callWcp(1, OpCode.LT, s, SECP256_K1N)); // s < secp256k1N - this.comparisons.set(1, this.callWcp(2, OpCode.LT, Bytes.EMPTY, r)); // 0 < r - this.comparisons.set(3, this.callWcp(3, OpCode.LT, Bytes.EMPTY, s)); // 0 < s - this.equalities.set(1, this.callWcp(4, OpCode.EQ, v, Bytes.of(27))); // v == 27 - this.equalities.set(2, this.callWcp(5, OpCode.EQ, v, Bytes.of(28))); // v == 28 - this.equalities.set(3, this.equalities.get(1) || this.equalities.get(2)); - this.equalities.set(7, true); + // Extract inputs + final EWord h = EWord.of(this.data.slice(0, 32)); + final EWord v = EWord.of(this.data.slice(32, 32)); + final EWord r = EWord.of(this.data.slice(64, 32)); + final EWord s = EWord.of(this.data.slice(96, 32)); + + // Set limb + limb.set(0, h.hi()); + limb.set(1, h.lo()); + limb.set(2, v.hi()); + limb.set(3, v.lo()); + limb.set(4, r.hi()); + limb.set(5, r.lo()); + limb.set(6, s.hi()); + limb.set(7, s.lo()); + + // Compute internal checks + // row i + boolean rIsInRange = callWcp(0, OpCode.LT, r, SECP256K1N); // r < secp256k1N + + // row i + 1 + boolean rIsPositive = callWcp(1, OpCode.LT, EWord.ZERO, r); // 0 < r + + // row i + 2 + boolean sIsInRange = callWcp(2, OpCode.LT, s, SECP256K1N); // s < secp256k1N + + // row i + 3 + boolean sIsPositive = callWcp(3, OpCode.LT, EWord.ZERO, s); // 0 < s + + // row i+ 4 + boolean vIs27 = callWcp(4, OpCode.EQ, v, EWord.of(27)); // v == 27 + + // row i + 5 + boolean vIs28 = callWcp(5, OpCode.EQ, v, EWord.of(28)); // v == 28 + + // Set hurdle + hurdle.set(0, rIsInRange && rIsPositive); + hurdle.set(1, sIsInRange && sIsPositive); + hurdle.set(2, hurdle.get(0) && hurdle.get(1)); + hurdle.set(INDEX_MAX_ECRECOVER_DATA, hurdle.get(2) && (vIs27 || vIs28)); + + // Set internal checks passed + this.internalChecksPassed = hurdle.get(INDEX_MAX_ECRECOVER_DATA); + + EWord recoveredAddress = EWord.ZERO; + + // Compute recoveredAddress, successBit and set circuitSelectorEcrecover + if (this.internalChecksPassed) { + recoveredAddress = extractRecoveredAddress(h, v, r, s); + this.circuitSelectorEcrecover = true; + } - this.fillHurdle(); + successBit = !recoveredAddress.isZero(); + limb.set(8, recoveredAddress.hi()); + limb.set(9, recoveredAddress.lo()); // Very unlikely edge case: if the ext module is never used elsewhere, we need to insert a // useless row, in order to trigger the construction of the first empty row, useful for the ext @@ -245,15 +366,40 @@ void handleRecover() { this.ext.callADDMOD(Bytes.EMPTY, Bytes.EMPTY, Bytes.EMPTY); } + private static EWord extractRecoveredAddress(EWord h, EWord v, EWord r, EWord s) { + SECP256K1 secp256K1 = new SECP256K1(); + try { + Optional optionalRecoveredAddress = + secp256K1.recoverPublicKeyFromSignature( + h.toBytes(), + SECPSignature.create( + r.toBigInteger(), + s.toBigInteger(), + (byte) (v.toInt() - 27), + SECP256K1N.toBigInteger())); + return optionalRecoveredAddress + .map(e -> EWord.of(Hash.keccak256(e.getEncodedBytes()).slice(32 - 20))) + .orElse(EWord.ZERO); + } catch (IllegalArgumentException e) { + System.err.print(e); + return EWord.ZERO; + } + } + + /* private void handlePointOnC1(final Bytes x, final Bytes y, int u, int i) { this.squares.set( - 6 * i + 2 * u, this.callExt(12 * i + 4 * u, OpCode.MULMOD, x, x, P)); // x² mod p + 6 * i + 2 * u, this.callExt(12 * i + 4 * u, OpCode.MULMOD, x, x, P_BN)); // x² mod p this.squares.set( - 1 + 2 * u + 6 * i, this.callExt(1 + 4 * u + 12 * i, OpCode.MULMOD, y, y, P)); // y² mod p + 1 + 2 * u + 6 * i, this.callExt(1 + 4 * u + 12 * i, OpCode.MULMOD, y, y, P_BN)); // y² mod p this.cubes.set( 2 * u + 6 * i, this.callExt( - 2 + 4 * u + 12 * i, OpCode.MULMOD, this.squares.get(2 * u + 6 * i), x, P)); // x³ mod p + 2 + 4 * u + 12 * i, + OpCode.MULMOD, + this.squares.get(2 * u + 6 * i), + x, + P_BN)); // x³ mod p this.cubes.set( 1 + 2 * u + 6 * i, this.callExt( @@ -261,11 +407,11 @@ private void handlePointOnC1(final Bytes x, final Bytes y, int u, int i) { OpCode.ADDMOD, this.cubes.get(2 * u + 6 * i), Bytes.of(3), - P)); // x³ + 3 mod p + P_BN)); // x³ + 3 mod p - this.comparisons.set(2 * u + 6 * i, this.callWcp(4 * u + 12 * i, OpCode.LT, x, P)); // x < p + this.comparisons.set(2 * u + 6 * i, this.callWcp(4 * u + 12 * i, OpCode.LT, x, P_BN)); // x < p this.comparisons.set( - 1 + 2 * u + 6 * i, this.callWcp(1 + 4 * u + 12 * i, OpCode.LT, y, P)); // y < p + 1 + 2 * u + 6 * i, this.callWcp(1 + 4 * u + 12 * i, OpCode.LT, y, P_BN)); // y < p this.equalities.set( 1 + 4 * u + 12 * i, @@ -307,10 +453,10 @@ void handlePairing() { this.handlePointOnC1(x, y, 0, i); - this.comparisons.set(6 * i + 2, this.callWcp(12 * i + 3, OpCode.LT, aIm, P)); - this.comparisons.set(6 * i + 3, this.callWcp(12 * i + 4, OpCode.LT, aRe, P)); - this.comparisons.set(6 * i + 4, this.callWcp(12 * i + 5, OpCode.LT, bIm, P)); - this.comparisons.set(6 * i + 5, this.callWcp(12 * i + 6, OpCode.LT, bRe, P)); + this.comparisons.set(6 * i + 2, this.callWcp(12 * i + 3, OpCode.LT, aIm, P_BN)); + this.comparisons.set(6 * i + 3, this.callWcp(12 * i + 4, OpCode.LT, aRe, P_BN)); + this.comparisons.set(6 * i + 4, this.callWcp(12 * i + 5, OpCode.LT, bIm, P_BN)); + this.comparisons.set(6 * i + 5, this.callWcp(12 * i + 6, OpCode.LT, bRe, P_BN)); this.equalities.set(12 * i + 7, true); this.equalities.set(12 * i + 11, true); } @@ -326,20 +472,61 @@ void handlePairing() { } } } + */ - private void traceRow(Trace trace, int i) { - trace.stamp(this.contextNumber); - // TODO: the rest - } + void trace(Trace trace, final int stamp, final long previousId) { + final Bytes deltaByte = + leftPadTo(Bytes.minimalBytes(id - previousId - 1), nBYTES_OF_DELTA_BYTES); - void trace(Trace trace) { for (int i = 0; i < this.nRows; i++) { - this.traceRow(trace, i); + boolean isData = i < this.nRowsData; + trace + .stamp(stamp) + .id(id) + .index(isData ? UnsignedByte.of(i) : UnsignedByte.of(i - this.nRowsData)) + .limb(limb.get(i)) + .totalSize(Bytes.ofUnsignedLong(getTotalSize(ecType, isData))) + .phase(getPhase(ecType, isData)) + .indexMax(Bytes.ofUnsignedLong(getIndexMax(ecType, isData))) + .successBit(successBit) + .isEcrecoverData(ecType == ECRECOVER && isData) + .isEcrecoverResult(ecType == ECRECOVER && !isData) + .isEcaddData(ecType == ECADD && isData) + .isEcaddResult(ecType == ECADD && !isData) + .isEcmulData(ecType == ECMUL && isData) + .isEcmulResult(ecType == ECMUL && !isData) + .isEcpairingData(ecType == ECPAIRING && isData) + .isEcpairingResult(ecType == ECPAIRING && !isData) + .totalPairings(Bytes.ofUnsignedLong(totalPairings)) + .accPairings(ecType == ECPAIRING && isData ? Bytes.ofUnsignedLong(1 + i) : Bytes.of(0)) + .internalChecksPassed(internalChecksPassed) + .hurdle(hurdle.get(i)) + .byteDelta( + i < nBYTES_OF_DELTA_BYTES ? UnsignedByte.of(deltaByte.get(i)) : UnsignedByte.of(0)) + .circuitSelectorEcrecover(ecType == ECRECOVER && circuitSelectorEcrecover) + .wcpFlag(wcpFlag.get(i)) + .wcpArg1Hi(wcpArg1Hi.get(i)) + .wcpArg1Lo(wcpArg1Lo.get(i)) + .wcpArg2Hi(wcpArg2Hi.get(i)) + .wcpArg2Lo(wcpArg2Lo.get(i)) + .wcpRes(wcpRes.get(i)) + .wcpInst(wcpInst.get(i).unsignedByteValue()) + .extFlag(extFlag.get(i)) + .extArg1Hi(extArg1Hi.get(i)) + .extArg1Lo(extArg1Lo.get(i)) + .extArg2Hi(extArg2Hi.get(i)) + .extArg2Lo(extArg2Lo.get(i)) + .extArg3Hi(extArg3Hi.get(i)) + .extArg3Lo(extArg3Lo.get(i)) + .extResHi(extResHi.get(i)) + .extResLo(extResLo.get(i)) + .extInst(extInst.get(i).unsignedByteValue()) + .fillAndValidateRow(); // TODO: add missing columns (stuff not related to ECRECOVER) } } @Override protected int computeLineCount() { - return this.nRows; + return this.nRowsData + this.nRowsResult; } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/Trace.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/Trace.java index fa465d5c80..540e6878fb 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/Trace.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/ecdata/Trace.java @@ -15,6 +15,7 @@ package net.consensys.linea.zktracer.module.ecdata; +import java.math.BigInteger; import java.nio.MappedByteBuffer; import java.util.BitSet; import java.util.List; @@ -30,130 +31,209 @@ * Please DO NOT ATTEMPT TO MODIFY this code directly. */ public class Trace { + public static final int ADDMOD = 0x8; + public static final int CT_MAX_LARGE_POINT = 0x7; + public static final int CT_MAX_SMALL_POINT = 0x3; + public static final int ECADD = 0x6; + public static final int ECMUL = 0x7; + public static final int ECPAIRING = 0x8; + public static final int ECRECOVER = 0x1; + public static final int INDEX_MAX_ECADD_DATA = 0x7; + public static final int INDEX_MAX_ECADD_RESULT = 0x3; + public static final int INDEX_MAX_ECMUL_DATA = 0x5; + public static final int INDEX_MAX_ECMUL_RESULT = 0x3; + public static final int INDEX_MAX_ECPAIRING_DATA_MIN = 0xb; + public static final int INDEX_MAX_ECPAIRING_RESULT = 0x1; + public static final int INDEX_MAX_ECRECOVER_DATA = 0x7; + public static final int INDEX_MAX_ECRECOVER_RESULT = 0x1; + public static final int MULMOD = 0x9; + public static final BigInteger P_BN_HI = new BigInteger("64323764613183177041862057485226039389"); + public static final BigInteger P_BN_LO = + new BigInteger("201385395114098847380338600778089168199"); + public static final BigInteger SECP256K1N_HI = + new BigInteger("340282366920938463463374607431768211455"); + public static final BigInteger SECP256K1N_LO = + new BigInteger("340282366920938463463374607427473243183"); + public static final int TOTAL_SIZE_ECADD_DATA = 0x80; + public static final int TOTAL_SIZE_ECADD_RESULT = 0x40; + public static final int TOTAL_SIZE_ECMUL_DATA = 0x60; + public static final int TOTAL_SIZE_ECMUL_RESULT = 0x40; + public static final int TOTAL_SIZE_ECPAIRING_DATA_MIN = 0xc4; + public static final int TOTAL_SIZE_ECPAIRING_RESULT = 0x20; + public static final int TOTAL_SIZE_ECRECOVER_DATA = 0x80; + public static final int TOTAL_SIZE_ECRECOVER_RESULT = 0x20; private final BitSet filled = new BitSet(); private int currentLine = 0; - private final MappedByteBuffer accDelta; private final MappedByteBuffer accPairings; - private final MappedByteBuffer allChecksPassed; + private final MappedByteBuffer acceptablePairOfPointForPairingCircuit; private final MappedByteBuffer byteDelta; - private final MappedByteBuffer comparisons; - private final MappedByteBuffer ctMin; - private final MappedByteBuffer cube; - private final MappedByteBuffer ecAdd; - private final MappedByteBuffer ecMul; - private final MappedByteBuffer ecPairing; - private final MappedByteBuffer ecRecover; - private final MappedByteBuffer equalities; + private final MappedByteBuffer circuitSelectorEcadd; + private final MappedByteBuffer circuitSelectorEcmul; + private final MappedByteBuffer circuitSelectorEcpairing; + private final MappedByteBuffer circuitSelectorEcrecover; + private final MappedByteBuffer circuitSelectorG2Membership; + private final MappedByteBuffer ct; + private final MappedByteBuffer ctMax; private final MappedByteBuffer extArg1Hi; private final MappedByteBuffer extArg1Lo; private final MappedByteBuffer extArg2Hi; private final MappedByteBuffer extArg2Lo; private final MappedByteBuffer extArg3Hi; private final MappedByteBuffer extArg3Lo; + private final MappedByteBuffer extFlag; private final MappedByteBuffer extInst; private final MappedByteBuffer extResHi; private final MappedByteBuffer extResLo; + private final MappedByteBuffer g2MembershipTestRequired; private final MappedByteBuffer hurdle; + private final MappedByteBuffer id; private final MappedByteBuffer index; + private final MappedByteBuffer indexMax; + private final MappedByteBuffer internalChecksPassed; + private final MappedByteBuffer isEcaddData; + private final MappedByteBuffer isEcaddResult; + private final MappedByteBuffer isEcmulData; + private final MappedByteBuffer isEcmulResult; + private final MappedByteBuffer isEcpairingData; + private final MappedByteBuffer isEcpairingResult; + private final MappedByteBuffer isEcrecoverData; + private final MappedByteBuffer isEcrecoverResult; + private final MappedByteBuffer isInfinity; + private final MappedByteBuffer isLargePoint; + private final MappedByteBuffer isSmallPoint; private final MappedByteBuffer limb; - private final MappedByteBuffer preliminaryChecksPassed; - private final MappedByteBuffer somethingWasntOnG2; - private final MappedByteBuffer square; + private final MappedByteBuffer notOnG2; + private final MappedByteBuffer notOnG2Acc; + private final MappedByteBuffer notOnG2AccMax; + private final MappedByteBuffer overallTrivialPairing; + private final MappedByteBuffer phase; private final MappedByteBuffer stamp; - private final MappedByteBuffer thisIsNotOnG2; - private final MappedByteBuffer thisIsNotOnG2Acc; + private final MappedByteBuffer successBit; private final MappedByteBuffer totalPairings; - private final MappedByteBuffer type; + private final MappedByteBuffer totalSize; private final MappedByteBuffer wcpArg1Hi; private final MappedByteBuffer wcpArg1Lo; private final MappedByteBuffer wcpArg2Hi; private final MappedByteBuffer wcpArg2Lo; + private final MappedByteBuffer wcpFlag; private final MappedByteBuffer wcpInst; private final MappedByteBuffer wcpRes; static List headers(int length) { return List.of( - new ColumnHeader("ecdata.ACC_DELTA", 32, length), - new ColumnHeader("ecdata.ACC_PAIRINGS", 2, length), - new ColumnHeader("ecdata.ALL_CHECKS_PASSED", 1, length), + new ColumnHeader("ecdata.ACC_PAIRINGS", 32, length), + new ColumnHeader("ecdata.ACCEPTABLE_PAIR_OF_POINT_FOR_PAIRING_CIRCUIT", 1, length), new ColumnHeader("ecdata.BYTE_DELTA", 1, length), - new ColumnHeader("ecdata.COMPARISONS", 1, length), - new ColumnHeader("ecdata.CT_MIN", 1, length), - new ColumnHeader("ecdata.CUBE", 32, length), - new ColumnHeader("ecdata.EC_ADD", 1, length), - new ColumnHeader("ecdata.EC_MUL", 1, length), - new ColumnHeader("ecdata.EC_PAIRING", 1, length), - new ColumnHeader("ecdata.EC_RECOVER", 1, length), - new ColumnHeader("ecdata.EQUALITIES", 1, length), + new ColumnHeader("ecdata.CIRCUIT_SELECTOR_ECADD", 1, length), + new ColumnHeader("ecdata.CIRCUIT_SELECTOR_ECMUL", 1, length), + new ColumnHeader("ecdata.CIRCUIT_SELECTOR_ECPAIRING", 1, length), + new ColumnHeader("ecdata.CIRCUIT_SELECTOR_ECRECOVER", 1, length), + new ColumnHeader("ecdata.CIRCUIT_SELECTOR_G2_MEMBERSHIP", 1, length), + new ColumnHeader("ecdata.CT", 2, length), + new ColumnHeader("ecdata.CT_MAX", 2, length), new ColumnHeader("ecdata.EXT_ARG1_HI", 32, length), new ColumnHeader("ecdata.EXT_ARG1_LO", 32, length), new ColumnHeader("ecdata.EXT_ARG2_HI", 32, length), new ColumnHeader("ecdata.EXT_ARG2_LO", 32, length), new ColumnHeader("ecdata.EXT_ARG3_HI", 32, length), new ColumnHeader("ecdata.EXT_ARG3_LO", 32, length), + new ColumnHeader("ecdata.EXT_FLAG", 1, length), new ColumnHeader("ecdata.EXT_INST", 1, length), new ColumnHeader("ecdata.EXT_RES_HI", 32, length), new ColumnHeader("ecdata.EXT_RES_LO", 32, length), + new ColumnHeader("ecdata.G2_MEMBERSHIP_TEST_REQUIRED", 1, length), new ColumnHeader("ecdata.HURDLE", 1, length), + new ColumnHeader("ecdata.ID", 8, length), new ColumnHeader("ecdata.INDEX", 1, length), + new ColumnHeader("ecdata.INDEX_MAX", 32, length), + new ColumnHeader("ecdata.INTERNAL_CHECKS_PASSED", 1, length), + new ColumnHeader("ecdata.IS_ECADD_DATA", 1, length), + new ColumnHeader("ecdata.IS_ECADD_RESULT", 1, length), + new ColumnHeader("ecdata.IS_ECMUL_DATA", 1, length), + new ColumnHeader("ecdata.IS_ECMUL_RESULT", 1, length), + new ColumnHeader("ecdata.IS_ECPAIRING_DATA", 1, length), + new ColumnHeader("ecdata.IS_ECPAIRING_RESULT", 1, length), + new ColumnHeader("ecdata.IS_ECRECOVER_DATA", 1, length), + new ColumnHeader("ecdata.IS_ECRECOVER_RESULT", 1, length), + new ColumnHeader("ecdata.IS_INFINITY", 1, length), + new ColumnHeader("ecdata.IS_LARGE_POINT", 1, length), + new ColumnHeader("ecdata.IS_SMALL_POINT", 1, length), new ColumnHeader("ecdata.LIMB", 32, length), - new ColumnHeader("ecdata.PRELIMINARY_CHECKS_PASSED", 1, length), - new ColumnHeader("ecdata.SOMETHING_WASNT_ON_G2", 1, length), - new ColumnHeader("ecdata.SQUARE", 32, length), + new ColumnHeader("ecdata.NOT_ON_G2", 1, length), + new ColumnHeader("ecdata.NOT_ON_G2_ACC", 1, length), + new ColumnHeader("ecdata.NOT_ON_G2_ACC_MAX", 1, length), + new ColumnHeader("ecdata.OVERALL_TRIVIAL_PAIRING", 1, length), + new ColumnHeader("ecdata.PHASE", 4, length), new ColumnHeader("ecdata.STAMP", 8, length), - new ColumnHeader("ecdata.THIS_IS_NOT_ON_G2", 1, length), - new ColumnHeader("ecdata.THIS_IS_NOT_ON_G2_ACC", 1, length), - new ColumnHeader("ecdata.TOTAL_PAIRINGS", 2, length), - new ColumnHeader("ecdata.TYPE", 1, length), + new ColumnHeader("ecdata.SUCCESS_BIT", 1, length), + new ColumnHeader("ecdata.TOTAL_PAIRINGS", 32, length), + new ColumnHeader("ecdata.TOTAL_SIZE", 32, length), new ColumnHeader("ecdata.WCP_ARG1_HI", 32, length), new ColumnHeader("ecdata.WCP_ARG1_LO", 32, length), new ColumnHeader("ecdata.WCP_ARG2_HI", 32, length), new ColumnHeader("ecdata.WCP_ARG2_LO", 32, length), + new ColumnHeader("ecdata.WCP_FLAG", 1, length), new ColumnHeader("ecdata.WCP_INST", 1, length), new ColumnHeader("ecdata.WCP_RES", 1, length)); } public Trace(List buffers) { - this.accDelta = buffers.get(0); - this.accPairings = buffers.get(1); - this.allChecksPassed = buffers.get(2); - this.byteDelta = buffers.get(3); - this.comparisons = buffers.get(4); - this.ctMin = buffers.get(5); - this.cube = buffers.get(6); - this.ecAdd = buffers.get(7); - this.ecMul = buffers.get(8); - this.ecPairing = buffers.get(9); - this.ecRecover = buffers.get(10); - this.equalities = buffers.get(11); - this.extArg1Hi = buffers.get(12); - this.extArg1Lo = buffers.get(13); - this.extArg2Hi = buffers.get(14); - this.extArg2Lo = buffers.get(15); - this.extArg3Hi = buffers.get(16); - this.extArg3Lo = buffers.get(17); - this.extInst = buffers.get(18); - this.extResHi = buffers.get(19); - this.extResLo = buffers.get(20); + this.accPairings = buffers.get(0); + this.acceptablePairOfPointForPairingCircuit = buffers.get(1); + this.byteDelta = buffers.get(2); + this.circuitSelectorEcadd = buffers.get(3); + this.circuitSelectorEcmul = buffers.get(4); + this.circuitSelectorEcpairing = buffers.get(5); + this.circuitSelectorEcrecover = buffers.get(6); + this.circuitSelectorG2Membership = buffers.get(7); + this.ct = buffers.get(8); + this.ctMax = buffers.get(9); + this.extArg1Hi = buffers.get(10); + this.extArg1Lo = buffers.get(11); + this.extArg2Hi = buffers.get(12); + this.extArg2Lo = buffers.get(13); + this.extArg3Hi = buffers.get(14); + this.extArg3Lo = buffers.get(15); + this.extFlag = buffers.get(16); + this.extInst = buffers.get(17); + this.extResHi = buffers.get(18); + this.extResLo = buffers.get(19); + this.g2MembershipTestRequired = buffers.get(20); this.hurdle = buffers.get(21); - this.index = buffers.get(22); - this.limb = buffers.get(23); - this.preliminaryChecksPassed = buffers.get(24); - this.somethingWasntOnG2 = buffers.get(25); - this.square = buffers.get(26); - this.stamp = buffers.get(27); - this.thisIsNotOnG2 = buffers.get(28); - this.thisIsNotOnG2Acc = buffers.get(29); - this.totalPairings = buffers.get(30); - this.type = buffers.get(31); - this.wcpArg1Hi = buffers.get(32); - this.wcpArg1Lo = buffers.get(33); - this.wcpArg2Hi = buffers.get(34); - this.wcpArg2Lo = buffers.get(35); - this.wcpInst = buffers.get(36); - this.wcpRes = buffers.get(37); + this.id = buffers.get(22); + this.index = buffers.get(23); + this.indexMax = buffers.get(24); + this.internalChecksPassed = buffers.get(25); + this.isEcaddData = buffers.get(26); + this.isEcaddResult = buffers.get(27); + this.isEcmulData = buffers.get(28); + this.isEcmulResult = buffers.get(29); + this.isEcpairingData = buffers.get(30); + this.isEcpairingResult = buffers.get(31); + this.isEcrecoverData = buffers.get(32); + this.isEcrecoverResult = buffers.get(33); + this.isInfinity = buffers.get(34); + this.isLargePoint = buffers.get(35); + this.isSmallPoint = buffers.get(36); + this.limb = buffers.get(37); + this.notOnG2 = buffers.get(38); + this.notOnG2Acc = buffers.get(39); + this.notOnG2AccMax = buffers.get(40); + this.overallTrivialPairing = buffers.get(41); + this.phase = buffers.get(42); + this.stamp = buffers.get(43); + this.successBit = buffers.get(44); + this.totalPairings = buffers.get(45); + this.totalSize = buffers.get(46); + this.wcpArg1Hi = buffers.get(47); + this.wcpArg1Lo = buffers.get(48); + this.wcpArg2Hi = buffers.get(49); + this.wcpArg2Lo = buffers.get(50); + this.wcpFlag = buffers.get(51); + this.wcpInst = buffers.get(52); + this.wcpRes = buffers.get(53); } public int size() { @@ -164,163 +244,136 @@ public int size() { return this.currentLine; } - public Trace accDelta(final Bytes b) { - if (filled.get(0)) { - throw new IllegalStateException("ecdata.ACC_DELTA already set"); + public Trace accPairings(final Bytes b) { + if (filled.get(1)) { + throw new IllegalStateException("ecdata.ACC_PAIRINGS already set"); } else { - filled.set(0); + filled.set(1); } final byte[] bs = b.toArrayUnsafe(); for (int i = bs.length; i < 32; i++) { - accDelta.put((byte) 0); + accPairings.put((byte) 0); } - accDelta.put(b.toArrayUnsafe()); + accPairings.put(b.toArrayUnsafe()); return this; } - public Trace accPairings(final short b) { - if (filled.get(1)) { - throw new IllegalStateException("ecdata.ACC_PAIRINGS already set"); + public Trace acceptablePairOfPointForPairingCircuit(final Boolean b) { + if (filled.get(0)) { + throw new IllegalStateException( + "ecdata.ACCEPTABLE_PAIR_OF_POINT_FOR_PAIRING_CIRCUIT already set"); } else { - filled.set(1); + filled.set(0); } - accPairings.putShort(b); + acceptablePairOfPointForPairingCircuit.put((byte) (b ? 1 : 0)); return this; } - public Trace allChecksPassed(final Boolean b) { + public Trace byteDelta(final UnsignedByte b) { if (filled.get(2)) { - throw new IllegalStateException("ecdata.ALL_CHECKS_PASSED already set"); + throw new IllegalStateException("ecdata.BYTE_DELTA already set"); } else { filled.set(2); } - allChecksPassed.put((byte) (b ? 1 : 0)); + byteDelta.put(b.toByte()); return this; } - public Trace byteDelta(final UnsignedByte b) { + public Trace circuitSelectorEcadd(final Boolean b) { if (filled.get(3)) { - throw new IllegalStateException("ecdata.BYTE_DELTA already set"); + throw new IllegalStateException("ecdata.CIRCUIT_SELECTOR_ECADD already set"); } else { filled.set(3); } - byteDelta.put(b.toByte()); + circuitSelectorEcadd.put((byte) (b ? 1 : 0)); return this; } - public Trace comparisons(final Boolean b) { + public Trace circuitSelectorEcmul(final Boolean b) { if (filled.get(4)) { - throw new IllegalStateException("ecdata.COMPARISONS already set"); + throw new IllegalStateException("ecdata.CIRCUIT_SELECTOR_ECMUL already set"); } else { filled.set(4); } - comparisons.put((byte) (b ? 1 : 0)); + circuitSelectorEcmul.put((byte) (b ? 1 : 0)); return this; } - public Trace ctMin(final UnsignedByte b) { + public Trace circuitSelectorEcpairing(final Boolean b) { if (filled.get(5)) { - throw new IllegalStateException("ecdata.CT_MIN already set"); + throw new IllegalStateException("ecdata.CIRCUIT_SELECTOR_ECPAIRING already set"); } else { filled.set(5); } - ctMin.put(b.toByte()); + circuitSelectorEcpairing.put((byte) (b ? 1 : 0)); return this; } - public Trace cube(final Bytes b) { + public Trace circuitSelectorEcrecover(final Boolean b) { if (filled.get(6)) { - throw new IllegalStateException("ecdata.CUBE already set"); + throw new IllegalStateException("ecdata.CIRCUIT_SELECTOR_ECRECOVER already set"); } else { filled.set(6); } - final byte[] bs = b.toArrayUnsafe(); - for (int i = bs.length; i < 32; i++) { - cube.put((byte) 0); - } - cube.put(b.toArrayUnsafe()); + circuitSelectorEcrecover.put((byte) (b ? 1 : 0)); return this; } - public Trace ecAdd(final Boolean b) { + public Trace circuitSelectorG2Membership(final Boolean b) { if (filled.get(7)) { - throw new IllegalStateException("ecdata.EC_ADD already set"); + throw new IllegalStateException("ecdata.CIRCUIT_SELECTOR_G2_MEMBERSHIP already set"); } else { filled.set(7); } - ecAdd.put((byte) (b ? 1 : 0)); + circuitSelectorG2Membership.put((byte) (b ? 1 : 0)); return this; } - public Trace ecMul(final Boolean b) { + public Trace ct(final short b) { if (filled.get(8)) { - throw new IllegalStateException("ecdata.EC_MUL already set"); + throw new IllegalStateException("ecdata.CT already set"); } else { filled.set(8); } - ecMul.put((byte) (b ? 1 : 0)); + ct.putShort(b); return this; } - public Trace ecPairing(final Boolean b) { + public Trace ctMax(final short b) { if (filled.get(9)) { - throw new IllegalStateException("ecdata.EC_PAIRING already set"); + throw new IllegalStateException("ecdata.CT_MAX already set"); } else { filled.set(9); } - ecPairing.put((byte) (b ? 1 : 0)); - - return this; - } - - public Trace ecRecover(final Boolean b) { - if (filled.get(10)) { - throw new IllegalStateException("ecdata.EC_RECOVER already set"); - } else { - filled.set(10); - } - - ecRecover.put((byte) (b ? 1 : 0)); - - return this; - } - - public Trace equalities(final Boolean b) { - if (filled.get(11)) { - throw new IllegalStateException("ecdata.EQUALITIES already set"); - } else { - filled.set(11); - } - - equalities.put((byte) (b ? 1 : 0)); + ctMax.putShort(b); return this; } public Trace extArg1Hi(final Bytes b) { - if (filled.get(12)) { + if (filled.get(10)) { throw new IllegalStateException("ecdata.EXT_ARG1_HI already set"); } else { - filled.set(12); + filled.set(10); } final byte[] bs = b.toArrayUnsafe(); @@ -333,10 +386,10 @@ public Trace extArg1Hi(final Bytes b) { } public Trace extArg1Lo(final Bytes b) { - if (filled.get(13)) { + if (filled.get(11)) { throw new IllegalStateException("ecdata.EXT_ARG1_LO already set"); } else { - filled.set(13); + filled.set(11); } final byte[] bs = b.toArrayUnsafe(); @@ -349,10 +402,10 @@ public Trace extArg1Lo(final Bytes b) { } public Trace extArg2Hi(final Bytes b) { - if (filled.get(14)) { + if (filled.get(12)) { throw new IllegalStateException("ecdata.EXT_ARG2_HI already set"); } else { - filled.set(14); + filled.set(12); } final byte[] bs = b.toArrayUnsafe(); @@ -365,10 +418,10 @@ public Trace extArg2Hi(final Bytes b) { } public Trace extArg2Lo(final Bytes b) { - if (filled.get(15)) { + if (filled.get(13)) { throw new IllegalStateException("ecdata.EXT_ARG2_LO already set"); } else { - filled.set(15); + filled.set(13); } final byte[] bs = b.toArrayUnsafe(); @@ -381,10 +434,10 @@ public Trace extArg2Lo(final Bytes b) { } public Trace extArg3Hi(final Bytes b) { - if (filled.get(16)) { + if (filled.get(14)) { throw new IllegalStateException("ecdata.EXT_ARG3_HI already set"); } else { - filled.set(16); + filled.set(14); } final byte[] bs = b.toArrayUnsafe(); @@ -397,10 +450,10 @@ public Trace extArg3Hi(final Bytes b) { } public Trace extArg3Lo(final Bytes b) { - if (filled.get(17)) { + if (filled.get(15)) { throw new IllegalStateException("ecdata.EXT_ARG3_LO already set"); } else { - filled.set(17); + filled.set(15); } final byte[] bs = b.toArrayUnsafe(); @@ -412,11 +465,23 @@ public Trace extArg3Lo(final Bytes b) { return this; } + public Trace extFlag(final Boolean b) { + if (filled.get(16)) { + throw new IllegalStateException("ecdata.EXT_FLAG already set"); + } else { + filled.set(16); + } + + extFlag.put((byte) (b ? 1 : 0)); + + return this; + } + public Trace extInst(final UnsignedByte b) { - if (filled.get(18)) { + if (filled.get(17)) { throw new IllegalStateException("ecdata.EXT_INST already set"); } else { - filled.set(18); + filled.set(17); } extInst.put(b.toByte()); @@ -425,10 +490,10 @@ public Trace extInst(final UnsignedByte b) { } public Trace extResHi(final Bytes b) { - if (filled.get(19)) { + if (filled.get(18)) { throw new IllegalStateException("ecdata.EXT_RES_HI already set"); } else { - filled.set(19); + filled.set(18); } final byte[] bs = b.toArrayUnsafe(); @@ -441,10 +506,10 @@ public Trace extResHi(final Bytes b) { } public Trace extResLo(final Bytes b) { - if (filled.get(20)) { + if (filled.get(19)) { throw new IllegalStateException("ecdata.EXT_RES_LO already set"); } else { - filled.set(20); + filled.set(19); } final byte[] bs = b.toArrayUnsafe(); @@ -456,6 +521,18 @@ public Trace extResLo(final Bytes b) { return this; } + public Trace g2MembershipTestRequired(final Boolean b) { + if (filled.get(20)) { + throw new IllegalStateException("ecdata.G2_MEMBERSHIP_TEST_REQUIRED already set"); + } else { + filled.set(20); + } + + g2MembershipTestRequired.put((byte) (b ? 1 : 0)); + + return this; + } + public Trace hurdle(final Boolean b) { if (filled.get(21)) { throw new IllegalStateException("ecdata.HURDLE already set"); @@ -468,141 +545,329 @@ public Trace hurdle(final Boolean b) { return this; } - public Trace index(final UnsignedByte b) { + public Trace id(final long b) { if (filled.get(22)) { - throw new IllegalStateException("ecdata.INDEX already set"); + throw new IllegalStateException("ecdata.ID already set"); } else { filled.set(22); } - index.put(b.toByte()); + id.putLong(b); return this; } - public Trace limb(final Bytes b) { + public Trace index(final UnsignedByte b) { if (filled.get(23)) { - throw new IllegalStateException("ecdata.LIMB already set"); + throw new IllegalStateException("ecdata.INDEX already set"); } else { filled.set(23); } - final byte[] bs = b.toArrayUnsafe(); - for (int i = bs.length; i < 32; i++) { - limb.put((byte) 0); - } - limb.put(b.toArrayUnsafe()); + index.put(b.toByte()); return this; } - public Trace preliminaryChecksPassed(final Boolean b) { + public Trace indexMax(final Bytes b) { if (filled.get(24)) { - throw new IllegalStateException("ecdata.PRELIMINARY_CHECKS_PASSED already set"); + throw new IllegalStateException("ecdata.INDEX_MAX already set"); } else { filled.set(24); } - preliminaryChecksPassed.put((byte) (b ? 1 : 0)); + final byte[] bs = b.toArrayUnsafe(); + for (int i = bs.length; i < 32; i++) { + indexMax.put((byte) 0); + } + indexMax.put(b.toArrayUnsafe()); return this; } - public Trace somethingWasntOnG2(final Boolean b) { + public Trace internalChecksPassed(final Boolean b) { if (filled.get(25)) { - throw new IllegalStateException("ecdata.SOMETHING_WASNT_ON_G2 already set"); + throw new IllegalStateException("ecdata.INTERNAL_CHECKS_PASSED already set"); } else { filled.set(25); } - somethingWasntOnG2.put((byte) (b ? 1 : 0)); + internalChecksPassed.put((byte) (b ? 1 : 0)); return this; } - public Trace square(final Bytes b) { + public Trace isEcaddData(final Boolean b) { if (filled.get(26)) { - throw new IllegalStateException("ecdata.SQUARE already set"); + throw new IllegalStateException("ecdata.IS_ECADD_DATA already set"); } else { filled.set(26); } - final byte[] bs = b.toArrayUnsafe(); - for (int i = bs.length; i < 32; i++) { - square.put((byte) 0); - } - square.put(b.toArrayUnsafe()); + isEcaddData.put((byte) (b ? 1 : 0)); return this; } - public Trace stamp(final long b) { + public Trace isEcaddResult(final Boolean b) { if (filled.get(27)) { - throw new IllegalStateException("ecdata.STAMP already set"); + throw new IllegalStateException("ecdata.IS_ECADD_RESULT already set"); } else { filled.set(27); } - stamp.putLong(b); + isEcaddResult.put((byte) (b ? 1 : 0)); return this; } - public Trace thisIsNotOnG2(final Boolean b) { + public Trace isEcmulData(final Boolean b) { if (filled.get(28)) { - throw new IllegalStateException("ecdata.THIS_IS_NOT_ON_G2 already set"); + throw new IllegalStateException("ecdata.IS_ECMUL_DATA already set"); } else { filled.set(28); } - thisIsNotOnG2.put((byte) (b ? 1 : 0)); + isEcmulData.put((byte) (b ? 1 : 0)); return this; } - public Trace thisIsNotOnG2Acc(final Boolean b) { + public Trace isEcmulResult(final Boolean b) { if (filled.get(29)) { - throw new IllegalStateException("ecdata.THIS_IS_NOT_ON_G2_ACC already set"); + throw new IllegalStateException("ecdata.IS_ECMUL_RESULT already set"); } else { filled.set(29); } - thisIsNotOnG2Acc.put((byte) (b ? 1 : 0)); + isEcmulResult.put((byte) (b ? 1 : 0)); return this; } - public Trace totalPairings(final short b) { + public Trace isEcpairingData(final Boolean b) { if (filled.get(30)) { - throw new IllegalStateException("ecdata.TOTAL_PAIRINGS already set"); + throw new IllegalStateException("ecdata.IS_ECPAIRING_DATA already set"); } else { filled.set(30); } - totalPairings.putShort(b); + isEcpairingData.put((byte) (b ? 1 : 0)); return this; } - public Trace type(final UnsignedByte b) { + public Trace isEcpairingResult(final Boolean b) { if (filled.get(31)) { - throw new IllegalStateException("ecdata.TYPE already set"); + throw new IllegalStateException("ecdata.IS_ECPAIRING_RESULT already set"); } else { filled.set(31); } - type.put(b.toByte()); + isEcpairingResult.put((byte) (b ? 1 : 0)); return this; } - public Trace wcpArg1Hi(final Bytes b) { + public Trace isEcrecoverData(final Boolean b) { if (filled.get(32)) { - throw new IllegalStateException("ecdata.WCP_ARG1_HI already set"); + throw new IllegalStateException("ecdata.IS_ECRECOVER_DATA already set"); } else { filled.set(32); } + isEcrecoverData.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace isEcrecoverResult(final Boolean b) { + if (filled.get(33)) { + throw new IllegalStateException("ecdata.IS_ECRECOVER_RESULT already set"); + } else { + filled.set(33); + } + + isEcrecoverResult.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace isInfinity(final Boolean b) { + if (filled.get(34)) { + throw new IllegalStateException("ecdata.IS_INFINITY already set"); + } else { + filled.set(34); + } + + isInfinity.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace isLargePoint(final Boolean b) { + if (filled.get(35)) { + throw new IllegalStateException("ecdata.IS_LARGE_POINT already set"); + } else { + filled.set(35); + } + + isLargePoint.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace isSmallPoint(final Boolean b) { + if (filled.get(36)) { + throw new IllegalStateException("ecdata.IS_SMALL_POINT already set"); + } else { + filled.set(36); + } + + isSmallPoint.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace limb(final Bytes b) { + if (filled.get(37)) { + throw new IllegalStateException("ecdata.LIMB already set"); + } else { + filled.set(37); + } + + final byte[] bs = b.toArrayUnsafe(); + for (int i = bs.length; i < 32; i++) { + limb.put((byte) 0); + } + limb.put(b.toArrayUnsafe()); + + return this; + } + + public Trace notOnG2(final Boolean b) { + if (filled.get(38)) { + throw new IllegalStateException("ecdata.NOT_ON_G2 already set"); + } else { + filled.set(38); + } + + notOnG2.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace notOnG2Acc(final Boolean b) { + if (filled.get(39)) { + throw new IllegalStateException("ecdata.NOT_ON_G2_ACC already set"); + } else { + filled.set(39); + } + + notOnG2Acc.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace notOnG2AccMax(final Boolean b) { + if (filled.get(40)) { + throw new IllegalStateException("ecdata.NOT_ON_G2_ACC_MAX already set"); + } else { + filled.set(40); + } + + notOnG2AccMax.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace overallTrivialPairing(final Boolean b) { + if (filled.get(41)) { + throw new IllegalStateException("ecdata.OVERALL_TRIVIAL_PAIRING already set"); + } else { + filled.set(41); + } + + overallTrivialPairing.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace phase(final int b) { + if (filled.get(42)) { + throw new IllegalStateException("ecdata.PHASE already set"); + } else { + filled.set(42); + } + + phase.putInt(b); + + return this; + } + + public Trace stamp(final long b) { + if (filled.get(43)) { + throw new IllegalStateException("ecdata.STAMP already set"); + } else { + filled.set(43); + } + + stamp.putLong(b); + + return this; + } + + public Trace successBit(final Boolean b) { + if (filled.get(44)) { + throw new IllegalStateException("ecdata.SUCCESS_BIT already set"); + } else { + filled.set(44); + } + + successBit.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace totalPairings(final Bytes b) { + if (filled.get(45)) { + throw new IllegalStateException("ecdata.TOTAL_PAIRINGS already set"); + } else { + filled.set(45); + } + + final byte[] bs = b.toArrayUnsafe(); + for (int i = bs.length; i < 32; i++) { + totalPairings.put((byte) 0); + } + totalPairings.put(b.toArrayUnsafe()); + + return this; + } + + public Trace totalSize(final Bytes b) { + if (filled.get(46)) { + throw new IllegalStateException("ecdata.TOTAL_SIZE already set"); + } else { + filled.set(46); + } + + final byte[] bs = b.toArrayUnsafe(); + for (int i = bs.length; i < 32; i++) { + totalSize.put((byte) 0); + } + totalSize.put(b.toArrayUnsafe()); + + return this; + } + + public Trace wcpArg1Hi(final Bytes b) { + if (filled.get(47)) { + throw new IllegalStateException("ecdata.WCP_ARG1_HI already set"); + } else { + filled.set(47); + } + final byte[] bs = b.toArrayUnsafe(); for (int i = bs.length; i < 32; i++) { wcpArg1Hi.put((byte) 0); @@ -613,10 +878,10 @@ public Trace wcpArg1Hi(final Bytes b) { } public Trace wcpArg1Lo(final Bytes b) { - if (filled.get(33)) { + if (filled.get(48)) { throw new IllegalStateException("ecdata.WCP_ARG1_LO already set"); } else { - filled.set(33); + filled.set(48); } final byte[] bs = b.toArrayUnsafe(); @@ -629,10 +894,10 @@ public Trace wcpArg1Lo(final Bytes b) { } public Trace wcpArg2Hi(final Bytes b) { - if (filled.get(34)) { + if (filled.get(49)) { throw new IllegalStateException("ecdata.WCP_ARG2_HI already set"); } else { - filled.set(34); + filled.set(49); } final byte[] bs = b.toArrayUnsafe(); @@ -645,10 +910,10 @@ public Trace wcpArg2Hi(final Bytes b) { } public Trace wcpArg2Lo(final Bytes b) { - if (filled.get(35)) { + if (filled.get(50)) { throw new IllegalStateException("ecdata.WCP_ARG2_LO already set"); } else { - filled.set(35); + filled.set(50); } final byte[] bs = b.toArrayUnsafe(); @@ -660,11 +925,23 @@ public Trace wcpArg2Lo(final Bytes b) { return this; } + public Trace wcpFlag(final Boolean b) { + if (filled.get(51)) { + throw new IllegalStateException("ecdata.WCP_FLAG already set"); + } else { + filled.set(51); + } + + wcpFlag.put((byte) (b ? 1 : 0)); + + return this; + } + public Trace wcpInst(final UnsignedByte b) { - if (filled.get(36)) { + if (filled.get(52)) { throw new IllegalStateException("ecdata.WCP_INST already set"); } else { - filled.set(36); + filled.set(52); } wcpInst.put(b.toByte()); @@ -673,10 +950,10 @@ public Trace wcpInst(final UnsignedByte b) { } public Trace wcpRes(final Boolean b) { - if (filled.get(37)) { + if (filled.get(53)) { throw new IllegalStateException("ecdata.WCP_RES already set"); } else { - filled.set(37); + filled.set(53); } wcpRes.put((byte) (b ? 1 : 0)); @@ -685,88 +962,89 @@ public Trace wcpRes(final Boolean b) { } public Trace validateRow() { - if (!filled.get(0)) { - throw new IllegalStateException("ecdata.ACC_DELTA has not been filled"); - } - if (!filled.get(1)) { throw new IllegalStateException("ecdata.ACC_PAIRINGS has not been filled"); } + if (!filled.get(0)) { + throw new IllegalStateException( + "ecdata.ACCEPTABLE_PAIR_OF_POINT_FOR_PAIRING_CIRCUIT has not been filled"); + } + if (!filled.get(2)) { - throw new IllegalStateException("ecdata.ALL_CHECKS_PASSED has not been filled"); + throw new IllegalStateException("ecdata.BYTE_DELTA has not been filled"); } if (!filled.get(3)) { - throw new IllegalStateException("ecdata.BYTE_DELTA has not been filled"); + throw new IllegalStateException("ecdata.CIRCUIT_SELECTOR_ECADD has not been filled"); } if (!filled.get(4)) { - throw new IllegalStateException("ecdata.COMPARISONS has not been filled"); + throw new IllegalStateException("ecdata.CIRCUIT_SELECTOR_ECMUL has not been filled"); } if (!filled.get(5)) { - throw new IllegalStateException("ecdata.CT_MIN has not been filled"); + throw new IllegalStateException("ecdata.CIRCUIT_SELECTOR_ECPAIRING has not been filled"); } if (!filled.get(6)) { - throw new IllegalStateException("ecdata.CUBE has not been filled"); + throw new IllegalStateException("ecdata.CIRCUIT_SELECTOR_ECRECOVER has not been filled"); } if (!filled.get(7)) { - throw new IllegalStateException("ecdata.EC_ADD has not been filled"); + throw new IllegalStateException("ecdata.CIRCUIT_SELECTOR_G2_MEMBERSHIP has not been filled"); } if (!filled.get(8)) { - throw new IllegalStateException("ecdata.EC_MUL has not been filled"); + throw new IllegalStateException("ecdata.CT has not been filled"); } if (!filled.get(9)) { - throw new IllegalStateException("ecdata.EC_PAIRING has not been filled"); + throw new IllegalStateException("ecdata.CT_MAX has not been filled"); } if (!filled.get(10)) { - throw new IllegalStateException("ecdata.EC_RECOVER has not been filled"); + throw new IllegalStateException("ecdata.EXT_ARG1_HI has not been filled"); } if (!filled.get(11)) { - throw new IllegalStateException("ecdata.EQUALITIES has not been filled"); + throw new IllegalStateException("ecdata.EXT_ARG1_LO has not been filled"); } if (!filled.get(12)) { - throw new IllegalStateException("ecdata.EXT_ARG1_HI has not been filled"); + throw new IllegalStateException("ecdata.EXT_ARG2_HI has not been filled"); } if (!filled.get(13)) { - throw new IllegalStateException("ecdata.EXT_ARG1_LO has not been filled"); + throw new IllegalStateException("ecdata.EXT_ARG2_LO has not been filled"); } if (!filled.get(14)) { - throw new IllegalStateException("ecdata.EXT_ARG2_HI has not been filled"); + throw new IllegalStateException("ecdata.EXT_ARG3_HI has not been filled"); } if (!filled.get(15)) { - throw new IllegalStateException("ecdata.EXT_ARG2_LO has not been filled"); + throw new IllegalStateException("ecdata.EXT_ARG3_LO has not been filled"); } if (!filled.get(16)) { - throw new IllegalStateException("ecdata.EXT_ARG3_HI has not been filled"); + throw new IllegalStateException("ecdata.EXT_FLAG has not been filled"); } if (!filled.get(17)) { - throw new IllegalStateException("ecdata.EXT_ARG3_LO has not been filled"); + throw new IllegalStateException("ecdata.EXT_INST has not been filled"); } if (!filled.get(18)) { - throw new IllegalStateException("ecdata.EXT_INST has not been filled"); + throw new IllegalStateException("ecdata.EXT_RES_HI has not been filled"); } if (!filled.get(19)) { - throw new IllegalStateException("ecdata.EXT_RES_HI has not been filled"); + throw new IllegalStateException("ecdata.EXT_RES_LO has not been filled"); } if (!filled.get(20)) { - throw new IllegalStateException("ecdata.EXT_RES_LO has not been filled"); + throw new IllegalStateException("ecdata.G2_MEMBERSHIP_TEST_REQUIRED has not been filled"); } if (!filled.get(21)) { @@ -774,66 +1052,130 @@ public Trace validateRow() { } if (!filled.get(22)) { - throw new IllegalStateException("ecdata.INDEX has not been filled"); + throw new IllegalStateException("ecdata.ID has not been filled"); } if (!filled.get(23)) { - throw new IllegalStateException("ecdata.LIMB has not been filled"); + throw new IllegalStateException("ecdata.INDEX has not been filled"); } if (!filled.get(24)) { - throw new IllegalStateException("ecdata.PRELIMINARY_CHECKS_PASSED has not been filled"); + throw new IllegalStateException("ecdata.INDEX_MAX has not been filled"); } if (!filled.get(25)) { - throw new IllegalStateException("ecdata.SOMETHING_WASNT_ON_G2 has not been filled"); + throw new IllegalStateException("ecdata.INTERNAL_CHECKS_PASSED has not been filled"); } if (!filled.get(26)) { - throw new IllegalStateException("ecdata.SQUARE has not been filled"); + throw new IllegalStateException("ecdata.IS_ECADD_DATA has not been filled"); } if (!filled.get(27)) { - throw new IllegalStateException("ecdata.STAMP has not been filled"); + throw new IllegalStateException("ecdata.IS_ECADD_RESULT has not been filled"); } if (!filled.get(28)) { - throw new IllegalStateException("ecdata.THIS_IS_NOT_ON_G2 has not been filled"); + throw new IllegalStateException("ecdata.IS_ECMUL_DATA has not been filled"); } if (!filled.get(29)) { - throw new IllegalStateException("ecdata.THIS_IS_NOT_ON_G2_ACC has not been filled"); + throw new IllegalStateException("ecdata.IS_ECMUL_RESULT has not been filled"); } if (!filled.get(30)) { - throw new IllegalStateException("ecdata.TOTAL_PAIRINGS has not been filled"); + throw new IllegalStateException("ecdata.IS_ECPAIRING_DATA has not been filled"); } if (!filled.get(31)) { - throw new IllegalStateException("ecdata.TYPE has not been filled"); + throw new IllegalStateException("ecdata.IS_ECPAIRING_RESULT has not been filled"); } if (!filled.get(32)) { - throw new IllegalStateException("ecdata.WCP_ARG1_HI has not been filled"); + throw new IllegalStateException("ecdata.IS_ECRECOVER_DATA has not been filled"); } if (!filled.get(33)) { - throw new IllegalStateException("ecdata.WCP_ARG1_LO has not been filled"); + throw new IllegalStateException("ecdata.IS_ECRECOVER_RESULT has not been filled"); } if (!filled.get(34)) { - throw new IllegalStateException("ecdata.WCP_ARG2_HI has not been filled"); + throw new IllegalStateException("ecdata.IS_INFINITY has not been filled"); } if (!filled.get(35)) { - throw new IllegalStateException("ecdata.WCP_ARG2_LO has not been filled"); + throw new IllegalStateException("ecdata.IS_LARGE_POINT has not been filled"); } if (!filled.get(36)) { - throw new IllegalStateException("ecdata.WCP_INST has not been filled"); + throw new IllegalStateException("ecdata.IS_SMALL_POINT has not been filled"); } if (!filled.get(37)) { + throw new IllegalStateException("ecdata.LIMB has not been filled"); + } + + if (!filled.get(38)) { + throw new IllegalStateException("ecdata.NOT_ON_G2 has not been filled"); + } + + if (!filled.get(39)) { + throw new IllegalStateException("ecdata.NOT_ON_G2_ACC has not been filled"); + } + + if (!filled.get(40)) { + throw new IllegalStateException("ecdata.NOT_ON_G2_ACC_MAX has not been filled"); + } + + if (!filled.get(41)) { + throw new IllegalStateException("ecdata.OVERALL_TRIVIAL_PAIRING has not been filled"); + } + + if (!filled.get(42)) { + throw new IllegalStateException("ecdata.PHASE has not been filled"); + } + + if (!filled.get(43)) { + throw new IllegalStateException("ecdata.STAMP has not been filled"); + } + + if (!filled.get(44)) { + throw new IllegalStateException("ecdata.SUCCESS_BIT has not been filled"); + } + + if (!filled.get(45)) { + throw new IllegalStateException("ecdata.TOTAL_PAIRINGS has not been filled"); + } + + if (!filled.get(46)) { + throw new IllegalStateException("ecdata.TOTAL_SIZE has not been filled"); + } + + if (!filled.get(47)) { + throw new IllegalStateException("ecdata.WCP_ARG1_HI has not been filled"); + } + + if (!filled.get(48)) { + throw new IllegalStateException("ecdata.WCP_ARG1_LO has not been filled"); + } + + if (!filled.get(49)) { + throw new IllegalStateException("ecdata.WCP_ARG2_HI has not been filled"); + } + + if (!filled.get(50)) { + throw new IllegalStateException("ecdata.WCP_ARG2_LO has not been filled"); + } + + if (!filled.get(51)) { + throw new IllegalStateException("ecdata.WCP_FLAG has not been filled"); + } + + if (!filled.get(52)) { + throw new IllegalStateException("ecdata.WCP_INST has not been filled"); + } + + if (!filled.get(53)) { throw new IllegalStateException("ecdata.WCP_RES has not been filled"); } @@ -844,88 +1186,89 @@ public Trace validateRow() { } public Trace fillAndValidateRow() { - if (!filled.get(0)) { - accDelta.position(accDelta.position() + 32); + if (!filled.get(1)) { + accPairings.position(accPairings.position() + 32); } - if (!filled.get(1)) { - accPairings.position(accPairings.position() + 2); + if (!filled.get(0)) { + acceptablePairOfPointForPairingCircuit.position( + acceptablePairOfPointForPairingCircuit.position() + 1); } if (!filled.get(2)) { - allChecksPassed.position(allChecksPassed.position() + 1); + byteDelta.position(byteDelta.position() + 1); } if (!filled.get(3)) { - byteDelta.position(byteDelta.position() + 1); + circuitSelectorEcadd.position(circuitSelectorEcadd.position() + 1); } if (!filled.get(4)) { - comparisons.position(comparisons.position() + 1); + circuitSelectorEcmul.position(circuitSelectorEcmul.position() + 1); } if (!filled.get(5)) { - ctMin.position(ctMin.position() + 1); + circuitSelectorEcpairing.position(circuitSelectorEcpairing.position() + 1); } if (!filled.get(6)) { - cube.position(cube.position() + 32); + circuitSelectorEcrecover.position(circuitSelectorEcrecover.position() + 1); } if (!filled.get(7)) { - ecAdd.position(ecAdd.position() + 1); + circuitSelectorG2Membership.position(circuitSelectorG2Membership.position() + 1); } if (!filled.get(8)) { - ecMul.position(ecMul.position() + 1); + ct.position(ct.position() + 2); } if (!filled.get(9)) { - ecPairing.position(ecPairing.position() + 1); + ctMax.position(ctMax.position() + 2); } if (!filled.get(10)) { - ecRecover.position(ecRecover.position() + 1); + extArg1Hi.position(extArg1Hi.position() + 32); } if (!filled.get(11)) { - equalities.position(equalities.position() + 1); + extArg1Lo.position(extArg1Lo.position() + 32); } if (!filled.get(12)) { - extArg1Hi.position(extArg1Hi.position() + 32); + extArg2Hi.position(extArg2Hi.position() + 32); } if (!filled.get(13)) { - extArg1Lo.position(extArg1Lo.position() + 32); + extArg2Lo.position(extArg2Lo.position() + 32); } if (!filled.get(14)) { - extArg2Hi.position(extArg2Hi.position() + 32); + extArg3Hi.position(extArg3Hi.position() + 32); } if (!filled.get(15)) { - extArg2Lo.position(extArg2Lo.position() + 32); + extArg3Lo.position(extArg3Lo.position() + 32); } if (!filled.get(16)) { - extArg3Hi.position(extArg3Hi.position() + 32); + extFlag.position(extFlag.position() + 1); } if (!filled.get(17)) { - extArg3Lo.position(extArg3Lo.position() + 32); + extInst.position(extInst.position() + 1); } if (!filled.get(18)) { - extInst.position(extInst.position() + 1); + extResHi.position(extResHi.position() + 32); } if (!filled.get(19)) { - extResHi.position(extResHi.position() + 32); + extResLo.position(extResLo.position() + 32); } if (!filled.get(20)) { - extResLo.position(extResLo.position() + 32); + g2MembershipTestRequired.position(g2MembershipTestRequired.position() + 1); } if (!filled.get(21)) { @@ -933,66 +1276,130 @@ public Trace fillAndValidateRow() { } if (!filled.get(22)) { - index.position(index.position() + 1); + id.position(id.position() + 8); } if (!filled.get(23)) { - limb.position(limb.position() + 32); + index.position(index.position() + 1); } if (!filled.get(24)) { - preliminaryChecksPassed.position(preliminaryChecksPassed.position() + 1); + indexMax.position(indexMax.position() + 32); } if (!filled.get(25)) { - somethingWasntOnG2.position(somethingWasntOnG2.position() + 1); + internalChecksPassed.position(internalChecksPassed.position() + 1); } if (!filled.get(26)) { - square.position(square.position() + 32); + isEcaddData.position(isEcaddData.position() + 1); } if (!filled.get(27)) { - stamp.position(stamp.position() + 8); + isEcaddResult.position(isEcaddResult.position() + 1); } if (!filled.get(28)) { - thisIsNotOnG2.position(thisIsNotOnG2.position() + 1); + isEcmulData.position(isEcmulData.position() + 1); } if (!filled.get(29)) { - thisIsNotOnG2Acc.position(thisIsNotOnG2Acc.position() + 1); + isEcmulResult.position(isEcmulResult.position() + 1); } if (!filled.get(30)) { - totalPairings.position(totalPairings.position() + 2); + isEcpairingData.position(isEcpairingData.position() + 1); } if (!filled.get(31)) { - type.position(type.position() + 1); + isEcpairingResult.position(isEcpairingResult.position() + 1); } if (!filled.get(32)) { - wcpArg1Hi.position(wcpArg1Hi.position() + 32); + isEcrecoverData.position(isEcrecoverData.position() + 1); } if (!filled.get(33)) { - wcpArg1Lo.position(wcpArg1Lo.position() + 32); + isEcrecoverResult.position(isEcrecoverResult.position() + 1); } if (!filled.get(34)) { - wcpArg2Hi.position(wcpArg2Hi.position() + 32); + isInfinity.position(isInfinity.position() + 1); } if (!filled.get(35)) { - wcpArg2Lo.position(wcpArg2Lo.position() + 32); + isLargePoint.position(isLargePoint.position() + 1); } if (!filled.get(36)) { - wcpInst.position(wcpInst.position() + 1); + isSmallPoint.position(isSmallPoint.position() + 1); } if (!filled.get(37)) { + limb.position(limb.position() + 32); + } + + if (!filled.get(38)) { + notOnG2.position(notOnG2.position() + 1); + } + + if (!filled.get(39)) { + notOnG2Acc.position(notOnG2Acc.position() + 1); + } + + if (!filled.get(40)) { + notOnG2AccMax.position(notOnG2AccMax.position() + 1); + } + + if (!filled.get(41)) { + overallTrivialPairing.position(overallTrivialPairing.position() + 1); + } + + if (!filled.get(42)) { + phase.position(phase.position() + 4); + } + + if (!filled.get(43)) { + stamp.position(stamp.position() + 8); + } + + if (!filled.get(44)) { + successBit.position(successBit.position() + 1); + } + + if (!filled.get(45)) { + totalPairings.position(totalPairings.position() + 32); + } + + if (!filled.get(46)) { + totalSize.position(totalSize.position() + 32); + } + + if (!filled.get(47)) { + wcpArg1Hi.position(wcpArg1Hi.position() + 32); + } + + if (!filled.get(48)) { + wcpArg1Lo.position(wcpArg1Lo.position() + 32); + } + + if (!filled.get(49)) { + wcpArg2Hi.position(wcpArg2Hi.position() + 32); + } + + if (!filled.get(50)) { + wcpArg2Lo.position(wcpArg2Lo.position() + 32); + } + + if (!filled.get(51)) { + wcpFlag.position(wcpFlag.position() + 1); + } + + if (!filled.get(52)) { + wcpInst.position(wcpInst.position() + 1); + } + + if (!filled.get(53)) { wcpRes.position(wcpRes.position() + 1); } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java index ebd22d7e4b..6a663c0e98 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java @@ -201,9 +201,9 @@ public void addTraceSection(TraceSection section) { private final Module add = new Add(this); private final Module bin = new Bin(this); private final Blake2fModexpData blake2fModexpData = new Blake2fModexpData(); + @Getter private final EcData ecData; private final Blockdata blockdata; private final Blockhash blockhash = new Blockhash(wcp); - private final EcData ecData; private final Euc euc; private final Ext ext = new Ext(this); private final Module mul = new Mul(this); @@ -336,9 +336,9 @@ public List getModulesToTrace() { this.add, this.bin, this.blake2fModexpData, + this.ecData, this.blockdata, this.blockhash, - // this.ecData, // TODO: not yet this.ext, this.euc, this.exp, diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/types/EWord.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/types/EWord.java index 0e06be7aed..27015b65f8 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/types/EWord.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/types/EWord.java @@ -15,6 +15,8 @@ package net.consensys.linea.zktracer.types; +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; + import java.math.BigInteger; import org.apache.tuweni.bytes.Bytes; @@ -117,6 +119,20 @@ public static EWord ofHexString(final String str) { return new EWord(str); } + /** + * From hi and lo BigIntegers to EVM word. + * + * @param hiBigInt the high half of the EWord + * @param loBigInt the low half of the EWord + * @return the EVM word + */ + public static EWord of(final BigInteger hiBigInt, final BigInteger loBigInt) { + return EWord.of( + Bytes.concatenate( + Bytes16.leftPad(bigIntegerToBytes(hiBigInt)), + Bytes16.leftPad(bigIntegerToBytes(loBigInt)))); + } + @Override public Number getValue() { return getAsBigInteger(); diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/EcDataTest.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/ecdata/EcDataTest.java similarity index 57% rename from arithmetization/src/test/java/net/consensys/linea/zktracer/module/EcDataTest.java rename to arithmetization/src/test/java/net/consensys/linea/zktracer/module/ecdata/EcDataTest.java index 7be7baaca0..822cf3fff5 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/EcDataTest.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/ecdata/EcDataTest.java @@ -13,13 +13,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -package net.consensys.linea.zktracer.module; +package net.consensys.linea.zktracer.module.ecdata; +import static net.consensys.linea.zktracer.module.ecdata.EcDataOperation.SECP256K1N; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import net.consensys.linea.zktracer.opcode.OpCode; +import net.consensys.linea.zktracer.testing.BytecodeCompiler; import net.consensys.linea.zktracer.testing.BytecodeRunner; import net.consensys.linea.zktracer.testing.EvmExtension; +import net.consensys.linea.zktracer.types.EWord; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Test; 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(EvmExtension.class) public class EcDataTest { @@ -38,4 +52,190 @@ void testEcRecoverWithEmptyExt() { "6080604052348015600f57600080fd5b5060476001601b6001620f00007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe609360201b60201c565b605157605060ce565b5b60006040518060400160405280600e81526020017f7a6b2d65766d206973206c6966650000000000000000000000000000000000008152509050805160208201f35b600060405186815285602082015284604082015283606082015260008084608001836001610bb8fa9150608081016040525095945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fdfe")) .run(); } + + private static Stream ecRecoverSource() { + EWord h = + EWord.ofHexString("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3"); + List v = + List.of( + EWord.of(28), + EWord.ZERO, + EWord.of(BigInteger.ONE, BigInteger.valueOf(27)), + EWord.of(BigInteger.ONE, BigInteger.valueOf(28))); + List r = + List.of( + EWord.ofHexString("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608"), + EWord.ZERO, + SECP256K1N, + SECP256K1N.add(EWord.of(1))); + List s = + List.of( + EWord.ofHexString("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada"), + EWord.ZERO, + SECP256K1N, + SECP256K1N.add(EWord.of(1))); + + List arguments = new ArrayList<>(); + + // Test cases where ICP = successBit = 1 (first one) or ICP = successBit = 0 (all the others) + for (int i = 0; i < v.size(); i++) { + for (int j = 0; j < r.size(); j++) { + for (int k = 0; k < s.size(); k++) { + arguments.add( + Arguments.of( + i + j + k == 0 ? "[ICP = 1, successBit = 1]" : "[ICP = 0, successBit = 0]", + h, + v.get(i), + r.get(j), + s.get(k), + i + j + k == 0, + i + j + k == 0)); + } + } + } + + // Test cases where ICP = successBit = 1 + arguments.add( + argumentsFromStrings( + "[ICP = 1, successBit = 1]", + "0x279d94621558f755796898fc4bd36b6d407cae77537865afe523b79c74cc680b", + "0x1b", + "0xc2ff96feed8749a5ad1c0714f950b5ac939d8acedbedcbc2949614ab8af06312", + "0x1feecd50adc6273fdd5d11c6da18c8cfe14e2787f5a90af7c7c1328e7d0a2c42", + true, + true)); + + arguments.add( + argumentsFromStrings( + "[ICP = 1, successBit = 1]", + "0x4be146e06cc1b37342b6b7b1fa8542ae58a62103b8af0f7d58f8a1ffffcf7914", + "0x1b", + "0xa7b0f504b652b3a621921c78c587fdf80a3ab590e22c304b0b0930e90c4e081d", + "0x5428459ef7e6bd079fbbb7c6fd95cc6c7fe68c93ed4ae75cee36810e79e8a0e5", + true, + true)); + + arguments.add( + argumentsFromStrings( + "[ICP = 1, successBit = 1]", + "0xca3e75570aea0e3dd8e7a9d38c2efa866f5ee2b18bf527a0f4e3248b7c7cf376", + "0x1c", + "0xf1136900c2cd16eacc676f2c7b70f3dfec13fd16a426aab4eda5d8047c30a9e9", + "0x4dad8f009ebe31bdc38133bc5fa60e9dca59d0366bd90e2ef12b465982c696aa", + true, + true)); + + arguments.add( + argumentsFromStrings( + "[ICP = 1, successBit = 1]", + "0x9a3fa82837622a34408888b40af937f21f4e6d051f04326d3d7717848c434448", + "0x1b", + "0x52a734f01d14d161795ba3b38ce329eba468e109b4f2e330af671649ffef4e0e", + "0xe3e2a22b830edf61554ab6c18c7efb9e37e1953c913784db0ef74e1e07c227d3", + true, + true)); + + // Test cases where ICP = 1 but successBit = 0 + // Failing reason QNR + arguments.add( + argumentsFromStrings( + "[ICP = 1, successBit = 0 due to QNR]", + "0x94f66d57fb0a3854a44d94956447e01f8b3f09845860f18856e792c821359162", + "0x1c", + "0x000000000000000000000000000000014551231950b75fc4402da1722fc9baed", + "0x44c819d6b971e456562fefc2408536bdfd9567ee1c6c7dd2a7076625953a1859", + true, + false)); + + // Failing reason INFINITY + arguments.add( + argumentsFromStrings( + "[ICP = 1, successBit = 0 due to INFINITY]", + "0xd33cfae367f4f7413985ff82dc7db3ffbf7a027fb5dad7097b4a15cc85ab6580", + "0x1c", + "0xa12b54d413c4ffaaecd59468de6a7d414d2fa7f2ba700d8e0753ca226410c806", + "0xe9956ef412dceeda0016fe0edfc4746452a8f4d02f21e28cfa6019ee1a8976e8", + true, + false)); + + // Failing reason INFINITY + arguments.add( + argumentsFromStrings( + "[ICP = 1, successBit = 0 due to INFINITY]", + "0x6ec17edf5cecd83ed50c08adfeba8146f69769231f4b7903eba38c2e7e98e173", + "0x1b", + "0xaeb8ffe3655e07edd6bde0ab79edd92d4e7a155385c3d8c8ca117bfd13633516", + "0x4da31701c798fe3078ee9de6e4d892242e235dc078df76b15a9ad82137c6250e", + true, + false)); + + return arguments.stream(); + } + + private static Arguments argumentsFromStrings( + String description, + String h, + String v, + String r, + String s, + boolean expectedInternalChecksPassed, + boolean expectedSuccessBit) { + return Arguments.of(description, h, v, r, s, expectedInternalChecksPassed, expectedSuccessBit); + } + + @ParameterizedTest + @MethodSource("ecRecoverSource") + void testEcRecover( + String description, + EWord h, + EWord v, + EWord r, + EWord s, + boolean expectedInternalChecksPassed, + boolean expectedSuccessBit) { + BytecodeCompiler program = + BytecodeCompiler.newProgram() + // First place the parameters in memory + .push(h) + .push(0) + .op(OpCode.MSTORE) + .push(v) // v + .push(0x20) + .op(OpCode.MSTORE) + .push(r) // r + .push(0x40) + .op(OpCode.MSTORE) + .push(s) // s + .push(0x60) + .op(OpCode.MSTORE) + // Do the call + .push(32) // retSize + .push(0x80) // retOffset + .push(0x80) // argSize + .push(0) // argOffset + .push(1) // address + .push(Bytes.fromHexStringLenient("0xFFFFFFFF")) // gas + .op(OpCode.STATICCALL); + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + // Retrieve recoveredAddress, internalChecksPassed, successBit + // Assert internalChecksPassed and successBit are what expected + EcDataOperation ecDataOperation = + bytecodeRunner.getHub().ecData().getOperations().stream().toList().get(0); + EWord recoveredAddress = + EWord.of( + ecDataOperation.limb().get(8).toUnsignedBigInteger(), + ecDataOperation.limb().get(9).toUnsignedBigInteger()); + boolean internalChecksPassed = ecDataOperation.internalChecksPassed(); + boolean successBit = ecDataOperation.successBit(); + + assertEquals(internalChecksPassed, expectedInternalChecksPassed); + assertEquals(successBit, expectedSuccessBit); + + System.out.println("recoveredAddress: " + recoveredAddress); + System.out.println("internalChecksPassed: " + internalChecksPassed); + System.out.println("successBit: " + successBit); + } } diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeRunner.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeRunner.java index a19e32ddb5..c66f4f73f1 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeRunner.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeRunner.java @@ -22,6 +22,7 @@ import lombok.Setter; import lombok.experimental.Accessors; import net.consensys.linea.zktracer.ZkTracer; +import net.consensys.linea.zktracer.module.hub.Hub; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.crypto.KeyPair; import org.hyperledger.besu.crypto.SECP256K1; @@ -37,6 +38,7 @@ @Accessors(fluent = true) public final class BytecodeRunner { private final Bytes byteCode; + ToyExecutionEnvironment toyExecutionEnvironment; /** * @param byteCode the byte code to test @@ -79,12 +81,18 @@ public void run() { final ToyWorld toyWorld = ToyWorld.builder().accounts(List.of(senderAccount, receiverAccount)).build(); - ToyExecutionEnvironment.builder() - .testValidator(x -> {}) - .toyWorld(toyWorld) - .zkTracerValidator(zkTracerValidator) - .transaction(tx) - .build() - .run(); + toyExecutionEnvironment = + ToyExecutionEnvironment.builder() + .testValidator(x -> {}) + .toyWorld(toyWorld) + .zkTracerValidator(zkTracerValidator) + .transaction(tx) + .build(); + + toyExecutionEnvironment.run(); + } + + public Hub getHub() { + return toyExecutionEnvironment.getHub(); } } diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyExecutionEnvironment.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyExecutionEnvironment.java index 98a00448b7..6770bbc93f 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyExecutionEnvironment.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyExecutionEnvironment.java @@ -38,6 +38,7 @@ import net.consensys.linea.blockcapture.snapshots.TransactionSnapshot; import net.consensys.linea.corset.CorsetValidator; import net.consensys.linea.zktracer.ZkTracer; +import net.consensys.linea.zktracer.module.hub.Hub; import net.consensys.linea.zktracer.module.hub.signals.Exceptions; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.*; @@ -241,4 +242,8 @@ private MainnetTransactionProcessor getMainnetTransactionProcessor() { feeMarket, CoinbaseFeePriceCalculator.eip1559()); } + + public Hub getHub() { + return tracer.getHub(); + } } diff --git a/gradle/trace-files.gradle b/gradle/trace-files.gradle index af85ca6be3..43be61bd4c 100644 --- a/gradle/trace-files.gradle +++ b/gradle/trace-files.gradle @@ -68,7 +68,7 @@ tasks.register('binreftable', TraceFilesTask) { // // Put here modules following the conventional MODULE/columns.lisp naming scheme. // -['bin', 'blockhash', 'ecdata', 'euc', 'mmio', 'shf', 'wcp', 'rlptxn', 'rom', 'romlex', 'trm', 'stp', 'loginfo', 'logdata'].each {moduleName -> +['bin', 'blockhash', 'euc', 'mmio', 'shf', 'wcp', 'rlptxn', 'rom', 'romlex', 'trm', 'stp', 'loginfo', 'logdata'].each {moduleName -> tasks.register(moduleName, TraceFilesTask) { group "Trace files generation" dependsOn corsetExists @@ -81,7 +81,7 @@ tasks.register('binreftable', TraceFilesTask) { // // Put here modules following the conventional MODULE/columns.lisp, MODULE/constants.lisp naming scheme. // -['mmu', 'blake2fmodexpdata', 'blockdata', 'oob', 'exp', 'rlptxrcpt', 'rlpaddr', 'shakiradata', 'mxp'].each {moduleName -> +['mmu', 'blake2fmodexpdata', 'blockdata', 'oob', 'exp', 'rlptxrcpt', 'rlpaddr', 'shakiradata', 'mxp', 'ecdata'].each {moduleName -> tasks.register(moduleName, TraceFilesTask) { group "Trace files generation" dependsOn corsetExists From efeb71e4319aebbebda61fd171dd8fcce67f6fad Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Tue, 28 May 2024 17:17:38 +0300 Subject: [PATCH 07/39] feat(oob): implement OOB module (#371) * Implement OOB * OOB fixed slice usage * OOB fixed ToyExecutionEnvironment after rebase * OOB fixed ByteCodeRunner, OobTests and partially updated module after rebase * OOB updated trace class and classes dependent from it * OOB update wip * OOB renamed precompiles methods * OOB fixed common precompiles * OOB updated blake2f and fixed common precompiles * OOB updated modexp * OOB updated setModexpLead * OOB completed update of modexp, review is needed * OOB fixed comments * OOB fixed maxMbsBbs * OOB fixed comp * OOB updated spillings.toml file * OOB fixed module key * OOB fixed minor bugs relared to prc-modexp_cds and hub justification predictions * OOB reactivated random offset in the tests for precompiles * OOB used parametrized junit tests * OOB substituted assert with assertEquals * fix(oob): renaming and refinements * fix(oob): reverted some renaming and fixed issue with replicated constants * fix(oob): removed assert from tests and getOob from byteCodeRunner and toyExecutionEnvironment * fix(oob): fixed trace-files.gradle after merge * fix(oob): update according to new constants file * fix: wrong constants * fix: fixed bug related to gas limit in BytecodeRunner * fix(oob): fixed call number * fix(oob): filled DATA 7 and 8 for opcodes still wip with 0 * fix(oob): fixed minor issues after review, major issue missing oob in modules to count * fix(oob): added EqualsAndHashCode * fix: removed unused class --------- Co-authored-by: Lorenzo Gentile --- .../consensys/linea/zktracer/ZkTracer.java | 1 - .../linea/zktracer/module/add/Add.java | 8 + .../linea/zktracer/module/hub/Hub.java | 9 +- .../fragment/imc/call/oob/opcodes/SStore.java | 4 +- .../linea/zktracer/module/mod/Mod.java | 7 +- .../linea/zktracer/module/mxp/MxpData.java | 83 +- .../linea/zktracer/module/oob/Oob.java | 169 +++ .../zktracer/module/oob/OobDataChannel.java | 30 +- .../zktracer/module/oob/OobOperation.java | 1302 +++++++++++++++++ .../linea/zktracer/module/oob/Trace.java | 64 +- .../Blake2fCallDataSizeParameters.java | 50 + .../parameters/Blake2fParamsParameters.java | 50 + .../parameters/CallDataLoadOobParameters.java | 47 + .../oob/parameters/CallOobParameters.java | 48 + .../oob/parameters/CreateOobParameters.java | 49 + .../parameters/DeploymentOobParameters.java | 47 + .../oob/parameters/JumpOobParameters.java | 47 + .../oob/parameters/JumpiOobParameters.java | 56 + .../ModexpCallDataSizeParameters.java | 49 + .../parameters/ModexpExtractParameters.java | 52 + .../oob/parameters/ModexpLeadParameters.java | 53 + .../parameters/ModexpPricingParameters.java | 53 + .../oob/parameters/ModexpXbsParameters.java | 51 + .../module/oob/parameters/OobParameters.java | 26 + .../PrecompileCommonOobParameters.java | 59 + .../ReturnDataCopyOobParameters.java | 57 + .../oob/parameters/SstoreOobParameters.java | 38 + .../linea/zktracer/module/shf/Res.java | 2 +- .../linea/zktracer/module/stp/Stp.java | 4 +- .../linea/zktracer/types/AddressUtils.java | 3 + .../linea/zktracer/types/Conversions.java | 2 +- .../src/main/resources/spillings.toml | 1 + .../linea/zktracer/module/mxp/MxpTest.java | 6 + .../zktracer/module/oob/OobCallTest.java | 241 +++ .../module/oob/OobJumpAndJumpiTest.java | 694 +++++++++ .../linea/zktracer/module/oob/OobRdcTest.java | 843 +++++++++++ .../module/oob/OobSha2RipemdIdentityTest.java | 300 ++++ .../zktracer/module/oob/OobTestCommon.java | 55 + .../module/rlpaddr/TestRlpAddress.java | 4 +- .../zktracer/module/shf/ShfRtTracerTest.java | 8 + .../zktracer/testing/BytecodeCompiler.java | 10 + .../zktracer/testing/BytecodeRunner.java | 13 +- .../test/resources/contracts/oob/Callee.sol | 10 + .../test/resources/contracts/oob/Caller.sol | 23 + .../oob/compiledContracts/Callee.bin-runtime | 1 + .../oob/compiledContracts/Caller.bin-runtime | 1 + gradle/tests.gradle | 1 + zkevm-constraints | 2 +- 48 files changed, 4624 insertions(+), 109 deletions(-) create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/Oob.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/OobOperation.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/Blake2fCallDataSizeParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/Blake2fParamsParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CallDataLoadOobParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CallOobParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CreateOobParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/DeploymentOobParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/JumpOobParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/JumpiOobParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpCallDataSizeParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpExtractParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpLeadParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpPricingParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpXbsParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/OobParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/PrecompileCommonOobParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ReturnDataCopyOobParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/SstoreOobParameters.java create mode 100644 arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobCallTest.java create mode 100644 arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobJumpAndJumpiTest.java create mode 100644 arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobRdcTest.java create mode 100644 arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobSha2RipemdIdentityTest.java create mode 100644 arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobTestCommon.java create mode 100644 arithmetization/src/test/resources/contracts/oob/Callee.sol create mode 100644 arithmetization/src/test/resources/contracts/oob/Caller.sol create mode 100644 arithmetization/src/test/resources/contracts/oob/compiledContracts/Callee.bin-runtime create mode 100644 arithmetization/src/test/resources/contracts/oob/compiledContracts/Caller.bin-runtime diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/ZkTracer.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/ZkTracer.java index f8b822d31d..467a4ae688 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/ZkTracer.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/ZkTracer.java @@ -79,7 +79,6 @@ public ZkTracer(final LineaL1L2BridgeConfiguration bridgeConfiguration) { // Load opcodes configured in src/main/resources/opcodes.yml. OpCodes.load(); - // Load spillings configured in src/main/resources/spillings.toml. try { final TomlTable table = diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/add/Add.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/add/Add.java index 66bf1d97a1..bb448aaffe 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/add/Add.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/add/Add.java @@ -15,6 +15,7 @@ package net.consensys.linea.zktracer.module.add; +import java.math.BigInteger; import java.nio.MappedByteBuffer; import java.util.List; @@ -23,6 +24,8 @@ import net.consensys.linea.zktracer.container.stacked.set.StackedSet; import net.consensys.linea.zktracer.module.Module; import net.consensys.linea.zktracer.module.hub.Hub; +import net.consensys.linea.zktracer.opcode.OpCode; +import org.apache.tuweni.bytes.Bytes32; import org.hyperledger.besu.evm.frame.MessageFrame; /** Implementation of a {@link Module} for addition/subtraction. */ @@ -72,4 +75,9 @@ public void commit(List buffers) { public int lineCount() { return this.chunks.lineCount(); } + + public BigInteger callADD(Bytes32 arg1, Bytes32 arg2) { + this.chunks.add(new AddOperation(OpCode.ADD, arg1, arg2)); + return arg1.toUnsignedBigInteger().add(arg2.toUnsignedBigInteger()); + } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java index 6a663c0e98..a2b46d1473 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java @@ -70,6 +70,7 @@ import net.consensys.linea.zktracer.module.mod.Mod; import net.consensys.linea.zktracer.module.mul.Mul; import net.consensys.linea.zktracer.module.mxp.Mxp; +import net.consensys.linea.zktracer.module.oob.Oob; import net.consensys.linea.zktracer.module.rlpaddr.RlpAddr; import net.consensys.linea.zktracer.module.rlptxn.RlpTxn; import net.consensys.linea.zktracer.module.rlptxrcpt.RlpTxrcpt; @@ -229,6 +230,8 @@ public void addTraceSection(TraceSection section) { private final Stp stp = new Stp(this, wcp, mod); private final L2Block l2Block; + @Getter private final Oob oob; + private final List modules; /* * Those modules are not traced, we just compute the number of calls to those @@ -255,6 +258,7 @@ public Hub(final Address l2l1ContractAddress, final Bytes l2l1Topic) { this.logData = new LogData(rlpTxrcpt); this.logInfo = new LogInfo(rlpTxrcpt); this.ecData = new EcData(this, this.wcp, this.ext); + this.oob = new Oob(this, (Add) this.add, this.mod, this.wcp); this.mmu = new Mmu( this.euc, @@ -308,6 +312,7 @@ public Hub(final Address l2l1ContractAddress, final Bytes l2l1Topic) { this.mod, this.mul, this.mxp, + this.oob, this.exp, this.rlpAddr, this.rlpTxn, @@ -349,6 +354,7 @@ public List getModulesToTrace() { this.mod, this.mul, this.mxp, + this.oob, this.rlpAddr, this.rlpTxn, this.rlpTxrcpt, @@ -388,6 +394,7 @@ public List getModulesToCount() { this.mod, this.mul, this.mxp, + this.oob, this.exp, this.rlpAddr, this.rlpTxn, @@ -644,7 +651,7 @@ void triggerModules(MessageFrame frame) { this.mxp.tracePreOpcode(frame); } if (this.pch.signals().oob()) { - // TODO: this.oob.tracePreOpcode(frame); + this.oob.tracePreOpcode(frame); } if (this.pch.signals().stp()) { this.stp.tracePreOpcode(frame); diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/oob/opcodes/SStore.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/oob/opcodes/SStore.java index 9ca25b68e7..5e47d70f62 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/oob/opcodes/SStore.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/oob/opcodes/SStore.java @@ -15,8 +15,8 @@ package net.consensys.linea.zktracer.module.hub.fragment.imc.call.oob.opcodes; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.GAS_CONST_G_CALL_STIPEND; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.OOB_INST_SSTORE; -import static net.consensys.linea.zktracer.module.oob.Trace.G_CALLSTIPEND; import static net.consensys.linea.zktracer.types.Conversions.booleanToBytes; import net.consensys.linea.zktracer.module.hub.fragment.imc.call.oob.OobCall; @@ -28,7 +28,7 @@ public record SStore(long gas) implements OobCall { public Bytes data(OobDataChannel i) { return switch (i) { case DATA_5 -> Bytes.ofUnsignedLong(gas); - case DATA_7 -> booleanToBytes(gas <= G_CALLSTIPEND); + case DATA_7 -> booleanToBytes(gas <= GAS_CONST_G_CALL_STIPEND); default -> Bytes.EMPTY; }; } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mod/Mod.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mod/Mod.java index 63b47150d7..7833df3ac8 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mod/Mod.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mod/Mod.java @@ -15,6 +15,7 @@ package net.consensys.linea.zktracer.module.mod; +import java.math.BigInteger; import java.nio.MappedByteBuffer; import java.util.List; @@ -87,8 +88,9 @@ public List columnsHeaders() { * @param arg1 the divider * @param arg2 the dividend */ - public void callDiv(Bytes32 arg1, Bytes32 arg2) { + public BigInteger callDIV(Bytes32 arg1, Bytes32 arg2) { this.chunks.add(new ModOperation(OpCode.DIV, arg1, arg2)); + return arg1.toUnsignedBigInteger().divide(arg2.toUnsignedBigInteger()); } /** @@ -97,7 +99,8 @@ public void callDiv(Bytes32 arg1, Bytes32 arg2) { * @param arg1 the number * @param arg2 the module */ - public void callMod(Bytes32 arg1, Bytes32 arg2) { + public BigInteger callMOD(Bytes32 arg1, Bytes32 arg2) { this.chunks.add(new ModOperation(OpCode.MOD, arg1, arg2)); + return arg1.toUnsignedBigInteger().mod(arg2.toUnsignedBigInteger()); } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/MxpData.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/MxpData.java index 0654dba5df..430e907dbd 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/MxpData.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/mxp/MxpData.java @@ -115,7 +115,7 @@ public MxpData(final MessageFrame frame, final Hub hub) { @Override protected int computeLineCount() { - return this.ctMax(); + return this.nRows(); } void compute() { @@ -131,15 +131,16 @@ void compute() { } private void setInitializeByteArrays() { - byte1 = new UnsignedByte[ctMax()]; - byte2 = new UnsignedByte[ctMax()]; - byte3 = new UnsignedByte[ctMax()]; - byte4 = new UnsignedByte[ctMax()]; - byteA = new UnsignedByte[ctMax()]; - byteW = new UnsignedByte[ctMax()]; - byteQ = new UnsignedByte[ctMax()]; - byteQQ = new UnsignedByte[ctMax()]; - byteR = new UnsignedByte[ctMax()]; + int nRows = nRows(); + byte1 = new UnsignedByte[nRows]; + byte2 = new UnsignedByte[nRows]; + byte3 = new UnsignedByte[nRows]; + byte4 = new UnsignedByte[nRows]; + byteA = new UnsignedByte[nRows]; + byteW = new UnsignedByte[nRows]; + byteQ = new UnsignedByte[nRows]; + byteQQ = new UnsignedByte[nRows]; + byteR = new UnsignedByte[nRows]; Arrays.fill(byte1, UnsignedByte.of(0)); Arrays.fill(byte2, UnsignedByte.of(0)); Arrays.fill(byte3, UnsignedByte.of(0)); @@ -383,12 +384,16 @@ private mxpExecutionPath getMxpExecutionPath() { public int ctMax() { return switch (this.getMxpExecutionPath()) { - case TRIVIAL -> CT_MAX_TRIVIAL + 1; - case NON_TRIVIAL_BUT_MXPX -> CT_MAX_NON_TRIVIAL_BUT_MXPX + 1; - case NON_TRIVIAL -> CT_MAX_NON_TRIVIAL + 1; + case TRIVIAL -> CT_MAX_TRIVIAL; + case NON_TRIVIAL_BUT_MXPX -> CT_MAX_NON_TRIVIAL_BUT_MXPX; + case NON_TRIVIAL -> CT_MAX_NON_TRIVIAL; }; } + public int nRows() { + return ctMax() + 1; + } + protected void setAccQAndByteQQ() { // accQ and byteQQ all equal to 0 by default if (this.getMxpExecutionPath() == mxpExecutionPath.NON_TRIVIAL) { @@ -414,7 +419,7 @@ protected void setAccQAndByteQQNonTrivialCase() { } protected void setBytes() { - int ctMax = ctMax(); + final int nRows = nRows(); Bytes32 b1 = UInt256.valueOf(acc1); Bytes32 b2 = UInt256.valueOf(acc2); Bytes32 b3 = UInt256.valueOf(acc3); @@ -422,14 +427,14 @@ protected void setBytes() { Bytes32 bA = UInt256.valueOf(accA); Bytes32 bW = UInt256.valueOf(accW); Bytes32 bQ = UInt256.valueOf(accQ); - for (int i = 0; i < ctMax; i++) { - byte1[i] = UnsignedByte.of(b1.get(b1.size() - 1 - ctMax + i)); - byte2[i] = UnsignedByte.of(b2.get(b2.size() - 1 - ctMax + i)); - byte3[i] = UnsignedByte.of(b3.get(b3.size() - 1 - ctMax + i)); - byte4[i] = UnsignedByte.of(b4.get(b4.size() - 1 - ctMax + i)); - byteA[i] = UnsignedByte.of(bA.get(bA.size() - 1 - ctMax + i)); - byteW[i] = UnsignedByte.of(bW.get(bW.size() - 1 - ctMax + i)); - byteQ[i] = UnsignedByte.of(bQ.get(bQ.size() - 1 - ctMax + i)); + for (int i = 0; i < nRows; i++) { + byte1[i] = UnsignedByte.of(b1.get(b1.size() - 1 - nRows + i)); + byte2[i] = UnsignedByte.of(b2.get(b2.size() - 1 - nRows + i)); + byte3[i] = UnsignedByte.of(b3.get(b3.size() - 1 - nRows + i)); + byte4[i] = UnsignedByte.of(b4.get(b4.size() - 1 - nRows + i)); + byteA[i] = UnsignedByte.of(bA.get(bA.size() - 1 - nRows + i)); + byteW[i] = UnsignedByte.of(bW.get(bW.size() - 1 - nRows + i)); + byteQ[i] = UnsignedByte.of(bQ.get(bQ.size() - 1 - nRows + i)); } } @@ -495,10 +500,10 @@ final void trace(int stamp, Trace trace) { final EWord eSize1 = EWord.of(this.size1); final EWord eSize2 = EWord.of(this.size2); - int ctMax = this.ctMax(); - int ctMaxComplement = 32 - ctMax; + final int nRows = this.nRows(); + final int nRowsComplement = 32 - nRows; - for (int i = 0; i < ctMax; i++) { + for (int i = 0; i < nRows; i++) { trace .stamp(stamp) .cn(Bytes.ofUnsignedLong(this.getContextNumber())) @@ -535,20 +540,20 @@ final void trace(int stamp, Trace trace) { .maxOffset2(bigIntegerToBytes(this.getMaxOffset2())) .maxOffset(bigIntegerToBytes(this.getMaxOffset())) .comp(this.isComp()) - .acc1(acc1Bytes32.slice(ctMaxComplement, 1 + i)) - .acc2(acc2Bytes32.slice(ctMaxComplement, 1 + i)) - .acc3(acc3Bytes32.slice(ctMaxComplement, 1 + i)) - .acc4(acc4Bytes32.slice(ctMaxComplement, 1 + i)) - .accA(accABytes32.slice(ctMaxComplement, 1 + i)) - .accW(accWBytes32.slice(ctMaxComplement, 1 + i)) - .accQ(accQBytes32.slice(ctMaxComplement, 1 + i)) - .byte1(UnsignedByte.of(acc1Bytes32.get(ctMaxComplement + i))) - .byte2(UnsignedByte.of(acc2Bytes32.get(ctMaxComplement + i))) - .byte3(UnsignedByte.of(acc3Bytes32.get(ctMaxComplement + i))) - .byte4(UnsignedByte.of(acc4Bytes32.get(ctMaxComplement + i))) - .byteA(UnsignedByte.of(accABytes32.get(ctMaxComplement + i))) - .byteW(UnsignedByte.of(accWBytes32.get(ctMaxComplement + i))) - .byteQ(UnsignedByte.of(accQBytes32.get(ctMaxComplement + i))) + .acc1(acc1Bytes32.slice(nRowsComplement, 1 + i)) + .acc2(acc2Bytes32.slice(nRowsComplement, 1 + i)) + .acc3(acc3Bytes32.slice(nRowsComplement, 1 + i)) + .acc4(acc4Bytes32.slice(nRowsComplement, 1 + i)) + .accA(accABytes32.slice(nRowsComplement, 1 + i)) + .accW(accWBytes32.slice(nRowsComplement, 1 + i)) + .accQ(accQBytes32.slice(nRowsComplement, 1 + i)) + .byte1(UnsignedByte.of(acc1Bytes32.get(nRowsComplement + i))) + .byte2(UnsignedByte.of(acc2Bytes32.get(nRowsComplement + i))) + .byte3(UnsignedByte.of(acc3Bytes32.get(nRowsComplement + i))) + .byte4(UnsignedByte.of(acc4Bytes32.get(nRowsComplement + i))) + .byteA(UnsignedByte.of(accABytes32.get(nRowsComplement + i))) + .byteW(UnsignedByte.of(accWBytes32.get(nRowsComplement + i))) + .byteQ(UnsignedByte.of(accQBytes32.get(nRowsComplement + i))) .byteQq(UnsignedByte.of(this.getByteQQ()[i].toInteger())) .byteR(UnsignedByte.of(this.getByteR()[i].toInteger())) .words(Bytes.ofUnsignedLong(this.getWords())) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/Oob.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/Oob.java new file mode 100644 index 0000000000..ab1bb2ac99 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/Oob.java @@ -0,0 +1,169 @@ +/* + * 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.module.oob; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; + +import java.math.BigInteger; +import java.nio.MappedByteBuffer; +import java.util.List; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.consensys.linea.zktracer.ColumnHeader; +import net.consensys.linea.zktracer.container.stacked.list.StackedList; +import net.consensys.linea.zktracer.module.Module; +import net.consensys.linea.zktracer.module.add.Add; +import net.consensys.linea.zktracer.module.hub.Hub; +import net.consensys.linea.zktracer.module.mod.Mod; +import net.consensys.linea.zktracer.module.wcp.Wcp; +import net.consensys.linea.zktracer.opcode.OpCode; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.internal.Words; + +@RequiredArgsConstructor +/** Implementation of a {@link Module} for out of bounds. */ +public class Oob implements Module { + + /** A list of the operations to trace */ + @Getter private final StackedList chunks = new StackedList<>(); + + private final Hub hub; + private final Add add; + private final Mod mod; + private final Wcp wcp; + + @Override + public String moduleKey() { + return "OOB"; + } + + static final List
PRECOMPILES_HANDLED_BY_OOB = + List.of( + Address.ECREC, + Address.SHA256, + Address.RIPEMD160, + Address.ID, + Address.ALTBN128_ADD, + Address.ALTBN128_MUL, + Address.ALTBN128_PAIRING, + Address.BLAKE2B_F_COMPRESSION); + + @Override + public void tracePreOpcode(MessageFrame frame) { // This will be renamed to tracePreOp + this.chunks.add(new OobOperation(frame, add, mod, wcp, hub, false, 0, 0)); + OpCode opCode = OpCode.of(frame.getCurrentOperation().getOpcode()); + + if (opCode.isCall()) { + Address target = Words.toAddress(frame.getStackItem(1)); + + if (PRECOMPILES_HANDLED_BY_OOB.contains(target)) { + if (target.equals(Address.BLAKE2B_F_COMPRESSION)) { + OobOperation oobOperation = new OobOperation(frame, add, mod, wcp, hub, true, 1, 0); + this.chunks.add(oobOperation); + boolean validCds = oobOperation.getOutgoingResLo()[0].equals(BigInteger.ONE); + if (validCds) { + this.chunks.add(new OobOperation(frame, add, mod, wcp, hub, true, 2, 0)); + } + } else if (target.equals(Address.MODEXP)) { + for (int i = 1; i <= 7; i++) { + this.chunks.add(new OobOperation(frame, add, mod, wcp, hub, true, 0, i)); + } + } else { + // Other precompiles case + this.chunks.add(new OobOperation(frame, add, mod, wcp, hub, true, 0, 0)); + } + } + } + } + + final void traceChunk(final OobOperation chunk, int stamp, Trace trace) { + int nRows = chunk.nRows(); + + for (int ct = 0; ct < nRows; ct++) { + trace = chunk.getOobParameters().trace(trace); + + // Note: if a value is bigger than 128, do not use Bytes.of and use Bytes.ofUnsignedType + // instead (according to size) + trace + .stamp(stamp) + .ct((short) ct) + .ctMax((short) chunk.maxCt()) + .oobInst(bigIntegerToBytes(chunk.getOobInst())) + .isJump(chunk.isJump()) + .isJumpi(chunk.isJumpi()) + .isRdc(chunk.isRdc()) + .isCdl(chunk.isCdl()) + .isXcall(chunk.isXCall()) + .isCall(chunk.isCall()) + .isCreate(chunk.isCreate()) + .isSstore(chunk.isSstore()) + .isDeployment(chunk.isDeployment()) + .isEcrecover(chunk.isEcRecover()) + .isSha2(chunk.isSha2()) + .isRipemd(chunk.isRipemd()) + .isIdentity(chunk.isIdentity()) + .isEcadd(chunk.isEcadd()) + .isEcmul(chunk.isEcmul()) + .isEcpairing(chunk.isEcpairing()) + .isBlake2FCds(chunk.isBlake2FCds()) + .isBlake2FParams(chunk.isBlake2FParams()) + .isModexpCds(chunk.isModexpCds()) + .isModexpXbs(chunk.isModexpXbs()) + .isModexpLead(chunk.isModexpLead()) + .isModexpPricing(chunk.isPrcModexpPricing()) + .isModexpExtract(chunk.isPrcModexpExtract()) + .addFlag(chunk.getAddFlag()[ct]) + .modFlag(chunk.getModFlag()[ct]) + .wcpFlag(chunk.getWcpFlag()[ct]) + .outgoingInst(chunk.getOutgoingInst()[ct]) + .outgoingData1(bigIntegerToBytes(chunk.getOutgoingData1()[ct])) + .outgoingData2(bigIntegerToBytes(chunk.getOutgoingData2()[ct])) + .outgoingData3(bigIntegerToBytes(chunk.getOutgoingData3()[ct])) + .outgoingData4(bigIntegerToBytes(chunk.getOutgoingData4()[ct])) + .outgoingResLo(bigIntegerToBytes(chunk.getOutgoingResLo()[ct])) + .validateRow(); + } + } + + @Override + public void enterTransaction() { + this.chunks.enter(); + } + + @Override + public void popTransaction() { + this.chunks.pop(); + } + + @Override + public int lineCount() { + return this.chunks.stream().mapToInt(OobOperation::nRows).sum(); + } + + @Override + public void commit(List buffers) { + Trace trace = new Trace(buffers); + for (int i = 0; i < this.chunks.size(); i++) { + this.traceChunk(this.chunks.get(i), i + 1, trace); + } + } + + public List columnsHeaders() { + return Trace.headers(this.lineCount()); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/OobDataChannel.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/OobDataChannel.java index ce8afc46ad..41146c7c33 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/OobDataChannel.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/OobDataChannel.java @@ -26,24 +26,16 @@ public enum OobDataChannel { DATA_8; public static OobDataChannel of(int i) { - if (i == 0) { - return DATA_1; - } else if (i == 1) { - return DATA_2; - } else if (i == 2) { - return DATA_3; - } else if (i == 3) { - return DATA_4; - } else if (i == 4) { - return DATA_5; - } else if (i == 5) { - return DATA_6; - } else if (i == 6) { - return DATA_7; - } else if (i == 7) { - return DATA_8; - } - - throw new IllegalArgumentException("unknown OOB data channel"); + return switch (i) { + case 0 -> DATA_1; + case 1 -> DATA_2; + case 2 -> DATA_3; + case 3 -> DATA_4; + case 4 -> DATA_5; + case 5 -> DATA_6; + case 6 -> DATA_7; + case 7 -> DATA_8; + default -> throw new IllegalArgumentException("unknown OOB data channel: %d".formatted(i)); + }; } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/OobOperation.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/OobOperation.java new file mode 100644 index 0000000000..86b31ff68b --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/OobOperation.java @@ -0,0 +1,1302 @@ +/* + * Copyright ConsenSys AG. + * + * 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.module.oob; + +import static com.google.common.math.BigIntegerMath.log2; +import static java.lang.Byte.toUnsignedInt; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.GAS_CONST_G_CALL_STIPEND; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_BLAKE2F_CDS; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_BLAKE2F_PARAMS; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_CALL; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_CDL; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_CREATE; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_DEPLOYMENT; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_ECADD; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_ECMUL; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_ECPAIRING; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_ECRECOVER; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_IDENTITY; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_JUMP; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_JUMPI; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_MODEXP_CDS; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_MODEXP_EXTRACT; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_MODEXP_LEAD; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_MODEXP_PRICING; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_MODEXP_XBS; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_RDC; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_RIPEMD; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_SHA2; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_SSTORE; +import static net.consensys.linea.zktracer.module.oob.Trace.CT_MAX_XCALL; +import static net.consensys.linea.zktracer.module.oob.Trace.G_QUADDIVISOR; +import static net.consensys.linea.zktracer.types.AddressUtils.getDeploymentAddress; +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBoolean; +import static net.consensys.linea.zktracer.types.Conversions.booleanToBigInteger; +import static net.consensys.linea.zktracer.types.Conversions.booleanToInt; + +import java.math.BigInteger; +import java.math.RoundingMode; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import net.consensys.linea.zktracer.ZkTracer; +import net.consensys.linea.zktracer.container.ModuleOperation; +import net.consensys.linea.zktracer.module.add.Add; +import net.consensys.linea.zktracer.module.hub.Hub; +import net.consensys.linea.zktracer.module.mod.Mod; +import net.consensys.linea.zktracer.module.oob.parameters.Blake2fCallDataSizeParameters; +import net.consensys.linea.zktracer.module.oob.parameters.Blake2fParamsParameters; +import net.consensys.linea.zktracer.module.oob.parameters.CallDataLoadOobParameters; +import net.consensys.linea.zktracer.module.oob.parameters.CallOobParameters; +import net.consensys.linea.zktracer.module.oob.parameters.CreateOobParameters; +import net.consensys.linea.zktracer.module.oob.parameters.DeploymentOobParameters; +import net.consensys.linea.zktracer.module.oob.parameters.JumpOobParameters; +import net.consensys.linea.zktracer.module.oob.parameters.JumpiOobParameters; +import net.consensys.linea.zktracer.module.oob.parameters.ModexpCallDataSizeParameters; +import net.consensys.linea.zktracer.module.oob.parameters.ModexpExtractParameters; +import net.consensys.linea.zktracer.module.oob.parameters.ModexpLeadParameters; +import net.consensys.linea.zktracer.module.oob.parameters.ModexpPricingParameters; +import net.consensys.linea.zktracer.module.oob.parameters.ModexpXbsParameters; +import net.consensys.linea.zktracer.module.oob.parameters.OobParameters; +import net.consensys.linea.zktracer.module.oob.parameters.PrecompileCommonOobParameters; +import net.consensys.linea.zktracer.module.oob.parameters.ReturnDataCopyOobParameters; +import net.consensys.linea.zktracer.module.oob.parameters.SstoreOobParameters; +import net.consensys.linea.zktracer.module.wcp.Wcp; +import net.consensys.linea.zktracer.opcode.OpCode; +import net.consensys.linea.zktracer.types.EWord; +import net.consensys.linea.zktracer.types.UnsignedByte; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.internal.Words; + +@Getter +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class OobOperation extends ModuleOperation { + @EqualsAndHashCode.Include private BigInteger oobInst; + @EqualsAndHashCode.Include private OobParameters oobParameters; + + private boolean isJump; + private boolean isJumpi; + private boolean isRdc; + private boolean isCdl; + private boolean isXCall; + private boolean isCall; + private boolean isCreate; + private boolean isSstore; + private boolean isDeployment; + + private boolean isEcRecover; + private boolean isSha2; + private boolean isRipemd; + private boolean isIdentity; + private boolean isEcadd; + private boolean isEcmul; + private boolean isEcpairing; + private boolean isBlake2FCds; + private boolean isBlake2FParams; + private boolean isModexpCds; + private boolean isModexpXbs; + private boolean isModexpLead; + private boolean prcModexpPricing; + private boolean prcModexpExtract; + + private boolean isModexpBbs; + private boolean isModexpEbs; + private boolean isModexpMbs; + + private final boolean[] addFlag; + private final boolean[] modFlag; + private final boolean[] wcpFlag; + + private final UnsignedByte[] outgoingInst; + + private final BigInteger[] outgoingData1; + private final BigInteger[] outgoingData2; + private final BigInteger[] outgoingData3; + private final BigInteger[] outgoingData4; + + private final BigInteger[] outgoingResLo; + + private BigInteger wghtSum; + + private BigInteger precompileCost; + + // Modules for lookups + private final Add add; + private final Mod mod; + private final Wcp wcp; + + private final Hub hub; + + private final int blake2FCallNumber; + private final int modexpCallNumber; + + public OobOperation( + final MessageFrame frame, + final Add add, + final Mod mod, + final Wcp wcp, + final Hub hub, + boolean isPrecompile, + int blake2FCallNumber, + int modexpCallNumber) { + this.add = add; + this.mod = mod; + this.wcp = wcp; + this.hub = hub; + + this.blake2FCallNumber = blake2FCallNumber; + this.modexpCallNumber = modexpCallNumber; + + if (isPrecompile) { + setPrecomileFlagsAndWghtSumAndIncomingInst(frame); + } else { + setOpCodeFlagsAndWghtSumAndIncomingInst(frame); + } + + // Init arrays + int nRows = nRows(); + addFlag = new boolean[nRows]; + modFlag = new boolean[nRows]; + wcpFlag = new boolean[nRows]; + + outgoingInst = new UnsignedByte[nRows]; + outgoingData1 = new BigInteger[nRows]; + outgoingData2 = new BigInteger[nRows]; + outgoingData3 = new BigInteger[nRows]; + outgoingData4 = new BigInteger[nRows]; + outgoingResLo = new BigInteger[nRows]; + + // TODO: ensure that the nonce update for CREATE is not already done + populateColumns(frame); + } + + private void setOpCodeFlagsAndWghtSumAndIncomingInst(MessageFrame frame) { + final OpCode opCode = OpCode.of(frame.getCurrentOperation().getOpcode()); + // In the case of CALLs and CREATEs this value will be replaced + wghtSum = UnsignedByte.of(opCode.byteValue()).toBigInteger(); + + switch (opCode) { + case JUMP: + isJump = true; + break; + case JUMPI: + isJumpi = true; + break; + case RETURNDATACOPY: + isRdc = true; + break; + case CALLDATALOAD: + isCdl = true; + break; + case CALL, CALLCODE, DELEGATECALL, STATICCALL: + if (opCode == OpCode.CALL + && !hub.pch().exceptions().stackUnderflow() + && hub.pch().exceptions().any()) { + isXCall = true; + wghtSum = BigInteger.valueOf(0xCC); + } else { + isCall = true; + wghtSum = BigInteger.valueOf(0xCA); + } + break; + case CREATE, CREATE2: + isCreate = true; + wghtSum = BigInteger.valueOf(0xCE); + break; + case SSTORE: + isSstore = true; + break; + case RETURN: + isDeployment = true; + break; + default: + throw new IllegalArgumentException("OpCode not relevant for Oob"); + } + oobInst = wghtSum; + } + + private void setPrecomileFlagsAndWghtSumAndIncomingInst(MessageFrame frame) { + final Address target = Words.toAddress(frame.getStackItem(1)); + + if (target.equals(Address.ECREC)) { + isEcRecover = true; + wghtSum = Bytes.fromHexString("FF01").toUnsignedBigInteger(); + } else if (target.equals(Address.SHA256)) { + isSha2 = true; + wghtSum = Bytes.fromHexString("FF02").toUnsignedBigInteger(); + } else if (target.equals(Address.RIPEMD160)) { + isRipemd = true; + wghtSum = Bytes.fromHexString("FF03").toUnsignedBigInteger(); + } else if (target.equals(Address.ID)) { + isIdentity = true; + wghtSum = Bytes.fromHexString("FF04").toUnsignedBigInteger(); + } else if (target.equals(Address.ALTBN128_ADD)) { + isEcadd = true; + wghtSum = Bytes.fromHexString("FF06").toUnsignedBigInteger(); + } else if (target.equals(Address.ALTBN128_MUL)) { + isEcmul = true; + wghtSum = Bytes.fromHexString("FF07").toUnsignedBigInteger(); + } else if (target.equals(Address.ALTBN128_PAIRING)) { + isEcpairing = true; + wghtSum = Bytes.fromHexString("FF08").toUnsignedBigInteger(); + } else if (target.equals(Address.BLAKE2B_F_COMPRESSION)) { + if (blake2FCallNumber == 1) { + isBlake2FCds = true; + wghtSum = Bytes.fromHexString("FA09").toUnsignedBigInteger(); + } else if (blake2FCallNumber == 2) { + isBlake2FParams = true; + wghtSum = Bytes.fromHexString("FB09").toUnsignedBigInteger(); + } + } else if (target.equals(Address.MODEXP)) { + switch (modexpCallNumber) { + case 1: + isModexpCds = true; + wghtSum = Bytes.fromHexString("FA05").toUnsignedBigInteger(); + case 2: + isModexpXbs = true; + isModexpBbs = true; + wghtSum = Bytes.fromHexString("FB05").toUnsignedBigInteger(); + case 3: + isModexpXbs = true; + isModexpEbs = true; + wghtSum = Bytes.fromHexString("FB05").toUnsignedBigInteger(); + case 4: + isModexpXbs = true; + isModexpMbs = true; + wghtSum = Bytes.fromHexString("FB05").toUnsignedBigInteger(); + case 5: + isModexpLead = true; + wghtSum = Bytes.fromHexString("FC05").toUnsignedBigInteger(); + case 6: + prcModexpPricing = true; + wghtSum = Bytes.fromHexString("FD05").toUnsignedBigInteger(); + case 7: + prcModexpExtract = true; + wghtSum = Bytes.fromHexString("FE05").toUnsignedBigInteger(); + } + } else { + throw new IllegalArgumentException("Precompile not relevant for Oob"); + } + oobInst = wghtSum; + } + + public boolean isInst() { + return isJump + || isJumpi + || isRdc + || isCdl + || isCall + || isXCall + || isCreate + || isSstore + || isDeployment; + } + + public boolean isCommonPrecompile() { + return isEcRecover || isSha2 || isRipemd || isIdentity || isEcadd || isEcmul || isEcpairing; + } + + public boolean isBlakePrecompile() { + return isBlake2FCds || isBlake2FParams; + } + + public boolean isModexpPrecompile() { + return isModexpCds || isModexpXbs || isModexpLead || prcModexpPricing || prcModexpExtract; + } + + public boolean isPrecompile() { + return isCommonPrecompile() || isBlakePrecompile() || isModexpPrecompile(); + } + + public int maxCt() { + return CT_MAX_JUMP * booleanToInt(isJump) + + CT_MAX_JUMPI * booleanToInt(isJumpi) + + CT_MAX_RDC * booleanToInt(isRdc) + + CT_MAX_CDL * booleanToInt(isCdl) + + CT_MAX_XCALL * booleanToInt(isXCall) + + CT_MAX_CALL * booleanToInt(isCall) + + CT_MAX_CREATE * booleanToInt(isCreate) + + CT_MAX_SSTORE * booleanToInt(isSstore) + + CT_MAX_DEPLOYMENT * booleanToInt(isDeployment) + + CT_MAX_ECRECOVER * booleanToInt(isEcRecover) + + CT_MAX_SHA2 * booleanToInt(isSha2) + + CT_MAX_RIPEMD * booleanToInt(isRipemd) + + CT_MAX_IDENTITY * booleanToInt(isIdentity) + + CT_MAX_ECADD * booleanToInt(isEcadd) + + CT_MAX_ECMUL * booleanToInt(isEcmul) + + CT_MAX_ECPAIRING * booleanToInt(isEcpairing) + + CT_MAX_BLAKE2F_CDS * booleanToInt(isBlake2FCds) + + CT_MAX_BLAKE2F_PARAMS * booleanToInt(isBlake2FParams) + + CT_MAX_MODEXP_CDS * booleanToInt(isModexpCds) + + CT_MAX_MODEXP_XBS * booleanToInt(isModexpXbs) + + CT_MAX_MODEXP_LEAD * booleanToInt(isModexpLead) + + CT_MAX_MODEXP_PRICING * booleanToInt(prcModexpPricing) + + CT_MAX_MODEXP_EXTRACT * booleanToInt(prcModexpExtract); + } + + public int nRows() { + return maxCt() + 1; + } + + private void populateColumns(final MessageFrame frame) { + final OpCode opCode = OpCode.of(frame.getCurrentOperation().getOpcode()); + + if (isInst()) { + if (isJump) { + JumpOobParameters jumpOobParameters = + new JumpOobParameters( + EWord.of(frame.getStackItem(0)), BigInteger.valueOf(frame.getCode().getSize())); + oobParameters = jumpOobParameters; + setJump(jumpOobParameters); + } else if (isJumpi) { + JumpiOobParameters jumpiOobParameters = + new JumpiOobParameters( + EWord.of(frame.getStackItem(0)), + EWord.of(frame.getStackItem(1)), + BigInteger.valueOf(frame.getCode().getSize())); + oobParameters = jumpiOobParameters; + setJumpi(jumpiOobParameters); + } else if (isRdc) { + ReturnDataCopyOobParameters rdcOobParameters = + new ReturnDataCopyOobParameters( + EWord.of(frame.getStackItem(1)), + EWord.of(frame.getStackItem(2)), + BigInteger.valueOf(frame.getReturnData().size())); + oobParameters = rdcOobParameters; + setRdc(rdcOobParameters); + } else if (isCdl) { + CallDataLoadOobParameters cdlOobParameters = + new CallDataLoadOobParameters( + EWord.of(frame.getStackItem(0)), BigInteger.valueOf(frame.getInputData().size())); + oobParameters = cdlOobParameters; + setCdl(cdlOobParameters); + } else if (isXCall) { + // CallOobParameters is used since this is a subcase of CALL + CallOobParameters callOobParameters = + new CallOobParameters( + EWord.of(frame.getStackItem(2)), + BigInteger.ZERO, + !frame.getStackItem(2).isZero(), + BigInteger.ZERO); + oobParameters = callOobParameters; + setXCall(callOobParameters); + } else if (isCall) { + final Account callerAccount = frame.getWorldUpdater().get(frame.getRecipientAddress()); + // DELEGATECALL, STATICCALL cases + EWord val = EWord.ZERO; + boolean nonZeroValue = false; + // CALL, CALLCODE cases + if (opCode == OpCode.CALL || opCode == OpCode.CALLCODE) { + val = EWord.of(frame.getStackItem(2)); + nonZeroValue = !frame.getStackItem(2).isZero(); + } + CallOobParameters callOobParameters = + new CallOobParameters( + val, + callerAccount.getBalance().toUnsignedBigInteger(), // balance (caller address) + nonZeroValue, + BigInteger.valueOf(frame.getDepth())); + oobParameters = callOobParameters; + setCall(callOobParameters); + } else if (isCreate) { + final Account creatorAccount = frame.getWorldUpdater().get(frame.getRecipientAddress()); + final Address deploymentAddress = getDeploymentAddress(frame); + final Account deployedAccount = frame.getWorldUpdater().get(deploymentAddress); + + long nonce = 0; + boolean hasCode = false; + if (deployedAccount != null) { + nonce = deployedAccount.getNonce(); + hasCode = deployedAccount.hasCode(); + } + + final CreateOobParameters createOobParameters = + new CreateOobParameters( + EWord.of(frame.getStackItem(0)), + creatorAccount.getBalance().toUnsignedBigInteger(), // balance (creator address) + BigInteger.valueOf(nonce), // nonce (deployment address) + hasCode, // has_code (deployment address) + BigInteger.valueOf(frame.getDepth())); + + oobParameters = createOobParameters; + setCreate(createOobParameters); + } else if (isSstore) { + final SstoreOobParameters sstoreOobParameters = + new SstoreOobParameters(BigInteger.valueOf(frame.getRemainingGas())); + + oobParameters = sstoreOobParameters; + setSstore(sstoreOobParameters); + } else if (isDeployment) { + final DeploymentOobParameters deploymentOobParameters = + new DeploymentOobParameters(EWord.of(frame.getStackItem(0))); + + oobParameters = deploymentOobParameters; + setDeployment(deploymentOobParameters); + } + } else if (isPrecompile()) { + // DELEGATECALL, STATICCALL cases + int argsOffset = 2; + // this corresponds to argsSize on evm.codes + int cdsIndex = 3; + // this corresponds to retSize on evm.codes + int returnAtCapacityIndex = 5; + // value is not part of the arguments for DELEGATECALL and STATICCALL + boolean transfersValue = false; + // CALL, CALLCODE cases + if (opCode == OpCode.CALL || opCode == OpCode.CALLCODE) { + argsOffset = 3; + cdsIndex = 4; + returnAtCapacityIndex = 6; + transfersValue = !frame.getStackItem(2).isZero(); + } + + final BigInteger callGas = + BigInteger.valueOf( + ZkTracer.gasCalculator.gasAvailableForChildCall( + frame, Words.clampedToLong(frame.getStackItem(0)), transfersValue)); + + final BigInteger cds = EWord.of(frame.getStackItem(cdsIndex)).toUnsignedBigInteger(); + // Note that this check will disappear since it will be the MXP module taking care of it + if (cds.compareTo(EWord.of(frame.getStackItem(cdsIndex)).loBigInt()) > 0) { + throw new IllegalArgumentException("cds hi part is non-zero"); + } + + final BigInteger returnAtCapacity = + EWord.of(frame.getStackItem(returnAtCapacityIndex)).toUnsignedBigInteger(); + + if (isCommonPrecompile()) { + PrecompileCommonOobParameters prcCommonOobParameters = + new PrecompileCommonOobParameters(callGas, cds, returnAtCapacity); + oobParameters = prcCommonOobParameters; + setPrecompile(prcCommonOobParameters); + if (isEcRecover || isEcadd || isEcmul) { + setPrcEcRecoverPrcEcaddPrcEcmul(prcCommonOobParameters); + } else if (isSha2 || isRipemd || isIdentity) { + setPrcSha2PrcRipemdPrcIdentity(prcCommonOobParameters); + } else if (isEcpairing) { + setEcpairing(prcCommonOobParameters); + } + } else if (isModexpPrecompile()) { + final Bytes unpaddedCallData = frame.shadowReadMemory(argsOffset, cds.longValue()); + // pad unpaddedCallData to 96 + final Bytes paddedCallData = + cds.intValue() < 96 + ? Bytes.concatenate(unpaddedCallData, Bytes.repeat((byte) 0, 96 - cds.intValue())) + : unpaddedCallData; + + // cds and the data below can be int when compared (after size check) + final BigInteger bbs = paddedCallData.slice(0, 32).toUnsignedBigInteger(); + final BigInteger ebs = paddedCallData.slice(32, 32).toUnsignedBigInteger(); + final BigInteger mbs = paddedCallData.slice(64, 32).toUnsignedBigInteger(); + + // Check if bbs, ebs and mbs are <= 512 + if (bbs.compareTo(BigInteger.valueOf(512)) > 0 + || ebs.compareTo(BigInteger.valueOf(512)) > 0 + || mbs.compareTo(BigInteger.valueOf(512)) > 0) { + throw new IllegalArgumentException("byte sizes are too big"); + } + + // pad paddedCallData to 96 + bbs + ebs + final Bytes doublePaddedCallData = + cds.intValue() < 96 + bbs.intValue() + ebs.intValue() + ? Bytes.concatenate( + paddedCallData, + Bytes.repeat((byte) 0, 96 + bbs.intValue() + ebs.intValue() - cds.intValue())) + : paddedCallData; + + final BigInteger leadingBytesOfExponent = + doublePaddedCallData + .slice(96 + bbs.intValue(), min(ebs.intValue(), 32)) + .toUnsignedBigInteger(); + + BigInteger exponentLog; + if (ebs.intValue() <= 32 && leadingBytesOfExponent.signum() == 0) { + exponentLog = BigInteger.ZERO; + } else if (ebs.intValue() <= 32 && leadingBytesOfExponent.signum() != 0) { + exponentLog = BigInteger.valueOf(log2(leadingBytesOfExponent, RoundingMode.FLOOR)); + } else if (ebs.intValue() > 32 && leadingBytesOfExponent.signum() != 0) { + exponentLog = + BigInteger.valueOf(8) + .multiply(ebs.subtract(BigInteger.valueOf(32))) + .add(BigInteger.valueOf(log2(leadingBytesOfExponent, RoundingMode.FLOOR))); + } else { + exponentLog = BigInteger.valueOf(8).multiply(ebs.subtract(BigInteger.valueOf(32))); + } + + if (isModexpCds) { + final ModexpCallDataSizeParameters prcModexpCdsParameters = + new ModexpCallDataSizeParameters(cds); + oobParameters = prcModexpCdsParameters; + setModexpCds(prcModexpCdsParameters); + } else if (isModexpXbs) { + final ModexpXbsParameters prcModexpXbsParameters; + if (isModexpBbs) { + prcModexpXbsParameters = + new ModexpXbsParameters( + EWord.of(bbs).hiBigInt(), EWord.of(bbs).loBigInt(), BigInteger.ZERO, false); + } else if (isModexpEbs) { + prcModexpXbsParameters = + new ModexpXbsParameters( + EWord.of(ebs).hiBigInt(), EWord.of(ebs).loBigInt(), BigInteger.ZERO, false); + } else { + // isModexpMbs + prcModexpXbsParameters = + new ModexpXbsParameters( + EWord.of(mbs).hiBigInt(), + EWord.of(mbs).loBigInt(), + EWord.of(bbs).loBigInt(), + true); + } + oobParameters = prcModexpXbsParameters; + setModexpXbs(prcModexpXbsParameters); + } else if (isModexpLead) { + final ModexpLeadParameters prcModexpLeadParameters = + new ModexpLeadParameters(bbs, cds, ebs); + + oobParameters = prcModexpLeadParameters; + setModexpLead(prcModexpLeadParameters); + } else if (prcModexpPricing) { + int maxMbsBbs = max(mbs.intValue(), bbs.intValue()); + final ModexpPricingParameters prcModexpPricingParameters = + new ModexpPricingParameters(callGas, returnAtCapacity, exponentLog, maxMbsBbs); + + oobParameters = prcModexpPricingParameters; + setPrcModexpPricing(prcModexpPricingParameters); + } else if (prcModexpExtract) { + final ModexpExtractParameters prcModexpExtractParameters = + new ModexpExtractParameters(cds, bbs, ebs, mbs); + + oobParameters = prcModexpExtractParameters; + setPrcModexpExtract(prcModexpExtractParameters); + } + } else if (isBlakePrecompile()) { + if (isBlake2FCds) { + final Blake2fCallDataSizeParameters prcBlake2FCdsParameters = + new Blake2fCallDataSizeParameters(cds, returnAtCapacity); + + oobParameters = prcBlake2FCdsParameters; + setBlake2FCds(prcBlake2FCdsParameters); + } else if (isBlake2FParams) { + final BigInteger blakeR = + frame + .shadowReadMemory(argsOffset, cds.longValue()) + .slice(0, 4) + .toUnsignedBigInteger(); + + final BigInteger blakeF = + BigInteger.valueOf( + toUnsignedInt(frame.shadowReadMemory(argsOffset, cds.longValue()).get(212))); + + final Blake2fParamsParameters prcBlake2FParamsParameters = + new Blake2fParamsParameters(callGas, blakeR, blakeF); + + oobParameters = prcBlake2FParamsParameters; + setBlake2FParams(prcBlake2FParamsParameters); + } + } else { + throw new RuntimeException("no opcode or precompile flag was set to true"); + } + } + } + + // Constraint systems for populating lookups + private void callToADD( + int k, BigInteger arg1Hi, BigInteger arg1Lo, BigInteger arg2Hi, BigInteger arg2Lo) { + final EWord arg1 = EWord.of(arg1Hi, arg1Lo); + final EWord arg2 = EWord.of(arg2Hi, arg2Lo); + addFlag[k] = true; + modFlag[k] = false; + wcpFlag[k] = false; + outgoingInst[k] = UnsignedByte.of(OpCode.ADD.byteValue()); + outgoingData1[k] = arg1Hi; + outgoingData2[k] = arg1Lo; + outgoingData3[k] = arg2Hi; + outgoingData4[k] = arg2Lo; + outgoingResLo[k] = + BigInteger.ZERO; // This value is never used and BigInteger.ZERO is a dummy one + + // lookup + add.callADD(arg1, arg2); + } + + private BigInteger callToDIV( + int k, BigInteger arg1Hi, BigInteger arg1Lo, BigInteger arg2Hi, BigInteger arg2Lo) { + final EWord arg1 = EWord.of(arg1Hi, arg1Lo); + final EWord arg2 = EWord.of(arg2Hi, arg2Lo); + addFlag[k] = false; + modFlag[k] = true; + wcpFlag[k] = false; + outgoingInst[k] = UnsignedByte.of(OpCode.DIV.byteValue()); + outgoingData1[k] = arg1Hi; + outgoingData2[k] = arg1Lo; + outgoingData3[k] = arg2Hi; + outgoingData4[k] = arg2Lo; + outgoingResLo[k] = mod.callDIV(arg1, arg2); + return outgoingResLo[k]; + } + + private BigInteger callToMOD( + int k, BigInteger arg1Hi, BigInteger arg1Lo, BigInteger arg2Hi, BigInteger arg2Lo) { + final EWord arg1 = EWord.of(arg1Hi, arg1Lo); + final EWord arg2 = EWord.of(arg2Hi, arg2Lo); + addFlag[k] = false; + modFlag[k] = true; + wcpFlag[k] = false; + outgoingInst[k] = UnsignedByte.of(OpCode.MOD.byteValue()); + outgoingData1[k] = arg1Hi; + outgoingData2[k] = arg1Lo; + outgoingData3[k] = arg2Hi; + outgoingData4[k] = arg2Lo; + outgoingResLo[k] = mod.callMOD(arg1, arg2); + return outgoingResLo[k]; + } + + private boolean callToLT( + int k, BigInteger arg1Hi, BigInteger arg1Lo, BigInteger arg2Hi, BigInteger arg2Lo) { + final EWord arg1 = EWord.of(arg1Hi, arg1Lo); + final EWord arg2 = EWord.of(arg2Hi, arg2Lo); + addFlag[k] = false; + modFlag[k] = false; + wcpFlag[k] = true; + outgoingInst[k] = UnsignedByte.of(OpCode.LT.byteValue()); + outgoingData1[k] = arg1Hi; + outgoingData2[k] = arg1Lo; + outgoingData3[k] = arg2Hi; + outgoingData4[k] = arg2Lo; + boolean r = wcp.callLT(arg1, arg2); + outgoingResLo[k] = booleanToBigInteger(r); + return r; + } + + private boolean callToGT( + int k, BigInteger arg1Hi, BigInteger arg1Lo, BigInteger arg2Hi, BigInteger arg2Lo) { + final EWord arg1 = EWord.of(arg1Hi, arg1Lo); + final EWord arg2 = EWord.of(arg2Hi, arg2Lo); + addFlag[k] = false; + modFlag[k] = false; + wcpFlag[k] = true; + outgoingInst[k] = UnsignedByte.of(OpCode.GT.byteValue()); + outgoingData1[k] = arg1Hi; + outgoingData2[k] = arg1Lo; + outgoingData3[k] = arg2Hi; + outgoingData4[k] = arg2Lo; + boolean r = wcp.callGT(arg1, arg2); + outgoingResLo[k] = booleanToBigInteger(r); + return r; + } + + private boolean callToISZERO(int k, BigInteger arg1Hi, BigInteger arg1Lo) { + final EWord arg1 = EWord.of(arg1Hi, arg1Lo); + addFlag[k] = false; + modFlag[k] = false; + wcpFlag[k] = true; + outgoingInst[k] = UnsignedByte.of(OpCode.ISZERO.byteValue()); + outgoingData1[k] = arg1Hi; + outgoingData2[k] = arg1Lo; + outgoingData3[k] = BigInteger.ZERO; + outgoingData4[k] = BigInteger.ZERO; + boolean r = wcp.callISZERO(arg1); + outgoingResLo[k] = booleanToBigInteger(r); + return r; + } + + private boolean callToEQ( + int k, BigInteger arg1Hi, BigInteger arg1Lo, BigInteger arg2Hi, BigInteger arg2Lo) { + final EWord arg1 = EWord.of(arg1Hi, arg1Lo); + final EWord arg2 = EWord.of(arg2Hi, arg2Lo); + addFlag[k] = false; + modFlag[k] = false; + wcpFlag[k] = true; + outgoingInst[k] = UnsignedByte.of(OpCode.EQ.byteValue()); + outgoingData1[k] = arg1Hi; + outgoingData2[k] = arg1Lo; + outgoingData3[k] = arg2Hi; + outgoingData4[k] = arg2Lo; + boolean r = wcp.callEQ(arg1, arg2); + outgoingResLo[k] = booleanToBigInteger(r); + return r; + } + + private void noCall(int k) { + addFlag[k] = false; + modFlag[k] = false; + wcpFlag[k] = false; + outgoingInst[k] = UnsignedByte.of(0); + outgoingData1[k] = BigInteger.ZERO; + outgoingData2[k] = BigInteger.ZERO; + outgoingData3[k] = BigInteger.ZERO; + outgoingData4[k] = BigInteger.ZERO; + outgoingResLo[k] = BigInteger.ZERO; + } + + // Methods to populate columns + private void setJump(JumpOobParameters jumpOobParameters) { + // row i + final boolean invalidPcNew = + !callToLT( + 0, + jumpOobParameters.pcNewHi(), + jumpOobParameters.pcNewLo(), + BigInteger.ZERO, + jumpOobParameters.codesize()); + } + + private void setJumpi(JumpiOobParameters jumpiOobParameters) { + // row i + final boolean invalidPcNew = + !callToLT( + 0, + jumpiOobParameters.pcNewHi(), + jumpiOobParameters.pcNewLo(), + BigInteger.ZERO, + jumpiOobParameters.codesize()); + + // row i + 1 + final boolean attemptJump = + !callToISZERO( + 1, jumpiOobParameters.jumpConditionHi(), jumpiOobParameters.jumpConditionLo()); + } + + private void setRdc(ReturnDataCopyOobParameters rdcOobParameters) { + // row i + final boolean rdcRoob = + !callToISZERO(0, rdcOobParameters.offsetHi(), rdcOobParameters.sizeHi()); + + // row i + 1 + if (!rdcRoob) { + callToADD( + 1, + BigInteger.ZERO, + rdcOobParameters.offsetLo(), + BigInteger.ZERO, + rdcOobParameters.sizeLo()); + } else { + noCall(1); + } + final BigInteger sum = + addFlag[1] ? rdcOobParameters.offsetLo().add(rdcOobParameters.sizeLo()) : BigInteger.ZERO; + + // row i + 2 + if (!rdcRoob) { + final boolean rdcSoob = + callToGT( + 2, + EWord.of(sum).hiBigInt(), + EWord.of(sum).loBigInt(), + BigInteger.ZERO, + rdcOobParameters.rds()); + } else { + noCall(2); + } + } + + private void setCdl(CallDataLoadOobParameters cdlOobParameters) { + // row i + final boolean touchesRam = + callToLT( + 0, + cdlOobParameters.offsetHi(), + cdlOobParameters.offsetLo(), + BigInteger.ZERO, + cdlOobParameters.cds()); + } + + private void setXCall(CallOobParameters callOobParameters) { + // row i + callToISZERO(0, callOobParameters.valHi(), callOobParameters.valLo()); + } + + private void setCall(CallOobParameters callOobParameters) { + // row i + boolean insufficientBalanceAbort = + callToLT( + 0, + BigInteger.ZERO, + callOobParameters.bal(), + callOobParameters.valHi(), + callOobParameters.valLo()); + + // row i + 1 + final boolean callStackDepthAbort = + !callToLT( + 1, BigInteger.ZERO, callOobParameters.csd(), BigInteger.ZERO, BigInteger.valueOf(1024)); + + // row i + 2 + callToISZERO(2, callOobParameters.valHi(), callOobParameters.valLo()); + } + + private void setCreate(CreateOobParameters createOobParameters) { + // row i + final boolean insufficientBalanceAbort = + callToLT( + 0, + BigInteger.ZERO, + createOobParameters.bal(), + createOobParameters.valHi(), + createOobParameters.valLo()); + + // row i + 1 + final boolean callStackDepthAbort = + !callToLT( + 1, + BigInteger.ZERO, + createOobParameters.csd(), + BigInteger.ZERO, + BigInteger.valueOf(1024)); + + // row i + 2 + final boolean nonZeroNonce = !callToISZERO(2, BigInteger.ZERO, createOobParameters.nonce()); + } + + private void setSstore(SstoreOobParameters sstoreOobParameters) { + // row i + final boolean sufficientGas = + callToLT( + 0, + BigInteger.ZERO, + BigInteger.valueOf(GAS_CONST_G_CALL_STIPEND), + BigInteger.ZERO, + sstoreOobParameters.gas()); + } + + private void setDeployment(DeploymentOobParameters deploymentOobParameters) { + // row i + final boolean exceedsMaxCodeSize = + callToLT( + 0, + BigInteger.ZERO, + BigInteger.valueOf(24576), + deploymentOobParameters.sizeHi(), + deploymentOobParameters.sizeLo()); + } + + private void setPrecompile(PrecompileCommonOobParameters prcOobParameters) { + // row i + final boolean cdsIsZero = callToISZERO(0, BigInteger.ZERO, prcOobParameters.getCds()); + + // row i + 1 + final boolean returnAtCapacityIsZero = + callToISZERO(1, BigInteger.ZERO, prcOobParameters.getReturnAtCapacity()); + + // Set cdsIsZero + prcOobParameters.setCdsIsZero(cdsIsZero); + + // Set returnAtCapacityIsZero + prcOobParameters.setReturnAtCapacityNonZero(!returnAtCapacityIsZero); + } + + private void setPrcEcRecoverPrcEcaddPrcEcmul( + PrecompileCommonOobParameters prcCommonOobParameters) { + precompileCost = + BigInteger.valueOf( + 3000L * booleanToInt(isEcRecover) + + 150L * booleanToInt(isEcadd) + + 6000L * booleanToInt(isEcmul)); + + // row i + 2 + final boolean insufficientGas = + callToLT( + 2, + BigInteger.ZERO, + prcCommonOobParameters.getCallGas(), + BigInteger.ZERO, + precompileCost); + + // Set hubSuccess + final boolean hubSuccess = !insufficientGas; + prcCommonOobParameters.setSuccess(hubSuccess); + + // Set returnGas + final BigInteger returnGas = + hubSuccess ? prcCommonOobParameters.getCallGas().subtract(precompileCost) : BigInteger.ZERO; + prcCommonOobParameters.setReturnGas(returnGas); + } + + private void setPrcSha2PrcRipemdPrcIdentity( + PrecompileCommonOobParameters prcCommonOobParameters) { + // row i + 2 + final BigInteger ceil = + callToDIV( + 2, + BigInteger.ZERO, + prcCommonOobParameters.getCds().add(BigInteger.valueOf(31)), + BigInteger.ZERO, + BigInteger.valueOf(32)); + + precompileCost = + (BigInteger.valueOf(5).add(ceil)) + .multiply( + BigInteger.valueOf( + 12L * booleanToInt(isSha2) + + 120L * booleanToInt(isRipemd) + + 3L * booleanToInt(isIdentity))); + + // row i + 3 + final boolean insufficientGas = + callToLT( + 3, + BigInteger.ZERO, + prcCommonOobParameters.getCallGas(), + BigInteger.ZERO, + precompileCost); + + // Set hubSuccess + final boolean hubSuccess = !insufficientGas; + prcCommonOobParameters.setSuccess(hubSuccess); + + // Set returnGas + final BigInteger returnGas = + hubSuccess ? prcCommonOobParameters.getCallGas().subtract(precompileCost) : BigInteger.ZERO; + prcCommonOobParameters.setReturnGas(returnGas); + } + + private void setEcpairing(PrecompileCommonOobParameters prcCommonOobParameters) { + // row i + 2 + final BigInteger remainder = + callToMOD( + 2, + BigInteger.ZERO, + prcCommonOobParameters.getCds(), + BigInteger.ZERO, + BigInteger.valueOf(192)); + + // row i + 3 + final boolean isMultipleOf192 = callToISZERO(3, BigInteger.ZERO, remainder); + + precompileCost = BigInteger.ZERO; + if (isMultipleOf192) { + precompileCost = + BigInteger.valueOf(45000) + .add( + BigInteger.valueOf(34000) + .multiply(prcCommonOobParameters.getCds().divide(BigInteger.valueOf(192)))); + } + + // row i + 4 + boolean insufficientGas = false; + if (isMultipleOf192) { + insufficientGas = + callToLT( + 4, + BigInteger.ZERO, + prcCommonOobParameters.getCallGas(), + BigInteger.ZERO, + precompileCost); + } else { + noCall(4); + } + + // Set hubSuccess + final boolean hubSuccess = isMultipleOf192 && !insufficientGas; + prcCommonOobParameters.setSuccess(hubSuccess); + + // Set returnGas + final BigInteger returnGas = + hubSuccess ? prcCommonOobParameters.getCallGas().subtract(precompileCost) : BigInteger.ZERO; + prcCommonOobParameters.setReturnGas(returnGas); + } + + private void setModexpCds(ModexpCallDataSizeParameters prcModexpCdsParameters) { + // row i + final boolean extractBbs = + callToLT( + 0, BigInteger.ZERO, BigInteger.ZERO, BigInteger.ZERO, prcModexpCdsParameters.getCds()); + + // row i + 1 + final boolean extractEbs = + callToLT( + 1, + BigInteger.ZERO, + prcModexpCdsParameters.getCds(), + BigInteger.ZERO, + BigInteger.valueOf(32)); + + // row i + 2 + final boolean extractMbs = + callToLT( + 2, + BigInteger.ZERO, + prcModexpCdsParameters.getCds(), + BigInteger.ZERO, + BigInteger.valueOf(64)); + + // Set extractBbs + prcModexpCdsParameters.setExtractBbs(extractBbs); + + // Set extractEbs + prcModexpCdsParameters.setExtractEbs(extractEbs); + + // Set extractMbs + prcModexpCdsParameters.setExtractMbs(extractMbs); + } + + private void setModexpXbs(ModexpXbsParameters prcModexpXbsParameters) { + // row i + final boolean compTo512 = + callToLT( + 0, + prcModexpXbsParameters.getXbsHi(), + prcModexpXbsParameters.getXbsLo(), + BigInteger.ZERO, + BigInteger.valueOf(513)); + + // row i + 1 + final boolean comp = + callToLT( + 1, + BigInteger.ZERO, + prcModexpXbsParameters.getXbsLo(), + BigInteger.ZERO, + prcModexpXbsParameters.getYbsLo()); + + // row i + 2 + callToISZERO(2, BigInteger.ZERO, prcModexpXbsParameters.getXbsLo()); + + // Set maxXbsYbs and xbsNonZero + if (!prcModexpXbsParameters.isComputeMax()) { + prcModexpXbsParameters.setMaxXbsYbs(BigInteger.ZERO); + prcModexpXbsParameters.setXbsNonZero(false); + } else { + prcModexpXbsParameters.setMaxXbsYbs( + comp ? prcModexpXbsParameters.getYbsLo() : prcModexpXbsParameters.getXbsLo()); + prcModexpXbsParameters.setXbsNonZero(!bigIntegerToBoolean(outgoingResLo[2])); + } + } + + private void setModexpLead(ModexpLeadParameters prcModexpLeadParameters) { + // row i + final boolean ebsIsZero = callToISZERO(0, BigInteger.ZERO, prcModexpLeadParameters.getEbs()); + + // row i + 1 + final boolean ebsLessThan32 = + callToLT( + 1, + BigInteger.ZERO, + prcModexpLeadParameters.getEbs(), + BigInteger.ZERO, + BigInteger.valueOf(32)); + + // row i + 2 + final boolean callDataContainsExponentBytes = + callToLT( + 2, + BigInteger.ZERO, + BigInteger.valueOf(96).add(prcModexpLeadParameters.getBbs()), + BigInteger.ZERO, + prcModexpLeadParameters.getCds()); + + // row i + 3 + boolean comp = false; + if (callDataContainsExponentBytes) { + comp = + callToLT( + 3, + BigInteger.ZERO, + prcModexpLeadParameters + .getCds() + .subtract(BigInteger.valueOf(96).add(prcModexpLeadParameters.getBbs())), + BigInteger.ZERO, + BigInteger.valueOf(32)); + } else { + noCall(3); + } + + // Set loadLead + final boolean loadLead = callDataContainsExponentBytes && !ebsIsZero; + prcModexpLeadParameters.setLoadLead(loadLead); + + // Set cdsCutoff + if (!callDataContainsExponentBytes) { + prcModexpLeadParameters.setCdsCutoff(0); + } else { + prcModexpLeadParameters.setCdsCutoff( + comp + ? (prcModexpLeadParameters + .getCds() + .subtract(BigInteger.valueOf(96).add(prcModexpLeadParameters.getBbs())) + .intValue()) + : 32); + } + // Set ebsCutoff + prcModexpLeadParameters.setEbsCutoff( + ebsLessThan32 ? prcModexpLeadParameters.getEbs().intValue() : 32); + + // Set subEbs32 + prcModexpLeadParameters.setSubEbs32( + ebsLessThan32 ? 0 : prcModexpLeadParameters.getEbs().intValue() - 32); + } + + private void setPrcModexpPricing(ModexpPricingParameters prcModexpPricingParameters) { + // row i + final boolean returnAtCapacityIsZero = + callToISZERO(0, BigInteger.ZERO, prcModexpPricingParameters.getReturnAtCapacity()); + + // row i + 1 + final boolean exponentLogIsZero = + callToISZERO(1, BigInteger.ZERO, prcModexpPricingParameters.getExponentLog()); + + // row i + 2 + final BigInteger fOfMax = + callToDIV( + 2, + BigInteger.ZERO, + BigInteger.valueOf( + (long) prcModexpPricingParameters.getMaxMbsBbs() + * prcModexpPricingParameters.getMaxMbsBbs() + + 7), + BigInteger.ZERO, + BigInteger.valueOf(8)); + + // row i + 3 + BigInteger bigNumerator; + if (!exponentLogIsZero) { + bigNumerator = fOfMax.multiply(prcModexpPricingParameters.getExponentLog()); + } else { + bigNumerator = fOfMax; + } + final BigInteger bigQuotient = + callToDIV( + 3, BigInteger.ZERO, bigNumerator, BigInteger.ZERO, BigInteger.valueOf(G_QUADDIVISOR)); + + // row i + 4 + final boolean bigQuotientLT200 = + callToLT(4, BigInteger.ZERO, bigQuotient, BigInteger.ZERO, BigInteger.valueOf(200)); + + // row i + 5 + precompileCost = bigQuotientLT200 ? BigInteger.valueOf(200) : bigQuotient; + + final boolean ramSuccess = + !callToLT( + 5, + BigInteger.ZERO, + prcModexpPricingParameters.getCallGas(), + BigInteger.ZERO, + precompileCost); + + // Set ramSuccess + prcModexpPricingParameters.setSuccess(ramSuccess); + + // Set returnGas + final BigInteger returnGas = + ramSuccess + ? prcModexpPricingParameters.getCallGas().subtract(precompileCost) + : BigInteger.ZERO; + prcModexpPricingParameters.setReturnGas(returnGas); + + // Set returnAtCapacityNonZero + prcModexpPricingParameters.setReturnAtCapacityNonZero(!returnAtCapacityIsZero); + } + + private void setPrcModexpExtract(ModexpExtractParameters prcModexpExtractParameters) { + // row i + final boolean bbsIsZero = callToISZERO(0, BigInteger.ZERO, prcModexpExtractParameters.getBbs()); + + // row i + 1 + final boolean ebsIsZero = callToISZERO(1, BigInteger.ZERO, prcModexpExtractParameters.getEbs()); + + // row i + 2 + final boolean mbsIsZero = callToISZERO(2, BigInteger.ZERO, prcModexpExtractParameters.getMbs()); + + // row i + 3 + final boolean callDataExtendsBeyondExponent = + callToLT( + 3, + BigInteger.ZERO, + BigInteger.valueOf(96) + .add(prcModexpExtractParameters.getBbs().add(prcModexpExtractParameters.getEbs())), + BigInteger.ZERO, + prcModexpExtractParameters.getCds()); + + // Set extractModulus + final boolean extractModulus = callDataExtendsBeyondExponent && !mbsIsZero; + prcModexpExtractParameters.setExtractModulus(extractModulus); + + // Set extractBase + final boolean extractBase = extractModulus && !bbsIsZero; + prcModexpExtractParameters.setExtractBase(extractBase); + + // Set extractExponent + final boolean extractExponent = extractModulus && !ebsIsZero; + prcModexpExtractParameters.setExtractExponent(extractExponent); + } + + private void setBlake2FCds(Blake2fCallDataSizeParameters prcBlake2FCdsParameters) { + // row i + final boolean validCds = + callToEQ( + 0, + BigInteger.ZERO, + prcBlake2FCdsParameters.getCds(), + BigInteger.ZERO, + BigInteger.valueOf(213)); + + // row i + 1 + final boolean returnAtCapacityIsZero = + callToISZERO(1, BigInteger.ZERO, prcBlake2FCdsParameters.getReturnAtCapacity()); + + // Set hubSuccess + prcBlake2FCdsParameters.setSuccess(validCds); + + // Set returnAtCapacityNonZero + prcBlake2FCdsParameters.setReturnAtCapacityNonZero(!returnAtCapacityIsZero); + } + + private void setBlake2FParams(Blake2fParamsParameters prcBlake2FParamsParameters) { + // row i + final boolean sufficientGas = + !callToLT( + 0, + BigInteger.ZERO, + prcBlake2FParamsParameters.getCallGas(), + BigInteger.ZERO, + prcBlake2FParamsParameters.getBlakeR()); // = ramSuccess + + // row i + 1 + final boolean fIsABit = + callToEQ( + 1, + BigInteger.ZERO, + prcBlake2FParamsParameters.getBlakeF(), + BigInteger.ZERO, + prcBlake2FParamsParameters + .getBlakeF() + .multiply(prcBlake2FParamsParameters.getBlakeF())); + + // Set ramSuccess + final boolean ramSuccess = sufficientGas && fIsABit; + prcBlake2FParamsParameters.setSuccess(ramSuccess); + + // Set returnGas + final BigInteger returnGas = + ramSuccess + ? (prcBlake2FParamsParameters + .getCallGas() + .subtract(prcBlake2FParamsParameters.getBlakeR())) + : BigInteger.ZERO; + + prcBlake2FParamsParameters.setReturnGas(returnGas); + } + + @Override + protected int computeLineCount() { + return this.nRows(); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/Trace.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/Trace.java index e0539edda0..0bd4a7f390 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/Trace.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/Trace.java @@ -30,9 +30,8 @@ * Please DO NOT ATTEMPT TO MODIFY this code directly. */ public class Trace { - public static final int ADD = 0x1; - public static final int CT_MAX_BLAKE2F_cds = 0x1; - public static final int CT_MAX_BLAKE2F_params = 0x1; + public static final int CT_MAX_BLAKE2F_CDS = 0x1; + public static final int CT_MAX_BLAKE2F_PARAMS = 0x1; public static final int CT_MAX_CALL = 0x2; public static final int CT_MAX_CDL = 0x0; public static final int CT_MAX_CREATE = 0x2; @@ -44,24 +43,17 @@ public class Trace { public static final int CT_MAX_IDENTITY = 0x3; public static final int CT_MAX_JUMP = 0x0; public static final int CT_MAX_JUMPI = 0x1; - public static final int CT_MAX_MODEXP_cds = 0x2; - public static final int CT_MAX_MODEXP_extract = 0x3; - public static final int CT_MAX_MODEXP_lead = 0x3; - public static final int CT_MAX_MODEXP_pricing = 0x5; - public static final int CT_MAX_MODEXP_xbs = 0x2; + public static final int CT_MAX_MODEXP_CDS = 0x2; + public static final int CT_MAX_MODEXP_EXTRACT = 0x3; + public static final int CT_MAX_MODEXP_LEAD = 0x3; + public static final int CT_MAX_MODEXP_PRICING = 0x5; + public static final int CT_MAX_MODEXP_XBS = 0x2; public static final int CT_MAX_RDC = 0x2; public static final int CT_MAX_RIPEMD = 0x3; public static final int CT_MAX_SHA2 = 0x3; public static final int CT_MAX_SSTORE = 0x0; public static final int CT_MAX_XCALL = 0x0; - public static final int DIV = 0x4; - public static final int EQ = 0x14; - public static final int GT = 0x11; - public static final int G_CALLSTIPEND = 0x8fc; public static final int G_QUADDIVISOR = 0x3; - public static final int ISZERO = 0x15; - public static final int LT = 0x10; - public static final int MOD = 0x6; private final BitSet filled = new BitSet(); private int currentLine = 0; @@ -124,8 +116,8 @@ static List headers(int length) { new ColumnHeader("oob.DATA_6", 32, length), new ColumnHeader("oob.DATA_7", 32, length), new ColumnHeader("oob.DATA_8", 32, length), - new ColumnHeader("oob.IS_BLAKE2F_cds", 1, length), - new ColumnHeader("oob.IS_BLAKE2F_params", 1, length), + new ColumnHeader("oob.IS_BLAKE2F_CDS", 1, length), + new ColumnHeader("oob.IS_BLAKE2F_PARAMS", 1, length), new ColumnHeader("oob.IS_CALL", 1, length), new ColumnHeader("oob.IS_CDL", 1, length), new ColumnHeader("oob.IS_CREATE", 1, length), @@ -137,11 +129,11 @@ static List headers(int length) { new ColumnHeader("oob.IS_IDENTITY", 1, length), new ColumnHeader("oob.IS_JUMP", 1, length), new ColumnHeader("oob.IS_JUMPI", 1, length), - new ColumnHeader("oob.IS_MODEXP_cds", 1, length), - new ColumnHeader("oob.IS_MODEXP_extract", 1, length), - new ColumnHeader("oob.IS_MODEXP_lead", 1, length), - new ColumnHeader("oob.IS_MODEXP_pricing", 1, length), - new ColumnHeader("oob.IS_MODEXP_xbs", 1, length), + new ColumnHeader("oob.IS_MODEXP_CDS", 1, length), + new ColumnHeader("oob.IS_MODEXP_EXTRACT", 1, length), + new ColumnHeader("oob.IS_MODEXP_LEAD", 1, length), + new ColumnHeader("oob.IS_MODEXP_PRICING", 1, length), + new ColumnHeader("oob.IS_MODEXP_XBS", 1, length), new ColumnHeader("oob.IS_RDC", 1, length), new ColumnHeader("oob.IS_RIPEMD", 1, length), new ColumnHeader("oob.IS_SHA2", 1, length), @@ -380,7 +372,7 @@ public Trace data8(final Bytes b) { public Trace isBlake2FCds(final Boolean b) { if (filled.get(11)) { - throw new IllegalStateException("oob.IS_BLAKE2F_cds already set"); + throw new IllegalStateException("oob.IS_BLAKE2F_CDS already set"); } else { filled.set(11); } @@ -392,7 +384,7 @@ public Trace isBlake2FCds(final Boolean b) { public Trace isBlake2FParams(final Boolean b) { if (filled.get(12)) { - throw new IllegalStateException("oob.IS_BLAKE2F_params already set"); + throw new IllegalStateException("oob.IS_BLAKE2F_PARAMS already set"); } else { filled.set(12); } @@ -536,7 +528,7 @@ public Trace isJumpi(final Boolean b) { public Trace isModexpCds(final Boolean b) { if (filled.get(24)) { - throw new IllegalStateException("oob.IS_MODEXP_cds already set"); + throw new IllegalStateException("oob.IS_MODEXP_CDS already set"); } else { filled.set(24); } @@ -548,7 +540,7 @@ public Trace isModexpCds(final Boolean b) { public Trace isModexpExtract(final Boolean b) { if (filled.get(25)) { - throw new IllegalStateException("oob.IS_MODEXP_extract already set"); + throw new IllegalStateException("oob.IS_MODEXP_EXTRACT already set"); } else { filled.set(25); } @@ -560,7 +552,7 @@ public Trace isModexpExtract(final Boolean b) { public Trace isModexpLead(final Boolean b) { if (filled.get(26)) { - throw new IllegalStateException("oob.IS_MODEXP_lead already set"); + throw new IllegalStateException("oob.IS_MODEXP_LEAD already set"); } else { filled.set(26); } @@ -572,7 +564,7 @@ public Trace isModexpLead(final Boolean b) { public Trace isModexpPricing(final Boolean b) { if (filled.get(27)) { - throw new IllegalStateException("oob.IS_MODEXP_pricing already set"); + throw new IllegalStateException("oob.IS_MODEXP_PRICING already set"); } else { filled.set(27); } @@ -584,7 +576,7 @@ public Trace isModexpPricing(final Boolean b) { public Trace isModexpXbs(final Boolean b) { if (filled.get(28)) { - throw new IllegalStateException("oob.IS_MODEXP_xbs already set"); + throw new IllegalStateException("oob.IS_MODEXP_XBS already set"); } else { filled.set(28); } @@ -844,11 +836,11 @@ public Trace validateRow() { } if (!filled.get(11)) { - throw new IllegalStateException("oob.IS_BLAKE2F_cds has not been filled"); + throw new IllegalStateException("oob.IS_BLAKE2F_CDS has not been filled"); } if (!filled.get(12)) { - throw new IllegalStateException("oob.IS_BLAKE2F_params has not been filled"); + throw new IllegalStateException("oob.IS_BLAKE2F_PARAMS has not been filled"); } if (!filled.get(13)) { @@ -896,23 +888,23 @@ public Trace validateRow() { } if (!filled.get(24)) { - throw new IllegalStateException("oob.IS_MODEXP_cds has not been filled"); + throw new IllegalStateException("oob.IS_MODEXP_CDS has not been filled"); } if (!filled.get(25)) { - throw new IllegalStateException("oob.IS_MODEXP_extract has not been filled"); + throw new IllegalStateException("oob.IS_MODEXP_EXTRACT has not been filled"); } if (!filled.get(26)) { - throw new IllegalStateException("oob.IS_MODEXP_lead has not been filled"); + throw new IllegalStateException("oob.IS_MODEXP_LEAD has not been filled"); } if (!filled.get(27)) { - throw new IllegalStateException("oob.IS_MODEXP_pricing has not been filled"); + throw new IllegalStateException("oob.IS_MODEXP_PRICING has not been filled"); } if (!filled.get(28)) { - throw new IllegalStateException("oob.IS_MODEXP_xbs has not been filled"); + throw new IllegalStateException("oob.IS_MODEXP_XBS has not been filled"); } if (!filled.get(29)) { diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/Blake2fCallDataSizeParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/Blake2fCallDataSizeParameters.java new file mode 100644 index 0000000000..f3e96b8a82 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/Blake2fCallDataSizeParameters.java @@ -0,0 +1,50 @@ +/* + * Copyright ConsenSys AG. + * + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; +import static net.consensys.linea.zktracer.types.Conversions.booleanToBytes; + +import java.math.BigInteger; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.consensys.linea.zktracer.module.oob.Trace; +import org.apache.tuweni.bytes.Bytes; + +@Getter +@RequiredArgsConstructor +public class Blake2fCallDataSizeParameters implements OobParameters { + private final BigInteger cds; + private final BigInteger returnAtCapacity; + + @Setter private boolean success; + @Setter private boolean returnAtCapacityNonZero; + + @Override + public Trace trace(Trace trace) { + return trace + .data1(ZERO) + .data2(bigIntegerToBytes(cds)) + .data3(bigIntegerToBytes(returnAtCapacity)) + .data4(booleanToBytes(success)) // Set after the constructor + .data5(ZERO) + .data6(Bytes.of(0)) + .data7(Bytes.of(0)) + .data8(booleanToBytes(returnAtCapacityNonZero)); // Set after the constructor + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/Blake2fParamsParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/Blake2fParamsParameters.java new file mode 100644 index 0000000000..305aba4638 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/Blake2fParamsParameters.java @@ -0,0 +1,50 @@ +/* + * Copyright ConsenSys AG. + * + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; +import static net.consensys.linea.zktracer.types.Conversions.booleanToBytes; + +import java.math.BigInteger; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.consensys.linea.zktracer.module.oob.Trace; + +@Getter +@RequiredArgsConstructor +public class Blake2fParamsParameters implements OobParameters { + private final BigInteger callGas; + private final BigInteger blakeR; + private final BigInteger blakeF; + + @Setter private boolean success; + @Setter private BigInteger returnGas; + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(callGas)) + .data2(ZERO) + .data3(ZERO) + .data4(booleanToBytes(success)) // Set after the constructor + .data5(bigIntegerToBytes(returnGas)) // Set after the constructor + .data6(bigIntegerToBytes(blakeR)) + .data7(bigIntegerToBytes(blakeF)) + .data8(ZERO); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CallDataLoadOobParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CallDataLoadOobParameters.java new file mode 100644 index 0000000000..a2267d08ad --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CallDataLoadOobParameters.java @@ -0,0 +1,47 @@ +/* + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; + +import java.math.BigInteger; + +import net.consensys.linea.zktracer.module.oob.Trace; +import net.consensys.linea.zktracer.types.EWord; + +public record CallDataLoadOobParameters(EWord offset, BigInteger cds) implements OobParameters { + + public BigInteger offsetHi() { + return offset.hiBigInt(); + } + + public BigInteger offsetLo() { + return offset.loBigInt(); + } + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(offsetHi())) + .data2(bigIntegerToBytes(offsetLo())) + .data3(ZERO) + .data4(ZERO) + .data5(bigIntegerToBytes(cds)) + .data6(ZERO) + .data7(ZERO) // TODO: temporary value; to fill when oob update is complete + .data8(ZERO); // TODO: temporary value; to fill when oob update is complete + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CallOobParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CallOobParameters.java new file mode 100644 index 0000000000..b01d703dff --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CallOobParameters.java @@ -0,0 +1,48 @@ +/* + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; + +import java.math.BigInteger; + +import net.consensys.linea.zktracer.module.oob.Trace; +import net.consensys.linea.zktracer.types.EWord; + +public record CallOobParameters(EWord val, BigInteger bal, boolean nonZeroValue, BigInteger csd) + implements OobParameters { + + public BigInteger valHi() { + return val.hiBigInt(); + } + + public BigInteger valLo() { + return val.loBigInt(); + } + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(valHi())) + .data2(bigIntegerToBytes(valLo())) + .data3(bigIntegerToBytes(bal)) + .data4(nonZeroValue ? ONE : ZERO) + .data5(ZERO) + .data6(bigIntegerToBytes(csd)) + .data7(ZERO) // TODO: temporary value; to fill when oob update is complete + .data8(ZERO); // TODO: temporary value; to fill when oob update is complete + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CreateOobParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CreateOobParameters.java new file mode 100644 index 0000000000..d118e6060e --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/CreateOobParameters.java @@ -0,0 +1,49 @@ +/* + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; + +import java.math.BigInteger; + +import net.consensys.linea.zktracer.module.oob.Trace; +import net.consensys.linea.zktracer.types.EWord; + +public record CreateOobParameters( + EWord val, BigInteger bal, BigInteger nonce, boolean hasCode, BigInteger csd) + implements OobParameters { + + public BigInteger valHi() { + return val.hiBigInt(); + } + + public BigInteger valLo() { + return val.loBigInt(); + } + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(valHi())) + .data2(bigIntegerToBytes(valLo())) + .data3(bigIntegerToBytes(bal)) + .data4(bigIntegerToBytes(nonce)) + .data5((hasCode ? ONE : ZERO)) + .data6(bigIntegerToBytes(csd)) + .data7(ZERO) // TODO: temporary value; to fill when oob update is complete + .data8(ZERO); // TODO: temporary value; to fill when oob update is complete + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/DeploymentOobParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/DeploymentOobParameters.java new file mode 100644 index 0000000000..7e492935ad --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/DeploymentOobParameters.java @@ -0,0 +1,47 @@ +/* + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; + +import java.math.BigInteger; + +import net.consensys.linea.zktracer.module.oob.Trace; +import net.consensys.linea.zktracer.types.EWord; + +public record DeploymentOobParameters(EWord size) implements OobParameters { + + public BigInteger sizeHi() { + return size.hiBigInt(); + } + + public BigInteger sizeLo() { + return size.loBigInt(); + } + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(sizeHi())) + .data2(bigIntegerToBytes(sizeLo())) + .data3(ZERO) + .data4(ZERO) + .data5(ZERO) + .data6(ZERO) + .data7(ZERO) // TODO: temporary value; to fill when oob update is complete + .data8(ZERO); // TODO: temporary value; to fill when oob update is complete + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/JumpOobParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/JumpOobParameters.java new file mode 100644 index 0000000000..0a1ea6b1da --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/JumpOobParameters.java @@ -0,0 +1,47 @@ +/* + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; + +import java.math.BigInteger; + +import net.consensys.linea.zktracer.module.oob.Trace; +import net.consensys.linea.zktracer.types.EWord; + +public record JumpOobParameters(EWord pcNew, BigInteger codesize) implements OobParameters { + + public BigInteger pcNewHi() { + return pcNew.hiBigInt(); + } + + public BigInteger pcNewLo() { + return pcNew.loBigInt(); + } + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(pcNewHi())) + .data2(bigIntegerToBytes(pcNewLo())) + .data3(ZERO) + .data4(ZERO) + .data5(bigIntegerToBytes(codesize)) + .data6(ZERO) + .data7(ZERO) // TODO: temporary value; to fill when oob update is complete + .data8(ZERO); // TODO: temporary value; to fill when oob update is complete + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/JumpiOobParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/JumpiOobParameters.java new file mode 100644 index 0000000000..9ffb247faf --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/JumpiOobParameters.java @@ -0,0 +1,56 @@ +/* + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; + +import java.math.BigInteger; + +import net.consensys.linea.zktracer.module.oob.Trace; +import net.consensys.linea.zktracer.types.EWord; + +public record JumpiOobParameters(EWord pcNew, EWord jumpCondition, BigInteger codesize) + implements OobParameters { + + public BigInteger pcNewHi() { + return pcNew.hiBigInt(); + } + + public BigInteger pcNewLo() { + return pcNew.loBigInt(); + } + + public BigInteger jumpConditionHi() { + return jumpCondition.hiBigInt(); + } + + public BigInteger jumpConditionLo() { + return jumpCondition.loBigInt(); + } + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(pcNewHi())) + .data2(bigIntegerToBytes(pcNewLo())) + .data3(bigIntegerToBytes(jumpConditionHi())) + .data4(bigIntegerToBytes(jumpConditionLo())) + .data5(bigIntegerToBytes(codesize)) + .data6(ZERO) + .data7(ZERO) // TODO: temporary value; to fill when oob update is complete + .data8(ZERO); // TODO: temporary value; to fill when oob update is complete + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpCallDataSizeParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpCallDataSizeParameters.java new file mode 100644 index 0000000000..b75222e75b --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpCallDataSizeParameters.java @@ -0,0 +1,49 @@ +/* + * Copyright ConsenSys AG. + * + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; +import static net.consensys.linea.zktracer.types.Conversions.booleanToBytes; + +import java.math.BigInteger; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.consensys.linea.zktracer.module.oob.Trace; + +@Getter +@RequiredArgsConstructor +public class ModexpCallDataSizeParameters implements OobParameters { + private final BigInteger cds; + + @Setter private boolean extractBbs; + @Setter private boolean extractEbs; + @Setter private boolean extractMbs; + + @Override + public Trace trace(Trace trace) { + return trace + .data1(ZERO) + .data2(bigIntegerToBytes(cds)) + .data3(booleanToBytes(extractBbs)) + .data4(booleanToBytes(extractEbs)) + .data5(booleanToBytes(extractMbs)) + .data6(ZERO) + .data7(ZERO) + .data8(ZERO); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpExtractParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpExtractParameters.java new file mode 100644 index 0000000000..c5db01cdc4 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpExtractParameters.java @@ -0,0 +1,52 @@ +/* + * Copyright ConsenSys AG. + * + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; +import static net.consensys.linea.zktracer.types.Conversions.booleanToBytes; + +import java.math.BigInteger; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.consensys.linea.zktracer.module.oob.Trace; + +@Getter +@RequiredArgsConstructor +public class ModexpExtractParameters implements OobParameters { + private final BigInteger cds; + private final BigInteger bbs; + private final BigInteger ebs; + private final BigInteger mbs; + + @Setter private boolean extractBase; + @Setter private boolean extractExponent; + @Setter private boolean extractModulus; + + @Override + public Trace trace(Trace trace) { + return trace + .data1(ZERO) + .data2(bigIntegerToBytes(cds)) + .data3(bigIntegerToBytes(bbs)) + .data4(bigIntegerToBytes(ebs)) + .data5(bigIntegerToBytes(mbs)) + .data6(booleanToBytes(extractBase)) + .data7(booleanToBytes(extractExponent)) + .data8(booleanToBytes(extractModulus)); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpLeadParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpLeadParameters.java new file mode 100644 index 0000000000..78d86c5039 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpLeadParameters.java @@ -0,0 +1,53 @@ +/* + * Copyright ConsenSys AG. + * + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; +import static net.consensys.linea.zktracer.types.Conversions.booleanToBytes; + +import java.math.BigInteger; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.consensys.linea.zktracer.module.oob.Trace; +import org.apache.tuweni.bytes.Bytes; + +@Getter +@RequiredArgsConstructor +public class ModexpLeadParameters implements OobParameters { + private final BigInteger bbs; + private final BigInteger cds; + private final BigInteger ebs; + + @Setter boolean loadLead; + @Setter int cdsCutoff; + @Setter int ebsCutoff; + @Setter int subEbs32; + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(bbs)) + .data2(bigIntegerToBytes(cds)) + .data3(bigIntegerToBytes(ebs)) + .data4(booleanToBytes(loadLead)) + .data5(ZERO) + .data6(Bytes.of(cdsCutoff)) + .data7(Bytes.of(ebsCutoff)) + .data8(Bytes.of(subEbs32)); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpPricingParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpPricingParameters.java new file mode 100644 index 0000000000..6d65d0327f --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpPricingParameters.java @@ -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.zktracer.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; +import static net.consensys.linea.zktracer.types.Conversions.booleanToBytes; + +import java.math.BigInteger; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.consensys.linea.zktracer.module.oob.Trace; +import org.apache.tuweni.bytes.Bytes; + +@Getter +@RequiredArgsConstructor +public class ModexpPricingParameters implements OobParameters { + private final BigInteger callGas; + private final BigInteger returnAtCapacity; + @Setter private boolean success; + private final BigInteger exponentLog; + private final int maxMbsBbs; + + @Setter private BigInteger returnGas; + @Setter private boolean returnAtCapacityNonZero; + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(callGas)) + .data2(ZERO) + .data3(bigIntegerToBytes(returnAtCapacity)) + .data4(booleanToBytes(success)) + .data5(bigIntegerToBytes(returnGas)) + .data6(bigIntegerToBytes(exponentLog)) + .data7(Bytes.of(maxMbsBbs)) + .data8(booleanToBytes(returnAtCapacityNonZero)); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpXbsParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpXbsParameters.java new file mode 100644 index 0000000000..b0c5ee18d8 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ModexpXbsParameters.java @@ -0,0 +1,51 @@ +/* + * Copyright ConsenSys AG. + * + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; +import static net.consensys.linea.zktracer.types.Conversions.booleanToBytes; + +import java.math.BigInteger; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.consensys.linea.zktracer.module.oob.Trace; + +@Getter +@RequiredArgsConstructor +public class ModexpXbsParameters implements OobParameters { + private final BigInteger xbsHi; + private final BigInteger xbsLo; + private final BigInteger ybsLo; + private final boolean computeMax; + + @Setter private BigInteger maxXbsYbs; + @Setter private boolean xbsNonZero; + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(xbsHi)) + .data2(bigIntegerToBytes(xbsLo)) + .data3(bigIntegerToBytes(ybsLo)) + .data4(booleanToBytes(computeMax)) + .data5(ZERO) + .data6(ZERO) + .data7(bigIntegerToBytes(maxXbsYbs)) + .data8(booleanToBytes(xbsNonZero)); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/OobParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/OobParameters.java new file mode 100644 index 0000000000..9ac37260f3 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/OobParameters.java @@ -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.zktracer.module.oob.parameters; + +import net.consensys.linea.zktracer.module.oob.Trace; +import org.apache.tuweni.bytes.Bytes; + +public interface OobParameters { + Bytes ZERO = Bytes.EMPTY; + Bytes ONE = Bytes.of(1); + + Trace trace(Trace trace); +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/PrecompileCommonOobParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/PrecompileCommonOobParameters.java new file mode 100644 index 0000000000..f6df85e656 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/PrecompileCommonOobParameters.java @@ -0,0 +1,59 @@ +/* + * Copyright ConsenSys AG. + * + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; +import static net.consensys.linea.zktracer.types.Conversions.booleanToBytes; + +import java.math.BigInteger; + +import lombok.Getter; +import lombok.Setter; +import net.consensys.linea.zktracer.module.oob.Trace; + +@Getter +public class PrecompileCommonOobParameters implements OobParameters { + + private BigInteger callGas; + private BigInteger cds; + private BigInteger returnAtCapacity; + @Setter private boolean success; + @Setter private BigInteger returnGas; + @Setter private boolean returnAtCapacityNonZero; + @Setter private boolean cdsIsZero; // Necessary to compute extractCallData and emptyCallData + + public PrecompileCommonOobParameters( + BigInteger callGas, BigInteger cds, BigInteger returnAtCapacity) { + this.callGas = callGas; + this.cds = cds; + this.returnAtCapacity = returnAtCapacity; + } + + @Override + public Trace trace(Trace trace) { + boolean extractCallData = success && !cdsIsZero; + boolean emptyCallData = success && cdsIsZero; + return trace + .data1(bigIntegerToBytes(callGas)) + .data2(bigIntegerToBytes(cds)) + .data3(bigIntegerToBytes(returnAtCapacity)) + .data4(booleanToBytes(success)) // Set after the constructor + .data5(bigIntegerToBytes(returnGas)) // Set after the constructor + .data6(booleanToBytes(extractCallData)) // Derived from other parameters + .data7(booleanToBytes(emptyCallData)) // Derived from other parameters + .data8(booleanToBytes(returnAtCapacityNonZero)); // Set after the constructor + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ReturnDataCopyOobParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ReturnDataCopyOobParameters.java new file mode 100644 index 0000000000..1d0aea84be --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/ReturnDataCopyOobParameters.java @@ -0,0 +1,57 @@ +/* + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; + +import java.math.BigInteger; + +import net.consensys.linea.zktracer.module.oob.Trace; +import net.consensys.linea.zktracer.types.EWord; +import org.apache.tuweni.bytes.Bytes; + +public record ReturnDataCopyOobParameters(EWord offset, EWord size, BigInteger rds) + implements OobParameters { + + public BigInteger offsetHi() { + return offset.hiBigInt(); + } + + public BigInteger offsetLo() { + return offset.loBigInt(); + } + + public BigInteger sizeHi() { + return size.hiBigInt(); + } + + public BigInteger sizeLo() { + return size.loBigInt(); + } + + @Override + public Trace trace(Trace trace) { + return trace + .data1(bigIntegerToBytes(offsetHi())) + .data2(bigIntegerToBytes(offsetLo())) + .data3(bigIntegerToBytes(sizeHi())) + .data4(Bytes.wrap(sizeLo().toByteArray())) + .data5(bigIntegerToBytes(rds)) + .data6(ZERO) + .data7(ZERO) // TODO: temporary value; to fill when oob update is complete + .data8(ZERO); // TODO: temporary value; to fill when oob update is complete + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/SstoreOobParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/SstoreOobParameters.java new file mode 100644 index 0000000000..1ac1910a6f --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/oob/parameters/SstoreOobParameters.java @@ -0,0 +1,38 @@ +/* + * 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.module.oob.parameters; + +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; + +import java.math.BigInteger; + +import net.consensys.linea.zktracer.module.oob.Trace; + +public record SstoreOobParameters(BigInteger gas) implements OobParameters { + + @Override + public Trace trace(Trace trace) { + return trace + .data1(ZERO) + .data2(ZERO) + .data3(ZERO) + .data4(ZERO) + .data5(bigIntegerToBytes(gas)) + .data6(ZERO) + .data7(ZERO) // TODO: temporary value; to fill when oob update is complete + .data8(ZERO); // TODO: temporary value; to fill when oob update is complete + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shf/Res.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shf/Res.java index 478b95a036..9a5be69b75 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shf/Res.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/shf/Res.java @@ -28,7 +28,6 @@ public class Res { public static Res create(final OpCode opCode, final Bytes32 arg1, final Bytes32 arg2) { final Bytes32 result = Shifter.shift(opCode, arg2, shiftBy(arg1)); - return new Res(Bytes16.wrap(result.slice(0, 16)), Bytes16.wrap(result.slice(16))); } @@ -38,6 +37,7 @@ private static int shiftBy(final Bytes32 arg) { private static boolean allButLastByteZero(final Bytes32 bytes) { for (int i = 0; i < 31; i++) { + // careful: bytes are signed if (bytes.get(i) != 0) { return false; } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/stp/Stp.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/stp/Stp.java index 2ba1cee782..26fcde80a4 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/stp/Stp.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/stp/Stp.java @@ -80,7 +80,7 @@ public void tracePreOpcode(final MessageFrame frame) { this.wcp.callLT(longToBytes32(chunk.gasActual()), Bytes32.ZERO); this.wcp.callLT(longToBytes32(chunk.gasActual()), longToBytes32(chunk.gasPrelim())); if (!chunk.oogx()) { - this.mod.callDiv(longToBytes32(chunk.getGDiff()), longToBytes32(64L)); + this.mod.callDIV(longToBytes32(chunk.getGDiff()), longToBytes32(64L)); } } case CALL, CALLCODE, DELEGATECALL, STATICCALL -> { @@ -92,7 +92,7 @@ public void tracePreOpcode(final MessageFrame frame) { } this.wcp.callLT(longToBytes32(chunk.gasActual()), longToBytes32(chunk.gasPrelim())); if (!chunk.oogx()) { - this.mod.callDiv(longToBytes32(chunk.getGDiff()), longToBytes32(64L)); + this.mod.callDIV(longToBytes32(chunk.getGDiff()), longToBytes32(64L)); this.wcp.callLT(chunk.gas().orElseThrow(), longToBytes32(chunk.get63of64GDiff())); } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/types/AddressUtils.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/types/AddressUtils.java index 6cded5f74b..b681bf15d5 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/types/AddressUtils.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/types/AddressUtils.java @@ -57,6 +57,9 @@ public static Address effectiveToAddress(Transaction tx) { .orElse(Address.contractAddress(tx.getSender(), tx.getNonce())); } + /* Warning: this method uses the nonce as currently found in the state + however, CREATE raises the nonce and so this method should only be called + pre OpCode and pre transaction for deployment */ public static Address getCreateAddress(final MessageFrame frame) { return Address.extract(getCreateRawAddress(frame)); } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/types/Conversions.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/types/Conversions.java index b1ab3d3dff..9904f4737e 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/types/Conversions.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/types/Conversions.java @@ -101,7 +101,7 @@ public static BigInteger booleanToBigInteger(final boolean input) { public static int booleanToInt(final boolean input) { return input ? 1 : 0; } - // Also implemented in oob branch (remove it after merge) + public static boolean bigIntegerToBoolean(BigInteger n) { if (!n.equals(BigInteger.ONE) && !n.equals(BigInteger.ZERO)) { throw new IllegalArgumentException( diff --git a/arithmetization/src/main/resources/spillings.toml b/arithmetization/src/main/resources/spillings.toml index 0e3fa7d654..add52375ca 100644 --- a/arithmetization/src/main/resources/spillings.toml +++ b/arithmetization/src/main/resources/spillings.toml @@ -28,6 +28,7 @@ SHF = 16 STP = 4 TRM = 7 WCP = 16 +OOB = 5 PRECOMPILE_ECRECOVER_EFFECTIVE_CALL = 0 PRECOMPILE_SHA2_BLOCKS = 0 diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/mxp/MxpTest.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/mxp/MxpTest.java index 634ddf33a1..9a8f51c0cc 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/mxp/MxpTest.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/mxp/MxpTest.java @@ -74,6 +74,11 @@ public class MxpTest { final OpCode[] opCodesHalting = new OpCode[] {OpCode.RETURN, OpCode.REVERT}; + @Test + void testMxpMinimalNonEmptyReturn() { + BytecodeRunner.of(Bytes.fromHexString("6101006000f3")).run(); + } + @Test void testMxpSimple() { BytecodeRunner.of( @@ -274,6 +279,7 @@ void testCall() { .run(); } + // Support methods private Bytes getRandomINITForCreate() { final int INSTRUCTION_COUNT_INIT = 256; BytecodeCompiler INIT = BytecodeCompiler.newProgram(); diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobCallTest.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobCallTest.java new file mode 100644 index 0000000000..437139003c --- /dev/null +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobCallTest.java @@ -0,0 +1,241 @@ +/* + * 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.module.oob; + +import static net.consensys.linea.zktracer.module.oob.OobTestCommon.assertNumberOfOnesInOobEvent1; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigInteger; +import java.util.List; + +import net.consensys.linea.zktracer.module.hub.Hub; +import net.consensys.linea.zktracer.testing.BytecodeRunner; +import net.consensys.linea.zktracer.testing.EvmExtension; +import net.consensys.linea.zktracer.testing.ToyAccount; +import net.consensys.linea.zktracer.testing.ToyExecutionEnvironment; +import net.consensys.linea.zktracer.testing.ToyTransaction; +import net.consensys.linea.zktracer.testing.ToyWorld; +import net.consensys.linea.zktracer.types.EWord; +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.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(EvmExtension.class) +@Disabled("Disabled since oob update is in progress") +public class OobCallTest { + + @Test + void TestCallSendValueGreaterThanBalanceHiNonZero() { + EWord balanceOfCaller = EWord.of(BigInteger.ONE); + EWord amountToSend = EWord.of(BigInteger.ONE, BigInteger.ZERO); + + testCallSendValue(balanceOfCaller, amountToSend, 1); + } + + @Test + void TestCallSendValueGreaterThanBalanceLoNonZero() { + EWord balanceOfCaller = EWord.of(BigInteger.ONE); + EWord amountToSend = EWord.of(BigInteger.ZERO, BigInteger.TWO); + + testCallSendValue(balanceOfCaller, amountToSend, 1); + } + + @Test + void TestCallSendValueGreaterThanBalanceHiLoNonZero() { + EWord balanceOfCaller = EWord.of(BigInteger.ONE); + EWord amountToSend = EWord.of(BigInteger.TWO, BigInteger.TWO); + + testCallSendValue(balanceOfCaller, amountToSend, 1); + } + + @Test + void TestCallSendValueSmallerThanBalanceLoNonZero() { + EWord balanceOfCaller = EWord.of(BigInteger.TWO); + EWord amountToSend = EWord.of(BigInteger.ZERO, BigInteger.ONE); + + testCallSendValue(balanceOfCaller, amountToSend); + } + + /* + The two tests below may fail because of java.lang.OutOfMemoryError: Java heap space + unless maxHeapSize is adjusted in tests.gradle + */ + @Test + void testRecursiveCalls1024() { + EWord iterations = EWord.of(BigInteger.valueOf(1024)); + + testRecursiveCalls(iterations); + } + + @Test + void testRecursiveCalls1025() { + EWord iterations = EWord.of(BigInteger.valueOf(1025)); + + testRecursiveCalls(iterations, 1); + } + + @Test + void TestRecursiveCallsWithBytecode() { + BytecodeRunner bytecodeRunner = + BytecodeRunner.of(Bytes.fromHexString("60006000600060006000305af1")); + bytecodeRunner.run(Wei.fromEth(400), 0xfffffffffffL); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().none()); + + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + // Support methods + private void testCallSendValue(EWord balanceOfCaller, EWord amountToSend) { + testCallSendValue(balanceOfCaller, amountToSend, 0); + } + + private void testCallSendValue( + EWord balanceOfCaller, EWord amountToSend, int numberOfOnesInOobEvent1) { + /* NOTE: The contracts in this method are compiled by using + solc *.sol --bin-runtime --evm-version london -o compiledContracts + i.e., we do not include the init code of the contracts in the bytecode + */ + + // User address + KeyPair keyPair = new SECP256K1().generateKeyPair(); + Address userAddress = Address.extract(Hash.hash(keyPair.getPublicKey().getEncodedBytes())); + ToyAccount userAccount = + ToyAccount.builder().balance(Wei.fromEth(1)).nonce(1).address(userAddress).build(); + + // Caller + ToyAccount contractCallerAccount = + ToyAccount.builder() + .balance(Wei.of(balanceOfCaller.toBigInteger())) + .nonce(2) + .address(Address.fromHexString("0xd9145CCE52D386f254917e481eB44e9943F39138")) + .code( + Bytes.fromHexString( + "60806040526004361061002d5760003560e01c806363acac8e14610039578063ff277a621461006257610034565b3661003457005b600080fd5b34801561004557600080fd5b50610060600480360381019061005b91906101b5565b61008b565b005b34801561006e57600080fd5b5061008960048036038101906100849190610240565b61010f565b005b600081111561010c573073ffffffffffffffffffffffffffffffffffffffff166363acac8e6001836100bd91906102af565b6040518263ffffffff1660e01b81526004016100d991906102f2565b600060405180830381600087803b1580156100f357600080fd5b505af1158015610107573d6000803e3d6000fd5b505050505b50565b60008290508073ffffffffffffffffffffffffffffffffffffffff1663671dcbd7836040518263ffffffff1660e01b81526004016000604051808303818588803b15801561015c57600080fd5b505af1158015610170573d6000803e3d6000fd5b5050505050505050565b600080fd5b6000819050919050565b6101928161017f565b811461019d57600080fd5b50565b6000813590506101af81610189565b92915050565b6000602082840312156101cb576101ca61017a565b5b60006101d9848285016101a0565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061020d826101e2565b9050919050565b61021d81610202565b811461022857600080fd5b50565b60008135905061023a81610214565b92915050565b600080604083850312156102575761025661017a565b5b60006102658582860161022b565b9250506020610276858286016101a0565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006102ba8261017f565b91506102c58361017f565b92508282039050818111156102dd576102dc610280565b5b92915050565b6102ec8161017f565b82525050565b600060208201905061030760008301846102e3565b9291505056fea2646970667358221220eeda6cd078a1e0b43b7e0e6267949ef02a8119de7d68431781e3b1ef33a616d464736f6c63430008150033")) + .build(); + + // Callee + ToyAccount contractCalleeAccount = + ToyAccount.builder() + .balance(Wei.fromEth(1)) + .nonce(3) + .address(Address.fromHexString("0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8")) + .code( + Bytes.fromHexString( + "60806040526004361061002d5760003560e01c806363acac8e14610039578063ff277a621461006257610034565b3661003457005b600080fd5b34801561004557600080fd5b50610060600480360381019061005b91906101b5565b61008b565b005b34801561006e57600080fd5b5061008960048036038101906100849190610240565b61010f565b005b600081111561010c573073ffffffffffffffffffffffffffffffffffffffff166363acac8e6001836100bd91906102af565b6040518263ffffffff1660e01b81526004016100d991906102f2565b600060405180830381600087803b1580156100f357600080fd5b505af1158015610107573d6000803e3d6000fd5b505050505b50565b60008290508073ffffffffffffffffffffffffffffffffffffffff1663671dcbd7836040518263ffffffff1660e01b81526004016000604051808303818588803b15801561015c57600080fd5b505af1158015610170573d6000803e3d6000fd5b5050505050505050565b600080fd5b6000819050919050565b6101928161017f565b811461019d57600080fd5b50565b6000813590506101af81610189565b92915050565b6000602082840312156101cb576101ca61017a565b5b60006101d9848285016101a0565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061020d826101e2565b9050919050565b61021d81610202565b811461022857600080fd5b50565b60008135905061023a81610214565b92915050565b600080604083850312156102575761025661017a565b5b60006102658582860161022b565b9250506020610276858286016101a0565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006102ba8261017f565b91506102c58361017f565b92508282039050818111156102dd576102dc610280565b5b92915050565b6102ec8161017f565b82525050565b600060208201905061030760008301846102e3565b9291505056fea2646970667358221220eeda6cd078a1e0b43b7e0e6267949ef02a8119de7d68431781e3b1ef33a616d464736f6c63430008150033")) + .build(); + + Transaction tx = + ToyTransaction.builder() + .sender(userAccount) + .to(contractCallerAccount) + .payload( + Bytes.fromHexString( + "0xff277a62000000000000000000000000d8b934580fce35a11b58c6d73adee468a2833fa8" + + amountToSend.toString().substring(2))) + .transactionType(TransactionType.FRONTIER) + .gasLimit(0xffffffffL) + .value(Wei.ZERO) + .keyPair(keyPair) + .build(); + + ToyWorld toyWorld = + ToyWorld.builder() + .accounts(List.of(userAccount, contractCallerAccount, contractCalleeAccount)) + .build(); + + ToyExecutionEnvironment toyExecutionEnvironment = + ToyExecutionEnvironment.builder() + .toyWorld(toyWorld) + .transaction(tx) + .testValidator(x -> {}) + .build(); + + toyExecutionEnvironment.run(); + + Hub hub = toyExecutionEnvironment.getHub(); + + assertTrue(hub.pch().exceptions().none()); + + assertNumberOfOnesInOobEvent1(toyExecutionEnvironment.getHub().oob(), numberOfOnesInOobEvent1); + } + + private void testRecursiveCalls(EWord iterations) { + testRecursiveCalls(iterations, 0); + } + + private void testRecursiveCalls(EWord iterations, int numberOfOnesInOobEvent1) { + /* NOTE: The contracts in this method are compiled by using + solc *.sol --bin-runtime --evm-version london -o compiledContracts + i.e., we do not include the init code of the contracts in the bytecode + */ + // User address + KeyPair keyPair = new SECP256K1().generateKeyPair(); + Address userAddress = Address.extract(Hash.hash(keyPair.getPublicKey().getEncodedBytes())); + ToyAccount userAccount = + ToyAccount.builder().balance(Wei.fromEth(25)).nonce(1).address(userAddress).build(); + + // Caller + ToyAccount contractCallerAccount = + ToyAccount.builder() + .balance(Wei.fromEth(1)) + .nonce(2) + .address(Address.fromHexString("0xd9145CCE52D386f254917e481eB44e9943F39138")) + .code( + Bytes.fromHexString( + "60806040526004361061002d5760003560e01c806363acac8e14610039578063ff277a621461006257610034565b3661003457005b600080fd5b34801561004557600080fd5b50610060600480360381019061005b91906101b5565b61008b565b005b34801561006e57600080fd5b5061008960048036038101906100849190610240565b61010f565b005b600081111561010c573073ffffffffffffffffffffffffffffffffffffffff166363acac8e6001836100bd91906102af565b6040518263ffffffff1660e01b81526004016100d991906102f2565b600060405180830381600087803b1580156100f357600080fd5b505af1158015610107573d6000803e3d6000fd5b505050505b50565b60008290508073ffffffffffffffffffffffffffffffffffffffff1663671dcbd7836040518263ffffffff1660e01b81526004016000604051808303818588803b15801561015c57600080fd5b505af1158015610170573d6000803e3d6000fd5b5050505050505050565b600080fd5b6000819050919050565b6101928161017f565b811461019d57600080fd5b50565b6000813590506101af81610189565b92915050565b6000602082840312156101cb576101ca61017a565b5b60006101d9848285016101a0565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061020d826101e2565b9050919050565b61021d81610202565b811461022857600080fd5b50565b60008135905061023a81610214565b92915050565b600080604083850312156102575761025661017a565b5b60006102658582860161022b565b9250506020610276858286016101a0565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006102ba8261017f565b91506102c58361017f565b92508282039050818111156102dd576102dc610280565b5b92915050565b6102ec8161017f565b82525050565b600060208201905061030760008301846102e3565b9291505056fea2646970667358221220eeda6cd078a1e0b43b7e0e6267949ef02a8119de7d68431781e3b1ef33a616d464736f6c63430008150033")) + .build(); + + Transaction tx = + ToyTransaction.builder() + .sender(userAccount) + .to(contractCallerAccount) + .payload(Bytes.fromHexString("0x63acac8e" + iterations.toString().substring(2))) + .transactionType(TransactionType.FRONTIER) + .gasLimit(0xffffffffffL) + .value(Wei.ZERO) + .keyPair(keyPair) + .build(); + + ToyWorld toyWorld = + ToyWorld.builder().accounts(List.of(userAccount, contractCallerAccount)).build(); + + ToyExecutionEnvironment toyExecutionEnvironment = + ToyExecutionEnvironment.builder() + .toyWorld(toyWorld) + .transaction(tx) + .testValidator(x -> {}) + .build(); + + toyExecutionEnvironment.run(); + + Hub hub = toyExecutionEnvironment.getHub(); + + assertTrue(hub.pch().exceptions().none()); + + assertNumberOfOnesInOobEvent1(toyExecutionEnvironment.getHub().oob(), numberOfOnesInOobEvent1); + } +} diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobJumpAndJumpiTest.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobJumpAndJumpiTest.java new file mode 100644 index 0000000000..9607c5f816 --- /dev/null +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobJumpAndJumpiTest.java @@ -0,0 +1,694 @@ +/* + * 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.module.oob; + +import static net.consensys.linea.zktracer.module.oob.OobTestCommon.assertOobEvents; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import net.consensys.linea.zktracer.module.hub.Hub; +import net.consensys.linea.zktracer.opcode.OpCode; +import net.consensys.linea.zktracer.testing.BytecodeCompiler; +import net.consensys.linea.zktracer.testing.BytecodeRunner; +import net.consensys.linea.zktracer.testing.EvmExtension; +import net.consensys.linea.zktracer.types.EWord; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(EvmExtension.class) +@Disabled("Disabled since oob update is in progress") +public class OobJumpAndJumpiTest { + + public static final BigInteger TWO_POW_128_MINUS_ONE = + BigInteger.ONE.shiftLeft(128).subtract(BigInteger.ONE); + + @Test + void TestJumpSequenceSuccessTrivial() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + appendJump(EWord.of(35), program); + appendStop(program); + + appendJumpDest(program); // PC = 35 + appendJump(EWord.of(71), program); + appendStop(program); + + appendJumpDest(program); // PC = 71 + appendJump(EWord.of(107), program); + appendStop(program); + + appendJumpDest(program); // PC = 107 + appendStop(program); + + // codesize = 109 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false, false, false}; + boolean[] oobEvent2 = new boolean[] {false, false, false}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpSequenceSuccessBackAndForth() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + appendJump(EWord.of(71), program); + appendStop(program); + + appendJumpDest(program); // PC = 35 + appendJump(EWord.of(107), program); + appendStop(program); + + appendJumpDest(program); // PC = 71 + appendJump(EWord.of(35), program); + appendStop(program); + + appendJumpDest(program); // PC = 107 + appendStop(program); + + // codesize = 109 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false, false, false}; + boolean[] oobEvent2 = new boolean[] {false, false, false}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpSequenceFailingNoJumpdestTrivial() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + appendJump(EWord.of(35), program); + appendStop(program); + + appendJumpDest(program); // PC = 35 + appendJump(EWord.of(71), program); + appendStop(program); + + appendJumpDest(program); // PC = 71 + appendJump(EWord.of(106), program); // It fails because 106 is not a JUMPDEST + appendStop(program); + + appendJumpDest(program); // PC = 107 + + // codesize = 108 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false, false, false}; + boolean[] oobEvent2 = new boolean[] {false, false, false}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpSequenceFailingOobTrivial() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + appendJump(EWord.of(35), program); + appendStop(program); + + appendJumpDest(program); // PC = 35 + appendJump(EWord.of(71), program); + appendStop(program); + + appendJumpDest(program); // PC = 71 + appendJump(EWord.of(108), program); // It fails because pc_new = 108 >= codesize = 108 + appendStop(program); + + appendJumpDest(program); // PC = 107 + + // codesize = 108 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false, false, true}; + boolean[] oobEvent2 = new boolean[] {false, false, false}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpSequenceSuccessRandom() { + final int N_JUMPS = 200; + final int MAX_JUMPDESTINATION = 256; + final int SPREADING_FACTOR = 256; + + // Generate N_JUMPS random jump destinations + List jumpDestinations = + generateJumpDestinations(N_JUMPS, MAX_JUMPDESTINATION, SPREADING_FACTOR); + + // Compute the PC of each jump destination + Map jumpDestinationsToPC = computeJumpDestinationsToPCForJUMP(jumpDestinations); + + // Init a byteCode with all STOPs (0x00) + int byteCodeNumberOfElements = jumpDestinations.get(jumpDestinations.size() - 1) + 1; + List byteCode = initByteCode(byteCodeNumberOfElements); + + // First jump + byteCode.set(0, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set(1, jumpDestinationsToPC.get(jumpDestinations.get(0))); + byteCode.set(2, Bytes.of(OpCode.JUMP.byteValue())); + + // Jumps in the middle + for (int i = 0; i < jumpDestinations.size() - 1; i++) { + int jumpSource = jumpDestinations.get(i); + byteCode.set(jumpSource, Bytes.of(OpCode.JUMPDEST.byteValue())); + byteCode.set(jumpSource + 1, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set( + jumpSource + 2, + jumpDestinationsToPC.get( + jumpDestinations.get(i + 1))); // Jump to the next jump destination + byteCode.set(jumpSource + 3, Bytes.of(OpCode.JUMP.byteValue())); + } + + // Last jump destination + byteCode.set( + jumpDestinations.get(jumpDestinations.size() - 1), Bytes.of(OpCode.JUMPDEST.byteValue())); + + // Run the generated bytecode + BytecodeRunner bytecodeRunner = BytecodeRunner.of(Bytes.concatenate(byteCode)); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[N_JUMPS]; + boolean[] oobEvent2 = new boolean[N_JUMPS]; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpSequenceSuccessRandomBackAndForth() { + final int N_JUMPS = 200; + final int MAX_JUMPDESTINATION = 256; + final int SPREADING_FACTOR = 256; + + // Generate N_JUMPS random jump destinations + List jumpDestinations = + generateJumpDestinations(N_JUMPS, MAX_JUMPDESTINATION, SPREADING_FACTOR); + + // Compute the PC of each jump destination + Map jumpDestinationsToPC = computeJumpDestinationsToPCForJUMP(jumpDestinations); + + // Init a byteCode with all STOPs (0x00) + int byteCodeNumberOfElements = jumpDestinations.get(jumpDestinations.size() - 1) + 1; + List byteCode = initByteCode(byteCodeNumberOfElements); + + // Define a permutation of the order of the jump destinations + List permutation = generatePermutation(jumpDestinations.size()); + + // First jump + byteCode.set(0, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set(1, jumpDestinationsToPC.get(jumpDestinations.get(permutation.get(0)))); + byteCode.set(2, Bytes.of(OpCode.JUMP.byteValue())); + + // Jumps in the middle + for (int i = 0; i < jumpDestinations.size() - 1; i++) { + int jumpSource = jumpDestinations.get(permutation.get(i)); + byteCode.set(jumpSource, Bytes.of(OpCode.JUMPDEST.byteValue())); + byteCode.set(jumpSource + 1, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set( + jumpSource + 2, + jumpDestinationsToPC.get( + jumpDestinations.get( + permutation.get(i + 1)))); // Jump to the next jump destination wrt permutation + byteCode.set(jumpSource + 3, Bytes.of(OpCode.JUMP.byteValue())); + } + + // Last jump destination + byteCode.set( + jumpDestinations.get(jumpDestinations.size() - 1), Bytes.of(OpCode.JUMPDEST.byteValue())); + + // Run the generated bytecode + BytecodeRunner bytecodeRunner = BytecodeRunner.of(Bytes.concatenate(byteCode)); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[N_JUMPS]; + boolean[] oobEvent2 = new boolean[N_JUMPS]; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpiSequenceSuccessTrivial() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + appendJumpi(EWord.of(68), EWord.of(1), program); + appendStop(program); + + appendJumpDest(program); // PC = 68 + appendJumpi(EWord.of(137), EWord.of(1), program); + appendStop(program); + + appendJumpDest(program); // PC = 137 + appendJumpi(EWord.of(206), EWord.of(1), program); + appendStop(program); + + appendJumpDest(program); // PC = 206 + appendStop(program); + + // codesize = 208 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false, false, false}; + boolean[] oobEvent2 = new boolean[] {true, true, true}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpiSequenceSuccessBackAndForth() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + appendJumpi(EWord.of(137), EWord.of(1), program); + appendStop(program); + + appendJumpDest(program); // PC = 68 + appendJumpi(EWord.of(206), EWord.of(1), program); + appendStop(program); + + appendJumpDest(program); // PC = 137 + appendJumpi(EWord.of(68), EWord.of(1), program); + appendStop(program); + + appendJumpDest(program); // PC = 206 + appendStop(program); + + // codesize = 208 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false, false, false}; + boolean[] oobEvent2 = new boolean[] {true, true, true}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpiSequenceFailingNoJumpdestTrivial() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + appendJumpi(EWord.of(68), EWord.of(1), program); + appendStop(program); + + appendJumpDest(program); // PC = 68 + appendJumpi(EWord.of(137), EWord.of(1), program); + appendStop(program); + + appendJumpDest(program); // PC = 137 + appendJumpi(EWord.of(205), EWord.of(1), program); // It fails because 205 is not a JUMPDEST + appendStop(program); + + appendJumpDest(program); // PC = 206 + + // codesize = 207 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false, false, false}; + boolean[] oobEvent2 = new boolean[] {true, true, true}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpiSequenceFailingOobTrivial() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + appendJumpi(EWord.of(68), EWord.of(1), program); + appendStop(program); + + appendJumpDest(program); // PC = 68 + appendJumpi(EWord.of(137), EWord.of(1), program); + appendStop(program); + + appendJumpDest(program); // PC = 137 + appendJumpi( + EWord.of(207), EWord.of(1), program); // It fails because pc_new = 207 >= codesize = 207 + appendStop(program); + + appendJumpDest(program); // PC = 206 + + // codesize = 207 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false, false, true}; + boolean[] oobEvent2 = new boolean[] {true, true, true}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestNoJumpi() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + appendJumpi(EWord.of(68), EWord.of(0), program); // jumpCondition is 0, that means no JUMPI + appendStop(program); + + appendJumpDest(program); // PC = 68 + + // codesize = 69 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false}; + boolean[] oobEvent2 = new boolean[] {false}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpiHiNonZero() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + EWord jumpCondition = EWord.of(TWO_POW_128_MINUS_ONE, BigInteger.ZERO); + appendJumpi(EWord.of(68), jumpCondition, program); + appendStop(program); + + appendJumpDest(program); // PC = 68 + + // codesize = 69 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false}; + boolean[] oobEvent2 = new boolean[] {true}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpiLoNonZero() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + EWord jumpCondition = EWord.of(BigInteger.valueOf(0), TWO_POW_128_MINUS_ONE); + appendJumpi(EWord.of(68), jumpCondition, program); + appendStop(program); + + appendJumpDest(program); // PC = 68 + + // codesize = 69 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false}; + boolean[] oobEvent2 = new boolean[] {true}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpiHiLoNonZero() { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + EWord jumpCondition = EWord.of(TWO_POW_128_MINUS_ONE, TWO_POW_128_MINUS_ONE); + appendJumpi(EWord.of(68), jumpCondition, program); + appendStop(program); + + appendJumpDest(program); // PC = 68 + + // codesize = 69 + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[] {false}; + boolean[] oobEvent2 = new boolean[] {true}; + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpiSequenceSuccessRandom() { + final int N_JUMPIS = 200; + final int MAX_JUMPDESTINATION = 256; + final int SPREADING_FACTOR = 256; + + // Generate N_JUMPIS random jump destinations + List jumpDestinations = + generateJumpDestinations(N_JUMPIS, MAX_JUMPDESTINATION, SPREADING_FACTOR); + + // Compute the PC of each jump destination + Map jumpDestinationsToPC = + computeJumpDestinationsToPCForJUMPI(jumpDestinations); + + // Init a byteCode with all STOPs (0x00) + int byteCodeNumberOfElements = jumpDestinations.get(jumpDestinations.size() - 1) + 1; + List byteCode = initByteCode(byteCodeNumberOfElements); + + // First jumpi + byteCode.set(0, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set(1, EWord.of(1)); + byteCode.set(2, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set(3, jumpDestinationsToPC.get(jumpDestinations.get(0))); + byteCode.set(4, Bytes.of(OpCode.JUMPI.byteValue())); + + // Jumpis in the middle + for (int i = 0; i < jumpDestinations.size() - 1; i++) { + int jumpiSource = jumpDestinations.get(i); + byteCode.set(jumpiSource, Bytes.of(OpCode.JUMPDEST.byteValue())); + byteCode.set(jumpiSource + 1, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set(jumpiSource + 2, EWord.of(1)); + byteCode.set(jumpiSource + 3, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set( + jumpiSource + 4, + jumpDestinationsToPC.get( + jumpDestinations.get(i + 1))); // Jumpi to the next jump destination + byteCode.set(jumpiSource + 5, Bytes.of(OpCode.JUMPI.byteValue())); + } + + // Last jump destination + byteCode.set( + jumpDestinations.get(jumpDestinations.size() - 1), Bytes.of(OpCode.JUMPDEST.byteValue())); + + // Run the generated bytecode + BytecodeRunner bytecodeRunner = BytecodeRunner.of(Bytes.concatenate(byteCode)); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[N_JUMPIS]; + boolean[] oobEvent2 = new boolean[N_JUMPIS]; + Arrays.fill(oobEvent2, true); + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + @Test + void TestJumpiSequenceSuccessRandomBackAndForth() { + final int N_JUMPIS = 200; + final int MAX_JUMPDESTINATION = 256; + final int SPREADING_FACTOR = 256; + + // Generate N_JUMPIS random jump destinations + List jumpDestinations = + generateJumpDestinations(N_JUMPIS, MAX_JUMPDESTINATION, SPREADING_FACTOR); + + // Compute the PC of each jump destination + Map jumpDestinationsToPC = + computeJumpDestinationsToPCForJUMPI(jumpDestinations); + + // Init a byteCode with all STOPs (0x00) + int byteCodeNumberOfElements = jumpDestinations.get(jumpDestinations.size() - 1) + 1; + List byteCode = initByteCode(byteCodeNumberOfElements); + + // Define a permutation of the order of the jump destinations + List permutation = generatePermutation(jumpDestinations.size()); + + // First jumpi + byteCode.set(0, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set(1, EWord.of(1)); + byteCode.set(2, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set(3, jumpDestinationsToPC.get(jumpDestinations.get(permutation.get(0)))); + byteCode.set(4, Bytes.of(OpCode.JUMPI.byteValue())); + + // Jumpis in the middle + for (int i = 0; i < jumpDestinations.size() - 1; i++) { + int jumpiSource = jumpDestinations.get(permutation.get(i)); + byteCode.set(jumpiSource, Bytes.of(OpCode.JUMPDEST.byteValue())); + byteCode.set(jumpiSource + 1, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set(jumpiSource + 2, EWord.of(1)); + byteCode.set(jumpiSource + 3, Bytes.of(OpCode.PUSH32.byteValue())); + byteCode.set( + jumpiSource + 4, + jumpDestinationsToPC.get( + jumpDestinations.get( + permutation.get(i + 1)))); // Jump to the next jump destination wrt permutation + byteCode.set(jumpiSource + 5, Bytes.of(OpCode.JUMPI.byteValue())); + } + + // Last jump destination + byteCode.set( + jumpDestinations.get(jumpDestinations.size() - 1), Bytes.of(OpCode.JUMPDEST.byteValue())); + + // Run the generated bytecode + BytecodeRunner bytecodeRunner = BytecodeRunner.of(Bytes.concatenate(byteCode)); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + assertFalse(hub.pch().exceptions().jumpFault()); + + boolean[] oobEvent1 = new boolean[N_JUMPIS]; + boolean[] oobEvent2 = new boolean[N_JUMPIS]; + Arrays.fill(oobEvent2, true); + assertOobEvents(bytecodeRunner.getHub().oob(), oobEvent1, oobEvent2); + } + + // Support methods + private List generateJumpDestinations( + int N_JUMPS, int MAX_JUMPDESTINATION, int SPREADING_FACTOR) { + return ThreadLocalRandom.current() + .ints(1, MAX_JUMPDESTINATION) + .distinct() + .limit(N_JUMPS) + .sorted() + .map(x -> x * SPREADING_FACTOR) + .boxed() + .toList(); + } + + private List generatePermutation(int jumpDestinationsSize) { + List permutation = + ThreadLocalRandom.current() + .ints(0, jumpDestinationsSize - 1) + .distinct() + .limit(jumpDestinationsSize - 1) + .boxed() + .collect(Collectors.toList()); + permutation.add( + jumpDestinationsSize - 1); // The last jump has to be always to the last instruction + return permutation; + } + + private Map computeJumpDestinationsToPCForJUMP(List jumpDestinations) { + Map jumpDestinationsToPC = new HashMap<>(); + for (int i = 0; i < jumpDestinations.size(); i++) { + jumpDestinationsToPC.put( + jumpDestinations.get(i), EWord.of(jumpDestinations.get(i) + (i + 1) * 31L)); + } + return jumpDestinationsToPC; + } + + private Map computeJumpDestinationsToPCForJUMPI(List jumpDestinations) { + Map jumpDestinationsToPC = new HashMap<>(); + for (int i = 0; i < jumpDestinations.size(); i++) { + jumpDestinationsToPC.put( + jumpDestinations.get(i), EWord.of(jumpDestinations.get(i) + (i + 1) * 62L)); + } + return jumpDestinationsToPC; + } + + private List initByteCode(int byteCodeNumberOfElements) { + return IntStream.range(0, byteCodeNumberOfElements) + .mapToObj(i -> Bytes.of(OpCode.STOP.byteValue())) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private void appendJump(EWord pcNew, BytecodeCompiler program) { + program.push(pcNew); + program.op(OpCode.JUMP); + System.out.println("Added JUMP at PC: " + program.compile().bitLength() / 8); + // 32 + 1 + 1 bytes + } + + private void appendJumpi(EWord pcNew, EWord jumpCondition, BytecodeCompiler program) { + program.push(jumpCondition); + program.push(pcNew); + program.op(OpCode.JUMPI); + System.out.println("Added JUMPI at PC: " + program.compile().bitLength() / 8); + // 32 + 1 + 32 + 1 + 1 bytes + } + + private void appendJumpDest(BytecodeCompiler program) { + program.op(OpCode.JUMPDEST); + System.out.println("Added JUMPDEST at PC: " + program.compile().bitLength() / 8); + // 1 byte + } + + private void appendStop(BytecodeCompiler program) { + program.op(OpCode.STOP); + System.out.println("Added STOP at PC: " + program.compile().bitLength() / 8); + // 1 byte + } +} diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobRdcTest.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobRdcTest.java new file mode 100644 index 0000000000..b87b61696c --- /dev/null +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobRdcTest.java @@ -0,0 +1,843 @@ +/* + * 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.module.oob; + +import static net.consensys.linea.zktracer.module.oob.OobTestCommon.assertNumberOfOnesInOobEvent1; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigInteger; + +import net.consensys.linea.zktracer.module.hub.Hub; +import net.consensys.linea.zktracer.opcode.OpCode; +import net.consensys.linea.zktracer.testing.BytecodeCompiler; +import net.consensys.linea.zktracer.testing.BytecodeRunner; +import net.consensys.linea.zktracer.testing.EvmExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(EvmExtension.class) +@Disabled("Disabled since oob update is in progress") +public class OobRdcTest { + + public static final BigInteger TWO_POW_128_LEFT = + BigInteger.ONE.shiftLeft(128).subtract(BigInteger.valueOf(100)); + + public static final BigInteger TWO_POW_128_RIGHT = + BigInteger.ONE.shiftLeft(128).subtract(BigInteger.valueOf(100)); + + @Test + void TestReturnDataCopyMaxPosZero() { + // maxPos = offset + size = 0 + 0 < rds = 32 + BytecodeCompiler program = initReturnDataCopyProgram(BigInteger.ZERO, BigInteger.ZERO); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 0); + } + + @Test + void TestReturnDataCopyMaxPosRds() { + // maxPos = offset + size = 12 + 20 = rds = 32 + BytecodeCompiler program = + initReturnDataCopyProgram(BigInteger.valueOf(12), BigInteger.valueOf(20)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 0); + } + + @Test + void TestReturnDataCopyMaxPosSmallerThanRds() { + // maxPos = offset + size = 3 + 4 < rds = 32 + BytecodeCompiler program = + initReturnDataCopyProgram(BigInteger.valueOf(3), BigInteger.valueOf(4)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 0); + } + + @Test + void TestReturnDataCopyMaxPosSmallerThanRdsAndOffsetZero() { + // maxPos = offset + size = 0 + 4 < rds = 32 + BytecodeCompiler program = + initReturnDataCopyProgram(BigInteger.valueOf(0), BigInteger.valueOf(4)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 0); + } + + @Test + void TestReturnDataCopyMaxPosSmallerThanRdsAndSizeZero() { + // maxPos = offset + size = 3 + 0 < rds = 32 + BytecodeCompiler program = + initReturnDataCopyProgram(BigInteger.valueOf(3), BigInteger.valueOf(0)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 0); + } + + // Failing cases + + // offset smaller cases + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetSmallerAndSizeSmall() { + // maxPos = offset + size = 10 + 23 > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgram(BigInteger.valueOf(10), BigInteger.valueOf(23)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetSmallerAndSizeBigLeft() { + // maxPos = offset + size = 10 + TWO_POW_128_LEFT > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(BigInteger.valueOf(10), TWO_POW_128_LEFT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetSmallerAndSizeBigRight() { + // maxPos = offset + size = 10 + TWO_POW_128_RIGHT > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(BigInteger.valueOf(10), TWO_POW_128_RIGHT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + // offset just greater cases + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetJustGreaterAndSizeZero() { + // maxPos = offset + size = 33 + 0 > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgram(BigInteger.valueOf(33), BigInteger.valueOf(0)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetJustGreaterAndSizeSmall() { + // maxPos = offset + size = 33 + 23 > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgram(BigInteger.valueOf(33), BigInteger.valueOf(23)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetJustGreaterAndSizeBigLeft() { + // maxPos = offset + size = 33 + TWO_POW_128_LEFT > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(BigInteger.valueOf(33), TWO_POW_128_LEFT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetJustGreaterAndSizeBigRight() { + // maxPos = offset + size = 33 + TWO_POW_128_RIGHT > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(BigInteger.valueOf(33), TWO_POW_128_RIGHT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + // offset big left cases + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetBigLeftAndSizeZero() { + // maxPos = offset + size = TWO_POW_128_LEFT + 0 > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(TWO_POW_128_LEFT, BigInteger.valueOf(0)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetBigLeftAndSizeSmall() { + // maxPos = offset + size = TWO_POW_128_LEFT + 23 > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(TWO_POW_128_LEFT, BigInteger.valueOf(23)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetBigLeftAndSizeBigLeft() { + // maxPos = offset + size = TWO_POW_128_LEFT + TWO_POW_128_LEFT > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(TWO_POW_128_LEFT, TWO_POW_128_LEFT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetBigLeftAndSizeBigRight() { + // maxPos = offset + size = TWO_POW_128_LEFT + TWO_POW_128_RIGHT > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(TWO_POW_128_LEFT, TWO_POW_128_RIGHT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + // offset big right cases + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetBigRightAndSizeZero() { + // maxPos = offset + size = TWO_POW_128_RIGHT + 0 > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(TWO_POW_128_RIGHT, BigInteger.valueOf(0)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetBigRightAndSizeSmall() { + // maxPos = offset + size = TWO_POW_128_RIGHT + 23 > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(TWO_POW_128_RIGHT, BigInteger.valueOf(23)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetBigRightAndSizeBigLeft() { + // maxPos = offset + size = TWO_POW_128_Right + TWO_POW_128_LEFT > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(TWO_POW_128_RIGHT, TWO_POW_128_LEFT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + @Test + void TestReturnDataCopyMaxPosGreaterThanRdsAndOffsetBigRightAndSizeBigRight() { + // maxPos = offset + size = TWO_POW_128_RIGHT + TWO_POW_128_RIGHT > 32 = rds + BytecodeCompiler program = initReturnDataCopyProgram(TWO_POW_128_RIGHT, TWO_POW_128_RIGHT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + } + + // Same cases but using identity precompile + @Test + void TestReturnDataCopyUsingIdentityPrecompileMaxPosZero() { + // maxPos = offset + size = 0 + 0 < rds = 32 + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(BigInteger.ZERO, BigInteger.ZERO); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 0); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void TestReturnDataCopyUsingIdentityPrecompileMaxPosRds() { + // maxPos = offset + size = 12 + 20 = rds = 32 + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile( + BigInteger.valueOf(12), BigInteger.valueOf(20)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 0); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void TestReturnDataCopyUsingIdentityPrecompileMaxPosSmallerThanRds() { + // maxPos = offset + size = 3 + 4 < rds = 32 + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile( + BigInteger.valueOf(3), BigInteger.valueOf(4)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 0); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void TestReturnDataCopyUsingIdentityPrecompileMaxPosSmallerThanRdsAndOffsetZero() { + // maxPos = offset + size = 0 + 4 < rds = 32 + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile( + BigInteger.valueOf(0), BigInteger.valueOf(4)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 0); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void TestReturnDataCopyUsingIdentityPrecompileMaxPosSmallerThanRdsAndSizeZero() { + // maxPos = offset + size = 3 + 0 < rds = 32 + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile( + BigInteger.valueOf(3), BigInteger.valueOf(0)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertFalse(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 0); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + // Failing cases + + // offset smaller cases + @Test + void TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetSmallerAndSizeSmall() { + // maxPos = offset + size = 10 + 23 > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile( + BigInteger.valueOf(10), BigInteger.valueOf(23)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetSmallerAndSizeBigLeft() { + // maxPos = offset + size = 10 + TWO_POW_128_LEFT > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(BigInteger.valueOf(10), TWO_POW_128_LEFT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetSmallerAndSizeBigRight() { + // maxPos = offset + size = 10 + TWO_POW_128_RIGHT > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(BigInteger.valueOf(10), TWO_POW_128_RIGHT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + // offset just greater cases + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetJustGreaterAndSizeZero() { + // maxPos = offset + size = 33 + 0 > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile( + BigInteger.valueOf(33), BigInteger.valueOf(0)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetJustGreaterAndSizeSmall() { + // maxPos = offset + size = 33 + 23 > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile( + BigInteger.valueOf(33), BigInteger.valueOf(23)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetJustGreaterAndSizeBigLeft() { + // maxPos = offset + size = 33 + TWO_POW_128_LEFT > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(BigInteger.valueOf(33), TWO_POW_128_LEFT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetJustGreaterAndSizeBigRight() { + // maxPos = offset + size = 33 + TWO_POW_128_RIGHT > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(BigInteger.valueOf(33), TWO_POW_128_RIGHT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + // offset big left cases + @Test + void TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetBigLeftAndSizeZero() { + // maxPos = offset + size = TWO_POW_128_LEFT + 0 > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(TWO_POW_128_LEFT, BigInteger.valueOf(0)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetBigLeftAndSizeSmall() { + // maxPos = offset + size = TWO_POW_128_LEFT + 23 > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(TWO_POW_128_LEFT, BigInteger.valueOf(23)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetBigLeftAndSizeBigLeft() { + // maxPos = offset + size = TWO_POW_128_LEFT + TWO_POW_128_LEFT > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(TWO_POW_128_LEFT, TWO_POW_128_LEFT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetBigLeftAndSizeBigRight() { + // maxPos = offset + size = TWO_POW_128_LEFT + TWO_POW_128_RIGHT > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(TWO_POW_128_LEFT, TWO_POW_128_RIGHT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + // offset big right cases + @Test + void TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetBigRightAndSizeZero() { + // maxPos = offset + size = TWO_POW_128_RIGHT + 0 > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(TWO_POW_128_RIGHT, BigInteger.valueOf(0)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetBigRightAndSizeSmall() { + // maxPos = offset + size = TWO_POW_128_RIGHT + 23 > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(TWO_POW_128_RIGHT, BigInteger.valueOf(23)); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetBigRightAndSizeBigLeft() { + // maxPos = offset + size = TWO_POW_128_Right + TWO_POW_128_LEFT > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(TWO_POW_128_RIGHT, TWO_POW_128_LEFT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + @Test + void + TestReturnDataCopyUsingIdentityPrecompileMaxPosGreaterThanRdsAndOffsetBigRightAndSizeBigRight() { + // maxPos = offset + size = TWO_POW_128_RIGHT + TWO_POW_128_RIGHT > 32 = rds + BytecodeCompiler program = + initReturnDataCopyProgramUsingIdentityPrecompile(TWO_POW_128_RIGHT, TWO_POW_128_RIGHT); + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + Hub hub = bytecodeRunner.getHub(); + + assertTrue(hub.pch().exceptions().returnDataCopyFault()); + assertNumberOfOnesInOobEvent1(bytecodeRunner.getHub().oob(), 1); + // Chunk with index 1 is the one corresponding to IDENTITY precompile + // precompileCost = (5 + ceil) * 3 where ceil = 1 + // cds is trivially 0 in initReturnDataCopyProgramUsingIdentityPrecompile + assertEquals( + bytecodeRunner.getHub().oob().getChunks().get(1).getPrecompileCost(), + BigInteger.valueOf(18)); + } + + // Support methods + BytecodeCompiler initReturnDataCopyProgram(BigInteger offset, BigInteger size) { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + // Creates a constructor that creates a contract which returns 32 FF + program + .push("7F7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + .push(0) + .op(OpCode.MSTORE) + .push("FF6000527FFF60005260206000F3000000000000000000000000000000000000") + .push(32) + .op(OpCode.MSTORE) + .push("000000000060205260296000F300000000000000000000000000000000000000") + .push(64) + .op(OpCode.MSTORE); + + // Create the contract with the constructor code above + program + .push(77) + .push(0) + .push(0) + .op(OpCode.CREATE); // Puts the new contract address on the stack + + // Call the deployed contract + program.push(0).push(0).push(0).push(0).op(OpCode.DUP5).push("FFFFFFFF").op(OpCode.STATICCALL); + + // Clear the stack + program.op(OpCode.POP).op(OpCode.POP); + + // Clear the memory + program + .push(0) + .push(0) + .op(OpCode.MSTORE) + .push(0) + .push(32) + .op(OpCode.MSTORE) + .push(0) + .push(64) + .op(OpCode.MSTORE); + + // Invoke RETURNDATACOPY + program.push(size).push(offset).push(0).op(OpCode.RETURNDATACOPY); + + return program; + } + + BytecodeCompiler initReturnDataCopyProgramUsingIdentityPrecompile( + BigInteger offset, BigInteger size) { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + // First place the parameters in memory + program + .push("AAAAAAAAAA9999999999BBBBBBBBBB8888888888CCCCCCCCCC7777777777DDDD") + . // data + push(0) + .op(OpCode.MSTORE); + + // Do the call + program + .push(0) + . // retSize + push(0) + . // retOffset + push(32) + . // argSize + push(0) + . // argOffset + push(4) + . // address + push("FFFFFFFF") + . // gas + op(OpCode.STATICCALL); + + // Clear the stack + program.op(OpCode.POP); + + // Clear the memory + program.push(0).push(0).op(OpCode.MSTORE); + + // Invoke RETURNDATACOPY + program.push(size).push(offset).push(0).op(OpCode.RETURNDATACOPY); + + return program; + } + + /* + @Test + void temp() { + BigInteger n1 = BigInteger.valueOf(0xFF); + BigInteger n2 = UnsignedByte.of(0xFF).toBigInteger(); + BigInteger n3 = Bytes.fromHexString("FF").toBigInteger(); + BigInteger n4 = Bytes.fromHexString("FF").toUnsignedBigInteger(); + BigInteger n5 = BigInteger.valueOf((byte) 0xFF); + System.out.println(n1); + System.out.println(n2); + System.out.println(n3); + System.out.println(n4); + System.out.println(n5); + } + */ +} diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobSha2RipemdIdentityTest.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobSha2RipemdIdentityTest.java new file mode 100644 index 0000000000..4273157700 --- /dev/null +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobSha2RipemdIdentityTest.java @@ -0,0 +1,300 @@ +/* + * 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.module.oob; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.google.common.io.BaseEncoding; +import net.consensys.linea.zktracer.opcode.OpCode; +import net.consensys.linea.zktracer.testing.BytecodeCompiler; +import net.consensys.linea.zktracer.testing.BytecodeRunner; +import net.consensys.linea.zktracer.testing.EvmExtension; +import org.bouncycastle.crypto.digests.RIPEMD160Digest; +import org.bouncycastle.util.encoders.Hex; +import org.hyperledger.besu.datatypes.Address; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(EvmExtension.class) +public class OobSha2RipemdIdentityTest { + Random random = new Random(1L); + static final int[] argSizes = + new int[] {1, 10, 20, 31, 32, 33, 63, 64, 65, 95, 96, 97, 127, 128, 129, 1000, 2000}; + + // Add back 0 + + // https://coderpad.io/blog/development/writing-a-parameterized-test-in-junit-with-examples/ + // https://stackoverflow.com/questions/76124016/pass-externally-defined-variable-to-junit-valuesource-annotation-in-a-paramete + static int[] argSizesSource() { + return argSizes; + } + + @ParameterizedTest + @MethodSource("argSizesSource") + void TestSha2(int argSize) throws NoSuchAlgorithmException { + String data = generateHexString(argSize); + ProgramAndRetInfo programAndRetInfo = initProgramInvokingPrecompile(data, Address.SHA256); + BytecodeCompiler program = programAndRetInfo.program(); + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + String referenceComputedHash = sha256(data); + String prcComputedHash = + bytecodeRunner + .getHub() + .currentFrame() + .frame() + .shadowReadMemory(programAndRetInfo.retOffset, programAndRetInfo.retSize) + .toString(); + System.out.println("Test SHA2-256 with random argSize = " + argSize); + System.out.println("Inp: 0x" + data); + System.out.println("Ref: " + referenceComputedHash); + System.out.println("Com: " + prcComputedHash); + // assertEquals(referenceComputedHash, prcComputedHash); + } + + @ParameterizedTest + @MethodSource("argSizesSource") + void TestIdentity(int argSize) { + String data = generateHexString(argSize); + ProgramAndRetInfo programAndRetInfo = initProgramInvokingPrecompile(data, Address.ID); + BytecodeCompiler program = programAndRetInfo.program(); + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + String returnedData = + bytecodeRunner + .getHub() + .currentFrame() + .frame() + .shadowReadMemory(programAndRetInfo.retOffset, programAndRetInfo.retSize) + .toString(); + System.out.println("Test IDENTITY with random argSize = " + argSize); + System.out.println("Inp: 0x" + data); + System.out.println("Ret: " + returnedData); + // assertEquals("0x" + data.toLowerCase(), returnedData); + } + + @ParameterizedTest + @MethodSource("argSizesSource") + void TestRipmd(int argSize) { + String data = generateHexString(argSize); + ProgramAndRetInfo programAndRetInfo = initProgramInvokingPrecompile(data, Address.RIPEMD160); + BytecodeCompiler program = programAndRetInfo.program(); + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + String referenceComputedHash = ripemd160(data); + String prcComputedHash = + bytecodeRunner + .getHub() + .currentFrame() + .frame() + .shadowReadMemory(programAndRetInfo.retOffset, programAndRetInfo.retSize) + .toString(); + System.out.println("Test RIPEMD-160 with random argSize = " + argSize); + System.out.println("Inp: 0x" + data); + System.out.println("Ref: " + referenceComputedHash); + System.out.println("Com: " + prcComputedHash); + // assertEquals(referenceComputedHash, prcComputedHash); + } + + // Support methods + private String generateHexString(int size) { + return IntStream.range(0, size) + .mapToObj(i -> String.format("%02x", random.nextInt(256))) + .collect(Collectors.joining()); + } + + private String sha256(String hexString) throws NoSuchAlgorithmException { + byte[] byteInput = BaseEncoding.base16().decode(hexString.toUpperCase()); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(byteInput); + return "0x" + BaseEncoding.base16().encode(hash).toLowerCase(); + } + + private static String ripemd160(String hexString) { + byte[] byteInput = BaseEncoding.base16().decode(hexString.toUpperCase()); + RIPEMD160Digest digest = new RIPEMD160Digest(); + digest.update(byteInput, 0, byteInput.length); + byte[] hash = new byte[digest.getDigestSize()]; + digest.doFinal(hash, 0); + return "0x000000000000000000000000" + Hex.toHexString(hash); + } + + @Test + void TestPrcSupportMethods() throws NoSuchAlgorithmException { + String data = generateHexString(32); + System.out.println("SHA2-256 of random data: " + sha256(data)); + System.out.println("RIPEMD-160 of random data: " + ripemd160(data)); + assert (sha256("") + .equals("0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")); + assert (sha256("00") + .equals("0x6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d")); + assert (sha256("0000") + .equals("0x96a296d224f285c67bee93c30f8a309157f0daa35dc5b87e410b78630a09cfc7")); + assert (sha256("ff") + .equals("0xa8100ae6aa1940d0b663bb31cd466142ebbdbd5187131b92d93818987832eb89")); + assert (sha256( + "aaaaaaaaaa9999999999bbbbbbbbbb8888888888cccccccccc7777777777ddddaaaaaaaaaa9999999999bbbbbbbbbb8888888888cccccccccc7777777777dddd") + .equals("0xfa3695ebdadeb06f552f983ff12deea809391ca80d10f0dd27fea25ae8a6daa7")); + assert (ripemd160("") + .equals("0x0000000000000000000000009c1185a5c5e9fc54612808977ee8f548b2258d31")); + assert (ripemd160("00") + .equals("0x000000000000000000000000c81b94933420221a7ac004a90242d8b1d3e5070d")); + assert (ripemd160("0000") + .equals("0x000000000000000000000000f7d50d120d655be4b88750873e00caf147f28a1b")); + assert (ripemd160("ff") + .equals("0x0000000000000000000000002c0c45d3ecab80fe060e5f1d7057cd2f8de5e557")); + assert (ripemd160( + "aaaaaaaaaa9999999999bbbbbbbbbb8888888888cccccccccc7777777777ddddaaaaaaaaaa9999999999bbbbbbbbbb8888888888cccccccccc7777777777dddd") + .equals("0x0000000000000000000000009c08e833ee0d5d3e42f332e2d22563b68617bfba")); + } + + private String padToEWord(String input, boolean toTheLeft) { + if (toTheLeft) { + return "00".repeat(32 - input.length() / 2) + input; + } else { + return input + "00".repeat(32 - input.length() / 2); + } + } + + private String subHexString(String input, int from, int to) { + return input.substring(from * 2, to * 2); + } + + private String subHexString(String input, int from) { + return input.substring(from * 2); + } + + private record ProgramAndRetInfo( + BytecodeCompiler program, int argSize, int argOffset, int retSize, int retOffset) {} + + ProgramAndRetInfo initProgramInvokingPrecompile(String data, Address address) { + int argSize = data.length() / 2; + int argOffset = 0; + + int retSize = address == Address.ID ? argSize : 32; + int retOffset = 0; + + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + // MSTORE data if argSize > 0 + if (argSize > 0) { + // Note that argSize <= 32 is treated in a slightly different way than argSize > 32 to avoid + // splitting the input and padding + if (argSize <= 32) { + // The random offset is applied before the EWord + // Generate a small random offset + int randomOffset = random.nextInt(1, 5); + argOffset = randomOffset + 32 - argSize; + retOffset = address == Address.ID ? randomOffset + 64 - argSize : randomOffset + 32; + + program + .push(data) + . // data + push(randomOffset) + .op(OpCode.MSTORE); + } else { // argSize > 32 + // The random offset is applied between the beginning of the first EWord and the input + // Generate a small random offset + argOffset = random.nextInt(1, 5); + retOffset = argOffset + argSize; + + // MSTORE first EWord + String firstEWord = padToEWord(subHexString(data, 0, 32 - argOffset), true); + program + .push(firstEWord) + . // data + push( + 0) // The argOffset is already taken into consideration by padding zeros to the left + .op(OpCode.MSTORE); + + // MSTORE EWord in the middle + int i = 1; + while (32 * i - argOffset + 32 < argSize) { + String middleEWord = subHexString(data, 32 * i - argOffset, 32 * i - argOffset + 32); + program + .push(middleEWord) + . // data + push(32 * i) + .op(OpCode.MSTORE); + i++; + } + + // MSTORE last EWord + String lastEWord = padToEWord(subHexString(data, 32 * i - argOffset), false); + program + .push(lastEWord) + . // data + push(32 * i) + .op(OpCode.MSTORE); + } + } + + program + .push(retSize) + . // retSize + push(retOffset) + . // retOffset + push(argSize) + . // argSize + push(argOffset) + . // argOffset + push(address) + . // address + push("FFFFFFFF") + . // gas + op(OpCode.STATICCALL); + + return new ProgramAndRetInfo(program, argSize, argOffset, retSize, retOffset); + } + + @ParameterizedTest + @MethodSource("argSizesSource") + void TestInitProgramInvokingPrecompileDataInMemorySupportMethod(int argSize) { + // This test is to ensure that the data written in memory is the same as the input data + String data = generateHexString(argSize); + + ProgramAndRetInfo programAndRetInfo = initProgramInvokingPrecompile(data, Address.ZERO); + BytecodeCompiler program = programAndRetInfo.program(); + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + bytecodeRunner.run(); + + String dataInMemory = + bytecodeRunner + .getHub() + .currentFrame() + .frame() + .shadowReadMemory(programAndRetInfo.argOffset, programAndRetInfo.argSize) + .toString(); + System.out.println("0x" + data); + System.out.println(dataInMemory); + assertEquals("0x" + data.toLowerCase(), dataInMemory); + } +} diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobTestCommon.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobTestCommon.java new file mode 100644 index 0000000000..154b699c38 --- /dev/null +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/oob/OobTestCommon.java @@ -0,0 +1,55 @@ +/* + * 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.module.oob; + +class OobTestCommon { + // TODO: all these methods are deprecated since oobEvent has been removed, update them once oob is + // done + // Support methods to assert that the oob events are set correctly + static boolean getOobEvent1AtRow(final Oob oob, int i) { + // return oob.getChunks().get(i).isOobEvent1(); + return false; + } + + static boolean getOobEvent2AtRow(final Oob oob, int i) { + // return oob.getChunks().get(i).isOobEvent2(); + return false; + } + + /* Note that the methods below refer to the values of oobEvent1 and oobEvent2 in the chunks, + * note the trace. One chunk may correspond to more than one row in the trace. + */ + static void assertOobEvents(final Oob oob, boolean[] oobEvent1, boolean[] oobEvent2) { + if (oobEvent1.length != oobEvent2.length) { + throw new IllegalArgumentException("oobEvent1 and oobEvent2 do not have the same length"); + } + assert (oobEvent1.length == oob.getChunks().size()); + for (int i = 0; i < oobEvent1.length; i++) { + assert (getOobEvent1AtRow(oob, i) == oobEvent1[i]); + assert (getOobEvent2AtRow(oob, i) == oobEvent2[i]); + } + } + + static void assertNumberOfOnesInOobEvent1(final Oob oob, int numberOfOnesInOobEvent1) { + /* + int actualNumberOfOnesInOobEvent1 = 0; + for (OobOperation oobOperation : oob.getOperations()) { + actualNumberOfOnesInOobEvent1 += oobOperation.isOobEvent1() ? 1 : 0; + } + assert (actualNumberOfOnesInOobEvent1 == numberOfOnesInOobEvent1); + */ + } +} diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/rlpaddr/TestRlpAddress.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/rlpaddr/TestRlpAddress.java index 69e6d87ba1..362a7e4a4b 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/rlpaddr/TestRlpAddress.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/rlpaddr/TestRlpAddress.java @@ -113,7 +113,7 @@ void test() { private ToyAccount randCreate(int initCodeSize) { byte[] value = bigIntegerToBytes(BigInteger.valueOf(randLong())).toArray(); return ToyAccount.builder() - .balance(Wei.MAX_WEI) + .balance(Wei.fromEth(1000)) .nonce(randLong()) .address(Address.wrap(Bytes.repeat((byte) 0x01, 20))) .code( @@ -135,7 +135,7 @@ private ToyAccount randCreateTwo(int initCodeSize) { byte[] value = bigIntegerToBytes(BigInteger.valueOf(randLong())).toArray(); return ToyAccount.builder() - .balance(Wei.MAX_WEI) + .balance(Wei.fromEth(1000)) .nonce(randLong()) .address(Address.wrap(Bytes.repeat((byte) 0x02, 20))) .code( diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/shf/ShfRtTracerTest.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/shf/ShfRtTracerTest.java index 17f23ad874..7d43f96a6a 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/module/shf/ShfRtTracerTest.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/module/shf/ShfRtTracerTest.java @@ -50,6 +50,14 @@ void testFailingBlockchainBlock(final int opCodeValue) { .run(); } + @Test + void TestShfResultFailure() { + BytecodeRunner.of( + Bytes.fromHexString( + "7faaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa7fa0000000000000000000000000000000000000000000000000000000000000001d")) + .run(); + } + @ParameterizedTest(name = "{0}") @MethodSource("provideRandomSarArguments") void testRandomSar(final Bytes32[] payload) { diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeCompiler.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeCompiler.java index 054163193d..eb6e4baab4 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeCompiler.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeCompiler.java @@ -184,6 +184,16 @@ public BytecodeCompiler push(final BigInteger xs) { return this.push(bigIntegerToBytes(xs)); } + /** + * Add a {@link OpCode#PUSH1} and a {@link String} argument representing a hex number. + * + * @param x String argument representing a hex number + * @return current instance + */ + public BytecodeCompiler push(final String x) { + return this.push(new BigInteger(x, 16)); + } + /** * Add a {@link OpCode#PUSH1} and int number argument. * diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeRunner.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeRunner.java index c66f4f73f1..a37b1f3de3 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeRunner.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/BytecodeRunner.java @@ -16,12 +16,14 @@ package net.consensys.linea.zktracer.testing; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import com.google.common.base.Preconditions; import lombok.Setter; import lombok.experimental.Accessors; import net.consensys.linea.zktracer.ZkTracer; +import net.consensys.linea.zktracer.module.constants.GlobalConstants; import net.consensys.linea.zktracer.module.hub.Hub; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.crypto.KeyPair; @@ -37,6 +39,7 @@ */ @Accessors(fluent = true) public final class BytecodeRunner { + public static final long DEFAULT_GAS_LIMIT = 25_000_000L; private final Bytes byteCode; ToyExecutionEnvironment toyExecutionEnvironment; @@ -54,6 +57,10 @@ public static BytecodeRunner of(Bytes byteCode) { @Setter private Consumer zkTracerValidator = zkTracer -> {}; public void run() { + this.run(Wei.fromEth(1), (long) GlobalConstants.LINEA_BLOCK_GAS_LIMIT); + } + + public void run(Wei senderBalance, Long gasLimit) { Preconditions.checkArgument(byteCode != null, "byteCode cannot be empty"); KeyPair keyPair = new SECP256K1().generateKeyPair(); @@ -62,9 +69,11 @@ public void run() { final ToyAccount senderAccount = ToyAccount.builder().balance(Wei.fromEth(1)).nonce(5).address(senderAddress).build(); + final Long selectedGasLimit = Optional.of(gasLimit).orElse(DEFAULT_GAS_LIMIT); + final ToyAccount receiverAccount = ToyAccount.builder() - .balance(Wei.fromEth(1)) + .balance(senderBalance) .nonce(6) .address(Address.fromHexString("0x1111111111111111111111111111111111111111")) .code(byteCode) @@ -75,7 +84,7 @@ public void run() { .sender(senderAccount) .to(receiverAccount) .keyPair(keyPair) - .gasLimit(25000000L) + .gasLimit(selectedGasLimit) .build(); final ToyWorld toyWorld = diff --git a/arithmetization/src/test/resources/contracts/oob/Callee.sol b/arithmetization/src/test/resources/contracts/oob/Callee.sol new file mode 100644 index 0000000000..28e519cd0a --- /dev/null +++ b/arithmetization/src/test/resources/contracts/oob/Callee.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract Callee { + event EtherReceived(address sender, uint256 amount); + + function calleeFunction() external payable { + emit EtherReceived(msg.sender, msg.value); + } +} \ No newline at end of file diff --git a/arithmetization/src/test/resources/contracts/oob/Caller.sol b/arithmetization/src/test/resources/contracts/oob/Caller.sol new file mode 100644 index 0000000000..b0b0cb9114 --- /dev/null +++ b/arithmetization/src/test/resources/contracts/oob/Caller.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.2 <0.9.0; + +import "./Callee.sol"; + +contract Caller { + + function invokeCalleeFunctionWithEther(address calleeAddress, uint256 amount) external { + Callee calleeContract = Callee(calleeAddress); + // Transfer amount to the Callee contract from Caller balance + calleeContract.calleeFunction{value: amount}(); + } + + + function invokeOwnFunctionRecursively(uint256 iterations) public { + if (iterations > 0) { + this.invokeOwnFunctionRecursively(iterations - 1); + } + } + + // Function to receive Ether + receive() external payable {} +} \ No newline at end of file diff --git a/arithmetization/src/test/resources/contracts/oob/compiledContracts/Callee.bin-runtime b/arithmetization/src/test/resources/contracts/oob/compiledContracts/Callee.bin-runtime new file mode 100644 index 0000000000..513907510f --- /dev/null +++ b/arithmetization/src/test/resources/contracts/oob/compiledContracts/Callee.bin-runtime @@ -0,0 +1 @@ +608060405260043610601c5760003560e01c8063671dcbd7146021575b600080fd5b60276029565b005b7f1e57e3bb474320be3d2c77138f75b7c3941292d647f5f9634e33a8e94e0e069b3334604051605892919060b6565b60405180910390a1565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000608b826062565b9050919050565b6099816082565b82525050565b6000819050919050565b60b081609f565b82525050565b600060408201905060c960008301856092565b60d4602083018460a9565b939250505056fea2646970667358221220db0fd3e56b2e3e01fc7e13c3d4079963fd4a197b9952ae46f9f273efe2eb481964736f6c63430008150033 \ No newline at end of file diff --git a/arithmetization/src/test/resources/contracts/oob/compiledContracts/Caller.bin-runtime b/arithmetization/src/test/resources/contracts/oob/compiledContracts/Caller.bin-runtime new file mode 100644 index 0000000000..bb7d293005 --- /dev/null +++ b/arithmetization/src/test/resources/contracts/oob/compiledContracts/Caller.bin-runtime @@ -0,0 +1 @@ +60806040526004361061002d5760003560e01c806363acac8e14610039578063ff277a621461006257610034565b3661003457005b600080fd5b34801561004557600080fd5b50610060600480360381019061005b91906101b5565b61008b565b005b34801561006e57600080fd5b5061008960048036038101906100849190610240565b61010f565b005b600081111561010c573073ffffffffffffffffffffffffffffffffffffffff166363acac8e6001836100bd91906102af565b6040518263ffffffff1660e01b81526004016100d991906102f2565b600060405180830381600087803b1580156100f357600080fd5b505af1158015610107573d6000803e3d6000fd5b505050505b50565b60008290508073ffffffffffffffffffffffffffffffffffffffff1663671dcbd7836040518263ffffffff1660e01b81526004016000604051808303818588803b15801561015c57600080fd5b505af1158015610170573d6000803e3d6000fd5b5050505050505050565b600080fd5b6000819050919050565b6101928161017f565b811461019d57600080fd5b50565b6000813590506101af81610189565b92915050565b6000602082840312156101cb576101ca61017a565b5b60006101d9848285016101a0565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061020d826101e2565b9050919050565b61021d81610202565b811461022857600080fd5b50565b60008135905061023a81610214565b92915050565b600080604083850312156102575761025661017a565b5b60006102658582860161022b565b9250506020610276858286016101a0565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006102ba8261017f565b91506102c58361017f565b92508282039050818111156102dd576102dc610280565b5b92915050565b6102ec8161017f565b82525050565b600060208201905061030760008301846102e3565b9291505056fea2646970667358221220eeda6cd078a1e0b43b7e0e6267949ef02a8119de7d68431781e3b1ef33a616d464736f6c63430008150033 \ No newline at end of file diff --git a/gradle/tests.gradle b/gradle/tests.gradle index 98a5a59643..7d5bf88b39 100644 --- a/gradle/tests.gradle +++ b/gradle/tests.gradle @@ -50,6 +50,7 @@ test { '--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED' ] + Set toImport = [ 'root.log.level', 'evm.log.level' diff --git a/zkevm-constraints b/zkevm-constraints index e4a3e05df4..89a0345561 160000 --- a/zkevm-constraints +++ b/zkevm-constraints @@ -1 +1 @@ -Subproject commit e4a3e05df4ff8f92b0cdf2afaed83ea6a856ff3f +Subproject commit 89a03455611d41cb4c1b292c0931e0edf65012a8 From c83b65ce21f138b99bdbf6e482e86c7ee49b1250 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Wed, 29 May 2024 12:15:05 +0200 Subject: [PATCH 08/39] fix: resolve conflicts with sequencer classes (#737) Signed-off-by: Fabio Di Fabio --- .../config/LineaL1L2BridgeCliOptions.java | 87 -------- .../config/LineaProfitabilityCliOptions.java | 199 ------------------ .../LineaProfitabilityConfiguration.java | 32 --- .../linea/config/LineaRpcCliOptions.java | 73 ------- .../linea/config/LineaRpcConfiguration.java | 22 -- .../LineaTransactionSelectorCliOptions.java | 150 ------------- ...LineaTransactionSelectorConfiguration.java | 28 --- .../LineaTransactionValidatorCliOptions.java | 106 ---------- ...ineaTransactionValidatorConfiguration.java | 29 --- .../config/converters/AddressConverter.java | 25 --- .../config/converters/BytesConverter.java | 25 --- .../linea/config/converters/WeiConverter.java | 28 --- .../linea/zktracer/testing/ToyAccount.java | 5 + .../testing/ToyExecutionEnvironment.java | 2 - gradle.properties | 2 +- gradle/dist.gradle | 2 +- .../consensys/linea/CorsetBlockProcessor.java | 12 +- .../linea/GeneralStateReferenceTestTools.java | 1 - 18 files changed, 12 insertions(+), 816 deletions(-) delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaL1L2BridgeCliOptions.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaRpcCliOptions.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaRpcConfiguration.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionValidatorCliOptions.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionValidatorConfiguration.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/converters/AddressConverter.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/converters/BytesConverter.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/config/converters/WeiConverter.java diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaL1L2BridgeCliOptions.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaL1L2BridgeCliOptions.java deleted file mode 100644 index cfd6c1db50..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaL1L2BridgeCliOptions.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.config; - -import com.google.common.base.MoreObjects; -import net.consensys.linea.config.converters.AddressConverter; -import net.consensys.linea.config.converters.BytesConverter; -import org.apache.tuweni.bytes.Bytes; -import org.hyperledger.besu.datatypes.Address; -import picocli.CommandLine; - -/** The Linea L1 L2 Bridge CLI options. */ -public class LineaL1L2BridgeCliOptions { - private static final String L1L2_BRIDGE_CONTRACT = "--plugin-linea-l1l2-bridge-contract"; - private static final String L1L2_BRIDGE_TOPIC = "--plugin-linea-l1l2-bridge-topic"; - - @CommandLine.Option( - names = {L1L2_BRIDGE_CONTRACT}, - paramLabel = "
", - converter = AddressConverter.class, - description = "The address of the L1 L2 bridge contract (default: ${DEFAULT-VALUE})") - private Address l1l2BridgeContract = Address.ZERO; - - @CommandLine.Option( - names = {L1L2_BRIDGE_TOPIC}, - paramLabel = "", - converter = BytesConverter.class, - description = "The log topic of the L1 L2 bridge (default: ${DEFAULT-VALUE})") - private Bytes l1l2BridgeTopic = Bytes.EMPTY; - - private LineaL1L2BridgeCliOptions() {} - - /** - * Create Linea cli options. - * - * @return the Linea cli options - */ - public static LineaL1L2BridgeCliOptions create() { - return new LineaL1L2BridgeCliOptions(); - } - - /** - * Linea cli options from config. - * - * @param config the config - * @return the Linea cli options - */ - public static LineaL1L2BridgeCliOptions fromConfig(final LineaL1L2BridgeConfiguration config) { - final LineaL1L2BridgeCliOptions options = create(); - options.l1l2BridgeContract = config.contract(); - options.l1l2BridgeTopic = config.topic(); - return options; - } - - /** - * To domain object Linea factory configuration. - * - * @return the Linea factory configuration - */ - public LineaL1L2BridgeConfiguration toDomainObject() { - return LineaL1L2BridgeConfiguration.builder() - .contract(l1l2BridgeContract) - .topic(l1l2BridgeTopic) - .build(); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add(L1L2_BRIDGE_CONTRACT, l1l2BridgeContract.toHexString()) - .add(L1L2_BRIDGE_TOPIC, l1l2BridgeTopic.toHexString()) - .toString(); - } -} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java deleted file mode 100644 index 14683bb59a..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * 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.config; - -import java.math.BigDecimal; - -import com.google.common.base.MoreObjects; -import jakarta.validation.constraints.Positive; -import net.consensys.linea.config.converters.WeiConverter; -import org.hyperledger.besu.datatypes.Wei; -import picocli.CommandLine; - -/** The Linea profitability calculator CLI options. */ -public class LineaProfitabilityCliOptions { - public static final String VERIFICATION_GAS_COST = "--plugin-linea-verification-gas-cost"; - public static final int DEFAULT_VERIFICATION_GAS_COST = 1_200_000; - - public static final String VERIFICATION_CAPACITY = "--plugin-linea-verification-capacity"; - public static final int DEFAULT_VERIFICATION_CAPACITY = 90_000; - - public static final String GAS_PRICE_RATIO = "--plugin-linea-gas-price-ratio"; - public static final int DEFAULT_GAS_PRICE_RATIO = 15; - - public static final String GAS_PRICE_ADJUSTMENT = "--plugin-linea-gas-price-adjustment"; - public static final Wei DEFAULT_GAS_PRICE_ADJUSTMENT = Wei.ZERO; - - public static final String MIN_MARGIN = "--plugin-linea-min-margin"; - public static final BigDecimal DEFAULT_MIN_MARGIN = BigDecimal.ONE; - - public static final String ESTIMATE_GAS_MIN_MARGIN = "--plugin-linea-estimate-gas-min-margin"; - public static final BigDecimal DEFAULT_ESTIMATE_GAS_MIN_MARGIN = BigDecimal.ONE; - - public static final String TX_POOL_MIN_MARGIN = "--plugin-linea-tx-pool-min-margin"; - public static final BigDecimal DEFAULT_TX_POOL_MIN_MARGIN = BigDecimal.valueOf(0.5); - - public static final String TX_POOL_ENABLE_CHECK_API = - "--plugin-linea-tx-pool-profitability-check-api-enabled"; - public static final boolean DEFAULT_TX_POOL_ENABLE_CHECK_API = true; - - public static final String TX_POOL_ENABLE_CHECK_P2P = - "--plugin-linea-tx-pool-profitability-check-p2p-enabled"; - public static final boolean DEFAULT_TX_POOL_ENABLE_CHECK_P2P = false; - - @Positive - @CommandLine.Option( - names = {VERIFICATION_GAS_COST}, - hidden = true, - paramLabel = "", - description = "L1 verification gas cost (default: ${DEFAULT-VALUE})") - private int verificationGasCost = DEFAULT_VERIFICATION_GAS_COST; - - @Positive - @CommandLine.Option( - names = {VERIFICATION_CAPACITY}, - hidden = true, - paramLabel = "", - description = "L1 verification capacity (default: ${DEFAULT-VALUE})") - private int verificationCapacity = DEFAULT_VERIFICATION_CAPACITY; - - @Positive - @CommandLine.Option( - names = {GAS_PRICE_RATIO}, - hidden = true, - paramLabel = "", - description = "L1/L2 gas price ratio (default: ${DEFAULT-VALUE})") - private int gasPriceRatio = DEFAULT_GAS_PRICE_RATIO; - - @CommandLine.Option( - names = {GAS_PRICE_ADJUSTMENT}, - hidden = true, - converter = WeiConverter.class, - paramLabel = "", - description = - "Amount to add to the calculated profitable gas price (default: ${DEFAULT-VALUE})") - private Wei gasPriceAdjustment = DEFAULT_GAS_PRICE_ADJUSTMENT; - - @Positive - @CommandLine.Option( - names = {MIN_MARGIN}, - hidden = true, - paramLabel = "", - description = "Minimum margin of a transaction to be selected (default: ${DEFAULT-VALUE})") - private BigDecimal minMargin = DEFAULT_MIN_MARGIN; - - @Positive - @CommandLine.Option( - names = {ESTIMATE_GAS_MIN_MARGIN}, - hidden = true, - paramLabel = "", - description = - "Recommend a specific gas price when using linea_estimateGas (default: ${DEFAULT-VALUE})") - private BigDecimal estimageGasMinMargin = DEFAULT_ESTIMATE_GAS_MIN_MARGIN; - - @Positive - @CommandLine.Option( - names = {TX_POOL_MIN_MARGIN}, - hidden = true, - paramLabel = "", - description = - "The min margin an incoming tx must have to be accepted in the txpool (default: ${DEFAULT-VALUE})") - private BigDecimal txPoolMinMargin = DEFAULT_TX_POOL_MIN_MARGIN; - - @CommandLine.Option( - names = {TX_POOL_ENABLE_CHECK_API}, - arity = "0..1", - hidden = true, - paramLabel = "", - description = - "Enable the profitability check for txs received via API? (default: ${DEFAULT-VALUE})") - private boolean txPoolCheckApiEnabled = DEFAULT_TX_POOL_ENABLE_CHECK_API; - - @CommandLine.Option( - names = {TX_POOL_ENABLE_CHECK_P2P}, - arity = "0..1", - hidden = true, - paramLabel = "", - description = - "Enable the profitability check for txs received via p2p? (default: ${DEFAULT-VALUE})") - private boolean txPoolCheckP2pEnabled = DEFAULT_TX_POOL_ENABLE_CHECK_P2P; - - private LineaProfitabilityCliOptions() {} - - /** - * Create Linea cli options. - * - * @return the Linea cli options - */ - public static LineaProfitabilityCliOptions create() { - return new LineaProfitabilityCliOptions(); - } - - /** - * Linea cli options from config. - * - * @param config the config - * @return the Linea cli options - */ - public static LineaProfitabilityCliOptions fromConfig( - final LineaProfitabilityConfiguration config) { - final LineaProfitabilityCliOptions options = create(); - options.verificationGasCost = config.verificationGasCost(); - options.verificationCapacity = config.verificationCapacity(); - options.gasPriceRatio = config.gasPriceRatio(); - options.gasPriceAdjustment = config.gasPriceAdjustment(); - options.minMargin = BigDecimal.valueOf(config.minMargin()); - options.estimageGasMinMargin = BigDecimal.valueOf(config.estimateGasMinMargin()); - options.txPoolMinMargin = BigDecimal.valueOf(config.txPoolMinMargin()); - options.txPoolCheckApiEnabled = config.txPoolCheckApiEnabled(); - options.txPoolCheckP2pEnabled = config.txPoolCheckP2pEnabled(); - return options; - } - - /** - * To domain object Linea factory configuration. - * - * @return the Linea factory configuration - */ - public LineaProfitabilityConfiguration toDomainObject() { - return LineaProfitabilityConfiguration.builder() - .verificationGasCost(verificationGasCost) - .verificationCapacity(verificationCapacity) - .gasPriceRatio(gasPriceRatio) - .gasPriceAdjustment(gasPriceAdjustment) - .minMargin(minMargin.doubleValue()) - .estimateGasMinMargin(estimageGasMinMargin.doubleValue()) - .txPoolMinMargin(txPoolMinMargin.doubleValue()) - .txPoolCheckApiEnabled(txPoolCheckApiEnabled) - .txPoolCheckP2pEnabled(txPoolCheckP2pEnabled) - .build(); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add(VERIFICATION_GAS_COST, verificationGasCost) - .add(VERIFICATION_CAPACITY, verificationCapacity) - .add(GAS_PRICE_RATIO, gasPriceRatio) - .add(GAS_PRICE_ADJUSTMENT, gasPriceAdjustment) - .add(MIN_MARGIN, minMargin) - .add(ESTIMATE_GAS_MIN_MARGIN, estimageGasMinMargin) - .add(TX_POOL_MIN_MARGIN, txPoolMinMargin) - .add(TX_POOL_ENABLE_CHECK_API, txPoolCheckApiEnabled) - .add(TX_POOL_ENABLE_CHECK_P2P, txPoolCheckP2pEnabled) - .toString(); - } -} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java deleted file mode 100644 index 514371ebd3..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.config; - -import lombok.Builder; -import org.hyperledger.besu.datatypes.Wei; - -/** The Linea profitability calculator configuration. */ -@Builder(toBuilder = true) -public record LineaProfitabilityConfiguration( - int verificationGasCost, - int verificationCapacity, - int gasPriceRatio, - Wei gasPriceAdjustment, - double minMargin, - double estimateGasMinMargin, - double txPoolMinMargin, - boolean txPoolCheckApiEnabled, - boolean txPoolCheckP2pEnabled) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaRpcCliOptions.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaRpcCliOptions.java deleted file mode 100644 index 05b1e543fe..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaRpcCliOptions.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.config; - -import com.google.common.base.MoreObjects; -import picocli.CommandLine; - -/** The Linea RPC CLI options. */ -public class LineaRpcCliOptions { - private static final String ESTIMATE_GAS_COMPATIBILITY_MODE_ENABLED = - "--plugin-linea-estimate-gas-compatibility-mode-enabled"; - - @CommandLine.Option( - names = {ESTIMATE_GAS_COMPATIBILITY_MODE_ENABLED}, - paramLabel = "", - description = - "Set to true to return the min mineable gas price, instead of the profitable price (default: ${DEFAULT-VALUE})") - private boolean estimateGasCompatibilityModeEnabled = false; - - private LineaRpcCliOptions() {} - - /** - * Create Linea RPC CLI options. - * - * @return the Linea RPC CLI options - */ - public static LineaRpcCliOptions create() { - return new LineaRpcCliOptions(); - } - - /** - * Linea RPC CLI options from config. - * - * @param config the config - * @return the Linea RPC CLI options - */ - public static LineaRpcCliOptions fromConfig(final LineaRpcConfiguration config) { - final LineaRpcCliOptions options = create(); - options.estimateGasCompatibilityModeEnabled = config.estimateGasCompatibilityModeEnabled(); - return options; - } - - /** - * To domain object Linea factory configuration. - * - * @return the Linea factory configuration - */ - public LineaRpcConfiguration toDomainObject() { - return LineaRpcConfiguration.builder() - .estimateGasCompatibilityModeEnabled(estimateGasCompatibilityModeEnabled) - .build(); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add(ESTIMATE_GAS_COMPATIBILITY_MODE_ENABLED, estimateGasCompatibilityModeEnabled) - .toString(); - } -} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaRpcConfiguration.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaRpcConfiguration.java deleted file mode 100644 index 5742ab45ef..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaRpcConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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.config; - -import lombok.Builder; - -/** The Linea RPC configuration. */ -@Builder(toBuilder = true) -public record LineaRpcConfiguration(boolean estimateGasCompatibilityModeEnabled) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java deleted file mode 100644 index 9f8f0ba3bd..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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.config; - -import com.google.common.base.MoreObjects; -import jakarta.validation.constraints.Positive; -import picocli.CommandLine; - -/** The Linea Transaction Selector CLI options. */ -public class LineaTransactionSelectorCliOptions { - public static final String MAX_BLOCK_CALLDATA_SIZE = "--plugin-linea-max-block-calldata-size"; - public static final int DEFAULT_MAX_BLOCK_CALLDATA_SIZE = 70_000; - - public static final String MODULE_LIMIT_FILE_PATH = "--plugin-linea-module-limit-file-path"; - public static final String DEFAULT_MODULE_LIMIT_FILE_PATH = "moduleLimitFile.toml"; - - public static final String OVER_LINE_COUNT_LIMIT_CACHE_SIZE = - "--plugin-linea-over-line-count-limit-cache-size"; - public static final int DEFAULT_OVER_LINE_COUNT_LIMIT_CACHE_SIZE = 10_000; - - public static final String MAX_GAS_PER_BLOCK = "--plugin-linea-max-block-gas"; - public static final long DEFAULT_MAX_GAS_PER_BLOCK = 30_000_000L; - - public static final String UNPROFITABLE_CACHE_SIZE = "--plugin-linea-unprofitable-cache-size"; - public static final int DEFAULT_UNPROFITABLE_CACHE_SIZE = 100_000; - - public static final String UNPROFITABLE_RETRY_LIMIT = "--plugin-linea-unprofitable-retry-limit"; - public static final int DEFAULT_UNPROFITABLE_RETRY_LIMIT = 10; - - @Positive - @CommandLine.Option( - names = {MAX_BLOCK_CALLDATA_SIZE}, - hidden = true, - paramLabel = "", - description = "Maximum size for the calldata of a block (default: ${DEFAULT-VALUE})") - private int maxBlockCallDataSize = DEFAULT_MAX_BLOCK_CALLDATA_SIZE; - - @CommandLine.Option( - names = {MODULE_LIMIT_FILE_PATH}, - hidden = true, - paramLabel = "", - description = - "Path to the toml file containing the module limits (default: ${DEFAULT-VALUE})") - private String moduleLimitFilePath = DEFAULT_MODULE_LIMIT_FILE_PATH; - - @Positive - @CommandLine.Option( - names = {OVER_LINE_COUNT_LIMIT_CACHE_SIZE}, - hidden = true, - paramLabel = "", - description = - "Max number of transactions that go over the line count limit we keep track of (default: ${DEFAULT-VALUE})") - private int overLineCountLimitCacheSize = DEFAULT_OVER_LINE_COUNT_LIMIT_CACHE_SIZE; - - @Positive - @CommandLine.Option( - names = {MAX_GAS_PER_BLOCK}, - hidden = true, - paramLabel = "", - description = "Sets max gas per block (default: ${DEFAULT-VALUE})") - private Long maxGasPerBlock = DEFAULT_MAX_GAS_PER_BLOCK; - - @Positive - @CommandLine.Option( - names = {UNPROFITABLE_CACHE_SIZE}, - hidden = true, - paramLabel = "", - description = - "Max number of unprofitable transactions we keep track of (default: ${DEFAULT-VALUE})") - private int unprofitableCacheSize = DEFAULT_UNPROFITABLE_CACHE_SIZE; - - @Positive - @CommandLine.Option( - names = {UNPROFITABLE_RETRY_LIMIT}, - hidden = true, - paramLabel = "", - description = - "Max number of unprofitable transactions we retry on each block creation (default: ${DEFAULT-VALUE})") - private int unprofitableRetryLimit = DEFAULT_UNPROFITABLE_RETRY_LIMIT; - - private LineaTransactionSelectorCliOptions() {} - - /** - * Create Linea cli options. - * - * @return the Linea cli options - */ - public static LineaTransactionSelectorCliOptions create() { - return new LineaTransactionSelectorCliOptions(); - } - - /** - * Linea cli options from config. - * - * @param config the config - * @return the Linea cli options - */ - public static LineaTransactionSelectorCliOptions fromConfig( - final LineaTransactionSelectorConfiguration config) { - final LineaTransactionSelectorCliOptions options = create(); - options.maxBlockCallDataSize = config.maxBlockCallDataSize(); - options.moduleLimitFilePath = config.moduleLimitsFilePath(); - options.overLineCountLimitCacheSize = config.overLinesLimitCacheSize(); - options.maxGasPerBlock = config.maxGasPerBlock(); - options.unprofitableCacheSize = config.unprofitableCacheSize(); - options.unprofitableRetryLimit = config.unprofitableRetryLimit(); - return options; - } - - /** - * To domain object Linea factory configuration. - * - * @return the Linea factory configuration - */ - public LineaTransactionSelectorConfiguration toDomainObject() { - return LineaTransactionSelectorConfiguration.builder() - .maxBlockCallDataSize(maxBlockCallDataSize) - .moduleLimitsFilePath(moduleLimitFilePath) - .overLinesLimitCacheSize(overLineCountLimitCacheSize) - .maxGasPerBlock(maxGasPerBlock) - .unprofitableCacheSize(unprofitableCacheSize) - .unprofitableRetryLimit(unprofitableRetryLimit) - .build(); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add(MAX_BLOCK_CALLDATA_SIZE, maxBlockCallDataSize) - .add(MODULE_LIMIT_FILE_PATH, moduleLimitFilePath) - .add(OVER_LINE_COUNT_LIMIT_CACHE_SIZE, overLineCountLimitCacheSize) - .add(MAX_GAS_PER_BLOCK, maxGasPerBlock) - .add(UNPROFITABLE_CACHE_SIZE, unprofitableCacheSize) - .add(UNPROFITABLE_RETRY_LIMIT, unprofitableRetryLimit) - .toString(); - } -} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java deleted file mode 100644 index 09afc4f2e7..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.config; - -import lombok.Builder; - -/** The Linea transaction selectors configuration. */ -@Builder(toBuilder = true) -public record LineaTransactionSelectorConfiguration( - int maxBlockCallDataSize, - String moduleLimitsFilePath, - int overLinesLimitCacheSize, - long maxGasPerBlock, - int unprofitableCacheSize, - int unprofitableRetryLimit) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionValidatorCliOptions.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionValidatorCliOptions.java deleted file mode 100644 index a97b2414ad..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionValidatorCliOptions.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.config; - -import com.google.common.base.MoreObjects; -import picocli.CommandLine; - -/** The Linea CLI options. */ -public class LineaTransactionValidatorCliOptions { - - public static final String DENY_LIST_PATH = "--plugin-linea-deny-list-path"; - public static final String DEFAULT_DENY_LIST_PATH = "lineaDenyList.txt"; - - public static final String MAX_TX_GAS_LIMIT_OPTION = "--plugin-linea-max-tx-gas-limit"; - public static final int DEFAULT_MAX_TRANSACTION_GAS_LIMIT = 30_000_000; - - public static final String MAX_TX_CALLDATA_SIZE = "--plugin-linea-max-tx-calldata-size"; - public static final int DEFAULT_MAX_TX_CALLDATA_SIZE = 60_000; - - @CommandLine.Option( - names = {DENY_LIST_PATH}, - hidden = true, - paramLabel = "", - description = - "Path to the file containing the deny list (default: " + DEFAULT_DENY_LIST_PATH + ")") - private String denyListPath = DEFAULT_DENY_LIST_PATH; - - @CommandLine.Option( - names = {MAX_TX_GAS_LIMIT_OPTION}, - hidden = true, - paramLabel = "", - description = - "Maximum gas limit for a transaction (default: " - + DEFAULT_MAX_TRANSACTION_GAS_LIMIT - + ")") - private int maxTxGasLimit = DEFAULT_MAX_TRANSACTION_GAS_LIMIT; - - @CommandLine.Option( - names = {MAX_TX_CALLDATA_SIZE}, - hidden = true, - paramLabel = "", - description = - "Maximum size for the calldata of a Transaction (default: " - + DEFAULT_MAX_TX_CALLDATA_SIZE - + ")") - private int maxTxCallDataSize = DEFAULT_MAX_TX_CALLDATA_SIZE; - - private LineaTransactionValidatorCliOptions() {} - - /** - * Create Linea cli options. - * - * @return the Linea cli options - */ - public static LineaTransactionValidatorCliOptions create() { - return new LineaTransactionValidatorCliOptions(); - } - - /** - * Cli options from config. - * - * @param config the config - * @return the cli options - */ - public static LineaTransactionValidatorCliOptions fromConfig( - final LineaTransactionValidatorConfiguration config) { - final LineaTransactionValidatorCliOptions options = create(); - options.denyListPath = config.denyListPath(); - options.maxTxGasLimit = config.maxTxGasLimit(); - options.maxTxCallDataSize = config.maxTxCalldataSize(); - - return options; - } - - /** - * To domain object Linea factory configuration. - * - * @return the Linea factory configuration - */ - public LineaTransactionValidatorConfiguration toDomainObject() { - return new LineaTransactionValidatorConfiguration( - denyListPath, maxTxGasLimit, maxTxCallDataSize); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add(DENY_LIST_PATH, denyListPath) - .add(MAX_TX_GAS_LIMIT_OPTION, maxTxGasLimit) - .add(MAX_TX_CALLDATA_SIZE, maxTxCallDataSize) - .toString(); - } -} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionValidatorConfiguration.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionValidatorConfiguration.java deleted file mode 100644 index 61f1ac3c84..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/LineaTransactionValidatorConfiguration.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.config; - -import lombok.Builder; - -/** - * The Linea transaction pool validation configuration. - * - * @param denyListPath the path to the file containing the addresses that are denied. - * @param maxTxGasLimit the maximum gas limit allowed for transactions - * @param maxTxCalldataSize the maximum size of calldata allowed for transactions - */ -@Builder(toBuilder = true) -public record LineaTransactionValidatorConfiguration( - String denyListPath, int maxTxGasLimit, int maxTxCalldataSize) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/converters/AddressConverter.java b/arithmetization/src/main/java/net/consensys/linea/config/converters/AddressConverter.java deleted file mode 100644 index a1c3c457c4..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/converters/AddressConverter.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.config.converters; - -import org.hyperledger.besu.datatypes.Address; -import picocli.CommandLine; - -public class AddressConverter implements CommandLine.ITypeConverter
{ - @Override - public Address convert(final String s) throws Exception { - return Address.fromHexString(s); - } -} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/converters/BytesConverter.java b/arithmetization/src/main/java/net/consensys/linea/config/converters/BytesConverter.java deleted file mode 100644 index 28d73c0122..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/converters/BytesConverter.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.config.converters; - -import org.apache.tuweni.bytes.Bytes; -import picocli.CommandLine; - -public class BytesConverter implements CommandLine.ITypeConverter { - @Override - public Bytes convert(final String s) throws Exception { - return Bytes.fromHexStringLenient(s); - } -} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/converters/WeiConverter.java b/arithmetization/src/main/java/net/consensys/linea/config/converters/WeiConverter.java deleted file mode 100644 index 33ea66181b..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/config/converters/WeiConverter.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.config.converters; - -import java.math.BigInteger; - -import org.apache.tuweni.bytes.Bytes; -import org.hyperledger.besu.datatypes.Wei; -import picocli.CommandLine; - -public class WeiConverter implements CommandLine.ITypeConverter { - @Override - public Bytes convert(final String s) throws Exception { - return Wei.of(new BigInteger(s)); - } -} diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyAccount.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyAccount.java index c426c6c91c..4dcf2480e5 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyAccount.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyAccount.java @@ -68,6 +68,11 @@ public Address getAddress() { return address; } + @Override + public boolean isStorageEmpty() { + return false; + } + @Override public Hash getAddressHash() { return addressHash.get(); diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyExecutionEnvironment.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyExecutionEnvironment.java index 6770bbc93f..493f781fb4 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyExecutionEnvironment.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/testing/ToyExecutionEnvironment.java @@ -160,7 +160,6 @@ private void executeFrom(final ConflationSnapshot conflation) { for (Transaction tx : body.getTransactions()) { transactionProcessor.processTransaction( - null, overridenToyWorld.updater(), (ProcessableBlockHeader) header, tx, @@ -198,7 +197,6 @@ private void execute() { for (Transaction tx : mockBlockBody.getTransactions()) { final TransactionProcessingResult result = transactionProcessor.processTransaction( - null, toyWorld.updater(), (ProcessableBlockHeader) header, tx, diff --git a/gradle.properties b/gradle.properties index f5b8942e81..af38a4a9e4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ releaseVersion=0.1.4-SNAPSHOT -besuVersion=24.2.0-SNAPSHOT +besuVersion=24.5-develop-62f4326 besuArtifactGroup=io.consensys.linea-besu distributionIdentifier=linea-arithmetization distributionBaseUrl=https://artifacts.consensys.net/public/linea-besu/raw/names/linea-besu.tar.gz/versions/ diff --git a/gradle/dist.gradle b/gradle/dist.gradle index 28631a9cc2..387412c2fd 100644 --- a/gradle/dist.gradle +++ b/gradle/dist.gradle @@ -50,7 +50,7 @@ jar { } from { - configurations.runtimeClasspath.filter( {! (it.name =~ /log4j.*\.jar/ )} ) + configurations.runtimeClasspath.filter( {! (it.name =~ /log4j.*\.jar/ ) && ! (it.name =~ /vertx.*\.jar/ )} ) .collect {it.isDirectory() ? it : zipTree(it) } } exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' diff --git a/reference-tests/src/test/java/net/consensys/linea/CorsetBlockProcessor.java b/reference-tests/src/test/java/net/consensys/linea/CorsetBlockProcessor.java index 528a53379d..02e3367820 100644 --- a/reference-tests/src/test/java/net/consensys/linea/CorsetBlockProcessor.java +++ b/reference-tests/src/test/java/net/consensys/linea/CorsetBlockProcessor.java @@ -31,7 +31,6 @@ import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockBody; import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.Deposit; import org.hyperledger.besu.ethereum.core.MutableWorldState; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionReceipt; @@ -46,10 +45,10 @@ import org.hyperledger.besu.ethereum.mainnet.WithdrawalsProcessor; import org.hyperledger.besu.ethereum.privacy.storage.PrivateMetadataUpdater; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; -import org.hyperledger.besu.ethereum.trie.bonsai.worldview.BonsaiWorldState; -import org.hyperledger.besu.ethereum.trie.bonsai.worldview.BonsaiWorldStateUpdateAccumulator; -import org.hyperledger.besu.ethereum.vm.BlockHashLookup; +import org.hyperledger.besu.ethereum.trie.diffbased.bonsai.worldview.BonsaiWorldState; +import org.hyperledger.besu.ethereum.trie.diffbased.bonsai.worldview.BonsaiWorldStateUpdateAccumulator; import org.hyperledger.besu.ethereum.vm.CachingBlockHashLookup; +import org.hyperledger.besu.evm.operation.BlockHashOperation; import org.hyperledger.besu.evm.worldstate.WorldUpdater; @Slf4j @@ -84,7 +83,6 @@ public BlockProcessingResult processBlock( final List transactions, final List ommers, final Optional> maybeWithdrawals, - final Optional> maybeDeposits, final PrivateMetadataUpdater privateMetadataUpdater) { final List receipts = new ArrayList<>(); long currentGasUsed = 0; @@ -105,7 +103,8 @@ public BlockProcessingResult processBlock( final WorldUpdater worldStateUpdater = worldState.updater(); - final BlockHashLookup blockHashLookup = new CachingBlockHashLookup(blockHeader, blockchain); + final BlockHashOperation.BlockHashLookup blockHashLookup = + new CachingBlockHashLookup(blockHeader, blockchain); final Address miningBeneficiary = miningBeneficiaryCalculator.calculateBeneficiary(blockHeader); @@ -126,7 +125,6 @@ public BlockProcessingResult processBlock( zkTracer.traceStartBlock(blockHeader, blockBody); final TransactionProcessingResult result = transactionProcessor.processTransaction( - blockchain, worldStateUpdater, blockHeader, transaction, diff --git a/reference-tests/src/test/java/net/consensys/linea/GeneralStateReferenceTestTools.java b/reference-tests/src/test/java/net/consensys/linea/GeneralStateReferenceTestTools.java index 01fe520c78..2b39868837 100644 --- a/reference-tests/src/test/java/net/consensys/linea/GeneralStateReferenceTestTools.java +++ b/reference-tests/src/test/java/net/consensys/linea/GeneralStateReferenceTestTools.java @@ -175,7 +175,6 @@ public static void executeTest(final GeneralStateTestCaseEipSpec spec) { final TransactionProcessingResult result = processor.processTransaction( - blockchain, worldStateUpdater, blockHeader, transaction, From 98788778b78c0a61aaf960a3a31b370f5419461a Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Wed, 29 May 2024 14:44:24 +0300 Subject: [PATCH 09/39] fix: update besu to 24.4-develop-a5a3eb8 and disable OOB, BLAKE2f_MODEXP_DATA and EXP modules (#738) Resolves: #715 Signed-off-by: Tsvetan Dimitrov --- .../net/consensys/linea/zktracer/module/hub/Hub.java | 9 ++++++--- zkevm-constraints | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java index a2b46d1473..e4f7fbb9e7 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java @@ -340,7 +340,8 @@ public List getModulesToTrace() { this, this.add, this.bin, - this.blake2fModexpData, + // WARNING: Temporarily disabled. + // this.blake2fModexpData, this.ecData, this.blockdata, this.blockhash, @@ -354,7 +355,8 @@ public List getModulesToTrace() { this.mod, this.mul, this.mxp, - this.oob, + // WARNING: Temporarily disabled. + // this.oob, this.rlpAddr, this.rlpTxn, this.rlpTxrcpt, @@ -394,7 +396,8 @@ public List getModulesToCount() { this.mod, this.mul, this.mxp, - this.oob, + // WARNING: Temporarily disabled. + // this.oob, this.exp, this.rlpAddr, this.rlpTxn, diff --git a/zkevm-constraints b/zkevm-constraints index 89a0345561..74923d612f 160000 --- a/zkevm-constraints +++ b/zkevm-constraints @@ -1 +1 @@ -Subproject commit 89a03455611d41cb4c1b292c0931e0edf65012a8 +Subproject commit 74923d612fe978b9905daacfe064c9a286306e7c From 21bc3ebe5d47c6db24476a3932437cfe3c1fadc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Bojarski?= <54240434+letypequividelespoubelles@users.noreply.github.com> Date: Fri, 31 May 2024 10:56:16 +0530 Subject: [PATCH 10/39] Fix/issue 729/few fixes in modexp data (#741) * build(constraints): update * feat(blalemodexp): fix and simplify * build(constraints): update * build: reenable blakemodexp * fix(blakemodexp): was called two times * fix(modexp): fix modulus is 0 * build(constraints): add blakemodexp --- .../blake2fmodexpdata/Blake2fComponents.java | 2 +- .../blake2fmodexpdata/Blake2fModexpData.java | 57 +++- .../Blake2fModexpDataOperation.java | 245 ++++++++---------- .../Blake2fModexpTraceHelper.java | 80 ------ .../module/blake2fmodexpdata/PhaseInfo.java | 18 -- .../module/blake2fmodexpdata/Trace.java | 150 +++++------ .../linea/zktracer/module/hub/Hub.java | 11 +- .../limits/precompiles/Blake2fRounds.java | 33 +-- .../precompiles/ModexpEffectiveCall.java | 56 ++-- zkevm-constraints | 2 +- 10 files changed, 265 insertions(+), 389 deletions(-) delete mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpTraceHelper.java delete mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/PhaseInfo.java diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fComponents.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fComponents.java index 81a28e0173..ea2cd22ca8 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fComponents.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fComponents.java @@ -17,4 +17,4 @@ import org.apache.tuweni.bytes.Bytes; -public record Blake2fComponents(Bytes rawInput, Bytes data, Bytes r, Bytes f) {} +public record Blake2fComponents(Bytes data, Bytes r, Bytes f) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpData.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpData.java index a81b350582..9fe18deeff 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpData.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpData.java @@ -23,15 +23,26 @@ import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_MODEXP_RESULT; import java.nio.MappedByteBuffer; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import lombok.RequiredArgsConstructor; import net.consensys.linea.zktracer.ColumnHeader; import net.consensys.linea.zktracer.container.stacked.set.StackedSet; import net.consensys.linea.zktracer.module.Module; +import net.consensys.linea.zktracer.module.wcp.Wcp; import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.evm.log.Log; +import org.hyperledger.besu.evm.worldstate.WorldView; +@RequiredArgsConstructor public class Blake2fModexpData implements Module { - private StackedSet state = new StackedSet<>(); + private final Wcp wcp; + private StackedSet operations = new StackedSet<>(); + private List sortedOperations = new ArrayList<>(); + private int numberOfOperationsAtStartTx = 0; @Override public String moduleKey() { @@ -40,17 +51,43 @@ public String moduleKey() { @Override public void enterTransaction() { - this.state.enter(); + this.operations.enter(); + } + + @Override + public void traceEndTx( + WorldView worldView, + Transaction tx, + boolean isSuccessful, + Bytes output, + List logs, + long gasUsed) { + final List newOperations = + new ArrayList<>(this.operations.sets.getLast()) + .stream().sorted(Comparator.comparingLong(Blake2fModexpDataOperation::id)).toList(); + + this.sortedOperations.addAll(newOperations); + final int numberOfOperationsAtEndTx = sortedOperations.size(); + for (int i = numberOfOperationsAtStartTx; i < numberOfOperationsAtEndTx; i++) { + final long previousID = i == 0 ? 0 : sortedOperations.get(i - 1).id(); + this.wcp.callLT(previousID, sortedOperations.get(i).id()); + } + } + + @Override + public void traceStartTx(WorldView worldView, Transaction tx) { + this.numberOfOperationsAtStartTx = operations.size(); } @Override public void popTransaction() { - this.state.pop(); + this.sortedOperations.removeAll(this.operations.sets.getLast()); + this.operations.pop(); } @Override public int lineCount() { - return this.state.lineCount(); + return this.operations.lineCount(); } @Override @@ -58,17 +95,15 @@ public List columnsHeaders() { return Trace.headers(this.lineCount()); } - public long call(final Blake2fModexpDataOperation operation) { - this.state.add(operation); - - return operation.prevHubStamp(); + public void call(final Blake2fModexpDataOperation operation) { + this.operations.add(operation); } @Override public void commit(List buffers) { Trace trace = new Trace(buffers); int stamp = 0; - for (Blake2fModexpDataOperation o : this.state) { + for (Blake2fModexpDataOperation o : this.sortedOperations) { stamp++; o.trace(trace, stamp); } @@ -88,8 +123,8 @@ public Bytes getInputDataByIdAndPhase(final int id, final int phase) { } private Blake2fModexpDataOperation getOperationById(final int id) { - for (Blake2fModexpDataOperation operation : this.state) { - if (id == operation.hubStampPlusOne) { + for (Blake2fModexpDataOperation operation : this.operations) { + if (id == operation.id) { return operation; } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpDataOperation.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpDataOperation.java index 8961d50559..a9c8bb85ea 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpDataOperation.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpDataOperation.java @@ -15,6 +15,15 @@ package net.consensys.linea.zktracer.module.blake2fmodexpdata; +import static net.consensys.linea.zktracer.module.blake2fmodexpdata.Trace.INDEX_MAX_BLAKE_DATA; +import static net.consensys.linea.zktracer.module.blake2fmodexpdata.Trace.INDEX_MAX_BLAKE_PARAMS; +import static net.consensys.linea.zktracer.module.blake2fmodexpdata.Trace.INDEX_MAX_BLAKE_RESULT; +import static net.consensys.linea.zktracer.module.blake2fmodexpdata.Trace.INDEX_MAX_MODEXP; +import static net.consensys.linea.zktracer.module.blake2fmodexpdata.Trace.INDEX_MAX_MODEXP_BASE; +import static net.consensys.linea.zktracer.module.blake2fmodexpdata.Trace.INDEX_MAX_MODEXP_EXPONENT; +import static net.consensys.linea.zktracer.module.blake2fmodexpdata.Trace.INDEX_MAX_MODEXP_MODULUS; +import static net.consensys.linea.zktracer.module.blake2fmodexpdata.Trace.INDEX_MAX_MODEXP_RESULT; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.LLARGE; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_BLAKE_DATA; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_BLAKE_PARAMS; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_BLAKE_RESULT; @@ -23,17 +32,11 @@ import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_MODEXP_MODULUS; import static net.consensys.linea.zktracer.module.constants.GlobalConstants.PHASE_MODEXP_RESULT; import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; -import static net.consensys.linea.zktracer.types.Conversions.bytesToUnsignedBytes; import static net.consensys.linea.zktracer.types.Utils.leftPadTo; -import static net.consensys.linea.zktracer.types.Utils.rightPadTo; import java.math.BigInteger; -import java.util.List; -import java.util.Map; import java.util.Optional; -import com.google.common.base.Preconditions; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.experimental.Accessors; import net.consensys.linea.zktracer.container.ModuleOperation; @@ -41,50 +44,25 @@ import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.crypto.Hash; -@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) @Accessors(fluent = true) public class Blake2fModexpDataOperation extends ModuleOperation { - private static final int MODEXP_COMPONENT_INT_BYTE_SIZE = 32; - private static final int MODEXP_COMPONENT_SIZE = 512; - private static final int MODEXP_LIMB_INT_BYTE_SIZE = 16; - private static final int BLAKE2f_DATA_SIZE = Trace.INDEX_MAX_BLAKE_DATA + 1; - private static final int BLAKE2f_RESULT_SIZE = Trace.INDEX_MAX_BLAKE_RESULT + 1; - private static final int BLAKE2f_PARAMS_SIZE = Trace.INDEX_MAX_BLAKE_PARAMS + 1; - private static final int BLAKE2f_LIMB_INT_BYTE_SIZE = 16; - private static final int MODEXP_COMPONENTS_LINE_COUNT = 32 * 4; + private static final int MODEXP_COMPONENT_BYTE_SIZE = LLARGE * (INDEX_MAX_MODEXP + 1); + private static final int MODEXP_COMPONENTS_LINE_COUNT = + (INDEX_MAX_MODEXP_BASE + 1) + + (INDEX_MAX_MODEXP_EXPONENT + 1) + + (INDEX_MAX_MODEXP_MODULUS + 1) + + (INDEX_MAX_MODEXP_RESULT + 1); private static final int BLAKE2f_COMPONENTS_LINE_COUNT = - BLAKE2f_DATA_SIZE + BLAKE2f_RESULT_SIZE + BLAKE2f_PARAMS_SIZE; - - private static final Map PHASE_INFO_MAP = - Map.of( - PHASE_MODEXP_BASE, - new PhaseInfo(PHASE_MODEXP_BASE, Trace.INDEX_MAX_MODEXP_BASE), - PHASE_MODEXP_EXPONENT, - new PhaseInfo(PHASE_MODEXP_EXPONENT, Trace.INDEX_MAX_MODEXP_EXPONENT), - PHASE_MODEXP_MODULUS, - new PhaseInfo(PHASE_MODEXP_MODULUS, Trace.INDEX_MAX_MODEXP_MODULUS), - PHASE_MODEXP_RESULT, - new PhaseInfo(PHASE_MODEXP_RESULT, Trace.INDEX_MAX_MODEXP_RESULT), - PHASE_BLAKE_DATA, - new PhaseInfo(PHASE_BLAKE_DATA, Trace.INDEX_MAX_BLAKE_DATA), - PHASE_BLAKE_PARAMS, - new PhaseInfo(PHASE_BLAKE_PARAMS, Trace.INDEX_MAX_BLAKE_PARAMS), - PHASE_BLAKE_RESULT, - new PhaseInfo(PHASE_BLAKE_RESULT, Trace.INDEX_MAX_BLAKE_RESULT)); - - @EqualsAndHashCode.Include public final long hubStampPlusOne; - @Getter private long prevHubStamp; - - @EqualsAndHashCode.Include public final Optional modexpComponents; - @EqualsAndHashCode.Include public final Optional blake2fComponents; + (INDEX_MAX_BLAKE_DATA + 1) + (INDEX_MAX_BLAKE_PARAMS + 1) + (INDEX_MAX_BLAKE_RESULT + 1); + + @Getter public final long id; + + public final Optional modexpComponents; + public final Optional blake2fComponents; public Blake2fModexpDataOperation( - long hubStamp, - long prevHubStamp, - ModexpComponents modexpComponents, - Blake2fComponents blake2fComponents) { - this.hubStampPlusOne = hubStamp + 1; - this.prevHubStamp = prevHubStamp; + long hubStamp, ModexpComponents modexpComponents, Blake2fComponents blake2fComponents) { + this.id = hubStamp + 1; this.modexpComponents = Optional.ofNullable(modexpComponents); this.blake2fComponents = Optional.ofNullable(blake2fComponents); } @@ -96,118 +74,111 @@ protected int computeLineCount() { : BLAKE2f_COMPONENTS_LINE_COUNT; } - void trace(Trace trace, int stamp) { + void trace(Trace trace, final int stamp) { final UnsignedByte stampByte = UnsignedByte.of(stamp); - final UnsignedByte[] hubStampDiffBytes = getHubStampDiffBytes(this.hubStampPlusOne); - - final Bytes modexpComponentsLimb = - modexpComponents.map(this::buildModexpComponentsLimb).orElse(Bytes.EMPTY); - - final Bytes blake2fResult = - blake2fComponents.map(c -> computeBlake2fResult(c.rawInput())).orElse(Bytes.EMPTY); - - var tracerBuilder = - Blake2fModexpTraceHelper.builder() - .trace(trace) - .currentHubStamp(this.hubStampPlusOne) - .prevHubStamp(prevHubStamp) - .phaseInfoMap(PHASE_INFO_MAP) - .hubStampDiffBytes(hubStampDiffBytes) - .stampByte(stampByte); - if (modexpComponents.isPresent()) { - Blake2fModexpTraceHelper modexpTraceHelper = - tracerBuilder - .startPhaseIndex(PHASE_MODEXP_BASE) - .endPhaseIndex(PHASE_MODEXP_RESULT) - .currentRowIndexFunction( - ((phaseInfo, phaseIndex, index) -> - phaseInfo.indexMax() * (phaseIndex - 1) + index)) - .traceLimbConsumer( - (rowIndex, phaseIndex) -> { - if (!modexpComponentsLimb.isEmpty()) { - trace.limb( - modexpComponentsLimb.slice( - MODEXP_LIMB_INT_BYTE_SIZE * rowIndex, MODEXP_LIMB_INT_BYTE_SIZE)); - } - }) - .build(); - - modexpTraceHelper.trace(); - - prevHubStamp = modexpTraceHelper.prevHubStamp(); + traceBase(trace, stampByte); + traceExponent(trace, stampByte); + traceModulus(trace, stampByte); + traceModexpResult(trace, stampByte); + return; } if (blake2fComponents.isPresent()) { - Blake2fComponents components = blake2fComponents.get(); - Blake2fModexpTraceHelper blake2fTraceHelper = - tracerBuilder - .startPhaseIndex(PHASE_BLAKE_DATA) - .endPhaseIndex(PHASE_BLAKE_RESULT) - .currentRowIndexFunction(((phaseInfo, phaseIndex, index) -> index)) - .traceLimbConsumer( - (rowIndex, phaseIndex) -> { - if (phaseIndex == PHASE_BLAKE_DATA) { - trace.limb( - components - .data() - .slice( - BLAKE2f_LIMB_INT_BYTE_SIZE * rowIndex, - BLAKE2f_LIMB_INT_BYTE_SIZE)); - } else if (phaseIndex == PHASE_BLAKE_PARAMS) { - if (rowIndex == Trace.INDEX_MAX_BLAKE_PARAMS - 1) { - trace.limb(components.r()); - } else { - trace.limb(components.f()); - } - } else { - trace.limb( - blake2fResult.slice( - BLAKE2f_LIMB_INT_BYTE_SIZE * rowIndex, BLAKE2f_LIMB_INT_BYTE_SIZE)); - } - }) - .build(); - - blake2fTraceHelper.trace(); - - prevHubStamp = blake2fTraceHelper.prevHubStamp(); + traceData(trace, stampByte); + traceParameter(trace, stampByte); + traceBlakeResult(trace, stampByte); } } - private Bytes buildModexpComponentsLimb(ModexpComponents components) { - final Bytes result = computeModexpResult(components); - final Bytes basePadded = leftPadTo(components.base(), MODEXP_COMPONENT_SIZE); - final Bytes expPadded = leftPadTo(components.exp(), MODEXP_COMPONENT_SIZE); - final Bytes modPadded = leftPadTo(components.mod(), MODEXP_COMPONENT_SIZE); - final Bytes resultPadded = leftPadTo(result, MODEXP_COMPONENT_SIZE); + private void traceData(Trace trace, UnsignedByte stamp) { + final Bytes input = blake2fComponents.get().data(); + for (int index = 0; index <= INDEX_MAX_BLAKE_DATA; index++) { + commonTrace(trace, stamp, index, input, INDEX_MAX_BLAKE_DATA); + trace.phase(UnsignedByte.of(PHASE_BLAKE_DATA)).isBlakeData(true).fillAndValidateRow(); + } + } - return Bytes.concatenate(basePadded, expPadded, modPadded, resultPadded); + private void traceParameter(Trace trace, UnsignedByte stamp) { + // r + commonTrace( + trace, stamp, 0, leftPadTo(blake2fComponents.get().r(), LLARGE), INDEX_MAX_BLAKE_PARAMS); + trace.phase(UnsignedByte.of(PHASE_BLAKE_PARAMS)).isBlakeParams(true).fillAndValidateRow(); + + // f + commonTrace( + trace, + stamp, + 1, + leftPadTo(blake2fComponents.get().f(), 2 * LLARGE), + INDEX_MAX_BLAKE_PARAMS); + trace.phase(UnsignedByte.of(PHASE_BLAKE_PARAMS)).isBlakeParams(true).fillAndValidateRow(); } - private Bytes computeModexpResult(ModexpComponents modexpComponents) { - final BigInteger baseBigInt = modexpComponents.base().toUnsignedBigInteger(); - final BigInteger expBigInt = modexpComponents.exp().toUnsignedBigInteger(); - final BigInteger modBigInt = modexpComponents.mod().toUnsignedBigInteger(); + private void traceBlakeResult(Trace trace, UnsignedByte stamp) { + final Bytes input = computeBlake2fResult(); + for (int index = 0; index <= INDEX_MAX_BLAKE_RESULT; index++) { + commonTrace(trace, stamp, index, input, INDEX_MAX_BLAKE_RESULT); + trace.phase(UnsignedByte.of(PHASE_BLAKE_RESULT)).isBlakeResult(true).fillAndValidateRow(); + } + } - if (List.of(baseBigInt, expBigInt, modBigInt).contains(BigInteger.ZERO)) { - return Bytes.EMPTY; + private void traceBase(Trace trace, final UnsignedByte stamp) { + final Bytes input = leftPadTo(modexpComponents.get().base(), MODEXP_COMPONENT_BYTE_SIZE); + for (int index = 0; index <= INDEX_MAX_MODEXP_BASE; index++) { + commonTrace(trace, stamp, index, input, INDEX_MAX_MODEXP_BASE); + trace.phase(UnsignedByte.of(PHASE_MODEXP_BASE)).isModexpBase(true).fillAndValidateRow(); } + } - return bigIntegerToBytes(baseBigInt.modPow(expBigInt, modBigInt)); + private void traceExponent(Trace trace, final UnsignedByte stamp) { + final Bytes input = leftPadTo(modexpComponents.get().exp(), MODEXP_COMPONENT_BYTE_SIZE); + for (int index = 0; index <= INDEX_MAX_MODEXP_EXPONENT; index++) { + commonTrace(trace, stamp, index, input, INDEX_MAX_MODEXP_EXPONENT); + trace + .phase(UnsignedByte.of(PHASE_MODEXP_EXPONENT)) + .isModexpExponent(true) + .fillAndValidateRow(); + } } - private Bytes computeBlake2fResult(Bytes input) { - return Hash.blake2bf(input); + private void traceModulus(Trace trace, final UnsignedByte stamp) { + final Bytes input = leftPadTo(modexpComponents.get().mod(), MODEXP_COMPONENT_BYTE_SIZE); + for (int index = 0; index <= INDEX_MAX_MODEXP_MODULUS; index++) { + commonTrace(trace, stamp, index, input, INDEX_MAX_MODEXP_MODULUS); + trace.phase(UnsignedByte.of(PHASE_MODEXP_MODULUS)).isModexpModulus(true).fillAndValidateRow(); + } } - private UnsignedByte[] getHubStampDiffBytes(long currentHubStamp) { - final long hubStampDiff = currentHubStamp - prevHubStamp - 1; + private void traceModexpResult(Trace trace, final UnsignedByte stamp) { + final Bytes input = leftPadTo(computeModexpResult(), MODEXP_COMPONENT_BYTE_SIZE); + for (int index = 0; index <= INDEX_MAX_MODEXP_RESULT; index++) { + commonTrace(trace, stamp, index, input, INDEX_MAX_MODEXP_RESULT); + trace.phase(UnsignedByte.of(PHASE_MODEXP_RESULT)).isModexpResult(true).fillAndValidateRow(); + } + } - Preconditions.checkArgument( - hubStampDiff < Math.pow(256, 6), "Hub stamp difference should never exceed 256 ^ 6"); + private void commonTrace(Trace trace, UnsignedByte stamp, int index, Bytes input, int indexMax) { + trace + .stamp(stamp) + .id(id) + .index(UnsignedByte.of(index)) + .indexMax(UnsignedByte.of(indexMax)) + .limb(input.slice(index * LLARGE, LLARGE)); + } + + private Bytes computeModexpResult() { + final BigInteger baseBigInt = modexpComponents.get().base().toUnsignedBigInteger(); + final BigInteger expBigInt = modexpComponents.get().exp().toUnsignedBigInteger(); + final BigInteger modBigInt = modexpComponents.get().mod().toUnsignedBigInteger(); + + return modBigInt.equals(BigInteger.ZERO) + ? Bytes.of(0) + : bigIntegerToBytes(baseBigInt.modPow(expBigInt, modBigInt)); + } - return bytesToUnsignedBytes( - rightPadTo(leftPadTo(Bytes.ofUnsignedInt(hubStampDiff), 6), 128).toArray()); + private Bytes computeBlake2fResult() { + return Hash.blake2bf(blake2fComponents.get().data()); } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpTraceHelper.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpTraceHelper.java deleted file mode 100644 index 849dcad3bb..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Blake2fModexpTraceHelper.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.module.blake2fmodexpdata; - -import java.util.Map; -import java.util.function.BiConsumer; - -import lombok.Builder; -import lombok.Getter; -import lombok.experimental.Accessors; -import net.consensys.linea.zktracer.types.UnsignedByte; -import org.apache.commons.lang3.function.TriFunction; - -@Builder -@Accessors(fluent = true) -public class Blake2fModexpTraceHelper { - private final long currentHubStamp; - @Getter private long prevHubStamp; - private final int startPhaseIndex; - private final int endPhaseIndex; - - private final Map phaseInfoMap; - private final Trace trace; - private final UnsignedByte stampByte; - private final BiConsumer traceLimbConsumer; - private final TriFunction currentRowIndexFunction; - private UnsignedByte[] hubStampDiffBytes; - - void trace() { - boolean[] phaseFlags = new boolean[7]; - - for (int phaseIndex = startPhaseIndex; phaseIndex <= endPhaseIndex; phaseIndex++) { - phaseFlags[phaseIndex - 1] = true; - - final PhaseInfo phaseInfo = phaseInfoMap.get(phaseIndex); - - final int indexMax = phaseInfo.indexMax(); - for (int index = 0; index <= indexMax; index++) { - int rowIndex = currentRowIndexFunction.apply(phaseInfo, phaseIndex, index); - - trace - .phase(UnsignedByte.of(phaseInfo.id())) - .deltaByte(hubStampDiffBytes[rowIndex]) - .id(currentHubStamp) - .index(UnsignedByte.of(index)) - .indexMax(UnsignedByte.of(indexMax)) - .stamp(stampByte); - - traceLimbConsumer.accept(rowIndex, phaseIndex); - - trace - .isModexpBase(phaseFlags[0]) - .isModexpExponent(phaseFlags[1]) - .isModexpModulus(phaseFlags[2]) - .isModexpResult(phaseFlags[3]) - .isBlakeData(phaseFlags[4]) - .isBlakeParams(phaseFlags[5]) - .isBlakeResult(phaseFlags[6]) - .validateRow(); - } - - phaseFlags[phaseIndex - 1] = false; - } - - prevHubStamp = currentHubStamp; - } -} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/PhaseInfo.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/PhaseInfo.java deleted file mode 100644 index c6a0be0c7b..0000000000 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/PhaseInfo.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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.module.blake2fmodexpdata; - -record PhaseInfo(int id, int indexMax) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Trace.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Trace.java index 92c8c54e58..79109ec7c6 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Trace.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/blake2fmodexpdata/Trace.java @@ -33,6 +33,7 @@ public class Trace { public static final int INDEX_MAX_BLAKE_DATA = 0xc; public static final int INDEX_MAX_BLAKE_PARAMS = 0x1; public static final int INDEX_MAX_BLAKE_RESULT = 0x3; + public static final int INDEX_MAX_MODEXP = 0x1f; public static final int INDEX_MAX_MODEXP_BASE = 0x1f; public static final int INDEX_MAX_MODEXP_EXPONENT = 0x1f; public static final int INDEX_MAX_MODEXP_MODULUS = 0x1f; @@ -41,7 +42,6 @@ public class Trace { private final BitSet filled = new BitSet(); private int currentLine = 0; - private final MappedByteBuffer deltaByte; private final MappedByteBuffer id; private final MappedByteBuffer index; private final MappedByteBuffer indexMax; @@ -58,7 +58,6 @@ public class Trace { static List headers(int length) { return List.of( - new ColumnHeader("blake2fmodexpdata.DELTA_BYTE", 1, length), new ColumnHeader("blake2fmodexpdata.ID", 8, length), new ColumnHeader("blake2fmodexpdata.INDEX", 1, length), new ColumnHeader("blake2fmodexpdata.INDEX_MAX", 1, length), @@ -75,20 +74,19 @@ static List headers(int length) { } public Trace(List buffers) { - this.deltaByte = buffers.get(0); - this.id = buffers.get(1); - this.index = buffers.get(2); - this.indexMax = buffers.get(3); - this.isBlakeData = buffers.get(4); - this.isBlakeParams = buffers.get(5); - this.isBlakeResult = buffers.get(6); - this.isModexpBase = buffers.get(7); - this.isModexpExponent = buffers.get(8); - this.isModexpModulus = buffers.get(9); - this.isModexpResult = buffers.get(10); - this.limb = buffers.get(11); - this.phase = buffers.get(12); - this.stamp = buffers.get(13); + this.id = buffers.get(0); + this.index = buffers.get(1); + this.indexMax = buffers.get(2); + this.isBlakeData = buffers.get(3); + this.isBlakeParams = buffers.get(4); + this.isBlakeResult = buffers.get(5); + this.isModexpBase = buffers.get(6); + this.isModexpExponent = buffers.get(7); + this.isModexpModulus = buffers.get(8); + this.isModexpResult = buffers.get(9); + this.limb = buffers.get(10); + this.phase = buffers.get(11); + this.stamp = buffers.get(12); } public int size() { @@ -99,23 +97,11 @@ public int size() { return this.currentLine; } - public Trace deltaByte(final UnsignedByte b) { - if (filled.get(0)) { - throw new IllegalStateException("blake2fmodexpdata.DELTA_BYTE already set"); - } else { - filled.set(0); - } - - deltaByte.put(b.toByte()); - - return this; - } - public Trace id(final long b) { - if (filled.get(1)) { + if (filled.get(0)) { throw new IllegalStateException("blake2fmodexpdata.ID already set"); } else { - filled.set(1); + filled.set(0); } id.putLong(b); @@ -124,10 +110,10 @@ public Trace id(final long b) { } public Trace index(final UnsignedByte b) { - if (filled.get(2)) { + if (filled.get(1)) { throw new IllegalStateException("blake2fmodexpdata.INDEX already set"); } else { - filled.set(2); + filled.set(1); } index.put(b.toByte()); @@ -136,10 +122,10 @@ public Trace index(final UnsignedByte b) { } public Trace indexMax(final UnsignedByte b) { - if (filled.get(3)) { + if (filled.get(2)) { throw new IllegalStateException("blake2fmodexpdata.INDEX_MAX already set"); } else { - filled.set(3); + filled.set(2); } indexMax.put(b.toByte()); @@ -148,10 +134,10 @@ public Trace indexMax(final UnsignedByte b) { } public Trace isBlakeData(final Boolean b) { - if (filled.get(4)) { + if (filled.get(3)) { throw new IllegalStateException("blake2fmodexpdata.IS_BLAKE_DATA already set"); } else { - filled.set(4); + filled.set(3); } isBlakeData.put((byte) (b ? 1 : 0)); @@ -160,10 +146,10 @@ public Trace isBlakeData(final Boolean b) { } public Trace isBlakeParams(final Boolean b) { - if (filled.get(5)) { + if (filled.get(4)) { throw new IllegalStateException("blake2fmodexpdata.IS_BLAKE_PARAMS already set"); } else { - filled.set(5); + filled.set(4); } isBlakeParams.put((byte) (b ? 1 : 0)); @@ -172,10 +158,10 @@ public Trace isBlakeParams(final Boolean b) { } public Trace isBlakeResult(final Boolean b) { - if (filled.get(6)) { + if (filled.get(5)) { throw new IllegalStateException("blake2fmodexpdata.IS_BLAKE_RESULT already set"); } else { - filled.set(6); + filled.set(5); } isBlakeResult.put((byte) (b ? 1 : 0)); @@ -184,10 +170,10 @@ public Trace isBlakeResult(final Boolean b) { } public Trace isModexpBase(final Boolean b) { - if (filled.get(7)) { + if (filled.get(6)) { throw new IllegalStateException("blake2fmodexpdata.IS_MODEXP_BASE already set"); } else { - filled.set(7); + filled.set(6); } isModexpBase.put((byte) (b ? 1 : 0)); @@ -196,10 +182,10 @@ public Trace isModexpBase(final Boolean b) { } public Trace isModexpExponent(final Boolean b) { - if (filled.get(8)) { + if (filled.get(7)) { throw new IllegalStateException("blake2fmodexpdata.IS_MODEXP_EXPONENT already set"); } else { - filled.set(8); + filled.set(7); } isModexpExponent.put((byte) (b ? 1 : 0)); @@ -208,10 +194,10 @@ public Trace isModexpExponent(final Boolean b) { } public Trace isModexpModulus(final Boolean b) { - if (filled.get(9)) { + if (filled.get(8)) { throw new IllegalStateException("blake2fmodexpdata.IS_MODEXP_MODULUS already set"); } else { - filled.set(9); + filled.set(8); } isModexpModulus.put((byte) (b ? 1 : 0)); @@ -220,10 +206,10 @@ public Trace isModexpModulus(final Boolean b) { } public Trace isModexpResult(final Boolean b) { - if (filled.get(10)) { + if (filled.get(9)) { throw new IllegalStateException("blake2fmodexpdata.IS_MODEXP_RESULT already set"); } else { - filled.set(10); + filled.set(9); } isModexpResult.put((byte) (b ? 1 : 0)); @@ -232,10 +218,10 @@ public Trace isModexpResult(final Boolean b) { } public Trace limb(final Bytes b) { - if (filled.get(11)) { + if (filled.get(10)) { throw new IllegalStateException("blake2fmodexpdata.LIMB already set"); } else { - filled.set(11); + filled.set(10); } final byte[] bs = b.toArrayUnsafe(); @@ -248,10 +234,10 @@ public Trace limb(final Bytes b) { } public Trace phase(final UnsignedByte b) { - if (filled.get(12)) { + if (filled.get(11)) { throw new IllegalStateException("blake2fmodexpdata.PHASE already set"); } else { - filled.set(12); + filled.set(11); } phase.put(b.toByte()); @@ -260,10 +246,10 @@ public Trace phase(final UnsignedByte b) { } public Trace stamp(final UnsignedByte b) { - if (filled.get(13)) { + if (filled.get(12)) { throw new IllegalStateException("blake2fmodexpdata.STAMP already set"); } else { - filled.set(13); + filled.set(12); } stamp.put(b.toByte()); @@ -273,58 +259,54 @@ public Trace stamp(final UnsignedByte b) { public Trace validateRow() { if (!filled.get(0)) { - throw new IllegalStateException("blake2fmodexpdata.DELTA_BYTE has not been filled"); - } - - if (!filled.get(1)) { throw new IllegalStateException("blake2fmodexpdata.ID has not been filled"); } - if (!filled.get(2)) { + if (!filled.get(1)) { throw new IllegalStateException("blake2fmodexpdata.INDEX has not been filled"); } - if (!filled.get(3)) { + if (!filled.get(2)) { throw new IllegalStateException("blake2fmodexpdata.INDEX_MAX has not been filled"); } - if (!filled.get(4)) { + if (!filled.get(3)) { throw new IllegalStateException("blake2fmodexpdata.IS_BLAKE_DATA has not been filled"); } - if (!filled.get(5)) { + if (!filled.get(4)) { throw new IllegalStateException("blake2fmodexpdata.IS_BLAKE_PARAMS has not been filled"); } - if (!filled.get(6)) { + if (!filled.get(5)) { throw new IllegalStateException("blake2fmodexpdata.IS_BLAKE_RESULT has not been filled"); } - if (!filled.get(7)) { + if (!filled.get(6)) { throw new IllegalStateException("blake2fmodexpdata.IS_MODEXP_BASE has not been filled"); } - if (!filled.get(8)) { + if (!filled.get(7)) { throw new IllegalStateException("blake2fmodexpdata.IS_MODEXP_EXPONENT has not been filled"); } - if (!filled.get(9)) { + if (!filled.get(8)) { throw new IllegalStateException("blake2fmodexpdata.IS_MODEXP_MODULUS has not been filled"); } - if (!filled.get(10)) { + if (!filled.get(9)) { throw new IllegalStateException("blake2fmodexpdata.IS_MODEXP_RESULT has not been filled"); } - if (!filled.get(11)) { + if (!filled.get(10)) { throw new IllegalStateException("blake2fmodexpdata.LIMB has not been filled"); } - if (!filled.get(12)) { + if (!filled.get(11)) { throw new IllegalStateException("blake2fmodexpdata.PHASE has not been filled"); } - if (!filled.get(13)) { + if (!filled.get(12)) { throw new IllegalStateException("blake2fmodexpdata.STAMP has not been filled"); } @@ -336,58 +318,54 @@ public Trace validateRow() { public Trace fillAndValidateRow() { if (!filled.get(0)) { - deltaByte.position(deltaByte.position() + 1); - } - - if (!filled.get(1)) { id.position(id.position() + 8); } - if (!filled.get(2)) { + if (!filled.get(1)) { index.position(index.position() + 1); } - if (!filled.get(3)) { + if (!filled.get(2)) { indexMax.position(indexMax.position() + 1); } - if (!filled.get(4)) { + if (!filled.get(3)) { isBlakeData.position(isBlakeData.position() + 1); } - if (!filled.get(5)) { + if (!filled.get(4)) { isBlakeParams.position(isBlakeParams.position() + 1); } - if (!filled.get(6)) { + if (!filled.get(5)) { isBlakeResult.position(isBlakeResult.position() + 1); } - if (!filled.get(7)) { + if (!filled.get(6)) { isModexpBase.position(isModexpBase.position() + 1); } - if (!filled.get(8)) { + if (!filled.get(7)) { isModexpExponent.position(isModexpExponent.position() + 1); } - if (!filled.get(9)) { + if (!filled.get(8)) { isModexpModulus.position(isModexpModulus.position() + 1); } - if (!filled.get(10)) { + if (!filled.get(9)) { isModexpResult.position(isModexpResult.position() + 1); } - if (!filled.get(11)) { + if (!filled.get(10)) { limb.position(limb.position() + 32); } - if (!filled.get(12)) { + if (!filled.get(11)) { phase.position(phase.position() + 1); } - if (!filled.get(13)) { + if (!filled.get(12)) { stamp.position(stamp.position() + 1); } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java index e4f7fbb9e7..c5829f66bb 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java @@ -201,7 +201,7 @@ public void addTraceSection(TraceSection section) { @Getter private final Wcp wcp = new Wcp(this); private final Module add = new Add(this); private final Module bin = new Bin(this); - private final Blake2fModexpData blake2fModexpData = new Blake2fModexpData(); + private final Blake2fModexpData blake2fModexpData = new Blake2fModexpData(this.wcp); @Getter private final EcData ecData; private final Blockdata blockdata; private final Blockhash blockhash = new Blockhash(wcp); @@ -340,8 +340,7 @@ public List getModulesToTrace() { this, this.add, this.bin, - // WARNING: Temporarily disabled. - // this.blake2fModexpData, + this.blake2fModexpData, this.ecData, this.blockdata, this.blockhash, @@ -355,7 +354,7 @@ public List getModulesToTrace() { this.mod, this.mul, this.mxp, - // WARNING: Temporarily disabled. + // TODO: Temporarily disabled. // this.oob, this.rlpAddr, this.rlpTxn, @@ -396,7 +395,7 @@ public List getModulesToCount() { this.mod, this.mul, this.mxp, - // WARNING: Temporarily disabled. + // TODO: Temporarily disabled. // this.oob, this.exp, this.rlpAddr, @@ -661,8 +660,6 @@ void triggerModules(MessageFrame frame) { } if (this.pch.signals().exp()) { this.exp.tracePreOpcode(frame); - this.modexpEffectiveCall.tracePreOpcode(frame); - // if (this.pch.exceptions().none() && this.pch.aborts().none()) } if (this.pch.signals().trm()) { this.trm.tracePreOpcode(frame); diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Blake2fRounds.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Blake2fRounds.java index 910d61866b..78887bfba9 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Blake2fRounds.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/Blake2fRounds.java @@ -16,8 +16,9 @@ package net.consensys.linea.zktracer.module.limits.precompiles; import java.nio.MappedByteBuffer; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.List; -import java.util.Stack; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -44,9 +45,7 @@ public final class Blake2fRounds implements Module { @Getter private final Blake2fModexpData blake2fModexpData; - private long lastDataCallHubStamp = 0; - - private final Stack counts = new Stack<>(); + private final Deque counts = new ArrayDeque<>(); @Override public String moduleKey() { @@ -54,10 +53,15 @@ public String moduleKey() { } @Override - public void enterTransaction() { + public void traceStartConflation(final long blockCount) { counts.push(0); } + @Override + public void enterTransaction() { + counts.push(counts.getFirst()); + } + @Override public void popTransaction() { counts.pop(); @@ -171,16 +175,11 @@ public void tracePreOpcode(MessageFrame frame) { final int rInt = r.toInt(); - final Bytes data = inputData.slice(4, BLAKE2f_INPUT_SIZE - 4); - if (opInfo.gasAllowanceForCall() >= rInt) { - this.lastDataCallHubStamp = - this.blake2fModexpData.call( - new Blake2fModexpDataOperation( - hub.stamp(), - lastDataCallHubStamp, - null, - new Blake2fComponents(inputData, data, r, Bytes.of(f)))); + final Bytes data = inputData.slice(4, BLAKE2f_INPUT_SIZE - 5); + this.blake2fModexpData.call( + new Blake2fModexpDataOperation( + hub.stamp(), null, new Blake2fComponents(data, r, Bytes.of(f)))); this.counts.push(this.counts.pop() + rInt); } } @@ -191,11 +190,7 @@ public void tracePreOpcode(MessageFrame frame) { @Override public int lineCount() { - int r = 0; - for (Integer count : this.counts) { - r += count; - } - return r; + return counts.getFirst(); } @Override diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/ModexpEffectiveCall.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/ModexpEffectiveCall.java index f003c59203..00ffe76961 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/ModexpEffectiveCall.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/limits/precompiles/ModexpEffectiveCall.java @@ -16,11 +16,13 @@ package net.consensys.linea.zktracer.module.limits.precompiles; import static net.consensys.linea.zktracer.module.Util.slice; +import static net.consensys.linea.zktracer.module.constants.GlobalConstants.WORD_SIZE; import java.math.BigInteger; import java.nio.MappedByteBuffer; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.List; -import java.util.Stack; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -45,11 +47,8 @@ public class ModexpEffectiveCall implements Module { private final Hub hub; @Getter private final Blake2fModexpData blake2fModexpData; - private final Stack counts = new Stack<>(); + private final Deque counts = new ArrayDeque<>(); private static final BigInteger PROVER_MAX_INPUT_BYTE_SIZE = BigInteger.valueOf(4096 / 8); - private static final int EVM_WORD_SIZE = 32; - - private long lastDataCallHubStamp = 0; @Override public String moduleKey() { @@ -57,10 +56,15 @@ public String moduleKey() { } @Override - public void enterTransaction() { + public void traceStartConflation(final long blockCount) { counts.push(0); } + @Override + public void enterTransaction() { + counts.push(counts.getFirst()); + } + @Override public void popTransaction() { counts.pop(); @@ -76,7 +80,7 @@ public void tracePreOpcode(MessageFrame frame) { final Bytes inputData = hub.transients().op().callData(); // Get the Base length - final BigInteger baseLength = slice(inputData, 0, EVM_WORD_SIZE).toUnsignedBigInteger(); + final BigInteger baseLength = slice(inputData, 0, WORD_SIZE).toUnsignedBigInteger(); if (isOutOfProverInputBounds(baseLength)) { log.info( "Too big argument, base byte length = {} > {}", @@ -88,8 +92,7 @@ public void tracePreOpcode(MessageFrame frame) { } // Get the Exponent length - final BigInteger expLength = - slice(inputData, EVM_WORD_SIZE, EVM_WORD_SIZE).toUnsignedBigInteger(); + final BigInteger expLength = slice(inputData, WORD_SIZE, WORD_SIZE).toUnsignedBigInteger(); if (isOutOfProverInputBounds(expLength)) { log.info( "Too big argument, exponent byte length = {} > {}", @@ -102,7 +105,7 @@ public void tracePreOpcode(MessageFrame frame) { // Get the Modulo length final BigInteger modLength = - slice(inputData, 2 * EVM_WORD_SIZE, EVM_WORD_SIZE).toUnsignedBigInteger(); + slice(inputData, 2 * WORD_SIZE, WORD_SIZE).toUnsignedBigInteger(); if (isOutOfProverInputBounds(modLength)) { log.info( "Too big argument, modulo byte length = {} > {}", @@ -118,28 +121,24 @@ public void tracePreOpcode(MessageFrame frame) { final int modLengthInt = modLength.intValueExact(); // Get the Base. - final Bytes baseComponent = slice(inputData, 3 * EVM_WORD_SIZE, baseLengthInt); + final Bytes baseComponent = slice(inputData, 3 * WORD_SIZE, baseLengthInt); // Get the Exponent. final Bytes expComponent = - slice(inputData, 3 * EVM_WORD_SIZE + baseLengthInt, expLength.intValueExact()); + slice(inputData, 3 * WORD_SIZE + baseLengthInt, expLength.intValueExact()); // Get the Modulus. final Bytes modComponent = slice( - inputData, - 3 * EVM_WORD_SIZE + baseLengthInt + expLengthInt, - modLength.intValueExact()); + inputData, 3 * WORD_SIZE + baseLengthInt + expLengthInt, modLength.intValueExact()); final long gasPrice = gasPrice(baseLengthInt, expLengthInt, modLengthInt, expComponent); if (hub.transients().op().gasAllowanceForCall() >= gasPrice) { - this.lastDataCallHubStamp = - this.blake2fModexpData.call( - new Blake2fModexpDataOperation( - hub.stamp(), - lastDataCallHubStamp, - new ModexpComponents(baseComponent, expComponent, modComponent), - null)); + this.blake2fModexpData.call( + new Blake2fModexpDataOperation( + hub.stamp(), + new ModexpComponents(baseComponent, expComponent, modComponent), + null)); this.counts.push(this.counts.pop() + 1); } } @@ -156,21 +155,20 @@ public static long gasCost(final Hub hub) { final Bytes inputData = hub.transients().op().callData(); // Get the Base length - final BigInteger baseLength = slice(inputData, 0, EVM_WORD_SIZE).toUnsignedBigInteger(); + final BigInteger baseLength = slice(inputData, 0, WORD_SIZE).toUnsignedBigInteger(); if (isOutOfProverInputBounds(baseLength)) { return 0; } // Get the Exponent length - final BigInteger expLength = - slice(inputData, EVM_WORD_SIZE, EVM_WORD_SIZE).toUnsignedBigInteger(); + final BigInteger expLength = slice(inputData, WORD_SIZE, WORD_SIZE).toUnsignedBigInteger(); if (isOutOfProverInputBounds(expLength)) { return 0; } // Get the Modulo length final BigInteger modLength = - slice(inputData, 2 * EVM_WORD_SIZE, EVM_WORD_SIZE).toUnsignedBigInteger(); + slice(inputData, 2 * WORD_SIZE, WORD_SIZE).toUnsignedBigInteger(); if (isOutOfProverInputBounds(modLength)) { return 0; } @@ -181,7 +179,7 @@ public static long gasCost(final Hub hub) { // Get the Exponent. final Bytes expComponent = - slice(inputData, 3 * EVM_WORD_SIZE + baseLengthInt, expLength.intValueExact()); + slice(inputData, 3 * WORD_SIZE + baseLengthInt, expLength.intValueExact()); return gasPrice(baseLengthInt, expLengthInt, modLengthInt, expComponent); } @@ -203,7 +201,7 @@ private static int expLengthPrime(int expLength, Bytes e) { return e.isZero() ? 0 : e.toUnsignedBigInteger().bitLength() - 1; } - final Bytes leadingWord = e.slice(0, EVM_WORD_SIZE); + final Bytes leadingWord = e.slice(0, WORD_SIZE); return 8 * (expLength - 32) + Math.max(leadingWord.bitLength() - 1, 0); } @@ -213,7 +211,7 @@ private static boolean isOutOfProverInputBounds(BigInteger modexpComponentLength @Override public int lineCount() { - return this.counts.stream().mapToInt(x -> x).sum(); + return this.counts.getFirst(); } @Override diff --git a/zkevm-constraints b/zkevm-constraints index 74923d612f..5cd66bce1d 160000 --- a/zkevm-constraints +++ b/zkevm-constraints @@ -1 +1 @@ -Subproject commit 74923d612fe978b9905daacfe064c9a286306e7c +Subproject commit 5cd66bce1de7a64c7221d18b529e13f3f2fe1316 From 9763fb5f90c3d75bda733301070a71d668669a0e Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Fri, 31 May 2024 10:58:20 +0300 Subject: [PATCH 11/39] feat(tx-init): initial implementation of TX_INIT phase (#666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(tx-init): initial implementation of TX_INIT phase * fix: delete unneeded files * fix: wrong method call * fix: naming rebase issue * fix(hub): prewarmed address in prewarming phase * style: spotless --------- Co-authored-by: Francois Bojarski Co-authored-by: François Bojarski <54240434+letypequividelespoubelles@users.noreply.github.com> --- .../zktracer/module/hub/AccountSnapshot.java | 97 +++++-- .../linea/zktracer/module/hub/Hub.java | 269 +++++++++++------- .../linea/zktracer/module/hub/State.java | 2 +- .../zktracer/module/hub/TransactionStack.java | 2 + .../defer/SkippedPostTransactionDefer.java | 14 +- .../module/hub/fragment/AccountFragment.java | 70 ++--- .../imc/call/mmu/opcode/CodeCopy.java | 2 +- .../fragment/imc/call/mmu/opcode/Create.java | 2 +- .../fragment/imc/call/mmu/opcode/Create2.java | 2 +- .../imc/call/mmu/opcode/ExtCodeCopy.java | 2 +- .../call/mmu/opcode/ReturnFromDeployment.java | 2 +- .../module/hub/section/CreateSection.java | 2 +- ...action.java => EndTransactionSection.java} | 4 +- .../linea/zktracer/module/rlptxn/RlpTxn.java | 2 +- .../linea/zktracer/module/rom/Rom.java | 8 +- .../linea/zktracer/module/romlex/RomLex.java | 2 +- .../zktracer/module/txndata/TxnData.java | 3 +- .../linea/zktracer/types/AddressUtils.java | 2 +- 18 files changed, 300 insertions(+), 187 deletions(-) rename arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/{EndTransaction.java => EndTransactionSection.java} (87%) diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/AccountSnapshot.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/AccountSnapshot.java index 0f2ad97dfd..8db53c9a9a 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/AccountSnapshot.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/AccountSnapshot.java @@ -17,32 +17,60 @@ import java.util.Optional; +import com.google.common.base.Preconditions; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.consensys.linea.zktracer.types.Bytecode; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.evm.account.Account; -public record AccountSnapshot( - Address address, - long nonce, - Wei balance, - boolean warm, - Bytecode code, - int deploymentNumber, - boolean deploymentStatus) { +@AllArgsConstructor +@Getter +@Setter +@Accessors(fluent = true) +public class AccountSnapshot { + private Address address; + private long nonce; + private Wei balance; + private boolean isWarm; + private Bytecode code; + private int deploymentNumber; + private boolean deploymentStatus; + + public AccountSnapshot decrementBalance(Wei quantity) { + Preconditions.checkState( + this.balance.greaterOrEqualThan(quantity), + "Insufficient balance: %s".formatted(this.balance)); + this.balance = this.balance.subtract(quantity); + return this; + } + + public AccountSnapshot incrementBalance(Wei quantity) { + this.balance = this.balance.add(quantity); + return this; + } + + public AccountSnapshot incrementNonce() { + this.nonce++; + return this; + } + public static AccountSnapshot fromAccount( - Account account, boolean warm, int deploymentNumber, boolean deploymentStatus) { - return fromAccount(Optional.ofNullable(account), warm, deploymentNumber, deploymentStatus); + Account account, boolean isWarm, int deploymentNumber, boolean deploymentStatus) { + return fromAccount(Optional.ofNullable(account), isWarm, deploymentNumber, deploymentStatus); } public static AccountSnapshot empty( - boolean warm, int deploymentNumber, boolean deploymentStatus) { + boolean isWarm, int deploymentNumber, boolean deploymentStatus) { return new AccountSnapshot( - Address.ZERO, 0, Wei.ZERO, warm, Bytecode.EMPTY, deploymentNumber, deploymentStatus); + Address.ZERO, 0, Wei.ZERO, isWarm, Bytecode.EMPTY, deploymentNumber, deploymentStatus); } public static AccountSnapshot fromAccount( - Optional account, boolean warm, int deploymentNumber, boolean deploymentStatus) { + Optional account, boolean isWarm, int deploymentNumber, boolean deploymentStatus) { return account .map( @@ -51,11 +79,11 @@ public static AccountSnapshot fromAccount( a.getAddress(), a.getNonce(), a.getBalance().copy(), - warm, + isWarm, new Bytecode(a.getCode().copy()), deploymentNumber, deploymentStatus)) - .orElseGet(() -> AccountSnapshot.empty(warm, deploymentNumber, deploymentStatus)); + .orElseGet(() -> AccountSnapshot.empty(isWarm, deploymentNumber, deploymentStatus)); } public AccountSnapshot debit(Wei quantity) { @@ -63,7 +91,18 @@ public AccountSnapshot debit(Wei quantity) { this.address, this.nonce + 1, this.balance.subtract(quantity), - this.warm, + this.isWarm, + this.code, + this.deploymentNumber, + this.deploymentStatus); + } + + public AccountSnapshot debit(Wei quantity, boolean isWarm) { + return new AccountSnapshot( + this.address, + this.nonce + 1, + this.balance.subtract(quantity), + isWarm, this.code, this.deploymentNumber, this.deploymentStatus); @@ -74,12 +113,25 @@ public AccountSnapshot deploy(Wei value) { this.address, this.nonce + 1, this.balance.add(value), - this.warm, + this.isWarm, this.code, this.deploymentNumber + 1, this.deploymentStatus); } + public AccountSnapshot deploy(Wei value, Bytecode code) { + Preconditions.checkState( + !this.deploymentStatus, "Deployment status should be false before deploying."); + return new AccountSnapshot( + this.address, + this.nonce + 1, + this.balance.add(value), + true, + code, + this.deploymentNumber + 1, + true); + } + public AccountSnapshot credit(Wei value) { return new AccountSnapshot( this.address, @@ -90,4 +142,15 @@ public AccountSnapshot credit(Wei value) { this.deploymentNumber, this.deploymentStatus); } + + public AccountSnapshot credit(Wei value, boolean isWarm) { + return new AccountSnapshot( + this.address, + this.nonce, + this.balance.add(value), + isWarm, + this.code, + this.deploymentNumber, + this.deploymentStatus); + } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java index c5829f66bb..61eb832f07 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/Hub.java @@ -17,6 +17,7 @@ import static net.consensys.linea.zktracer.types.AddressUtils.effectiveToAddress; import static net.consensys.linea.zktracer.types.AddressUtils.isPrecompile; +import static net.consensys.linea.zktracer.types.AddressUtils.precompileAddress; import java.nio.MappedByteBuffer; import java.util.ArrayList; @@ -28,6 +29,7 @@ import java.util.Set; import java.util.stream.Stream; +import com.google.common.base.Preconditions; import lombok.Getter; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; @@ -50,6 +52,7 @@ import net.consensys.linea.zktracer.module.hub.precompiles.PrecompileInvocation; import net.consensys.linea.zktracer.module.hub.section.*; import net.consensys.linea.zktracer.module.hub.signals.PlatformController; +import net.consensys.linea.zktracer.module.hub.transients.DeploymentInfo; import net.consensys.linea.zktracer.module.hub.transients.Transients; import net.consensys.linea.zktracer.module.limits.Keccak; import net.consensys.linea.zktracer.module.limits.L2Block; @@ -119,6 +122,7 @@ @Slf4j @Accessors(fluent = true) public class Hub implements Module { + private static final int TAU = 8; public static final GasProjector GAS_PROJECTOR = new GasProjector(); @@ -417,45 +421,40 @@ public List getModulesToCount() { * @param world a view onto the state */ void processStateSkip(WorldView world) { - this.state.stamps().stampHubIncrements(); - boolean isDeployment = this.transients.tx().besuTx().getTo().isEmpty(); + this.state.stamps().incrementHubStamp(); + final boolean isDeployment = this.transients.tx().besuTx().getTo().isEmpty(); // // 3 sections -- account changes // // From account information - Address fromAddress = this.transients.tx().besuTx().getSender(); - AccountSnapshot oldFromAccount = + final Address fromAddress = this.transients.tx().besuTx().getSender(); + final AccountSnapshot oldFromAccount = AccountSnapshot.fromAccount( world.get(fromAddress), - false, + isPrecompile(fromAddress), this.transients.conflation().deploymentInfo().number(fromAddress), false); // To account information - Address toAddress = effectiveToAddress(this.transients.tx().besuTx()); + final Address toAddress = effectiveToAddress(this.transients.tx().besuTx()); if (isDeployment) { this.transients.conflation().deploymentInfo().deploy(toAddress); } - boolean toIsWarm = - (fromAddress == toAddress) - || isPrecompile(toAddress); // should never happen – no TX to PC allowed - AccountSnapshot oldToAccount = + final AccountSnapshot oldToAccount = AccountSnapshot.fromAccount( world.get(toAddress), - toIsWarm, + isPrecompile(toAddress), this.transients.conflation().deploymentInfo().number(toAddress), false); // Miner account information - boolean minerIsWarm = - (this.transients.block().minerAddress() == fromAddress) - || (this.transients.block().minerAddress() == toAddress) - || isPrecompile(this.transients.block().minerAddress()); - AccountSnapshot oldMinerAccount = + final Address minerAddress = this.transients.block().minerAddress(); + + final AccountSnapshot oldMinerAccount = AccountSnapshot.fromAccount( - world.get(this.transients.block().minerAddress()), - minerIsWarm, + world.get(minerAddress), + isPrecompile(minerAddress), this.transients .conflation() .deploymentInfo() @@ -473,7 +472,7 @@ void processStateSkip(WorldView world) { } /** - * Traces the warm-up information of a transaction + * Traces the isWarm-up information of a transaction * * @param world a view onto the state */ @@ -485,38 +484,75 @@ void processStateWarm(WorldView world) { .ifPresent( preWarmed -> { if (!preWarmed.isEmpty()) { - this.state.stamps().stampHubIncrements(); + Set
seenAddresses = new HashSet<>(precompileAddress); + this.state.stamps().incrementHubStamp(); - Set
seenAddresses = new HashSet<>(); Map> seenKeys = new HashMap<>(); List fragments = new ArrayList<>(); + final TransactionStack.MetaTransaction tx = this.transients.tx(); + final Transaction besuTx = tx.besuTx(); + final Address senderAddress = besuTx.getSender(); + final Address receiverAddress = effectiveToAddress(besuTx); + for (AccessListEntry entry : preWarmed) { - Address address = entry.address(); - AccountSnapshot snapshot = + this.state.stamps().incrementHubStamp(); + + final Address address = entry.address(); + if (senderAddress.equals(address)) { + tx.isSenderPreWarmed(true); + } + + if (receiverAddress.equals(address)) { + tx.isReceiverPreWarmed(true); + } + + final DeploymentInfo deploymentInfo = + this.transients.conflation().deploymentInfo(); + + final int deploymentNumber = deploymentInfo.number(address); + Preconditions.checkArgument( + !deploymentInfo.isDeploying(address), + "Deployment status during TX_INIT phase of any address should always be false"); + + final boolean isAccountWarm = seenAddresses.contains(address); + final AccountSnapshot preWarmingAccountSnapshot = AccountSnapshot.fromAccount( - world.get(address), seenAddresses.contains(address), 0, false); + world.get(address), isAccountWarm, deploymentNumber, false); + + final AccountSnapshot postWarmingAccountSnapshot = + AccountSnapshot.fromAccount( + world.get(address), true, deploymentNumber, false); + fragments.add( - this.factories.accountFragment().make(snapshot, snapshot, false, 0, false)); + this.factories + .accountFragment() + .makeWithTrm( + preWarmingAccountSnapshot, postWarmingAccountSnapshot, address)); + seenAddresses.add(address); List keys = entry.storageKeys(); - for (Bytes32 key_ : keys) { - UInt256 key = UInt256.fromBytes(key_); - EWord value = + for (Bytes32 k : keys) { + this.state.stamps().incrementHubStamp(); + + final UInt256 key = UInt256.fromBytes(k); + final EWord value = Optional.ofNullable(world.get(address)) .map(account -> EWord.of(account.getStorageValue(key))) .orElse(EWord.ZERO); + fragments.add( new StorageFragment( address, - this.transients.conflation().deploymentInfo().number(address), + deploymentInfo.number(address), EWord.of(key), value, value, value, seenKeys.computeIfAbsent(address, x -> new HashSet<>()).contains(key), true)); + seenKeys.get(address).add(key); } } @@ -533,71 +569,83 @@ void processStateWarm(WorldView world) { * @param world a view onto the state */ void processStateInit(WorldView world) { - this.state.stamps().stampHubIncrements(); - final boolean isDeployment = this.transients.tx().besuTx().getTo().isEmpty(); - final Address toAddress = effectiveToAddress(this.transients.tx().besuTx()); - if (isDeployment) { - this.transients.conflation().deploymentInfo().deploy(toAddress); - } + this.state.stamps().incrementHubStamp(); + final TransactionStack.MetaTransaction tx = this.transients.tx(); + final boolean isDeployment = tx.besuTx().getTo().isEmpty(); + final Address toAddress = effectiveToAddress(tx.besuTx()); + final DeploymentInfo deploymentInfo = this.transients.conflation().deploymentInfo(); - final Address fromAddress = this.transients.tx().besuTx().getSender(); + final Address fromAddress = tx.besuTx().getSender(); final Account fromAccount = world.get(fromAddress); - final AccountSnapshot fromSnapshot = + final AccountSnapshot preInitFromSnapshot = AccountSnapshot.fromAccount( fromAccount, - true, - this.transients.conflation().deploymentInfo().number(fromAddress), - this.transients.conflation().deploymentInfo().isDeploying(fromAddress)); - - final Account toAccount = world.get(toAddress); - final AccountSnapshot toSnapshot = - AccountSnapshot.fromAccount( - toAccount, - true, - this.transients.conflation().deploymentInfo().number(toAddress), - this.transients.conflation().deploymentInfo().isDeploying(toAddress)); + tx.isSenderPreWarmed(), + deploymentInfo.number(fromAddress), + deploymentInfo.isDeploying(fromAddress)); final Wei transactionGasPrice = ZkTracer.feeMarket .getTransactionPriceCalculator() .price( - (org.hyperledger.besu.ethereum.core.Transaction) this.transients.tx().besuTx(), + (org.hyperledger.besu.ethereum.core.Transaction) tx.besuTx(), Optional.of(this.transients.block().baseFee())); - final Wei value = (Wei) this.transients.tx().besuTx().getValue(); - final AccountSnapshot fromPostDebitSnapshot = - fromSnapshot.debit( - transactionGasPrice.multiply(this.transients.tx().besuTx().getGasLimit()).add(value)); + final Wei value = (Wei) tx.besuTx().getValue(); + final AccountSnapshot postInitFromSnapshot = + preInitFromSnapshot.debit( + transactionGasPrice.multiply(tx.besuTx().getGasLimit()).add(value), true); final boolean isSelfCredit = toAddress.equals(fromAddress); + + final Account toAccount = world.get(toAddress); + + final AccountSnapshot preInitToSnapshot = + isSelfCredit + ? postInitFromSnapshot + : AccountSnapshot.fromAccount( + toAccount, + tx.isReceiverPreWarmed(), + deploymentInfo.number(toAddress), + deploymentInfo.isDeploying(toAddress)); + + if (isDeployment) { + deploymentInfo.deploy(toAddress); + } + + final Bytecode initBytecode = new Bytecode(tx.besuTx().getInit().orElse(Bytes.EMPTY)); + final AccountSnapshot postInitToSnapshot = + isDeployment + ? preInitToSnapshot.deploy(value, initBytecode) + : preInitToSnapshot.credit(value, true); + final TransactionFragment txFragment = TransactionFragment.prepare( this.transients.conflation().number(), this.transients.block().minerAddress(), - this.transients.tx().besuTx(), + tx.besuTx(), true, - ((org.hyperledger.besu.ethereum.core.Transaction) this.transients.tx().besuTx()) + ((org.hyperledger.besu.ethereum.core.Transaction) tx.besuTx()) .getEffectiveGasPrice(Optional.ofNullable(this.transients().block().baseFee())), this.transients.block().baseFee(), 0 // TODO: find getInitialGas ); this.defers.postTx(txFragment); + final AccountFragment.AccountFragmentFactory accountFragmentFactory = + this.factories.accountFragment(); + this.addTraceSection( new TxInitSection( this, - this.factories.accountFragment().make(fromSnapshot, fromPostDebitSnapshot), - isDeployment - ? this.factories.accountFragment().make(toSnapshot, toSnapshot.deploy(value)) - : (isSelfCredit - ? this.factories - .accountFragment() - .make(fromPostDebitSnapshot, fromPostDebitSnapshot.credit(value)) - : this.factories.accountFragment().make(toSnapshot, toSnapshot.credit(value))), + accountFragmentFactory.make(preInitFromSnapshot, postInitFromSnapshot), + accountFragmentFactory + .make(preInitToSnapshot, postInitToSnapshot) + .requiresCodeFragmentIndex(true), ImcFragment.forTxInit(this), ContextFragment.initializeExecutionContext(this), txFragment)); - this.transients.tx().state(TxState.TX_EXEC); + tx.state(TxState.TX_EXEC); } public CallFrame currentFrame() { @@ -677,7 +725,8 @@ void triggerModules(MessageFrame frame) { void processStateExec(MessageFrame frame) { this.currentFrame().frame(frame); - this.state.stamps().stampHubIncrements(); + this.state.stamps().incrementHubStamp(); + this.pch.setup(frame); this.state.stamps().stampSubmodules(this.pch()); @@ -698,38 +747,52 @@ void processStateExec(MessageFrame frame) { void processStateFinal(WorldView worldView, Transaction tx, boolean isSuccess) { this.transients().tx().state(TxState.TX_FINAL); - this.state.stamps().stampHubIncrements(); + this.state.stamps().incrementHubStamp(); - Address fromAddress = this.transients.tx().besuTx().getSender(); - Account fromAccount = worldView.get(fromAddress); - AccountSnapshot fromSnapshot = + final Address fromAddress = this.transients.tx().besuTx().getSender(); + final Account fromAccount = worldView.get(fromAddress); + final DeploymentInfo deploymentInfo = this.transients.conflation().deploymentInfo(); + final AccountSnapshot preFinalFromSnapshot = AccountSnapshot.fromAccount( fromAccount, true, - this.transients.conflation().deploymentInfo().number(fromAddress), - this.transients.conflation().deploymentInfo().isDeploying(fromAddress)); + deploymentInfo.number(fromAddress), + deploymentInfo.isDeploying(fromAddress)); + + // TODO: still no finished + final AccountSnapshot postFinalFromSnapshot = + AccountSnapshot.fromAccount( + fromAccount, + true, + deploymentInfo.number(fromAddress), + deploymentInfo.isDeploying(fromAddress)); Account minerAccount = worldView.get(this.transients.block().minerAddress()); - AccountSnapshot minerSnapshot = + AccountSnapshot preFinalCoinbaseSnapshot = AccountSnapshot.fromAccount( minerAccount, true, - this.transients - .conflation() - .deploymentInfo() - .number(this.transients.block().minerAddress()), - this.transients - .conflation() - .deploymentInfo() - .isDeploying(this.transients.block().minerAddress())); + deploymentInfo.number(this.transients.block().minerAddress()), + deploymentInfo.isDeploying(this.transients.block().minerAddress())); + + // TODO: still not finished + AccountSnapshot postFinalCoinbaseSnapshot = + AccountSnapshot.fromAccount( + minerAccount, + true, + deploymentInfo.number(this.transients.block().minerAddress()), + deploymentInfo.isDeploying(this.transients.block().minerAddress())); + + final AccountFragment.AccountFragmentFactory accountFragmentFactory = + this.factories.accountFragment(); if (isSuccess) { // if no revert: 2 account rows (sender, coinbase) + 1 tx row this.addTraceSection( - new EndTransaction( + new EndTransactionSection( this, - this.factories.accountFragment().make(fromSnapshot, fromSnapshot, false, 0, false), - this.factories.accountFragment().make(minerSnapshot, minerSnapshot, false, 0, false), + accountFragmentFactory.make(preFinalFromSnapshot, postFinalFromSnapshot), + accountFragmentFactory.make(preFinalCoinbaseSnapshot, postFinalCoinbaseSnapshot), TransactionFragment.prepare( this.transients.conflation().number(), this.transients.block().minerAddress(), @@ -747,19 +810,26 @@ void processStateFinal(WorldView worldView, Transaction tx, boolean isSuccess) { // otherwise 4 account rows (sender, coinbase, sender, recipient) + 1 tx row Address toAddress = this.transients.tx().besuTx().getSender(); Account toAccount = worldView.get(toAddress); - AccountSnapshot toSnapshot = + AccountSnapshot preFinalToSnapshot = + AccountSnapshot.fromAccount( + toAccount, + true, + deploymentInfo.number(toAddress), + deploymentInfo.isDeploying(toAddress)); + + // TODO: still not finished + AccountSnapshot postFinalToSnapshot = AccountSnapshot.fromAccount( toAccount, true, - this.transients.conflation().deploymentInfo().number(toAddress), - this.transients.conflation().deploymentInfo().isDeploying(toAddress)); + deploymentInfo.number(toAddress), + deploymentInfo.isDeploying(toAddress)); this.addTraceSection( - new EndTransaction( + new EndTransactionSection( this, - this.factories.accountFragment().make(fromSnapshot, fromSnapshot, false, 0, false), - this.factories.accountFragment().make(minerSnapshot, minerSnapshot, false, 0, false), - this.factories.accountFragment().make(fromSnapshot, fromSnapshot, false, 0, false), - this.factories.accountFragment().make(toSnapshot, toSnapshot, false, 0, false))); + accountFragmentFactory.make(preFinalFromSnapshot, postFinalFromSnapshot), + accountFragmentFactory.make(preFinalToSnapshot, postFinalToSnapshot), + accountFragmentFactory.make(preFinalCoinbaseSnapshot, postFinalCoinbaseSnapshot))); } } @@ -781,7 +851,9 @@ public void traceStartTx(final WorldView world, final Transaction tx) { this.enterTransaction(); - if (this.transients.tx().shouldSkip(world)) { + if (this.transients + .tx() + .shouldSkip(world)) /* TODO: should use requiresEvmExecution instead of recomputing it */ { this.transients.tx().state(TxState.TX_SKIP); this.processStateSkip(world); } else { @@ -1234,7 +1306,7 @@ void traceOperation(MessageFrame frame) { this.currentFrame(), this.factories .accountFragment() - .makeWithTrm(accountSnapshot, accountSnapshot, false, 0, false, rawTargetAddress)); + .makeWithTrm(accountSnapshot, accountSnapshot, rawTargetAddress)); this.addTraceSection(accountSection); } @@ -1263,11 +1335,8 @@ void traceOperation(MessageFrame frame) { this.currentFrame().opCode() == OpCode.EXTCODECOPY ? this.factories .accountFragment() - .makeWithTrm( - accountSnapshot, accountSnapshot, false, 0, false, rawTargetAddress) - : this.factories - .accountFragment() - .make(accountSnapshot, accountSnapshot, false, 0, false)); + .makeWithTrm(accountSnapshot, accountSnapshot, rawTargetAddress) + : this.factories.accountFragment().make(accountSnapshot, accountSnapshot)); } else { copySection.addFragment( this, this.currentFrame(), ContextFragment.readContextData(callStack)); @@ -1491,9 +1560,7 @@ void traceOperation(MessageFrame frame) { new JumpSection( this, ContextFragment.readContextData(callStack), - this.factories - .accountFragment() - .make(codeAccountSnapshot, codeAccountSnapshot, false, 0, false), + this.factories.accountFragment().make(codeAccountSnapshot, codeAccountSnapshot), ImcFragment.forOpcode(this, frame)); this.addTraceSection(jumpSection); diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/State.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/State.java index ded291b987..0ed1f507cc 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/State.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/State.java @@ -131,7 +131,7 @@ Stamps spinOff() { return new Stamps(this.hub, this.mmu, this.mxp, this.hashInfo); } - void stampHubIncrements() { + void incrementHubStamp() { this.hub++; } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/TransactionStack.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/TransactionStack.java index aa7df38a28..eee07b4d60 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/TransactionStack.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/TransactionStack.java @@ -111,6 +111,8 @@ public static class MetaTransaction { @Getter private long initialGas; @Getter private final StorageInitialValues storage = new StorageInitialValues(); @Getter @Setter @Builder.Default int endStamp = -1; + @Getter @Setter boolean isSenderPreWarmed; + @Getter @Setter boolean isReceiverPreWarmed; @Getter @Setter boolean requiresEvmExecution; /** diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/defer/SkippedPostTransactionDefer.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/defer/SkippedPostTransactionDefer.java index c938d57fe7..cab82eb200 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/defer/SkippedPostTransactionDefer.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/defer/SkippedPostTransactionDefer.java @@ -50,21 +50,21 @@ public void runPostTx(Hub hub, WorldView state, Transaction tx, boolean isSucces AccountSnapshot newFromAccount = AccountSnapshot.fromAccount( state.get(fromAddress), - true, + this.oldFromAccount.isWarm(), hub.transients().conflation().deploymentInfo().number(fromAddress), false); AccountSnapshot newToAccount = AccountSnapshot.fromAccount( state.get(toAddress), - true, + this.oldToAccount.isWarm(), hub.transients().conflation().deploymentInfo().number(toAddress), false); AccountSnapshot newMinerAccount = AccountSnapshot.fromAccount( state.get(minerAddress), - true, + this.oldMinerAccount.isWarm(), hub.transients().conflation().deploymentInfo().number(minerAddress), false); @@ -74,13 +74,11 @@ public void runPostTx(Hub hub, WorldView state, Transaction tx, boolean isSucces hub, // 3 lines -- account changes // From - hub.factories().accountFragment().make(oldFromAccount, newFromAccount, false, 0, false), + hub.factories().accountFragment().make(oldFromAccount, newFromAccount), // To - hub.factories().accountFragment().make(oldToAccount, newToAccount, false, 0, false), + hub.factories().accountFragment().make(oldToAccount, newToAccount), // Miner - hub.factories() - .accountFragment() - .make(oldMinerAccount, newMinerAccount, false, 0, false), + hub.factories().accountFragment().make(oldMinerAccount, newMinerAccount), // 1 line -- transaction data TransactionFragment.prepare( diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/AccountFragment.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/AccountFragment.java index 2509c45a41..584acb09df 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/AccountFragment.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/AccountFragment.java @@ -29,14 +29,18 @@ import net.consensys.linea.zktracer.module.hub.Trace; import net.consensys.linea.zktracer.module.hub.defer.DeferRegistry; import net.consensys.linea.zktracer.module.hub.defer.PostConflationDefer; +import net.consensys.linea.zktracer.module.hub.defer.PostTransactionDefer; import net.consensys.linea.zktracer.types.EWord; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Transaction; import org.hyperledger.besu.evm.worldstate.WorldView; @Accessors(fluent = true) -public final class AccountFragment implements TraceFragment, PostConflationDefer { +public final class AccountFragment + implements TraceFragment, PostTransactionDefer, PostConflationDefer { + /** * {@link AccountFragment} creation requires access to a {@link DeferRegistry} for post-conflation * data gathering, which is provided by this factory. @@ -53,63 +57,31 @@ public AccountFragment makeWithTrm( AccountSnapshot oldState, AccountSnapshot newState, Bytes toTrim) { return new AccountFragment(this.defers, oldState, newState, Optional.of(toTrim)); } - - public AccountFragment make( - AccountSnapshot oldState, - AccountSnapshot newState, - boolean debit, - long cost, - boolean createAddress) { - return new AccountFragment( - this.defers, oldState, newState, debit, cost, createAddress, Optional.empty()); - } - - public AccountFragment makeWithTrm( - AccountSnapshot oldState, - AccountSnapshot newState, - boolean debit, - long cost, - boolean createAddress, - final Bytes addressToTrim) { - return new AccountFragment( - this.defers, oldState, newState, debit, cost, createAddress, Optional.of(addressToTrim)); - } } @Getter private final Address who; private final AccountSnapshot oldState; private final AccountSnapshot newState; - private final boolean debit; - private final long cost; - private final boolean createAddress; - @Setter private int deploymentNumberInfnty = 0; // retconned on conflation end + @Setter private int deploymentNumberInfinity = 0; // retconned on conflation end + private final int deploymentNumber; + private final boolean isDeployment; @Setter private boolean existsInfinity = false; // retconned on conflation end + private int codeFragmentIndex; + @Setter private boolean requiresCodeFragmentIndex; private final Optional addressToTrim; - private AccountFragment( - final DeferRegistry defers, - AccountSnapshot oldState, - AccountSnapshot newState, - Optional addressToTrim) { - this(defers, oldState, newState, false, 0, false, addressToTrim); - } - public AccountFragment( final DeferRegistry defers, AccountSnapshot oldState, AccountSnapshot newState, - boolean debit, - long cost, - boolean createAddress, Optional addressToTrim) { Preconditions.checkArgument(oldState.address().equals(newState.address())); this.who = oldState.address(); this.oldState = oldState; this.newState = newState; - this.debit = debit; - this.cost = cost; - this.createAddress = createAddress; + this.deploymentNumber = newState.deploymentNumber(); + this.isDeployment = newState.deploymentStatus(); this.addressToTrim = addressToTrim; defers.postConflation(this); @@ -140,6 +112,7 @@ public Trace trace(Trace trace) { .pAccountCodeHashLoNew(eCodeHashNew.lo()) .pAccountHasCode(oldState.code().getCodeHash() != Hash.EMPTY) .pAccountHasCodeNew(newState.code().getCodeHash() != Hash.EMPTY) + .pAccountCodeFragmentIndex(Bytes.of(this.codeFragmentIndex)) .pAccountExists( oldState.nonce() > 0 || oldState.code().getCodeHash() != Hash.EMPTY @@ -148,19 +121,28 @@ public Trace trace(Trace trace) { newState.nonce() > 0 || newState.code().getCodeHash() != Hash.EMPTY || !newState.balance().isZero()) - .pAccountWarmth(oldState.warm()) - .pAccountWarmthNew(newState.warm()) + .pAccountWarmth(oldState.isWarm()) + .pAccountWarmthNew(newState.isWarm()) .pAccountDeploymentNumber(Bytes.ofUnsignedInt(oldState.deploymentNumber())) .pAccountDeploymentNumberNew(Bytes.ofUnsignedInt(newState.deploymentNumber())) - .pAccountDeploymentNumberInfty(Bytes.ofUnsignedInt(deploymentNumberInfnty)) + .pAccountDeploymentNumberInfty(Bytes.ofUnsignedInt(deploymentNumberInfinity)) .pAccountDeploymentStatus(oldState.deploymentStatus()) .pAccountDeploymentStatusNew(newState.deploymentStatus()) .pAccountDeploymentStatusInfty(existsInfinity); } + @Override + public void runPostTx(Hub hub, WorldView state, Transaction tx, boolean isSuccessful) {} + @Override public void runPostConflation(Hub hub, WorldView world) { - this.deploymentNumberInfnty = hub.transients().conflation().deploymentInfo().number(this.who); + this.deploymentNumberInfinity = hub.transients().conflation().deploymentInfo().number(this.who); this.existsInfinity = world.get(this.who) != null; + // this.codeFragmentIndex = + // this.requiresCodeFragmentIndex + // ? hub.romLex() + // .getCodeFragmentIndexByMetadata( + // ContractMetadata.make(this.who, this.deploymentNumber, this.isDeployment)) + // : 0; } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/CodeCopy.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/CodeCopy.java index c3a7da67ff..82cbaf0b59 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/CodeCopy.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/CodeCopy.java @@ -46,6 +46,6 @@ public CodeCopy(final Hub hub) { @Override public int sourceId() { - return this.hub.romLex().getCfiByMetadata(this.contract); + return this.hub.romLex().getCodeFragmentIndexByMetadata(this.contract); } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/Create.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/Create.java index 0465274565..3f249dc42b 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/Create.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/Create.java @@ -46,7 +46,7 @@ public Create(final Hub hub) { @Override public int targetId() { - return this.hub.romLex().getCfiByMetadata(this.contract); + return this.hub.romLex().getCodeFragmentIndexByMetadata(this.contract); } @Override diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/Create2.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/Create2.java index f5a4bb5251..3a18430a54 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/Create2.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/Create2.java @@ -48,7 +48,7 @@ public Create2(final Hub hub) { @Override public int targetId() { - return this.hub.romLex().getCfiByMetadata(this.contract); + return this.hub.romLex().getCodeFragmentIndexByMetadata(this.contract); } @Override diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/ExtCodeCopy.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/ExtCodeCopy.java index c8cf76648c..ddf029ed3d 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/ExtCodeCopy.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/ExtCodeCopy.java @@ -52,7 +52,7 @@ public ExtCodeCopy(final Hub hub) { @Override public int sourceId() { try { - return this.hub.romLex().getCfiByMetadata(this.contract); + return this.hub.romLex().getCodeFragmentIndexByMetadata(this.contract); } catch (Exception ignored) { // Triggered if the external bytecode is empty, and thus absent from the ROMLex. return 0; diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/ReturnFromDeployment.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/ReturnFromDeployment.java index 27a07c5c9c..ebd450df88 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/ReturnFromDeployment.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/fragment/imc/call/mmu/opcode/ReturnFromDeployment.java @@ -55,6 +55,6 @@ public ReturnFromDeployment(final Hub hub) { @Override public int targetId() { - return this.hub.romLex().getCfiByMetadata(this.contract); + return this.hub.romLex().getCodeFragmentIndexByMetadata(this.contract); } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/CreateSection.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/CreateSection.java index 831f789220..d742a1c751 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/CreateSection.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/CreateSection.java @@ -171,7 +171,7 @@ public void runPostTx(Hub hub, WorldView state, Transaction tx, boolean isSucces EWord.of(this.initialGas), EWord.ZERO, false, - oldCreatedSnapshot.warm(), + oldCreatedSnapshot.isWarm(), this.exceptions.outOfGas(), upfrontCost, allButOneSixtyFourth(this.initialGas - upfrontCost), diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/EndTransaction.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/EndTransactionSection.java similarity index 87% rename from arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/EndTransaction.java rename to arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/EndTransactionSection.java index 0887558454..f95adc442e 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/EndTransaction.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/section/EndTransactionSection.java @@ -18,8 +18,8 @@ import net.consensys.linea.zktracer.module.hub.Hub; import net.consensys.linea.zktracer.module.hub.fragment.TraceFragment; -public class EndTransaction extends TraceSection { - public EndTransaction(Hub hub, TraceFragment... fragments) { +public class EndTransactionSection extends TraceSection { + public EndTransactionSection(Hub hub, TraceFragment... fragments) { this.addFragmentsWithoutStack(hub, fragments); } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlptxn/RlpTxn.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlptxn/RlpTxn.java index 4c2bed32f0..d917ab0d5d 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlptxn/RlpTxn.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rlptxn/RlpTxn.java @@ -140,7 +140,7 @@ public void traceChunk(RlpTxnChunk chunk, int absTxNum, Trace trace) { traceValue.requiresEvmExecution = chunk.requireEvmExecution(); traceValue.codeFragmentIndex = chunk.tx().getTo().isEmpty() && chunk.requireEvmExecution() - ? this.romLex.getCfiByMetadata( + ? this.romLex.getCodeFragmentIndexByMetadata( ContractMetadata.underDeployment( Address.contractAddress(chunk.tx().getSender(), chunk.tx().getNonce()), 1)) : 0; diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rom/Rom.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rom/Rom.java index 1aadfe882d..8fc7e21896 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rom/Rom.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/rom/Rom.java @@ -53,11 +53,11 @@ public List columnsHeaders() { public void commit(List buffers) { final Trace trace = new Trace(buffers); - int cfi = 0; - final int cfiInfty = this.romLex.sortedChunks().size(); + int codeFragmentIndex = 0; + final int codeFragmentIndexInfinity = this.romLex.sortedChunks().size(); for (RomChunk chunk : this.romLex.sortedChunks()) { - cfi += 1; - chunk.trace(trace, cfi, cfiInfty); + codeFragmentIndex += 1; + chunk.trace(trace, codeFragmentIndex, codeFragmentIndexInfinity); } } } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomLex.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomLex.java index ae53f3ba30..757141c57e 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomLex.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/romlex/RomLex.java @@ -74,7 +74,7 @@ public void popTransaction() { this.chunks.pop(); } - public int getCfiByMetadata(final ContractMetadata metadata) { + public int getCodeFragmentIndexByMetadata(final ContractMetadata metadata) { if (this.sortedChunks.isEmpty()) { throw new RuntimeException("Chunks have not been sorted yet"); } diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/txndata/TxnData.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/txndata/TxnData.java index f875377e48..015855c7fe 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/txndata/TxnData.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/txndata/TxnData.java @@ -160,7 +160,8 @@ private void traceTx( final long coinbaseLo = coinbase.hi().trimLeadingZeros().toLong(); final int codeFragmentIndex = tx.isDeployment() && tx.requiresEvmExecution() - ? this.romLex.getCfiByMetadata(ContractMetadata.underDeployment(tx.to(), 1)) + ? this.romLex.getCodeFragmentIndexByMetadata( + ContractMetadata.underDeployment(tx.to(), 1)) : 0; final boolean copyTxCd = tx.requiresEvmExecution() && tx.callDataSize() != 0; final long fromHi = from.hi().slice(12, 4).toLong(); diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/types/AddressUtils.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/types/AddressUtils.java index b681bf15d5..71123aef60 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/types/AddressUtils.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/types/AddressUtils.java @@ -29,7 +29,7 @@ public class AddressUtils { private static final Bytes CREATE2_PREFIX = Bytes.of(0xff); - private static final List
precompileAddress = + public static final List
precompileAddress = List.of( Address.ECREC, Address.SHA256, From 14b7ea55320dfb5e78814e3deafb0341e826135c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Bojarski?= <54240434+letypequividelespoubelles@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:51:08 +0400 Subject: [PATCH 12/39] feat(gas): implementation module whithout triggering (#750) --------- Co-authored-by: Lorenzo Gentile --- .../linea/zktracer/module/gas/Gas.java | 74 +++++ .../zktracer/module/gas/GasOperation.java | 65 ++++ .../zktracer/module/gas/GasParameters.java | 20 ++ .../linea/zktracer/module/gas/Trace.java | 292 ++++++++++++++++++ gradle/trace-files.gradle | 2 +- zkevm-constraints | 2 +- 6 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/Gas.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/GasOperation.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/GasParameters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/Trace.java diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/Gas.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/Gas.java new file mode 100644 index 0000000000..4d40ee9121 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/Gas.java @@ -0,0 +1,74 @@ +/* + * 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.module.gas; + +import java.math.BigInteger; +import java.nio.MappedByteBuffer; +import java.util.List; + +import net.consensys.linea.zktracer.ColumnHeader; +import net.consensys.linea.zktracer.container.stacked.list.StackedList; +import net.consensys.linea.zktracer.module.Module; +import org.hyperledger.besu.evm.frame.MessageFrame; + +public class Gas implements Module { + /** A list of the operations to trace */ + private final StackedList chunks = new StackedList<>(); + + @Override + public String moduleKey() { + return "GAS"; + } + + @Override + public void enterTransaction() { + this.chunks.enter(); + } + + @Override + public void popTransaction() { + this.chunks.pop(); + } + + @Override + public int lineCount() { + return this.chunks.lineCount(); + } + + @Override + public List columnsHeaders() { + return null; + } + + @Override + public void tracePreOpcode(MessageFrame frame) { + GasParameters gasParameters = extractGasParameters(frame); + this.chunks.add(new GasOperation(gasParameters)); + } + + private GasParameters extractGasParameters(MessageFrame frame) { + return new GasParameters(BigInteger.ZERO, BigInteger.ZERO, false); + } + + @Override + public void commit(List buffers) { + final Trace trace = new Trace(buffers); + for (int i = 0; i < this.chunks.size(); i++) { + GasOperation gasOperation = this.chunks.get(i); + gasOperation.trace(i + 1, trace); + } + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/GasOperation.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/GasOperation.java new file mode 100644 index 0000000000..e242022323 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/GasOperation.java @@ -0,0 +1,65 @@ +/* + * 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.module.gas; + +import static net.consensys.linea.zktracer.module.gas.Trace.CT_MAX; +import static net.consensys.linea.zktracer.types.Conversions.bigIntegerToBytes; +import static net.consensys.linea.zktracer.types.Conversions.booleanToInt; + +import java.math.BigInteger; + +import lombok.EqualsAndHashCode; +import net.consensys.linea.zktracer.container.ModuleOperation; +import net.consensys.linea.zktracer.types.UnsignedByte; +import org.apache.tuweni.bytes.Bytes; + +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class GasOperation extends ModuleOperation { + @EqualsAndHashCode.Include GasParameters gasParameters; + Bytes acc1; + Bytes acc2; + + public GasOperation(GasParameters gasParameters) { + this.gasParameters = gasParameters; + acc1 = bigIntegerToBytes(gasParameters.gasActl()); + acc2 = + bigIntegerToBytes( + (BigInteger.valueOf((2L * booleanToInt(gasParameters.oogx()) - 1)) + .multiply(gasParameters.gasCost().subtract(gasParameters.gasActl()))) + .subtract(BigInteger.valueOf(booleanToInt(gasParameters.oogx())))); + } + + @Override + protected int computeLineCount() { + return CT_MAX + 1; + } + + public void trace(int stamp, Trace trace) { + for (short i = 0; i < CT_MAX + 1; i++) { + trace + .stamp(stamp) + .ct(i) + .gasActl(gasParameters.gasActl().longValue()) + .gasCost(bigIntegerToBytes(gasParameters.gasCost())) + .oogx(gasParameters.oogx()) + .byte1(UnsignedByte.of(acc1.get(i))) + .byte2(UnsignedByte.of(acc2.get(i))) + .acc1(acc1.slice(0, i + 1)) + .acc2(acc2.slice(0, i + 1)) + .validateRow(); + } + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/GasParameters.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/GasParameters.java new file mode 100644 index 0000000000..9106de5266 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/GasParameters.java @@ -0,0 +1,20 @@ +/* + * 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.module.gas; + +import java.math.BigInteger; + +public record GasParameters(BigInteger gasActl, BigInteger gasCost, boolean oogx) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/Trace.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/Trace.java new file mode 100644 index 0000000000..e7db5f6fbd --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/gas/Trace.java @@ -0,0 +1,292 @@ +/* + * 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.module.gas; + +import java.nio.MappedByteBuffer; +import java.util.BitSet; +import java.util.List; + +import net.consensys.linea.zktracer.ColumnHeader; +import net.consensys.linea.zktracer.types.UnsignedByte; +import org.apache.tuweni.bytes.Bytes; + +/** + * WARNING: This code is generated automatically. + * + *

Any modifications to this code may be overwritten and could lead to unexpected behavior. + * Please DO NOT ATTEMPT TO MODIFY this code directly. + */ +public class Trace { + public static final int CT_MAX = 0x7; + + private final BitSet filled = new BitSet(); + private int currentLine = 0; + + private final MappedByteBuffer acc1; + private final MappedByteBuffer acc2; + private final MappedByteBuffer byte1; + private final MappedByteBuffer byte2; + private final MappedByteBuffer ct; + private final MappedByteBuffer gasActl; + private final MappedByteBuffer gasCost; + private final MappedByteBuffer oogx; + private final MappedByteBuffer stamp; + + static List headers(int length) { + return List.of( + new ColumnHeader("gas.ACC_1", 32, length), + new ColumnHeader("gas.ACC_2", 32, length), + new ColumnHeader("gas.BYTE_1", 1, length), + new ColumnHeader("gas.BYTE_2", 1, length), + new ColumnHeader("gas.CT", 2, length), + new ColumnHeader("gas.GAS_ACTL", 8, length), + new ColumnHeader("gas.GAS_COST", 32, length), + new ColumnHeader("gas.OOGX", 1, length), + new ColumnHeader("gas.STAMP", 8, length)); + } + + public Trace(List buffers) { + this.acc1 = buffers.get(0); + this.acc2 = buffers.get(1); + this.byte1 = buffers.get(2); + this.byte2 = buffers.get(3); + this.ct = buffers.get(4); + this.gasActl = buffers.get(5); + this.gasCost = buffers.get(6); + this.oogx = buffers.get(7); + this.stamp = buffers.get(8); + } + + public int size() { + if (!filled.isEmpty()) { + throw new RuntimeException("Cannot measure a trace with a non-validated row."); + } + + return this.currentLine; + } + + public Trace acc1(final Bytes b) { + if (filled.get(0)) { + throw new IllegalStateException("gas.ACC_1 already set"); + } else { + filled.set(0); + } + + final byte[] bs = b.toArrayUnsafe(); + for (int i = bs.length; i < 32; i++) { + acc1.put((byte) 0); + } + acc1.put(b.toArrayUnsafe()); + + return this; + } + + public Trace acc2(final Bytes b) { + if (filled.get(1)) { + throw new IllegalStateException("gas.ACC_2 already set"); + } else { + filled.set(1); + } + + final byte[] bs = b.toArrayUnsafe(); + for (int i = bs.length; i < 32; i++) { + acc2.put((byte) 0); + } + acc2.put(b.toArrayUnsafe()); + + return this; + } + + public Trace byte1(final UnsignedByte b) { + if (filled.get(2)) { + throw new IllegalStateException("gas.BYTE_1 already set"); + } else { + filled.set(2); + } + + byte1.put(b.toByte()); + + return this; + } + + public Trace byte2(final UnsignedByte b) { + if (filled.get(3)) { + throw new IllegalStateException("gas.BYTE_2 already set"); + } else { + filled.set(3); + } + + byte2.put(b.toByte()); + + return this; + } + + public Trace ct(final short b) { + if (filled.get(4)) { + throw new IllegalStateException("gas.CT already set"); + } else { + filled.set(4); + } + + ct.putShort(b); + + return this; + } + + public Trace gasActl(final long b) { + if (filled.get(5)) { + throw new IllegalStateException("gas.GAS_ACTL already set"); + } else { + filled.set(5); + } + + gasActl.putLong(b); + + return this; + } + + public Trace gasCost(final Bytes b) { + if (filled.get(6)) { + throw new IllegalStateException("gas.GAS_COST already set"); + } else { + filled.set(6); + } + + final byte[] bs = b.toArrayUnsafe(); + for (int i = bs.length; i < 32; i++) { + gasCost.put((byte) 0); + } + gasCost.put(b.toArrayUnsafe()); + + return this; + } + + public Trace oogx(final Boolean b) { + if (filled.get(7)) { + throw new IllegalStateException("gas.OOGX already set"); + } else { + filled.set(7); + } + + oogx.put((byte) (b ? 1 : 0)); + + return this; + } + + public Trace stamp(final long b) { + if (filled.get(8)) { + throw new IllegalStateException("gas.STAMP already set"); + } else { + filled.set(8); + } + + stamp.putLong(b); + + return this; + } + + public Trace validateRow() { + if (!filled.get(0)) { + throw new IllegalStateException("gas.ACC_1 has not been filled"); + } + + if (!filled.get(1)) { + throw new IllegalStateException("gas.ACC_2 has not been filled"); + } + + if (!filled.get(2)) { + throw new IllegalStateException("gas.BYTE_1 has not been filled"); + } + + if (!filled.get(3)) { + throw new IllegalStateException("gas.BYTE_2 has not been filled"); + } + + if (!filled.get(4)) { + throw new IllegalStateException("gas.CT has not been filled"); + } + + if (!filled.get(5)) { + throw new IllegalStateException("gas.GAS_ACTL has not been filled"); + } + + if (!filled.get(6)) { + throw new IllegalStateException("gas.GAS_COST has not been filled"); + } + + if (!filled.get(7)) { + throw new IllegalStateException("gas.OOGX has not been filled"); + } + + if (!filled.get(8)) { + throw new IllegalStateException("gas.STAMP has not been filled"); + } + + filled.clear(); + this.currentLine++; + + return this; + } + + public Trace fillAndValidateRow() { + if (!filled.get(0)) { + acc1.position(acc1.position() + 32); + } + + if (!filled.get(1)) { + acc2.position(acc2.position() + 32); + } + + if (!filled.get(2)) { + byte1.position(byte1.position() + 1); + } + + if (!filled.get(3)) { + byte2.position(byte2.position() + 1); + } + + if (!filled.get(4)) { + ct.position(ct.position() + 2); + } + + if (!filled.get(5)) { + gasActl.position(gasActl.position() + 8); + } + + if (!filled.get(6)) { + gasCost.position(gasCost.position() + 32); + } + + if (!filled.get(7)) { + oogx.position(oogx.position() + 1); + } + + if (!filled.get(8)) { + stamp.position(stamp.position() + 8); + } + + filled.clear(); + this.currentLine++; + + return this; + } + + public void build() { + if (!filled.isEmpty()) { + throw new IllegalStateException("Cannot build trace with a non-validated row."); + } + } +} diff --git a/gradle/trace-files.gradle b/gradle/trace-files.gradle index 43be61bd4c..b43f38bbec 100644 --- a/gradle/trace-files.gradle +++ b/gradle/trace-files.gradle @@ -81,7 +81,7 @@ tasks.register('binreftable', TraceFilesTask) { // // Put here modules following the conventional MODULE/columns.lisp, MODULE/constants.lisp naming scheme. // -['mmu', 'blake2fmodexpdata', 'blockdata', 'oob', 'exp', 'rlptxrcpt', 'rlpaddr', 'shakiradata', 'mxp', 'ecdata'].each {moduleName -> +['mmu', 'blake2fmodexpdata', 'blockdata', 'oob', 'exp', 'rlptxrcpt', 'rlpaddr', 'shakiradata', 'mxp', 'ecdata', 'gas'].each {moduleName -> tasks.register(moduleName, TraceFilesTask) { group "Trace files generation" dependsOn corsetExists diff --git a/zkevm-constraints b/zkevm-constraints index 5cd66bce1d..332f32683d 160000 --- a/zkevm-constraints +++ b/zkevm-constraints @@ -1 +1 @@ -Subproject commit 5cd66bce1de7a64c7221d18b529e13f3f2fe1316 +Subproject commit 332f32683d21bf539d3fcd8ce13baa9b5ba715be From 110d119f053d296d879d8a8e268d9ea6937f0f1c Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Tue, 4 Jun 2024 18:39:50 +0300 Subject: [PATCH 13/39] feat(besu-plugins-integratio): migrate besu plugins from linea-sequencer (#752) Add following plugins: - ContinuousTracingPlugin - CaptureEndpointServicePlugin - CountersEndpointServicePlugin - TracesEndpointServicePlugin Additionally remove downloading besu jar as it is unneeded anymore and thus reducing the end jar file size. Resolves: #751 Signed-off-by: Tsvetan Dimitrov --- .../linea/AbstractLineaRequiredPlugin.java | 55 + .../AbstractLineaSharedOptionsPlugin.java | 68 + .../config/LineaL1L2BridgeCliOptions.java | 87 + .../linea/config/LineaTracerCliOptions.java | 71 + .../config/LineaTracerConfiguration.java | 22 + .../config/converters/AddressConverter.java | 25 + .../config/converters/BytesConverter.java | 25 + .../linea/config/converters/WeiConverter.java | 28 + .../continoustracing/ContinuousTracer.java | 96 + .../ContinuousTracingBlockAddedListener.java | 92 + .../ContinuousTracingCliOptions.java | 57 + .../ContinuousTracingConfiguration.java | 17 + .../ContinuousTracingPlugin.java | 108 + .../SlackNotificationService.java | 129 + .../continoustracing/TraceFailureHandler.java | 53 + .../exception/InvalidBlockTraceException.java | 31 + .../InvalidTraceHandlerException.java | 21 + .../exception/TraceVerificationException.java | 24 + .../consensys/linea/rpc/capture/Capture.java | 21 + .../capture/CaptureEndpointServicePlugin.java | 69 + .../linea/rpc/capture/CaptureParams.java | 43 + .../linea/rpc/capture/CaptureToFile.java | 86 + .../linea/rpc/counters/Counters.java | 26 + .../CountersEndpointServicePlugin.java | 75 + .../rpc/counters/CountersRequestParams.java | 56 + .../rpc/counters/GenerateCountersV0.java | 118 + .../GenerateConflatedTracesV0.java | 142 + .../linea/rpc/tracegeneration/TraceFile.java | 23 + .../tracegeneration/TraceRequestParams.java | 57 + .../TracesEndpointServicePlugin.java | 77 + .../resources/replays/2982051-2982061.json.gz | Bin 0 -> 1033940 bytes .../resources/replays/3149945-3149946.json | 15695 ++++++++++++++++ .../resources/replays/3149945-3149946.json.gz | Bin 0 -> 952079 bytes gradle/dist.gradle | 80 - gradle/publishing.gradle | 23 - 35 files changed, 17497 insertions(+), 103 deletions(-) create mode 100644 arithmetization/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/AbstractLineaSharedOptionsPlugin.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaL1L2BridgeCliOptions.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaTracerCliOptions.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/config/LineaTracerConfiguration.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/config/converters/AddressConverter.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/config/converters/BytesConverter.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/config/converters/WeiConverter.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracer.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingBlockAddedListener.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingCliOptions.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingConfiguration.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingPlugin.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/continoustracing/SlackNotificationService.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/continoustracing/TraceFailureHandler.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/InvalidBlockTraceException.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/InvalidTraceHandlerException.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/TraceVerificationException.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/capture/Capture.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureEndpointServicePlugin.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureParams.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureToFile.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/counters/Counters.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/counters/CountersEndpointServicePlugin.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/counters/CountersRequestParams.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/counters/GenerateCountersV0.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/GenerateConflatedTracesV0.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TraceFile.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TraceRequestParams.java create mode 100644 arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TracesEndpointServicePlugin.java create mode 100644 arithmetization/src/test/resources/replays/2982051-2982061.json.gz create mode 100644 arithmetization/src/test/resources/replays/3149945-3149946.json create mode 100644 arithmetization/src/test/resources/replays/3149945-3149946.json.gz diff --git a/arithmetization/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java b/arithmetization/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java new file mode 100644 index 0000000000..27ec17621c --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java @@ -0,0 +1,55 @@ +/* + * 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; + +import lombok.extern.slf4j.Slf4j; +import org.hyperledger.besu.plugin.BesuContext; +import org.hyperledger.besu.plugin.BesuPlugin; + +@Slf4j +public abstract class AbstractLineaRequiredPlugin extends AbstractLineaSharedOptionsPlugin { + + /** + * Linea plugins extending this class will halt startup of Besu in case of exception during + * registration. + * + *

If that's NOT desired, the plugin should implement {@link BesuPlugin} directly. + * + * @param context + */ + @Override + public void register(final BesuContext context) { + super.register(context); + try { + log.info("Registering Linea plugin " + this.getClass().getName()); + + doRegister(context); + + } catch (Exception e) { + log.error("Halting Besu startup: exception in plugin registration: ", e); + e.printStackTrace(); + // System.exit will cause besu to exit + System.exit(1); + } + } + + /** + * Linea plugins need to implement this method. Called by {@link BesuPlugin} register method + * + * @param context + */ + public abstract void doRegister(final BesuContext context); +} diff --git a/arithmetization/src/main/java/net/consensys/linea/AbstractLineaSharedOptionsPlugin.java b/arithmetization/src/main/java/net/consensys/linea/AbstractLineaSharedOptionsPlugin.java new file mode 100644 index 0000000000..7e1c4b27d5 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/AbstractLineaSharedOptionsPlugin.java @@ -0,0 +1,68 @@ +/* + * 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; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaTracerCliOptions; +import net.consensys.linea.config.LineaTracerConfiguration; +import org.hyperledger.besu.plugin.BesuContext; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.services.PicoCLIOptions; + +@Slf4j +public abstract class AbstractLineaSharedOptionsPlugin implements BesuPlugin { + private static final String CLI_OPTIONS_PREFIX = "linea"; + private static boolean cliOptionsRegistered = false; + private static boolean configured = false; + protected static LineaTracerCliOptions tracerCliOptions; + protected static LineaTracerConfiguration tracerConfiguration; + + @Override + public synchronized void register(final BesuContext context) { + if (!cliOptionsRegistered) { + final PicoCLIOptions cmdlineOptions = + context + .getService(PicoCLIOptions.class) + .orElseThrow( + () -> + new IllegalStateException( + "Failed to obtain PicoCLI options from the BesuContext")); + tracerCliOptions = LineaTracerCliOptions.create(); + + cmdlineOptions.addPicoCLIOptions(CLI_OPTIONS_PREFIX, tracerCliOptions); + cliOptionsRegistered = true; + } + } + + @Override + public void beforeExternalServices() { + if (!configured) { + tracerConfiguration = tracerCliOptions.toDomainObject(); + configured = true; + } + + log.debug("Configured plugin {} with tracer configuration: {}", getName(), tracerConfiguration); + } + + @Override + public void start() {} + + @Override + public void stop() { + cliOptionsRegistered = false; + configured = false; + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaL1L2BridgeCliOptions.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaL1L2BridgeCliOptions.java new file mode 100644 index 0000000000..cfd6c1db50 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/config/LineaL1L2BridgeCliOptions.java @@ -0,0 +1,87 @@ +/* + * 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.config; + +import com.google.common.base.MoreObjects; +import net.consensys.linea.config.converters.AddressConverter; +import net.consensys.linea.config.converters.BytesConverter; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Address; +import picocli.CommandLine; + +/** The Linea L1 L2 Bridge CLI options. */ +public class LineaL1L2BridgeCliOptions { + private static final String L1L2_BRIDGE_CONTRACT = "--plugin-linea-l1l2-bridge-contract"; + private static final String L1L2_BRIDGE_TOPIC = "--plugin-linea-l1l2-bridge-topic"; + + @CommandLine.Option( + names = {L1L2_BRIDGE_CONTRACT}, + paramLabel = "

", + converter = AddressConverter.class, + description = "The address of the L1 L2 bridge contract (default: ${DEFAULT-VALUE})") + private Address l1l2BridgeContract = Address.ZERO; + + @CommandLine.Option( + names = {L1L2_BRIDGE_TOPIC}, + paramLabel = "", + converter = BytesConverter.class, + description = "The log topic of the L1 L2 bridge (default: ${DEFAULT-VALUE})") + private Bytes l1l2BridgeTopic = Bytes.EMPTY; + + private LineaL1L2BridgeCliOptions() {} + + /** + * Create Linea cli options. + * + * @return the Linea cli options + */ + public static LineaL1L2BridgeCliOptions create() { + return new LineaL1L2BridgeCliOptions(); + } + + /** + * Linea cli options from config. + * + * @param config the config + * @return the Linea cli options + */ + public static LineaL1L2BridgeCliOptions fromConfig(final LineaL1L2BridgeConfiguration config) { + final LineaL1L2BridgeCliOptions options = create(); + options.l1l2BridgeContract = config.contract(); + options.l1l2BridgeTopic = config.topic(); + return options; + } + + /** + * To domain object Linea factory configuration. + * + * @return the Linea factory configuration + */ + public LineaL1L2BridgeConfiguration toDomainObject() { + return LineaL1L2BridgeConfiguration.builder() + .contract(l1l2BridgeContract) + .topic(l1l2BridgeTopic) + .build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add(L1L2_BRIDGE_CONTRACT, l1l2BridgeContract.toHexString()) + .add(L1L2_BRIDGE_TOPIC, l1l2BridgeTopic.toHexString()) + .toString(); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaTracerCliOptions.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaTracerCliOptions.java new file mode 100644 index 0000000000..0d75650ee8 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/config/LineaTracerCliOptions.java @@ -0,0 +1,71 @@ +/* + * 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.config; + +import com.google.common.base.MoreObjects; +import picocli.CommandLine; + +public class LineaTracerCliOptions { + + public static final String MODULE_LIMIT_FILE_PATH = "--plugin-linea-module-limit-file-path"; + public static final String DEFAULT_MODULE_LIMIT_FILE_PATH = "moduleLimitFile.toml"; + + @CommandLine.Option( + names = {MODULE_LIMIT_FILE_PATH}, + hidden = true, + paramLabel = "", + description = + "Path to the toml file containing the module limits (default: ${DEFAULT-VALUE})") + private String moduleLimitFilePath = DEFAULT_MODULE_LIMIT_FILE_PATH; + + private LineaTracerCliOptions() {} + + /** + * Create Linea cli options. + * + * @return the Linea cli options + */ + public static LineaTracerCliOptions create() { + return new LineaTracerCliOptions(); + } + + /** + * Linea cli options from config. + * + * @param config the config + * @return the Linea cli options + */ + public static LineaTracerCliOptions fromConfig(final LineaTracerConfiguration config) { + final LineaTracerCliOptions options = create(); + options.moduleLimitFilePath = config.moduleLimitsFilePath(); + return options; + } + + /** + * To domain object Linea factory configuration. + * + * @return the Linea factory configuration + */ + public LineaTracerConfiguration toDomainObject() { + return LineaTracerConfiguration.builder().moduleLimitsFilePath(moduleLimitFilePath).build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add(MODULE_LIMIT_FILE_PATH, moduleLimitFilePath) + .toString(); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/LineaTracerConfiguration.java b/arithmetization/src/main/java/net/consensys/linea/config/LineaTracerConfiguration.java new file mode 100644 index 0000000000..a9b690334c --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/config/LineaTracerConfiguration.java @@ -0,0 +1,22 @@ +/* + * 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.config; + +import lombok.Builder; + +/** The Linea tracer configuration. */ +@Builder(toBuilder = true) +public record LineaTracerConfiguration(String moduleLimitsFilePath) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/converters/AddressConverter.java b/arithmetization/src/main/java/net/consensys/linea/config/converters/AddressConverter.java new file mode 100644 index 0000000000..a1c3c457c4 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/config/converters/AddressConverter.java @@ -0,0 +1,25 @@ +/* + * 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.config.converters; + +import org.hyperledger.besu.datatypes.Address; +import picocli.CommandLine; + +public class AddressConverter implements CommandLine.ITypeConverter
{ + @Override + public Address convert(final String s) throws Exception { + return Address.fromHexString(s); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/converters/BytesConverter.java b/arithmetization/src/main/java/net/consensys/linea/config/converters/BytesConverter.java new file mode 100644 index 0000000000..28d73c0122 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/config/converters/BytesConverter.java @@ -0,0 +1,25 @@ +/* + * 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.config.converters; + +import org.apache.tuweni.bytes.Bytes; +import picocli.CommandLine; + +public class BytesConverter implements CommandLine.ITypeConverter { + @Override + public Bytes convert(final String s) throws Exception { + return Bytes.fromHexStringLenient(s); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/config/converters/WeiConverter.java b/arithmetization/src/main/java/net/consensys/linea/config/converters/WeiConverter.java new file mode 100644 index 0000000000..33ea66181b --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/config/converters/WeiConverter.java @@ -0,0 +1,28 @@ +/* + * 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.config.converters; + +import java.math.BigInteger; + +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Wei; +import picocli.CommandLine; + +public class WeiConverter implements CommandLine.ITypeConverter { + @Override + public Bytes convert(final String s) throws Exception { + return Wei.of(new BigInteger(s)); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracer.java b/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracer.java new file mode 100644 index 0000000000..eba0f4ee71 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracer.java @@ -0,0 +1,96 @@ +/* + * 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.continoustracing; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.continoustracing.exception.InvalidBlockTraceException; +import net.consensys.linea.continoustracing.exception.TraceVerificationException; +import net.consensys.linea.corset.CorsetValidator; +import net.consensys.linea.zktracer.ZkTracer; +import org.apache.commons.io.FileUtils; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.plugin.data.BlockTraceResult; +import org.hyperledger.besu.plugin.data.TransactionTraceResult; +import org.hyperledger.besu.plugin.services.TraceService; + +@Slf4j +public class ContinuousTracer { + private static final Optional TRACES_PATH = + Optional.ofNullable(System.getenv("TRACES_DIR")).map(Paths::get); + private final TraceService traceService; + private final CorsetValidator corsetValidator; + + public ContinuousTracer(final TraceService traceService, final CorsetValidator corsetValidator) { + this.traceService = traceService; + this.corsetValidator = corsetValidator; + } + + public CorsetValidator.Result verifyTraceOfBlock( + final Hash blockHash, final String zkEvmBin, final ZkTracer zkTracer) + throws TraceVerificationException, InvalidBlockTraceException { + zkTracer.traceStartConflation(1); + + final BlockTraceResult blockTraceResult; + try { + blockTraceResult = traceService.traceBlock(blockHash, zkTracer); + } catch (final Exception e) { + throw new TraceVerificationException(blockHash, e.getMessage()); + } finally { + // TODO: After consulting with the Arithmetization team, it is ok to pass the world state as + // null for now, but it + // should be fixed at some point. + zkTracer.traceEndConflation(null); + } + + for (final TransactionTraceResult transactionTraceResult : + blockTraceResult.transactionTraceResults()) { + if (transactionTraceResult.getStatus() != TransactionTraceResult.Status.SUCCESS) { + throw new InvalidBlockTraceException( + transactionTraceResult.getTxHash(), + transactionTraceResult.errorMessage().orElse("Unknown error")); + } + } + + final CorsetValidator.Result result; + try { + result = + corsetValidator.validate( + TRACES_PATH.map(zkTracer::writeToTmpFile).orElseGet(zkTracer::writeToTmpFile), + zkEvmBin); + if (!result.isValid()) { + log.error("Trace of block {} is not valid", blockHash.toHexString()); + return result; + } + } catch (RuntimeException e) { + log.error( + "Error while validating trace of block {}: {}", blockHash.toHexString(), e.getMessage()); + throw new TraceVerificationException(blockHash, e.getMessage()); + } + + try { + FileUtils.delete(result.traceFile()); + } catch (IOException e) { + log.warn("Error while deleting trace file {}: {}", result.traceFile(), e.getMessage()); + } + + log.info("Trace of block {} is valid", blockHash.toHexString()); + return result; + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingBlockAddedListener.java b/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingBlockAddedListener.java new file mode 100644 index 0000000000..f34c255fc6 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingBlockAddedListener.java @@ -0,0 +1,92 @@ +/* + * 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.continoustracing; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.continoustracing.exception.InvalidBlockTraceException; +import net.consensys.linea.continoustracing.exception.InvalidTraceHandlerException; +import net.consensys.linea.continoustracing.exception.TraceVerificationException; +import net.consensys.linea.corset.CorsetValidator; +import net.consensys.linea.zktracer.ZkTracer; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.plugin.data.AddedBlockContext; +import org.hyperledger.besu.plugin.data.BlockHeader; +import org.hyperledger.besu.plugin.services.BesuEvents; + +@Slf4j +public class ContinuousTracingBlockAddedListener implements BesuEvents.BlockAddedListener { + private final ContinuousTracer continuousTracer; + private final TraceFailureHandler traceFailureHandler; + private final String zkEvmBin; + + static final int BLOCK_PARALLELISM = 5; + final ThreadPoolExecutor pool = + new ThreadPoolExecutor( + BLOCK_PARALLELISM, + BLOCK_PARALLELISM, + 0L, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(BLOCK_PARALLELISM), + new ThreadPoolExecutor.CallerRunsPolicy()); + + public ContinuousTracingBlockAddedListener( + final ContinuousTracer continuousTracer, + final TraceFailureHandler traceFailureHandler, + final String zkEvmBin) { + this.continuousTracer = continuousTracer; + this.traceFailureHandler = traceFailureHandler; + this.zkEvmBin = zkEvmBin; + } + + @Override + public void onBlockAdded(final AddedBlockContext addedBlockContext) { + pool.submit( + () -> { + final BlockHeader blockHeader = addedBlockContext.getBlockHeader(); + final Hash blockHash = blockHeader.getBlockHash(); + log.info("Tracing block {} ({})", blockHeader.getNumber(), blockHash.toHexString()); + + try { + final CorsetValidator.Result traceResult = + continuousTracer.verifyTraceOfBlock(blockHash, zkEvmBin, new ZkTracer()); + Files.delete(traceResult.traceFile().toPath()); + + if (!traceResult.isValid()) { + log.error("Corset returned and error for block {}", blockHeader.getNumber()); + traceFailureHandler.handleCorsetFailure(blockHeader, traceResult); + return; + } + log.info("Trace for block {} verified successfully", blockHeader.getNumber()); + } catch (InvalidBlockTraceException e) { + log.error("Error while tracing block {}: {}", blockHeader.getNumber(), e.getMessage()); + traceFailureHandler.handleBlockTraceFailure(blockHeader.getNumber(), e.txHash(), e); + } catch (TraceVerificationException e) { + log.error(e.getMessage()); + } catch (InvalidTraceHandlerException e) { + log.error("Error while handling invalid trace: {}", e.getMessage()); + } catch (IOException e) { + log.error("IO error: {}", e.getMessage()); + } finally { + log.info("End of tracing block {}", blockHeader.getNumber()); + } + }); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingCliOptions.java b/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingCliOptions.java new file mode 100644 index 0000000000..4cc72a2ad6 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingCliOptions.java @@ -0,0 +1,57 @@ +/* + * 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.continoustracing; + +import com.google.common.base.MoreObjects; +import picocli.CommandLine; + +public class ContinuousTracingCliOptions { + public static final String CONTINUOUS_TRACING_ENABLED = + "--plugin-linea-continuous-tracing-enabled"; + public static final String CONTINUOUS_TRACING_ZK_EVM_BIN = + "--plugin-linea-continuous-tracing-zk-evm-bin"; + + @CommandLine.Option( + names = {CONTINUOUS_TRACING_ENABLED}, + hidden = true, + paramLabel = "", + description = "Enable continuous tracing (default: false)") + private boolean continuousTracingEnabled = false; + + @CommandLine.Option( + names = {CONTINUOUS_TRACING_ZK_EVM_BIN}, + hidden = true, + paramLabel = "", + description = "Path to the ZkEvm binary") + private String zkEvmBin = null; + + private ContinuousTracingCliOptions() {} + + public static ContinuousTracingCliOptions create() { + return new ContinuousTracingCliOptions(); + } + + public ContinuousTracingConfiguration toDomainObject() { + return new ContinuousTracingConfiguration(continuousTracingEnabled, zkEvmBin); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add(CONTINUOUS_TRACING_ENABLED, continuousTracingEnabled) + .add(CONTINUOUS_TRACING_ZK_EVM_BIN, zkEvmBin) + .toString(); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingConfiguration.java b/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingConfiguration.java new file mode 100644 index 0000000000..c213b9d5b7 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingConfiguration.java @@ -0,0 +1,17 @@ +/* + * 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.continoustracing; + +public record ContinuousTracingConfiguration(boolean continuousTracing, String zkEvmBin) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingPlugin.java b/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingPlugin.java new file mode 100644 index 0000000000..8a76acf84c --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/continoustracing/ContinuousTracingPlugin.java @@ -0,0 +1,108 @@ +/* + * 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.continoustracing; + +import java.util.Optional; + +import com.google.auto.service.AutoService; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.corset.CorsetValidator; +import org.hyperledger.besu.plugin.BesuContext; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.services.BesuEvents; +import org.hyperledger.besu.plugin.services.PicoCLIOptions; +import org.hyperledger.besu.plugin.services.TraceService; + +@Slf4j +@AutoService(BesuPlugin.class) +public class ContinuousTracingPlugin implements BesuPlugin { + public static final String NAME = "linea-continuous"; + public static final String ENV_WEBHOOK_URL = "SLACK_SHADOW_NODE_WEBHOOK_URL"; + + private final ContinuousTracingCliOptions options; + private BesuContext context; + + public ContinuousTracingPlugin() { + options = ContinuousTracingCliOptions.create(); + } + + @Override + public Optional getName() { + return Optional.of(NAME); + } + + @Override + public void register(final BesuContext context) { + final PicoCLIOptions cmdlineOptions = + context + .getService(PicoCLIOptions.class) + .orElseThrow( + () -> + new IllegalStateException( + "Expecting a PicoCLI options to register CLI options with, but none found.")); + + cmdlineOptions.addPicoCLIOptions(getName().get(), options); + + this.context = context; + } + + @Override + public void start() { + log.info("Starting {} with configuration: {}", NAME, options); + + final ContinuousTracingConfiguration tracingConfiguration = options.toDomainObject(); + + if (!tracingConfiguration.continuousTracing()) { + return; + } + + // BesuEvents can only be requested after the plugin has been registered. + final BesuEvents besuEvents = + context + .getService(BesuEvents.class) + .orElseThrow( + () -> + new IllegalStateException( + "Expecting a BesuEvents to register events with, but none found.")); + + final TraceService traceService = + context + .getService(TraceService.class) + .orElseThrow( + () -> new IllegalStateException("Expecting a TraceService, but none found.")); + + if (tracingConfiguration.zkEvmBin() == null) { + log.error("zkEvmBin must be specified when continuousTracing is enabled"); + System.exit(1); + } + + final String webHookUrl = System.getenv(ENV_WEBHOOK_URL); + if (webHookUrl == null) { + log.error( + "Webhook URL must be specified as environment variable {} when continuousTracing is enabled", + ENV_WEBHOOK_URL); + System.exit(1); + } + + besuEvents.addBlockAddedListener( + new ContinuousTracingBlockAddedListener( + new ContinuousTracer(traceService, new CorsetValidator()), + new TraceFailureHandler(SlackNotificationService.create(webHookUrl)), + tracingConfiguration.zkEvmBin())); + } + + @Override + public void stop() {} +} diff --git a/arithmetization/src/main/java/net/consensys/linea/continoustracing/SlackNotificationService.java b/arithmetization/src/main/java/net/consensys/linea/continoustracing/SlackNotificationService.java new file mode 100644 index 0000000000..d3a10eabc6 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/continoustracing/SlackNotificationService.java @@ -0,0 +1,129 @@ +/* + * 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.continoustracing; + +import static com.slack.api.model.block.Blocks.asBlocks; +import static com.slack.api.model.block.Blocks.divider; +import static com.slack.api.model.block.Blocks.section; +import static com.slack.api.model.block.composition.BlockCompositions.markdownText; + +import java.io.IOException; +import java.util.stream.Collectors; + +import com.slack.api.Slack; +import com.slack.api.webhook.Payload; +import com.slack.api.webhook.WebhookResponse; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.corset.CorsetValidator; +import org.hyperledger.besu.datatypes.Hash; + +@Slf4j +public class SlackNotificationService { + final Slack slack; + final String webHookUrl; + + protected SlackNotificationService(final Slack slack, final String webHookUrl) { + this.slack = slack; + this.webHookUrl = webHookUrl; + } + + public static SlackNotificationService create(final String webHookUrl) { + return new SlackNotificationService(Slack.getInstance(), webHookUrl); + } + + public void sendCorsetFailureNotification( + final long blockNumber, final String blockHash, final CorsetValidator.Result validationResult) + throws IOException { + final Payload messagePayload = + Payload.builder() + .text("Slack couldn't properly display the message.") + .blocks( + asBlocks( + section( + section -> + section.text( + markdownText( + String.format( + "*Trace verification failure for block %d (%s)", + blockNumber, blockHash)))), + divider(), + section( + section -> + section.text( + markdownText( + "Trace verification failed with the following error:\n\n" + + "```" + + validationResult + .corsetOutput() + // Remove all ANSI escape codes that Slack does not like + .replaceAll("\u001B\\[[;\\d]*m", "") + + "```\n\n" + + "Trace file: " + + validationResult.traceFile()))))) + .build(); + + WebhookResponse response = slack.send(webHookUrl, messagePayload); + checkResponse(response); + } + + public void sendBlockTraceFailureNotification( + final long blockNumber, final Hash txHash, final Throwable throwable) throws IOException { + + log.info("Throwable.getMessage(): {}", throwable.getMessage()); + + final Payload messagePayload = + Payload.builder() + .text("Slack couldn't properly display the message.") + .blocks( + asBlocks( + section( + section -> + section.text( + markdownText( + String.format( + "*Error while tracing transaction %s in block %d*", + txHash.toHexString(), blockNumber)))), + divider(), + section( + section -> + section.text( + markdownText( + "Trace generation failed with the following error:\n\n" + + "```" + // more than 2000 characters will cause a 400 error when + // sending the message + + throwable + .getMessage() + .lines() + .limit(3) + .collect(Collectors.joining("\n")) + .substring(0, 2000) + + "```"))))) + .build(); + + WebhookResponse response = slack.send(webHookUrl, messagePayload); + checkResponse(response); + } + + private void checkResponse(final WebhookResponse response) throws IOException { + if (response.getCode() != 200) { + throw new IOException( + "Error while sending notification: status code: " + + response.getCode() + + ", body: " + + response.getBody()); + } + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/continoustracing/TraceFailureHandler.java b/arithmetization/src/main/java/net/consensys/linea/continoustracing/TraceFailureHandler.java new file mode 100644 index 0000000000..ed62584ef9 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/continoustracing/TraceFailureHandler.java @@ -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.continoustracing; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.continoustracing.exception.InvalidTraceHandlerException; +import net.consensys.linea.corset.CorsetValidator; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.plugin.data.BlockHeader; + +@Slf4j +public class TraceFailureHandler { + final SlackNotificationService slackNotificationService; + + public TraceFailureHandler(final SlackNotificationService slackNotificationService) { + this.slackNotificationService = slackNotificationService; + } + + public void handleCorsetFailure( + final BlockHeader blockHeader, final CorsetValidator.Result result) + throws InvalidTraceHandlerException { + try { + slackNotificationService.sendCorsetFailureNotification( + blockHeader.getNumber(), blockHeader.getBlockHash().toHexString(), result); + } catch (IOException e) { + log.error("Error while sending slack notification: {}", e.getMessage()); + throw new InvalidTraceHandlerException(e); + } + } + + public void handleBlockTraceFailure( + final long blockNumber, final Hash txHash, final Throwable throwable) { + try { + slackNotificationService.sendBlockTraceFailureNotification(blockNumber, txHash, throwable); + } catch (IOException e) { + log.error("Error while handling block trace failure: {}", e.getMessage()); + } + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/InvalidBlockTraceException.java b/arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/InvalidBlockTraceException.java new file mode 100644 index 0000000000..80847cf15f --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/InvalidBlockTraceException.java @@ -0,0 +1,31 @@ +/* + * 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.continoustracing.exception; + +import org.hyperledger.besu.datatypes.Hash; + +public class InvalidBlockTraceException extends Throwable { + final Hash txHash; + + public InvalidBlockTraceException(final Hash txHash, final String message) { + super(message); + + this.txHash = txHash; + } + + public Hash txHash() { + return txHash; + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/InvalidTraceHandlerException.java b/arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/InvalidTraceHandlerException.java new file mode 100644 index 0000000000..5e4b2f4ecb --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/InvalidTraceHandlerException.java @@ -0,0 +1,21 @@ +/* + * 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.continoustracing.exception; + +public class InvalidTraceHandlerException extends Throwable { + public InvalidTraceHandlerException(Throwable t) { + super(t); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/TraceVerificationException.java b/arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/TraceVerificationException.java new file mode 100644 index 0000000000..5fb13608c5 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/continoustracing/exception/TraceVerificationException.java @@ -0,0 +1,24 @@ +/* + * 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.continoustracing.exception; + +import org.hyperledger.besu.datatypes.Hash; + +public class TraceVerificationException extends Throwable { + public TraceVerificationException(final Hash blockHash, final String message) { + super( + "Verification of trace of block " + blockHash + " has failed.\nError message: " + message); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/capture/Capture.java b/arithmetization/src/main/java/net/consensys/linea/rpc/capture/Capture.java new file mode 100644 index 0000000000..7277bdd99b --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/capture/Capture.java @@ -0,0 +1,21 @@ +/* + * 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.rpc.capture; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** FileTrace represents an execution trace. */ +public record Capture(@JsonProperty("capture") String capture) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureEndpointServicePlugin.java b/arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureEndpointServicePlugin.java new file mode 100644 index 0000000000..3868fb07c4 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureEndpointServicePlugin.java @@ -0,0 +1,69 @@ +/* + * 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.rpc.capture; + +import java.util.Optional; + +import com.google.auto.service.AutoService; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.AbstractLineaRequiredPlugin; +import org.hyperledger.besu.plugin.BesuContext; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.services.RpcEndpointService; + +/** + * Registers RPC endpoints .This class provides an RPC endpoint named + * 'generateConflatedTracesToFileV0' under the 'rollup' namespace. It uses {@link CaptureToFile} to + * generate conflated file traces. This class provides an RPC endpoint named + * 'generateConflatedTracesToFileV0' under the 'rollup' namespace. + */ +@AutoService(BesuPlugin.class) +@Slf4j +public class CaptureEndpointServicePlugin extends AbstractLineaRequiredPlugin { + + /** + * Register the RPC service. + * + * @param context the BesuContext to be used. + */ + @Override + public void doRegister(final BesuContext context) { + CaptureToFile method = new CaptureToFile(context); + + Optional service = context.getService(RpcEndpointService.class); + createAndRegister( + method, + service.orElseThrow( + () -> + new RuntimeException("Failed to obtain RpcEndpointService from the BesuContext."))); + } + + /** + * Create and register the RPC service. + * + * @param method the RollupGenerateConflatedTracesToFileV0 method to be used. + * @param rpcEndpointService the RpcEndpointService to be registered. + */ + private void createAndRegister( + final CaptureToFile method, final RpcEndpointService rpcEndpointService) { + rpcEndpointService.registerRPCEndpoint( + method.getNamespace(), method.getName(), method::execute); + } + + /** Start the RPC service. This method loads the OpCodes. */ + @Override + public void start() {} +} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureParams.java b/arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureParams.java new file mode 100644 index 0000000000..a9973d99ee --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureParams.java @@ -0,0 +1,43 @@ +/* + * 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.rpc.capture; + +import java.security.InvalidParameterException; + +/** Holds needed parameters for sending an execution trace generation request. */ +@SuppressWarnings("unused") +public record CaptureParams(long fromBlock, long toBlock) { + private static final int EXPECTED_PARAMS_SIZE = 2; + + /** + * Parses a list of params to a {@link CaptureParams} object. + * + * @param params an array of parameters. + * @return a parsed {@link CaptureParams} object.. + */ + public static CaptureParams createTraceParams(final Object[] params) { + // validate params size + if (params.length != EXPECTED_PARAMS_SIZE) { + throw new InvalidParameterException( + String.format("Expected %d parameters but got %d", EXPECTED_PARAMS_SIZE, params.length)); + } + + long fromBlock = Long.parseLong(params[0].toString()); + long toBlock = Long.parseLong(params[1].toString()); + + return new CaptureParams(fromBlock, toBlock); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureToFile.java b/arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureToFile.java new file mode 100644 index 0000000000..958a9b3c14 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/capture/CaptureToFile.java @@ -0,0 +1,86 @@ +/* + * 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.rpc.capture; + +import com.google.common.base.Stopwatch; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.blockcapture.BlockCapturer; +import org.hyperledger.besu.plugin.BesuContext; +import org.hyperledger.besu.plugin.services.TraceService; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; + +/** + * Sets up an RPC endpoint for generating conflated file trace. This class provides an RPC endpoint + * named 'generateConflatedTracesToFileV0' under the 'rollup' namespace. When this endpoint is + * called, it triggers the execution of the 'execute' method, which generates conflated file traces + * based on the provided request parameters and writes them to a file. + */ +@Slf4j +public class CaptureToFile { + private final BesuContext besuContext; + private TraceService traceService; + + public CaptureToFile(final BesuContext besuContext) { + this.besuContext = besuContext; + } + + public String getNamespace() { + return "rollup"; + } + + public String getName() { + return "captureConflation"; + } + + /** + * Handles execution traces generation logic. + * + * @param request holds parameters of the RPC request. + * @return an execution file trace. + */ + public Capture execute(final PluginRpcRequest request) { + if (this.traceService == null) { + this.traceService = getTraceService(); + } + + CaptureParams params = CaptureParams.createTraceParams(request.getParams()); + final long fromBlock = params.fromBlock(); + final long toBlock = params.toBlock(); + final BlockCapturer tracer = new BlockCapturer(); + + Stopwatch sw = Stopwatch.createStarted(); + traceService.trace( + fromBlock, + toBlock, + worldStateBeforeTracing -> { + tracer.setWorld(worldStateBeforeTracing); + tracer.traceStartConflation(toBlock - fromBlock + 1); + }, + tracer::traceEndConflation, + tracer); + log.info("[CAPTURE] capture for {}-{} computed in {}", fromBlock, toBlock, sw); + return new Capture(tracer.toJson()); + } + + private TraceService getTraceService() { + return this.besuContext + .getService(TraceService.class) + .orElseThrow( + () -> + new RuntimeException( + "Unable to find trace service. Please ensure TraceService is registered.")); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/counters/Counters.java b/arithmetization/src/main/java/net/consensys/linea/rpc/counters/Counters.java new file mode 100644 index 0000000000..9e1d2e3610 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/counters/Counters.java @@ -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.rpc.counters; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** FileTrace represents an execution trace. */ +public record Counters( + @JsonProperty("tracesEngineVersion") String tracesEngineVersion, + @JsonProperty("blockNumber") long blockNumber, + @JsonProperty("tracesCounters") Map traceCountersByModule) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/counters/CountersEndpointServicePlugin.java b/arithmetization/src/main/java/net/consensys/linea/rpc/counters/CountersEndpointServicePlugin.java new file mode 100644 index 0000000000..8c46578da4 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/counters/CountersEndpointServicePlugin.java @@ -0,0 +1,75 @@ +/* + * 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.rpc.counters; + +import com.google.auto.service.AutoService; +import net.consensys.linea.AbstractLineaSharedOptionsPlugin; +import org.hyperledger.besu.plugin.BesuContext; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.services.RpcEndpointService; + +/** + * Sets up an RPC endpoint for generating trace counters. + * + *

The CountersEndpointServicePlugin registers an RPC endpoint named + * 'getTracesCountersByBlockNumberV0' under the 'rollup' namespace. When this endpoint is called, + * returns trace counters based on the provided request parameters. See {@link GenerateCountersV0} + */ +@AutoService(BesuPlugin.class) +public class CountersEndpointServicePlugin extends AbstractLineaSharedOptionsPlugin { + private BesuContext besuContext; + private RpcEndpointService rpcEndpointService; + /** + * Register the RPC service. + * + * @param context the BesuContext to be used. + */ + @Override + public void register(final BesuContext context) { + super.register(context); + besuContext = context; + rpcEndpointService = + context + .getService(RpcEndpointService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain RpcEndpointService from the BesuContext.")); + } + + @Override + public void beforeExternalServices() { + super.beforeExternalServices(); + GenerateCountersV0 method = new GenerateCountersV0(besuContext); + createAndRegister(method, rpcEndpointService); + } + + /** + * Create and register the RPC service. + * + * @param method the RollupGenerateCountersV0 method to be used. + * @param rpcEndpointService the RpcEndpointService to be registered. + */ + private void createAndRegister( + final GenerateCountersV0 method, final RpcEndpointService rpcEndpointService) { + rpcEndpointService.registerRPCEndpoint( + method.getNamespace(), method.getName(), method::execute); + } + + /** Start the RPC service. This method loads the OpCodes. */ + @Override + public void start() {} +} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/counters/CountersRequestParams.java b/arithmetization/src/main/java/net/consensys/linea/rpc/counters/CountersRequestParams.java new file mode 100644 index 0000000000..83e48fc69d --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/counters/CountersRequestParams.java @@ -0,0 +1,56 @@ +/* + * 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.rpc.counters; + +import java.security.InvalidParameterException; + +import net.consensys.linea.zktracer.ZkTracer; + +/** Holds needed parameters for sending an execution trace generation request. */ +@SuppressWarnings("unused") +public record CountersRequestParams(long blockNumber, String runtimeVersion) { + private static final int EXPECTED_PARAMS_SIZE = 2; + + /** + * Parses a list of params to a {@link CountersRequestParams} object. + * + * @param params an array of parameters. + * @return a parsed {@link CountersRequestParams} object.. + */ + public static CountersRequestParams createTraceParams(final Object[] params) { + // validate params size + if (params.length != EXPECTED_PARAMS_SIZE) { + throw new InvalidParameterException( + String.format("Expected %d parameters but got %d", EXPECTED_PARAMS_SIZE, params.length)); + } + + long blockNumber = Long.parseLong(params[0].toString()); + String version = params[1].toString(); + + if (!version.equals(getTracerRuntime())) { + throw new InvalidParameterException( + String.format( + "INVALID_TRACES_VERSION: Runtime version is %s, requesting version %s", + getTracerRuntime(), version)); + } + + return new CountersRequestParams(blockNumber, version); + } + + private static String getTracerRuntime() { + return ZkTracer.class.getPackage().getSpecificationVersion(); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/counters/GenerateCountersV0.java b/arithmetization/src/main/java/net/consensys/linea/rpc/counters/GenerateCountersV0.java new file mode 100644 index 0000000000..c59803beef --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/counters/GenerateCountersV0.java @@ -0,0 +1,118 @@ +/* + * 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.rpc.counters; + +import java.util.Map; + +import com.google.common.base.Stopwatch; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.zktracer.ZkTracer; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; +import org.hyperledger.besu.plugin.BesuContext; +import org.hyperledger.besu.plugin.services.TraceService; +import org.hyperledger.besu.plugin.services.exception.PluginRpcEndpointException; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; + +/** This class is used to generate trace counters. */ +@Slf4j +public class GenerateCountersV0 { + private static final int CACHE_SIZE = 10_000; + static final Cache> cache = + CacheBuilder.newBuilder().maximumSize(CACHE_SIZE).build(); + + private final BesuContext besuContext; + private TraceService traceService; + + /** + * Constructor for RollupGenerateCountersV0. + * + * @param besuContext the BesuContext to be used. + */ + public GenerateCountersV0(final BesuContext besuContext) { + this.besuContext = besuContext; + } + + public String getNamespace() { + return "rollup"; + } + + public String getName() { + return "getTracesCountersByBlockNumberV0"; + } + + /** + * Executes an RPC request to generate trace counters. + * + * @param request The PluginRpcRequest object encapsulating the parameters of the RPC request. + * @return A Counters object encapsulating the results of the counters generation (Modules Line + * Count). The method uses a caching mechanism to store and retrieve previously computed trace + * counters for specific block numbers + *

If an exception occurs during the execution of the request, it is caught and wrapped in + * a PluginRpcEndpointException and rethrown. + */ + public Counters execute(final PluginRpcRequest request) { + if (traceService == null) { + traceService = initTraceService(); + } + + try { + final Stopwatch sw = Stopwatch.createStarted(); + final CountersRequestParams params = + CountersRequestParams.createTraceParams(request.getParams()); + final long requestedBlockNumber = params.blockNumber(); + + final Counters r = + new Counters( + params.runtimeVersion(), + requestedBlockNumber, + cache + .asMap() + .computeIfAbsent( + requestedBlockNumber, + blockNumber -> { + final ZkTracer tracer = new ZkTracer(); + traceService.trace( + blockNumber, + blockNumber, + worldStateBeforeTracing -> tracer.traceStartConflation(1), + tracer::traceEndConflation, + tracer); + + return tracer.getModulesLineCount(); + })); + log.info("counters for {} returned in {}", requestedBlockNumber, sw); + return r; + } catch (Exception ex) { + throw new PluginRpcEndpointException(RpcErrorType.PLUGIN_INTERNAL_ERROR, ex.getMessage()); + } + } + + /** + * Initialize the TraceService. + * + * @return the initialized TraceService. + */ + private TraceService initTraceService() { + return besuContext + .getService(TraceService.class) + .orElseThrow( + () -> + new RuntimeException( + "Unable to find trace service. Please ensure TraceService is registered.")); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/GenerateConflatedTracesV0.java b/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/GenerateConflatedTracesV0.java new file mode 100644 index 0000000000..d03bcb5037 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/GenerateConflatedTracesV0.java @@ -0,0 +1,142 @@ +/* + * 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.rpc.tracegeneration; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import com.google.common.base.Stopwatch; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.zktracer.ZkTracer; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; +import org.hyperledger.besu.plugin.BesuContext; +import org.hyperledger.besu.plugin.services.BesuConfiguration; +import org.hyperledger.besu.plugin.services.TraceService; +import org.hyperledger.besu.plugin.services.exception.PluginRpcEndpointException; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; + +/** + * Sets up an RPC endpoint for generating conflated file trace. This class provides an RPC endpoint + * named 'generateConflatedTracesToFileV0' under the 'rollup' namespace. When this endpoint is + * called, it triggers the execution of the 'execute' method, which generates conflated file traces + * based on the provided request parameters and writes them to a file. + */ +@Slf4j +public class GenerateConflatedTracesV0 { + private final BesuContext besuContext; + private Path tracesPath; + private TraceService traceService; + + public GenerateConflatedTracesV0(final BesuContext besuContext) { + this.besuContext = besuContext; + } + + public String getNamespace() { + return "rollup"; + } + + public String getName() { + return "generateConflatedTracesToFileV0"; + } + + /** + * Handles execution traces generation logic. + * + * @param request holds parameters of the RPC request. + * @return an execution file trace. + */ + public TraceFile execute(final PluginRpcRequest request) { + Stopwatch sw = Stopwatch.createStarted(); + if (this.traceService == null) { + this.traceService = getTraceService(); + } + if (this.tracesPath == null) { + this.tracesPath = getTracesPath(); + } + + try { + TraceRequestParams params = TraceRequestParams.createTraceParams(request.getParams()); + + final long fromBlock = params.fromBlock(); + final long toBlock = params.toBlock(); + final ZkTracer tracer = new ZkTracer(); + traceService.trace( + fromBlock, + toBlock, + worldStateBeforeTracing -> tracer.traceStartConflation(toBlock - fromBlock + 1), + tracer::traceEndConflation, + tracer); + log.info("[TRACING] trace for {}-{} computed in {}", fromBlock, toBlock, sw); + sw.reset().start(); + final String path = writeTraceToFile(tracer, params.runtimeVersion()); + log.info("[TRACING] trace for {}-{} serialized to {} in {}", path, toBlock, fromBlock, sw); + return new TraceFile(params.runtimeVersion(), path); + } catch (Exception ex) { + throw new PluginRpcEndpointException(RpcErrorType.PLUGIN_INTERNAL_ERROR, ex.getMessage()); + } + } + + private Path getTracesPath() { + final String envVar = System.getenv("TRACES_DIR"); + if (envVar == null) { + return this.besuContext + .getService(BesuConfiguration.class) + .map(BesuConfiguration::getDataPath) + .map(x -> x.resolve("traces")) + .orElseThrow( + () -> + new RuntimeException( + "Unable to find data path. Please ensure BesuConfiguration is registered.")); + } else { + return Paths.get(envVar); + } + } + + private TraceService getTraceService() { + return this.besuContext + .getService(TraceService.class) + .orElseThrow( + () -> + new RuntimeException( + "Unable to find trace service. Please ensure TraceService is registered.")); + } + + private String writeTraceToFile(final ZkTracer tracer, final String traceRuntimeVersion) { + final Path fileName = generateOutputFileName(traceRuntimeVersion); + tracer.writeToFile(fileName); + return fileName.toAbsolutePath().toString(); + } + + private Path generateOutputFileName(final String tracesEngineVersion) { + if (!Files.isDirectory(tracesPath) && !tracesPath.toFile().mkdirs()) { + throw new RuntimeException( + String.format( + "Trace directory '%s' does not exist and could not be made.", + tracesPath.toAbsolutePath())); + } + + return tracesPath.resolve( + Paths.get( + String.format( + "%.10s-%s.traces.%s", + System.currentTimeMillis(), tracesEngineVersion, getFileFormat()))); + } + + private String getFileFormat() { + return "lt"; + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TraceFile.java b/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TraceFile.java new file mode 100644 index 0000000000..a6248490ae --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TraceFile.java @@ -0,0 +1,23 @@ +/* + * 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.rpc.tracegeneration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** FileTrace represents an execution trace. */ +public record TraceFile( + @JsonProperty("tracesEngineVersion") String tracesEngineVersion, + @JsonProperty("traceFileName") String traceFileName) {} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TraceRequestParams.java b/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TraceRequestParams.java new file mode 100644 index 0000000000..ff785de937 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TraceRequestParams.java @@ -0,0 +1,57 @@ +/* + * 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.rpc.tracegeneration; + +import java.security.InvalidParameterException; + +import net.consensys.linea.zktracer.ZkTracer; + +/** Holds needed parameters for sending an execution trace generation request. */ +@SuppressWarnings("unused") +public record TraceRequestParams(long fromBlock, long toBlock, String runtimeVersion) { + private static final int EXPECTED_PARAMS_SIZE = 3; + + /** + * Parses a list of params to a {@link TraceRequestParams} object. + * + * @param params an array of parameters. + * @return a parsed {@link TraceRequestParams} object.. + */ + public static TraceRequestParams createTraceParams(final Object[] params) { + // validate params size + if (params.length != EXPECTED_PARAMS_SIZE) { + throw new InvalidParameterException( + String.format("Expected %d parameters but got %d", EXPECTED_PARAMS_SIZE, params.length)); + } + + long fromBlock = Long.parseLong(params[0].toString()); + long toBlock = Long.parseLong(params[1].toString()); + String version = params[2].toString(); + + if (!version.equals(getTracerRuntime())) { + throw new InvalidParameterException( + String.format( + "INVALID_TRACES_VERSION: Runtime version is %s, requesting version %s", + getTracerRuntime(), version)); + } + + return new TraceRequestParams(fromBlock, toBlock, version); + } + + private static String getTracerRuntime() { + return ZkTracer.class.getPackage().getSpecificationVersion(); + } +} diff --git a/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TracesEndpointServicePlugin.java b/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TracesEndpointServicePlugin.java new file mode 100644 index 0000000000..b4bb038418 --- /dev/null +++ b/arithmetization/src/main/java/net/consensys/linea/rpc/tracegeneration/TracesEndpointServicePlugin.java @@ -0,0 +1,77 @@ +/* + * 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.rpc.tracegeneration; + +import com.google.auto.service.AutoService; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.AbstractLineaSharedOptionsPlugin; +import org.hyperledger.besu.plugin.BesuContext; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.services.RpcEndpointService; + +/** + * Registers RPC endpoints .This class provides an RPC endpoint named + * 'generateConflatedTracesToFileV0' under the 'rollup' namespace. It uses {@link + * GenerateConflatedTracesV0} to generate conflated file traces. This class provides an RPC endpoint + * named 'generateConflatedTracesToFileV0' under the 'rollup' namespace. + */ +@AutoService(BesuPlugin.class) +@Slf4j +public class TracesEndpointServicePlugin extends AbstractLineaSharedOptionsPlugin { + private BesuContext besuContext; + private RpcEndpointService rpcEndpointService; + /** + * Register the RPC service. + * + * @param context the BesuContext to be used. + */ + @Override + public void register(final BesuContext context) { + super.register(context); + besuContext = context; + rpcEndpointService = + context + .getService(RpcEndpointService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain RpcEndpointService from the BesuContext.")); + } + + @Override + public void beforeExternalServices() { + super.beforeExternalServices(); + GenerateConflatedTracesV0 method = new GenerateConflatedTracesV0(besuContext); + + createAndRegister(method, rpcEndpointService); + } + + /** + * Create and register the RPC service. + * + * @param method the RollupGenerateConflatedTracesToFileV0 method to be used. + * @param rpcEndpointService the RpcEndpointService to be registered. + */ + private void createAndRegister( + final GenerateConflatedTracesV0 method, final RpcEndpointService rpcEndpointService) { + rpcEndpointService.registerRPCEndpoint( + method.getNamespace(), method.getName(), method::execute); + } + + /** Start the RPC service. This method loads the OpCodes. */ + @Override + public void start() {} +} diff --git a/arithmetization/src/test/resources/replays/2982051-2982061.json.gz b/arithmetization/src/test/resources/replays/2982051-2982061.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..0c3e504b04f068d129addd9a436225b76e251fa1 GIT binary patch literal 1033940 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