forked from Qortal/qortal
Added initial support to download Pirate wallet libraries from QDN, using hardcoded transaction signature.
Using a hardcoded signature ensures that the libraries cannot be swapped out without a core auto update, which requires the standard dev team approval process.
This commit is contained in:
parent
4a58f90223
commit
1b9128289f
@ -44,6 +44,10 @@ package com.rust.litewalletjni;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.PirateChainWalletController;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class LiteWalletJni {
|
||||
|
||||
@ -74,26 +78,14 @@ public class LiteWalletJni {
|
||||
LOGGER.info("OS Architecture: {}", osArchitecture);
|
||||
|
||||
try {
|
||||
String libPath;
|
||||
|
||||
if (osName.equals("Mac OS X") && osArchitecture.equals("x86_64")) {
|
||||
libPath = "librust.dylib";
|
||||
}
|
||||
else if (osName.equals("Linux") && osArchitecture.equals("aarch64")) {
|
||||
libPath = "/home/pi/librust.so";
|
||||
}
|
||||
else if (osName.equals("Linux") && osArchitecture.equals("amd64")) {
|
||||
libPath = "/etc/qortal/librust.so";
|
||||
}
|
||||
else if (osName.contains("Windows") && osArchitecture.equals("amd64")) {
|
||||
libPath = "C:\\Users\\User\\Repositories\\pirate-litewalletjni\\src\\target\\release\\rust.dll";
|
||||
}
|
||||
else {
|
||||
String libFileName = PirateChainWalletController.getRustLibFilename();
|
||||
if (libFileName == null) {
|
||||
LOGGER.info("Library not found for OS: {}, arch: {}", osName, osArchitecture);
|
||||
return;
|
||||
}
|
||||
|
||||
System.load(libPath);
|
||||
Path libPath = Paths.get(PirateChainWalletController.getRustLibOuterDirectory().toString(), libFileName);
|
||||
System.load(libPath.toAbsolutePath().toString());
|
||||
loaded = true;
|
||||
}
|
||||
catch (UnsatisfiedLinkError e) {
|
||||
|
@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
@ -56,6 +55,7 @@ import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.ZipUtils;
|
||||
@ -254,7 +254,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, null);
|
||||
return this.getStatus(service, name, null, build);
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, null, build);
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -276,7 +276,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier);
|
||||
return this.getStatus(service, name, identifier, build);
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, identifier, build);
|
||||
}
|
||||
|
||||
|
||||
@ -1247,24 +1247,6 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
|
||||
private ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) {
|
||||
|
||||
// If "build=true" has been specified in the query string, build the resource before returning its status
|
||||
if (build != null && build == true) {
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
|
||||
try {
|
||||
if (!reader.isBuilding()) {
|
||||
reader.loadSynchronously(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// No need to handle exception, as it will be reflected in the status
|
||||
}
|
||||
}
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
return resource.getStatus(false);
|
||||
}
|
||||
|
||||
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Determine and add the status of each resource
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
|
@ -1,15 +1,36 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import com.rust.litewalletjni.LiteWalletJni;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.PirateWallet;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.ArbitraryTransaction;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.FilesystemUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class PirateChainWalletController extends Thread {
|
||||
@ -23,6 +44,10 @@ public class PirateChainWalletController extends Thread {
|
||||
|
||||
private boolean running;
|
||||
private PirateWallet currentWallet = null;
|
||||
private boolean shouldLoadWallet = false;
|
||||
private String loadStatus = null;
|
||||
|
||||
private static String qdnWalletSignature = "EsfUw54perxkEtfoUoL7Z97XPrNsZRZXePVZPz3cwRm9qyEPSofD5KmgVpDqVitQp7LhnZRmL6z2V9hEe1YS45T";
|
||||
|
||||
|
||||
private PirateChainWalletController() {
|
||||
@ -40,12 +65,28 @@ public class PirateChainWalletController extends Thread {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Pirate Chain Wallet Controller");
|
||||
|
||||
LiteWalletJni.loadLibrary();
|
||||
|
||||
try {
|
||||
while (running && !Controller.isStopping()) {
|
||||
Thread.sleep(1000);
|
||||
|
||||
// Wait until we have a request to load the wallet
|
||||
if (!shouldLoadWallet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!LiteWalletJni.isLoaded()) {
|
||||
this.loadLibrary();
|
||||
|
||||
// If still not loaded, sleep to prevent too many requests
|
||||
if (!LiteWalletJni.isLoaded()) {
|
||||
Thread.sleep(5 * 1000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Wallet is downloaded, so clear the status
|
||||
this.loadStatus = null;
|
||||
|
||||
if (this.currentWallet == null) {
|
||||
// Nothing to do yet
|
||||
continue;
|
||||
@ -91,6 +132,137 @@ public class PirateChainWalletController extends Thread {
|
||||
}
|
||||
|
||||
|
||||
// QDN & wallet libraries
|
||||
|
||||
private void loadLibrary() throws InterruptedException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Check if architecture is supported
|
||||
String libFileName = PirateChainWalletController.getRustLibFilename();
|
||||
if (libFileName == null) {
|
||||
String osName = System.getProperty("os.name");
|
||||
String osArchitecture = System.getProperty("os.arch");
|
||||
this.loadStatus = String.format("Unsupported architecture (%s %s)", osName, osArchitecture);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the library exists in the wallets folder
|
||||
Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory();
|
||||
Path libPath = Paths.get(libDirectory.toString(), libFileName);
|
||||
if (Files.exists(libPath)) {
|
||||
// Already downloaded; we can load the library right away
|
||||
LiteWalletJni.loadLibrary();
|
||||
return;
|
||||
}
|
||||
|
||||
// Library not found, so check if we've fetched the resource from QDN
|
||||
ArbitraryTransactionData t = this.getTransactionData(repository);
|
||||
if (t == null) {
|
||||
// Can't find the transaction - maybe on a different chain?
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until we have a sufficient number of peers to attempt QDN downloads
|
||||
List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
|
||||
if (handshakedPeers.size() < Settings.getInstance().getMinBlockchainPeers()) {
|
||||
// Wait for more peers
|
||||
this.loadStatus = String.format("Searching for peers...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build resource
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(t.getName(),
|
||||
ArbitraryDataFile.ResourceIdType.NAME, t.getService(), t.getIdentifier());
|
||||
try {
|
||||
arbitraryDataReader.loadSynchronously(false);
|
||||
} catch (MissingDataException e) {
|
||||
LOGGER.info("Missing data when loading Pirate Chain library");
|
||||
}
|
||||
|
||||
// Check its status
|
||||
ArbitraryResourceStatus status = ArbitraryTransactionUtils.getStatus(
|
||||
t.getService(), t.getName(), t.getIdentifier(), false);
|
||||
|
||||
if (status.getStatus() != ArbitraryResourceStatus.Status.READY) {
|
||||
LOGGER.info("Not ready yet: {}", status.getTitle());
|
||||
this.loadStatus = String.format("Downloading wallet files... (%d / %d)", status.getLocalChunkCount(), status.getTotalChunkCount());
|
||||
return;
|
||||
}
|
||||
|
||||
// Files are downloaded, so copy the necessary files to the wallets folder
|
||||
// Delete the wallets/*/lib directory first, in case earlier versions of the wallet are present
|
||||
Path walletsLibDirectory = PirateChainWalletController.getWalletsLibDirectory();
|
||||
if (Files.exists(walletsLibDirectory)) {
|
||||
FilesystemUtils.safeDeleteDirectory(walletsLibDirectory, false);
|
||||
}
|
||||
Files.createDirectories(libDirectory);
|
||||
FileUtils.copyDirectory(arbitraryDataReader.getFilePath().toFile(), libDirectory.toFile());
|
||||
|
||||
// Clear reader cache so only one copy exists
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(t.getName(),
|
||||
ArbitraryDataFile.ResourceIdType.NAME, t.getService(), t.getIdentifier());
|
||||
resource.deleteCache();
|
||||
|
||||
// Finally, load the library
|
||||
LiteWalletJni.loadLibrary();
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue when loading Pirate Chain library", e);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Error when loading Pirate Chain library: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private ArbitraryTransactionData getTransactionData(Repository repository) {
|
||||
try {
|
||||
byte[] signature = Base58.decode(qdnWalletSignature);
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (!(transactionData instanceof ArbitraryTransactionData))
|
||||
return null;
|
||||
|
||||
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
|
||||
if (arbitraryTransaction != null) {
|
||||
return (ArbitraryTransactionData) arbitraryTransaction.getTransactionData();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (DataException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getRustLibFilename() {
|
||||
String osName = System.getProperty("os.name");
|
||||
String osArchitecture = System.getProperty("os.arch");
|
||||
|
||||
if (osName.equals("Mac OS X") && osArchitecture.equals("x86_64")) {
|
||||
return "librust-macos-x86_64.dylib";
|
||||
}
|
||||
else if (osName.equals("Linux") && osArchitecture.equals("aarch64")) {
|
||||
return "librust-linux-aarch64.so";
|
||||
}
|
||||
else if (osName.equals("Linux") && osArchitecture.equals("amd64")) {
|
||||
return "librust-linux-x86_64.so";
|
||||
}
|
||||
else if (osName.contains("Windows") && osArchitecture.equals("amd64")) {
|
||||
return "librust-windows-x86_64.dll";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Path getWalletsLibDirectory() {
|
||||
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", "lib");
|
||||
}
|
||||
|
||||
public static Path getRustLibOuterDirectory() {
|
||||
String sigPrefix = qdnWalletSignature.substring(0, 8);
|
||||
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", "lib", sigPrefix);
|
||||
}
|
||||
|
||||
|
||||
// Wallet functions
|
||||
|
||||
public boolean initWithEntropy58(String entropy58) {
|
||||
return this.initWithEntropy58(entropy58, false);
|
||||
}
|
||||
@ -100,6 +272,12 @@ public class PirateChainWalletController extends Thread {
|
||||
}
|
||||
|
||||
private boolean initWithEntropy58(String entropy58, boolean isNullSeedWallet) {
|
||||
// If the JNI library isn't loaded yet then we can't proceed
|
||||
if (!LiteWalletJni.isLoaded()) {
|
||||
shouldLoadWallet = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] entropyBytes = Base58.decode(entropy58);
|
||||
|
||||
if (entropyBytes == null || entropyBytes.length != 32) {
|
||||
@ -190,9 +368,11 @@ public class PirateChainWalletController extends Thread {
|
||||
}
|
||||
|
||||
public String getSyncStatus() {
|
||||
// TODO: check library exists, and show status of download if not
|
||||
|
||||
if (this.currentWallet == null || !this.currentWallet.isInitialized()) {
|
||||
if (this.loadStatus != null) {
|
||||
return this.loadStatus;
|
||||
}
|
||||
|
||||
return "Not initialized yet";
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import com.rust.litewalletjni.LiteWalletJni;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@ -9,13 +8,13 @@ import org.bouncycastle.util.encoders.DecoderException;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.controller.PirateChainWalletController;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@ -37,27 +36,26 @@ public class PirateWallet {
|
||||
private String seedPhrase;
|
||||
private boolean ready = false;
|
||||
|
||||
private final String params;
|
||||
private final String saplingOutput64;
|
||||
private final String saplingSpend64;
|
||||
private String params;
|
||||
private String saplingOutput64;
|
||||
private String saplingSpend64;
|
||||
|
||||
private final static String SERVER_URI = "https://lightd.pirate.black:443/";
|
||||
private final static String COIN_PARAMS_RESOURCE = "piratechain/coinparams.json";
|
||||
private final static String SAPLING_OUTPUT_RESOURCE = "piratechain/saplingoutput_base64";
|
||||
private final static String SAPLING_SPEND_RESOURCE = "piratechain/saplingspend_base64";
|
||||
private final static String COIN_PARAMS_FILENAME = "coinparams.json";
|
||||
private final static String SAPLING_OUTPUT_FILENAME = "saplingoutput_base64";
|
||||
private final static String SAPLING_SPEND_FILENAME = "saplingspend_base64";
|
||||
|
||||
public PirateWallet(byte[] entropyBytes, boolean isNullSeedWallet) throws IOException {
|
||||
this.entropyBytes = entropyBytes;
|
||||
this.isNullSeedWallet = isNullSeedWallet;
|
||||
|
||||
final URL paramsUrl = Resources.getResource(COIN_PARAMS_RESOURCE);
|
||||
this.params = Resources.toString(paramsUrl, StandardCharsets.UTF_8);
|
||||
Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory();
|
||||
if (!Files.exists(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME))) {
|
||||
return;
|
||||
}
|
||||
|
||||
final URL saplingOutput64Url = Resources.getResource(SAPLING_OUTPUT_RESOURCE);
|
||||
this.saplingOutput64 = Resources.toString(saplingOutput64Url, StandardCharsets.ISO_8859_1);
|
||||
|
||||
final URL saplingSpend64Url = Resources.getResource(SAPLING_SPEND_RESOURCE);
|
||||
this.saplingSpend64 = Resources.toString(saplingSpend64Url, StandardCharsets.ISO_8859_1);
|
||||
this.params = Files.readString(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME));
|
||||
this.saplingOutput64 = Files.readString(Paths.get(libDirectory.toString(), SAPLING_OUTPUT_FILENAME));
|
||||
this.saplingSpend64 = Files.readString(Paths.get(libDirectory.toString(), SAPLING_SPEND_FILENAME));
|
||||
|
||||
this.ready = this.initialize();
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ public class ArbitraryResourceStatus {
|
||||
}
|
||||
}
|
||||
|
||||
private Status status;
|
||||
private String id;
|
||||
private String title;
|
||||
private String description;
|
||||
@ -37,6 +38,7 @@ public class ArbitraryResourceStatus {
|
||||
}
|
||||
|
||||
public ArbitraryResourceStatus(Status status, Integer localChunkCount, Integer totalChunkCount) {
|
||||
this.status = status;
|
||||
this.id = status.toString();
|
||||
this.title = status.title;
|
||||
this.description = status.description;
|
||||
@ -47,4 +49,20 @@ public class ArbitraryResourceStatus {
|
||||
public ArbitraryResourceStatus(Status status) {
|
||||
this(status, null, null);
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
public Integer getLocalChunkCount() {
|
||||
return this.localChunkCount;
|
||||
}
|
||||
|
||||
public Integer getTotalChunkCount() {
|
||||
return this.totalChunkCount;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,10 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataFileChunk;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
@ -410,4 +413,31 @@ public class ArbitraryTransactionUtils {
|
||||
return transactions.stream().skip(offset).limit(limit).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Lookup status of resource
|
||||
* @param service
|
||||
* @param name
|
||||
* @param identifier
|
||||
* @param build
|
||||
* @return
|
||||
*/
|
||||
public static ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) {
|
||||
|
||||
// If "build" has been specified, build the resource before returning its status
|
||||
if (build != null && build == true) {
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
|
||||
try {
|
||||
if (!reader.isBuilding()) {
|
||||
reader.loadSynchronously(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// No need to handle exception, as it will be reflected in the status
|
||||
}
|
||||
}
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
return resource.getStatus(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user