diff --git a/README.md b/README.md index cdc754f..28c127e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -## CLI Miner for META PoW on the Sui Blockchain +## CLI Miner for META and FOMO PoW on the Sui Blockchain - [Meta](https://github.com/suidouble/sui_meta) is the PoW coin on the Sui blockchain -- [Web Miner](https://suimine.xyz/) +- [Web Miner](https://suimine.xyz/) for the FOMO token - [Follow me on X](https://x.com/suidouble) - CLI Miner: @@ -11,7 +11,22 @@ CLI miner expects you to have node.js installed of version >= 18 [node.js](https git clone https://github.com/suidouble/sui_meta_miner.git cd sui_meta_miner npm install -node mine.js --chain=mainnet --phrase="secretphrase" +``` + +#### Run it + +Miner supports both META and FOMO coins: + +``` +node mine.js --meta --chain=mainnet --phrase="secretphrase" +node mine.js --fomo --chain=mainnet --phrase="secretphrase" +``` + +or you can run it in the boss mode, mining both: + +``` +node mine.js --fomo --meta --chain=mainnet --phrase="secretphrase" + ``` Where secretphrase is 24 words secret phrase for your wallet private key or private key in the format of "suiprivkey1....." ( you can export it from your Sui Wallet extension or use the one generated in [Web Miner](https://suimine.xyz/) ) diff --git a/config.js b/config.js index 002906c..300b41d 100644 --- a/config.js +++ b/config.js @@ -17,6 +17,21 @@ const settings = { "packageId": "0x3c680197c3d3c3437f78a962f4be294596c5ebea6cea6764284319d5e832e8e4", "blockStoreId": "0x57035b093ecefd396cd4ccf1a4bf685622b58353566b2f29b271afded2cb4390", "treasuryId": "0xeefe819f6f1b219f9460bfc78bfbac6568e2aec78cf808d2005ff2367e1de528", + + "fomo": { + "packageId": '0xa340e3db1332c21f20f5c08bef0fa459e733575f9a7e2f5faca64f72cd5a54f2', + "configId": '0x684de611aafbc5fbb6aa4288b4050281219dd5efc5764de22ba0f30b2bb2dd15', + "buses": [ + '0x1aa2497f14d27b2d7c2ebd9dd607cda0279dc4d54a57e3a8476a1236474b6567', + '0x367fdbde371d96018a572e354a6af4f74b69f412c0817eabff52bb166da6df0c', + '0x5e42608bc32e6ff4bd024ce3928832ebfa27efe557ba4763084222226e3413c0', + '0x98b9610b1b61cb02cae304d8b8a4fbd4e5cb018d24adf59f728ff886b597e9dc', + '0xaae095f1fd39d20418efd6ded79303fe8b53751907a17d2723594480655785f0', + '0xc941f30b0714306d256a8efa56d9b1fb9e63ebd94de14d6a4b5d9c52ece98bc5', + '0xcc18677841febd80ad34a4535c88ec402a968d24a824c00af29d1b5737e528d0', + '0xea11f05cb06456755a0b1b52bbd1b4a9e186ec89495f6ba02094ed84a075248a', + ], + }, }, "testnet": { "phrase": "coin coin coin coin coin coin coin coin coin coin coin coin coin coin coin coin coin coin coin coin coin coin coin coin", @@ -32,4 +47,12 @@ if (argv.phrase) { settings[selectedChain].phrase = argv.phrase; } +settings[selectedChain].do = {meta: true}; +if (argv.fomo) { + settings[selectedChain].do.fomo = true; + if (!argv.meta) { + settings[selectedChain].do.meta = false; + } +} + export default settings[selectedChain]; diff --git a/includes/Miner.js b/includes/Miner.js index f8d8c85..5a1bd3c 100644 --- a/includes/Miner.js +++ b/includes/Miner.js @@ -19,7 +19,7 @@ export default class Miner { this._treasury = null; this._movePackage = null; - this._nonceFinder = new NonceFinder(); + this._nonceFinder = new NonceFinder({ name: 'META' }); } async checkObjects() { @@ -71,39 +71,6 @@ export default class Miner { return balance; } - async initGenesis() { - await this.checkObjects(); - - const tx = new suidouble.Transaction(); - const clockId = '0x0000000000000000000000000000000000000000000000000000000000000006'; - - tx.moveCall({ - target: `${this._packageId}::mining::init_genesis`, - arguments: [ - tx.object(clockId), - tx.object(this._blockStore.id), - ], - // typeArguments: [`${testScenario._packages.admin.address}::mining::BlockInfo`], - }); - - const r = await this._suiMaster.signAndExecuteTransaction({ - transaction: tx, - requestType: 'WaitForLocalExecution', - sender: this._suiMaster.address, - options: { - "showEffects": true, // @todo: remove? - "showEvents": true, // @todo: remove? - "showObjectChanges": true, - showType: true, - showContent: true, - showOwner: true, - showDisplay: true, - }, - }); - - return (r?.effects?.status?.status == 'success'); - } - async mine(startNonce = 0, meta = null, payload = null) { await this.checkObjects(); @@ -126,12 +93,13 @@ export default class Miner { const __checkForOutdatedInterval = setInterval(()=>{ this.hasBlockInfoChanged(blockInfo) .then((changed)=>{ + console.log('META | block hash changed', changed); if (changed) { isOutdated = true; this._nonceFinder.pleaseStop(); } }); - }, 1000); + }, 3000); while (!foundValid && !isOutdated) { // nonce = await this.findValidNonce(preparedHash, blockInfo.difficulty, nonce); @@ -139,12 +107,12 @@ export default class Miner { nonce = await this._nonceFinder.findValidNonce(preparedHash, blockInfo.target); if (nonce !== null) { - console.log('valid nonce '+nonce+' found in '+((new Date()).getTime() - startFindingNonceAt)+'ms'); + console.log('META | valid nonce '+nonce+' found in '+((new Date()).getTime() - startFindingNonceAt)+'ms'); const success = await this.submit(nonce, meta, payload); if (success) { foundValid = true; } else { - console.log('blockInfo was wrong!!!'); + console.log('META | blockInfo was wrong!!!'); nonce = nonce + 1; blockInfo = await this.getBlockInfo(); // maybe we have wrong block info? preparedHash = this.prepareHash(blockInfo, meta, payload); @@ -206,13 +174,13 @@ export default class Miner { }); if (r && r.effects && r.effects.status && r.effects.status.status && r.effects.status.status == 'success') { - console.log('valid nonce submited'); + console.log('META | valid nonce submited'); return true; } else { - console.log('can not submit nonce'); + console.log('META | can not submit nonce'); } } catch (e) { - console.log('can not submit nonce'); + console.log('META | can not submit nonce'); console.error(e); } @@ -282,7 +250,7 @@ export default class Miner { async hasBlockInfoChanged(blockInfo) { const currentBlockInfo = await this.getBlockInfo(); - console.log('Current previous_hash: '+currentBlockInfo.previousHashAsBigInt); + // console.log('Current previous_hash: '+currentBlockInfo.previousHashAsBigInt); if (currentBlockInfo.previousHashAsBigInt != blockInfo.previousHashAsBigInt) { return true; } diff --git a/includes/NonceFinder.js b/includes/NonceFinder.js index 83f14d2..94b334b 100644 --- a/includes/NonceFinder.js +++ b/includes/NonceFinder.js @@ -10,6 +10,8 @@ export default class NonceFinder { this._workersCount = 8; this._askedToStop = false; + + this._name = params.name; } pleaseStop() { @@ -57,7 +59,7 @@ export default class NonceFinder { let i = 0; for (const worker of this._workers) { const tryNonce = this.getNextNonceToTry(); - console.log('thread #'+i+' | trying nonce: ', tryNonce); + // console.log('thread #'+i+' | trying nonce: ', tryNonce); const promise = new Promise((res,rej)=>{ worker.findSalt(preparedHash, maxTarget, tryNonce) .then((foundNonce)=>{ @@ -81,11 +83,11 @@ export default class NonceFinder { } catch (e) { foundGoodHash = false; } - console.log('current rate is: '+this.hashesPerSecond()+' H/s'); + console.log((this._name ? this._name : '') + ' | current rate is: '+this.hashesPerSecond()+' H/s'); } while (!foundGoodHash && !this._askedToStop); if (this._askedToStop) { // we are stoping once - console.log('stoping finding nonce as we were asked to stop'); + console.log((this._name ? this._name : '') + ' | goes to the next nonces block'); this._askedToStop = false; } diff --git a/includes/fomo/FomoMiner.js b/includes/fomo/FomoMiner.js new file mode 100644 index 0000000..a1d74f6 --- /dev/null +++ b/includes/fomo/FomoMiner.js @@ -0,0 +1,343 @@ + +import suidouble from 'suidouble'; +import { bcs } from '@mysten/sui/bcs'; +import hasher from 'js-sha3'; +// import { spawn, Thread, Worker } from "threads"; + +import { bytesTou64, bigIntTo32Bytes, u64toBytes } from '../math.js'; +import NonceFinder from '../NonceFinder.js'; + +export default class FomoMiner { + constructor(params = {}) { + this._suiMaster = params.suiMaster || null; + + this._buses = params.buses || null; + this._configId = params.configId || null; + this._packageId = params.packageId || null; + + // this._blockStore = null; // to be initialized as SuiObject in .checkObjects() initializer + // this._treasury = null; + // this._movePackage = null; + + this._nonceFinder = new NonceFinder({ name: 'FOMO' }); + + this._config = null; + this._movePackage = null; + } + + async checkObjects() { + if (this.__checkObjectsPromise) { + return await this.__checkObjectsPromise; + } + + this.__checkObjectsPromiseResolver = null; // to be sure it's executed once async + this.__checkObjectsPromise = new Promise((res)=>{ this.__checkObjectsPromiseResolver = res; }); + + if (!this._configId || !this._packageId || !this._buses) { + throw new Error('FOMO | configId, packageId are required'); + } + + const SuiObject = suidouble.SuiObject; + + const config = new SuiObject({ + id: this._configId, + suiMaster: this._suiMaster, + }); + // const treasury = new SuiObject({ + // id: this._treasuryId, + // suiMaster: this._suiMaster, + // }); + this._suiMaster.objectStorage.push(config); + // this._suiMaster.objectStorage.push(treasury); + + await config.fetchFields(); // get fields from the blockchain + + this._config = config; + // this._treasury = treasury; + + const movePackage = this._suiMaster.addPackage({ + id: this._packageId, + }); + await movePackage.isOnChain(); // check the package on the blockchain + + this._movePackage = movePackage; + + this.__checkObjectsPromiseResolver(true); // initialized + + return true; + } + + async getOrCreateMiner() { + await this.checkObjects(); + + // check for owned objects + if (this._suiMaster._debug) { + console.log('FOMO | Trying to find the miner object already registered on the blockchain....'); + } + const paginated = await this._movePackage.modules.miner.getOwnedObjects({ typeName: 'Miner' }); + let miner = null; + await paginated.forEach((suiObject)=>{ miner = suiObject; }); + + if (miner) { + if (this._suiMaster._debug) { + console.log('FOMO | It is there, id is: ', miner.id); + } + return miner; + } + + console.log('FOMO | Can not find it. Lets register the new one...'); + + await this._movePackage.modules.miner.moveCall('register', []); + + console.log('FOMO | Miner succesfully registered'); + await new Promise((res)=>{ setTimeout(res, 2000); }); + + return await this.getOrCreateMiner(); + } + + async fetchBus() { + await this.checkObjects(); + const randomBusId = this._buses[Math.floor(Math.random() * this._buses.length)]; + + // console.log(randomBusId); + + const bus = new (this._suiMaster.SuiObject)({ id: randomBusId, suiMaster: this._suiMaster }); + await bus.fetchFields(); + + return bus; + } + + async hasBlockInfoChanged(oldHash) { + const miner = await this.getOrCreateMiner(); + const newHash = new Uint8Array(miner.fields.current_hash); // changed on the new block + + // console.log('Current hash: '+newHash); + if (bytesTou64(oldHash) != bytesTou64(newHash)) { + return true; + } + return false; + } + + async mine(startNonce = 0) { + await this.checkObjects(); + + let miner = await this.getOrCreateMiner(); + let bus = await this.fetchBus(); + const currentHash = new Uint8Array(miner.fields.current_hash); // changed on the new block + const signerAddressBytes = bcs.Address.serialize(this._suiMaster.address).toBytes(); + const difficulty = Number(bus.fields.difficulty); + const difficultyAsTarget = '0x'+(''.padEnd(difficulty*2, '00').padEnd(64, 'ff')); + + + let foundValid = false; + let preparedHash = this.prepareHash(currentHash, signerAddressBytes); + let nonce = startNonce || 0; + const startFindingNonceAt = (new Date()).getTime(); + + let isOutdated = false; + const __checkForOutdatedInterval = setInterval(()=>{ + this.hasBlockInfoChanged(currentHash) + .then((changed)=>{ + console.log('FOMO | block hash changed', changed); + if (changed) { + isOutdated = true; + this._nonceFinder.pleaseStop(); + } + }); + }, 3000); + + while (!foundValid && !isOutdated) { + nonce = await this._nonceFinder.findValidNonce(preparedHash, difficultyAsTarget); + + if (nonce !== null) { + console.log('FOMO | valid nonce '+nonce+' found in '+((new Date()).getTime() - startFindingNonceAt)+'ms'); + const success = await this.submit(nonce, bus, miner); + if (success) { + foundValid = true; + } else { + console.log('FOMO | blockInfo was wrong!!!'); + nonce = nonce + 1; + + miner = await this.getOrCreateMiner(); + preparedHash = this.prepareHash(new Uint8Array(miner.fields.current_hash), signerAddressBytes); + } + } else { + // asked to stop + isOutdated = true; + } + + }; + + clearInterval(__checkForOutdatedInterval); + + return true; + } + + async prepare() { + await this.checkObjects(); + + const miner = await this.getOrCreateMiner(); + const currentHash = new Uint8Array(miner.fields.current_hash); // changed on the new block + let startNonce = BigInt(0); + const signerAddressBytes = bcs.Address.serialize(this._suiMaster.address).toBytes(); + + + let bus = await this.fetchBus(); + console.log(signerAddressBytes); + console.log(currentHash); + console.log(miner.fields.current_hash); + + const difficulty = Number(bus.fields.difficulty); + console.log(difficulty); + + const difficultyAsTarget = '0x'+(''.padEnd(difficulty*2, '00').padEnd(64, 'ff')); + console.log(difficultyAsTarget); + + // const hash = this.createHash(currentHash, signerAddressBytes, startNonce); + // const hashIsValid = this.validateHash(hash, difficulty); + + // console.log(hash); + // console.log(hashIsValid); + + console.log(this.busIsOk(bus)); + + // let found = false; + // while(!found) { + // const hash = this.createHash(currentHash, signerAddressBytes, startNonce); + // const hashIsValid = this.validateHash(hash, difficulty); + + // console.log(startNonce, hashIsValid); + // if (hashIsValid) { + // found = true; + // } + + // startNonce = startNonce + 1n; + // } + + const preparedHash = this.prepareHash(currentHash, signerAddressBytes); + let nonce = await this._nonceFinder.findValidNonce(preparedHash, difficultyAsTarget); + console.log(nonce); + + if (nonce) { + this.submit(nonce, bus, miner); + } + } + + busIsOk(bus) { + const epochLength = 60000; + const fundsOk = BigInt(bus.fields.rewards) >= BigInt(bus.fields.reward_rate); + const threshold = Number(bus.fields.last_reset) + epochLength; + + const buffer = 4000; + const resetTimeOk = Date.now() < threshold - buffer; + + return resetTimeOk && fundsOk; + } + + async submit(nonce, bus, miner) { + // await this.checkObjects(); + + // if (!meta) { + // meta = new Uint8Array([]); + // } + // if (!payload) { + // payload = new Uint8Array([]); + // } + + const tx = new suidouble.Transaction(); + + const args = [ + tx.pure('u64', nonce), + tx.object(bus.id), // bus + tx.object(miner.id), // miner + tx.object('0x0000000000000000000000000000000000000000000000000000000000000006'), // clock + // tx.object(this._treasury.id), + // suidouble.txInput(tx, 'vector', Array.from(meta)), + // suidouble.txInput(tx, 'vector', Array.from(payload)), + // suidouble.txInput(tx, 'u64', nonce), + // tx.object(this._suiMaster.address), + ]; + + const moveCallResult = tx.moveCall({ + target: `${this._packageId}::fomo::mine`, + arguments: args + }); + + tx.transferObjects([moveCallResult], this._suiMaster.address); + + try { + const r = await this._suiMaster.signAndExecuteTransaction({ + transaction: tx, + requestType: 'WaitForLocalExecution', + sender: this._suiMaster.address, + options: { + "showEffects": true, // @todo: remove? + "showEvents": true, // @todo: remove? + "showObjectChanges": true, + showType: true, + showContent: true, + showOwner: true, + showDisplay: true, + }, + }); + + if (r && r.effects && r.effects.status && r.effects.status.status && r.effects.status.status == 'success') { + console.log('FOMO | valid nonce submited'); + return true; + } else { + console.log('FOMO | can not submit nonce'); + } + } catch (e) { + console.log('FOMO | can not submit nonce'); + console.error(e); + } + + return false; + } + + async waitUntilNextReset(currentReset) { + const epochLength = 60000; + const bus = await this.fetchBus(); + const nextReset = Number(bus.fields.last_reset) + epochLength; + const timeUntilNextReset = nextReset - Date.now(); + + if (timeUntilNextReset > 0) { + await new Promise((res)=>setTimeout(res, timeUntilNextReset)); + } + + while (true) { + const freshBus = await this.fetchBus(); + if (Number(freshBus.fields.last_reset) !== Number(currentReset)) { + return true; + } else { + if (Date.now() > nextReset + 12000) { + return false; + } + await new Promise((res)=>setTimeout(res, 1500)); + } + } + } + + prepareHash(currentHash, signerAddressBytes) { + const prepared = new Uint8Array(32 + 32); // nonce bytes would be empty + prepared.set(currentHash, 0); + prepared.set(signerAddressBytes, 32); + + return prepared; + } + + createHash(currentHash, signerAddressBytes, nonce) { + const dataToHash = new Uint8Array(32 + 32 + 8); + dataToHash.set(currentHash, 0); + dataToHash.set(signerAddressBytes, 32); + dataToHash.set(u64toBytes(nonce), 64); + + return bigIntTo32Bytes(BigInt('0x'+hasher.keccak256(dataToHash))); + } + + validateHash(hash, difficulty) { + return hash.slice(0, difficulty).reduce((a, b) => a + b, 0) === 0; + } + + +} \ No newline at end of file diff --git a/mine.js b/mine.js index 9a6d77a..2e9ac87 100644 --- a/mine.js +++ b/mine.js @@ -1,14 +1,19 @@ import { SuiMaster } from 'suidouble'; import config from './config.js'; import Miner from './includes/Miner.js'; +import FomoMiner from './includes/fomo/FomoMiner.js'; const run = async()=>{ const phrase = config.phrase; const chain = config.chain; + if (!config.phrase || !config.chain) { + throw new Error('phrase and chain parameters are required'); + } + const suiMasterParams = { client: chain, - debug: true, + debug: !!config.debug, }; if (phrase.indexOf('suiprivkey') === 0) { suiMasterParams.privateKey = phrase; @@ -16,29 +21,58 @@ const run = async()=>{ suiMasterParams.phrase = phrase; } const suiMaster = new SuiMaster(suiMasterParams); + await suiMaster.initialize(); - const miner = new Miner({ - suiMaster, - packageId: config.packageId, - blockStoreId: config.blockStoreId, - treasuryId: config.treasuryId, - }); + console.log('suiMaster connected as ', suiMaster.address); - let i = 0; - let balance = null; + const miners = {}; - while (true) { - // await miner.printAdjustDifficultyEvents(); - await miner.mine(); - i = i + 1; - // balance = await miner.getBTCBalance(); - // console.log('BTC balance: ', balance); + const doMine = async(minerInstance)=>{ + while (true) { + await minerInstance.mine(); + await new Promise((res)=>setTimeout(res, 100)); + }; + }; - // await miner.printAdjustDifficultyEvents(); - } + + if (config.do.meta) { + const miner = new Miner({ + suiMaster, + packageId: config.packageId, + blockStoreId: config.blockStoreId, + treasuryId: config.treasuryId, + }); + miners.meta = miner; + doMine(miners.meta); + }; + if (config.do.fomo) { + const fomoMiner = new FomoMiner({ + suiMaster, + packageId: config.fomo.packageId, + configId: config.fomo.configId, + buses: config.fomo.buses, + }); + miners.fomo = fomoMiner; + doMine(miners.fomo); + }; + + + + // // let i = 0; + // // let balance = null; + + // while (true) { + // // await miner.printAdjustDifficultyEvents(); + // await miner.mine(); + // i = i + 1; + // // balance = await miner.getBTCBalance(); + // // console.log('BTC balance: ', balance); + + // // await miner.printAdjustDifficultyEvents(); + // } }; run() .then(()=>{ - console.error('done'); + console.error('running'); }); \ No newline at end of file