diff --git a/Java/src/org/ciyam/at/API.java b/Java/src/org/ciyam/at/API.java index 6adecaf..5c45d72 100644 --- a/Java/src/org/ciyam/at/API.java +++ b/Java/src/org/ciyam/at/API.java @@ -12,6 +12,12 @@ package org.ciyam.at; */ public abstract class API { + /** Returns fee for executing opcode in terms of execution "steps" */ + public abstract int getOpCodeSteps(OpCode opcode); + + /** Returns fee per execution "step" */ + public abstract long getFeePerStep(); + /** Returns current blockchain's height */ public abstract int getCurrentBlockHeight(); @@ -62,17 +68,8 @@ public abstract class API { /** Return AT's current balance */ public abstract long getCurrentBalance(MachineState state); - /** Return AT's previous balance at end of last execution round. Does not include any amounts sent to AT since */ - public abstract long getPreviousBalance(MachineState state); - /** Pay passed amount, or current balance if necessary, (fee inclusive) to address in B */ - public abstract void payAmountToB(long value1, MachineState state); - - /** Pay AT's current balance to address in B */ - public abstract void payCurrentBalanceToB(MachineState state); - - /** Pay AT's previous balance to address in B */ - public abstract void payPreviousBalanceToB(MachineState state); + public abstract void payAmountToB(long amount, MachineState state); /** Send 'message' in A to address in B */ public abstract void messageAToB(MachineState state); @@ -84,7 +81,10 @@ public abstract class API { */ public abstract long addMinutesToTimestamp(Timestamp timestamp, long minutes, MachineState state); - /** AT has encountered fatal error. Return remaining funds to creator */ + /** AT has finished. Return remaining funds to creator */ + public abstract void onFinished(long amount, MachineState state); + + /** AT has encountered fatal error */ public abstract void onFatalError(MachineState state, ExecutionException e); /** Pre-execute checking of param requirements for platform-specific functions */ @@ -92,7 +92,7 @@ public abstract class API { throws IllegalFunctionCodeException; /** - * Platform-specific function execution + * Platform-specific function execution after checking correct calling OpCode * * @throws ExecutionException */ @@ -103,6 +103,11 @@ public abstract class API { state.setIsSleeping(isSleeping); } + /** Convenience method to allow subclasses to test package-scoped MachineState.isFirstOpCodeAfterSleeping */ + protected boolean isFirstOpCodeAfterSleeping(MachineState state) { + return state.isFirstOpCodeAfterSleeping(); + } + /** Convenience methods to allow subclasses to access package-scoped a1-a4, b1-b4 variables */ protected void setA1(MachineState state, long value) { state.a1 = value; diff --git a/Java/src/org/ciyam/at/FunctionCode.java b/Java/src/org/ciyam/at/FunctionCode.java index c3c4e37..0bf8263 100644 --- a/Java/src/org/ciyam/at/FunctionCode.java +++ b/Java/src/org/ciyam/at/FunctionCode.java @@ -810,7 +810,7 @@ public enum FunctionCode { GET_CURRENT_BALANCE(0x0400, 0, true) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - functionData.returnValue = state.getAPI().getCurrentBalance(state); + functionData.returnValue = state.getCurrentBalance(); } }, /** @@ -821,7 +821,7 @@ public enum FunctionCode { GET_PREVIOUS_BALANCE(0x0401, 0, true) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - functionData.returnValue = state.getAPI().getPreviousBalance(state); + functionData.returnValue = state.getPreviousBalance(); } }, /** @@ -832,7 +832,18 @@ public enum FunctionCode { PAY_TO_ADDRESS_IN_B(0x0402, 1, false) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - state.getAPI().payAmountToB(functionData.value1, state); + // Reduce amount to current balance if insufficient funds to pay full amount in value1 + long amount = Math.max(state.getCurrentBalance(), functionData.value1); + + // Actually pay + state.getAPI().payAmountToB(amount, state); + + // Update current balance to reflect payment + state.setCurrentBalance(state.getCurrentBalance() - amount); + + // With no balance left, this AT is effectively finished? + if (state.getCurrentBalance() == 0) + state.setIsFinished(true); } }, /** @@ -842,7 +853,11 @@ public enum FunctionCode { PAY_ALL_TO_ADDRESS_IN_B(0x0403, 0, false) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - state.getAPI().payCurrentBalanceToB(state); + state.getAPI().payAmountToB(state.getCurrentBalance(), state); + + // With no balance left, this AT is effectively finished? + state.setCurrentBalance(0); + state.setIsFinished(true); } }, /** @@ -853,7 +868,18 @@ public enum FunctionCode { PAY_PREVIOUS_TO_ADDRESS_IN_B(0x0404, 0, false) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - state.getAPI().payPreviousBalanceToB(state); + // Reduce amount to previous balance if insufficient funds to pay previous balance amount + long amount = Math.max(state.getCurrentBalance(), state.getPreviousBalance()); + + // Actually pay + state.getAPI().payAmountToB(amount, state); + + // Update current balance to reflect payment + state.setCurrentBalance(state.getCurrentBalance() - amount); + + // With no balance left, this AT is effectively finished? + if (state.getCurrentBalance() == 0) + state.setIsFinished(true); } }, /** diff --git a/Java/src/org/ciyam/at/MachineState.java b/Java/src/org/ciyam/at/MachineState.java index 011e1f4..0883ca0 100644 --- a/Java/src/org/ciyam/at/MachineState.java +++ b/Java/src/org/ciyam/at/MachineState.java @@ -26,7 +26,10 @@ public class MachineState { public static final int ADDRESS_SIZE = 4; /** Maximum value for an address in the code segment */ - public static final int MAX_CODE_ADDRESS = 0x1fffffff; + public static final int MAX_CODE_ADDRESS = 0x0000ffff; + + /** Maximum number of steps per execution round */ + public static final int MAX_STEPS = 500; private static class VersionedConstants { /** Bytes per code page */ @@ -109,11 +112,18 @@ public class MachineState { /* package */ long b3; /* package */ long b4; + // Internal use private int currentBlockHeight; + private long currentBalance; - /** Number of opcodes processed this execution */ + /** Previous balance after end of last round of execution */ + private long previousBalance; + + /** Number of opcodes processed this execution round */ private int steps; + private boolean isFirstOpCodeAfterSleeping; + private API api; private LoggerInterface logger; @@ -175,6 +185,8 @@ public class MachineState { this.api = api; this.currentBlockHeight = 0; + this.currentBalance = 0; + this.previousBalance = 0; this.steps = 0; this.logger = logger; } @@ -223,6 +235,7 @@ public class MachineState { this.frozenBalance = null; this.isFinished = false; this.hadFatalError = false; + this.previousBalance = 0; } // Getters / setters @@ -344,6 +357,7 @@ public class MachineState { return this.currentBlockHeight; } + /** So API can determine final execution fee */ public int getSteps() { return this.steps; } @@ -356,6 +370,25 @@ public class MachineState { return this.logger; } + public long getCurrentBalance() { + return this.currentBalance; + } + + // For FunctionCode use + /* package */ void setCurrentBalance(long currentBalance) { + this.currentBalance = currentBalance; + } + + // For FunctionCode use + /* package */ long getPreviousBalance() { + return this.previousBalance; + } + + // For FunctionCode/API use + /* package */ boolean isFirstOpCodeAfterSleeping() { + return this.isFirstOpCodeAfterSleeping; + } + // Serialization /** For serializing a machine state */ @@ -387,6 +420,7 @@ public class MachineState { // Actual state bytes.write(toByteArray(this.programCounter)); bytes.write(toByteArray(this.onStopAddress)); + bytes.write(toByteArray(this.previousBalance)); // Various flags Flags flags = new Flags(); @@ -474,6 +508,7 @@ public class MachineState { // Actual state state.programCounter = byteBuffer.getInt(); state.onStopAddress = byteBuffer.getInt(); + state.previousBalance = byteBuffer.getLong(); // Various flags (reverse order to toBytes) Flags flags = state.new Flags(byteBuffer.getInt()); @@ -555,11 +590,39 @@ public class MachineState { (byte) (value >> 48), (byte) (value >> 56) }; } - // Actual execution - + /** + * Actually perform a round of execution + *
+ * On return, caller is expected to call getCurrentBalance() to update their account records, and also to call getSteps() to calculate final execution fee
+ * for block records.
+ */
public void execute() {
- // Set byte buffer position using program counter
- codeByteBuffer.position(this.programCounter);
+ // Initialization
+ this.steps = 0;
+ this.currentBlockHeight = api.getCurrentBlockHeight();
+ this.currentBalance = api.getCurrentBalance(this);
+ this.isFirstOpCodeAfterSleeping = false;
+
+ // Pre-execution checks
+ if (this.isFinished) {
+ logger.debug("Not executing as already finished!");
+ return;
+ }
+
+ if (this.isFrozen && this.currentBalance <= this.frozenBalance) {
+ logger.debug("Not executing as current balance [" + this.currentBalance + "] hasn't increased since being frozen at [" + this.frozenBalance + "]");
+ return;
+ }
+
+ if (this.isSleeping && this.sleepUntilHeight != null && this.currentBlockHeight < this.sleepUntilHeight) {
+ logger.debug("Not executing as current block height [" + this.currentBlockHeight + "] hasn't reached sleep-until block height ["
+ + this.sleepUntilHeight + "]");
+ return;
+ }
+
+ // If we were previously sleeping then set first-opcode-after-sleeping to help FunctionCodes that need to detect this
+ if (this.isSleeping)
+ this.isFirstOpCodeAfterSleeping = true;
// Reset for this round of execution
this.isSleeping = false;
@@ -567,8 +630,11 @@ public class MachineState {
this.isStopped = false;
this.isFrozen = false;
this.frozenBalance = null;
- this.steps = 0;
- this.currentBlockHeight = api.getCurrentBlockHeight();
+
+ long feePerStep = this.api.getFeePerStep();
+
+ // Set byte buffer position using program counter
+ codeByteBuffer.position(this.programCounter);
while (!this.isSleeping && !this.isStopped && !this.isFinished && !this.isFrozen) {
byte rawOpCode = codeByteBuffer.get();
@@ -580,7 +646,27 @@ public class MachineState {
this.logger.debug("[PC: " + String.format("%04x", this.programCounter) + "] " + nextOpCode.name());
- // TODO: Request cost from API, apply cost to balance, etc.
+ // Request opcode step-fee from API, apply fee to balance, etc.
+ int opcodeSteps = this.api.getOpCodeSteps(nextOpCode);
+ long opcodeFee = opcodeSteps * feePerStep;
+
+ if (this.steps + opcodeSteps > MAX_STEPS) {
+ logger.debug("Enforced sleep due to exceeding maximum number of steps (" + MAX_STEPS + ") per execution round");
+ this.isSleeping = true;
+ break;
+ }
+
+ if (this.currentBalance < opcodeFee) {
+ // Not enough balance left to continue execution - freeze AT
+ logger.debug("Frozen due to lack of balance");
+ this.isFrozen = true;
+ this.frozenBalance = this.currentBalance;
+ break;
+ }
+
+ // Apply opcode step-fee
+ this.currentBalance -= opcodeFee;
+ this.steps += opcodeSteps;
// At this point, programCounter is BEFORE opcode (and args).
nextOpCode.execute(this);
@@ -594,7 +680,7 @@ public class MachineState {
this.isFinished = true;
this.hadFatalError = true;
- // Ask API to refund remaining funds back to AT's creator
+ // Notify API that there was an error
this.api.onFatalError(this, e);
break;
}
@@ -603,13 +689,30 @@ public class MachineState {
codeByteBuffer.position(this.programCounter);
}
- ++this.steps;
+ // No longer true
+ this.isFirstOpCodeAfterSleeping = false;
+ }
+
+ if (this.isSleeping) {
+ if (this.sleepUntilHeight != null)
+ this.logger.debug("Sleeping until block " + this.sleepUntilHeight);
+ else
+ this.logger.debug("Sleeping until next block");
}
if (this.isStopped) {
this.logger.debug("Setting program counter to stop address: " + String.format("%04x", this.onStopAddress));
this.programCounter = this.onStopAddress;
}
+
+ if (this.isFinished) {
+ this.logger.debug("Finished - refunding remaining funds back to creator");
+ this.api.onFinished(this.currentBalance, this);
+ this.currentBalance = 0;
+ }
+
+ // Set new value for previousBalance prior to serialization, ready for next round
+ this.previousBalance = this.currentBalance;
}
/** Return disassembly of code bytes */
diff --git a/Java/tests/TestACCT.java b/Java/tests/TestACCT.java
index c974f63..12ca354 100644
--- a/Java/tests/TestACCT.java
+++ b/Java/tests/TestACCT.java
@@ -71,6 +71,8 @@ public class TestACCT {
private byte[] executeAndCheck(MachineState state) {
state.execute();
+ api.setCurrentBalance(state.getCurrentBalance());
+
byte[] stateBytes = state.toBytes();
MachineState restoredState = MachineState.fromBytes(api, logger, stateBytes);
byte[] restoredStateBytes = restoredState.toBytes();
diff --git a/Java/tests/common/ACCTAPI.java b/Java/tests/common/ACCTAPI.java
index 4eef496..7856b33 100644
--- a/Java/tests/common/ACCTAPI.java
+++ b/Java/tests/common/ACCTAPI.java
@@ -12,6 +12,7 @@ import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
+import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
public class ACCTAPI extends API {
@@ -46,7 +47,6 @@ public class ACCTAPI extends API {
private List