Initial support for account flags + tx (genesis account use only atm)

This commit is contained in:
catbref 2019-02-27 17:00:54 +00:00
parent 85acc4d9df
commit 2dc1720af8
12 changed files with 478 additions and 8 deletions

View File

@ -226,4 +226,16 @@ public class Account {
LOGGER.trace(String.format("Account %s defaultGroupId now %d", accountData.getAddress(), defaultGroupId));
}
// Account flags
public Integer getFlags() throws DataException {
return this.repository.getAccountRepository().getFlags(this.address);
}
public void setFlags(int flags) throws DataException {
AccountData accountData = this.buildAccountData();
accountData.setFlags(flags);
this.repository.getAccountRepository().setFlags(accountData);
}
}

View File

@ -14,6 +14,7 @@ public class AccountData {
protected byte[] reference;
protected byte[] publicKey;
protected int defaultGroupId;
protected int flags;
// Constructors
@ -21,15 +22,16 @@ public class AccountData {
protected AccountData() {
}
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId) {
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags) {
this.address = address;
this.reference = reference;
this.publicKey = publicKey;
this.defaultGroupId = defaultGroupId;
this.flags = flags;
}
public AccountData(String address) {
this(address, null, null, Group.NO_GROUP);
this(address, null, null, Group.NO_GROUP, 0);
}
// Getters/Setters
@ -62,6 +64,14 @@ public class AccountData {
this.defaultGroupId = defaultGroupId;
}
public int getFlags() {
return this.flags;
}
public void setFlags(int flags) {
this.flags = flags;
}
// Comparison
@Override

View File

@ -0,0 +1,104 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
import org.qora.account.GenesisAccount;
import org.qora.block.GenesisBlock;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = {TransactionData.class})
//JAXB: use this subclass if XmlDiscriminatorNode matches XmlDiscriminatorValue below:
@XmlDiscriminatorValue("ACCOUNT_FLAGS")
public class AccountFlagsTransactionData extends TransactionData {
private String target;
private int andMask;
private int orMask;
private int xorMask;
private Integer previousFlags;
// Constructors
// For JAXB
protected AccountFlagsTransactionData() {
super(TransactionType.ACCOUNT_FLAGS);
}
public void afterUnmarshal(Unmarshaller u, Object parent) {
/*
* If we're being constructed as part of the genesis block info inside blockchain config
* and no specific creator's public key is supplied
* then use genesis account's public key.
*/
if (parent instanceof GenesisBlock.GenesisInfo && this.creatorPublicKey == null)
this.creatorPublicKey = GenesisAccount.PUBLIC_KEY;
}
public AccountFlagsTransactionData(long timestamp, int groupId, byte[] reference, byte[] creatorPublicKey, String target, int andMask, int orMask,
int xorMask, Integer previousFlags, BigDecimal fee, byte[] signature) {
super(TransactionType.ACCOUNT_FLAGS, timestamp, groupId, reference, creatorPublicKey, fee, signature);
this.target = target;
this.andMask = andMask;
this.orMask = orMask;
this.xorMask = xorMask;
this.previousFlags = previousFlags;
}
// Typically used in deserialization context
public AccountFlagsTransactionData(long timestamp, int groupId, byte[] reference, byte[] creatorPublicKey, String target, int andMask, int orMask,
int xorMask, BigDecimal fee, byte[] signature) {
this(timestamp, groupId, reference, creatorPublicKey, target, andMask, orMask, xorMask, null, fee, signature);
}
// Getters / setters
public String getTarget() {
return this.target;
}
public int getAndMask() {
return this.andMask;
}
public int getOrMask() {
return this.orMask;
}
public int getXorMask() {
return this.xorMask;
}
public Integer getPreviousFlags() {
return this.previousFlags;
}
public void setPreviousFlags(Integer previousFlags) {
this.previousFlags = previousFlags;
}
// Re-expose to JAXB
@Override
@XmlElement
public byte[] getCreatorPublicKey() {
return super.getCreatorPublicKey();
}
@Override
@XmlElement
public void setCreatorPublicKey(byte[] creatorPublicKey) {
super.setCreatorPublicKey(creatorPublicKey);
}
}

