diff --git a/src/main/java/org/qora/api/resource/AddressesResource.java b/src/main/java/org/qora/api/resource/AddressesResource.java index ea91728d..a8f1fe20 100644 --- a/src/main/java/org/qora/api/resource/AddressesResource.java +++ b/src/main/java/org/qora/api/resource/AddressesResource.java @@ -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 getProxying(@QueryParam("proxiedFor") List recipients, + @QueryParam("proxiedBy") List 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( diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index 0b33f5c9..2750ef82 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -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. + *

+ * 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 diff --git a/src/main/java/org/qora/block/BlockGenerator.java b/src/main/java/org/qora/block/BlockGenerator.java index 57248b29..9a28e232 100644 --- a/src/main/java/org/qora/block/BlockGenerator.java +++ b/src/main/java/org/qora/block/BlockGenerator.java @@ -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 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 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 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 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); diff --git a/src/main/java/org/qora/blockgenerator.java b/src/main/java/org/qora/blockgenerator.java index 0e8d8edb..d4a4ea74 100644 --- a/src/main/java/org/qora/blockgenerator.java +++ b/src/main/java/org/qora/blockgenerator.java @@ -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() { diff --git a/src/main/java/org/qora/controller/Controller.java b/src/main/java/org/qora/controller/Controller.java index 06f6a208..3f014e83 100644 --- a/src/main/java/org/qora/controller/Controller.java +++ b/src/main/java/org/qora/controller/Controller.java @@ -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 { diff --git a/src/main/java/org/qora/data/account/ProxyForgerData.java b/src/main/java/org/qora/data/account/ProxyForgerData.java index 714565e4..455ea2fd 100644 --- a/src/main/java/org/qora/data/account/ProxyForgerData.java +++ b/src/main/java/org/qora/data/account/ProxyForgerData.java @@ -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); + } + } diff --git a/src/main/java/org/qora/repository/AccountRepository.java b/src/main/java/org/qora/repository/AccountRepository.java index f7b6f3dc..0d54aa22 100644 --- a/src/main/java/org/qora/repository/AccountRepository.java +++ b/src/main/java/org/qora/repository/AccountRepository.java @@ -85,6 +85,8 @@ public interface AccountRepository { public ProxyForgerData getProxyForgeData(byte[] proxyPublicKey) throws DataException; + public List findProxyAccounts(List recipients, List 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; diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java index e3335e73..81e8aaa9 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java @@ -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 findProxyAccounts(List recipients, List forgers, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT forger, recipient, share, proxy_public_key FROM ProxyForgers "; + List 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 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"); diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java index c9fa6f6d..297a83ae 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java @@ -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, "?")); + } + } \ No newline at end of file diff --git a/src/main/java/org/qora/settings/Settings.java b/src/main/java/org/qora/settings/Settings.java index b8012fdc..39c64351 100644 --- a/src/main/java/org/qora/settings/Settings.java +++ b/src/main/java/org/qora/settings/Settings.java @@ -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 generatorKeys; + // Constructors private Settings() { @@ -228,4 +235,8 @@ public class Settings { return this.useBitcoinTestNet; } + public List getGeneratorKeys() { + return this.generatorKeys; + } + }