1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import * as types from "../../momiji-helpers/utils/types"
import { BatchBuilder } from "../../momiji-helpers/utils/batchBuilder";
import { keccak_tx } from '../../momiji-helpers/circuits/helpers/codegen/keccak_tx';
import { tx_as_hash } from '../../momiji-helpers/circuits/helpers/codegen/tx_as_hash';
export class Publisher extends BatchBuilder {
private publishTimeout: NodeJS.Timeout;
private signer: types.EthersSigner | undefined;
private mempool: types.Mempool = {};
private provingQueue: Promise<types.RecursionInputs[]> = Promise.resolve([]);
private batch: types.RecursionInputs[] = [];
private confirmed: types.Confirmed = {};
private publishing: boolean = false;
private proving: boolean = false;
private sweeping: boolean = false;
constructor(config: types.GlobalConfig) {
super(config);
this.publishTimeout = setInterval(this.publish, 60 * 1000);
this.mempool = {};
this.confirmed = {};
}
initializePublisher = async () => {
await this.initializeBatchBuilder((_tx: types.Transaction) => this.newTransactionReceived(_tx));
await this.setupListeners();
console.log(`⌚ Event watcher started.`)
await this.printRoot();
console.log(`🟣 Welcome to the Offshift Prover Network.`)
this.signer && console.log(`🔑 Publisher Address: ${await this.signer.getAddress()}.`)
}
async addToMempool(tx: types.Transaction, txid: string): Promise<boolean> {
let pi_hash = await tx_as_hash(tx.public_inputs);
if (pi_hash != tx.proof_artifacts.proofData.publicInputs[0]) return false;
if (!this.mempool.hasOwnProperty(txid) && !this.confirmed.hasOwnProperty(txid)) {
this.mempool[txid] = tx;
let verified: boolean = false;
console.log(`❕ Verifying ${txid}.`)
const proofU8: Uint8Array = Uint8Array.from(Object.values(tx.proof_artifacts.proofData.proof))
tx.proof_artifacts.proofData.proof = proofU8
verified = await this.backends.transaction.verifyProof(tx.proof_artifacts.proofData)
if (!verified) {
return false;
}
return true;
}
return false;
}
async newTransactionReceived(tx: types.Transaction): Promise<void> {
if (this.batch.length >= 15) {
console.log(`🔴 Transaction rejected -- batch is full`);
return;
}
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 setupListeners(): Promise<void> {
if (!this.contracts) await this.initializePublisher();
if (!this.contracts) return;
this.contracts.state.on(this.contracts.state.filters.TransactionPublish(undefined, undefined, undefined), async (tx: any, _: any, txId: any) => {
this.confirmed[txId] = true;
console.log(`✔️ Transaction confirmed: ${txId}.`);
});
this.contracts.state.on(this.contracts.state.filters.TransactionBroadcast(undefined, undefined, undefined), async (tx: any, _: any, txId: any) => {
const public_inputs = {
current_root: tx.transaction.current_root,
utxo_root: tx.transaction.utxo_root,
deposit_amount: tx.transaction.amount,
withdrawals: new types.NoirFr(tx.transaction.withdrawals.map((w: any) => BigInt(w)).reduce((a: any, b: any) => a + b)).toString(),
commitment_in: tx.transaction.commitments_in,
commitment_out: tx.transaction.commitments,
nullifier_hashes: tx.transaction.nullifier_hashes,
contract_only_inputs: txId
}
const txAsHash: string = await tx_as_hash(public_inputs)
let newTx: types.Transaction = {
public_inputs: public_inputs,
contract_inputs: tx.transaction,
proof_artifacts: {
proofData: {
proof: Uint8Array.from(Buffer.from(tx.proofU8.slice(2), "hex")),
publicInputs: [txAsHash]
},
proofAsFields: tx.proof,
vkAsFields: types.tx_vk,
vkHash: types.tx_vk_hash
}
}
this.newTransactionReceived(newTx);
});
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.batch = [];
this.printRoot();
return;
});
}
printRoot = async () => console.log(`🌳 Current State Root: ${await this.contracts.state.merkleRoot()}`)
sweepProfit = async (txs: types.Transaction[] | types.Transaction): Promise<types.Transaction> => {
if (!Array.isArray(txs)) txs = [txs];
const utxo_commitments: types.UTXO_Commitment[] = txs.map(tx => tx.contract_inputs.encrypted_utxo.filter(utxo => utxo.amount != types.ZERO_VALUE)).flat().map(utxo => {
return {
secret: utxo.secret as string,
amount: utxo.amount as string,
asset_type: utxo.data as string,
spend_in_same_batch: true
}
})
const utxo_encrypted: types.UTXO_Encrypted[] = await this._generateUTXOEncrypted([{
amount: types.toFixedHex(0, true)
}])
const withdrawalAmount: bigint = utxo_commitments.map(utxo => BigInt(utxo.amount)).reduce((prev, curr) => prev + curr)
const withdrawals: types.WithdrawalSwap[] = await this._generateWithdrawals([{
amount: (new types.NoirFr(withdrawalAmount)).toString(),
recipient: types.toFixedHex(1, true),
swap_percentage: 100
}], 1)
const proverTx: types.Transaction = await this._generateTransactionProof(utxo_commitments, utxo_encrypted, withdrawals);
return proverTx;
}
publishReady = async (txs: types.RecursionInputs[], contractPublish: types.ContractPublish): Promise<boolean> => {
console.log(`💰 Calculating prover fees...`)
const transactions: types.Transaction[] = txs.filter(tx => tx.transaction !== undefined).map(tx => tx.transaction as types.Transaction)
const utxo_commitments: types.UTXO_Commitment[] = transactions.map(tx => tx.contract_inputs.encrypted_utxo.filter(utxo => utxo.amount != types.ZERO_VALUE)).flat().map(utxo => {
return {
secret: utxo.secret as string,
amount: utxo.amount as string,
asset_type: utxo.data as string
}
})
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)
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
const publishReady: boolean = (withdrawalAmountEther >= txFeeEstimate)
return publishReady
}
queueToProve = async (tx: types.Transaction) => {
if (this.publishing) {
console.log(`⚠️ Batch is publishing. Cannot prove additional transactions.`);
return;
}
this.provingQueue = this.provingQueue.then(async (data): Promise<types.RecursionInputs[]> => {
return new Promise(async (res, rej) => {
if (data.length >= 15) {
console.log(`⚠️ Batch is full. Cannot prove additional transactions.`);
res(data);
}
this.proving = true;
let _newData = await this.prove(data, tx);
this.batch = _newData;
this.proving = false;
res(_newData);
})
}).catch(async e => {
console.log(e);
throw new Error(`❌ An unknown error occured while proving transactions.`);
})
}
prove = async (data: types.RecursionInputs[], tx: types.Transaction): Promise<types.RecursionInputs[]> => {
return new Promise(async (res, rej) => {
console.log(`⏳ Proving ${await keccak_tx(tx.public_inputs)}.`)
const proof = await this.rollupTransaction(data, tx);
console.log(`✔️ Proved ${await keccak_tx(tx.public_inputs)}.`)
return res(proof);
})
}
publish = async () => {
if (this.publishing) return;
if (this.proving) return;
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;
console.log(`🧹 Sweeping prover fees...`)
this.sweeping = true
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)
this.sweeping = false
this.queueToProve(proverTx)
this.publishing = true;
await this.provingQueue
console.log(`🗞️ Publishing batch...`)
const contractPublishProver: types.ContractPublish = await this.preparePublish(this.batch)
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));
this.provingQueue = Promise.resolve([]);
this.batch = [];
this.publishTimeout = setInterval(this.publish, 60 * 1000);
this.publishing = false;
return;
}
}