Browse Source

Added support for execution steps/fees.

Two new API calls:
    getOpCodeSteps(OpCode) : int
    getFeePerStep() : long
This allows API to determine cost per "step" and charge more for (say) function opcodes.

MachineState knows the balance at the end of each execution round,
so now AT's previousBalance is managed by MachineState and added to serialized data.

API calls payCurrentBalanceToB and payPreviousBalanceToB are absorbed into payAmountToB,
and amounts passed are calculated by FunctionCode instead.

Added API call onFinished(long amount, MachineState state) for when AT has finished
so API can return remaining funds (amount) to creator.

Added API call isFirstOpCodeAfterSleeping() : boolean to replace dodgy test of
state.getSteps() == 0 in getRandomUsingTransactionInA().

The API call getCurrentBalance is now used by MachineState to find out AT's balance
at the beginning of execution round. After this, MachineState manages the balance until
the end of execution, whereby the caller can find out the new balance from MachineState.
Corrected some code in FunctionCode to get current balance from MachineState instead in
light of above. (Ditto previousBalance).

Added MAX_STEPS to MachineState to ATs can be made to sleep if they execute too many
steps in one execution round.

Added some pre-execution checks to MachineState.execute() to prevent execution in some
cases, like already finished, or not enough balance to un-freeze, or not reached
required block while sleeping, etc.

Unit tests pass, but there are no test for new steps/fee code yet!
master
catbref 6 years ago
parent
commit
297ccbdaf6
  1. 29
      Java/src/org/ciyam/at/API.java
  2. 36
      Java/src/org/ciyam/at/FunctionCode.java
  3. 125
      Java/src/org/ciyam/at/MachineState.java
  4. 2
      Java/tests/TestACCT.java
  5. 58
      Java/tests/common/ACCTAPI.java
  6. 38
      Java/tests/common/TestAPI.java

29
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;

36
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);
}
},
/**

125
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;
/** Previous balance after end of last round of execution */
private long previousBalance;
/** Number of opcodes processed this execution */
/** 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
* <p>
* 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 */

2
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();

58
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<Block> blockchain;
private Map<String, Account> 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

38
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

Loading…
Cancel
Save