3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-11 17:55:50 +00:00

More work on CIYAM AT support.

ATs can create AT-Transactions which contain payments (of any asset) and/or messages.

Legacy Qora1 DeployATTransactions create AT records in the repository but set to "finished"
so that they never execute.

More repository support for ATs.

In HSQLDB, create a new TYPE called ATStateHash which is used to verify the same AT outcome
on a per-block basis.
Added Accounts.account as a foreign key to AccountBalances with ON DELETE CASCADE.
ATStates now include state_hash and fees on a per-block basis.
ATTransactions now include asset_id.

When transforming DeployATTransactions, don't include any signature when collating bytes for signing!
This commit is contained in:
catbref 2018-10-15 15:12:41 +01:00
parent e9d8b3e6e3
commit 46eee3cbce
21 changed files with 332 additions and 42 deletions

Binary file not shown.

View File

@ -1 +1 @@
1d6f5d634a2c4e570a5a8af260a51653
ab1560171ae5c6c15b0dfa8e6cccc7f8

View File

@ -1 +1 @@
c6387380bc5db1f0a98ecbb480b17bd89b564401
c293c9656f43b432a08053f19ec5aa0de1cd10ea

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>org.ciyam</groupId>
<artifactId>at</artifactId>
<versioning>
<release>1.0</release>
<versions>
<version>1.0</version>
</versions>
<lastUpdated>20181015085522</lastUpdated>
</versioning>
</metadata>

View File

@ -7,6 +7,6 @@
<versions>
<version>1.0</version>
</versions>
<lastUpdated>20181003154752</lastUpdated>
<lastUpdated>20181015081124</lastUpdated>
</versioning>
</metadata>

View File

@ -1 +1 @@
bc81bc1f9b74a4eececd5dd8b29e47d8
2369bf36c52580a89d5ea71a0f037a82

View File

@ -1 +1 @@
feefde4343bda4d6e13159e5c01f8b4f8963a1bc
6bc38899b93ffce2286ae26f7af0b2d8b69db3cf

View File

@ -10,22 +10,25 @@ public class ATTransactionData extends TransactionData {
private byte[] senderPublicKey;
private String recipient;
private BigDecimal amount;
private Long assetId;
private byte[] message;
// Constructors
public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, byte[] message, BigDecimal fee, long timestamp, byte[] reference,
byte[] signature) {
public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp,
byte[] reference, byte[] signature) {
super(TransactionType.AT, fee, senderPublicKey, timestamp, reference, signature);
this.senderPublicKey = senderPublicKey;
this.recipient = recipient;
this.amount = amount;
this.assetId = assetId;
this.message = message;
}
public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, byte[] message, BigDecimal fee, long timestamp, byte[] reference) {
this(senderPublicKey, recipient, amount, message, fee, timestamp, reference, null);
public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp,
byte[] reference) {
this(senderPublicKey, recipient, amount, assetId, message, fee, timestamp, reference, null);
}
// Getters/Setters
@ -42,6 +45,10 @@ public class ATTransactionData extends TransactionData {
return this.amount;
}
public Long getAssetId() {
return this.assetId;
}
public byte[] getMessage() {
return this.message;
}

View File

