From e522fb312dd68a100e17b7bc57dcba708c3e803f Mon Sep 17 00:00:00 2001
From: catbref <misc-github@talk2dom.com>
Date: Sat, 20 Nov 2021 15:22:50 +0000
Subject: [PATCH 1/3] Improved test support: * pom.xml now builds JavaDoc,
 sources and test JARs * more descriptive output from test classes to aid
 debugging * actually add/collate AT-generated transactions to test blockchain
 for analysis * test block period changed from 10 minutes to 1 minute *
 'quiet' logger aded that doesn't emit DEBUG log entries

---
 Java/pom.xml                                  | 55 ++++++++++++++++
 .../java/org/ciyam/at/AtLoggerFactory.java    |  1 +
 Java/src/main/java/org/ciyam/at/OpCode.java   |  6 +-
 .../ciyam/at/BlockchainFunctionCodeTests.java | 16 ++++-
 .../org/ciyam/at/test/ExecutableTest.java     |  3 +
 .../org/ciyam/at/test/QuietTestLogger.java    | 37 +++++++++++
 .../ciyam/at/test/QuietTestLoggerFactory.java | 13 ++++
 .../test/java/org/ciyam/at/test/TestAPI.java  | 62 ++++++++++++++++---
 8 files changed, 181 insertions(+), 12 deletions(-)
 create mode 100644 Java/src/test/java/org/ciyam/at/test/QuietTestLogger.java
 create mode 100644 Java/src/test/java/org/ciyam/at/test/QuietTestLoggerFactory.java

diff --git a/Java/pom.xml b/Java/pom.xml
index f49c4cd..25fd5d7 100644
--- a/Java/pom.xml
+++ b/Java/pom.xml
@@ -6,16 +6,20 @@
 	<artifactId>AT</artifactId>
 	<version>1.4.0</version>
 	<packaging>jar</packaging>
+
 	<properties>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 		<skipTests>false</skipTests>
 
 		<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
+		<maven-source-plugin.version>3.2.0</maven-source-plugin.version>
 		<maven-javadoc-plugin.version>3.3.1</maven-javadoc-plugin.version>
 		<maven-surefire-plugin.version>3.0.0-M4</maven-surefire-plugin.version>
+		<maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
 
 		<bouncycastle.version>1.64</bouncycastle.version>
 	</properties>
+
 	<build>
 		<sourceDirectory>src/main/java</sourceDirectory>
 		<testSourceDirectory>src/test/java</testSourceDirectory>
@@ -37,6 +41,19 @@
 					<skipTests>${skipTests}</skipTests>
 				</configuration>
 			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-source-plugin</artifactId>
+				<version>${maven-source-plugin.version}</version>
+				<executions>
+					<execution>
+						<id>attach-sources</id>
+						<goals>
+							<goal>jar</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-javadoc-plugin</artifactId>
@@ -50,9 +67,47 @@
 					</execution>
 				</executions>
 			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<version>${maven-jar-plugin.version}</version>
+				<executions>
+					<execution>
+						<goals>
+							<goal>test-jar</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
 		</plugins>
 	</build>
+
 	<dependencies>
