From c9035edd2c8502ad51c60785eb78d183680db5b3 Mon Sep 17 00:00:00 2001 From: catbref Date: Tue, 19 Mar 2019 18:31:09 +0000 Subject: [PATCH] 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 --- .../qora/api/resource/AddressesResource.java | 58 +++++++++++++++ src/main/java/org/qora/block/Block.java | 73 ++++++++++++++++++- .../java/org/qora/block/BlockGenerator.java | 59 +++++++++++---- src/main/java/org/qora/blockgenerator.java | 2 +- .../java/org/qora/controller/Controller.java | 11 +-- .../qora/data/account/ProxyForgerData.java | 8 ++ .../qora/repository/AccountRepository.java | 2 + .../hsqldb/HSQLDBAccountRepository.java | 47 ++++++++++++ .../repository/hsqldb/HSQLDBRepository.java | 6 ++ src/main/java/org/qora/settings/Settings.java | 11 +++ 10 files changed, 248 insertions(+), 29 deletions(-) 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; + } + }