View File

@ -37,7 +37,8 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
GroupKickTransactionData.class, GroupInviteTransactionData.class,
JoinGroupTransactionData.class, LeaveGroupTransactionData.class,
GroupApprovalTransactionData.class, SetGroupTransactionData.class,
UpdateAssetTransactionData.class
UpdateAssetTransactionData.class,
AccountFlagsTransactionData.class
})
//All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)

View File

@ -18,6 +18,9 @@ public interface AccountRepository {
/** Returns account's default groupID or null if account not found. */
public Integer getDefaultGroupId(String address) throws DataException;
/** Returns account's flags or null if account not found. */
public Integer getFlags(String address) throws DataException;
/**
* Ensures at least minimal account info in repository.
* <p>
@ -39,6 +42,13 @@ public interface AccountRepository {
*/
public void setDefaultGroupId(AccountData accountData) throws DataException;
/**
* Saves account's flags, and public key if present, in repository.
* <p>
* Note: ignores other fields like last reference, default groupID.
*/
public void setFlags(AccountData accountData) throws DataException;
public void delete(String address) throws DataException;
// Account balances

View File

@ -26,15 +26,16 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public AccountData getAccount(String address) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT reference, public_key, default_group_id FROM Accounts WHERE account = ?", address)) {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT reference, public_key, default_group_id, flags FROM Accounts WHERE account = ?", address)) {
if (resultSet == null)
return null;
byte[] reference = resultSet.getBytes(1);
byte[] publicKey = resultSet.getBytes(2);
int defaultGroupId = resultSet.getInt(3);
int flags = resultSet.getInt(4);
return new AccountData(address, reference, publicKey, defaultGroupId);
return new AccountData(address, reference, publicKey, defaultGroupId, flags);
} catch (SQLException e) {
throw new DataException("Unable to fetch account info from repository", e);
}
@ -65,6 +66,19 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public Integer getFlags(String address) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT flags FROM Accounts WHERE account = ?", address)) {
if (resultSet == null)
return null;
// Column is NOT NULL so this should never implicitly convert to 0
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Unable to fetch account's flags from repository", e);
}
}
@Override
public void ensureAccount(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
@ -116,6 +130,23 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void setFlags(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
saveHelper.bind("account", accountData.getAddress()).bind("flags", accountData.getFlags());
byte[] publicKey = accountData.getPublicKey();
if (publicKey != null)
saveHelper.bind("public_key", publicKey);
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save account's flags into repository", e);
}
}
@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

View File

@ -690,6 +690,14 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE AssetTrades ADD initiator_saving QoraAmount NOT NULL DEFAULT 0");
break;
case 44:
// Account flags
stmt.execute("ALTER TABLE Accounts ADD COLUMN flags INT NOT NULL DEFAULT 0");
// Corresponding transaction to set/clear flags
stmt.execute("CREATE TABLE AccountFlagsTransactions (signature Signature, creator QoraPublicKey NOT NULL, target QoraAddress NOT NULL, and_mask INT NOT NULL, or_mask INT NOT NULL, xor_mask INT NOT NULL, "
+ "previous_flags INT, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
default:
// nothing to do
return false;

View File

@ -0,0 +1,59 @@
package org.qora.repository.hsqldb.transaction;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.qora.data.transaction.AccountFlagsTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.hsqldb.HSQLDBRepository;
import org.qora.repository.hsqldb.HSQLDBSaver;
public class HSQLDBAccountFlagsTransactionRepository extends HSQLDBTransactionRepository {
public HSQLDBAccountFlagsTransactionRepository(HSQLDBRepository repository) {
this.repository = repository;
}
TransactionData fromBase(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, BigDecimal fee, byte[] signature) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT target, and_mask, or_mask, xor_mask, previous_flags FROM AccountFlagsTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
String target = resultSet.getString(1);
int andMask = resultSet.getInt(2);
int orMask = resultSet.getInt(3);
int xorMask = resultSet.getInt(4);
Integer previousFlags = resultSet.getInt(5);
if (resultSet.wasNull())
previousFlags = null;
return new AccountFlagsTransactionData(timestamp, txGroupId, reference, creatorPublicKey, target, andMask, orMask, xorMask, previousFlags, fee,
signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch account flags transaction from repository", e);
}
}
@Override
public void save(TransactionData transactionData) throws DataException {
AccountFlagsTransactionData accountFlagsTransactionData = (AccountFlagsTransactionData) transactionData;
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountFlagsTransactions");
saveHelper.bind("signature", accountFlagsTransactionData.getSignature()).bind("creator", accountFlagsTransactionData.getCreatorPublicKey())
.bind("target", accountFlagsTransactionData.getTarget()).bind("and_mask", accountFlagsTransactionData.getAndMask())
.bind("or_mask", accountFlagsTransactionData.getOrMask()).bind("xor_mask", accountFlagsTransactionData.getXorMask())
.bind("previous_flags", accountFlagsTransactionData.getPreviousFlags());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save account flags transaction into repository", e);
}
}
}

