You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
519 lines
15 KiB
519 lines
15 KiB
"use strict"; |
|
var __defProp = Object.defineProperty; |
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; |
|
var __getOwnPropNames = Object.getOwnPropertyNames; |
|
var __hasOwnProp = Object.prototype.hasOwnProperty; |
|
var __export = (target, all) => { |
|
for (var name in all) |
|
__defProp(target, name, { get: all[name], enumerable: true }); |
|
}; |
|
var __copyProps = (to, from, except, desc) => { |
|
if (from && typeof from === "object" || typeof from === "function") { |
|
for (let key of __getOwnPropNames(from)) |
|
if (!__hasOwnProp.call(to, key) && key !== except) |
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); |
|
} |
|
return to; |
|
}; |
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); |
|
|
|
// nip77.ts |
|
var nip77_exports = {}; |
|
__export(nip77_exports, { |
|
Negentropy: () => Negentropy, |
|
NegentropyStorageVector: () => NegentropyStorageVector, |
|
NegentropySync: () => NegentropySync |
|
}); |
|
module.exports = __toCommonJS(nip77_exports); |
|
var import_utils = require("@noble/ciphers/utils"); |
|
var import_sha256 = require("@noble/hashes/sha256"); |
|
var PROTOCOL_VERSION = 97; |
|
var ID_SIZE = 32; |
|
var FINGERPRINT_SIZE = 16; |
|
var Mode = { |
|
Skip: 0, |
|
Fingerprint: 1, |
|
IdList: 2 |
|
}; |
|
var WrappedBuffer = class { |
|
_raw; |
|
length; |
|
constructor(buffer) { |
|
if (typeof buffer === "number") { |
|
this._raw = new Uint8Array(buffer); |
|
this.length = 0; |
|
} else if (buffer instanceof Uint8Array) { |
|
this._raw = new Uint8Array(buffer); |
|
this.length = buffer.length; |
|
} else { |
|
this._raw = new Uint8Array(512); |
|
this.length = 0; |
|
} |
|
} |
|
unwrap() { |
|
return this._raw.subarray(0, this.length); |
|
} |
|
get capacity() { |
|
return this._raw.byteLength; |
|
} |
|
extend(buf) { |
|
if (buf instanceof WrappedBuffer) |
|
buf = buf.unwrap(); |
|
if (typeof buf.length !== "number") |
|
throw Error("bad length"); |
|
const targetSize = buf.length + this.length; |
|
if (this.capacity < targetSize) { |
|
const oldRaw = this._raw; |
|
const newCapacity = Math.max(this.capacity * 2, targetSize); |
|
this._raw = new Uint8Array(newCapacity); |
|
this._raw.set(oldRaw); |
|
} |
|
this._raw.set(buf, this.length); |
|
this.length += buf.length; |
|
} |
|
shift() { |
|
const first = this._raw[0]; |
|
this._raw = this._raw.subarray(1); |
|
this.length--; |
|
return first; |
|
} |
|
shiftN(n = 1) { |
|
const firstSubarray = this._raw.subarray(0, n); |
|
this._raw = this._raw.subarray(n); |
|
this.length -= n; |
|
return firstSubarray; |
|
} |
|
}; |
|
function decodeVarInt(buf) { |
|
let res = 0; |
|
while (1) { |
|
if (buf.length === 0) |
|
throw Error("parse ends prematurely"); |
|
let byte = buf.shift(); |
|
res = res << 7 | byte & 127; |
|
if ((byte & 128) === 0) |
|
break; |
|
} |
|
return res; |
|
} |
|
function encodeVarInt(n) { |
|
if (n === 0) |
|
return new WrappedBuffer(new Uint8Array([0])); |
|
let o = []; |
|
while (n !== 0) { |
|
o.push(n & 127); |
|
n >>>= 7; |
|
} |
|
o.reverse(); |
|
for (let i = 0; i < o.length - 1; i++) |
|
o[i] |= 128; |
|
return new WrappedBuffer(new Uint8Array(o)); |
|
} |
|
function getByte(buf) { |
|
return getBytes(buf, 1)[0]; |
|
} |
|
function getBytes(buf, n) { |
|
if (buf.length < n) |
|
throw Error("parse ends prematurely"); |
|
return buf.shiftN(n); |
|
} |
|
var Accumulator = class { |
|
buf; |
|
constructor() { |
|
this.setToZero(); |
|
} |
|
setToZero() { |
|
this.buf = new Uint8Array(ID_SIZE); |
|
} |
|
add(otherBuf) { |
|
let currCarry = 0, nextCarry = 0; |
|
let p = new DataView(this.buf.buffer); |
|
let po = new DataView(otherBuf.buffer); |
|
for (let i = 0; i < 8; i++) { |
|
let offset = i * 4; |
|
let orig = p.getUint32(offset, true); |
|
let otherV = po.getUint32(offset, true); |
|
let next = orig; |
|
next += currCarry; |
|
next += otherV; |
|
if (next > 4294967295) |
|
nextCarry = 1; |
|
p.setUint32(offset, next & 4294967295, true); |
|
currCarry = nextCarry; |
|
nextCarry = 0; |
|
} |
|
} |
|
negate() { |
|
let p = new DataView(this.buf.buffer); |
|
for (let i = 0; i < 8; i++) { |
|
let offset = i * 4; |
|
p.setUint32(offset, ~p.getUint32(offset, true)); |
|
} |
|
let one = new Uint8Array(ID_SIZE); |
|
one[0] = 1; |
|
this.add(one); |
|
} |
|
getFingerprint(n) { |
|
let input = new WrappedBuffer(); |
|
input.extend(this.buf); |
|
input.extend(encodeVarInt(n)); |
|
let hash = (0, import_sha256.sha256)(input.unwrap()); |
|
return hash.subarray(0, FINGERPRINT_SIZE); |
|
} |
|
}; |
|
var NegentropyStorageVector = class { |
|
items; |
|
sealed; |
|
constructor() { |
|
this.items = []; |
|
this.sealed = false; |
|
} |
|
insert(timestamp, id) { |
|
if (this.sealed) |
|
throw Error("already sealed"); |
|
const idb = (0, import_utils.hexToBytes)(id); |
|
if (idb.byteLength !== ID_SIZE) |
|
throw Error("bad id size for added item"); |
|
this.items.push({ timestamp, id: idb }); |
|
} |
|
seal() { |
|
if (this.sealed) |
|
throw Error("already sealed"); |
|
this.sealed = true; |
|
this.items.sort(itemCompare); |
|
for (let i = 1; i < this.items.length; i++) { |
|
if (itemCompare(this.items[i - 1], this.items[i]) === 0) |
|
throw Error("duplicate item inserted"); |
|
} |
|
} |
|
unseal() { |
|
this.sealed = false; |
|
} |
|
size() { |
|
this._checkSealed(); |
|
return this.items.length; |
|
} |
|
getItem(i) { |
|
this._checkSealed(); |
|
if (i >= this.items.length) |
|
throw Error("out of range"); |
|
return this.items[i]; |
|
} |
|
iterate(begin, end, cb) { |
|
this._checkSealed(); |
|
this._checkBounds(begin, end); |
|
for (let i = begin; i < end; ++i) { |
|
if (!cb(this.items[i], i)) |
|
break; |
|
} |
|
} |
|
findLowerBound(begin, end, bound) { |
|
this._checkSealed(); |
|
this._checkBounds(begin, end); |
|
return this._binarySearch(this.items, begin, end, (a) => itemCompare(a, bound) < 0); |
|
} |
|
fingerprint(begin, end) { |
|
let out = new Accumulator(); |
|
out.setToZero(); |
|
this.iterate(begin, end, (item) => { |
|
out.add(item.id); |
|
return true; |
|
}); |
|
return out.getFingerprint(end - begin); |
|
} |
|
_checkSealed() { |
|
if (!this.sealed) |
|
throw Error("not sealed"); |
|
} |
|
_checkBounds(begin, end) { |
|
if (begin > end || end > this.items.length) |
|
throw Error("bad range"); |
|
} |
|
_binarySearch(arr, first, last, cmp) { |
|
let count = last - first; |
|
while (count > 0) { |
|
let it = first; |
|
let step = Math.floor(count / 2); |
|
it += step; |
|
if (cmp(arr[it])) { |
|
first = ++it; |
|
count -= step + 1; |
|
} else { |
|
count = step; |
|
} |
|
} |
|
return first; |
|
} |
|
}; |
|
var Negentropy = class { |
|
storage; |
|
frameSizeLimit; |
|
lastTimestampIn; |
|
lastTimestampOut; |
|
constructor(storage, frameSizeLimit = 6e4) { |
|
if (frameSizeLimit < 4096) |
|
throw Error("frameSizeLimit too small"); |
|
this.storage = storage; |
|
this.frameSizeLimit = frameSizeLimit; |
|
this.lastTimestampIn = 0; |
|
this.lastTimestampOut = 0; |
|
} |
|
_bound(timestamp, id) { |
|
return { timestamp, id: id || new Uint8Array(0) }; |
|
} |
|
initiate() { |
|
let output = new WrappedBuffer(); |
|
output.extend(new Uint8Array([PROTOCOL_VERSION])); |
|
this.splitRange(0, this.storage.size(), this._bound(Number.MAX_VALUE), output); |
|
return (0, import_utils.bytesToHex)(output.unwrap()); |
|
} |
|
reconcile(queryMsg, onhave, onneed) { |
|
const query = new WrappedBuffer((0, import_utils.hexToBytes)(queryMsg)); |
|
this.lastTimestampIn = this.lastTimestampOut = 0; |
|
let fullOutput = new WrappedBuffer(); |
|
fullOutput.extend(new Uint8Array([PROTOCOL_VERSION])); |
|
let protocolVersion = getByte(query); |
|
if (protocolVersion < 96 || protocolVersion > 111) |
|
throw Error("invalid negentropy protocol version byte"); |
|
if (protocolVersion !== PROTOCOL_VERSION) { |
|
throw Error("unsupported negentropy protocol version requested: " + (protocolVersion - 96)); |
|
} |
|
let storageSize = this.storage.size(); |
|
let prevBound = this._bound(0); |
|
let prevIndex = 0; |
|
let skip = false; |
|
while (query.length !== 0) { |
|
let o = new WrappedBuffer(); |
|
let doSkip = () => { |
|
if (skip) { |
|
skip = false; |
|
o.extend(this.encodeBound(prevBound)); |
|
o.extend(encodeVarInt(Mode.Skip)); |
|
} |
|
}; |
|
let currBound = this.decodeBound(query); |
|
let mode = decodeVarInt(query); |
|
let lower = prevIndex; |
|
let upper = this.storage.findLowerBound(prevIndex, storageSize, currBound); |
|
if (mode === Mode.Skip) { |
|
skip = true; |
|
} else if (mode === Mode.Fingerprint) { |
|
let theirFingerprint = getBytes(query, FINGERPRINT_SIZE); |
|
let ourFingerprint = this.storage.fingerprint(lower, upper); |
|
if (compareUint8Array(theirFingerprint, ourFingerprint) !== 0) { |
|
doSkip(); |
|
this.splitRange(lower, upper, currBound, o); |
|
} else { |
|
skip = true; |
|
} |
|
} else if (mode === Mode.IdList) { |
|
let numIds = decodeVarInt(query); |
|
let theirElems = {}; |
|
for (let i = 0; i < numIds; i++) { |
|
let e = getBytes(query, ID_SIZE); |
|
theirElems[(0, import_utils.bytesToHex)(e)] = e; |
|
} |
|
skip = true; |
|
this.storage.iterate(lower, upper, (item) => { |
|
let k = item.id; |
|
const id = (0, import_utils.bytesToHex)(k); |
|
if (!theirElems[id]) { |
|
onhave?.(id); |
|
} else { |
|
delete theirElems[(0, import_utils.bytesToHex)(k)]; |
|
} |
|
return true; |
|
}); |
|
if (onneed) { |
|
for (let v of Object.values(theirElems)) { |
|
onneed((0, import_utils.bytesToHex)(v)); |
|
} |
|
} |
|
} else { |
|
throw Error("unexpected mode"); |
|
} |
|
if (this.exceededFrameSizeLimit(fullOutput.length + o.length)) { |
|
let remainingFingerprint = this.storage.fingerprint(upper, storageSize); |
|
fullOutput.extend(this.encodeBound(this._bound(Number.MAX_VALUE))); |
|
fullOutput.extend(encodeVarInt(Mode.Fingerprint)); |
|
fullOutput.extend(remainingFingerprint); |
|
break; |
|
} else { |
|
fullOutput.extend(o); |
|
} |
|
prevIndex = upper; |
|
prevBound = currBound; |
|
} |
|
return fullOutput.length === 1 ? null : (0, import_utils.bytesToHex)(fullOutput.unwrap()); |
|
} |
|
splitRange(lower, upper, upperBound, o) { |
|
let numElems = upper - lower; |
|
let buckets = 16; |
|
if (numElems < buckets * 2) { |
|
o.extend(this.encodeBound(upperBound)); |
|
o.extend(encodeVarInt(Mode.IdList)); |
|
o.extend(encodeVarInt(numElems)); |
|
this.storage.iterate(lower, upper, (item) => { |
|
o.extend(item.id); |
|
return true; |
|
}); |
|
} else { |
|
let itemsPerBucket = Math.floor(numElems / buckets); |
|
let bucketsWithExtra = numElems % buckets; |
|
let curr = lower; |
|
for (let i = 0; i < buckets; i++) { |
|
let bucketSize = itemsPerBucket + (i < bucketsWithExtra ? 1 : 0); |
|
let ourFingerprint = this.storage.fingerprint(curr, curr + bucketSize); |
|
curr += bucketSize; |
|
let nextBound; |
|
if (curr === upper) { |
|
nextBound = upperBound; |
|
} else { |
|
let prevItem; |
|
let currItem; |
|
this.storage.iterate(curr - 1, curr + 1, (item, index) => { |
|
if (index === curr - 1) |
|
prevItem = item; |
|
else |
|
currItem = item; |
|
return true; |
|
}); |
|
nextBound = this.getMinimalBound(prevItem, currItem); |
|
} |
|
o.extend(this.encodeBound(nextBound)); |
|
o.extend(encodeVarInt(Mode.Fingerprint)); |
|
o.extend(ourFingerprint); |
|
} |
|
} |
|
} |
|
exceededFrameSizeLimit(n) { |
|
return n > this.frameSizeLimit - 200; |
|
} |
|
decodeTimestampIn(encoded) { |
|
let timestamp = decodeVarInt(encoded); |
|
timestamp = timestamp === 0 ? Number.MAX_VALUE : timestamp - 1; |
|
if (this.lastTimestampIn === Number.MAX_VALUE || timestamp === Number.MAX_VALUE) { |
|
this.lastTimestampIn = Number.MAX_VALUE; |
|
return Number.MAX_VALUE; |
|
} |
|
timestamp += this.lastTimestampIn; |
|
this.lastTimestampIn = timestamp; |
|
return timestamp; |
|
} |
|
decodeBound(encoded) { |
|
let timestamp = this.decodeTimestampIn(encoded); |
|
let len = decodeVarInt(encoded); |
|
if (len > ID_SIZE) |
|
throw Error("bound key too long"); |
|
let id = getBytes(encoded, len); |
|
return { timestamp, id }; |
|
} |
|
encodeTimestampOut(timestamp) { |
|
if (timestamp === Number.MAX_VALUE) { |
|
this.lastTimestampOut = Number.MAX_VALUE; |
|
return encodeVarInt(0); |
|
} |
|
let temp = timestamp; |
|
timestamp -= this.lastTimestampOut; |
|
this.lastTimestampOut = temp; |
|
return encodeVarInt(timestamp + 1); |
|
} |
|
encodeBound(key) { |
|
let output = new WrappedBuffer(); |
|
output.extend(this.encodeTimestampOut(key.timestamp)); |
|
output.extend(encodeVarInt(key.id.length)); |
|
output.extend(key.id); |
|
return output; |
|
} |
|
getMinimalBound(prev, curr) { |
|
if (curr.timestamp !== prev.timestamp) { |
|
return this._bound(curr.timestamp); |
|
} else { |
|
let sharedPrefixBytes = 0; |
|
let currKey = curr.id; |
|
let prevKey = prev.id; |
|
for (let i = 0; i < ID_SIZE; i++) { |
|
if (currKey[i] !== prevKey[i]) |
|
break; |
|
sharedPrefixBytes++; |
|
} |
|
return this._bound(curr.timestamp, curr.id.subarray(0, sharedPrefixBytes + 1)); |
|
} |
|
} |
|
}; |
|
function compareUint8Array(a, b) { |
|
for (let i = 0; i < a.byteLength; i++) { |
|
if (a[i] < b[i]) |
|
return -1; |
|
if (a[i] > b[i]) |
|
return 1; |
|
} |
|
if (a.byteLength > b.byteLength) |
|
return 1; |
|
if (a.byteLength < b.byteLength) |
|
return -1; |
|
return 0; |
|
} |
|
function itemCompare(a, b) { |
|
if (a.timestamp === b.timestamp) { |
|
return compareUint8Array(a.id, b.id); |
|
} |
|
return a.timestamp - b.timestamp; |
|
} |
|
var NegentropySync = class { |
|
relay; |
|
storage; |
|
neg; |
|
filter; |
|
subscription; |
|
onhave; |
|
onneed; |
|
constructor(relay, storage, filter, params = {}) { |
|
this.relay = relay; |
|
this.storage = storage; |
|
this.neg = new Negentropy(storage); |
|
this.onhave = params.onhave; |
|
this.onneed = params.onneed; |
|
this.filter = filter; |
|
this.subscription = this.relay.prepareSubscription([{}], { label: params.label || "negentropy" }); |
|
this.subscription.oncustom = (data) => { |
|
switch (data[0]) { |
|
case "NEG-MSG": { |
|
if (data.length < 3) { |
|
console.warn(`got invalid NEG-MSG from ${this.relay.url}: ${data}`); |
|
} |
|
try { |
|
const response = this.neg.reconcile(data[2], this.onhave, this.onneed); |
|
if (response) { |
|
this.relay.send(`["NEG-MSG", "${this.subscription.id}", "${response}"]`); |
|
} else { |
|
this.close(); |
|
params.onclose?.(); |
|
} |
|
} catch (error) { |
|
console.error("negentropy reconcile error:", error); |
|
params?.onclose?.(`reconcile error: ${error}`); |
|
} |
|
break; |
|
} |
|
case "NEG-CLOSE": { |
|
const reason = data[2]; |
|
console.warn("negentropy error:", reason); |
|
params.onclose?.(reason); |
|
break; |
|
} |
|
case "NEG-ERR": { |
|
params.onclose?.(); |
|
} |
|
} |
|
}; |
|
} |
|
async start() { |
|
const initMsg = this.neg.initiate(); |
|
this.relay.send(`["NEG-OPEN","${this.subscription.id}",${JSON.stringify(this.filter)},"${initMsg}"]`); |
|
} |
|
close() { |
|
this.relay.send(`["NEG-CLOSE","${this.subscription.id}"]`); |
|
this.subscription.close(); |
|
} |
|
};
|
|
|