Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Java/src/main/java/org/ciyam/at/API.java
Original file line number Diff line number Diff line change
Expand Up @@ -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".
* <p>
* 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();

Expand Down
5 changes: 2 additions & 3 deletions Java/src/main/java/org/ciyam/at/FunctionCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
Expand Down
10 changes: 9 additions & 1 deletion Java/src/main/java/org/ciyam/at/MachineState.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
120 changes: 120 additions & 0 deletions Java/src/test/java/org/ciyam/at/PlatformFunctionStepTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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.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);
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 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;
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);
}
}
}
Loading