Commit f813ab6b authored by John Doe's avatar John Doe
Browse files

🟣

parent 671a85fd
# Private key used for publishing
RPC_URL=
# RPC that supports eth_getLogs
RPC_URL=https://rpc.mevblocker.io/
# Private key
# If private key is filled, it will be used for both deposits and publishing -- be careful if you are doing both
PRIVATE_KEY=
THREADS=0 # Use all available threads
......@@ -23,4 +24,7 @@ RELAYS=
# Nostr Relay APIs ("Seed" nodes) -- expects an array returned
MAX_RELAYS=8
SEEDS=https://api.nostr.watch/v1/online
\ No newline at end of file
SEEDS=https://api.nostr.watch/v1/online
# Your public ip in case you are using a tunneling service
PUBLIC_IP=
\ No newline at end of file
......@@ -24,3 +24,4 @@ yarn-error.log*
# Ignore OS generated files
.DS_Store
Thumbs.db
package-lock.json
\ No newline at end of file
[submodule "momiji-helpers"]
path = momiji-helpers
url = http://open.offshift.io/greybeard/momiji-helpers.git
FROM node:20.0
FROM node:20.2
RUN mkdir /code && mkdir /code/node
WORKDIR /code/node
RUN wget https://github.com/noir-lang/noir/releases/download/v0.28.0/nargo-x86_64-unknown-linux-gnu.tar.gz
......@@ -9,5 +9,9 @@ COPY . .
RUN npm install
RUN cd ./momiji-helpers/ && npm install
WORKDIR /code/node
ENTRYPOINT [ "npm", "start"]
RUN apt update && apt install -y tor libc++-dev
RUN echo "HiddenServiceDir /var/lib/tor/hidden_service/" >> /etc/tor/torrc
RUN echo "HiddenServicePort 80 127.0.0.1:5150" >> /etc/tor/torrc
RUN service tor start
RUN update-rc.d tor enable
ENTRYPOINT [ "npm", "start" ]
......@@ -24,7 +24,7 @@ sudo docker build -t xftnode .
for running
```sh
sudo docker run -it xftnode
sudo docker run -p 5150:5150 -it xftnode
```
### npm usage
- Install noirup, the Nargo package manager
......
// @ts-ignore
import * as types from './types';
import { TransactionBuilder } from "./transactionBuilder";
import { createLibp2p } from 'libp2p'
import { webSockets } from '@libp2p/websockets'
import * as filters from '@libp2p/websockets/filters'
// import { createLibp2p } from 'libp2p'
// import * as filters from '@libp2p/websockets/filters'
import { finalizeEvent, verifyEvent, setNostrWasm, VerifiedEvent, Event } from 'nostr-tools/wasm'
import { SimplePool } from 'nostr-tools/pool'
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { mplex } from '@libp2p/mplex'
import { identify } from "@libp2p/identify"
import { dcutr } from "@libp2p/dcutr"
// import { gossipsub } from '@chainsafe/libp2p-gossipsub'
// import { noise } from '@chainsafe/libp2p-noise'
// import { yamux } from '@chainsafe/libp2p-yamux'
// import { mplex } from '@libp2p/mplex'
// import { identify } from "@libp2p/identify"
// import { dcutr } from "@libp2p/dcutr"
import { getPublicKey } from 'nostr-tools/pure';
import { initNostrWasm } from 'nostr-wasm'
import { multiaddr } from '@multiformats/multiaddr'
// import { multiaddr } from '@multiformats/multiaddr'
import { useWebSocketImplementation } from 'nostr-tools/pool'
import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery'
import WebSocket from 'ws'
import { Block } from 'ethers';
if (typeof global === "object") useWebSocketImplementation(WebSocket);
export class Peering extends TransactionBuilder {
p2p: any | undefined;
pool: SimplePool = new SimplePool();
// p2p: any | undefined;
pool: SimplePool = new SimplePool();
relays: string[];
seeds: string[];
rebroadcast: any | undefined;
callback: Function;
// callback: Function;
constructor(_config: types.GlobalConfig) {
super(_config);
if (!this.config.gossip) throw new Error("Gossip config not found");
// if (!this.config.gossip) throw new Error("Gossip config not found");
this.pool = new SimplePool();
this.relays = this.config.gossip!.relays;
this.seeds = this.config.gossip!.seeds;
this.callback = () => true;
// this.callback = () => true;
}
postEvent = async (_sk: Uint8Array, _timestamp: number) => Promise.any(this.pool.publish(this.relays, finalizeEvent({
postEvent = async (_sk: Uint8Array, _timestamp: number, ips: string) => Promise.any(this.pool.publish(this.relays, finalizeEvent({
kind: 1,
created_at: _timestamp,
tags: [],
content: this.p2p.getMultiaddrs().map((addr: any) => multiaddr(addr).toString()).join(',')
content: ips
}, _sk)))
postTransaction = async (_tx: types.Transaction) => {
const hexString = (await this.contracts.state.getAddress()).slice(2).padStart(64, '0');
const matches = hexString.match(/.{1,2}/g);
const skUint8Array = matches ? new Uint8Array(matches.map(byte => parseInt(byte, 16))) : new Uint8Array();
const _pk: string = getPublicKey(skUint8Array);
if (this.p2p.services.pubsub.getSubscribers(_pk).length > 0) {
await this.p2p.services.pubsub.publish(_pk, new TextEncoder().encode(JSON.stringify(_tx)))
} else {
setTimeout(async () => await this.postTransaction(_tx), 10_000);
}
};
// postTransaction = async (_tx: types.Transaction) => {
// const hexString = (await this.contracts.state.getAddress()).slice(2).padStart(64, '0');
// const matches = hexString.match(/.{1,2}/g);
// const skUint8Array = matches ? new Uint8Array(matches.map(byte => parseInt(byte, 16))) : new Uint8Array();
// const _pk: string = getPublicKey(skUint8Array);
// if (this.p2p.services.pubsub.getSubscribers(_pk).length > 0) {
// await this.p2p.services.pubsub.publish(_pk, new TextEncoder().encode(JSON.stringify(_tx)))
// } else {
// setTimeout(async () => await this.postTransaction(_tx), 10_000);
// }
// };
initializePeering = async (callback?: Function): Promise<void> => {
await this.initializeTransactionBuilder();
const hexString = (await this.contracts.state.getAddress()).slice(2).padStart(64, '0');
const hexString = "6835ccbeddc1c1c91fed2116099592a309f98d0df34d8dc72e770df83cc3b065".padStart(64, '0');
const matches = hexString.match(/.{1,2}/g);
const skUint8Array = matches ? new Uint8Array(matches.map(byte => parseInt(byte, 16))) : new Uint8Array();
const skUint8Array = matches ? new Uint8Array(matches.map(byte => parseInt(byte, 16))) : new Uint8Array()
let _pk = getPublicKey(skUint8Array);
await this.fetchRelays();
await initNostrWasm().then(setNostrWasm);
const latestBlock = await this.config.provider.getBlock("latest");
let multiAddrs: string[] = [];
this.p2p = await createLibp2p({
addresses: {
listen: (typeof global === "object") ? ['/ip4/0.0.0.0/tcp/5150/ws'] : []
},
transports: [webSockets({
filter: filters.all
})],
connectionEncryption: [noise()],
streamMuxers: [yamux(), mplex()],
services: {
pubsub: gossipsub(),
identify: identify(),
dcutr: dcutr()
},
connectionGater: {
denyDialMultiaddr: () => {
return false
}
},
peerDiscovery: [
pubsubPeerDiscovery({
interval: 10_000
})
]
});
await this.p2p.start()
this.p2p.services.pubsub.subscribe(_pk)
await this.p2p.services.pubsub.start()
this.p2p.addEventListener('connection:open', async (event: CustomEvent) => {
console.log("peering.ts - connection open", event.detail.id.toString())
})
this.p2p.addEventListener('connection:close', () => {
console.log("peering.ts - connection closed")
})
this.p2p.addEventListener("peer:connect", async (event: CustomEvent) => {
console.log("peering.ts - peer connect")
})
this.p2p.addEventListener("peer:discovery", (event: CustomEvent) => {
console.log("peering.ts - peer discovered:", event.detail.id.toString())
})
if (typeof global === "object") {
try {
this.pool.subscribeMany(
this.relays.slice(0, this.config.gossip!.maxRelays),
[
{
kinds: [1],
authors: [_pk],
since: latestBlock!.timestamp
},
],
{
onevent: async (event) => event.content.split(',')
.filter(addr => {
try {
multiaddr(addr);
return true;
} catch (error) {
return false;
}
})
.forEach(async (addr) => this.addPeer(addr)),
oneose: async () => true,
onclose: async () => true,
}
)
} catch (error) {
let ip: string
if (this.config.ip == "") {
let ips: string[] = []
let ipServices = ["https://api.ipify.org", "https://icanhazip.com", "https://ifconfig.me/ip", "https://checkip.amazonaws.com/"]
for (let i = 0; i < ipServices.length; i++) {
let res = await fetch(ipServices[i])
ips.push((await res.text()).trim())
}
} else {
try {
const events: Event[] = await this.pool.querySync(this.relays.slice(0, this.config.gossip!.maxRelays),
{
kinds: [1],
authors: [_pk],
since: latestBlock!.timestamp - (typeof global === 'object' ? 0 : 86400)
}
)
console.log("peering.ts - nostr events", events)
multiAddrs = events.map(event => event.content.split(","))
.flat(1)
.filter(addr => {
try {
multiaddr(addr);
return true;
} catch (error) {
return false;
}
})
for (let addr in multiAddrs) await this.addPeer(multiAddrs[addr]);
} catch (error) {
console.log("error, peering.ts - initializePeering(), pool.querySync()", error)
let last = ips[0]
for (let i = 0; i < ips.length; i++) {
if (last === ips[i]) {
last = ips[i]
} else {
console.log("Please set your public ip in the config file")
return
}
}
ip = last
}else {
ip = this.config.ip as string
}
if (typeof global === 'object') {
this.rebroadcast = setInterval(async () => await this.postEvent(skUint8Array, latestBlock!.timestamp), 60 * 60 * 1000);
await this.postEvent(skUint8Array, latestBlock!.timestamp);
this.p2p.services.pubsub.addEventListener('message', (message: CustomEvent) => {
if (!(message.detail.topic == _pk) || !callback) return;
callback(JSON.parse(new TextDecoder().decode(message.detail.data)));
})
this.rebroadcast = setInterval(async () => {
await this.postEvent(skUint8Array, latestBlock!.timestamp, ip)
}, 60 * 60 * 1000);
await this.postEvent(skUint8Array, latestBlock!.timestamp, ip);
}
}
addPeer = async (peerMultiaddr: string): Promise<void> => {
if (!this.p2p) {
throw new Error('P2P is not initialized');
}
try {
await this.p2p.dial(multiaddr(peerMultiaddr));
console.log(`Successfully connected to peer: ${peerMultiaddr}`);
} catch (error) {
}
};
queryEvents = async (latestBlock: number) => {
const hexString = "6835ccbeddc1c1c91fed2116099592a309f98d0df34d8dc72e770df83cc3b065".padStart(64, '0');
const matches = hexString.match(/.{1,2}/g);
const skUint8Array = matches ? new Uint8Array(matches.map(byte => parseInt(byte, 16))) : new Uint8Array()
let _pk = getPublicKey(skUint8Array);
let event = await this.pool.querySync(this.relays, {
kinds: [1],
authors: [_pk],
since: latestBlock - (typeof global === 'object' ? 0 : 86400)
})
}
fetchRelays = async (refresh: boolean = false): Promise<string[]> => {
if (this.seeds.length == 0 || refresh) return this.relays;
for (const seed of this.seeds) {
......
......@@ -88,6 +88,7 @@ export type GlobalConfig = {
withdrawal?: string,
verbose?: boolean,
threads?: number,
ip?: string,
provider: Provider
}
......
......@@ -2,6 +2,7 @@ import { Publisher } from "./modules/momiji";
import * as types from "../momiji-helpers/utils/types";
import { ethers } from "ethers";
import 'dotenv/config';
import { WebSocketServer } from "ws";
const main = async () => {
let _globalConfig: types.GlobalConfig = {
......@@ -18,10 +19,50 @@ const main = async () => {
verbose: (process.env.VERBOSE) ? (process.env.VERBOSE.toLowerCase() === "true") : false,
profit: (process.env.PROFIT) ? parseInt(process.env.PROFIT) : 0
};
let publisher = new Publisher(_globalConfig);
await publisher.initializePublisher();
const wss = new WebSocketServer({
host: '0.0.0.0',
port: 5150
});
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', async function message(data) {
if (publisher.queue.length >= 14){
ws.send('{status:"error", msg:"Prover queue is full"}')
return
}
const valid = await publisher.newTransactionReceivedMultiple(JSON.parse(data.toString()));
if (valid){
ws.send('{status:"ok", msg:"Proof added to queue"}');
} else {
ws.send('{status:"error", msg:"Invalid proof"}');
}
});
});
let queuing = false
const wenQueue = async() => {
if (!queuing && publisher.queue.length > 0) {
queuing = true;
await publisher.proveMultiple()
queuing = false;
}
}
let publishing = false
const wenPublish = async () => {
if (!publishing && !queuing && publisher.batch.length > 0) {
publishing = true
await publisher.publishSingle()
publishing = false
}
}
setInterval(wenQueue, 1e4)
setInterval(wenPublish, 1e5)
}
console.log(`💫 Starting publisher...`)
......
......@@ -4,19 +4,22 @@ import { keccak_tx } from '../../momiji-helpers/circuits/helpers/codegen/keccak_
import { tx_as_hash } from '../../momiji-helpers/circuits/helpers/codegen/tx_as_hash';
export class Publisher extends BatchBuilder {
private publishTimeout: NodeJS.Timeout;
// private publishTimeout: NodeJS.Timeout;
private signer: types.EthersSigner | undefined;
private mempool: types.Mempool = {};
private provingQueue: Promise<types.RecursionInputs[]> = Promise.resolve([]);
private batch: types.RecursionInputs[] = [];
public batch: types.RecursionInputs[] = [];
private confirmed: types.Confirmed = {};
private publishing: boolean = false;
private proving: boolean = false;
private sweeping: boolean = false;
public queue: types.Transaction[] = [];
public accumulator: string = types.ZERO_VALUE;
private contractPublish: types.ContractPublish = {} as types.ContractPublish;
constructor(config: types.GlobalConfig) {
super(config);
this.publishTimeout = setInterval(this.publish, 60 * 1000);
// this.publishTimeout = setInterval(this.publish, 60 * 1000);
this.mempool = {};
this.confirmed = {};
}
......@@ -60,12 +63,46 @@ export class Publisher extends BatchBuilder {
}
let txid = await keccak_tx(tx.public_inputs);
let _verified = await this.addToMempool(tx, txid)
if (_verified) this.queueToProve(tx)
else console.log(`🔴 Transaction rejected -- failed to verify: ${txid}`);
}
async newTransactionReceivedSingle(tx: types.Transaction): Promise<void> {
let txid = await keccak_tx(tx.public_inputs);
let _verified = await this.addToMempool(tx, txid)
if (_verified) {
await this.proveSingle(tx);
}
else console.log(`🔴 Transaction rejected -- failed to verify: ${txid}`);
}
async newTransactionReceivedMultiple(tx: types.Transaction): Promise<boolean> {
let txid = await keccak_tx(tx.public_inputs);
let _verified = await this.addToMempool(tx, txid)
if (_verified) {
this.queue.push(tx)
return true
}
else {
console.log(`🔴 Transaction rejected -- failed to verify: ${txid}`);
return false
};
}
async proveMultiple(): Promise<void> {
for (let i = 0; i < this.queue.length; i ++) {
if (this.batch.length >= 15) {
console.log(`⚠️ Batch is full. Cannot prove additional transactions.`)
this.queue = this.queue.slice(i)
return
}
this.batch = await this.proveSingle(this.queue[i])
}
this.queue = []
return
}
async setupListeners(): Promise<void> {
if (!this.contracts) await this.initializePublisher();
if (!this.contracts) return;
......@@ -104,7 +141,8 @@ export class Publisher extends BatchBuilder {
this.contracts.state.on(this.contracts.state.filters.BatchPublish(undefined, undefined, undefined, undefined, undefined), async (event: any) => {
console.log(`🎯 Batch published: ${event}.`);
this.provingQueue = Promise.resolve([]);
// this.provingQueue = Promise.resolve([]);
this.queue = []
this.batch = [];
this.printRoot();
return;
......@@ -131,7 +169,7 @@ export class Publisher extends BatchBuilder {
const withdrawals: types.WithdrawalSwap[] = await this._generateWithdrawals([{
amount: (new types.NoirFr(withdrawalAmount)).toString(),
recipient: types.toFixedHex(1, true),
swap_percentage: 100
swap_percentage: 100
}], 1)
const proverTx: types.Transaction = await this._generateTransactionProof(utxo_commitments, utxo_encrypted, withdrawals);
return proverTx;
......@@ -149,7 +187,13 @@ export class Publisher extends BatchBuilder {
})
const withdrawalAmount: bigint = utxo_commitments.map(utxo => BigInt(utxo.amount)).reduce((prev, curr) => prev + curr)
const withdrawalAmountEther: bigint = await this._getEtherFromXFT(withdrawalAmount)
const gasEstimate: bigint = await this.contracts.state.publish.estimateGas(contractPublish.proof, contractPublish.batch)
let gasEstimate: bigint
try {
gasEstimate = await this.contracts.state.publish.estimateGas(contractPublish.proof, contractPublish.batch)
} catch {
this.batch.pop()
return false
}
const maxFeePerGas: bigint = await this.config.provider.getFeeData().then(feeData => feeData.maxFeePerGas as bigint)
const maxFeePerGasAdjusted: bigint = (this.config.profit) ? maxFeePerGas + BigInt(this.config.profit * 1e9) : maxFeePerGas
const txFeeEstimate: bigint = gasEstimate * maxFeePerGasAdjusted
......@@ -189,6 +233,15 @@ export class Publisher extends BatchBuilder {
})
}
async proveSingle(tx: types.Transaction): Promise<types.RecursionInputs[]> {
this.proving = true;
console.log(`⏳ Proving ${await keccak_tx(tx.public_inputs)}.`)
const proof = await this.rollupTransaction(this.batch, tx);
console.log(`✔️ Proved ${await keccak_tx(tx.public_inputs)}.`)
this.proving = false
return proof
}
publish = async () => {
if (this.publishing) return;
......@@ -196,10 +249,9 @@ export class Publisher extends BatchBuilder {
if (this.sweeping) return;
if (this.batch.length === 0) return;
await this.provingQueue;
const contractPublish: types.ContractPublish = await this.preparePublish(this.batch)
if (!(await this.publishReady(this.batch, contractPublish))) return;
// await this.provingQueue;
// const contractPublish: types.ContractPublish = await this.preparePublish(this.batch)
// if (!(await this.publishReady(this.batch, contractPublish))) return;
console.log(`🧹 Sweeping prover fees...`)
this.sweeping = true
......@@ -213,7 +265,7 @@ export class Publisher extends BatchBuilder {
console.log(`🗞️ Publishing batch...`)
const contractPublishProver: types.ContractPublish = await this.preparePublish(this.batch)
clearInterval(this.publishTimeout);
// clearInterval(this.publishTimeout);
await this.contracts.state.publish(contractPublishProver.proof, contractPublishProver.batch)
.then((tx: any) => console.log(`📡 Batch published: ${tx.hash}.`), (error: any) => console.log(error.message));
......@@ -221,7 +273,48 @@ export class Publisher extends BatchBuilder {
this.provingQueue = Promise.resolve([]);
this.batch = [];
this.publishTimeout = setInterval(this.publish, 60 * 1000);
// this.publishTimeout = setInterval(this.publish, 60 * 1000);
this.publishing = false;
return;
}
async publishSingle() {
if (this.publishing) return;
if (this.proving) return;
if (this.batch.length === 0) return;
// If the accumulator has changed since the last pass
// ie. a new transaction has been added to the batch
if (this.accumulator !== this.batch[this.batch.length - 1].accumulator) {
console.log(`📝 Preparing batch publish`)
this.proving = true
this.contractPublish = await this.preparePublish(this.batch)
this.accumulator = this.batch[this.batch.length - 1].accumulator
this.proving = false
return;
}
if (!(await this.publishReady(this.batch, this.contractPublish))) return;
this.publishing = true
console.log(`🧹 Sweeping prover fees...`)
const transactions: types.Transaction[] = this.batch.filter(tx => tx.transaction !== undefined).map(tx => tx.transaction as types.Transaction)
const proverTx: types.Transaction = await this.sweepProfit(transactions)
const proverBatch: types.RecursionInputs[] = await this.proveSingle(proverTx)
const contractPublishProver: types.ContractPublish = await this.preparePublish(proverBatch)
console.log(`🗞️ Publishing batch...`)
await this.contracts.state.publish(contractPublishProver.proof, contractPublishProver.batch)
.then((tx: any) => console.log(`📡 Batch published: ${tx.hash}.`), (error: any) => console.log(error.message));
this.queue = [];
this.batch = [];
this.accumulator = types.ZERO_VALUE;
this.contractPublish = {} as types.ContractPublish
if (Object.keys(this.mempool).length >= 1000) this.mempool = {}
if (Object.keys(this.confirmed).length >= 1000) this.confirmed = {}
this.publishing = false;
return;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment