mirror of
https://github.com/Qortal/AT.git
synced 2025-01-30 19:02:14 +00:00
Improvements to TestAPI and ExecutableTest to help testing external ATs like lottery, etc.
No changes to core AT.
This commit is contained in:
parent
e522fb312d
commit
ae23aac716
@ -35,18 +35,55 @@ public class MiscTests extends ExecutableTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFreeze() throws ExecutionException {
|
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
|
// Infinite loop
|
||||||
codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(0);
|
codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(0);
|
||||||
|
|
||||||
// We need enough rounds to exhaust balance
|
// Test a few rounds to make sure AT is frozen and stays frozen
|
||||||
long minRounds = TestAPI.DEFAULT_INITIAL_BALANCE / TestAPI.MAX_STEPS_PER_ROUND + 1;
|
for (int i = 0; i < 3; ++i) {
|
||||||
for (long i = 0; i < minRounds; ++i)
|
|
||||||
execute(true);
|
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());
|
assertTrue(state.isFrozen());
|
||||||
|
|
||||||
Long frozenBalance = state.getFrozenBalance();
|
Long frozenBalance = state.getFrozenBalance();
|
||||||
assertNotNull(frozenBalance);
|
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
|
@Test
|
||||||
|
@ -4,17 +4,18 @@ import java.nio.ByteBuffer;
|
|||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.ciyam.at.AtLoggerFactory;
|
||||||
import org.ciyam.at.MachineState;
|
import org.ciyam.at.MachineState;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.BeforeClass;
|
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
|
public 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 CALL_STACK_OFFSET = DATA_OFFSET + TestUtils.NUM_DATA_PAGES * MachineState.VALUE_SIZE;
|
||||||
|
|
||||||
public TestLoggerFactory loggerFactory;
|
public AtLoggerFactory loggerFactory;
|
||||||
public TestAPI api;
|
public TestAPI api;
|
||||||
public MachineState state;
|
public MachineState state;
|
||||||
public ByteBuffer codeByteBuffer;
|
public ByteBuffer codeByteBuffer;
|
||||||
@ -24,6 +25,7 @@ public abstract class ExecutableTest {
|
|||||||
public int userStackOffset;
|
public int userStackOffset;
|
||||||
public int userStackSize;
|
public int userStackSize;
|
||||||
public byte[] packedState;
|
public byte[] packedState;
|
||||||
|
public byte[] codeBytes;
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void beforeClass() {
|
public static void beforeClass() {
|
||||||
@ -50,28 +52,39 @@ public abstract class ExecutableTest {
|
|||||||
loggerFactory = null;
|
loggerFactory = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void execute(boolean onceOnly) {
|
public void execute(boolean onceOnly) {
|
||||||
byte[] headerBytes = TestUtils.HEADER_BYTES;
|
|
||||||
byte[] codeBytes = codeByteBuffer.array();
|
|
||||||
byte[] dataBytes = dataByteBuffer.array();
|
|
||||||
|
|
||||||
if (packedState == null) {
|
if (packedState == null) {
|
||||||
// First time
|
// First time
|
||||||
System.out.println("First execution - deploying...");
|
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);
|
state = new MachineState(api, loggerFactory, headerBytes, codeBytes, dataBytes);
|
||||||
packedState = state.toBytes();
|
packedState = state.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
state = MachineState.fromBytes(api, loggerFactory, packedState, codeBytes);
|
execute_once();
|
||||||
|
} while (!onceOnly && !state.isFinished());
|
||||||
|
|
||||||
System.out.println("Starting execution round!");
|
unwrapState(state);
|
||||||
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()));
|
|
||||||
|
|
||||||
|
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
|
// Actual execution
|
||||||
|
api.preExecute(state);
|
||||||
state.execute();
|
state.execute();
|
||||||
|
packedState = state.toBytes();
|
||||||
|
|
||||||
System.out.println("After execution round:");
|
System.out.println("After execution round:");
|
||||||
System.out.println("Steps: " + state.getSteps());
|
System.out.println("Steps: " + state.getSteps());
|
||||||
@ -94,22 +107,23 @@ public abstract class ExecutableTest {
|
|||||||
|
|
||||||
long newBalance = state.getCurrentBalance();
|
long newBalance = state.getCurrentBalance();
|
||||||
System.out.println("New balance: " + TestAPI.prettyAmount(newBalance));
|
System.out.println("New balance: " + TestAPI.prettyAmount(newBalance));
|
||||||
|
|
||||||
|
// Update AT balance due to execution costs, etc.
|
||||||
api.setCurrentBalance(newBalance);
|
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
|
// Add block, possibly containing AT-created transactions, to chain to at least provide block hashes
|
||||||
api.addCurrentBlockToChain();
|
api.addCurrentBlockToChain();
|
||||||
|
|
||||||
// Bump block height
|
// Bump block height
|
||||||
api.bumpCurrentBlockHeight();
|
api.bumpCurrentBlockHeight();
|
||||||
|
|
||||||
packedState = state.toBytes();
|
System.out.println("Execution round finished\n");
|
||||||
System.out.println("Execution round finished\n");
|
|
||||||
} while (!onceOnly && !state.isFinished());
|
|
||||||
|
|
||||||
unwrapState(state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected byte[] unwrapState(MachineState state) {
|
public byte[] unwrapState(MachineState state) {
|
||||||
// Ready for diagnosis
|
// Ready for diagnosis
|
||||||
byte[] stateBytes = state.toBytes();
|
byte[] stateBytes = state.toBytes();
|
||||||
|
|
||||||
@ -124,30 +138,30 @@ public abstract class ExecutableTest {
|
|||||||
return stateBytes;
|
return stateBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected long getData(int address) {
|
public long getData(int address) {
|
||||||
int index = DATA_OFFSET + address * MachineState.VALUE_SIZE;
|
int index = DATA_OFFSET + address * MachineState.VALUE_SIZE;
|
||||||
return stateByteBuffer.getLong(index);
|
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;
|
int index = DATA_OFFSET + address * MachineState.VALUE_SIZE;
|
||||||
stateByteBuffer.slice().position(index).get(dest);
|
stateByteBuffer.slice().position(index).get(dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getCallStackPosition() {
|
public int getCallStackPosition() {
|
||||||
return TestUtils.NUM_CALL_STACK_PAGES * MachineState.ADDRESS_SIZE - callStackSize;
|
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;
|
int index = CALL_STACK_OFFSET + 4 + address - TestUtils.NUM_CALL_STACK_PAGES * MachineState.ADDRESS_SIZE + callStackSize;
|
||||||
return stateByteBuffer.getInt(index);
|
return stateByteBuffer.getInt(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getUserStackPosition() {
|
public int getUserStackPosition() {
|
||||||
return TestUtils.NUM_USER_STACK_PAGES * MachineState.VALUE_SIZE - userStackSize;
|
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;
|
int index = userStackOffset + 4 + address - TestUtils.NUM_USER_STACK_PAGES * MachineState.VALUE_SIZE + userStackSize;
|
||||||
return stateByteBuffer.getLong(index);
|
return stateByteBuffer.getLong(index);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ public class TestAPI extends API {
|
|||||||
public static final int STEPS_PER_FUNCTION_CALL = 10;
|
public static final int STEPS_PER_FUNCTION_CALL = 10;
|
||||||
|
|
||||||
/** Initial balance for simple test scenarios. */
|
/** 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. */
|
/** Initial block height for simple test scenarios. */
|
||||||
public static final int DEFAULT_INITIAL_BLOCK_HEIGHT = 10;
|
public static final int DEFAULT_INITIAL_BLOCK_HEIGHT = 10;
|
||||||
/** AT creation block height for simple test scenarios. */
|
/** AT creation block height for simple test scenarios. */
|
||||||
@ -118,11 +118,20 @@ public class TestAPI extends API {
|
|||||||
blockchain.add(new TestBlock());
|
blockchain.add(new TestBlock());
|
||||||
|
|
||||||
// Set up test accounts
|
// 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(AT_ADDRESS, DEFAULT_INITIAL_BALANCE).addToMap(accounts);
|
||||||
new TestAccount("Initiator", 100000L).addToMap(accounts);
|
new TestAccount("Initiator", DEFAULT_INITIAL_BALANCE * 2).addToMap(accounts);
|
||||||
new TestAccount("Responder", 200000L).addToMap(accounts);
|
new TestAccount("Responder", DEFAULT_INITIAL_BALANCE * 3).addToMap(accounts);
|
||||||
new TestAccount("Bystander", 300000L).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) {
|
public static byte[] encodeAddress(String address) {
|
||||||
@ -163,6 +172,7 @@ public class TestAPI extends API {
|
|||||||
public TestBlock addBlockToChain(TestBlock newBlock) {
|
public TestBlock addBlockToChain(TestBlock newBlock) {
|
||||||
blockchain.add(newBlock);
|
blockchain.add(newBlock);
|
||||||
final int blockHeight = blockchain.size();
|
final int blockHeight = blockchain.size();
|
||||||
|
StringBuilder sb = new StringBuilder(256);
|
||||||
|
|
||||||
for (int seq = 0; seq < newBlock.transactions.size(); ++seq) {
|
for (int seq = 0; seq < newBlock.transactions.size(); ++seq) {
|
||||||
TestTransaction transaction = newBlock.transactions.get(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
|
// Transaction sent/received by AT? Add to AT transactions list
|
||||||
if (transaction.sender.equals(AT_ADDRESS) || transaction.recipient.equals(AT_ADDRESS))
|
if (transaction.sender.equals(AT_ADDRESS) || transaction.recipient.equals(AT_ADDRESS))
|
||||||
atTransactions.add(transaction);
|
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;
|
return newBlock;
|
||||||
@ -299,11 +347,13 @@ public class TestAPI extends API {
|
|||||||
|
|
||||||
if (transaction.recipient.equals("AT")) {
|
if (transaction.recipient.equals("AT")) {
|
||||||
// Found a transaction
|
// 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,
|
blockHeight,
|
||||||
transactionSequence,
|
transactionSequence,
|
||||||
|
transaction.txType.equals(ATTransactionType.PAYMENT) ? prettyAmount(transaction.amount) : "",
|
||||||
transaction.txType.name(),
|
transaction.txType.name(),
|
||||||
transaction.sender));
|
transaction.sender
|
||||||
|
));
|
||||||
|
|
||||||
// Generate pseudo-hash of transaction
|
// Generate pseudo-hash of transaction
|
||||||
this.setA(state, transaction.txHash);
|
this.setA(state, transaction.txHash);
|
||||||
@ -415,12 +465,11 @@ public class TestAPI extends API {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
recipient.balance += amount;
|
System.out.println(String.format("Creating PAYMENT of %s to %s", prettyAmount(amount), recipient.address));
|
||||||
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 previousBalance = state.getCurrentBalance();
|
||||||
final long newBalance = previousBalance - amount;
|
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
|
// Add suitable transaction to currentBlock
|
||||||
|
|
||||||
@ -465,10 +514,16 @@ public class TestAPI extends API {
|
|||||||
System.out.println("Finished - refunding remaining to creator");
|
System.out.println("Finished - refunding remaining to creator");
|
||||||
|
|
||||||
TestAccount atCreatorAccount = accounts.get(AT_CREATOR_ADDRESS);
|
TestAccount atCreatorAccount = accounts.get(AT_CREATOR_ADDRESS);
|
||||||
atCreatorAccount.balance += amount;
|
System.out.println(String.format("Creating PAYMENT of %s to AT creator %s", prettyAmount(amount), atCreatorAccount.address));
|
||||||
System.out.println(String.format("Paid %s to AT creator '%s', their balance now: %s", prettyAmount(amount), atCreatorAccount.address, prettyAmount(atCreatorAccount.balance)));
|
|
||||||
|
|
||||||
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
|
@Override
|
||||||
|
Loading…
Reference in New Issue
Block a user