View File

@ -0,0 +1,134 @@
package org.qora.transaction;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.qora.account.Account;
import org.qora.account.GenesisAccount;
import org.qora.asset.Asset;
import org.qora.data.transaction.AccountFlagsTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
public class AccountFlagsTransaction extends Transaction {
// Properties
private AccountFlagsTransactionData accountFlagsTransactionData;
// Constructors
public AccountFlagsTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.accountFlagsTransactionData = (AccountFlagsTransactionData) this.transactionData;
}
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.emptyList();
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
if (address.equals(this.getCreator().getAddress()))
return true;
if (address.equals(this.getTarget().getAddress()))
return true;
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);
if (address.equals(this.getCreator().getAddress()))
amount = amount.subtract(this.transactionData.getFee());
return amount;
}
// Navigation
public Account getTarget() {
return new Account(this.repository, this.accountFlagsTransactionData.getTarget());
}
// Processing
@Override
public ValidationResult isValid() throws DataException {
Account creator = getCreator();
// Only genesis account can modify flags
if (!creator.getAddress().equals(new GenesisAccount(repository).getAddress()))
return ValidationResult.NO_FLAG_PERMISSION;
// Check fee is zero or positive
if (accountFlagsTransactionData.getFee().compareTo(BigDecimal.ZERO) < 0)
return ValidationResult.NEGATIVE_FEE;
// Check reference
if (!Arrays.equals(creator.getLastReference(), accountFlagsTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
// Check creator has enough funds
if (creator.getConfirmedBalance(Asset.QORA).compareTo(accountFlagsTransactionData.getFee()) < 0)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
@Override
public void process() throws DataException {
Account target = getTarget();
int previousFlags = target.getFlags();
accountFlagsTransactionData.setPreviousFlags(previousFlags);
// Save this transaction with target account's previous flags value
this.repository.getTransactionRepository().save(accountFlagsTransactionData);
// Set account's new flags
int newFlags = previousFlags & accountFlagsTransactionData.getAndMask()
| accountFlagsTransactionData.getOrMask() ^ accountFlagsTransactionData.getXorMask();
target.setFlags(newFlags);
// Update creator's balance
Account creator = getCreator();
creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(accountFlagsTransactionData.getFee()));
// Update creator's reference
creator.setLastReference(accountFlagsTransactionData.getSignature());
}
@Override
public void orphan() throws DataException {
// Revert
Account target = getTarget();
target.setFlags(accountFlagsTransactionData.getPreviousFlags());
// Delete this transaction itself
this.repository.getTransactionRepository().delete(accountFlagsTransactionData);
Account creator = getCreator();
// Update creator's balance
creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(accountFlagsTransactionData.getFee()));
// Update creator's reference
creator.setLastReference(accountFlagsTransactionData.getReference());
}
}

View File