@ -1,5 +1,7 @@
package qora.at;
import java.nio.ByteBuffer;
import org.ciyam.at.MachineState;
import data.at.ATData;
@ -26,20 +28,51 @@ public class AT {
public AT(Repository repository, DeployATTransactionData deployATTransactionData) throws DataException {
this.repository = repository;
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
this.atData = new ATData(deployATTransactionData.getATAddress(), machineState.version, machineState.codeByteBuffer.array(), machineState.isSleeping,
machineState.sleepUntilHeight, machineState.isFinished, machineState.hadFatalError, machineState.isFrozen, machineState.frozenBalance,
deployATTransactionData.getSignature());
String atAddress = this.atData.getATAddress();
String atAddress = deployATTransactionData.getATAddress();
int height = this.repository.getBlockRepository().getBlockchainHeight();
byte[] stateData = machineState.toBytes();
this.atStateData = new ATStateData(atAddress, height, stateData);
byte[] creationBytes = deployATTransactionData.getCreationBytes();
short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
if (version >= 2) {
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
this.atData = new ATData(atAddress, machineState.version, machineState.getCodeBytes(), machineState.getIsSleeping(),
machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), machineState.getIsFrozen(),
machineState.getFrozenBalance(), deployATTransactionData.getSignature());
this.atStateData = new ATStateData(atAddress, height, machineState.toBytes());
} else {
// Legacy v1 AT in 'dead' state
// Extract code bytes length
ByteBuffer byteBuffer = ByteBuffer.wrap(deployATTransactionData.getCreationBytes());
short numCodePages = byteBuffer.get(2 + 2);
byteBuffer.position(6 * 2 + 8);
int codeLen = 0;
if (numCodePages * 256 < 257) {
codeLen = (int) (byteBuffer.get() & 0xff);
} else if (numCodePages * 256 < Short.MAX_VALUE + 1) {
codeLen = byteBuffer.getShort() & 0xffff;
} else if (numCodePages * 256 <= Integer.MAX_VALUE) {
codeLen = byteBuffer.getInt();
}
// Extract code bytes
byte[] codeBytes = new byte[codeLen];
byteBuffer.get(codeBytes);
this.atData = new ATData(deployATTransactionData.getATAddress(), 1, codeBytes, false, null, true, false, false, (Long) null,
deployATTransactionData.getSignature());
this.atStateData = new ATStateData(deployATTransactionData.getATAddress(), height, null);
}
}
// Processing
public void deploy() throws DataException {
this.repository.getATRepository().save(this.atData);
this.repository.getATRepository().save(this.atStateData);

View File

@ -612,7 +612,7 @@ public class Block {
Transaction.ValidationResult validationResult = transaction.isValid();
if (validationResult != Transaction.ValidationResult.OK) {
LOGGER.error("Error during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": "
+ validationResult.value);
+ validationResult.name());
return ValidationResult.TRANSACTION_INVALID;
}

View File

@ -0,0 +1,128 @@
package qora.transaction;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import data.PaymentData;
import data.transaction.ATTransactionData;
import data.transaction.TransactionData;
import qora.account.Account;
import qora.account.PublicKeyAccount;
import qora.assets.Asset;
import qora.payment.Payment;
import repository.DataException;
import repository.Repository;
public class ATTransaction extends Transaction {
// Properties
private ATTransactionData atTransactionData;
// Other useful constants
public static final int MAX_DATA_SIZE = 256;
// Constructors
public ATTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.atTransactionData = (ATTransactionData) this.transactionData;
}
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.singletonList(new Account(this.repository, atTransactionData.getRecipient()));
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
if (address.equals(this.getSender().getAddress()))
return true;
if (address.equals(atTransactionData.getRecipient()))
return true;
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);
String senderAddress = this.getSender().getAddress();
if (address.equals(senderAddress)) {
amount = amount.subtract(this.atTransactionData.getFee());
if (atTransactionData.getAmount() != null && atTransactionData.getAssetId() == Asset.QORA)
amount = amount.subtract(atTransactionData.getAmount());
}
if (address.equals(atTransactionData.getRecipient()) && atTransactionData.getAmount() != null)
amount = amount.add(atTransactionData.getAmount());
return amount;
}
// Navigation
public Account getSender() throws DataException {
return new PublicKeyAccount(this.repository, this.atTransactionData.getSenderPublicKey());
}
// Processing
private PaymentData getPaymentData() {
if (atTransactionData.getAmount() == null)
return null;
return new PaymentData(atTransactionData.getRecipient(), atTransactionData.getAssetId(), atTransactionData.getAmount());
}
@Override
public ValidationResult isValid() throws DataException {
// Check reference is correct
Account sender = getSender();
if (!Arrays.equals(sender.getLastReference(), atTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
if (this.atTransactionData.getMessage().length > MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
// If we have no payment then we're done
if (this.atTransactionData.getAmount() == null)
return ValidationResult.OK;
// Wrap and delegate final payment checks to Payment class
return new Payment(this.repository).isValid(atTransactionData.getSenderPublicKey(), getPaymentData(), atTransactionData.getFee());
}
@Override
public void process() throws DataException {
// Save this transaction itself
this.repository.getTransactionRepository().save(this.transactionData);
if (this.atTransactionData.getAmount() != null)
// Wrap and delegate payment processing to Payment class. Only update recipient's last reference if transferring QORA.
new Payment(this.repository).process(atTransactionData.getSenderPublicKey(), getPaymentData(), atTransactionData.getFee(),
atTransactionData.getSignature(), false);
}
@Override
public void orphan() throws DataException {
// Delete this transaction
this.repository.getTransactionRepository().delete(this.transactionData);
if (this.atTransactionData.getAmount() != null)
// Wrap and delegate payment processing to Payment class. Only revert recipient's last reference if transferring QORA.
new Payment(this.repository).orphan(atTransactionData.getSenderPublicKey(), getPaymentData(), atTransactionData.getFee(),
atTransactionData.getSignature(), atTransactionData.getReference(), false);
}
}

View File

@ -75,6 +75,13 @@ public class DeployATTransaction extends Transaction {
return amount;
}
/** Returns AT version from the header bytes */
private short getVersion() {
byte[] creationBytes = deployATTransactionData.getCreationBytes();
short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
return version;
}
/** Make sure deployATTransactionData has an ATAddress */
private void ensureATAddress() throws DataException {
if (this.deployATTransactionData.getATAddress() != null)
@ -82,7 +89,7 @@ public class DeployATTransaction extends Transaction {
int blockHeight = this.getHeight();
if (blockHeight == 0)
blockHeight = this.repository.getBlockRepository().getBlockchainHeight();
blockHeight = this.repository.getBlockRepository().getBlockchainHeight() + 1;
try {
byte[] name = this.deployATTransactionData.getName().getBytes("UTF-8");
@ -163,11 +170,8 @@ public class DeployATTransaction extends Transaction {
if (creator.getConfirmedBalance(Asset.QORA).compareTo(minimumBalance) < 0)
return ValidationResult.NO_BALANCE;
// Check creation bytes are valid (for v3+)
byte[] creationBytes = deployATTransactionData.getCreationBytes();
short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
if (version >= 3) {
// Check creation bytes are valid (for v2+)
if (this.getVersion() >= 2) {
// Do actual validation
} else {
// Skip validation for old, dead ATs
@ -194,6 +198,14 @@ public class DeployATTransaction extends Transaction {
// Update creator's reference
creator.setLastReference(deployATTransactionData.getSignature());
// Update AT's reference, which also creates AT account
Account atAccount = this.getATAccount();
atAccount.setLastReference(deployATTransactionData.getSignature());
// Update AT's balance
atAccount.setConfirmedBalance(Asset.QORA, deployATTransactionData.getAmount());
}
@Override
@ -212,6 +224,9 @@ public class DeployATTransaction extends Transaction {
// Update creator's reference
creator.setLastReference(deployATTransactionData.getReference());
// Delete AT's account
this.repository.getAccountRepository().delete(this.deployATTransactionData.getATAddress());
}
}

View File

@ -193,11 +193,14 @@ public abstract class Transaction {
case MULTIPAYMENT:
return new MultiPaymentTransaction(repository, transactionData);
case DEPLOY_AT:
return new DeployATTransaction(repository, transactionData);
case MESSAGE:
return new MessageTransaction(repository, transactionData);
case DEPLOY_AT:
return new DeployATTransaction(repository, transactionData);
case AT:
return new ATTransaction(repository, transactionData);
default:
throw new IllegalStateException("Unsupported transaction type [" + transactionData.getType().value + "] during fetch from repository");

View File

@ -11,6 +11,8 @@ public interface AccountRepository {
public void save(AccountData accountData) throws DataException;
public void delete(String address) throws DataException;
// Account balances
public AccountBalanceData getBalance(String address, long assetId) throws DataException;

View File

@ -69,7 +69,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public void delete(String atAddress) throws DataException {
try {
this.repository.delete("ATs", "atAddress = ?", atAddress);
this.repository.delete("ATs", "AT_address = ?", atAddress);
// AT States also deleted via ON DELETE CASCADE
} catch (SQLException e) {
throw new DataException("Unable to delete AT from repository", e);

View File

@ -17,6 +17,8 @@ public class HSQLDBAccountRepository implements AccountRepository {
this.repository = repository;
}
// General account
@Override
public AccountData getAccount(String address) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT reference FROM Accounts WHERE account = ?", address)) {
@ -41,6 +43,19 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void delete(String address) throws DataException {
// NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
// definition.
try {
this.repository.delete("Accounts", "account = ?", address);
} catch (SQLException e) {
throw new DataException("Unable to delete account from repository", e);
}
}
// Account balances
@Override
public AccountBalanceData getBalance(String address, long assetId) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? and asset_id = ?", address, assetId)) {

View File

@ -99,6 +99,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TYPE ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC_NO_PAD");
stmt.execute("CREATE TYPE ATCode AS BLOB(64K)"); // 16bit * 1
stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4
stmt.execute("CREATE TYPE ATStateHash as VARBINARY(32)");
stmt.execute("CREATE TYPE ATMessage AS VARBINARY(256)");
break;
@ -302,7 +303,7 @@ public class HSQLDBDatabaseUpdates {
// Accounts
stmt.execute("CREATE TABLE Accounts (account QoraAddress, reference Signature, PRIMARY KEY (account))");
stmt.execute("CREATE TABLE AccountBalances (account QoraAddress, asset_id AssetID, balance QoraAmount NOT NULL, "
+ "PRIMARY KEY (account, asset_id))");
+ "PRIMARY KEY (account, asset_id), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)");
break;
case 23:
@ -358,11 +359,12 @@ public class HSQLDBDatabaseUpdates {
// For finding executable ATs
stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, AT_address)");
// AT state on a per-block basis
stmt.execute("CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, state_data ATState, "
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
stmt.execute(
"CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, state_data ATState, state_hash ATStateHash NOT NULL, fees QoraAmount NOT NULL, "
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
// Generated AT Transactions
stmt.execute(
"CREATE TABLE ATTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress, amount QoraAmount NOT NULL, message ATMessage, "
"CREATE TABLE ATTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress, amount QoraAmount, asset_id AssetID, message ATMessage, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;

View File

@ -0,0 +1,63 @@
package repository.hsqldb.transaction;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import data.transaction.ATTransactionData;
import data.transaction.TransactionData;
import repository.DataException;
import repository.hsqldb.HSQLDBRepository;
import repository.hsqldb.HSQLDBSaver;
public class HSQLDBATTransactionRepository extends HSQLDBTransactionRepository {
public HSQLDBATTransactionRepository(HSQLDBRepository repository) {
this.repository = repository;
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT sender, recipient, amount, asset_id, message FROM ATTransactions WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
byte[] senderPublicKey = resultSet.getBytes(1);
String recipient = resultSet.getString(2);
BigDecimal amount = resultSet.getBigDecimal(3);
if (resultSet.wasNull())
amount = null;
Long assetId = resultSet.getLong(4);
if (resultSet.wasNull())
assetId = null;
byte[] message = resultSet.getBytes(5);
if (resultSet.wasNull())
message = null;
return new ATTransactionData(senderPublicKey, recipient, amount, assetId, message, fee, timestamp, reference, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT transaction from repository", e);
}
}
@Override
public void save(TransactionData transactionData) throws DataException {
ATTransactionData atTransactionData = (ATTransactionData) transactionData;
HSQLDBSaver saveHelper = new HSQLDBSaver("ATTransactions");
saveHelper.bind("signature", atTransactionData.getSignature()).bind("sender", atTransactionData.getSenderPublicKey())
.bind("recipient", atTransactionData.getRecipient()).bind("amount", atTransactionData.getAmount())
.bind("asset_id", atTransactionData.getAssetId()).bind("message", atTransactionData.getMessage());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save AT transaction into repository", e);
}
}
}

View File

@ -37,6 +37,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
private HSQLDBMultiPaymentTransactionRepository multiPaymentTransactionRepository;
private HSQLDBDeployATTransactionRepository deployATTransactionRepository;
private HSQLDBMessageTransactionRepository messageTransactionRepository;
private HSQLDBATTransactionRepository atTransactionRepository;
public HSQLDBTransactionRepository(HSQLDBRepository repository) {
this.repository = repository;
@ -57,6 +58,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
this.multiPaymentTransactionRepository = new HSQLDBMultiPaymentTransactionRepository(repository);
this.deployATTransactionRepository = new HSQLDBDeployATTransactionRepository(repository);
this.messageTransactionRepository = new HSQLDBMessageTransactionRepository(repository);
this.atTransactionRepository = new HSQLDBATTransactionRepository(repository);
}
protected HSQLDBTransactionRepository() {
@ -154,8 +156,11 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
case MESSAGE:
return this.messageTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
case AT:
return this.atTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
default:
throw new DataException("Unsupported transaction type [" + type.value + "] during fetch from HSQLDB repository");
throw new DataException("Unsupported transaction type [" + type.name() + "] during fetch from HSQLDB repository");
}
}
@ -317,8 +322,12 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
this.messageTransactionRepository.save(transactionData);
break;
case AT:
this.atTransactionRepository.save(transactionData);
break;
default:
throw new DataException("Unsupported transaction type [" + transactionData.getType().value + "] during save into HSQLDB repository");
throw new DataException("Unsupported transaction type [" + transactionData.getType().name() + "] during save into HSQLDB repository");
}
}

View File

@ -97,7 +97,9 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
Serialization.serializeSizedString(bytes, deployATTransactionData.getTags());
bytes.write(deployATTransactionData.getCreationBytes());
byte[] creationBytes = deployATTransactionData.getCreationBytes();
bytes.write(Ints.toByteArray(creationBytes.length));
bytes.write(creationBytes);
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount());
@ -146,15 +148,14 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
// Omitted: Serialization.serializeSizedString(bytes, deployATTransactionData.getTags());
bytes.write(deployATTransactionData.getCreationBytes());
byte[] creationBytes = deployATTransactionData.getCreationBytes();
bytes.write(Ints.toByteArray(creationBytes.length));
bytes.write(creationBytes);
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount());
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getFee());
if (deployATTransactionData.getSignature() != null)
bytes.write(deployATTransactionData.getSignature());
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);

View File

@ -221,8 +221,8 @@ public class v1feeder extends Thread {
ValidationResult result = block.isValid();
if (result != ValidationResult.OK) {
LOGGER.error("Invalid block, validation result code: " + result.value);
throw new RuntimeException("Invalid block, validation result code: " + result.value);
LOGGER.error("Invalid block, validation result: " + result.name());
throw new RuntimeException("Invalid block, validation result: " + result.name());
}
block.process();