+		<dependency>
+			<groupId>org.apache.maven.plugins</groupId>
+			<artifactId>maven-compiler-plugin</artifactId>
+			<version>${maven-compiler-plugin.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.maven.plugins</groupId>
+			<artifactId>maven-surefire-plugin</artifactId>
+			<version>${maven-surefire-plugin.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.maven.plugins</groupId>
+			<artifactId>maven-source-plugin</artifactId>
+			<version>${maven-source-plugin.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.maven.plugins</groupId>
+			<artifactId>maven-javadoc-plugin</artifactId>
+			<version>${maven-javadoc-plugin.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.maven.plugins</groupId>
+			<artifactId>maven-jar-plugin</artifactId>
+			<version>${maven-jar-plugin.version}</version>
+		</dependency>
 		<dependency>
 			<groupId>org.bouncycastle</groupId>
 			<artifactId>bcprov-jdk15on</artifactId>
diff --git a/Java/src/main/java/org/ciyam/at/AtLoggerFactory.java b/Java/src/main/java/org/ciyam/at/AtLoggerFactory.java
index 2a07fa2..b327e8d 100644
--- a/Java/src/main/java/org/ciyam/at/AtLoggerFactory.java
+++ b/Java/src/main/java/org/ciyam/at/AtLoggerFactory.java
@@ -1,5 +1,6 @@
 package org.ciyam.at;
 
+@FunctionalInterface
 public interface AtLoggerFactory {
 
 	AtLogger create(final Class<?> loggerName);
diff --git a/Java/src/main/java/org/ciyam/at/OpCode.java b/Java/src/main/java/org/ciyam/at/OpCode.java
index ae13ece..04854dd 100644
--- a/Java/src/main/java/org/ciyam/at/OpCode.java
+++ b/Java/src/main/java/org/ciyam/at/OpCode.java
@@ -1070,7 +1070,11 @@ public enum OpCode {
 
 	public byte[] compile(Object... args) throws CompilationException {
 		if (args.length != this.params.length)
-			throw new IllegalArgumentException(String.format("%s requires %d args, only %d passed", this.name(), this.params.length, args.length));
+			throw new IllegalArgumentException(String.format("%s requires %d arg%s, but %d passed",
+					this.name(),
+					this.params.length,
+					this.params.length != 1 ? "s" : "",
+					args.length));
 
 		ByteBuffer byteBuffer = ByteBuffer.allocate(32); // 32 should easily be enough
 
diff --git a/Java/src/test/java/org/ciyam/at/BlockchainFunctionCodeTests.java b/Java/src/test/java/org/ciyam/at/BlockchainFunctionCodeTests.java
index 5df06f8..4ae7402 100644
--- a/Java/src/test/java/org/ciyam/at/BlockchainFunctionCodeTests.java
+++ b/Java/src/test/java/org/ciyam/at/BlockchainFunctionCodeTests.java
@@ -86,13 +86,27 @@ public class BlockchainFunctionCodeTests extends ExecutableTest {
 
 	@Test
 	public void testPutPreviousBlockHashIntoA() throws ExecutionException {
-		int previousBlockHeight = TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT - 1;
+		// Generate some blocks containing transactions (but none to AT)
+		TestBlock newBlock = api.generateBlockWithNonAtTransactions();
+		api.addBlockToChain(newBlock);
+		api.bumpCurrentBlockHeight();
+
+		newBlock = api.generateBlockWithNonAtTransactions();
+		api.addBlockToChain(newBlock);
+		api.bumpCurrentBlockHeight();
+
+		// Generate a block containing transaction to AT
+		newBlock = api.generateBlockWithAtTransaction();
+		api.addBlockToChain(newBlock);
+		int previousBlockHeight = api.getCurrentBlockHeight();
+		api.bumpCurrentBlockHeight();
 
 		codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_PREVIOUS_BLOCK_HASH_INTO_A.value);
 		codeByteBuffer.put(OpCode.FIN_IMD.value);
 
 		execute(true);
 
+		// previousBlockHeight - 1 because index into blockchain starts at 0, whereas block heights start at 1
 		byte[] expectedBlockHash = api.blockchain.get(previousBlockHeight - 1).blockHash;
 
 		byte[] aBytes = api.getA(state);
diff --git a/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java b/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java
index cc5bff8..96a52c8 100644
--- a/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java
+++ b/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java
@@ -96,6 +96,9 @@ public abstract class ExecutableTest {
 			System.out.println("New balance: " + TestAPI.prettyAmount(newBalance));
 			api.setCurrentBalance(newBalance);
 
+			// Add block, possibly containing AT-created transactions, to chain to at least provide block hashes
+			api.addCurrentBlockToChain();
+
 			// Bump block height
 			api.bumpCurrentBlockHeight();
 
diff --git a/Java/src/test/java/org/ciyam/at/test/QuietTestLogger.java b/Java/src/test/java/org/ciyam/at/test/QuietTestLogger.java
new file mode 100644
index 0000000..f27c809
--- /dev/null
+++ b/Java/src/test/java/org/ciyam/at/test/QuietTestLogger.java
@@ -0,0 +1,37 @@
+package org.ciyam.at.test;
+
+import org.ciyam.at.AtLogger;
+
+import java.util.function.Supplier;
+
+public class QuietTestLogger implements AtLogger {
+
+	@Override
+	public void error(String message) {
+		System.err.println("ERROR: " + message);
+	}
+
+	@Override
+	public void error(Supplier<String> messageSupplier) {
+		System.err.println("ERROR: " + messageSupplier.get());
+	}
+
+	@Override
+	public void debug(String message) {
+	}
+
+	@Override
+	public void debug(Supplier<String> messageSupplier) {
+	}
+
+	@Override
+	public void echo(String message) {
+		System.err.println("ECHO: " + message);
+	}
+
+	@Override
+	public void echo(Supplier<String> messageSupplier) {
+		System.err.println("ECHO: " + messageSupplier.get());
+	}
+
+}
diff --git a/Java/src/test/java/org/ciyam/at/test/QuietTestLoggerFactory.java b/Java/src/test/java/org/ciyam/at/test/QuietTestLoggerFactory.java
new file mode 100644
index 0000000..d1325d1
--- /dev/null
+++ b/Java/src/test/java/org/ciyam/at/test/QuietTestLoggerFactory.java
@@ -0,0 +1,13 @@
+package org.ciyam.at.test;
+
+import org.ciyam.at.AtLogger;
+import org.ciyam.at.AtLoggerFactory;
+
+public class QuietTestLoggerFactory implements AtLoggerFactory {
+
+	@Override
+	public AtLogger create(Class<?> loggerName) {
+		return new QuietTestLogger();
+	}
+
+}
diff --git a/Java/src/test/java/org/ciyam/at/test/TestAPI.java b/Java/src/test/java/org/ciyam/at/test/TestAPI.java
index d500eef..316dca0 100644
--- a/Java/src/test/java/org/ciyam/at/test/TestAPI.java
+++ b/Java/src/test/java/org/ciyam/at/test/TestAPI.java
@@ -19,7 +19,7 @@ import org.ciyam.at.Timestamp;
 public class TestAPI extends API {
 
 	/** Average period between blocks, in seconds. */
-	public static final int BLOCK_PERIOD = 10 * 60;
+	public static final int BLOCK_PERIOD = 60;
 	/** Maximum number of steps before auto-sleep. */
 	public static final int MAX_STEPS_PER_ROUND = 500;
 	/** Op-code step multiplier for calling functions. */
@@ -102,29 +102,27 @@ public class TestAPI extends API {
 		}
 	}
 
-	public List<TestBlock> blockchain;
-	public Map<String, TestAccount> accounts;
-	public Map<String, TestTransaction> transactions;
+	public List<TestBlock> blockchain = new ArrayList<>();
+	public Map<String, TestAccount> accounts = new HashMap<>();
+	public Map<String, TestTransaction> transactions = new HashMap<>();
+	public List<TestTransaction> atTransactions = new ArrayList<>();
 
+	private TestBlock currentBlock = new TestBlock();
 	private int currentBlockHeight;
 
 	public TestAPI() {
 		this.currentBlockHeight = DEFAULT_INITIAL_BLOCK_HEIGHT;
 
 		// Fill block chain from block 1 to initial height with empty blocks
-		blockchain = new ArrayList<>();
 		for (int h = 1; h <= this.currentBlockHeight; ++h)
 			blockchain.add(new TestBlock());
 
 		// Set up test accounts
-		accounts = new HashMap<>();
 		new TestAccount(AT_CREATOR_ADDRESS, 1000000L).addToMap(accounts);
 		new TestAccount(AT_ADDRESS, DEFAULT_INITIAL_BALANCE).addToMap(accounts);
 		new TestAccount("Initiator", 100000L).addToMap(accounts);
 		new TestAccount("Responder", 200000L).addToMap(accounts);
 		new TestAccount("Bystander", 300000L).addToMap(accounts);
-
-		transactions = new HashMap<>();
 	}
 
 	public static byte[] encodeAddress(String address) {
@@ -153,6 +151,15 @@ public class TestAPI extends API {
 		this.currentBlockHeight = blockHeight;
 	}
 
+	public void addTransactionToCurrentBlock(TestTransaction testTransaction) {
+		currentBlock.transactions.add(testTransaction);
+	}
+
+	public void addCurrentBlockToChain() {
+		addBlockToChain(currentBlock);
+		currentBlock = new TestBlock();
+	}
+
 	public TestBlock addBlockToChain(TestBlock newBlock) {
 		blockchain.add(newBlock);
 		final int blockHeight = blockchain.size();
@@ -165,6 +172,10 @@ public class TestAPI extends API {
 
 			// Add to transactions map
 			transactions.put(stringifyHash(transaction.txHash), transaction);
+
+			// Transaction sent/received by AT? Add to AT transactions list
+			if (transaction.sender.equals(AT_ADDRESS) || transaction.recipient.equals(AT_ADDRESS))
+				atTransactions.add(transaction);
 		}
 
 		return newBlock;
@@ -288,7 +299,11 @@ public class TestAPI extends API {
 
 			if (transaction.recipient.equals("AT")) {
 				// Found a transaction
-				System.out.println("Found transaction at height " + blockHeight + " sequence " + transactionSequence);
+				System.out.println(String.format("Found transaction at height %d, sequence %d: %s from %s",
+						blockHeight,
+						transactionSequence,
+						transaction.txType.name(),
+						transaction.sender));
 
 				// Generate pseudo-hash of transaction
 				this.setA(state, transaction.txHash);
@@ -392,12 +407,29 @@ public class TestAPI extends API {
 		if (recipient == null)
 			throw new IllegalStateException("Refusing to pay to unknown account: " + address);
 
+		if (amount < 0)
+			throw new IllegalStateException(String.format("Refusing to pay negative amount: %s", amount));
+
+		if (amount == 0) {
+			System.out.println(String.format("Skipping zero-amount payment to account %s", address));
+			return;
+		}
+
 		recipient.balance += amount;
 		System.out.println(String.format("Paid %s to '%s', their balance now: %s", prettyAmount(amount), recipient.address, prettyAmount(recipient.balance)));
 
 		final long previousBalance = state.getCurrentBalance();
 		final long newBalance = previousBalance - amount;
 		System.out.println(String.format("AT balance was %s, now: %s", prettyAmount(previousBalance), prettyAmount(newBalance)));
+
+		// Add suitable transaction to currentBlock
+
+		// Generate tx hash
+		byte[] txHash = new byte[32];
+		RANDOM.nextBytes(txHash);
+
+		TestTransaction testTransaction = new TestTransaction(txHash, AT_ADDRESS, recipient.address, amount);
+		addTransactionToCurrentBlock(testTransaction);
 	}
 
 	@Override
@@ -409,7 +441,17 @@ public class TestAPI extends API {
 		if (recipient == null)
 			throw new IllegalStateException("Refusing to send message to unknown account: " + address);
 
-		recipient.messages.add(this.getA(state));
+		byte[] message = this.getA(state);
+		recipient.messages.add(message);
+
+		// Add suitable transaction to currentBlock
+
+		// Generate tx hash
+		byte[] txHash = new byte[32];
+		RANDOM.nextBytes(txHash);
+
+		TestTransaction testTransaction = new TestTransaction(txHash, AT_ADDRESS, recipient.address, message);
+		addTransactionToCurrentBlock(testTransaction);
 	}
 
 	@Override

From ae23aac71630bc71d6c8bffab372e56a7337e611 Mon Sep 17 00:00:00 2001
From: catbref <misc-github@talk2dom.com>
Date: Sun, 28 Nov 2021 13:42:42 +0000
Subject: [PATCH 2/3] Improvements to TestAPI and ExecutableTest to help
 testing external ATs like lottery, etc. No changes to core AT.

---
 .../src/test/java/org/ciyam/at/MiscTests.java | 43 +++++++++-
 .../org/ciyam/at/test/ExecutableTest.java     | 74 ++++++++++-------
 .../test/java/org/ciyam/at/test/TestAPI.java  | 81 ++++++++++++++++---
 3 files changed, 152 insertions(+), 46 deletions(-)

diff --git a/Java/src/test/java/org/ciyam/at/MiscTests.java b/Java/src/test/java/org/ciyam/at/MiscTests.java
index 21841cd..9ccc5dd 100644
--- a/Java/src/test/java/org/ciyam/at/MiscTests.java
+++ b/Java/src/test/java/org/ciyam/at/MiscTests.java
@@ -35,18 +35,55 @@ public class MiscTests extends ExecutableTest {
 
 	@Test
 	public void testFreeze() throws ExecutionException {
+		// Choose initial balance so it used up before max-steps-per-round triggers
+		long initialBalance = 5L;
+		api.accounts.get(TestAPI.AT_ADDRESS).balance = initialBalance;
+
 		// Infinite loop
 		codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(0);
 
-		// We need enough rounds to exhaust balance
-		long minRounds = TestAPI.DEFAULT_INITIAL_BALANCE / TestAPI.MAX_STEPS_PER_ROUND + 1;
-		for (long i = 0; i < minRounds; ++i)
+		// Test a few rounds to make sure AT is frozen and stays frozen
+		for (int i = 0; i < 3; ++i) {
 			execute(true);
 
+			assertTrue(state.isFrozen());
+
+			Long frozenBalance = state.getFrozenBalance();
+			assertNotNull(frozenBalance);
+		}
+	}
+
+	@Test
+	public void testUnfreeze() throws ExecutionException {
+		// Choose initial balance so it used up before max-steps-per-round triggers
+		long initialBalance = 5L;
+		api.setCurrentBalance(initialBalance);
+
+		// Infinite loop
+		codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(0);
+
+		// Execute to make sure AT is frozen and stays frozen
+		execute(true);
+
 		assertTrue(state.isFrozen());
 
 		Long frozenBalance = state.getFrozenBalance();
 		assertNotNull(frozenBalance);
+
+		// Send payment to AT to allow unfreezing
+		// Payment needs to be enough to trigger max-steps-per-round so we can detect unfreezing
+		api.setCurrentBalance(TestAPI.MAX_STEPS_PER_ROUND * api.getFeePerStep() * 2);
+
+		// Execute AT
+		execute(true);
+
+		// We expect AT to be sleeping, not frozen
+		assertFalse(state.isFrozen());
+
+		frozenBalance = state.getFrozenBalance();
+		assertNull(frozenBalance);
+
+		assertTrue(state.isSleeping());
 	}
 
 	@Test
diff --git a/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java b/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java
index 96a52c8..c1cba48 100644
--- a/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java
+++ b/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java
@@ -4,17 +4,18 @@ import java.nio.ByteBuffer;
 import java.security.Security;
 
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.ciyam.at.AtLoggerFactory;
 import org.ciyam.at.MachineState;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.BeforeClass;
 
-public abstract class ExecutableTest {
+public class ExecutableTest {
 
-	private static final int DATA_OFFSET = MachineState.HEADER_LENGTH; // code bytes are not present
-	private static final int CALL_STACK_OFFSET = DATA_OFFSET + TestUtils.NUM_DATA_PAGES * MachineState.VALUE_SIZE;
+	public static final int DATA_OFFSET = MachineState.HEADER_LENGTH; // code bytes are not present
+	public static final int CALL_STACK_OFFSET = DATA_OFFSET + TestUtils.NUM_DATA_PAGES * MachineState.VALUE_SIZE;
 
-	public TestLoggerFactory loggerFactory;
+	public AtLoggerFactory loggerFactory;
 	public TestAPI api;
 	public MachineState state;
 	public ByteBuffer codeByteBuffer;
@@ -24,6 +25,7 @@ public abstract class ExecutableTest {
 	public int userStackOffset;
 	public int userStackSize;
 	public byte[] packedState;
+	public byte[] codeBytes;
 
 	@BeforeClass
 	public static void beforeClass() {
@@ -50,28 +52,39 @@ public abstract class ExecutableTest {
 		loggerFactory = null;
 	}
 
-	protected void execute(boolean onceOnly) {
-		byte[] headerBytes = TestUtils.HEADER_BYTES;
-		byte[] codeBytes = codeByteBuffer.array();
-		byte[] dataBytes = dataByteBuffer.array();
-
+	public void execute(boolean onceOnly) {
 		if (packedState == null) {
 			// First time
 			System.out.println("First execution - deploying...");
+			byte[] headerBytes = TestUtils.HEADER_BYTES;
+			codeBytes = codeByteBuffer.array();
+			byte[] dataBytes = dataByteBuffer.array();
+
 			state = new MachineState(api, loggerFactory, headerBytes, codeBytes, dataBytes);
 			packedState = state.toBytes();
 		}
 
 		do {
-			state = MachineState.fromBytes(api, loggerFactory, packedState, codeBytes);
+			execute_once();
+		} while (!onceOnly && !state.isFinished());
 
-			System.out.println("Starting execution round!");
-			System.out.println("Current block height: " + api.getCurrentBlockHeight());
-			System.out.println("Previous balance: " + TestAPI.prettyAmount(state.getPreviousBalance()));
-			System.out.println("Current balance: " + TestAPI.prettyAmount(state.getCurrentBalance()));
+		unwrapState(state);
+	}
 
+	public void execute_once() {
+		state = MachineState.fromBytes(api, loggerFactory, packedState, codeBytes);
+
+		System.out.println("Starting execution round!");
+		System.out.println("Current block height: " + api.getCurrentBlockHeight());
+		System.out.println("Previous balance: " + TestAPI.prettyAmount(state.getPreviousBalance()));
+		System.out.println("Current balance: " + TestAPI.prettyAmount(api.getCurrentBalance(state)));
+
+		// Actual execution
+		if (api.willExecute(state, api.getCurrentBlockHeight())) {
 			// Actual execution
+			api.preExecute(state);
 			state.execute();
+			packedState = state.toBytes();
 
 			System.out.println("After execution round:");
 			System.out.println("Steps: " + state.getSteps());
@@ -94,22 +107,23 @@ public abstract class ExecutableTest {
 
 			long newBalance = state.getCurrentBalance();
 			System.out.println("New balance: " + TestAPI.prettyAmount(newBalance));
+
+			// Update AT balance due to execution costs, etc.
 			api.setCurrentBalance(newBalance);
+		} else {
+			System.out.println("Skipped execution round");
+		}
 
-			// Add block, possibly containing AT-created transactions, to chain to at least provide block hashes
-			api.addCurrentBlockToChain();
+		// Add block, possibly containing AT-created transactions, to chain to at least provide block hashes
+		api.addCurrentBlockToChain();
 
-			// Bump block height
-			api.bumpCurrentBlockHeight();
+		// Bump block height
+		api.bumpCurrentBlockHeight();
 
-			packedState = state.toBytes();
-			System.out.println("Execution round finished\n");
-		} while (!onceOnly && !state.isFinished());
-
-		unwrapState(state);
+		System.out.println("Execution round finished\n");
 	}
 
-	protected byte[] unwrapState(MachineState state) {
+	public byte[] unwrapState(MachineState state) {
 		// Ready for diagnosis
 		byte[] stateBytes = state.toBytes();
 
@@ -124,30 +138,30 @@ public abstract class ExecutableTest {
 		return stateBytes;
 	}
 
-	protected long getData(int address) {
+	public long getData(int address) {
 		int index = DATA_OFFSET + address * MachineState.VALUE_SIZE;
 		return stateByteBuffer.getLong(index);
 	}
 
-	protected void getDataBytes(int address, byte[] dest) {
+	public void getDataBytes(int address, byte[] dest) {
 		int index = DATA_OFFSET + address * MachineState.VALUE_SIZE;
 		stateByteBuffer.slice().position(index).get(dest);
 	}
 
-	protected int getCallStackPosition() {
+	public int getCallStackPosition() {
 		return TestUtils.NUM_CALL_STACK_PAGES * MachineState.ADDRESS_SIZE - callStackSize;
 	}
 
-	protected int getCallStackEntry(int address) {
+	public int getCallStackEntry(int address) {
 		int index = CALL_STACK_OFFSET + 4 + address - TestUtils.NUM_CALL_STACK_PAGES * MachineState.ADDRESS_SIZE + callStackSize;
 		return stateByteBuffer.getInt(index);
 	}
 
-	protected int getUserStackPosition() {
+	public int getUserStackPosition() {
 		return TestUtils.NUM_USER_STACK_PAGES * MachineState.VALUE_SIZE - userStackSize;
 	}
 
-	protected long getUserStackEntry(int address) {
+	public long getUserStackEntry(int address) {
 		int index = userStackOffset + 4 + address - TestUtils.NUM_USER_STACK_PAGES * MachineState.VALUE_SIZE + userStackSize;
 		return stateByteBuffer.getLong(index);
 	}
diff --git a/Java/src/test/java/org/ciyam/at/test/TestAPI.java b/Java/src/test/java/org/ciyam/at/test/TestAPI.java
index 316dca0..7ade50c 100644
--- a/Java/src/test/java/org/ciyam/at/test/TestAPI.java
+++ b/Java/src/test/java/org/ciyam/at/test/TestAPI.java
@@ -26,7 +26,7 @@ public class TestAPI extends API {
 	public static final int STEPS_PER_FUNCTION_CALL = 10;
 
 	/** Initial balance for simple test scenarios. */
-	public static final long DEFAULT_INITIAL_BALANCE = 1234L;
+	public static final long DEFAULT_INITIAL_BALANCE = 10_0000_0000L;
 	/** Initial block height for simple test scenarios. */
 	public static final int DEFAULT_INITIAL_BLOCK_HEIGHT = 10;
 	/** AT creation block height for simple test scenarios. */
@@ -118,11 +118,20 @@ public class TestAPI extends API {
 			blockchain.add(new TestBlock());
 
 		// Set up test accounts
-		new TestAccount(AT_CREATOR_ADDRESS, 1000000L).addToMap(accounts);
+		new TestAccount(AT_CREATOR_ADDRESS, DEFAULT_INITIAL_BALANCE).addToMap(accounts);
 		new TestAccount(AT_ADDRESS, DEFAULT_INITIAL_BALANCE).addToMap(accounts);
-		new TestAccount("Initiator", 100000L).addToMap(accounts);
-		new TestAccount("Responder", 200000L).addToMap(accounts);
-		new TestAccount("Bystander", 300000L).addToMap(accounts);
+		new TestAccount("Initiator", DEFAULT_INITIAL_BALANCE * 2).addToMap(accounts);
+		new TestAccount("Responder", DEFAULT_INITIAL_BALANCE * 3).addToMap(accounts);
+		new TestAccount("Bystander", DEFAULT_INITIAL_BALANCE * 4).addToMap(accounts);
+	}
+
+	// Hook to be overridden
+	protected boolean willExecute(MachineState state, int blockHeight) {
+		return true;
+	}
+
+	// Hook to be oveerridden
+	protected void preExecute(MachineState state) {
 	}
 
 	public static byte[] encodeAddress(String address) {
@@ -163,6 +172,7 @@ public class TestAPI extends API {
 	public TestBlock addBlockToChain(TestBlock newBlock) {
 		blockchain.add(newBlock);
 		final int blockHeight = blockchain.size();
+		StringBuilder sb = new StringBuilder(256);
 
 		for (int seq = 0; seq < newBlock.transactions.size(); ++seq) {
 			TestTransaction transaction = newBlock.transactions.get(seq);
@@ -176,6 +186,44 @@ public class TestAPI extends API {
 			// Transaction sent/received by AT? Add to AT transactions list
 			if (transaction.sender.equals(AT_ADDRESS) || transaction.recipient.equals(AT_ADDRESS))
 				atTransactions.add(transaction);
+
+			// Process PAYMENT transactions
+			if (transaction.txType == ATTransactionType.PAYMENT) {
+				sb.setLength(0);
+				sb.append(transaction.sender)
+						.append(" sent ")
+						.append(prettyAmount(transaction.amount));
+
+				// Subtract amount from sender
+				TestAccount senderAccount = accounts.get(transaction.sender);
+				if (senderAccount == null)
+					throw new IllegalStateException(String.format("Can't send from unknown sender %s: no funds!",
+							transaction.sender));
+
+				// Do not apply if sender is AT because balance update already performed during execution
+				if (!transaction.sender.equals(AT_ADDRESS)) {
+					senderAccount.balance -= transaction.amount;
+				}
+
+				if (senderAccount.balance < 0)
+					throw new IllegalStateException(String.format("Can't send %s from %s: insufficient funds (%s)",
+							prettyAmount(transaction.amount),
+							transaction.sender,
+							prettyAmount(senderAccount.balance)));
+
+				// Add amount to recipient
+				sb.append(" to ");
+				TestAccount recipientAccount = accounts.get(transaction.recipient);
+				if (recipientAccount == null) {
+					sb.append("(new) ");
+					recipientAccount = new TestAccount(transaction.recipient, 0);
+					accounts.put(transaction.recipient, recipientAccount);
+				}
+				recipientAccount.balance += transaction.amount;
+				sb.append(transaction.recipient);
+
+				System.out.println(sb.toString());
+			}
 		}
 
 		return newBlock;
@@ -299,11 +347,13 @@ public class TestAPI extends API {
 
 			if (transaction.recipient.equals("AT")) {
 				// Found a transaction
-				System.out.println(String.format("Found transaction at height %d, sequence %d: %s from %s",
+				System.out.println(String.format("Found transaction at height %d, sequence %d: %s %s from %s",
 						blockHeight,
 						transactionSequence,
+						transaction.txType.equals(ATTransactionType.PAYMENT) ? prettyAmount(transaction.amount) : "",
 						transaction.txType.name(),
-						transaction.sender));
+						transaction.sender
+				));
 
 				// Generate pseudo-hash of transaction
 				this.setA(state, transaction.txHash);
@@ -415,12 +465,11 @@ public class TestAPI extends API {
 			return;
 		}
 
-		recipient.balance += amount;
-		System.out.println(String.format("Paid %s to '%s', their balance now: %s", prettyAmount(amount), recipient.address, prettyAmount(recipient.balance)));
+		System.out.println(String.format("Creating PAYMENT of %s to %s", prettyAmount(amount), recipient.address));
 
 		final long previousBalance = state.getCurrentBalance();
 		final long newBalance = previousBalance - amount;
-		System.out.println(String.format("AT balance was %s, now: %s", prettyAmount(previousBalance), prettyAmount(newBalance)));
+		System.out.println(String.format("AT current balance was %s, now: %s", prettyAmount(previousBalance), prettyAmount(newBalance)));
 
 		// Add suitable transaction to currentBlock
 
@@ -465,10 +514,16 @@ public class TestAPI extends API {
 		System.out.println("Finished - refunding remaining to creator");
 
 		TestAccount atCreatorAccount = accounts.get(AT_CREATOR_ADDRESS);
-		atCreatorAccount.balance += amount;
-		System.out.println(String.format("Paid %s to AT creator '%s', their balance now: %s", prettyAmount(amount), atCreatorAccount.address, prettyAmount(atCreatorAccount.balance)));
+		System.out.println(String.format("Creating PAYMENT of %s to AT creator %s", prettyAmount(amount), atCreatorAccount.address));
 
-		accounts.get(AT_ADDRESS).balance -= amount;
+		// Add suitable transaction to currentBlock
+
+		// Generate tx hash
+		byte[] txHash = new byte[32];
+		RANDOM.nextBytes(txHash);
+
+		TestTransaction testTransaction = new TestTransaction(txHash, AT_ADDRESS, atCreatorAccount.address, amount);
+		addTransactionToCurrentBlock(testTransaction);
 	}
 
 	@Override

From 836ef215d44b43d80c222fe0f2154f31c84d1abd Mon Sep 17 00:00:00 2001
From: catbref <misc-github@talk2dom.com>
Date: Thu, 2 Dec 2021 21:10:11 +0000
Subject: [PATCH 3/3] Minor JavaDoc fix-up, in particular the wrong explanation
 for SLP_VAL OpCode

---
 Java/src/main/java/org/ciyam/at/OpCode.java | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/Java/src/main/java/org/ciyam/at/OpCode.java b/Java/src/main/java/org/ciyam/at/OpCode.java
index 04854dd..a6ffb88 100644
--- a/Java/src/main/java/org/ciyam/at/OpCode.java
+++ b/Java/src/main/java/org/ciyam/at/OpCode.java
@@ -561,7 +561,7 @@ public enum OpCode {
 	/**
 	 * <b>SL</b>ee<b>P</b> until <b>DAT</b>a<br>
 	 * <code>0x25 addr</code><br>
-	 * <code>sleep until $addr, then carry on from current PC</code><br>
+	 * Sleep until <code>$addr</code>, then carry on from current <code>PC</code><br>
 	 * Note: The value from <code>$addr</code> is considered to be a block height.
 	 */
 	SLP_DAT(0x25, OpCodeParam.BLOCK_HEIGHT) {
@@ -634,7 +634,7 @@ public enum OpCode {
 	/**
 	 * <b>SL</b>ee<b>P</b> <b>IM</b>me<b>D</b>iately<br>
 	 * <code>0x2a</code><br>
-	 * <code>sleep until next block, then carry on from current PC</code>
+	 * Sleep until next block, then carry on from current <code>PC</code>
 	 */
 	SLP_IMD(0x2a) {
 		@Override
@@ -659,8 +659,7 @@ public enum OpCode {
 	/**
 	 * <b>SL</b>ee<b>P</b> for <b>VAL</b>ue blocks<br>
 	 * <code>0x2c value</code><br>
-	 * <code>sleep until $addr, then carry on from current PC</code><br>
-	 * Note: The value from <code>$addr</code> is considered to be a block height.
+	 * Sleep for <code>value</code> blocks, then carry on from current <code>PC</code>
 	 */
 	SLP_VAL(0x2c, OpCodeParam.VALUE) {
 		@Override