From 4f70a186b264c3f9c47def3284a6c85be081a66a Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Tue, 21 Jul 2026 22:41:55 -0400 Subject: [PATCH] feat: add 256-bit A/B arithmetic, hashing-fee hook, passthrough cleanup Batch of three AT-library features for the next pinned release: * A/B 256-bit arithmetic (function codes 0x0140-0x0147): ADD_A_TO_B, ADD_B_TO_A, SUB_A_FROM_B, SUB_B_FROM_A, MUL_A_BY_B, MUL_B_BY_A, DIV_A_BY_B, DIV_B_BY_A. A1..A4 / B1..B4 are treated as one 256-bit little-endian unsigned integer (least significant register first), matching Signum's extended API codes and argument direction. Add, subtract and multiply wrap modulo 2^256 (bit-identical to Signum's signed two's-complement results). Division is unsigned and truncating, and divide-by-zero throws IllegalOperationException to match this library's existing DIV_DAT/DIV_VAL error convention (Signum instead silently no-ops; a fatal error is safer for consensus code). * Hashing-fee discrimination: new API.getHashingFunctionSteps() hook is consulted for the built-in hashing functions (MD5/RMD160/SHA256/ HASH160 variants, see FunctionCode.isHashingFunction()) so platforms can price hashing differently from the flat per-function-call cost without matching raw function code values. Default delegates to the existing getOpCodeSteps() overloads, so with no override execution costs are unchanged, including for platforms that only override the general external-function overload. * Platform passthrough cleanup: the 0x0500-0x06ff platform-specific range is now expressed via documented PLATFORM_CODE_START/END constants and FunctionCode.isPlatformFunctionCode(), with expanded API_PASSTHROUGH javadoc describing the dispatch contract. Behavior of valueOf() is unchanged. All existing bytecode executes with identical behavior and cost; new function codes were previously fatal "unknown function code" errors. Co-Authored-By: Claude Fable 5 --- Java/src/main/java/org/ciyam/at/API.java | 18 ++ .../main/java/org/ciyam/at/FunctionCode.java | 194 +++++++++++++- .../main/java/org/ciyam/at/MachineState.java | 7 +- .../at/ABArithmeticFunctionCodeTests.java | 248 ++++++++++++++++++ .../ciyam/at/HashingFunctionStepTests.java | 177 +++++++++++++ .../ciyam/at/PlatformPassthroughTests.java | 111 ++++++++ 6 files changed, 752 insertions(+), 3 deletions(-) create mode 100644 Java/src/test/java/org/ciyam/at/ABArithmeticFunctionCodeTests.java create mode 100644 Java/src/test/java/org/ciyam/at/HashingFunctionStepTests.java create mode 100644 Java/src/test/java/org/ciyam/at/PlatformPassthroughTests.java diff --git a/Java/src/main/java/org/ciyam/at/API.java b/Java/src/main/java/org/ciyam/at/API.java index 9e98575..908b810 100644 --- a/Java/src/main/java/org/ciyam/at/API.java +++ b/Java/src/main/java/org/ciyam/at/API.java @@ -59,6 +59,24 @@ public int getOpCodeSteps(OpCode opcode, short rawFunctionCode, MachineState sta return this.getOpCodeSteps(opcode); } + /** + * Returns fee for executing one of the built-in hashing functions in terms of execution "steps". + *

+ * This is called instead of {@link #getOpCodeSteps(OpCode, short, MachineState)} when the external-function + * opcode about to execute calls one of the built-in hashing functions (see {@link FunctionCode#isHashingFunction()}), + * allowing platforms to charge hashing differently from the flat per-function-call cost without matching raw + * function code values themselves. The default delegates to the general external-function overload, which in turn + * defaults to the opcode-only method, so existing API implementations and pricing remain unchanged. + * + * @param opcode external-function opcode about to execute + * @param functionCode built-in hashing function about to execute + * @param state current machine state before the function is charged or executed + * @return number of execution steps to charge for the function call + */ + public int getHashingFunctionSteps(OpCode opcode, FunctionCode functionCode, MachineState state) { + return this.getOpCodeSteps(opcode, functionCode.value, state); + } + /** Returns fee per execution "step" */ public abstract long getFeePerStep(); diff --git a/Java/src/main/java/org/ciyam/at/FunctionCode.java b/Java/src/main/java/org/ciyam/at/FunctionCode.java index 74b700f..3a139ef 100644 --- a/Java/src/main/java/org/ciyam/at/FunctionCode.java +++ b/Java/src/main/java/org/ciyam/at/FunctionCode.java @@ -1,5 +1,6 @@ package org.ciyam.at; +import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -647,6 +648,116 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s functionData.returnValue = Long.valueOf(result); } }, + /** + * Add A to B (256-bit)
+ * 0x0140
+ * B = B + A
+ * A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256. + */ + ADD_A_TO_B(0x0140, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + storeUnsigned256IntoB(state, unsigned256FromB(state).add(unsigned256FromA(state))); + } + }, + /** + * Add B to A (256-bit)
+ * 0x0141
+ * A = A + B
+ * A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256. + */ + ADD_B_TO_A(0x0141, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + storeUnsigned256IntoA(state, unsigned256FromA(state).add(unsigned256FromB(state))); + } + }, + /** + * Subtract A from B (256-bit)
+ * 0x0142
+ * B = B - A
+ * A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256. + */ + SUB_A_FROM_B(0x0142, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + storeUnsigned256IntoB(state, unsigned256FromB(state).subtract(unsigned256FromA(state))); + } + }, + /** + * Subtract B from A (256-bit)
+ * 0x0143
+ * A = A - B
+ * A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256. + */ + SUB_B_FROM_A(0x0143, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + storeUnsigned256IntoA(state, unsigned256FromA(state).subtract(unsigned256FromB(state))); + } + }, + /** + * Multiply A by B (256-bit)
+ * 0x0144
+ * B = A * B
+ * A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256, + * i.e. only the low 256 bits of the product are kept. + */ + MUL_A_BY_B(0x0144, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + storeUnsigned256IntoB(state, unsigned256FromA(state).multiply(unsigned256FromB(state))); + } + }, + /** + * Multiply B by A (256-bit)
+ * 0x0145
+ * A = A * B
+ * A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256, + * i.e. only the low 256 bits of the product are kept. + */ + MUL_B_BY_A(0x0145, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + storeUnsigned256IntoA(state, unsigned256FromA(state).multiply(unsigned256FromB(state))); + } + }, + /** + * Divide A by B (256-bit)
+ * 0x0146
+ * B = A / B
+ * A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Division is unsigned and truncating.
+ * Can also throw IllegalOperationException if divide-by-zero attempted. + */ + DIV_A_BY_B(0x0146, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + BigInteger divisor = unsigned256FromB(state); + + if (divisor.signum() == 0) + throw new IllegalOperationException("Divide by zero"); + + storeUnsigned256IntoB(state, unsigned256FromA(state).divide(divisor)); + } + }, + /** + * Divide B by A (256-bit)
+ * 0x0147
+ * A = B / A
+ * A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Division is unsigned and truncating.
+ * Can also throw IllegalOperationException if divide-by-zero attempted. + */ + DIV_B_BY_A(0x0147, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + BigInteger divisor = unsigned256FromA(state); + + if (divisor.signum() == 0) + throw new IllegalOperationException("Divide by zero"); + + storeUnsigned256IntoA(state, unsigned256FromB(state).divide(divisor)); + } + }, /** * MD5 data into B
* 0x0200 start-addr byte-length
@@ -1128,7 +1239,13 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s /** * 0x0500 - 0x06ff
* Platform-specific functions.
- * These are passed through to the API + *

+ * Function codes in the range {@link #PLATFORM_CODE_START} to {@link #PLATFORM_CODE_END} (inclusive) are not implemented, + * or even enumerated, by this library. Instead they are dispatched to the platform's {@link API} implementation: + * signature checks via {@link API#platformSpecificPreExecuteCheck(int, boolean, MachineState, short)} + * and execution via {@link API#platformSpecificPostCheckExecute(FunctionData, MachineState, short)}, + * both receiving the raw function code. This allows platforms to add whole families of function codes + * (e.g. chain-query functions, persistent-map functions) without requiring a new release of this library. */ API_PASSTHROUGH(0x0500, 0, false) { @Override @@ -1148,6 +1265,11 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s public final int paramCount; public final boolean returnsValue; + /** First function code (inclusive) of the platform-specific range dispatched to the API - see {@link #API_PASSTHROUGH}. */ + public static final int PLATFORM_CODE_START = 0x0500; + /** Last function code (inclusive) of the platform-specific range dispatched to the API - see {@link #API_PASSTHROUGH}. */ + public static final int PLATFORM_CODE_END = 0x06ff; + private static final Map map = Arrays.stream(FunctionCode.values()) .collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode)); @@ -1157,14 +1279,37 @@ private FunctionCode(int value, int paramCount, boolean returnsValue) { this.returnsValue = returnsValue; } + /** Returns whether value is within the platform-specific function code range dispatched to the API. */ + public static boolean isPlatformFunctionCode(int value) { + return value >= PLATFORM_CODE_START && value <= PLATFORM_CODE_END; + } + public static FunctionCode valueOf(int value) { // Platform-specific? - if (value >= 0x0500 && value <= 0x06ff) + if (isPlatformFunctionCode(value)) return API_PASSTHROUGH; return map.get((short) value); } + /** Returns whether this function is one of the built-in hashing functions, which platforms may price separately - see {@link API#getHashingFunctionSteps}. */ + public boolean isHashingFunction() { + switch (this) { + case MD5_INTO_B: + case CHECK_MD5_WITH_B: + case RMD160_INTO_B: + case CHECK_RMD160_WITH_B: + case SHA256_INTO_B: + case CHECK_SHA256_WITH_B: + case HASH160_INTO_B: + case CHECK_HASH160_WITH_B: + return true; + + default: + return false; + } + } + public void preExecuteCheck(int paramCount, boolean returnValueExpected) throws ExecutionException { if (paramCount != this.paramCount) throw new IllegalFunctionCodeException( @@ -1239,4 +1384,49 @@ protected void checkDataAddress(MachineState state, long address, int count) thr throw new ExecutionException(String.format("%s data address %d out of bounds: 0 to %d", this.name(), address, maxAddress)); } + /** Mask used to wrap 256-bit arithmetic results, i.e. 2256 - 1. */ + private static final BigInteger UNSIGNED_256_MASK = BigInteger.ONE.shiftLeft(256).subtract(BigInteger.ONE); + + /** Returns A1 through A4 as an unsigned 256-bit integer, treating A1 as least significant. */ + protected static BigInteger unsigned256FromA(MachineState state) { + return toUnsigned256(state.a1, state.a2, state.a3, state.a4); + } + + /** Returns B1 through B4 as an unsigned 256-bit integer, treating B1 as least significant. */ + protected static BigInteger unsigned256FromB(MachineState state) { + return toUnsigned256(state.b1, state.b2, state.b3, state.b4); + } + + /** Returns four 64-bit values as an unsigned 256-bit integer, treating value1 as least significant. */ + private static BigInteger toUnsigned256(long value1, long value2, long value3, long value4) { + ByteBuffer byteBuffer = ByteBuffer.allocate(4 * MachineState.VALUE_SIZE); + + byteBuffer.putLong(value4); + byteBuffer.putLong(value3); + byteBuffer.putLong(value2); + byteBuffer.putLong(value1); + + return new BigInteger(1, byteBuffer.array()); + } + + /** Stores low 256 bits of value into A1 through A4, treating A1 as least significant. */ + protected static void storeUnsigned256IntoA(MachineState state, BigInteger value) { + BigInteger wrapped = value.and(UNSIGNED_256_MASK); + + state.a1 = wrapped.longValue(); + state.a2 = wrapped.shiftRight(64).longValue(); + state.a3 = wrapped.shiftRight(128).longValue(); + state.a4 = wrapped.shiftRight(192).longValue(); + } + + /** Stores low 256 bits of value into B1 through B4, treating B1 as least significant. */ + protected static void storeUnsigned256IntoB(MachineState state, BigInteger value) { + BigInteger wrapped = value.and(UNSIGNED_256_MASK); + + state.b1 = wrapped.longValue(); + state.b2 = wrapped.shiftRight(64).longValue(); + state.b3 = wrapped.shiftRight(128).longValue(); + state.b4 = wrapped.shiftRight(192).longValue(); + } + } diff --git a/Java/src/main/java/org/ciyam/at/MachineState.java b/Java/src/main/java/org/ciyam/at/MachineState.java index 3974bd7..c826f7b 100644 --- a/Java/src/main/java/org/ciyam/at/MachineState.java +++ b/Java/src/main/java/org/ciyam/at/MachineState.java @@ -859,7 +859,12 @@ public void execute() { && nextOpCode.value <= OpCode.EXT_FUN_VAL.value && this.codeByteBuffer.remaining() >= Short.BYTES) { short rawFunctionCode = this.codeByteBuffer.getShort(this.codeByteBuffer.position()); - opcodeSteps = this.api.getOpCodeSteps(nextOpCode, rawFunctionCode, this); + FunctionCode functionCode = FunctionCode.valueOf(rawFunctionCode); + + if (functionCode != null && functionCode.isHashingFunction()) + opcodeSteps = this.api.getHashingFunctionSteps(nextOpCode, functionCode, this); + else + opcodeSteps = this.api.getOpCodeSteps(nextOpCode, rawFunctionCode, this); } else { opcodeSteps = this.api.getOpCodeSteps(nextOpCode); } diff --git a/Java/src/test/java/org/ciyam/at/ABArithmeticFunctionCodeTests.java b/Java/src/test/java/org/ciyam/at/ABArithmeticFunctionCodeTests.java new file mode 100644 index 0000000..66c3f78 --- /dev/null +++ b/Java/src/test/java/org/ciyam/at/ABArithmeticFunctionCodeTests.java @@ -0,0 +1,248 @@ +package org.ciyam.at; + +import static org.junit.Assert.*; + +import org.ciyam.at.test.ExecutableTest; +import org.junit.Test; + +/** + * Tests for the 256-bit A/B arithmetic function codes (0x0140 - 0x0147). + *

+ * A1..A4 / B1..B4 are treated as 256-bit unsigned integers, least significant register first. + */ +public class ABArithmeticFunctionCodeTests extends ExecutableTest { + + private static final int A_SOURCE_ADDRESS = 0; + private static final int B_SOURCE_ADDRESS = 4; + private static final int A_RESULT_ADDRESS = 8; + private static final int B_RESULT_ADDRESS = 12; + + private static final long ALL_ONES = 0xffffffffffffffffL; + private static final long TOP_BIT = 0x8000000000000000L; + + @Test + public void testAddAToB() throws ExecutionException { + // B = B + A + executeFunction(FunctionCode.ADD_A_TO_B, + new long[] { 3L, 0L, 0L, 0L }, + new long[] { 5L, 0L, 0L, 0L }); + + assertA("A should be unmodified", 3L, 0L, 0L, 0L); + assertB("B should hold sum", 8L, 0L, 0L, 0L); + } + + @Test + public void testAddBToA() throws ExecutionException { + // A = A + B + executeFunction(FunctionCode.ADD_B_TO_A, + new long[] { 3L, 0L, 0L, 0L }, + new long[] { 5L, 0L, 0L, 0L }); + + assertA("A should hold sum", 8L, 0L, 0L, 0L); + assertB("B should be unmodified", 5L, 0L, 0L, 0L); + } + + @Test + public void testAddCarriesAcrossAllLimbs() throws ExecutionException { + // B is 2^192 - 1 so adding 1 carries through limbs 1, 2 and 3 into limb 4 + executeFunction(FunctionCode.ADD_A_TO_B, + new long[] { 1L, 0L, 0L, 0L }, + new long[] { ALL_ONES, ALL_ONES, ALL_ONES, 0L }); + + assertB("carry should propagate to top limb", 0L, 0L, 0L, 1L); + } + + @Test + public void testAddWrapsModulo2To256() throws ExecutionException { + // B is 2^256 - 1 so adding 5 wraps to 4 + executeFunction(FunctionCode.ADD_A_TO_B, + new long[] { 5L, 0L, 0L, 0L }, + new long[] { ALL_ONES, ALL_ONES, ALL_ONES, ALL_ONES }); + + assertB("sum should wrap modulo 2^256", 4L, 0L, 0L, 0L); + } + + @Test + public void testSubAFromB() throws ExecutionException { + // B = B - A + executeFunction(FunctionCode.SUB_A_FROM_B, + new long[] { 3L, 0L, 0L, 0L }, + new long[] { 5L, 0L, 0L, 0L }); + + assertA("A should be unmodified", 3L, 0L, 0L, 0L); + assertB("B should hold difference", 2L, 0L, 0L, 0L); + } + + @Test + public void testSubBFromA() throws ExecutionException { + // A = A - B + executeFunction(FunctionCode.SUB_B_FROM_A, + new long[] { 5L, 0L, 0L, 0L }, + new long[] { 3L, 0L, 0L, 0L }); + + assertA("A should hold difference", 2L, 0L, 0L, 0L); + assertB("B should be unmodified", 3L, 0L, 0L, 0L); + } + + @Test + public void testSubBorrowsAcrossAllLimbs() throws ExecutionException { + // B is 2^192 so subtracting 1 borrows through limbs 3, 2 and 1 + executeFunction(FunctionCode.SUB_A_FROM_B, + new long[] { 1L, 0L, 0L, 0L }, + new long[] { 0L, 0L, 0L, 1L }); + + assertB("borrow should propagate from top limb", ALL_ONES, ALL_ONES, ALL_ONES, 0L); + } + + @Test + public void testSubWrapsModulo2To256() throws ExecutionException { + // 0 - 1 should wrap to 2^256 - 1 + executeFunction(FunctionCode.SUB_A_FROM_B, + new long[] { 1L, 0L, 0L, 0L }, + new long[] { 0L, 0L, 0L, 0L }); + + assertB("difference should wrap modulo 2^256", ALL_ONES, ALL_ONES, ALL_ONES, ALL_ONES); + } + + @Test + public void testMulAByB() throws ExecutionException { + // B = A * B + executeFunction(FunctionCode.MUL_A_BY_B, + new long[] { 6L, 0L, 0L, 0L }, + new long[] { 7L, 0L, 0L, 0L }); + + assertA("A should be unmodified", 6L, 0L, 0L, 0L); + assertB("B should hold product", 42L, 0L, 0L, 0L); + } + + @Test + public void testMulBByA() throws ExecutionException { + // A = A * B + executeFunction(FunctionCode.MUL_B_BY_A, + new long[] { 6L, 0L, 0L, 0L }, + new long[] { 7L, 0L, 0L, 0L }); + + assertA("A should hold product", 42L, 0L, 0L, 0L); + assertB("B should be unmodified", 7L, 0L, 0L, 0L); + } + + @Test + public void testMulCarriesAcrossLimbs() throws ExecutionException { + // 2^64 * 2^64 = 2^128, i.e. limb 3 + executeFunction(FunctionCode.MUL_A_BY_B, + new long[] { 0L, 1L, 0L, 0L }, + new long[] { 0L, 1L, 0L, 0L }); + + assertB("product should reach limb 3", 0L, 0L, 1L, 0L); + } + + @Test + public void testMulWrapsModulo2To256() throws ExecutionException { + // 3 * 2^255 = 2^256 + 2^255, and only the low 256 bits (2^255) are kept + executeFunction(FunctionCode.MUL_A_BY_B, + new long[] { 3L, 0L, 0L, 0L }, + new long[] { 0L, 0L, 0L, TOP_BIT }); + + assertB("product should wrap modulo 2^256", 0L, 0L, 0L, TOP_BIT); + } + + @Test + public void testDivAByB() throws ExecutionException { + // B = A / B, with unsigned truncating division + executeFunction(FunctionCode.DIV_A_BY_B, + new long[] { 7L, 0L, 0L, 0L }, + new long[] { 2L, 0L, 0L, 0L }); + + assertA("A should be unmodified", 7L, 0L, 0L, 0L); + assertB("B should hold truncated quotient", 3L, 0L, 0L, 0L); + } + + @Test + public void testDivBByA() throws ExecutionException { + // A = B / A, with unsigned truncating division + executeFunction(FunctionCode.DIV_B_BY_A, + new long[] { 2L, 0L, 0L, 0L }, + new long[] { 7L, 0L, 0L, 0L }); + + assertA("A should hold truncated quotient", 3L, 0L, 0L, 0L); + assertB("B should be unmodified", 7L, 0L, 0L, 0L); + } + + @Test + public void testDivQuotientSpansLimbs() throws ExecutionException { + // 2^192 / 2 = 2^191, i.e. top bit of limb 3 + executeFunction(FunctionCode.DIV_A_BY_B, + new long[] { 0L, 0L, 0L, 1L }, + new long[] { 2L, 0L, 0L, 0L }); + + assertB("quotient should span limb boundary", 0L, 0L, TOP_BIT, 0L); + } + + @Test + public void testDivIsUnsigned() throws ExecutionException { + // A is 2^255, which is negative if interpreted as signed, so unsigned division must give 2^254 + executeFunction(FunctionCode.DIV_A_BY_B, + new long[] { 0L, 0L, 0L, TOP_BIT }, + new long[] { 2L, 0L, 0L, 0L }); + + assertB("division should be unsigned", 0L, 0L, 0L, 0x4000000000000000L); + } + + @Test + public void testDivAByZeroBIsFatalError() throws ExecutionException { + executeFunction(FunctionCode.DIV_A_BY_B, + new long[] { 7L, 0L, 0L, 0L }, + new long[] { 0L, 0L, 0L, 0L }); + + assertTrue(state.isFinished()); + assertTrue(state.hadFatalError()); + } + + @Test + public void testDivBByZeroAIsFatalError() throws ExecutionException { + executeFunction(FunctionCode.DIV_B_BY_A, + new long[] { 0L, 0L, 0L, 0L }, + new long[] { 7L, 0L, 0L, 0L }); + + assertTrue(state.isFinished()); + assertTrue(state.hadFatalError()); + } + + /** Loads A and B registers with passed values, executes passed function, then saves A and B back into the data segment. */ + private void executeFunction(FunctionCode functionCode, long[] aValues, long[] bValues) { + // A register source values + for (long aValue : aValues) + dataByteBuffer.putLong(aValue); + + // B register source values + assertEquals(B_SOURCE_ADDRESS * MachineState.VALUE_SIZE, dataByteBuffer.position()); + for (long bValue : bValues) + dataByteBuffer.putLong(bValue); + + codeByteBuffer.put(OpCode.EXT_FUN_VAL.value).putShort(FunctionCode.SET_A_DAT.value).putLong(A_SOURCE_ADDRESS); + codeByteBuffer.put(OpCode.EXT_FUN_VAL.value).putShort(FunctionCode.SET_B_DAT.value).putLong(B_SOURCE_ADDRESS); + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(functionCode.value); + codeByteBuffer.put(OpCode.EXT_FUN_VAL.value).putShort(FunctionCode.GET_A_DAT.value).putLong(A_RESULT_ADDRESS); + codeByteBuffer.put(OpCode.EXT_FUN_VAL.value).putShort(FunctionCode.GET_B_DAT.value).putLong(B_RESULT_ADDRESS); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + } + + private void assertA(String message, long expectedA1, long expectedA2, long expectedA3, long expectedA4) { + assertFalse(state.hadFatalError()); + assertEquals(message + " (A1)", expectedA1, getData(A_RESULT_ADDRESS)); + assertEquals(message + " (A2)", expectedA2, getData(A_RESULT_ADDRESS + 1)); + assertEquals(message + " (A3)", expectedA3, getData(A_RESULT_ADDRESS + 2)); + assertEquals(message + " (A4)", expectedA4, getData(A_RESULT_ADDRESS + 3)); + } + + private void assertB(String message, long expectedB1, long expectedB2, long expectedB3, long expectedB4) { + assertFalse(state.hadFatalError()); + assertEquals(message + " (B1)", expectedB1, getData(B_RESULT_ADDRESS)); + assertEquals(message + " (B2)", expectedB2, getData(B_RESULT_ADDRESS + 1)); + assertEquals(message + " (B3)", expectedB3, getData(B_RESULT_ADDRESS + 2)); + assertEquals(message + " (B4)", expectedB4, getData(B_RESULT_ADDRESS + 3)); + } + +} diff --git a/Java/src/test/java/org/ciyam/at/HashingFunctionStepTests.java b/Java/src/test/java/org/ciyam/at/HashingFunctionStepTests.java new file mode 100644 index 0000000..0284236 --- /dev/null +++ b/Java/src/test/java/org/ciyam/at/HashingFunctionStepTests.java @@ -0,0 +1,177 @@ +package org.ciyam.at; + +import org.ciyam.at.test.ExecutableTest; +import org.ciyam.at.test.TestAPI; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * Tests for platform-defined pricing of the built-in hashing functions. + *

+ * With no platform override, hashing functions must cost exactly the same as any other function call. + */ +public class HashingFunctionStepTests extends ExecutableTest { + + private static final int DATA_START_ADDRESS = 2; + private static final int DATA_BYTE_LENGTH = 16; + + @Test + public void testIsHashingFunctionClassification() { + FunctionCode[] hashingFunctions = new FunctionCode[] { + FunctionCode.MD5_INTO_B, FunctionCode.CHECK_MD5_WITH_B, + FunctionCode.RMD160_INTO_B, FunctionCode.CHECK_RMD160_WITH_B, + FunctionCode.SHA256_INTO_B, FunctionCode.CHECK_SHA256_WITH_B, + FunctionCode.HASH160_INTO_B, FunctionCode.CHECK_HASH160_WITH_B }; + + for (FunctionCode hashingFunction : hashingFunctions) + assertTrue(hashingFunction.name() + " should be a hashing function", hashingFunction.isHashingFunction()); + + for (FunctionCode functionCode : FunctionCode.values()) + if (functionCode.value < 0x0200 || functionCode.value > 0x0207) + assertFalse(functionCode.name() + " should not be a hashing function", functionCode.isHashingFunction()); + } + + @Test + public void testDefaultHashingPricingIsUnchanged() { + addHashingCode(FunctionCode.SHA256_INTO_B); + + execute(true); + + assertTrue(state.isStopped()); + assertFalse(state.hadFatalError()); + // SET_VAL + SET_VAL + EXT_FUN_DAT_2 + STP_IMD + assertEquals(1 + 1 + TestAPI.STEPS_PER_FUNCTION_CALL + 1, state.getSteps()); + } + + @Test + public void testHashingHookReceivesFunctionCodeAndState() { + int hashingSteps = 77; + HashingPricingTestAPI pricingApi = new HashingPricingTestAPI(hashingSteps); + api = pricingApi; + long initialBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + + addHashingCode(FunctionCode.SHA256_INTO_B); + + execute(true); + + assertTrue(state.isStopped()); + assertFalse(state.hadFatalError()); + assertEquals(OpCode.EXT_FUN_DAT_2, pricingApi.pricedOpCode); + assertSame(FunctionCode.SHA256_INTO_B, pricingApi.pricedFunctionCode); + assertSame(state, pricingApi.pricedState); + assertEquals(1, pricingApi.hashingPricings); + assertEquals(1 + 1 + hashingSteps + 1, state.getSteps()); + assertEquals(initialBalance - 1 - 1 - hashingSteps - 1, api.accounts.get(TestAPI.AT_ADDRESS).balance); + } + + @Test + public void testHashingOverrideAppliesToEveryHashingFunction() { + FunctionCode[] hashingFunctions = new FunctionCode[] { + FunctionCode.MD5_INTO_B, FunctionCode.RMD160_INTO_B, + FunctionCode.SHA256_INTO_B, FunctionCode.HASH160_INTO_B }; + + int hashingSteps = 20; + HashingPricingTestAPI pricingApi = new HashingPricingTestAPI(hashingSteps); + api = pricingApi; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(DATA_START_ADDRESS); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(DATA_BYTE_LENGTH); + for (FunctionCode hashingFunction : hashingFunctions) + codeByteBuffer.put(OpCode.EXT_FUN_DAT_2.value).putShort(hashingFunction.value).putInt(0).putInt(1); + codeByteBuffer.put(OpCode.STP_IMD.value); + + execute(true); + + assertTrue(state.isStopped()); + assertFalse(state.hadFatalError()); + assertEquals(hashingFunctions.length, pricingApi.hashingPricings); + assertEquals(1 + 1 + hashingFunctions.length * hashingSteps + 1, state.getSteps()); + } + + @Test + public void testHashingOverrideDoesNotAffectOtherFunctions() { + int hashingSteps = 77; + HashingPricingTestAPI pricingApi = new HashingPricingTestAPI(hashingSteps); + api = pricingApi; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(DATA_START_ADDRESS); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(DATA_BYTE_LENGTH); + // Non-hashing function call should still be charged the ordinary flat cost + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SWAP_A_AND_B.value); + codeByteBuffer.put(OpCode.EXT_FUN_DAT_2.value).putShort(FunctionCode.SHA256_INTO_B.value).putInt(0).putInt(1); + codeByteBuffer.put(OpCode.STP_IMD.value); + + execute(true); + + assertTrue(state.isStopped()); + assertFalse(state.hadFatalError()); + assertEquals(1, pricingApi.hashingPricings); + assertEquals(1 + 1 + TestAPI.STEPS_PER_FUNCTION_CALL + hashingSteps + 1, state.getSteps()); + } + + @Test + public void testGeneralFunctionPricingOverrideStillAppliesToHashing() { + // Platforms that only override the general external-function overload (like existing consumers) + // must still have that override consulted for hashing functions, via default delegation. + int functionSteps = 33; + GeneralPricingTestAPI pricingApi = new GeneralPricingTestAPI(functionSteps); + api = pricingApi; + + addHashingCode(FunctionCode.MD5_INTO_B); + + execute(true); + + assertTrue(state.isStopped()); + assertFalse(state.hadFatalError()); + assertEquals(FunctionCode.MD5_INTO_B.value, pricingApi.pricedFunctionCode); + assertEquals(1 + 1 + functionSteps + 1, state.getSteps()); + } + + private void addHashingCode(FunctionCode hashingFunction) { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(DATA_START_ADDRESS); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(DATA_BYTE_LENGTH); + codeByteBuffer.put(OpCode.EXT_FUN_DAT_2.value).putShort(hashingFunction.value).putInt(0).putInt(1); + codeByteBuffer.put(OpCode.STP_IMD.value); + } + + private static class HashingPricingTestAPI extends TestAPI { + private final int hashingSteps; + private OpCode pricedOpCode; + private FunctionCode pricedFunctionCode; + private MachineState pricedState; + private int hashingPricings; + + private HashingPricingTestAPI(int hashingSteps) { + this.hashingSteps = hashingSteps; + } + + @Override + public int getHashingFunctionSteps(OpCode opcode, FunctionCode functionCode, MachineState state) { + this.pricedOpCode = opcode; + this.pricedFunctionCode = functionCode; + this.pricedState = state; + ++this.hashingPricings; + return this.hashingSteps; + } + } + + private static class GeneralPricingTestAPI extends TestAPI { + private final int functionSteps; + private short pricedFunctionCode; + + private GeneralPricingTestAPI(int functionSteps) { + this.functionSteps = functionSteps; + } + + @Override + public int getOpCodeSteps(OpCode opcode, short rawFunctionCode, MachineState state) { + this.pricedFunctionCode = rawFunctionCode; + return this.functionSteps; + } + } + +} diff --git a/Java/src/test/java/org/ciyam/at/PlatformPassthroughTests.java b/Java/src/test/java/org/ciyam/at/PlatformPassthroughTests.java new file mode 100644 index 0000000..5fb6008 --- /dev/null +++ b/Java/src/test/java/org/ciyam/at/PlatformPassthroughTests.java @@ -0,0 +1,111 @@ +package org.ciyam.at; + +import org.ciyam.at.test.ExecutableTest; +import org.ciyam.at.test.TestAPI; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * Tests for the platform-specific function code range (0x0500 - 0x06ff) being dispatched + * to the API without this library enumerating the individual codes. + */ +public class PlatformPassthroughTests extends ExecutableTest { + + /** A platform function code, deep in the 0x06xx sub-range, that the library knows nothing about. */ + private static final short CUSTOM_PLATFORM_FUNCTION_CODE = 0x0677; + + @Test + public void testPlatformRangeBounds() { + assertFalse(FunctionCode.isPlatformFunctionCode(FunctionCode.PLATFORM_CODE_START - 1)); + assertTrue(FunctionCode.isPlatformFunctionCode(FunctionCode.PLATFORM_CODE_START)); + assertTrue(FunctionCode.isPlatformFunctionCode(0x0600)); + assertTrue(FunctionCode.isPlatformFunctionCode(FunctionCode.PLATFORM_CODE_END)); + assertFalse(FunctionCode.isPlatformFunctionCode(FunctionCode.PLATFORM_CODE_END + 1)); + } + + @Test + public void testPlatformRangeMapsToApiPassthrough() { + assertSame(FunctionCode.API_PASSTHROUGH, FunctionCode.valueOf(FunctionCode.PLATFORM_CODE_START)); + assertSame(FunctionCode.API_PASSTHROUGH, FunctionCode.valueOf(0x0600)); + assertSame(FunctionCode.API_PASSTHROUGH, FunctionCode.valueOf(FunctionCode.PLATFORM_CODE_END)); + + assertNull(FunctionCode.valueOf(FunctionCode.PLATFORM_CODE_START - 1)); + assertNull(FunctionCode.valueOf(FunctionCode.PLATFORM_CODE_END + 1)); + } + + @Test + public void testCustomPlatformFunctionIsDispatchedToApi() { + PassthroughTestAPI passthroughApi = new PassthroughTestAPI(); + api = passthroughApi; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(12345L); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(CUSTOM_PLATFORM_FUNCTION_CODE).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + assertTrue(state.isFinished()); + assertFalse(state.hadFatalError()); + assertEquals(CUSTOM_PLATFORM_FUNCTION_CODE, passthroughApi.preCheckedFunctionCode); + assertEquals(CUSTOM_PLATFORM_FUNCTION_CODE, passthroughApi.executedFunctionCode); + assertEquals(12345L, passthroughApi.executedValue1); + } + + @Test + public void testCustomPlatformFunctionWrongSignatureIsFatalError() { + PassthroughTestAPI passthroughApi = new PassthroughTestAPI(); + api = passthroughApi; + + // Our custom function takes one arg and returns no value, so EXT_FUN_RET is the wrong opcode for it + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(CUSTOM_PLATFORM_FUNCTION_CODE).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + assertTrue(state.isFinished()); + assertTrue(state.hadFatalError()); + assertEquals(CUSTOM_PLATFORM_FUNCTION_CODE, passthroughApi.preCheckedFunctionCode); + assertEquals(0, passthroughApi.executedFunctionCode); + } + + /** TestAPI extended with a platform function code the library's enum does not contain. */ + private static class PassthroughTestAPI extends TestAPI { + private short preCheckedFunctionCode; + private short executedFunctionCode; + private long executedValue1; + + @Override + public void platformSpecificPreExecuteCheck(int paramCount, boolean returnValueExpected, + MachineState state, short rawFunctionCode) throws IllegalFunctionCodeException { + if (rawFunctionCode != CUSTOM_PLATFORM_FUNCTION_CODE) { + super.platformSpecificPreExecuteCheck(paramCount, returnValueExpected, state, rawFunctionCode); + return; + } + + this.preCheckedFunctionCode = rawFunctionCode; + + // Takes one arg, no return value + if (paramCount != 1 || returnValueExpected) + throw new IllegalFunctionCodeException("Passed paramCount (" + paramCount + ") and returnValueExpected (" + returnValueExpected + + ") do not match platform-specific function code 0x" + String.format("%04x", rawFunctionCode)); + } + + @Override + public void platformSpecificPostCheckExecute(FunctionData functionData, MachineState state, + short rawFunctionCode) throws ExecutionException { + if (rawFunctionCode != CUSTOM_PLATFORM_FUNCTION_CODE) { + super.platformSpecificPostCheckExecute(functionData, state, rawFunctionCode); + return; + } + + this.executedFunctionCode = rawFunctionCode; + this.executedValue1 = functionData.value1; + } + } + +}