interim commit with proxy forging repository/transaction support

no block validity/generator support yet
This commit is contained in:
catbref 2019-03-01 17:18:06 +00:00
parent 2dc1720af8
commit 3c06d358b7
22 changed files with 1131 additions and 10 deletions

View File

@ -21,6 +21,11 @@ public class Account {
private static final Logger LOGGER = LogManager.getLogger(Account.class);
public static final int TIER1_FORGING_MASK = 0x1;
public static final int TIER2_FORGING_MASK = 0x2;
public static final int TIER3_FORGING_MASK = 0x4;
public static final int FORGING_MASK = TIER1_FORGING_MASK | TIER2_FORGING_MASK | TIER3_FORGING_MASK;
public static final int ADDRESS_LENGTH = 25;
protected Repository repository;
@ -238,4 +243,12 @@ public class Account {
this.repository.getAccountRepository().setFlags(accountData);
}
// Forging Enabler
public void setForgingEnabler(String address) throws DataException {
AccountData accountData = this.buildAccountData();
accountData.setForgingEnabler(address);
this.repository.getAccountRepository().setForgingEnabler(accountData);
}
}

View File

@ -3,6 +3,7 @@ package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -10,6 +11,7 @@ import java.math.BigDecimal;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
@ -24,12 +26,17 @@ import org.qora.api.ApiExceptionFactory;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountData;
import org.qora.data.transaction.ProxyForgingTransactionData;
import org.qora.group.Group;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
import org.qora.transform.Transformer;
import org.qora.transform.transaction.ProxyForgingTransactionTransformer;
import org.qora.utils.Base58;
@Path("/addresses")
@ -261,4 +268,50 @@ public class AddressesResource {
}
}
@POST
@Path("/proxyforging")
@Operation(
summary = "Build raw, unsigned, PROXY_FORGING transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = ProxyForgingTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, PROXY_FORGING transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String proxyForging(ProxyForgingTransactionData transactionData) {
if (Settings.getInstance().isApiRestricted())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = ProxyForgingTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -15,6 +16,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
@ -28,10 +30,16 @@ import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory;
import org.qora.block.Block;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.EnableForgingTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
import org.qora.transform.transaction.EnableForgingTransactionTransformer;
import org.qora.utils.Base58;
@Path("/blocks")
@ -518,4 +526,50 @@ public class BlocksResource {
}
}
@POST
@Path("/enableforging")
@Operation(
summary = "Build raw, unsigned, ENABLE_FORGING transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = EnableForgingTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, ENABLE_FORGING transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String enableForging(EnableForgingTransactionData transactionData) {
if (Settings.getInstance().isApiRestricted())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = EnableForgingTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@ -15,6 +15,7 @@ public class AccountData {
protected byte[] publicKey;
protected int defaultGroupId;
protected int flags;
protected String forgingEnabler;
// Constructors
@ -22,16 +23,17 @@ public class AccountData {
protected AccountData() {
}
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags) {
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, String forgingEnabler) {
this.address = address;
this.reference = reference;
this.publicKey = publicKey;
this.defaultGroupId = defaultGroupId;
this.flags = flags;
this.forgingEnabler = forgingEnabler;
}
public AccountData(String address) {
this(address, null, null, Group.NO_GROUP, 0);
this(address, null, null, Group.NO_GROUP, 0, null);
}
// Getters/Setters
@ -72,6 +74,14 @@ public class AccountData {
this.flags = flags;
}
public String getForgingEnabler() {
return this.forgingEnabler;
}
public void setForgingEnabler(String forgingEnabler) {
this.forgingEnabler = forgingEnabler;
}
// Comparison
@Override

View File

@ -0,0 +1,50 @@
package org.qora.data.account;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class ProxyForgerData {
// Properties
private byte[] forgerPublicKey;
private String recipient;
private byte[] proxyPublicKey;
private BigDecimal share;
// Constructors
// For JAXB
protected ProxyForgerData() {
}
// Used when fetching from repository
public ProxyForgerData(byte[] forgerPublicKey, String recipient, byte[] proxyPublicKey, BigDecimal share) {
this.forgerPublicKey = forgerPublicKey;
this.recipient = recipient;
this.proxyPublicKey = proxyPublicKey;
this.share = share;
}
// Getters / setters
public byte[] getForgerPublicKey() {
return this.forgerPublicKey;
}
public String getRecipient() {
return this.recipient;
}
public byte[] getProxyPublicKey() {
return this.proxyPublicKey;
}
public BigDecimal getShare() {
return this.share;
}
}

View File

@ -0,0 +1,53 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
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})
public class EnableForgingTransactionData extends TransactionData {
private String target;
// Constructors
// For JAXB
protected EnableForgingTransactionData() {
super(TransactionType.ENABLE_FORGING);
}
public EnableForgingTransactionData(long timestamp, int groupId, byte[] reference, byte[] creatorPublicKey, String target, BigDecimal fee, byte[] signature) {
super(TransactionType.ENABLE_FORGING, timestamp, groupId, reference, creatorPublicKey, fee, signature);
this.target = target;
}
// Getters / setters
public String getTarget() {
return this.target;
}
// Re-expose to JAXB
@Override
@XmlElement
public byte[] getCreatorPublicKey() {
return super.getCreatorPublicKey();
}
@Override
@XmlElement
public void setCreatorPublicKey(byte[] creatorPublicKey) {
super.setCreatorPublicKey(creatorPublicKey);
}
}

