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; + } + } + +}