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 blockchain; private Map accounts; private long balanceAT; - private long previousBalanceAT; // public ACCTAPI() { @@ -68,8 +68,10 @@ public class ACCTAPI extends API { Account bystander = new Account("Bystander", 999); this.accounts.put(bystander.address, bystander); + Account creator = new Account("Creator", 0); + this.accounts.put(creator.address, creator); + this.balanceAT = 50000; - this.previousBalanceAT = this.balanceAT; } public void generateNextBlock(byte[] secret) { @@ -125,8 +127,6 @@ public class ACCTAPI extends API { } this.blockchain.add(block); - - this.previousBalanceAT = this.balanceAT; } /** Convert long to little-endian byte array */ @@ -150,6 +150,16 @@ public class ACCTAPI extends API { return accounts.get(accountIndex).address; } + @Override + public int getOpCodeSteps(OpCode opcode) { + return 1; + } + + @Override + public long getFeePerStep() { + return 1L; + } + @Override public int getCurrentBlockHeight() { return this.blockchain.size(); @@ -274,29 +284,24 @@ public class ACCTAPI extends API { return this.balanceAT; } - @Override - public long getPreviousBalance(MachineState state) { - return this.previousBalanceAT; + public void setCurrentBalance(long balance) { + this.balanceAT = balance; + System.out.println("New AT balance: " + balance); } @Override - public void payAmountToB(long value1, MachineState state) { - char firstChar = String.format("%c", state.getB1()).charAt(0); + public void payAmountToB(long amount, MachineState state) { + // Determine recipient using first char in B1 + char firstChar = String.format("%c", (byte) state.getB1()).charAt(0); Account recipient = this.accounts.values().stream().filter((account) -> account.address.charAt(0) == firstChar).findFirst().get(); - recipient.balance += value1; - System.out.println("Paid " + value1 + " to " + recipient.address + ", their balance now: " + recipient.balance); - this.balanceAT -= value1; - System.out.println("Our balance now: " + this.balanceAT); - } - @Override - public void payCurrentBalanceToB(MachineState state) { - // NOT USED - } + // Simulate payment + recipient.balance += amount; + System.out.println("Paid " + amount + " to " + recipient.address + ", their balance now: " + recipient.balance); - @Override - public void payPreviousBalanceToB(MachineState state) { - // NOT USED + // For debugging, output our new balance + long balance = state.getCurrentBalance() - amount; + System.out.println("Our balance now: " + balance); } @Override @@ -310,10 +315,19 @@ public class ACCTAPI extends API { return timestamp.longValue(); } + @Override + public void onFinished(long amount, MachineState state) { + System.out.println("Finished - refunding remaining to creator"); + + Account creator = this.accounts.get("Creator"); + creator.balance += amount; + System.out.println("Paid " + amount + " to " + creator.address + ", their balance now: " + creator.balance); + } + @Override public void onFatalError(MachineState state, ExecutionException e) { System.out.println("Fatal error: " + e.getMessage()); - System.out.println("No error address set - refunding to creator and finishing"); + System.out.println("No error address set - will refund to creator and finish"); } @Override diff --git a/Java/tests/common/TestAPI.java b/Java/tests/common/TestAPI.java index fb711b1..89011df 100644 --- a/Java/tests/common/TestAPI.java +++ b/Java/tests/common/TestAPI.java @@ -5,6 +5,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 TestAPI extends API { @@ -21,6 +22,19 @@ public class TestAPI extends API { ++this.currentBlockHeight; } + @Override + public int getOpCodeSteps(OpCode opcode) { + if (opcode.value >= OpCode.EXT_FUN.value && opcode.value <= OpCode.EXT_FUN_RET_DAT_2.value) + return 10; + + return 1; + } + + @Override + public long getFeePerStep() { + return 1L; + } + @Override public int getCurrentBlockHeight() { return this.currentBlockHeight; @@ -65,7 +79,7 @@ public class TestAPI extends API { @Override public long generateRandomUsingTransactionInA(MachineState state) { - if (state.getSteps() != 0) { + if (isFirstOpCodeAfterSleeping(state)) { // First call System.out.println("generateRandomUsingTransactionInA: first call - sleeping"); @@ -115,20 +129,7 @@ public class TestAPI extends API { } @Override - public long getPreviousBalance(MachineState state) { - return 10000L; - } - - @Override - public void payAmountToB(long value1, MachineState state) { - } - - @Override - public void payCurrentBalanceToB(MachineState state) { - } - - @Override - public void payPreviousBalanceToB(MachineState state) { + public void payAmountToB(long amount, MachineState state) { } @Override @@ -141,10 +142,15 @@ public class TestAPI extends API { return timestamp.longValue(); } + @Override + public void onFinished(long amount, MachineState state) { + System.out.println("Finished - refunding remaining to creator"); + } + @Override public void onFatalError(MachineState state, ExecutionException e) { System.out.println("Fatal error: " + e.getMessage()); - System.out.println("No error address set - refunding to creator and finishing"); + System.out.println("No error address set - will refund to creator and finish"); } @Override