View File

@ -0,0 +1,81 @@
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.XmlTransient;
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})
public class ProxyForgingTransactionData extends TransactionData {
private byte[] forgerPublicKey;
private String recipient;
private byte[] proxyPublicKey;
private BigDecimal share;
// No need to ever expose this via API
@XmlTransient
@Schema(hidden = true)
private BigDecimal previousShare;
// Constructors
// For JAXB
protected ProxyForgingTransactionData() {
super(TransactionType.PROXY_FORGING);
}
public void afterUnmarshal(Unmarshaller u, Object parent) {
this.creatorPublicKey = this.forgerPublicKey;
}
public ProxyForgingTransactionData(long timestamp, int groupId, byte[] reference, byte[] forgerPublicKey, String recipient, byte[] proxyPublicKey, BigDecimal share, BigDecimal previousShare, BigDecimal fee, byte[] signature) {
super(TransactionType.PROXY_FORGING, timestamp, groupId, reference, forgerPublicKey, fee, signature);
this.forgerPublicKey = forgerPublicKey;
this.recipient = recipient;
this.proxyPublicKey = proxyPublicKey;
this.share = share;
this.previousShare = previousShare;
}
// Used in deserialization context
public ProxyForgingTransactionData(long timestamp, int groupId, byte[] reference, byte[] forgerPublicKey, String recipient, byte[] proxyPublicKey, BigDecimal share, BigDecimal fee, byte[] signature) {
this(timestamp, groupId, reference, forgerPublicKey, recipient, proxyPublicKey, share, null, fee, signature);
}
// Getters / setters
public byte[] getForgerPublicKey() {
return this.forgerPublicKey;
}
public String getRecipient() {
return this.recipient;
}
public byte[] getProxyPublicKey() {
return this.proxyPublicKey;
}
public BigDecimal getShare() {
return this.share;
}
public BigDecimal getPreviousShare() {
return this.previousShare;
}
public void setPreviousShare(BigDecimal previousShare) {
this.previousShare = previousShare;
}
}

View File

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

View File

