minigun-sv
Version:
Lightweight Bitcoin Stress Test Tool.
294 lines (276 loc) • 12.9 kB
JavaScript
var bsv = require('bsv')
var Utils = require('./utils')
var Client = require('./client')
var fs = require("fs")
var shouldCache = {}
const OUTPUT_SIZE = 80
const INPUT_SIZE = 150
const BASE_TX_SIZE = 250
const DUST_LIMIT = 546
const MAX_OUTS_PER_TX = 1000
//const FIRE_THESHOLD = 750
var FEE_PER_KB = 550
var FIRE_THESHOLD = DUST_LIMIT + (BASE_TX_SIZE + INPUT_SIZE + OUTPUT_SIZE) * FEE_PER_KB / 1000
//var RECYCLE_LIMIT = 80
function Barrel(privateKey, recycleAddr, blockChain) {
if (!(this instanceof Barrel)) {
return new Barrel(privateKey, recycleAddr, blockChain)
}
recalculateParameters()
this.privateKey = bsv.PrivateKey(privateKey)
this.script = Utils.createScript(this.privateKey.toAddress())
this.barrelAddr = this.privateKey.toAddress()
this.recycleAddr = recycleAddr
this.blockChain = blockChain
this.blockChain.listenAddr(this.privateKey.toAddress(), (utxos) => {
this.fireUTXOs(utxos)
})
this.recycleBucket = []
this.holdBucket = []
this.blockChain.listenAddr(recycleAddr, (utxos) => {
this.recycleBucket = this.recycleBucket.concat(utxos)
if (this.recycleBucket.length > 800) global.reload(true)
})
}
function recalculateParameters() {
FEE_PER_KB = global.feePerKB
global.FEE_PER_KB = global.feePerKB
FIRE_THESHOLD = Math.ceil(DUST_LIMIT + (BASE_TX_SIZE + INPUT_SIZE + OUTPUT_SIZE) * FEE_PER_KB / 1000)
global.FIRE_THESHOLD = Math.ceil(DUST_LIMIT + (BASE_TX_SIZE + INPUT_SIZE + OUTPUT_SIZE) * FEE_PER_KB / 1000)
}
/*
// pre-calculate fees
Barrel.nextlevelfee = function (levelfee) {
return global.TransactionSize + (levelfee + global.OutputSize) * (global.SplitRato - 1)
}
Barrel.calcFeeLevel = function () {
// recalculate fee level
var fee = global.Level0Fee
levelfees = []
for (var i = 0; i < global.MaxLevel; i++) {
levelfees[i] = fee
var fee = Barrel.nextlevelfee(fee)
}
}
Barrel.calcFeeLevel()
*/
Barrel.prototype.fireUTXOs = async function (utxos) {
// recoil and load
var startTime = new Date().getTime()
global.log.log(`[Minigun] Loading ${utxos.length} Ammo(UTXOs)`)
//global.log.log(utxos)
if (global.threshold != 0) {
// Threshold control
var canfire = this.blockChain.count
this.holdBucket = this.holdBucket.concat(utxos)
utxos = this.holdBucket.slice(0, canfire)
this.holdBucket = this.holdBucket.slice(canfire)
}
//utxos.forEach(utxo => this.fireUTXO(utxo))
// async processing, avoid blocking
for (var i = 0; i < utxos.length; i++) {
await this.fireUTXO(utxos[i])
}
if (global.load) global.log.log(`[Minigun] ${utxos.length} UTXO ammo handled in ${new Date().getTime() - startTime}ms`)
else global.log.log(`[Minigun] ${utxos.length} UTXO ammo fired in ${new Date().getTime() - startTime}ms, ${Math.floor(utxos.length / ((new Date().getTime() - startTime) / 1000))} TPS - ${new Date().getTime()}`)
//if (this.recycleBucket > 100) this.loadFrom(this.privateKey, this.recycleBucket)
if (utxos.length < global.reloadTheshold) {
global.log.log('[Minigun] Firable ammo below reload theshold, try reloading.')
global.reload()
}
global.log.log(`[Minigun] Caching ${this.barrelAddr.toString()} UTXOs: ${this.holdBucket.length}`)
fs.writeFileSync(`${this.barrelAddr}.utxos`, JSON.stringify(this.holdBucket))
}
Barrel.prototype.fireUTXO = async function (utxo) {
var tx = await this.buildTX(utxo)
if (tx && global.load) {
global.bullets.push(tx)
} else {
await this.blockChain.getReady()
this.fireTX(tx)
}
}
Barrel.prototype.buildTX = async function (utxo) {
// 使用Change链,而非分裂
var tx = bsv.Transaction()
if (utxo.satoshis < FIRE_THESHOLD) {
// Fire Shell
if (utxo.satoshis < DUST_LIMIT + BASE_TX_SIZE) {
// useless
this.recycleBucket.push(utxo)
return null
}
tx.from(utxo).change(this.recycleAddr)
tx.feePerKb(FEE_PER_KB)
} else {
// Load Shell
//global.log.log("[Minigun] A utxo with lots of satoshis, making it a ammo chain")
var nOut = Math.min(MAX_OUTS_PER_TX, Math.floor((utxo.satoshis - BASE_TX_SIZE - DUST_LIMIT - OUTPUT_SIZE) / (FIRE_THESHOLD + OUTPUT_SIZE)))
while (nOut-- > 0) tx.to(this.barrelAddr, FIRE_THESHOLD)
tx.from(utxo)
if (tx.inputAmount - tx.outputAmount > DUST_LIMIT + (OUTPUT_SIZE + tx.toString().length / 2 + BASE_TX_SIZE) * FEE_PER_KB / 1024) {
tx.change(this.recycleAddr)
tx.feePerKb(FEE_PER_KB)
}
}
tx.sign(this.privateKey)
return tx
}
/*
Barrel.prototype.buildTXEX = function (utxo) {
var level = global.MaxLevel - 1
while (utxo.satoshis < levelfees[level]) level--
var tx = bsv.Transaction()
if (level <= 0) {
// recycle shell
tx.from(utxo).change(this.recycleAddr)
} else {
// Split
tx.from(utxo)
if (level > global.LevelTheshold) {
// 加速分裂(2倍)
var splitTarget = (global.SplitRato - 1) * (global.SplitRato - 1)
var splitValue = levelfees[level - 2]
} else {
// 正常分裂
var splitTarget = global.SplitRato - 1
var splitValue = levelfees[level - 1]
}
// 生成分裂output
for (var i = 0; i < (splitTarget); i++) {
tx.to(this.nextBarrelAddr, splitValue)
}
// 回收剩余资金
var change = utxo.satoshis - splitValue * splitTarget
if (change > 546) tx.change(this.recycleAddr)
}
tx.sign(this.privateKey)
return tx
}
*/
Barrel.prototype.fireTX = function (tx) {
if (tx) {
this.blockChain.broadcast(tx)
if (global.recordTX) fs.writeFileSync("./tx/" + tx.id, tx.toString())
}
}
Barrel.prototype.setRecycle = function (recycleAddr) {
this.recycleAddr = recycleAddr
}
Barrel.prototype.loadFrom = function (privateKey, providedUTXOs) {
var barrelAddr = this.privateKey.toAddress()
var ammoAddr = bsv.PrivateKey(privateKey).toAddress()
var getUTXOs
global.log.log(`[Minigun] Loading from ${ammoAddr}`)
// load from recycle bucket if there are utxos in recycle bucket
global.log.log(`[Minigun] Recycle Bucket has ${this.recycleBucket.length} UTXO(s) in total, and has ${this.recycleBucket.filter(utxo => utxo.address == ammoAddr).length} UTXO(s) for ${ammoAddr}`)
//this.recycleBucket.slice(0,5).forEach(utxo => console.log(utxo))
if (this.recycleBucket.filter(utxo => utxo.address.toString() == ammoAddr.toString()).length > 10) {
getUTXOs = new Promise((resolve, reject) => {
global.log.log(`[Minigun] Loading in-bucket utxos from ${ammoAddr.toString()}`)
var utxos = this.recycleBucket.filter(utxo => utxo.address.toString() == ammoAddr.toString())
this.recycleBucket = this.recycleBucket.filter(utxo => utxo.address.toString() != ammoAddr.toString())
resolve(utxos)
})
} else if (providedUTXOs && Array.isArray(providedUTXOs)) {
global.log.log(`[Minigun] Initial loading`)
getUTXOs = new Promise((resolve, reject) => {
resolve(providedUTXOs)
})
} else if (!global.initialLoaded) {
global.log.log(`[Minigun] Querying loadable ammo in ${ammoAddr} from API`)
getUTXOs = Client.getUTXOs(ammoAddr).catch(e => {
console.log(`Exception during loading ${ammoAddr.toString()}`)
console.log(e)
return []
}).then(utxos => {
shouldCache[ammoAddr.toString()] = true
return utxos
})
} else {
global.log.log(`[Minigun] Nothing to be loaded`)
getUTXOs = new Promise((resolve) => resolve([]))
}
getUTXOs.then(utxos => {
if (shouldCache[ammoAddr.toString()]) {
global.log.log(`[Minigun] Caching ${ammoAddr.toString()} UTXOs: ${utxos.length}`)
fs.writeFileSync(`${ammoAddr}.utxos`, JSON.stringify(utxos))
}
return utxos
})
var startTime = new Date().getTime()
return getUTXOs.then(utxos => {
global.log.log(`[Minigun] Ammo(UTXOs) can be loaded from ${ammoAddr.toString()}: ${utxos.length}`)
if (utxos.length == 0) return []
var txs = []
var oneRound = FIRE_THESHOLD * MAX_OUTS_PER_TX
var aggregateUtxos = utxos.filter(utxo => utxo.satoshis < oneRound)
var roundUtxos = utxos.filter(utxo => utxo.satoshis >= oneRound)
// Round to bullets
roundUtxos.forEach(utxo => {
var nRound = Math.min(MAX_OUTS_PER_TX, Math.floor((utxo.satoshis - BASE_TX_SIZE - INPUT_SIZE - DUST_LIMIT - OUTPUT_SIZE) / (FIRE_THESHOLD * MAX_OUTS_PER_TX + OUTPUT_SIZE)))
var nBullet = Math.min(MAX_OUTS_PER_TX, Math.floor((utxo.satoshis - BASE_TX_SIZE - INPUT_SIZE - DUST_LIMIT - OUTPUT_SIZE - nRound * (FIRE_THESHOLD * MAX_OUTS_PER_TX + OUTPUT_SIZE)) / (FIRE_THESHOLD + OUTPUT_SIZE)))
global.log.log(`[Minigun] Pushing ${nRound} round(s) and ${nBullet} bullet(s) into ammo case`)
var tx = bsv.Transaction()
tx.addSafeData(["Stress Test", "[G] Heavy MachineGun!"])
while (nRound-- > 0) tx.to(this.recycleAddr, FIRE_THESHOLD * MAX_OUTS_PER_TX)
while (nBullet-- > 0) tx.to(barrelAddr, FIRE_THESHOLD)
tx.from(utxo)
tx.feePerKb(FEE_PER_KB)
tx.change(this.recycleAddr)
if (tx.getChangeOutput() && tx.getChangeOutput().satoshis < DUST_LIMIT) {
// Abandon dust change
tx.removeOutput(tx.outputs.length - 1)
}
tx.sign(privateKey)
txs.push(tx)
if (tx.isFullySigned()) {
this.fireTX(tx)
}
})
// aggregate to rounds and bullets
//var recycleCount = RECYCLE_LIMIT
var recycleCount = Math.floor(global.threshold / 4)
while (aggregateUtxos.length > 0 && recycleCount > 0) {
utxoschunk = aggregateUtxos.slice(0, global.recycleTheshold)
aggregateUtxos = aggregateUtxos.slice(global.recycleTheshold)
var satoshisTotal = utxoschunk.reduce((total, utxo) => total + utxo.satoshis, 0)
var nRound = Math.min(MAX_OUTS_PER_TX, Math.floor((satoshisTotal - BASE_TX_SIZE - utxoschunk.length * INPUT_SIZE - DUST_LIMIT - OUTPUT_SIZE) / (FIRE_THESHOLD * MAX_OUTS_PER_TX + OUTPUT_SIZE)))
var nBullet = Math.min(MAX_OUTS_PER_TX, Math.floor((satoshisTotal - BASE_TX_SIZE - utxoschunk.length * INPUT_SIZE - DUST_LIMIT - OUTPUT_SIZE - nRound * (FIRE_THESHOLD * MAX_OUTS_PER_TX + OUTPUT_SIZE)) / (FIRE_THESHOLD + OUTPUT_SIZE)))
global.log.log(`[Minigun] Recycling ${utxoschunk.length} utxo(s) into ${nRound} round(s) and ${nBullet} bullet(s), ${aggregateUtxos.length} utxo(s) remains.`)
var tx = bsv.Transaction()
tx.addSafeData(["Stress Test", "[G] Heavy MachineGun!"])
while (nRound-- > 0) tx.to(this.recycleAddr, FIRE_THESHOLD * MAX_OUTS_PER_TX)
while (nBullet-- > 0) tx.to(barrelAddr, FIRE_THESHOLD)
utxoschunk.forEach(utxo => {
// each input require a ECDSA sign, it's quite slow.
tx.from(utxo)
})
tx.feePerKb(FEE_PER_KB)
tx.change(this.recycleAddr)
if (tx.getChangeOutput() && tx.getChangeOutput().satoshis < DUST_LIMIT) {
// Abandon dust change
tx.removeOutput(tx.outputs.length - 1)
}
tx.sign(privateKey)
txs.push(tx)
// fire immediately, because it takes a long time to assemble all recycle TXs, long enough to lost connection with node.
if (tx.isFullySigned()) {
this.fireTX(tx)
recycleCount--
}
}
// return utxos to storage
if (aggregateUtxos.length > 0) {
var recycleUTXOs = aggregateUtxos.filter(utxo => utxo.address == this.recycleAddr)
var barrelUTXOs = aggregateUtxos.filter(utxo => utxo.address == this.barrelAddr)
this.recycleBucket = this.recycleBucket.concat(recycleUTXOs)
this.holdBucket = this.holdBucket.concat(barrelUTXOs)
// TODO: dump the rest
}
return txs
}).then(txs => {
if (txs.length > 0) global.log.log(`[Minigun] Reloaded from ${ammoAddr.toString()} in ${new Date().getTime() - startTime}ms`)
})
}
module.exports = Barrel