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
+ * 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;
+ }
+ }
+
+}
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).
+ *