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; package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation; 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.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody; 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 io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET; import javax.ws.rs.GET;
@ -15,6 +18,7 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
@ -25,7 +29,9 @@ import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory; import org.qora.api.ApiExceptionFactory;
import org.qora.asset.Asset; import org.qora.asset.Asset;
import org.qora.crypto.Crypto; import org.qora.crypto.Crypto;
import org.qora.crypto.Ed25519;
import org.qora.data.account.AccountData; import org.qora.data.account.AccountData;
import org.qora.data.account.ProxyForgerData;
import org.qora.data.transaction.ProxyForgingTransactionData; import org.qora.data.transaction.ProxyForgingTransactionData;
import org.qora.group.Group; import org.qora.group.Group;
import org.qora.repository.DataException; 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 @POST
@Path("/proxyforging") @Path("/proxyforging")
@Operation( @Operation(

View File

@ -240,6 +240,61 @@ public class Block {
generator.getPublicKey(), generatorSignature, atCount, atFees); 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 // Getters/setters
public BlockData getBlockData() { public BlockData getBlockData() {
@ -359,7 +414,7 @@ public class Block {
return actualBlockTime; 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" // 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() // Where a "correct guess" is an integer greater than the threshold represented by calcBlockHash()
byte[] targetBytes = new byte[32]; byte[] targetBytes = new byte[32];
@ -371,9 +426,17 @@ public class Block {
BigInteger baseTarget = BigInteger.valueOf(calcBaseTarget(calcNextBlockGeneratingBalance())); BigInteger baseTarget = BigInteger.valueOf(calcBaseTarget(calcNextBlockGeneratingBalance()));
target = target.divide(baseTarget); 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 // Multiply by account's generating balance
// So the greater the account's generating balance then the greater the remaining "correct guesses" // 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; return target;
} }
@ -410,8 +473,8 @@ public class Block {
return new BigInteger(1, hash); return new BigInteger(1, hash);
} }
/** Calculate next block's timestamp, given next block's version, generator signature and generator's private key */ /** Calculate next block's timestamp, given next block's version, generator signature and generator's public key */
private long calcNextBlockTimestamp(int nextBlockVersion, byte[] nextBlockGeneratorSignature, PrivateKeyAccount nextBlockGenerator) throws DataException { private long calcNextBlockTimestamp(int nextBlockVersion, byte[] nextBlockGeneratorSignature, PublicKeyAccount nextBlockGenerator) throws DataException {
BigInteger hashValue = calcNextBlockHash(nextBlockVersion, nextBlockGeneratorSignature, nextBlockGenerator); BigInteger hashValue = calcNextBlockHash(nextBlockVersion, nextBlockGeneratorSignature, nextBlockGenerator);
BigInteger target = calcGeneratorsTarget(nextBlockGenerator); BigInteger target = calcGeneratorsTarget(nextBlockGenerator);
@ -946,6 +1009,8 @@ public class Block {
BlockData parentBlockData = parentBlock.getBlockData(); BlockData parentBlockData = parentBlock.getBlockData();
BigInteger hashValue = this.calcBlockHash(); BigInteger hashValue = this.calcBlockHash();
// calcGeneratorsTarget handles proxy forging aspect
BigInteger target = parentBlock.calcGeneratorsTarget(this.generator); BigInteger target = parentBlock.calcGeneratorsTarget(this.generator);
// Multiply target by guesses // Multiply target by guesses

View File

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

View File

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

View File

@ -4,6 +4,9 @@ import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; 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 // All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@ -47,4 +50,9 @@ public class ProxyForgerData {
return this.share; 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 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 save(ProxyForgerData proxyForgerData) throws DataException;
public void delete(byte[] forgerPublickey, String recipient) 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.AccountRepository;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import static org.qora.repository.hsqldb.HSQLDBRepository.nPlaceholders;
public class HSQLDBAccountRepository implements AccountRepository { public class HSQLDBAccountRepository implements AccountRepository {
protected HSQLDBRepository repository; 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 @Override
public void save(ProxyForgerData proxyForgerData) throws DataException { public void save(ProxyForgerData proxyForgerData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("ProxyForgers"); HSQLDBSaver saveHelper = new HSQLDBSaver("ProxyForgers");

View File

@ -11,6 +11,7 @@ import java.time.Instant;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque; import java.util.Deque;
import java.util.TimeZone; import java.util.TimeZone;
@ -407,4 +408,9 @@ public class HSQLDBRepository implements Repository {
return offsetDateTime.toInstant().toEpochMilli(); 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.FileReader;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.util.List;
import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException; import javax.xml.bind.JAXBException;
@ -12,6 +13,7 @@ import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller; import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.transform.stream.StreamSource; import javax.xml.transform.stream.StreamSource;
import org.apache.logging.log4j.LogManager; 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.exceptions.XMLMarshalException;
import org.eclipse.persistence.jaxb.JAXBContextFactory; import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties; import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qora.api.Base58TypeAdapter;
import org.qora.block.BlockChain; import org.qora.block.BlockChain;
// All properties to be converted to JSON via JAXB // All properties to be converted to JSON via JAXB
@ -62,6 +65,10 @@ public class Settings {
private String blockchainConfig = "blockchain.json"; private String blockchainConfig = "blockchain.json";
private boolean useBitcoinTestNet = false; private boolean useBitcoinTestNet = false;
// Private keys to use for generating blocks
@XmlJavaTypeAdapter(type = byte[].class, value = Base58TypeAdapter.class)
private List<byte[]> generatorKeys;
// Constructors // Constructors
private Settings() { private Settings() {
@ -228,4 +235,8 @@ public class Settings {
return this.useBitcoinTestNet; return this.useBitcoinTestNet;
} }
public List<byte[]> getGeneratorKeys() {
return this.generatorKeys;
}
} }