diff --git a/src/main/java/org/qora/api/resource/AdminResource.java b/src/main/java/org/qora/api/resource/AdminResource.java index 201840b4..c113ce71 100644 --- a/src/main/java/org/qora/api/resource/AdminResource.java +++ b/src/main/java/org/qora/api/resource/AdminResource.java @@ -3,8 +3,10 @@ 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.enums.ParameterIn; +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; @@ -12,14 +14,18 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.List; import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.DELETE; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import org.qora.account.PrivateKeyAccount; import org.qora.api.ApiError; import org.qora.api.ApiErrors; import org.qora.api.ApiExceptionFactory; @@ -29,6 +35,11 @@ import org.qora.controller.Controller; import org.qora.repository.DataException; import org.qora.repository.Repository; import org.qora.repository.RepositoryManager; +import org.qora.data.account.ForgingAccountData; +import org.qora.repository.DataException; +import org.qora.repository.Repository; +import org.qora.repository.RepositoryManager; +import org.qora.utils.Base58; @Path("/admin") @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @@ -137,7 +148,101 @@ public class AdminResource { } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } + } + @GET + @Path("/forgingaccounts") + @Operation( + summary = "List accounts used to forge by BlockGenerator", + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = ForgingAccountData.class))) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public List getForgingAccounts() { + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getAccountRepository().getForgingAccounts(); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/forgingaccounts") + @Operation( + summary = "Add account to use to forge by BlockGenerator", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public String addForgingAccount(String seed58) { + byte[] seed = Base58.decode(seed58.trim()); + + // Check seed is valid + try { + new PrivateKeyAccount(null, seed); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY, e); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + ForgingAccountData forgingAccountData = new ForgingAccountData(seed); + + repository.getAccountRepository().save(forgingAccountData); + repository.saveChanges(); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + + return "true"; + } + + @DELETE + @Path("/forgingaccounts") + @Operation( + summary = "Delete account to use to forge by BlockGenerator", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + public String deleteForgingAccount(String seed58) { + byte[] seed = Base58.decode(seed58.trim()); + + try (final Repository repository = RepositoryManager.getRepository()) { + if (repository.getAccountRepository().delete(seed) == 0) + return "false"; + + repository.saveChanges(); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + + return "true"; } } diff --git a/src/main/java/org/qora/api/resource/UtilsResource.java b/src/main/java/org/qora/api/resource/UtilsResource.java index 62252adf..c7d2d927 100644 --- a/src/main/java/org/qora/api/resource/UtilsResource.java +++ b/src/main/java/org/qora/api/resource/UtilsResource.java @@ -385,9 +385,14 @@ public class UtilsResource { if (privateKey.length != 32) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - byte[] publicKey = new PrivateKeyAccount(null, privateKey).getPublicKey(); + try { + byte[] publicKey = new PrivateKeyAccount(null, privateKey).getPublicKey(); + + return Base58.encode(publicKey); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY, e); + } - return Base58.encode(publicKey); } @GET diff --git a/src/main/java/org/qora/block/BlockGenerator.java b/src/main/java/org/qora/block/BlockGenerator.java index 9a28e232..0b93774a 100644 --- a/src/main/java/org/qora/block/BlockGenerator.java +++ b/src/main/java/org/qora/block/BlockGenerator.java @@ -12,6 +12,7 @@ import org.apache.logging.log4j.Logger; import org.qora.account.PrivateKeyAccount; import org.qora.block.Block.ValidationResult; import org.qora.controller.Controller; +import org.qora.data.account.ForgingAccountData; import org.qora.data.block.BlockData; import org.qora.data.transaction.TransactionData; import org.qora.repository.BlockRepository; @@ -45,11 +46,6 @@ 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 @@ -63,32 +59,37 @@ public class BlockGenerator extends Thread { repository.saveChanges(); } - 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; - List newBlocks = null; + List newBlocks = new ArrayList<>(); 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); - newBlocks = null; + newBlocks.clear(); } - // 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); + // Do we need to build any potential new blocks? + List forgingAccountsData = repository.getAccountRepository().getForgingAccounts(); + List forgingAccounts = forgingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getSeed())).collect(Collectors.toList()); - // The blocks for other generators require less effort... - for (int i = 1; i < generators.size(); ++i) - newBlocks.add(newBlock.regenerate(generators.get(i))); + // Discard accounts we have blocks for + forgingAccounts.removeIf(account -> newBlocks.stream().anyMatch(newBlock -> newBlock.getGenerator().getAddress().equals(account.getAddress()))); + + for (PrivateKeyAccount generator : forgingAccounts) { + // First block does the AT heavy-lifting + if (newBlocks.isEmpty()) { + Block newBlock = new Block(repository, previousBlock.getBlockData(), generator); + newBlocks.add(newBlock); + } else { + // The blocks for other generators require less effort... + Block newBlock = newBlocks.get(0); + newBlocks.add(newBlock.regenerate(generator)); + } } // Make sure we're the only thread modifying the blockchain @@ -131,7 +132,7 @@ public class BlockGenerator extends Thread { if (validationResult != ValidationResult.OK) { // No longer valid? Report and discard LOGGER.error("Valid, generated block now invalid '" + validationResult.name() + "' after adding unconfirmed transactions?"); - newBlock = null; + newBlocks.clear(); break generation; } @@ -146,7 +147,7 @@ public class BlockGenerator extends Thread { } catch (DataException e) { // Unable to process block - report and discard LOGGER.error("Unable to process newly generated block?", e); - newBlock = null; + newBlocks.clear(); } } finally { blockchainLock.unlock(); diff --git a/src/main/java/org/qora/data/account/ForgingAccountData.java b/src/main/java/org/qora/data/account/ForgingAccountData.java new file mode 100644 index 00000000..881cf932 --- /dev/null +++ b/src/main/java/org/qora/data/account/ForgingAccountData.java @@ -0,0 +1,29 @@ +package org.qora.data.account; + +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 ForgingAccountData { + + // Properties + protected byte[] seed; + + // Constructors + + // For JAXB + protected ForgingAccountData() { + } + + public ForgingAccountData(byte[] seed) { + this.seed = seed; + } + + // Getters/Setters + + public byte[] getSeed() { + return this.seed; + } + +} diff --git a/src/main/java/org/qora/repository/AccountRepository.java b/src/main/java/org/qora/repository/AccountRepository.java index 0d54aa22..b29d36dc 100644 --- a/src/main/java/org/qora/repository/AccountRepository.java +++ b/src/main/java/org/qora/repository/AccountRepository.java @@ -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.ForgingAccountData; import org.qora.data.account.ProxyForgerData; public interface AccountRepository { @@ -91,4 +92,12 @@ public interface AccountRepository { public void delete(byte[] forgerPublickey, String recipient) throws DataException; + // Forging accounts used by BlockGenerator + + public List getForgingAccounts() throws DataException; + + public void save(ForgingAccountData forgingAccountData) throws DataException; + + public int delete(byte[] forgingAccountSeed) 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 81e8aaa9..625b4434 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java @@ -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.ForgingAccountData; import org.qora.data.account.ProxyForgerData; import org.qora.repository.AccountRepository; import org.qora.repository.DataException; @@ -401,4 +402,45 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + // Forging accounts used by BlockGenerator + + public List getForgingAccounts() throws DataException { + List forgingAccounts = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute("SELECT forger_seed FROM ForgingAccounts")) { + if (resultSet == null) + return forgingAccounts; + + do { + byte[] forgerSeed = resultSet.getBytes(1); + + forgingAccounts.add(new ForgingAccountData(forgerSeed)); + } while (resultSet.next()); + + return forgingAccounts; + } catch (SQLException e) { + throw new DataException("Unable to find forging accounts in repository", e); + } + } + + public void save(ForgingAccountData forgingAccountData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("ForgingAccounts"); + + saveHelper.bind("forger_seed", forgingAccountData.getSeed()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save forging account into repository", e); + } + } + + public int delete(byte[] forgingAccountSeed) throws DataException { + try { + return this.repository.delete("ForgingAccounts", "forger_seed = ?", forgingAccountSeed); + } catch (SQLException e) { + throw new DataException("Unable to delete forging account from repository", e); + } + } + } diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index aca6f3e4..1cca80a0 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -719,6 +719,12 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE INDEX ProxyForgersProxyPublicKeyIndex ON ProxyForgers (proxy_public_key)"); break; + case 40: + // Stash of private keys used for generating blocks. These should be proxy keys! + stmt.execute("CREATE TYPE QoraKeySeed AS VARBINARY(32)"); + stmt.execute("CREATE TABLE ForgingAccounts (forger_seed QoraKeySeed NOT NULL, PRIMARY KEY (forger_seed))"); + break; + default: // nothing to do return false; diff --git a/src/main/java/org/qora/settings/Settings.java b/src/main/java/org/qora/settings/Settings.java index 39c64351..b8012fdc 100644 --- a/src/main/java/org/qora/settings/Settings.java +++ b/src/main/java/org/qora/settings/Settings.java @@ -5,7 +5,6 @@ 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; @@ -13,7 +12,6 @@ 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; @@ -21,7 +19,6 @@ 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 @@ -65,10 +62,6 @@ 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() { @@ -235,8 +228,4 @@ public class Settings { return this.useBitcoinTestNet; } - public List getGeneratorKeys() { - return this.generatorKeys; - } - }