Interim proxy minting commit

Added /addresses/proxying to find proxy forging mappings.

Added /addresses/proxykey/{genprivkey}/{recipientpubkey} to calculate proxy private key.

New Block.regenerate factory method to create new Blocks
 but without having to reprocess ATs, etc.

Added support for proxied generator in Block.calcGeneratorsTarget

BlockGenerator now generates and checks new blocks for various generators,
 including proxy generators.

BlockGenerator now uses generator private keys supplied by Settings.
Corresponding changes to Settings to load base58-encoded private keys.

+ minor stuff
This commit is contained in:
catbref 2019-03-19 18:31:09 +00:00
parent 9b859f3efd
commit c9035edd2c
10 changed files with 248 additions and 29 deletions

View File

@ -1,6 +1,8 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
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;
@ -8,6 +10,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
@ -15,6 +18,7 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@ -25,7 +29,9 @@ import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.crypto.Ed25519;
import org.qora.data.account.AccountData;
import org.qora.data.account.ProxyForgerData;
import org.qora.data.transaction.ProxyForgingTransactionData;
import org.qora.group.Group;
import org.qora.repository.DataException;
@ -268,6 +274,58 @@ public class AddressesResource {
}
}
@GET
@Path("/proxying")
@Operation(
summary = "List accounts involved in proxy forging, with reward percentage",
description = "Returns list of accounts. At least one of \"proxiedFor\" or \"proxiedBy\" needs to be supplied.",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = ProxyForgerData.class)))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<ProxyForgerData> getProxying(@QueryParam("proxiedFor") List<String> recipients,
@QueryParam("proxiedBy") List<String> forgers,
@Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
) @QueryParam("offset") Integer offset, @Parameter(
ref = "reverse"
) @QueryParam("reverse") Boolean reverse) {
if (recipients.isEmpty() && forgers.isEmpty())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getAccountRepository().findProxyAccounts(recipients, forgers, limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/proxykey/{generatorprivatekey}/{recipientpublickey}")
@Operation(
summary = "Calculate proxy private key",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
public String calculateProxyKey(@PathParam("generatorprivatekey") String generatorKey58, @PathParam("recipientpublickey") String recipientKey58) {
byte[] generatorKey = Base58.decode(generatorKey58);
byte[] recipientKey = Base58.decode(recipientKey58);
byte[] sharedSecret = Ed25519.getSharedSecret(recipientKey, generatorKey);
byte[] proxySeed = Crypto.digest(sharedSecret);
return Base58.encode(proxySeed);
}
@POST
@Path("/proxyforging")
@Operation(

View File

@ -240,6 +240,61 @@ public class Block {
generator.getPublicKey(), generatorSignature, atCount, atFees);
}
/**
* Construct another block using this block as template, but with different generator account.
* <p>
* NOTE: uses the same transactions list, AT states, etc.
*
* @param generator
* @return
* @throws DataException
*/
public Block regenerate(PrivateKeyAccount generator) throws DataException {
Block newBlock = new Block(this.repository, this.blockData);
BlockData parentBlockData = this.getParent();
Block parentBlock = new Block(repository, parentBlockData);
newBlock.generator = generator;
// Copy AT state data
newBlock.ourAtStates = this.ourAtStates;
newBlock.atStates = newBlock.ourAtStates;
newBlock.ourAtFees = this.ourAtFees;
// Calculate new block timestamp
int version = this.blockData.getVersion();
byte[] reference = this.blockData.getReference();
BigDecimal generatingBalance = this.blockData.getGeneratingBalance();
byte[] generatorSignature;
try {
generatorSignature = generator
.sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator));
} catch (TransformationException e) {
throw new DataException("Unable to calculate next block generator signature", e);
}
long timestamp = parentBlock.calcNextBlockTimestamp(version, generatorSignature, generator);
newBlock.transactions = this.transactions;
int transactionCount = this.blockData.getTransactionCount();
BigDecimal totalFees = this.blockData.getTotalFees();
byte[] transactionsSignature = null; // We'll calculate this later
Integer height = this.blockData.getHeight();
int atCount = newBlock.ourAtStates.size();
BigDecimal atFees = newBlock.ourAtFees;
newBlock.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generator.getPublicKey(), generatorSignature, atCount, atFees);
// Resign to update transactions signature
newBlock.sign();
return newBlock;
}
// Getters/setters
public BlockData getBlockData() {
@ -359,7 +414,7 @@ public class Block {
return actualBlockTime;
}
private BigInteger calcGeneratorsTarget(Account nextBlockGenerator) throws DataException {
private BigInteger calcGeneratorsTarget(PublicKeyAccount nextBlockGenerator) throws DataException {
// Start with 32-byte maximum integer representing all possible correct "guesses"
// Where a "correct guess" is an integer greater than the threshold represented by calcBlockHash()
byte[] targetBytes = new byte[32];
@ -371,9 +426,17 @@ public class Block {
BigInteger baseTarget = BigInteger.valueOf(calcBaseTarget(calcNextBlockGeneratingBalance()));
target = target.divide(baseTarget);
// If generator is actually proxy account then use forger's account to calculate target.
BigDecimal generatingBalance;
ProxyForgerData proxyForgerData = this.repository.getAccountRepository().getProxyForgeData(nextBlockGenerator.getPublicKey());
if (proxyForgerData != null)
generatingBalance = new PublicKeyAccount(this.repository, proxyForgerData.getForgerPublicKey()).getGeneratingBalance();
else
generatingBalance = nextBlockGenerator.getGeneratingBalance();
// Multiply by account's generating balance
// So the greater the account's generating balance then the greater the remaining "correct guesses"
target = target.multiply(nextBlockGenerator.getGeneratingBalance().toBigInteger());
target = target.multiply(generatingBalance.toBigInteger());
return target;
}
@ -410,8 +473,8 @@ public class Block {
return new BigInteger(1, hash);
}
/** Calculate next block's timestamp, given next block's version, generator signature and generator's private key */
private long calcNextBlockTimestamp(int nextBlockVersion, byte[] nextBlockGeneratorSignature, PrivateKeyAccount nextBlockGenerator) throws DataException {
/** Calculate next block's timestamp, given next block's version, generator signature and generator's public key */
private long calcNextBlockTimestamp(int nextBlockVersion, byte[] nextBlockGeneratorSignature, PublicKeyAccount nextBlockGenerator) throws DataException {
BigInteger hashValue = calcNextBlockHash(nextBlockVersion, nextBlockGeneratorSignature, nextBlockGenerator);
BigInteger target = calcGeneratorsTarget(nextBlockGenerator);
@ -946,6 +1009,8 @@ public class Block {
BlockData parentBlockData = parentBlock.getBlockData();
BigInteger hashValue = this.calcBlockHash();
// calcGeneratorsTarget handles proxy forging aspect
BigInteger target = parentBlock.calcGeneratorsTarget(this.generator);
// Multiply target by guesses

View File

@ -1,8 +1,11 @@
package org.qora.block;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -26,8 +29,6 @@ import org.qora.utils.Base58;
public class BlockGenerator extends Thread {
// Properties
private byte[] generatorPrivateKey;
private PrivateKeyAccount generator;
private boolean running;
// Other properties
@ -35,8 +36,7 @@ public class BlockGenerator extends Thread {
// Constructors
public BlockGenerator(byte[] generatorPrivateKey) {
this.generatorPrivateKey = generatorPrivateKey;
public BlockGenerator() {
this.running = true;
}
@ -45,6 +45,11 @@ public class BlockGenerator extends Thread {
public void run() {
Thread.currentThread().setName("BlockGenerator");
List<byte[]> generatorKeys = Settings.getInstance().getGeneratorKeys();
// No generators?
if (generatorKeys.isEmpty())
return;
try (final Repository repository = RepositoryManager.getRepository()) {
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
// Wipe existing unconfirmed transactions
@ -58,37 +63,59 @@ public class BlockGenerator extends Thread {
repository.saveChanges();
}
generator = new PrivateKeyAccount(repository, generatorPrivateKey);
List<PrivateKeyAccount> generators = generatorKeys.stream().map(key -> new PrivateKeyAccount(repository, key)).collect(Collectors.toList());
// Going to need this a lot...
BlockRepository blockRepository = repository.getBlockRepository();
Block previousBlock = null;
Block newBlock = null;
List<Block> newBlocks = null;
while (running) {
// Check blockchain hasn't changed
BlockData lastBlockData = blockRepository.getLastBlock();
if (previousBlock == null || !Arrays.equals(previousBlock.getSignature(), lastBlockData.getSignature())) {
previousBlock = new Block(repository, lastBlockData);
newBlock = null;
newBlocks = null;
}
// Do we need to build a potential new block?
if (newBlock == null)
newBlock = new Block(repository, previousBlock.getBlockData(), generator);
// Do we need to build a potential new blocks?
if (newBlocks == null) {
// First block does the AT heavy-lifting
newBlocks = new ArrayList<>(generators.size());
Block newBlock = new Block(repository, previousBlock.getBlockData(), generators.get(0));
newBlocks.add(newBlock);
// The blocks for other generators require less effort...
for (int i = 1; i < generators.size(); ++i)
newBlocks.add(newBlock.regenerate(generators.get(i)));
}
// Make sure we're the only thread modifying the blockchain
Lock blockchainLock = Controller.getInstance().getBlockchainLock();
if (blockchainLock.tryLock())
generation: try {
// Is new block's timestamp valid yet?
// We do a separate check as some timestamp checks are skipped for testnet
if (newBlock.isTimestampValid() != ValidationResult.OK)
List<Block> goodBlocks = new ArrayList<>();
for (Block testBlock : newBlocks) {
// Is new block's timestamp valid yet?
// We do a separate check as some timestamp checks are skipped for testnet
if (testBlock.isTimestampValid() != ValidationResult.OK)
continue;
// Is new block valid yet? (Before adding unconfirmed transactions)
if (testBlock.isValid() != ValidationResult.OK)
continue;
goodBlocks.add(testBlock);
}
if (goodBlocks.isEmpty())
break generation;
// Is new block valid yet? (Before adding unconfirmed transactions)
if (newBlock.isValid() != ValidationResult.OK)
break generation;
// Pick random generator
int winningIndex = new Random().nextInt(goodBlocks.size());
Block newBlock = goodBlocks.get(winningIndex);
// Delete invalid transactions
deleteInvalidTransactions(repository);

View File

@ -47,7 +47,7 @@ public class blockgenerator {
System.exit(2);
}
BlockGenerator blockGenerator = new BlockGenerator(privateKey);
BlockGenerator blockGenerator = new BlockGenerator();
blockGenerator.start();
Runtime.getRuntime().addShutdownHook(new Thread() {

View File

@ -42,7 +42,6 @@ import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.utils.Base58;
import org.qora.utils.NTP;
public class Controller extends Thread {
@ -158,13 +157,9 @@ public class Controller extends Thread {
System.exit(2);
}
// XXX extract private key needed for block gen
if (args.length == 0 || !args[0].equals("NO-BLOCK-GEN")) {
LOGGER.info("Starting block generator");
byte[] privateKey = Base58.decode(args.length > 0 ? args[0] : "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
blockGenerator = new BlockGenerator(privateKey);
blockGenerator.start();
}
LOGGER.info("Starting block generator");
blockGenerator = new BlockGenerator();
blockGenerator.start();
LOGGER.info("Starting API on port " + Settings.getInstance().getApiPort());
try {

View File

@ -4,6 +4,9 @@ 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.crypto.Crypto;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@ -47,4 +50,9 @@ public class ProxyForgerData {
return this.share;
}
@XmlElement(name = "forger")
public String getForger() {
return Crypto.toAddress(this.forgerPublicKey);
}
}

View File

@ -85,6 +85,8 @@ public interface AccountRepository {
public ProxyForgerData getProxyForgeData(byte[] proxyPublicKey) throws DataException;
public List<ProxyForgerData> findProxyAccounts(List<String> recipients, List<String> forgers, Integer limit, Integer offset, Boolean reverse) throws DataException;
public void save(ProxyForgerData proxyForgerData) throws DataException;
public void delete(byte[] forgerPublickey, String recipient) throws DataException;

View File

@ -15,6 +15,8 @@ import org.qora.data.account.ProxyForgerData;
import org.qora.repository.AccountRepository;
import org.qora.repository.DataException;
import static org.qora.repository.hsqldb.HSQLDBRepository.nPlaceholders;
public class HSQLDBAccountRepository implements AccountRepository {
protected HSQLDBRepository repository;
@ -331,6 +333,51 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public List<ProxyForgerData> findProxyAccounts(List<String> recipients, List<String> forgers, Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT forger, recipient, share, proxy_public_key FROM ProxyForgers ";
List<Object> args = new ArrayList<>();
if (!forgers.isEmpty()) {
sql += "JOIN Accounts ON Accounts.public_key = ProxyForgers.forger "
+ "WHERE Accounts.account IN (" + nPlaceholders(forgers.size()) + ") ";
args.addAll(forgers);
}
if (!recipients.isEmpty()) {
sql += forgers.isEmpty() ? "WHERE " : "AND ";
sql += "recipient IN (" + nPlaceholders(recipients.size()) + ") ";
args.addAll(recipients);
}
sql += "ORDER BY recipient, share";
if (reverse != null && reverse)
sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
List<ProxyForgerData> proxyAccounts = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql, args.toArray())) {
if (resultSet == null)
return proxyAccounts;
do {
byte[] forgerPublicKey = resultSet.getBytes(1);
String recipient = resultSet.getString(2);
BigDecimal share = resultSet.getBigDecimal(3);
byte[] proxyPublicKey = resultSet.getBytes(4);
proxyAccounts.add(new ProxyForgerData(forgerPublicKey, recipient, proxyPublicKey, share));
} while (resultSet.next());
return proxyAccounts;
} catch (SQLException e) {
throw new DataException("Unable to find proxy forge accounts in repository", e);
}
}
@Override
public void save(ProxyForgerData proxyForgerData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("ProxyForgers");

View File

@ -11,6 +11,7 @@ import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.TimeZone;
@ -407,4 +408,9 @@ public class HSQLDBRepository implements Repository {
return offsetDateTime.toInstant().toEpochMilli();
}
/** Convenience method to return n comma-separated, placeholders as a string. */
public static String nPlaceholders(int n) {
return String.join(", ", Collections.nCopies(n, "?"));
}
}

View File

@ -5,6 +5,7 @@ import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
@ -12,6 +13,7 @@ import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.transform.stream.StreamSource;
import org.apache.logging.log4j.LogManager;
@ -19,6 +21,7 @@ import org.apache.logging.log4j.Logger;
import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qora.api.Base58TypeAdapter;
import org.qora.block.BlockChain;
// All properties to be converted to JSON via JAXB
@ -62,6 +65,10 @@ public class Settings {
private String blockchainConfig = "blockchain.json";
private boolean useBitcoinTestNet = false;
// Private keys to use for generating blocks
@XmlJavaTypeAdapter(type = byte[].class, value = Base58TypeAdapter.class)
private List<byte[]> generatorKeys;
// Constructors
private Settings() {
@ -228,4 +235,8 @@ public class Settings {
return this.useBitcoinTestNet;
}
public List<byte[]> getGeneratorKeys() {
return this.generatorKeys;
}
}