@ -4,6 +4,7 @@ import java.util.List;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.account.ProxyForgerData;
public interface AccountRepository {
@ -21,6 +22,9 @@ public interface AccountRepository {
/** Returns account's flags or null if account not found. */
public Integer getFlags(String address) throws DataException;
/** Returns number of accounts enabled to forge by given address. */
public int countForgingAccountsEnabledByAddress(String address) throws DataException;
/**
* Ensures at least minimal account info in repository.
* <p>
@ -49,6 +53,14 @@ public interface AccountRepository {
*/
public void setFlags(AccountData accountData) throws DataException;
/**
* Saves account's forging enabler, and public key if present, in repository.
* <p>
* Note: ignores other fields like last reference, default groupID.
*/
public void setForgingEnabler(AccountData accountData) throws DataException;
/** Delete account from repository. */
public void delete(String address) throws DataException;
// Account balances
@ -67,4 +79,14 @@ public interface AccountRepository {
public void delete(String address, long assetId) throws DataException;
// Proxy forging
public ProxyForgerData getProxyForgeData(byte[] forgerPublicKey, String recipient) throws DataException;
public ProxyForgerData getProxyForgeData(byte[] proxyPublicKey) throws DataException;
public void save(ProxyForgerData proxyForgerData) throws DataException;
public void delete(byte[] forgerPublickey, String recipient) throws DataException;
}

View File

@ -92,6 +92,15 @@ public interface BlockRepository {
return getTransactionsFromSignature(signature, null, null, null);
}
/**
* Returns number of blocks forged by account with given public key.
*
* @param publicKey
* @return number of blocks
* @throws DataException
*/
public int countForgedBlocks(byte[] publicKey) throws DataException;
/**
* Saves block into repository.
*

View File

@ -11,6 +11,7 @@ import java.util.stream.Collectors;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.account.ProxyForgerData;
import org.qora.repository.AccountRepository;
import org.qora.repository.DataException;
@ -26,7 +27,8 @@ 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, flags FROM Accounts WHERE account = ?", address)) {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT reference, public_key, default_group_id, flags, forging_enabler FROM Accounts WHERE account = ?", address)) {
if (resultSet == null)
return null;
@ -34,8 +36,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
byte[] publicKey = resultSet.getBytes(2);
int defaultGroupId = resultSet.getInt(3);
int flags = resultSet.getInt(4);
String forgingEnabler = resultSet.getString(5);
return new AccountData(address, reference, publicKey, defaultGroupId, flags);
return new AccountData(address, reference, publicKey, defaultGroupId, flags, forgingEnabler);
} catch (SQLException e) {
throw new DataException("Unable to fetch account info from repository", e);
}
@ -79,6 +82,15 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public int countForgingAccountsEnabledByAddress(String address) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT COUNT(*) FROM Accounts WHERE forging_enabler = ? LIMIT 1", address)) {
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Unable to count forging accounts enabled in repository", e);
}
}
@Override
public void ensureAccount(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
@ -147,6 +159,23 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void setForgingEnabler(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
saveHelper.bind("account", accountData.getAddress()).bind("forging_enabler", accountData.getForgingEnabler());
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 forging enabler 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
@ -267,4 +296,62 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
// Proxy forging
@Override
public ProxyForgerData getProxyForgeData(byte[] forgerPublicKey, String recipient) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT proxy_public_key, share FROM ProxyForgers WHERE forger = ? AND recipient = ?",
forgerPublicKey, recipient)) {
if (resultSet == null)
return null;
byte[] proxyPublicKey = resultSet.getBytes(1);
BigDecimal share = resultSet.getBigDecimal(2);
return new ProxyForgerData(forgerPublicKey, recipient, proxyPublicKey, share);
} catch (SQLException e) {
throw new DataException("Unable to fetch proxy forge info from repository", e);
}
}
@Override
public ProxyForgerData getProxyForgeData(byte[] proxyPublicKey) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT forger, recipient, share FROM ProxyForgers WHERE proxy_public_key = ?",
proxyPublicKey)) {
if (resultSet == null)
return null;
byte[] forgerPublicKey = resultSet.getBytes(1);
String recipient = resultSet.getString(2);
BigDecimal share = resultSet.getBigDecimal(3);
return new ProxyForgerData(forgerPublicKey, recipient, proxyPublicKey, share);
} catch (SQLException e) {
throw new DataException("Unable to fetch proxy forge info from repository", e);
}
}
@Override
public void save(ProxyForgerData proxyForgerData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("ProxyForgers");
saveHelper.bind("forger", proxyForgerData.getForgerPublicKey()).bind("recipient", proxyForgerData.getRecipient())
.bind("proxy_public_key", proxyForgerData.getProxyPublicKey()).bind("share", proxyForgerData.getShare());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save proxy forge info into repository", e);
}
}
@Override
public void delete(byte[] forgerPublickey, String recipient) throws DataException {
try {
this.repository.delete("ProxyForgers", "forger = ? and recipient = ?", forgerPublickey, recipient);
} catch (SQLException e) {
throw new DataException("Unable to delete proxy forge info from repository", e);
}
}
}

View File

@ -149,6 +149,15 @@ public class HSQLDBBlockRepository implements BlockRepository {
return transactions;
}
@Override
public int countForgedBlocks(byte[] publicKey) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT COUNT(*) FROM Blocks WHERE generator = ? LIMIT 1", publicKey)) {
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Unable to fetch forged blocks count from repository", e);
}
}
@Override
public void save(BlockData blockData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Blocks");

View File

@ -698,6 +698,27 @@ public class HSQLDBDatabaseUpdates {
+ "previous_flags INT, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
case 38:
// Enabling other accounts to forge
// Transaction to allow one account to enable other account to forge
stmt.execute("CREATE TABLE EnableForgingTransactions (signature Signature, creator QoraPublicKey NOT NULL, target QoraAddress NOT NULL, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
// Modification to accounts to record who enabled them to forge (useful for counting accounts and potentially orphaning)
stmt.execute("ALTER TABLE Accounts ADD COLUMN forging_enabler QoraAddress");
break;
case 39:
// Proxy forging
// Transaction emitted by forger announcing they are forging on behalf of recipient
stmt.execute("CREATE TABLE ProxyForgingTransactions (signature Signature, forger QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, proxy_public_key QoraPublicKey NOT NULL, share DECIMAL(5,2) NOT NULL, "
+ "previous_share DECIMAL(5,2), PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
// Table of current shares
stmt.execute("CREATE TABLE ProxyForgers (forger QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, proxy_public_key QoraPublicKey NOT NULL, share DECIMAL(5,2) NOT NULL, "
+ "PRIMARY KEY (forger, recipient))");
// Proxy-forged blocks will contain proxy public key, which will be used to look up block reward sharing, so create index for those lookups
stmt.execute("CREATE INDEX ProxyForgersProxyPublicKeyIndex ON ProxyForgers (proxy_public_key)");
break;
default:
// nothing to do
return false;

View File

@ -0,0 +1,49 @@
package org.qora.repository.hsqldb.transaction;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.qora.data.transaction.EnableForgingTransactionData;
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 HSQLDBEnableForgingTransactionRepository extends HSQLDBTransactionRepository {
public HSQLDBEnableForgingTransactionRepository(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 FROM EnableForgingTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
String target = resultSet.getString(1);
return new EnableForgingTransactionData(timestamp, txGroupId, reference, creatorPublicKey, target, 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 {
EnableForgingTransactionData enableForgingTransactionData = (EnableForgingTransactionData) transactionData;
HSQLDBSaver saveHelper = new HSQLDBSaver("EnableForgingTransactions");
saveHelper.bind("signature", enableForgingTransactionData.getSignature()).bind("creator", enableForgingTransactionData.getCreatorPublicKey())
.bind("target", enableForgingTransactionData.getTarget());
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,53 @@
package org.qora.repository.hsqldb.transaction;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.qora.data.transaction.ProxyForgingTransactionData;
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 HSQLDBProxyForgingTransactionRepository extends HSQLDBTransactionRepository {
public HSQLDBProxyForgingTransactionRepository(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 recipient, proxy_public_key, share, previous_share FROM ProxyForgingTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
String recipient = resultSet.getString(1);
byte[] proxyPublicKey = resultSet.getBytes(2);
BigDecimal share = resultSet.getBigDecimal(3);
BigDecimal previousShare = resultSet.getBigDecimal(4);
return new ProxyForgingTransactionData(timestamp, txGroupId, reference, creatorPublicKey, recipient, proxyPublicKey, share, previousShare, fee, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch proxy forging transaction from repository", e);
}
}
@Override
public void save(TransactionData transactionData) throws DataException {
ProxyForgingTransactionData proxyForgingTransactionData = (ProxyForgingTransactionData) transactionData;
HSQLDBSaver saveHelper = new HSQLDBSaver("ProxyForgingTransactions");
saveHelper.bind("signature", proxyForgingTransactionData.getSignature()).bind("forger", proxyForgingTransactionData.getForgerPublicKey())
.bind("recipient", proxyForgingTransactionData.getRecipient()).bind("proxy_public_key", proxyForgingTransactionData.getProxyPublicKey())
.bind("share", proxyForgingTransactionData.getShare()).bind("previous_share", proxyForgingTransactionData.getPreviousShare());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save proxy forging transaction into repository", e);
}
}
}

View File

@ -0,0 +1,198 @@
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.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.data.transaction.EnableForgingTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
public class EnableForgingTransaction extends Transaction {
public static final int TIER1_MIN_FORGED_BLOCKS = 50;
public static final int TIER1_MAX_ENABLED_ACCOUNTS = 5;
public static final int TIER2_MIN_FORGED_BLOCKS = 5;
public static final int TIER2_MAX_ENABLED_ACCOUNTS = 5;
// Properties
private EnableForgingTransactionData enableForgingTransactionData;
// Constructors
public EnableForgingTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.enableForgingTransactionData = (EnableForgingTransactionData) 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.enableForgingTransactionData.getTarget());
}
// Processing
@Override
public ValidationResult isValid() throws DataException {
PublicKeyAccount creator = getCreator();
// Creator needs to have at least one forging-enabled account flag set
Integer creatorFlags = creator.getFlags();
if (creatorFlags == null)
return ValidationResult.INVALID_ADDRESS;
if ((creatorFlags & Account.FORGING_MASK) == 0)
return ValidationResult.NO_FORGING_PERMISSION;
// Tier3 forgers can't enable further accounts
if ((creatorFlags & Account.TIER3_FORGING_MASK) != 0)
return ValidationResult.FORGING_ENABLE_LIMIT;
Account target = getTarget();
// Target needs to NOT have ANY forging-enabled account flags set
Integer targetFlags = target.getFlags();
if (targetFlags != null && (targetFlags & Account.FORGING_MASK) != 0)
return ValidationResult.FORGING_ALREADY_ENABLED;
// Has creator reached minimum requirements?
int numberForgedBlocks = this.repository.getBlockRepository().countForgedBlocks(creator.getPublicKey());
int numberEnabledAccounts = this.repository.getAccountRepository().countForgingAccountsEnabledByAddress(creator.getAddress());
if ((creatorFlags & Account.TIER1_FORGING_MASK) != 0) {
// Tier1: minimum 2,500 forged blocks & max 50 accounts
if (numberForgedBlocks < TIER1_MIN_FORGED_BLOCKS)
return ValidationResult.FORGE_MORE_BLOCKS;
if (numberEnabledAccounts >= TIER1_MAX_ENABLED_ACCOUNTS)
return ValidationResult.FORGING_ENABLE_LIMIT;
} else if ((creatorFlags & Account.TIER2_FORGING_MASK) != 0) {
// Tier2: minimum 50 forged blocks & max 50 accounts
if (numberForgedBlocks < TIER2_MIN_FORGED_BLOCKS)
return ValidationResult.FORGE_MORE_BLOCKS;
if (numberEnabledAccounts >= TIER2_MAX_ENABLED_ACCOUNTS)
return ValidationResult.FORGING_ENABLE_LIMIT;
}
// Check fee is zero or positive
if (enableForgingTransactionData.getFee().compareTo(BigDecimal.ZERO) < 0)
return ValidationResult.NEGATIVE_FEE;
// Check reference
if (!Arrays.equals(creator.getLastReference(), enableForgingTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
// Check creator has enough funds
if (creator.getConfirmedBalance(Asset.QORA).compareTo(enableForgingTransactionData.getFee()) < 0)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
@Override
public void process() throws DataException {
Account creator = getCreator();
int creatorFlags = creator.getFlags();
int forgeBit = 0;
if ((creatorFlags & Account.TIER1_FORGING_MASK) != 0)
forgeBit = Account.TIER2_FORGING_MASK;
else
forgeBit = Account.TIER3_FORGING_MASK;
Account target = getTarget();
Integer targetFlags = target.getFlags();
if (targetFlags == null)
targetFlags = 0;
targetFlags |= forgeBit;
target.setFlags(targetFlags);
target.setForgingEnabler(creator.getAddress());
// Save this transaction
this.repository.getTransactionRepository().save(enableForgingTransactionData);
// Update creator's balance
creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(enableForgingTransactionData.getFee()));
// Update creator's reference
creator.setLastReference(enableForgingTransactionData.getSignature());
}
@Override
public void orphan() throws DataException {
// Revert
Account creator = getCreator();
int creatorFlags = creator.getFlags();
int forgeBit = 0;
if ((creatorFlags & Account.TIER1_FORGING_MASK) != 0)
forgeBit = Account.TIER2_FORGING_MASK;
else
forgeBit = Account.TIER3_FORGING_MASK;
Account target = getTarget();
int targetFlags = target.getFlags();
targetFlags &= ~forgeBit;
target.setFlags(targetFlags);
target.setForgingEnabler(null);
// Delete this transaction itself
this.repository.getTransactionRepository().delete(enableForgingTransactionData);
// Update creator's balance
creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(enableForgingTransactionData.getFee()));
// Update creator's reference
creator.setLastReference(enableForgingTransactionData.getReference());
}
}

View File

@ -0,0 +1,174 @@
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.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountData;
import org.qora.data.account.ProxyForgerData;
import org.qora.data.transaction.ProxyForgingTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.transform.Transformer;
public class ProxyForgingTransaction extends Transaction {
// Properties
private ProxyForgingTransactionData proxyForgingTransactionData;
// Constructors
public ProxyForgingTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.proxyForgingTransactionData = (ProxyForgingTransactionData) 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.getForger().getAddress()))
return true;
if (address.equals(this.getRecipient().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.getForger().getAddress()))
amount = amount.subtract(this.transactionData.getFee());
return amount;
}
// Navigation
public PublicKeyAccount getForger() {
return new PublicKeyAccount(this.repository, this.proxyForgingTransactionData.getForgerPublicKey());
}
public Account getRecipient() {
return new Account(this.repository, this.proxyForgingTransactionData.getRecipient());
}
// Processing
private static final BigDecimal MAX_SHARE = BigDecimal.valueOf(100).setScale(2);
@Override
public ValidationResult isValid() throws DataException {
// Check reward share given to recipient
if (this.proxyForgingTransactionData.getShare().compareTo(BigDecimal.ZERO) <= 0
|| this.proxyForgingTransactionData.getShare().compareTo(MAX_SHARE) >= 0)
return ValidationResult.INVALID_FORGE_SHARE;
PublicKeyAccount creator = getCreator();
// Creator needs to have at least one forging-enabled account flag set
Integer creatorFlags = creator.getFlags();
if (creatorFlags == null)
return ValidationResult.INVALID_ADDRESS;
if ((creatorFlags & Account.FORGING_MASK) == 0)
return ValidationResult.NO_FORGING_PERMISSION;
// Check proxy public key is correct length
if (this.proxyForgingTransactionData.getProxyPublicKey().length != Transformer.PUBLIC_KEY_LENGTH)
return ValidationResult.INVALID_PUBLIC_KEY;
Account recipient = getRecipient();
if (!Crypto.isValidAddress(recipient.getAddress()))
return ValidationResult.INVALID_ADDRESS;
// Check recipient has known public key
AccountData recipientData = this.repository.getAccountRepository().getAccount(recipient.getAddress());
if (recipientData == null || recipientData.getPublicKey() == null)
return ValidationResult.PUBLIC_KEY_UNKNOWN;
// Check fee is positive
if (proxyForgingTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_FEE;
// Check reference
if (!Arrays.equals(creator.getLastReference(), proxyForgingTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
// Check creator has enough funds
if (creator.getConfirmedBalance(Asset.QORA).compareTo(proxyForgingTransactionData.getFee()) < 0)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
@Override
public void process() throws DataException {
PublicKeyAccount forger = getForger();
// Grab any previous share info for orphaning purposes
ProxyForgerData proxyForgerData = this.repository.getAccountRepository().getProxyForgeData(forger.getPublicKey(),
proxyForgingTransactionData.getRecipient());
if (proxyForgerData != null)
proxyForgingTransactionData.setPreviousShare(proxyForgerData.getShare());
// Save this transaction, with previous share info
this.repository.getTransactionRepository().save(proxyForgingTransactionData);
// Save proxy forging info
proxyForgerData = new ProxyForgerData(forger.getPublicKey(), proxyForgingTransactionData.getRecipient(), proxyForgingTransactionData.getProxyPublicKey(), proxyForgingTransactionData.getShare());
this.repository.getAccountRepository().save(proxyForgerData);
// Update forger's balance
forger.setConfirmedBalance(Asset.QORA, forger.getConfirmedBalance(Asset.QORA).subtract(proxyForgingTransactionData.getFee()));
// Update forger's reference
forger.setLastReference(proxyForgingTransactionData.getSignature());
}
@Override
public void orphan() throws DataException {
// Revert
PublicKeyAccount forger = getForger();
if (proxyForgingTransactionData.getPreviousShare() != null) {
// Revert previous sharing arrangement
ProxyForgerData proxyForgerData = new ProxyForgerData(forger.getPublicKey(), proxyForgingTransactionData.getRecipient(),
proxyForgingTransactionData.getProxyPublicKey(), proxyForgingTransactionData.getPreviousShare());
this.repository.getAccountRepository().save(proxyForgerData);
} else {
// No previous arrangement so simply delete
this.repository.getAccountRepository().delete(forger.getPublicKey(), proxyForgingTransactionData.getRecipient());
}
// Delete this transaction itself
this.repository.getTransactionRepository().delete(proxyForgingTransactionData);
// Update forger's balance
forger.setConfirmedBalance(Asset.QORA, forger.getConfirmedBalance(Asset.QORA).add(proxyForgingTransactionData.getFee()));
// Update forger's reference
forger.setLastReference(proxyForgingTransactionData.getReference());
}
}

View File

@ -73,7 +73,9 @@ public abstract class Transaction {
GROUP_APPROVAL(33, false),
SET_GROUP(34, false),
UPDATE_ASSET(35, true),
ACCOUNT_FLAGS(36, false);
ACCOUNT_FLAGS(36, false),
ENABLE_FORGING(37, false),
PROXY_FORGING(38, false);
public final int value;
public final boolean needsApproval;
@ -192,6 +194,13 @@ public abstract class Transaction {
INVALID_ASSET_OWNER(70),
AT_IS_FINISHED(71),
NO_FLAG_PERMISSION(72),
NO_FORGING_PERMISSION(73),
FORGING_ALREADY_ENABLED(74),
FORGE_MORE_BLOCKS(75),
FORGING_ENABLE_LIMIT(76),
INVALID_FORGE_SHARE(77),
PUBLIC_KEY_UNKNOWN(78),
INVALID_PUBLIC_KEY(79),
NOT_YET_RELEASED(1000);
public final int value;

View File

@ -311,8 +311,8 @@ public class BlockTransformer extends Transformer {
bytes.write(block.getBlockData().getGeneratorSignature());
for (Transaction transaction : transactions) {
// For legacy blocks, we don't include AT-Transactions
if (block.getBlockData().getVersion() < 4 && transaction.getTransactionData().getType() == TransactionType.AT)
// We don't include AT-Transactions as AT-state/output is dealt with elsewhere in the block code
if (transaction.getTransactionData().getType() == TransactionType.AT)
continue;
if (!transaction.isSignatureValid())

View File

@ -0,0 +1,83 @@
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.EnableForgingTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transform.TransformationException;
import org.qora.utils.Serialization;
public class EnableForgingTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int TARGET_LENGTH = ADDRESS_LENGTH;
private static final int EXTRAS_LENGTH = TARGET_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("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);
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
return new EnableForgingTransactionData(timestamp, txGroupId, reference, creatorPublicKey, target, fee, signature);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
return getBaseLength(transactionData) + EXTRAS_LENGTH;
}
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
try {
EnableForgingTransactionData enableForgingTransactionData = (EnableForgingTransactionData) transactionData;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
transformCommonBytes(transactionData, bytes);
Serialization.serializeAddress(bytes, enableForgingTransactionData.getTarget());
Serialization.serializeBigDecimal(bytes, enableForgingTransactionData.getFee());
if (enableForgingTransactionData.getSignature() != null)
bytes.write(enableForgingTransactionData.getSignature());
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);
}
}
}

View File

@ -0,0 +1,93 @@
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.ProxyForgingTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transform.TransformationException;
import org.qora.utils.Serialization;
public class ProxyForgingTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int TARGET_LENGTH = ADDRESS_LENGTH;
private static final int EXTRAS_LENGTH = TARGET_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("forger's public key", TransformationType.PUBLIC_KEY);
layout.add("recipient account's address", TransformationType.ADDRESS);
layout.add("proxy's public key", TransformationType.PUBLIC_KEY);
layout.add("recipient's share of block rewards", TransformationType.AMOUNT);
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[] forgerPublicKey = Serialization.deserializePublicKey(byteBuffer);
String recipient = Serialization.deserializeAddress(byteBuffer);
byte[] proxyPublicKey = Serialization.deserializePublicKey(byteBuffer);
BigDecimal share = Serialization.deserializeBigDecimal(byteBuffer);
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
return new ProxyForgingTransactionData(timestamp, txGroupId, reference, forgerPublicKey, recipient, proxyPublicKey, share, fee, signature);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
return getBaseLength(transactionData) + EXTRAS_LENGTH;
}
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
try {
ProxyForgingTransactionData proxyForgingTransactionData = (ProxyForgingTransactionData) transactionData;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
transformCommonBytes(transactionData, bytes);
Serialization.serializeAddress(bytes, proxyForgingTransactionData.getRecipient());
bytes.write(proxyForgingTransactionData.getProxyPublicKey());
Serialization.serializeBigDecimal(bytes, proxyForgingTransactionData.getShare());
Serialization.serializeBigDecimal(bytes, proxyForgingTransactionData.getFee());
if (proxyForgingTransactionData.getSignature() != null)
bytes.write(proxyForgingTransactionData.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, 0));
accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.NO_GROUP, 0, null));
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, 0));
accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.NO_GROUP, 0, null));
// Mock balance
accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, initialSenderBalance));