forked from Qortal/qortal
Replace settings-based generator private keys with DB table and API calls
This commit is contained in:
parent
c9035edd2c
commit
2f6ef32f35
@ -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<ForgingAccountData> 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";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -385,9 +385,14 @@ public class UtilsResource {
|
||||
if (privateKey.length != 32)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -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<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
|
||||
@ -63,32 +59,37 @@ public class BlockGenerator extends Thread {
|
||||
repository.saveChanges();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
List<Block> newBlocks = null;
|
||||
List<Block> 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<ForgingAccountData> forgingAccountsData = repository.getAccountRepository().getForgingAccounts();
|
||||
List<PrivateKeyAccount> forgingAccounts = forgingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getSeed())).collect(Collectors.toList());
|
||||
|
||||
// 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...
|
||||
for (int i = 1; i < generators.size(); ++i)
|
||||
newBlocks.add(newBlock.regenerate(generators.get(i)));
|
||||
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();
|
||||
|
29
src/main/java/org/qora/data/account/ForgingAccountData.java
Normal file
29
src/main/java/org/qora/data/account/ForgingAccountData.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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<ForgingAccountData> getForgingAccounts() throws DataException;
|
||||
|
||||
public void save(ForgingAccountData forgingAccountData) throws DataException;
|
||||
|
||||
public int delete(byte[] forgingAccountSeed) throws DataException;
|
||||
|
||||
}
|
||||
|
@ -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<ForgingAccountData> getForgingAccounts() throws DataException {
|
||||
List<ForgingAccountData> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<byte[]> generatorKeys;
|
||||
|
||||
// Constructors
|
||||
|
||||
private Settings() {
|
||||
@ -235,8 +228,4 @@ public class Settings {
|
||||
return this.useBitcoinTestNet;
|
||||
}
|
||||
|
||||
public List<byte[]> getGeneratorKeys() {
|
||||
return this.generatorKeys;
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user