From 6ebba8d36fcf108fed75996c76970a75bc5e16c9 Mon Sep 17 00:00:00 2001 From: Asuka Date: Mon, 26 Oct 2026 14:47:57 +0800 Subject: [PATCH 1/2] perf(vm): store JUMPDEST positions in a bitmap Replace the HashSet of program counters. Cache key, size, and admission stay the same. --- .../core/vm/program/ProgramPrecompile.java | 24 ++++-- .../core/vm/program/JumpDestBitmapTest.java | 86 +++++++++++++++++++ 2 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 actuator/src/test/java/org/tron/core/vm/program/JumpDestBitmapTest.java diff --git a/actuator/src/main/java/org/tron/core/vm/program/ProgramPrecompile.java b/actuator/src/main/java/org/tron/core/vm/program/ProgramPrecompile.java index 280d4239f54..b3989b3fbf8 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/ProgramPrecompile.java +++ b/actuator/src/main/java/org/tron/core/vm/program/ProgramPrecompile.java @@ -1,7 +1,5 @@ package org.tron.core.vm.program; -import java.util.HashSet; -import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.tron.common.runtime.vm.DataWord; import org.tron.core.vm.Op; @@ -10,15 +8,22 @@ @Slf4j(topic = "VM") public class ProgramPrecompile { - private final Set jumpDest = new HashSet<>(); + // Bit 1 means compile() would have added this pc to the old HashSet. + private final byte[] jumpDestBits; + private final int codeLength; + + private ProgramPrecompile(int codeLength) { + this.codeLength = codeLength; + this.jumpDestBits = new byte[(codeLength + 7) >>> 3]; + } public static ProgramPrecompile compile(byte[] ops) { - ProgramPrecompile ret = new ProgramPrecompile(); + ProgramPrecompile ret = new ProgramPrecompile(ops.length); for (int i = 0; i < ops.length; ++i) { int op = ops[i] & 0xff; if (op == Op.JUMPDEST) { - ret.jumpDest.add(i); + ret.setJumpDest(i); } if (op >= Op.PUSH1 && op <= Op.PUSH32) { @@ -28,6 +33,10 @@ public static ProgramPrecompile compile(byte[] ops) { return ret; } + private void setJumpDest(int pc) { + jumpDestBits[pc >>> 3] |= (byte) (1 << (pc & 7)); + } + public static byte[] getCode(byte[] ops) { for (int i = 0; i < ops.length; ++i) { @@ -54,6 +63,9 @@ public static byte[] getCode(byte[] ops) { } public boolean hasJumpDest(int pc) { - return jumpDest.contains(pc); + if (pc < 0 || pc >= codeLength) { + return false; + } + return (jumpDestBits[pc >>> 3] & (1 << (pc & 7))) != 0; } } diff --git a/actuator/src/test/java/org/tron/core/vm/program/JumpDestBitmapTest.java b/actuator/src/test/java/org/tron/core/vm/program/JumpDestBitmapTest.java new file mode 100644 index 00000000000..92474267d65 --- /dev/null +++ b/actuator/src/test/java/org/tron/core/vm/program/JumpDestBitmapTest.java @@ -0,0 +1,86 @@ +package org.tron.core.vm.program; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.util.HashSet; +import java.util.Set; +import org.junit.Test; +import org.tron.core.vm.Op; + +public class JumpDestBitmapTest { + + @Test + public void bitmapMatchesTheOldJumpDestSet() { + assertSame(new byte[0]); + assertSame(new byte[] {op(Op.JUMPDEST)}); + assertSame(new byte[] {op(Op.PUSH1), op(Op.JUMPDEST), op(Op.JUMPDEST)}); + assertSame(new byte[] {op(Op.PUSH1), op(Op.JUMPDEST)}); + assertSame(truncatedPush32()); + assertSame(everyPushWidth()); + assertSame(allJumpDests(300)); + + ProgramPrecompile analysis = ProgramPrecompile.compile(new byte[] {op(Op.JUMPDEST)}); + assertEquals(false, analysis.hasJumpDest(-1)); + assertEquals(false, analysis.hasJumpDest(1)); + assertEquals(false, analysis.hasJumpDest(Integer.MIN_VALUE)); + } + + private static void assertSame(byte[] ops) { + ProgramPrecompile analysis = ProgramPrecompile.compile(ops); + Set expected = oldJumpDests(ops); + for (int pc = -2; pc <= ops.length + 2; pc++) { + assertEquals(ops.length + " pc " + pc, expected.contains(pc), analysis.hasJumpDest(pc)); + } + } + + private static Set oldJumpDests(byte[] ops) { + Set jumpDest = new HashSet<>(); + for (int i = 0; i < ops.length; ++i) { + int op = ops[i] & 0xff; + if (op == Op.JUMPDEST) { + jumpDest.add(i); + } + if (op >= Op.PUSH1 && op <= Op.PUSH32) { + i += op - Op.PUSH1 + 1; + } + } + return jumpDest; + } + + private static byte[] truncatedPush32() { + byte[] ops = new byte[10]; + ops[0] = op(Op.PUSH32); + ops[3] = op(Op.JUMPDEST); + return ops; + } + + private static byte[] everyPushWidth() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (int width = 1; width <= 32; width++) { + for (int align = 0; align < 8; align++) { + for (int pad = 0; pad < align; pad++) { + out.write(0); + } + out.write(op(Op.PUSH1 + width - 1)); + for (int n = 0; n < width; n++) { + out.write(n == width - 1 ? op(Op.JUMPDEST) : 0); + } + out.write(op(Op.JUMPDEST)); + } + } + return out.toByteArray(); + } + + private static byte op(int opcode) { + return (byte) opcode; + } + + private static byte[] allJumpDests(int length) { + byte[] ops = new byte[length]; + for (int i = 0; i < ops.length; i++) { + ops[i] = op(Op.JUMPDEST); + } + return ops; + } +} From edc038e0de70e5766d227fac0761049a8746652a Mon Sep 17 00:00:00 2001 From: Asuka Date: Tue, 27 Oct 2026 14:53:27 +0800 Subject: [PATCH 2/2] test(vm): cover JUMPDEST bitmap edge cases --- .../core/vm/program/JumpDestBitmapTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/actuator/src/test/java/org/tron/core/vm/program/JumpDestBitmapTest.java b/actuator/src/test/java/org/tron/core/vm/program/JumpDestBitmapTest.java index 92474267d65..9acc0b3ecfc 100644 --- a/actuator/src/test/java/org/tron/core/vm/program/JumpDestBitmapTest.java +++ b/actuator/src/test/java/org/tron/core/vm/program/JumpDestBitmapTest.java @@ -4,6 +4,7 @@ import java.io.ByteArrayOutputStream; import java.util.HashSet; +import java.util.Random; import java.util.Set; import org.junit.Test; import org.tron.core.vm.Op; @@ -26,12 +27,47 @@ public void bitmapMatchesTheOldJumpDestSet() { assertEquals(false, analysis.hasJumpDest(Integer.MIN_VALUE)); } + @Test + public void singleJumpDestDoesNotSetOtherBits() { + for (int length = 1; length <= 65; length++) { + for (int dest = 0; dest < length; dest++) { + byte[] ops = new byte[length]; + ops[dest] = op(Op.JUMPDEST); + ProgramPrecompile analysis = ProgramPrecompile.compile(ops); + for (int pc = -1; pc <= length; pc++) { + assertEquals("length " + length + " dest " + dest + " pc " + pc, + pc == dest, analysis.hasJumpDest(pc)); + } + } + } + } + + @Test + public void randomBytecodeMatchesTheOldJumpDestSet() { + Random random = new Random(0x5b60617fL); + for (int sample = 0; sample < 10000; sample++) { + byte[] ops = new byte[random.nextInt(8193)]; + random.nextBytes(ops); + // Exercise both arbitrary bytecode and bytecode with densely packed destinations. + if ((sample & 1) != 0) { + for (int pc = 0; pc < ops.length; pc++) { + if (random.nextInt(4) == 0) { + ops[pc] = op(Op.JUMPDEST); + } + } + } + assertSame(ops); + } + } + private static void assertSame(byte[] ops) { ProgramPrecompile analysis = ProgramPrecompile.compile(ops); Set expected = oldJumpDests(ops); for (int pc = -2; pc <= ops.length + 2; pc++) { assertEquals(ops.length + " pc " + pc, expected.contains(pc), analysis.hasJumpDest(pc)); } + assertEquals(false, analysis.hasJumpDest(Integer.MIN_VALUE)); + assertEquals(false, analysis.hasJumpDest(Integer.MAX_VALUE)); } private static Set oldJumpDests(byte[] ops) {