moved chat pow to java

This commit is contained in:
PhilReact 2024-11-21 22:56:01 +02:00
parent c6f6ab58f1
commit 2fd09f5e20
11 changed files with 637 additions and 42 deletions

View File

@ -39,6 +39,10 @@ dependencies {
implementation "org.mindrot:jbcrypt:0.4" implementation "org.mindrot:jbcrypt:0.4"
implementation "at.favre.lib:bcrypt:0.10.2" implementation "at.favre.lib:bcrypt:0.10.2"
implementation 'com.password4j:password4j:1.8.2' implementation 'com.password4j:password4j:1.8.2'
implementation 'com.dylibso.chicory:runtime:1.0.0-M1'
implementation 'commons-net:commons-net:3.6'
implementation 'org.bouncycastle:bcprov-jdk15to18:1.76'
implementation 'com.google.guava:guava:32.1.2-jre'
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"

View File

@ -0,0 +1,126 @@
package com.github.Qortal.qortalMobile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public abstract class Crypto {
/**
* Returns 32-byte SHA-256 digest of message passed in input.
*
* @param input
* variable-length byte[] message
* @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
*/
public static byte[] digest(byte[] input) {
if (input == null)
return null;
try {
// SHA2-256
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
return sha256.digest(input);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 message digest not available");
}
}
/**
* Returns 32-byte SHA-256 digest of message passed in input.
*
* @param input
* variable-length ByteBuffer message
* @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
*/
public static byte[] digest(ByteBuffer input) {
if (input == null)
return null;
try {
// SHA2-256
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
sha256.update(input);
return sha256.digest();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 message digest not available");
}
}
/**
* Returns 32-byte digest of two rounds of SHA-256 on message passed in input.
*
* @param input
* variable-length byte[] message
* @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
*/
public static byte[] doubleDigest(byte[] input) {
return digest(digest(input));
}
/**
* Returns 32-byte SHA-256 digest of file passed in input.
*
* @param file
* file in which to perform digest
* @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
*
* @throws IOException if the file cannot be read
*/
public static byte[] digest(File file) throws IOException {
return Crypto.digest(file, 8192);
}
/**
* Returns 32-byte SHA-256 digest of file passed in input, in hex format
*
* @param file
* file in which to perform digest
* @return String digest as a hexadecimal string, or null if SHA-256 algorithm can't be accessed
*
* @throws IOException if the file cannot be read
*/
public static String digestHexString(File file, int bufferSize) throws IOException {
byte[] digest = Crypto.digest(file, bufferSize);
// Convert to hex
StringBuilder stringBuilder = new StringBuilder();
for (byte b : digest) {
stringBuilder.append(String.format("%02x", b));
}
return stringBuilder.toString();
}
/**
* Returns 32-byte SHA-256 digest of file passed in input.
*
* @param file
* file in which to perform digest
* @param bufferSize
* the number of bytes to load into memory
* @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
*
* @throws IOException if the file cannot be read
*/
public static byte[] digest(File file, int bufferSize) throws IOException {
try {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = new byte[bufferSize];
int count;
while ((count = fileInputStream.read(bytes)) != -1) {
sha256.update(bytes, 0, count);
}
fileInputStream.close();
return sha256.digest();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 message digest not available");
}
}
}

View File

@ -2,12 +2,14 @@ package com.github.Qortal.qortalMobile;
import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeActivity;
import com.github.Qortal.qortalMobile.NativeBcrypt; import com.github.Qortal.qortalMobile.NativeBcrypt;
import com.github.Qortal.qortalMobile.NativePOW;
import android.os.Bundle; import android.os.Bundle;
public class MainActivity extends BridgeActivity { public class MainActivity extends BridgeActivity {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
registerPlugin(NativeBcrypt.class); registerPlugin(NativeBcrypt.class);
registerPlugin(NativePOW.class);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);

View File

@ -0,0 +1,168 @@
package com.github.Qortal.qortalMobile;
import com.github.Qortal.qortalMobile.NTP;
import com.github.Qortal.qortalMobile.Crypto;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeoutException;
public class MemoryPoW {
/**
* Compute a MemoryPoW nonce
*
* @param data
* @param workBufferLength
* @param difficulty
* @return
* @throws TimeoutException
*/
public static Integer compute2(byte[] data, int workBufferLength, long difficulty) {
try {
return MemoryPoW.compute2(data, workBufferLength, difficulty, null);
} catch (TimeoutException e) {
// This won't happen, because above timeout is null
return null;
}
}
/**
* Compute a MemoryPoW nonce, with optional timeout
*
* @param data
* @param workBufferLength
* @param difficulty
* @param timeout maximum number of milliseconds to compute for before giving up,<br>or null if no timeout
* @return
* @throws TimeoutException
*/
public static Integer compute2(byte[] data, int workBufferLength, long difficulty, Long timeout) throws TimeoutException {
long startTime = NTP.getTime() != null ? NTP.getTime() : System.currentTimeMillis();
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
long[] longHash = new long[4];
ByteBuffer byteBuffer = ByteBuffer.wrap(hash);
longHash[0] = byteBuffer.getLong();
longHash[1] = byteBuffer.getLong();
longHash[2] = byteBuffer.getLong();
longHash[3] = byteBuffer.getLong();
byteBuffer = null;
int longBufferLength = workBufferLength / 8;
long[] workBuffer = new long[longBufferLength];
long[] state = new long[4];
long seed = 8682522807148012L;
long seedMultiplier = 1181783497276652981L;
// For each nonce...
int nonce = -1;
long result = 0;
do {
++nonce;
// If we've been interrupted, exit fast with invalid value
if (Thread.currentThread().isInterrupted())
return -1;
if (timeout != null) {
long now = NTP.getTime() != null ? NTP.getTime() : System.currentTimeMillis();
if (now > startTime + timeout) {
throw new TimeoutException("Timeout reached");
}
}
seed *= seedMultiplier; // per nonce
state[0] = longHash[0] ^ seed;
state[1] = longHash[1] ^ seed;
state[2] = longHash[2] ^ seed;
state[3] = longHash[3] ^ seed;
// Fill work buffer with random
for (int i = 0; i < workBuffer.length; ++i)
workBuffer[i] = xoshiro256p(state);
// Random bounce through whole buffer
result = workBuffer[0];
for (int i = 0; i < 1024; ++i) {
int index = (int) (xoshiro256p(state) & Integer.MAX_VALUE) % workBuffer.length;
result ^= workBuffer[index];
}
// Return if final value > difficulty
} while (Long.numberOfLeadingZeros(result) < difficulty);
return nonce;
}
public static boolean verify2(byte[] data, int workBufferLength, long difficulty, int nonce) {
return verify2(data, null, workBufferLength, difficulty, nonce);
}
public static boolean verify2(byte[] data, long[] workBuffer, int workBufferLength, long difficulty, int nonce) {
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
long[] longHash = new long[4];
ByteBuffer byteBuffer = ByteBuffer.wrap(hash);
longHash[0] = byteBuffer.getLong();
longHash[1] = byteBuffer.getLong();
longHash[2] = byteBuffer.getLong();
longHash[3] = byteBuffer.getLong();
byteBuffer = null;
int longBufferLength = workBufferLength / 8;
if (workBuffer == null)
workBuffer = new long[longBufferLength];
long[] state = new long[4];
long seed = 8682522807148012L;
long seedMultiplier = 1181783497276652981L;
for (int i = 0; i <= nonce; ++i)
seed *= seedMultiplier;
state[0] = longHash[0] ^ seed;
state[1] = longHash[1] ^ seed;
state[2] = longHash[2] ^ seed;
state[3] = longHash[3] ^ seed;
// Fill work buffer with random
for (int i = 0; i < workBuffer.length; ++i)
workBuffer[i] = xoshiro256p(state);
// Random bounce through whole buffer
long result = workBuffer[0];
for (int i = 0; i < 1024; ++i) {
int index = (int) (xoshiro256p(state) & Integer.MAX_VALUE) % workBuffer.length;
result ^= workBuffer[index];
}
return Long.numberOfLeadingZeros(result) >= difficulty;
}
private static final long xoshiro256p(long[] state) {
final long result = state[0] + state[3];
final long temp = state[1] << 17;
state[2] ^= state[0];
state[3] ^= state[1];
state[1] ^= state[2];
state[0] ^= state[3];
state[2] ^= temp;
state[3] = (state[3] << 45) | (state[3] >>> (64 - 45)); // rol64(s[3], 45);
return result;
}
}

View File

@ -0,0 +1,241 @@
package com.github.Qortal.qortalMobile;
import org.apache.commons.net.ntp.NTPUDPClient;
import org.apache.commons.net.ntp.NtpV3Packet;
import org.apache.commons.net.ntp.TimeInfo;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class NTP implements Runnable {
private static final String TAG = "NTP";
private static boolean isStarted = false;
private static volatile boolean isStopping = false;
private static ExecutorService instanceExecutor;
private static NTP instance;
private static volatile boolean isOffsetSet = false;
private static volatile long offset = 0;
static class NTPServer {
private static final int MIN_POLL = 64;
public char usage = ' ';
public String remote;
public String refId;
public Integer stratum;
public char type = 'u'; // unicast
public int poll = MIN_POLL;
public byte reach = 0;
public Long delay;
public Double offset;
public Double jitter;
private Deque<Double> offsets = new LinkedList<>();
private double totalSquareOffsets = 0.0;
private long nextPoll;
private Long lastGood;
public NTPServer(String remote) {
this.remote = remote;
}
public boolean doPoll(NTPUDPClient client, final long now) {
Thread.currentThread().setName(String.format("NTP: %s", this.remote));
try {
boolean isUpdated = false;
try {
TimeInfo timeInfo = client.getTime(InetAddress.getByName(remote));
timeInfo.computeDetails();
NtpV3Packet ntpMessage = timeInfo.getMessage();
this.refId = ntpMessage.getReferenceIdString();
this.stratum = ntpMessage.getStratum();
this.poll = Math.max(MIN_POLL, 1 << ntpMessage.getPoll());
this.delay = timeInfo.getDelay();
this.offset = (double) timeInfo.getOffset();
if (this.offsets.size() == 8) {
double oldOffset = this.offsets.removeFirst();
this.totalSquareOffsets -= oldOffset * oldOffset;
}
this.offsets.addLast(this.offset);
this.totalSquareOffsets += this.offset * this.offset;
this.jitter = Math.sqrt(this.totalSquareOffsets / this.offsets.size());
this.reach = (byte) ((this.reach << 1) | 1);
this.lastGood = now;
isUpdated = true;
} catch (IOException e) {
this.reach <<= 1;
Log.e(TAG, "Error polling server: " + remote, e);
}
this.nextPoll = now + this.poll * 1000;
return isUpdated;
} finally {
Thread.currentThread().setName("NTP (dormant)");
}
}
public Integer getWhen() {
if (this.lastGood == null)
return null;
return (int) ((System.currentTimeMillis() - this.lastGood) / 1000);
}
}
private final NTPUDPClient client;
private final List<NTPServer> ntpServers = new ArrayList<>();
private final ExecutorService serverExecutor;
private NTP(String[] serverNames) {
client = new NTPUDPClient();
client.setDefaultTimeout(2000);
for (String serverName : serverNames)
ntpServers.add(new NTPServer(serverName));
serverExecutor = Executors.newCachedThreadPool();
}
public static synchronized void start(String[] serverNames) {
if (isStarted)
return;
isStarted = true;
instanceExecutor = Executors.newSingleThreadExecutor();
instance = new NTP(serverNames);
instanceExecutor.execute(instance);
Log.d(TAG, "NTP started with servers: " + String.join(", ", serverNames));
}
public static void shutdownNow() {
if (instanceExecutor != null)
instanceExecutor.shutdownNow();
Log.d(TAG, "NTP shutdown.");
}
public static synchronized void setFixedOffset(Long offset) {
NTP.offset = offset;
isOffsetSet = true;
Log.d(TAG, "Fixed offset set: " + offset);
}
public static Long getTime() {
if (!isOffsetSet)
return null;
return System.currentTimeMillis() + NTP.offset;
}
public void run() {
Thread.currentThread().setName("NTP instance");
try {
while (!isStopping) {
Thread.sleep(1000);
boolean haveUpdates = pollServers();
if (!haveUpdates)
continue;
calculateOffset();
}
} catch (InterruptedException e) {
Log.d(TAG, "NTP instance interrupted.");
}
}
private boolean pollServers() throws InterruptedException {
final long now = System.currentTimeMillis();
List<NTPServer> pendingServers = ntpServers.stream().filter(ntpServer -> now >= ntpServer.nextPoll).collect(Collectors.toList());
CompletionService<Boolean> ecs = new ExecutorCompletionService<>(serverExecutor);
for (NTPServer server : pendingServers)
ecs.submit(() -> server.doPoll(client, now));
boolean haveUpdate = false;
for (int i = 0; i < pendingServers.size(); ++i) {
if (isStopping)
return false;
try {
haveUpdate = ecs.take().get() || haveUpdate;
} catch (ExecutionException e) {
Log.e(TAG, "Error during server polling", e);
}
}
return haveUpdate;
}
private void calculateOffset() {
double s0 = 0;
double s1 = 0;
double s2 = 0;
for (NTPServer server : ntpServers) {
if (server.offset == null) {
server.usage = ' ';
continue;
}
server.usage = '+';
double value = server.offset * (double) server.stratum;
s0 += 1;
s1 += value;
s2 += value * value;
}
if (s0 < ntpServers.size() / 3 + 1) {
Log.d(TAG, String.format("Not enough replies (%d) to calculate network time", (int) s0));
} else {
double thresholdStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
double mean = s1 / s0;
// Now only consider offsets within 1 stddev
s0 = 0;
s1 = 0;
s2 = 0;
for (NTPServer server : ntpServers) {
if (server.offset == null || server.reach == 0)
continue;
if (Math.abs(server.offset * (double) server.stratum - mean) > thresholdStddev)
continue;
server.usage = '*';
s0 += 1;
s1 += server.offset;
s2 += server.offset * server.offset;
}
if (s0 > 1) {
double filteredMean = s1 / s0;
NTP.offset = (long) filteredMean;
isOffsetSet = true;
Log.d(TAG, "New NTP offset: " + NTP.offset);
} else {
Log.d(TAG, "Not enough useful values to calculate network time.");
}
}
}
}

View File

@ -0,0 +1,56 @@
package com.github.Qortal.qortalMobile;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.JSObject;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import android.util.Log;
import com.github.Qortal.qortalMobile.MemoryPoW;
import java.util.Iterator;
@CapacitorPlugin(name = "NativePOW")
public class NativePOW extends Plugin {
@PluginMethod
public void computeProofOfWork(PluginCall call) {
try {
// Extract parameters from the call
JSObject chatBytesObject = call.getObject("chatBytes", new JSObject());
int difficulty = call.getInt("difficulty", 0);
// Convert chatBytesObject to a byte array
byte[] chatBytes = jsObjectToByteArray(chatBytesObject);
// Use the MemoryPoW.compute2 method
int workBufferLength = 8 * 1024 * 1024; // 8 MiB buffer
Integer nonce = MemoryPoW.compute2(chatBytes, workBufferLength, difficulty);
// Return result to the plugin caller
JSObject result = new JSObject();
result.put("nonce", nonce);
call.resolve(result);
} catch (Exception e) {
call.reject("Error computing proof-of-work", e);
}
}
private byte[] jsObjectToByteArray(JSObject jsObject) {
int length = jsObject.length();
byte[] array = new byte[length];
Iterator<String> keys = jsObject.keys();
while (keys.hasNext()) {
String key = keys.next();
int index = Integer.parseInt(key);
int value = jsObject.getInteger(key);
array[index] = (byte) value;
}
return array;
}
}

Binary file not shown.

View File

@ -29,6 +29,8 @@ import PhraseWallet from "./utils/generateWallet/phrase-wallet";
import { RequestQueueWithPromise } from "./utils/queue/queue"; import { RequestQueueWithPromise } from "./utils/queue/queue";
import { validateAddress } from "./utils/validateAddress"; import { validateAddress } from "./utils/validateAddress";
import { Sha256 } from "asmcrypto.js"; import { Sha256 } from "asmcrypto.js";
import NativePOW from './utils/nativepow'
import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest"; import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest";
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes"; import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes";
import { import {
@ -98,7 +100,7 @@ import {
import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage"; import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage";
import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch'; import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch';
import { LocalNotifications } from '@capacitor/local-notifications'; import { LocalNotifications } from '@capacitor/local-notifications';
import ChatComputePowWorker from './chatComputePow.worker.js?worker'; // import ChatComputePowWorker from './chatComputePow.worker.js?worker';
const uid = new ShortUniqueId({ length: 9, dictionary: 'number' }); const uid = new ShortUniqueId({ length: 9, dictionary: 'number' });
@ -383,29 +385,13 @@ function playNotificationSound() {
// chrome.runtime.sendMessage({ action: "PLAY_NOTIFICATION_SOUND" }); // chrome.runtime.sendMessage({ action: "PLAY_NOTIFICATION_SOUND" });
} }
const worker = new ChatComputePowWorker() // const worker = new ChatComputePowWorker()
export async function performPowTask(chatBytes, difficulty) { export async function performPowTask(chatBytes, difficulty) {
return new Promise((resolve, reject) => { const chatBytesArray = Uint8Array.from(Object.values(chatBytes));
worker.onmessage = (e) => { const result = await NativePOW.computeProofOfWork({ chatBytes, difficulty });
if (e.data.error) { return {nonce: result.nonce, chatBytesArray}
reject(new Error(e.data.error));
} else {
resolve(e.data);
}
};
worker.onerror = (err) => {
reject(err);
};
// Send the task to the worker
worker.postMessage({
chatBytes,
path: `${import.meta.env.BASE_URL}memory-pow.wasm.full`,
difficulty,
});
});
} }
const handleNotificationDirect = async (directs) => { const handleNotificationDirect = async (directs) => {

View File

@ -1,6 +1,6 @@
import { Sha256 } from 'asmcrypto.js'; import { Sha256 } from 'asmcrypto.js';
import wasmInit from './memory-pow.wasm?init'; import wasmInit from './memory-pow.wasm?init';
import NativePOW from './utils/nativepow'
let compute; // Exported compute function from Wasm let compute; // Exported compute function from Wasm
let memory; // WebAssembly.Memory instance let memory; // WebAssembly.Memory instance
let heap; // Uint8Array view of the memory buffer let heap; // Uint8Array view of the memory buffer
@ -58,24 +58,26 @@ function sbrk(size) {
// Proof-of-Work computation function // Proof-of-Work computation function
async function computePow(chatBytes, difficulty) { async function computePow(chatBytes, difficulty) {
if (!compute) { // if (!compute) {
throw new Error('WebAssembly module not initialized. Call loadWasm first.'); // throw new Error('WebAssembly module not initialized. Call loadWasm first.');
} // }
const chatBytesArray = Uint8Array.from(Object.values(chatBytes)); // const chatBytesArray = Uint8Array.from(Object.values(chatBytes));
const chatBytesHash = new Sha256().process(chatBytesArray).finish().result; // const chatBytesHash = new Sha256().process(chatBytesArray).finish().result;
// Allocate memory for the hash // // Allocate memory for the hash
const hashPtr = sbrk(32); // const hashPtr = sbrk(32);
const hashAry = new Uint8Array(memory.buffer, hashPtr, 32); // const hashAry = new Uint8Array(memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash); // hashAry.set(chatBytesHash);
// Reuse the work buffer if already allocated // // Reuse the work buffer if already allocated
if (!workBufferPtr) { // if (!workBufferPtr) {
workBufferPtr = sbrk(workBufferLength); // workBufferPtr = sbrk(workBufferLength);
} // }
console.log('native')
const nonce = compute(hashPtr, workBufferPtr, workBufferLength, difficulty); const nonce = await NativePOW.computeProofOfWork({ chatBytes, difficulty });
console.log('nonce', nonce)
(hashPtr, workBufferPtr, workBufferLength, difficulty);
return { nonce, chatBytesArray }; return { nonce, chatBytesArray };
} }
@ -86,9 +88,9 @@ self.addEventListener('message', async (e) => {
try { try {
// Initialize Wasm if not already done // Initialize Wasm if not already done
if (!compute) { // if (!compute) {
await loadWasm(); // await loadWasm();
} // }
// Perform the POW computation // Perform the POW computation
const result = await computePow(chatBytes, difficulty); const result = await computePow(chatBytes, difficulty);

View File

@ -7,6 +7,7 @@ import { ThemeProvider, createTheme } from '@mui/material/styles';
import { CssBaseline } from '@mui/material'; import { CssBaseline } from '@mui/material';
import { MessageQueueProvider } from './MessageQueueContext.tsx'; import { MessageQueueProvider } from './MessageQueueContext.tsx';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import './utils/nativepow.ts'
const theme = createTheme({ const theme = createTheme({
palette: { palette: {
primary: { primary: {

9
src/utils/nativepow.ts Normal file
View File

@ -0,0 +1,9 @@
import { registerPlugin } from '@capacitor/core';
export interface NativePOWPlugin {
computeProofOfWork(options: { chatBytes: string; difficulty: number }): Promise<{ nonce: string }>;
}
const NativePOW = registerPlugin<NativePOWPlugin>('NativePOW');
export default NativePOW