From 6450658f959a681082ab514e103b99987229ecc8 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Tue, 21 Jul 2026 19:47:26 -0400 Subject: [PATCH 1/2] feat: support platform-specific function pricing --- Java/src/main/java/org/ciyam/at/API.java | 17 ++++ .../main/java/org/ciyam/at/MachineState.java | 10 +- .../ciyam/at/PlatformFunctionStepTests.java | 95 +++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 Java/src/test/java/org/ciyam/at/PlatformFunctionStepTests.java diff --git a/Java/src/main/java/org/ciyam/at/API.java b/Java/src/main/java/org/ciyam/at/API.java index 78e9bb6..9e98575 100644 --- a/Java/src/main/java/org/ciyam/at/API.java +++ b/Java/src/main/java/org/ciyam/at/API.java @@ -42,6 +42,23 @@ public static ATTransactionType valueOf(long value) { /** Returns fee for executing opcode in terms of execution "steps" */ public abstract int getOpCodeSteps(OpCode opcode); + /** + * Returns fee for executing an external-function opcode in terms of execution "steps". + *

+ * This overload is called before the machine enforces its step and balance budgets, and before + * the external function executes. Platforms that need function-specific pricing can inspect the + * raw function code and current machine state here. The default delegates to the opcode-only + * method so existing API implementations and pricing remain unchanged. + * + * @param opcode external-function opcode about to execute + * @param rawFunctionCode raw function code encoded after the opcode + * @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 getOpCodeSteps(OpCode opcode, short rawFunctionCode, MachineState state) { + return this.getOpCodeSteps(opcode); + } + /** Returns fee per execution "step" */ public abstract long getFeePerStep(); diff --git a/Java/src/main/java/org/ciyam/at/MachineState.java b/Java/src/main/java/org/ciyam/at/MachineState.java index 8f2e782..3974bd7 100644 --- a/Java/src/main/java/org/ciyam/at/MachineState.java +++ b/Java/src/main/java/org/ciyam/at/MachineState.java @@ -854,7 +854,15 @@ public void execute() { this.logger.debug(() -> String.format("[PC: %04x] %s", this.programCounter, nextOpCode.name())); // Request opcode step-fee from API, apply fee to balance, etc. - int opcodeSteps = this.api.getOpCodeSteps(nextOpCode); + int opcodeSteps; + if (nextOpCode.value >= OpCode.EXT_FUN.value + && 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); + } else { + opcodeSteps = this.api.getOpCodeSteps(nextOpCode); + } long opcodeFee = opcodeSteps * feePerStep; if (this.steps + opcodeSteps > maxSteps) { diff --git a/Java/src/test/java/org/ciyam/at/PlatformFunctionStepTests.java b/Java/src/test/java/org/ciyam/at/PlatformFunctionStepTests.java new file mode 100644 index 0000000..e67bd97 --- /dev/null +++ b/Java/src/test/java/org/ciyam/at/PlatformFunctionStepTests.java @@ -0,0 +1,95 @@ +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; + +public class PlatformFunctionStepTests extends ExecutableTest { + + private static final short PLATFORM_FUNCTION_CODE = 0x0501; + + @Test + public void testDefaultExternalFunctionPricingIsUnchanged() { + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(PLATFORM_FUNCTION_CODE).putInt(0); + codeByteBuffer.put(OpCode.STP_IMD.value); + + execute(true); + + assertTrue(state.isStopped()); + assertFalse(state.isFinished()); + assertFalse(state.hadFatalError()); + assertEquals(TestAPI.STEPS_PER_FUNCTION_CALL + 1, state.getSteps()); + } + + @Test + public void testRawFunctionCodeAndStateReachPricingHook() { + int functionSteps = 123; + FunctionPricingTestAPI pricingApi = new FunctionPricingTestAPI(functionSteps); + api = pricingApi; + long initialBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(PLATFORM_FUNCTION_CODE).putInt(0); + codeByteBuffer.put(OpCode.STP_IMD.value); + + execute(true); + + assertEquals(OpCode.EXT_FUN_DAT, pricingApi.pricedOpCode); + assertEquals(PLATFORM_FUNCTION_CODE, pricingApi.pricedFunctionCode); + assertSame(state, pricingApi.pricedState); + assertEquals(1, pricingApi.functionExecutions); + assertEquals(functionSteps + 1, state.getSteps()); + assertEquals(initialBalance - functionSteps - 1, api.accounts.get(TestAPI.AT_ADDRESS).balance); + } + + @Test + public void testFunctionSpecificCostSleepsBeforeExecution() { + FunctionPricingTestAPI pricingApi = new FunctionPricingTestAPI(TestAPI.MAX_STEPS_PER_ROUND + 1); + api = pricingApi; + long initialBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(PLATFORM_FUNCTION_CODE).putInt(0); + codeByteBuffer.put(OpCode.STP_IMD.value); + + execute(true); + + assertTrue(state.isSleeping()); + assertFalse(state.isFinished()); + assertFalse(state.hadFatalError()); + assertEquals(0, state.getProgramCounter()); + assertEquals(0, state.getSteps()); + assertEquals(0, pricingApi.functionExecutions); + assertEquals(initialBalance, api.accounts.get(TestAPI.AT_ADDRESS).balance); + } + + private static class FunctionPricingTestAPI extends TestAPI { + private final int functionSteps; + private OpCode pricedOpCode; + private short pricedFunctionCode; + private MachineState pricedState; + private int functionExecutions; + + private FunctionPricingTestAPI(int functionSteps) { + this.functionSteps = functionSteps; + } + + @Override + public int getOpCodeSteps(OpCode opcode, short rawFunctionCode, MachineState state) { + this.pricedOpCode = opcode; + this.pricedFunctionCode = rawFunctionCode; + this.pricedState = state; + return this.functionSteps; + } + + @Override + public void platformSpecificPostCheckExecute(FunctionData functionData, MachineState state, + short rawFunctionCode) throws ExecutionException { + ++this.functionExecutions; + super.platformSpecificPostCheckExecute(functionData, state, rawFunctionCode); + } + } +} From 33df17d976af63037ca345b928cc2daf350d9a58 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Tue, 21 Jul 2026 20:22:30 -0400 Subject: [PATCH 2/2] fix: enforce platform function prechecks --- .../main/java/org/ciyam/at/FunctionCode.java | 5 ++-- .../ciyam/at/PlatformFunctionStepTests.java | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Java/src/main/java/org/ciyam/at/FunctionCode.java b/Java/src/main/java/org/ciyam/at/FunctionCode.java index f49c03c..74b700f 100644 --- a/Java/src/main/java/org/ciyam/at/FunctionCode.java +++ b/Java/src/main/java/org/ciyam/at/FunctionCode.java @@ -1138,9 +1138,8 @@ public void preExecuteCheck(int paramCount, boolean returnValueExpected) throws @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - // XXX somehow we need to call something like this: - // state.getAPI().platformSpecificPreExecuteCheck(functionData.paramCount, functionData.returnValueExpected, rawFunctionCode); - + state.getAPI().platformSpecificPreExecuteCheck(functionData.paramCount, + functionData.returnValueExpected, state, rawFunctionCode); state.getAPI().platformSpecificPostCheckExecute(functionData, state, rawFunctionCode); } }; diff --git a/Java/src/test/java/org/ciyam/at/PlatformFunctionStepTests.java b/Java/src/test/java/org/ciyam/at/PlatformFunctionStepTests.java index e67bd97..7369393 100644 --- a/Java/src/test/java/org/ciyam/at/PlatformFunctionStepTests.java +++ b/Java/src/test/java/org/ciyam/at/PlatformFunctionStepTests.java @@ -41,11 +41,28 @@ public void testRawFunctionCodeAndStateReachPricingHook() { assertEquals(OpCode.EXT_FUN_DAT, pricingApi.pricedOpCode); assertEquals(PLATFORM_FUNCTION_CODE, pricingApi.pricedFunctionCode); assertSame(state, pricingApi.pricedState); + assertEquals(1, pricingApi.functionPreChecks); assertEquals(1, pricingApi.functionExecutions); assertEquals(functionSteps + 1, state.getSteps()); assertEquals(initialBalance - functionSteps - 1, api.accounts.get(TestAPI.AT_ADDRESS).balance); } + @Test + public void testPlatformPreCheckRejectsWrongSignatureBeforeExecution() { + FunctionPricingTestAPI pricingApi = new FunctionPricingTestAPI(TestAPI.STEPS_PER_FUNCTION_CALL); + api = pricingApi; + + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(PLATFORM_FUNCTION_CODE).putInt(0); + codeByteBuffer.put(OpCode.STP_IMD.value); + + execute(true); + + assertTrue(state.isFinished()); + assertTrue(state.hadFatalError()); + assertEquals(1, pricingApi.functionPreChecks); + assertEquals(0, pricingApi.functionExecutions); + } + @Test public void testFunctionSpecificCostSleepsBeforeExecution() { FunctionPricingTestAPI pricingApi = new FunctionPricingTestAPI(TestAPI.MAX_STEPS_PER_ROUND + 1); @@ -71,12 +88,20 @@ private static class FunctionPricingTestAPI extends TestAPI { private OpCode pricedOpCode; private short pricedFunctionCode; private MachineState pricedState; + private int functionPreChecks; private int functionExecutions; private FunctionPricingTestAPI(int functionSteps) { this.functionSteps = functionSteps; } + @Override + public void platformSpecificPreExecuteCheck(int paramCount, boolean returnValueExpected, + MachineState state, short rawFunctionCode) throws IllegalFunctionCodeException { + ++this.functionPreChecks; + super.platformSpecificPreExecuteCheck(paramCount, returnValueExpected, state, rawFunctionCode); + } + @Override public int getOpCodeSteps(OpCode opcode, short rawFunctionCode, MachineState state) { this.pricedOpCode = opcode;