@ -72,7 +72,8 @@ public abstract class Transaction {
LEAVE_GROUP(32, false),
GROUP_APPROVAL(33, false),
SET_GROUP(34, false),
UPDATE_ASSET(35, true);
UPDATE_ASSET(35, true),
ACCOUNT_FLAGS(36, false);
public final int value;
public final boolean needsApproval;
@ -190,6 +191,7 @@ public abstract class Transaction {
MULTIPLE_NAMES_FORBIDDEN(69),
INVALID_ASSET_OWNER(70),
AT_IS_FINISHED(71),
NO_FLAG_PERMISSION(72),
NOT_YET_RELEASED(1000);
public final int value;

View File

@ -0,0 +1,99 @@
package org.qora.transform.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import org.qora.block.BlockChain;
import org.qora.data.transaction.AccountFlagsTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transform.TransformationException;
import org.qora.utils.Serialization;
import com.google.common.primitives.Ints;
public class AccountFlagsTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int TARGET_LENGTH = ADDRESS_LENGTH;
private static final int AND_MASK_LENGTH = INT_LENGTH;
private static final int OR_MASK_LENGTH = INT_LENGTH;
private static final int XOR_MASK_LENGTH = INT_LENGTH;
private static final int EXTRAS_LENGTH = TARGET_LENGTH + AND_MASK_LENGTH + OR_MASK_LENGTH + XOR_MASK_LENGTH;
protected static final TransactionLayout layout;
static {
layout = new TransactionLayout();
layout.add("txType: " + TransactionType.GROUP_INVITE.valueString, TransformationType.INT);
layout.add("timestamp", TransformationType.TIMESTAMP);
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("account's public key", TransformationType.PUBLIC_KEY);
layout.add("target account's address", TransformationType.ADDRESS);
layout.add("flags AND mask", TransformationType.INT);
layout.add("flags OR mask", TransformationType.INT);
layout.add("flags XOR mask", TransformationType.INT);
layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE);
}
public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
long timestamp = byteBuffer.getLong();
int txGroupId = 0;
if (timestamp >= BlockChain.getInstance().getQoraV2Timestamp())
txGroupId = byteBuffer.getInt();
byte[] reference = new byte[REFERENCE_LENGTH];
byteBuffer.get(reference);
byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer);
String target = Serialization.deserializeAddress(byteBuffer);
int andMask = byteBuffer.getInt();
int orMask = byteBuffer.getInt();
int xorMask = byteBuffer.getInt();
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
return new AccountFlagsTransactionData(timestamp, txGroupId, reference, creatorPublicKey, target, andMask, orMask, xorMask, fee, signature);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
return getBaseLength(transactionData) + EXTRAS_LENGTH;
}
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
try {
AccountFlagsTransactionData accountFlagsTransactionData = (AccountFlagsTransactionData) transactionData;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
transformCommonBytes(transactionData, bytes);
Serialization.serializeAddress(bytes, accountFlagsTransactionData.getTarget());
bytes.write(Ints.toByteArray(accountFlagsTransactionData.getAndMask()));
bytes.write(Ints.toByteArray(accountFlagsTransactionData.getOrMask()));
bytes.write(Ints.toByteArray(accountFlagsTransactionData.getXorMask()));
Serialization.serializeBigDecimal(bytes, accountFlagsTransactionData.getFee());
if (accountFlagsTransactionData.getSignature() != null)
bytes.write(accountFlagsTransactionData.getSignature());
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);
}
}
}

View File

@ -103,7 +103,7 @@ public class TransactionTests extends Common {
// Create test generator account
generator = new PrivateKeyAccount(repository, generatorSeed);
accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.NO_GROUP));
accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.NO_GROUP, 0));
accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORA, initialGeneratorBalance));
// Create test sender account
@ -111,7 +111,7 @@ public class TransactionTests extends Common {
// Mock account
reference = senderSeed;
accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.NO_GROUP));
accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.NO_GROUP, 0));
// Mock balance
accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, initialSenderBalance));