Browse Source

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.
pirate-chain
CalDescent 2 years ago
parent
commit
1b9128289f
  1. 24
      src/main/java/com/rust/litewalletjni/LiteWalletJni.java
  2. 24
      src/main/java/org/qortal/api/resource/ArbitraryResource.java
  3. 188
      src/main/java/org/qortal/controller/PirateChainWalletController.java
  4. 30
      src/main/java/org/qortal/crosschain/PirateWallet.java
  5. 18
      src/main/java/org/qortal/data/arbitrary/ArbitraryResourceStatus.java
  6. 30
      src/main/java/org/qortal/utils/ArbitraryTransactionUtils.java

24
src/main/java/com/rust/litewalletjni/LiteWalletJni.java

@ -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) {

24
src/main/java/org/qortal/api/resource/ArbitraryResource.java

@ -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<>();

188
src/main/java/org/qortal/controller/PirateChainWalletController.java

@ -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";
}

30
src/main/java/org/qortal/crosschain/PirateWallet.java

@ -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);
final URL saplingOutput64Url = Resources.getResource(SAPLING_OUTPUT_RESOURCE);
this.saplingOutput64 = Resources.toString(saplingOutput64Url, StandardCharsets.ISO_8859_1);
Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory();
if (!Files.exists(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME))) {
return;
}
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();
}

18
src/main/java/org/qortal/data/arbitrary/ArbitraryResourceStatus.java

@ -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;
}
}

30
src/main/java/org/qortal/utils/ArbitraryTransactionUtils.java

@ -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…
Cancel
Save