mirror of
https://github.com/EvernodeXRPL/hpcore.git
synced 2026-04-29 15:37:59 +00:00
Updated js client and contract libs. (#273)
* Updated js contract lib to for ES spec compatibility. * Added 'ed' prefix to generated keys in js client lib. * Fixed js client lib incorrect connection timeout tracking.
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
const outputValidationPassThreshold = 0.8;
|
||||
const connectionCheckIntervalMs = 1000;
|
||||
const recentActivityThresholdMs = 3000;
|
||||
const edKeyType = 237;
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
@@ -40,22 +41,39 @@
|
||||
Object.freeze(events);
|
||||
|
||||
/*--- Included in public interface. ---*/
|
||||
// privateKeyHex: Hex private key with prefix ('ed').
|
||||
// Returns 'ed' (237) prefixed binary public/private keys.
|
||||
const generateKeys = async (privateKeyHex = null) => {
|
||||
|
||||
await initSodium();
|
||||
|
||||
if (!privateKeyHex) {
|
||||
const keys = sodium.crypto_sign_keypair();
|
||||
|
||||
const binPrivateKey = new Uint8Array(65);
|
||||
binPrivateKey[0] = edKeyType;
|
||||
binPrivateKey.set(keys.privateKey, 1);
|
||||
|
||||
const binPublicKey = new Uint8Array(33);
|
||||
binPublicKey[0] = edKeyType;
|
||||
binPublicKey.set(keys.publicKey, 1);
|
||||
|
||||
return {
|
||||
privateKey: keys.privateKey,
|
||||
publicKey: keys.publicKey
|
||||
privateKey: binPrivateKey,
|
||||
publicKey: binPublicKey
|
||||
}
|
||||
}
|
||||
else {
|
||||
const binPrivateKey = hexToUint8Array(privateKeyHex);
|
||||
if (binPrivateKey[0] != edKeyType)
|
||||
throw "Invaid key type. 'ed' expected.";
|
||||
|
||||
const binPublicKey = new Uint8Array(33);
|
||||
binPublicKey[0] = edKeyType;
|
||||
binPublicKey.set(binPrivateKey.slice(33), 1);
|
||||
return {
|
||||
privateKey: Uint8Array.from(binPrivateKey),
|
||||
publicKey: Uint8Array.from(binPrivateKey.slice(32))
|
||||
privateKey: binPrivateKey,
|
||||
publicKey: binPublicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,8 +163,8 @@
|
||||
let initialConnectSuccess = null;
|
||||
|
||||
// Tracks when was the earliest time that we were missing some required connections.
|
||||
// 0 indicates we are no missing any connections.
|
||||
let connectionsMissingFrom = new Date().getTime();
|
||||
// 0 indicates we are not missing any connections. This will be initially set when connect() is called.
|
||||
let connectionsMissingFrom = 0;
|
||||
|
||||
// Checks for missing connections and attempts to establish them.
|
||||
const reviewConnections = () => {
|
||||
@@ -231,7 +249,10 @@
|
||||
if (status > 0)
|
||||
return;
|
||||
|
||||
// Start the missing-connections timer tracking from this point onwards.
|
||||
connectionsMissingFrom = new Date().getTime();
|
||||
reviewConnections();
|
||||
|
||||
return new Promise(resolve => {
|
||||
initialConnectSuccess = resolve;
|
||||
})
|
||||
@@ -356,11 +377,11 @@
|
||||
|
||||
// Get the signature and issuer pubkey bytes based on the data type.
|
||||
// (json encoding will use hex string and bson will use buffer)
|
||||
const pubkey = isString(pair[0]) ? hexToUint8Array(pair[0].substring(2)) : pair[0].buffer.slice(1); // Skip prefix byte.
|
||||
const binPubkey = isString(pair[0]) ? hexToUint8Array(pair[0]) : pair[0].buffer;
|
||||
const sig = isString(pair[1]) ? hexToUint8Array(pair[1]) : pair[1].buffer;
|
||||
|
||||
// Check whether the pubkey is in unl and whether signature is valid.
|
||||
if (!passedKeys[pubkeyHex] && unlKeysLookup[pubkeyHex] && sodium.crypto_sign_verify_detached(sig, rootHash, pubkey))
|
||||
if (!passedKeys[pubkeyHex] && unlKeysLookup[pubkeyHex] && sodium.crypto_sign_verify_detached(sig, rootHash, binPubkey.slice(1)))
|
||||
passedKeys[pubkeyHex] = true;
|
||||
}
|
||||
|
||||
@@ -372,7 +393,7 @@
|
||||
const validateOutput = (msg, trustedKeys) => {
|
||||
|
||||
// Calculate combined output hash with user's pubkey.
|
||||
const outputHash = getHash([[0xED], clientKeys.publicKey, ...msgHelper.spreadArrayField(msg.outputs)]);
|
||||
const outputHash = getHash([clientKeys.publicKey, ...msgHelper.spreadArrayField(msg.outputs)]);
|
||||
|
||||
const result = getMerkleHash(msg.hashes, msgHelper.stringifyValue(outputHash));
|
||||
if (result[0] == true) {
|
||||
@@ -747,12 +768,12 @@
|
||||
this.createUserChallengeResponse = (userChallenge, serverChallenge, msgProtocol) => {
|
||||
// For challenge response encoding Hot Pocket always uses json.
|
||||
// Challenge response will specify the protocol to use for contract messages.
|
||||
const sigBytes = sodium.crypto_sign_detached(userChallenge, keys.privateKey);
|
||||
const sigBytes = sodium.crypto_sign_detached(userChallenge, keys.privateKey.slice(1));
|
||||
|
||||
return {
|
||||
type: "user_challenge_response",
|
||||
sig: this.binaryEncode(sigBytes),
|
||||
pubkey: "ed" + this.binaryEncode(keys.publicKey),
|
||||
pubkey: this.binaryEncode(keys.publicKey),
|
||||
server_challenge: serverChallenge,
|
||||
protocol: msgProtocol
|
||||
}
|
||||
@@ -770,7 +791,7 @@
|
||||
}
|
||||
|
||||
const serlializedInpContainer = this.serializeObject(inpContainer);
|
||||
const sigBytes = sodium.crypto_sign_detached(serlializedInpContainer, keys.privateKey);
|
||||
const sigBytes = sodium.crypto_sign_detached(serlializedInpContainer, keys.privateKey.slice(1));
|
||||
|
||||
const signedInpContainer = {
|
||||
type: "contract_input",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const fs = require('fs');
|
||||
const tty = require('tty');
|
||||
require('process');
|
||||
|
||||
const MAX_SEQ_PACKET_SIZE = 128 * 1024;
|
||||
const controlMessages = {
|
||||
@@ -18,81 +17,89 @@ Object.freeze(clientProtocols);
|
||||
const PATCH_CONFIG_PATH = "../patch.cfg";
|
||||
const POST_EXEC_SCRIPT_NAME = "post_exec.sh";
|
||||
|
||||
/*
|
||||
* Class members prefixed with '__' represent private members until ES spec fully supports '#' notation.
|
||||
*/
|
||||
|
||||
class HotPocketContract {
|
||||
|
||||
#controlChannel = null;
|
||||
#clientProtocol = null;
|
||||
constructor() {
|
||||
this.__controlChannel = null;
|
||||
this.__clientProtocol = null;
|
||||
|
||||
this.__executeContract = (hpargs, contractFunc) => {
|
||||
// Keeps track of all the tasks (promises) that must be awaited before the termination.
|
||||
const pendingTasks = [];
|
||||
const nplChannel = new NplChannel(hpargs.npl_fd);
|
||||
|
||||
const users = new UsersCollection(hpargs.user_in_fd, hpargs.users, this.__clientProtocol);
|
||||
const unl = new UnlCollection(hpargs.readonly, hpargs.unl, nplChannel, pendingTasks);
|
||||
const executionContext = new ContractContext(hpargs, users, unl, this.__controlChannel);
|
||||
|
||||
invokeCallback(contractFunc, executionContext).catch(errHandler).finally(() => {
|
||||
// Wait for any pending tasks added during execution.
|
||||
Promise.all(pendingTasks).catch(errHandler).finally(() => {
|
||||
nplChannel.close();
|
||||
this.__terminate();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.__terminate = () => {
|
||||
this.__controlChannel.send({ type: controlMessages.contractEnd });
|
||||
this.__controlChannel.close();
|
||||
}
|
||||
}
|
||||
|
||||
init(contractFunc, clientProtocol = clientProtocols.json) {
|
||||
|
||||
if (this.#controlChannel) // Already initialized.
|
||||
return false;
|
||||
return new Promise(resolve => {
|
||||
if (this.__controlChannel) { // Already initialized.
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#clientProtocol = clientProtocol;
|
||||
this.__clientProtocol = clientProtocol;
|
||||
|
||||
// Check whether we are running on a console and provide error.
|
||||
if (tty.isatty(process.stdin.fd)) {
|
||||
console.error("Error: Hot Pocket smart contracts must be executed via Hot Pocket.");
|
||||
return false;
|
||||
}
|
||||
// Check whether we are running on a console and provide error.
|
||||
if (tty.isatty(process.stdin.fd)) {
|
||||
console.error("Error: Hot Pocket smart contracts must be executed via Hot Pocket.");
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse HotPocket args.
|
||||
const argsJson = fs.readFileSync(process.stdin.fd, 'utf8');
|
||||
const hpargs = JSON.parse(argsJson);
|
||||
|
||||
this.#controlChannel = new ControlChannel(hpargs.control_fd);
|
||||
this.#executeContract(hpargs, contractFunc);
|
||||
return true;
|
||||
}
|
||||
|
||||
#executeContract = (hpargs, contractFunc) => {
|
||||
// Keeps track of all the tasks (promises) that must be awaited before the termination.
|
||||
const pendingTasks = [];
|
||||
const nplChannel = new NplChannel(hpargs.npl_fd);
|
||||
|
||||
const users = new UsersCollection(hpargs.user_in_fd, hpargs.users, this.#clientProtocol);
|
||||
const unl = new UnlCollection(hpargs.readonly, hpargs.unl, nplChannel, pendingTasks);
|
||||
const executionContext = new ContractContext(hpargs, users, unl, this.#controlChannel);
|
||||
|
||||
invokeCallback(contractFunc, executionContext).catch(errHandler).finally(() => {
|
||||
// Wait for any pending tasks added during execution.
|
||||
Promise.all(pendingTasks).catch(errHandler).finally(() => {
|
||||
nplChannel.close();
|
||||
this.#terminate();
|
||||
// Parse HotPocket args.
|
||||
fs.readFile(process.stdin.fd, 'utf8', (err, argsJson) => {
|
||||
const hpargs = JSON.parse(argsJson);
|
||||
this.__controlChannel = new ControlChannel(hpargs.control_fd);
|
||||
this.__executeContract(hpargs, contractFunc);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#terminate = () => {
|
||||
this.#controlChannel.send({ type: controlMessages.contractEnd });
|
||||
this.#controlChannel.close();
|
||||
}
|
||||
}
|
||||
|
||||
class ContractContext {
|
||||
|
||||
#controlChannel = null;
|
||||
#patchConfig = null;
|
||||
|
||||
constructor(hpargs, users, unl, controlChannel) {
|
||||
this.#controlChannel = controlChannel;
|
||||
this.__controlChannel = controlChannel;
|
||||
this.readonly = hpargs.readonly;
|
||||
this.timestamp = hpargs.timestamp;
|
||||
this.users = users;
|
||||
this.unl = unl; // Not available in readonly mode.
|
||||
this.lcl_seq_no = hpargs.lcl_seq_no; // Not available in readonly mode.
|
||||
this.lcl_hash = hpargs.lcl_hash; // Not available in readonly mode.
|
||||
this.#patchConfig = new PatchConfig();
|
||||
this.__patchConfig = new PatchConfig();
|
||||
}
|
||||
|
||||
// Returns the config values in patch config.
|
||||
getConfig() {
|
||||
return this.#patchConfig.getConfig();
|
||||
return this.__patchConfig.getConfig();
|
||||
}
|
||||
|
||||
// Updates the config with given config object and save the patch config.
|
||||
updateConfig(config) {
|
||||
return this.#patchConfig.updateConfig(config);
|
||||
return this.__patchConfig.updateConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,11 +163,9 @@ class PatchConfig {
|
||||
|
||||
class UsersCollection {
|
||||
|
||||
#users = {};
|
||||
#infd = null;
|
||||
|
||||
constructor(userInputsFd, usersObj, clientProtocol) {
|
||||
this.#infd = userInputsFd;
|
||||
this.__users = {};
|
||||
this.__infd = userInputsFd;
|
||||
|
||||
Object.entries(usersObj).forEach(([pubKey, arr]) => {
|
||||
|
||||
@@ -168,55 +173,50 @@ class UsersCollection {
|
||||
arr.splice(0, 1); // Remove first element (output fd). The rest are pairs of msg offset/length tuples.
|
||||
|
||||
const channel = new UserChannel(outfd, clientProtocol);
|
||||
this.#users[pubKey] = new User(pubKey, channel, arr);
|
||||
this.__users[pubKey] = new User(pubKey, channel, arr);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns the User for the specified pubkey. Returns null if not found.
|
||||
find(pubKey) {
|
||||
return this.#users[pubKey]
|
||||
return this.__users[pubKey]
|
||||
}
|
||||
|
||||
// Returns all the currently connected users.
|
||||
list() {
|
||||
return Object.values(this.#users);
|
||||
return Object.values(this.__users);
|
||||
}
|
||||
|
||||
count() {
|
||||
return Object.keys(this.#users).length;
|
||||
return Object.keys(this.__users).length;
|
||||
}
|
||||
|
||||
async read(input) {
|
||||
const [offset, size] = input;
|
||||
const buf = Buffer.alloc(size);
|
||||
await readAsync(this.#infd, buf, offset, size);
|
||||
await readAsync(this.__infd, buf, offset, size);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
pubKey = null;
|
||||
inputs = null;
|
||||
#channel = null;
|
||||
|
||||
constructor(pubKey, channel, inputs) {
|
||||
this.pubKey = pubKey;
|
||||
this.inputs = inputs;
|
||||
this.#channel = channel;
|
||||
this.__channel = channel;
|
||||
}
|
||||
|
||||
async send(msg) {
|
||||
await this.#channel.send(msg);
|
||||
await this.__channel.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
class UserChannel {
|
||||
#outfd = -1;
|
||||
#clientProtocol = null;
|
||||
|
||||
constructor(outfd, clientProtocol) {
|
||||
this.#outfd = outfd;
|
||||
this.#clientProtocol = clientProtocol;
|
||||
this.__outfd = outfd;
|
||||
this.__clientProtocol = clientProtocol;
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
@@ -224,7 +224,7 @@ class UserChannel {
|
||||
let headerBuf = Buffer.alloc(4);
|
||||
// Writing message length in big endian format.
|
||||
headerBuf.writeUInt32BE(messageBuf.byteLength)
|
||||
return writevAsync(this.#outfd, [headerBuf, messageBuf]);
|
||||
return writevAsync(this.__outfd, [headerBuf, messageBuf]);
|
||||
}
|
||||
|
||||
serialize(msg) {
|
||||
@@ -235,7 +235,7 @@ class UserChannel {
|
||||
if (Buffer.isBuffer(msg))
|
||||
return msg;
|
||||
|
||||
if (this.#clientProtocol == clientProtocols.bson) {
|
||||
if (this.__clientProtocol == clientProtocols.bson) {
|
||||
return Buffer.from(msg);
|
||||
}
|
||||
else { // json
|
||||
@@ -250,21 +250,18 @@ class UserChannel {
|
||||
}
|
||||
|
||||
class UnlCollection {
|
||||
nodes = {};
|
||||
#channel = null;
|
||||
#readonly = false;
|
||||
#pendingTasks = null;
|
||||
|
||||
constructor(readonly, unl, channel, pendingTasks) {
|
||||
this.#readonly = readonly;
|
||||
this.#pendingTasks = pendingTasks;
|
||||
this.nodes = {};
|
||||
this.__readonly = readonly;
|
||||
this.__pendingTasks = pendingTasks;
|
||||
|
||||
if (!readonly) {
|
||||
unl.forEach(pubKey => {
|
||||
this.nodes[pubKey] = new UnlNode(pubKey);
|
||||
});
|
||||
|
||||
this.#channel = channel;
|
||||
this.__channel = channel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,26 +282,25 @@ class UnlCollection {
|
||||
// Registers for NPL messages.
|
||||
onMessage(callback) {
|
||||
|
||||
if (this.#readonly)
|
||||
if (this.__readonly)
|
||||
throw "NPL messages not available in readonly mode.";
|
||||
|
||||
this.#channel.consume((pubKey, msg) => {
|
||||
this.#pendingTasks.push(invokeCallback(callback, this.nodes[pubKey], msg));
|
||||
this.__channel.consume((pubKey, msg) => {
|
||||
this.__pendingTasks.push(invokeCallback(callback, this.nodes[pubKey], msg));
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcasts a message to all unl nodes (including self if self is part of unl).
|
||||
async send(msg) {
|
||||
if (this.#readonly)
|
||||
if (this.__readonly)
|
||||
throw "NPL messages not available in readonly mode.";
|
||||
|
||||
await this.#channel.send(msg);
|
||||
await this.__channel.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Represents a node that's part of unl.
|
||||
class UnlNode {
|
||||
pubKey = null;
|
||||
|
||||
constructor(pubKey) {
|
||||
this.pubKey = pubKey;
|
||||
@@ -314,23 +310,21 @@ class UnlNode {
|
||||
// Represents the node-party-line that can be used to communicate with unl nodes.
|
||||
class NplChannel {
|
||||
|
||||
#readStream = null;
|
||||
#fd = -1;
|
||||
|
||||
constructor(fd) {
|
||||
this.#fd = fd;
|
||||
this.__fd = fd;
|
||||
this.__readStream = null;
|
||||
}
|
||||
|
||||
consume(onMessage) {
|
||||
|
||||
this.#readStream = fs.createReadStream(null, { fd: this.#fd, highWaterMark: MAX_SEQ_PACKET_SIZE });
|
||||
this.__readStream = fs.createReadStream(null, { fd: this.__fd, highWaterMark: MAX_SEQ_PACKET_SIZE });
|
||||
|
||||
// From the hotpocket when sending the npl messages first it sends the pubkey of the particular node
|
||||
// and then the message, First data buffer is taken as pubkey and the second one as message,
|
||||
// then npl message object is constructed and the event is emmited.
|
||||
let pubKey = null;
|
||||
|
||||
this.#readStream.on("data", (data) => {
|
||||
this.__readStream.on("data", (data) => {
|
||||
if (!pubKey) {
|
||||
pubKey = data.toString();
|
||||
}
|
||||
@@ -340,46 +334,44 @@ class NplChannel {
|
||||
}
|
||||
});
|
||||
|
||||
this.#readStream.on("error", (err) => { });
|
||||
this.__readStream.on("error", (err) => { });
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
const buf = Buffer.from(msg);
|
||||
if (buf.length > MAX_SEQ_PACKET_SIZE)
|
||||
throw ("NPL message exceeds max size " + MAX_SEQ_PACKET_SIZE);
|
||||
return writeAsync(this.#fd, buf);
|
||||
return writeAsync(this.__fd, buf);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.#readStream && this.#readStream.close();
|
||||
this.__readStream && this.__readStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ControlChannel {
|
||||
|
||||
#readStream = null;
|
||||
#fd = -1;
|
||||
|
||||
constructor(fd) {
|
||||
this.#fd = fd;
|
||||
this.__fd = fd;
|
||||
this.__readStream = null;
|
||||
}
|
||||
|
||||
consume(onMessage) {
|
||||
this.#readStream = fs.createReadStream(null, { fd: this.#fd, highWaterMark: MAX_SEQ_PACKET_SIZE });
|
||||
this.#readStream.on("data", onMessage);
|
||||
this.#readStream.on("error", (err) => { });
|
||||
this.__readStream = fs.createReadStream(null, { fd: this.__fd, highWaterMark: MAX_SEQ_PACKET_SIZE });
|
||||
this.__readStream.on("data", onMessage);
|
||||
this.__readStream.on("error", (err) => { });
|
||||
}
|
||||
|
||||
send(obj) {
|
||||
const buf = Buffer.from(JSON.stringify(obj));
|
||||
if (buf.length > MAX_SEQ_PACKET_SIZE)
|
||||
throw ("Control message exceeds max size " + MAX_SEQ_PACKET_SIZE);
|
||||
return writeAsync(this.#fd, buf);
|
||||
return writeAsync(this.__fd, buf);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.#readStream && this.#readStream.close();
|
||||
this.__readStream && this.__readStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user