run-db
Version:
A local database that indexes jig states from RUN transactions
976 lines (781 loc) • 33.7 kB
JavaScript
/**
* database.js
*
* Layer between the database and the application
*/
const Sqlite3Database = require('better-sqlite3')
const Run = require('run-sdk')
const bsv = require('bsv')
// ------------------------------------------------------------------------------------------------
// Globals
// ------------------------------------------------------------------------------------------------
const HEIGHT_MEMPOOL = -1
const HEIGHT_UNKNOWN = null
// The + in the following 2 queries before downloaded improves performance by NOT using the
// tx_downloaded index, which is rarely an improvement over a simple filter for single txns.
// See: https://www.sqlite.org/optoverview.html
const IS_READY_TO_EXECUTE_SQL = `
SELECT (
downloaded = 1
AND executable = 1
AND executed = 0
AND (has_code = 0 OR (SELECT COUNT(*) FROM trust WHERE trust.txid = tx.txid AND trust.value = 1) = 1)
AND txid NOT IN ban
AND (
SELECT COUNT(*)
FROM tx AS tx2
JOIN deps
ON deps.up = tx2.txid
WHERE deps.down = tx.txid
AND (+tx2.downloaded = 0 OR (tx2.executable = 1 AND tx2.executed = 0))
) = 0
) AS ready
FROM tx
WHERE txid = ?
`
const GET_DOWNSTREADM_READY_TO_EXECUTE_SQL = `
SELECT down
FROM deps
JOIN tx
ON tx.txid = deps.down
WHERE up = ?
AND +downloaded = 1
AND executable = 1
AND executed = 0
AND (has_code = 0 OR (SELECT COUNT(*) FROM trust WHERE trust.txid = tx.txid AND trust.value = 1) = 1)
AND txid NOT IN ban
AND (
SELECT COUNT(*)
FROM tx AS tx2
JOIN deps
ON deps.up = tx2.txid
WHERE deps.down = tx.txid
AND (+tx2.downloaded = 0 OR (tx2.executable = 1 AND tx2.executed = 0))
) = 0
`
// ------------------------------------------------------------------------------------------------
// Database
// ------------------------------------------------------------------------------------------------
class Database {
constructor (path, logger, readonly = false) {
this.path = path
this.logger = logger
this.db = null
this.readonly = readonly
this.onReadyToExecute = null
this.onAddTransaction = null
this.onDeleteTransaction = null
this.onTrustTransaction = null
this.onUntrustTransaction = null
this.onBanTransaction = null
this.onUnbanTransaction = null
this.onUntrustTransaction = null
this.onRequestDownload = null
}
open () {
this.logger.debug('Opening' + (this.readonly ? ' readonly' : '') + ' database')
if (this.db) throw new Error('Database already open')
this.db = new Sqlite3Database(this.path, { readonly: this.readonly })
// 100MB cache
this.db.pragma('cache_size = 6400')
this.db.pragma('page_size = 16384')
// WAL mode allows simultaneous readers
this.db.pragma('journal_mode = WAL')
// Synchronizes WAL at checkpoints
this.db.pragma('synchronous = NORMAL')
if (!this.readonly) {
// Initialise and perform upgrades
this.initializeV1()
this.initializeV2()
this.initializeV3()
this.initializeV4()
this.initializeV5()
this.initializeV6()
this.initializeV7()
}
this.addNewTransactionStmt = this.db.prepare('INSERT OR IGNORE INTO tx (txid, height, time, bytes, has_code, executable, executed, indexed) VALUES (?, null, ?, null, 0, 0, 0, 0)')
this.setTransactionBytesStmt = this.db.prepare('UPDATE tx SET bytes = ? WHERE txid = ?')
this.setTransactionExecutableStmt = this.db.prepare('UPDATE tx SET executable = ? WHERE txid = ?')
this.setTransactionTimeStmt = this.db.prepare('UPDATE tx SET time = ? WHERE txid = ?')
this.setTransactionHeightStmt = this.db.prepare(`UPDATE tx SET height = ? WHERE txid = ? AND (height IS NULL OR height = ${HEIGHT_MEMPOOL})`)
this.setTransactionHasCodeStmt = this.db.prepare('UPDATE tx SET has_code = ? WHERE txid = ?')
this.setTransactionExecutedStmt = this.db.prepare('UPDATE tx SET executed = ? WHERE txid = ?')
this.setTransactionIndexedStmt = this.db.prepare('UPDATE tx SET indexed = ? WHERE txid = ?')
this.hasTransactionStmt = this.db.prepare('SELECT txid FROM tx WHERE txid = ?')
this.getTransactionHexStmt = this.db.prepare('SELECT LOWER(HEX(bytes)) AS hex FROM tx WHERE txid = ?')
this.getTransactionTimeStmt = this.db.prepare('SELECT time FROM tx WHERE txid = ?')
this.getTransactionHeightStmt = this.db.prepare('SELECT height FROM tx WHERE txid = ?')
this.getTransactionHasCodeStmt = this.db.prepare('SELECT has_code FROM tx WHERE txid = ?')
this.getTransactionIndexedStmt = this.db.prepare('SELECT indexed FROM tx WHERE txid = ?')
this.getTransactionFailedStmt = this.db.prepare('SELECT (executed = 1 AND indexed = 0) AS failed FROM tx WHERE txid = ?')
this.getTransactionDownloadedStmt = this.db.prepare('SELECT downloaded FROM tx WHERE txid = ?')
this.deleteTransactionStmt = this.db.prepare('DELETE FROM tx WHERE txid = ?')
this.unconfirmTransactionStmt = this.db.prepare(`UPDATE tx SET height = ${HEIGHT_MEMPOOL} WHERE txid = ?`)
this.getTransactionsAboveHeightStmt = this.db.prepare('SELECT txid FROM tx WHERE height > ?')
this.getMempoolTransactionsBeforeTimeStmt = this.db.prepare(`SELECT txid FROM tx WHERE height = ${HEIGHT_MEMPOOL} AND time < ?`)
this.getTransactionsToDownloadStmt = this.db.prepare('SELECT txid FROM tx WHERE downloaded = 0')
this.getTransactionsDownloadedCountStmt = this.db.prepare('SELECT COUNT(*) AS count FROM tx WHERE downloaded = 1')
this.getTransactionsIndexedCountStmt = this.db.prepare('SELECT COUNT(*) AS count FROM tx WHERE indexed = 1')
this.isReadyToExecuteStmt = this.db.prepare(IS_READY_TO_EXECUTE_SQL)
this.getDownstreamReadyToExecuteStmt = this.db.prepare(GET_DOWNSTREADM_READY_TO_EXECUTE_SQL)
this.setSpendStmt = this.db.prepare('INSERT OR REPLACE INTO spends (location, spend_txid) VALUES (?, ?)')
this.setUnspentStmt = this.db.prepare('INSERT OR IGNORE INTO spends (location, spend_txid) VALUES (?, null)')
this.getSpendStmt = this.db.prepare('SELECT spend_txid FROM spends WHERE location = ?')
this.unspendOutputsStmt = this.db.prepare('UPDATE spends SET spend_txid = null WHERE spend_txid = ?')
this.deleteSpendsStmt = this.db.prepare('DELETE FROM spends WHERE location LIKE ? || \'%\'')
this.addDepStmt = this.db.prepare('INSERT OR IGNORE INTO deps (up, down) VALUES (?, ?)')
this.deleteDepsStmt = this.db.prepare('DELETE FROM deps WHERE down = ?')
this.getDownstreamStmt = this.db.prepare('SELECT down FROM deps WHERE up = ?')
this.getUpstreamUnexecutedCodeStmt = this.db.prepare(`
SELECT txdeps.txid as txid
FROM (SELECT up AS txid FROM deps WHERE down = ?) as txdeps
JOIN tx ON tx.txid = txdeps.txid
WHERE tx.executable = 1 AND tx.executed = 0 AND tx.has_code = 1
`)
this.setJigStateStmt = this.db.prepare('INSERT OR IGNORE INTO jig (location, state, class, lock, scripthash) VALUES (?, ?, null, null, null)')
this.setJigClassStmt = this.db.prepare('UPDATE jig SET class = ? WHERE location = ?')
this.setJigLockStmt = this.db.prepare('UPDATE jig SET lock = ? WHERE location = ?')
this.setJigScripthashStmt = this.db.prepare('UPDATE jig SET scripthash = ? WHERE location = ?')
this.getJigStateStmt = this.db.prepare('SELECT state FROM jig WHERE location = ?')
this.deleteJigStatesStmt = this.db.prepare('DELETE FROM jig WHERE location LIKE ? || \'%\'')
const getAllUnspentSql = `
SELECT spends.location AS location FROM spends
JOIN jig ON spends.location = jig.location
WHERE spends.spend_txid IS NULL`
this.getAllUnspentStmt = this.db.prepare(getAllUnspentSql)
this.getAllUnspentByClassStmt = this.db.prepare(`${getAllUnspentSql} AND jig.class = ?`)
this.getAllUnspentByLockStmt = this.db.prepare(`${getAllUnspentSql} AND jig.lock = ?`)
this.getAllUnspentByScripthashStmt = this.db.prepare(`${getAllUnspentSql} AND jig.scripthash = ?`)
this.getAllUnspentByClassLockStmt = this.db.prepare(`${getAllUnspentSql} AND jig.class = ? AND lock = ?`)
this.getAllUnspentByClassScripthashStmt = this.db.prepare(`${getAllUnspentSql} AND jig.class = ? AND scripthash = ?`)
this.getAllUnspentByLockScripthashStmt = this.db.prepare(`${getAllUnspentSql} AND jig.lock = ? AND scripthash = ?`)
this.getAllUnspentByClassLockScripthashStmt = this.db.prepare(`${getAllUnspentSql} AND jig.class = ? AND jig.lock = ? AND scripthash = ?`)
this.getNumUnspentStmt = this.db.prepare('SELECT COUNT(*) as unspent FROM spends JOIN jig ON spends.location = jig.location WHERE spends.spend_txid IS NULL')
this.setBerryStateStmt = this.db.prepare('INSERT OR IGNORE INTO berry (location, state) VALUES (?, ?)')
this.getBerryStateStmt = this.db.prepare('SELECT state FROM berry WHERE location = ?')
this.deleteBerryStatesStmt = this.db.prepare('DELETE FROM berry WHERE location LIKE ? || \'%\'')
this.setTrustedStmt = this.db.prepare('INSERT OR REPLACE INTO trust (txid, value) VALUES (?, ?)')
this.getTrustlistStmt = this.db.prepare('SELECT txid FROM trust WHERE value = 1')
this.isTrustedStmt = this.db.prepare('SELECT COUNT(*) FROM trust WHERE txid = ? AND value = 1')
this.banStmt = this.db.prepare('INSERT OR REPLACE INTO ban (txid) VALUES (?)')
this.unbanStmt = this.db.prepare('DELETE FROM ban WHERE txid = ?')
this.isBannedStmt = this.db.prepare('SELECT COUNT(*) FROM ban WHERE txid = ?')
this.getBanlistStmt = this.db.prepare('SELECT txid FROM ban')
this.getHeightStmt = this.db.prepare('SELECT value FROM crawl WHERE key = \'height\'')
this.getHashStmt = this.db.prepare('SELECT value FROM crawl WHERE key = \'hash\'')
this.setHeightStmt = this.db.prepare('UPDATE crawl SET value = ? WHERE key = \'height\'')
this.setHashStmt = this.db.prepare('UPDATE crawl SET value = ? WHERE key = \'hash\'')
this.markExecutingStmt = this.db.prepare('INSERT OR IGNORE INTO executing (txid) VALUES (?)')
this.unmarkExecutingStmt = this.db.prepare('DELETE FROM executing WHERE txid = ?')
}
initializeV1 () {
if (this.db.pragma('user_version')[0].user_version !== 0) return
this.logger.info('Setting up database v1')
this.transaction(() => {
this.db.pragma('user_version = 1')
this.db.prepare(
`CREATE TABLE IF NOT EXISTS tx (
txid TEXT NOT NULL,
height INTEGER,
time INTEGER,
hex TEXT,
has_code INTEGER,
executable INTEGER,
executed INTEGER,
indexed INTEGER,
UNIQUE(txid)
)`
).run()
this.db.prepare(
`CREATE TABLE IF NOT EXISTS spends (
location TEXT NOT NULL PRIMARY KEY,
spend_txid TEXT
) WITHOUT ROWID`
).run()
this.db.prepare(
`CREATE TABLE IF NOT EXISTS deps (
up TEXT NOT NULL,
down TEXT NOT NULL,
UNIQUE(up, down)
)`
).run()
this.db.prepare(
`CREATE TABLE IF NOT EXISTS jig (
location TEXT NOT NULL PRIMARY KEY,
state TEXT NOT NULL,
class TEXT,
scripthash TEXT,
lock TEXT
) WITHOUT ROWID`
).run()
this.db.prepare(
`CREATE TABLE IF NOT EXISTS berry (
location TEXT NOT NULL PRIMARY KEY,
state TEXT NOT NULL
) WITHOUT ROWID`
).run()
this.db.prepare(
`CREATE TABLE IF NOT EXISTS trust (
txid TEXT NOT NULL PRIMARY KEY,
value INTEGER
) WITHOUT ROWID`
).run()
this.db.prepare(
`CREATE TABLE IF NOT EXISTS ban (
txid TEXT NOT NULL PRIMARY KEY
) WITHOUT ROWID`
).run()
this.db.prepare(
`CREATE TABLE IF NOT EXISTS crawl (
role TEXT UNIQUE,
height INTEGER,
hash TEXT
)`
).run()
this.db.prepare(
'CREATE INDEX IF NOT EXISTS tx_txid_index ON tx (txid)'
).run()
this.db.prepare(
'CREATE INDEX IF NOT EXISTS jig_index ON jig (class)'
).run()
this.db.prepare(
'INSERT OR IGNORE INTO crawl (role, height, hash) VALUES (\'tip\', 0, NULL)'
).run()
})
}
initializeV2 () {
if (this.db.pragma('user_version')[0].user_version !== 1) return
this.logger.info('Setting up database v2')
this.transaction(() => {
this.db.pragma('user_version = 2')
this.db.prepare(
`CREATE TABLE tx_v2 (
txid TEXT NOT NULL,
height INTEGER,
time INTEGER,
bytes BLOB,
has_code INTEGER,
executable INTEGER,
executed INTEGER,
indexed INTEGER
)`
).run()
const txids = this.db.prepare('SELECT txid FROM tx').all().map(row => row.txid)
const gettx = this.db.prepare('SELECT * FROM tx WHERE txid = ?')
const insert = this.db.prepare('INSERT INTO tx_v2 (txid, height, time, bytes, has_code, executable, executed, indexed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)')
this.logger.info('Migrating data')
for (const txid of txids) {
const row = gettx.get(txid)
const bytes = row.hex ? Buffer.from(row.hex, 'hex') : null
insert.run(row.txid, row.height, row.time, bytes, row.has_code, row.executable, row.executed, row.indexed)
}
this.db.prepare(
'DROP INDEX tx_txid_index'
).run()
this.db.prepare(
'DROP TABLE tx'
).run()
this.db.prepare(
'ALTER TABLE tx_v2 RENAME TO tx'
).run()
this.db.prepare(
'CREATE INDEX IF NOT EXISTS tx_txid_index ON tx (txid)'
).run()
this.logger.info('Saving results')
})
this.logger.info('Optimizing database')
this.db.prepare('VACUUM').run()
}
initializeV3 () {
if (this.db.pragma('user_version')[0].user_version !== 2) return
this.logger.info('Setting up database v3')
this.transaction(() => {
this.db.pragma('user_version = 3')
this.db.prepare('CREATE INDEX IF NOT EXISTS deps_up_index ON deps (up)').run()
this.db.prepare('CREATE INDEX IF NOT EXISTS deps_down_index ON deps (down)').run()
this.db.prepare('CREATE INDEX IF NOT EXISTS trust_txid_index ON trust (txid)').run()
this.logger.info('Saving results')
})
}
initializeV4 () {
if (this.db.pragma('user_version')[0].user_version !== 3) return
this.logger.info('Setting up database v4')
this.transaction(() => {
this.db.pragma('user_version = 4')
this.db.prepare('ALTER TABLE tx ADD COLUMN downloaded INTEGER GENERATED ALWAYS AS (bytes IS NOT NULL) VIRTUAL').run()
this.db.prepare('CREATE INDEX IF NOT EXISTS tx_downloaded_index ON tx (downloaded)').run()
this.logger.info('Saving results')
})
}
initializeV5 () {
if (this.db.pragma('user_version')[0].user_version !== 4) return
this.logger.info('Setting up database v5')
this.transaction(() => {
this.db.pragma('user_version = 5')
this.db.prepare('CREATE INDEX IF NOT EXISTS ban_txid_index ON ban (txid)').run()
this.db.prepare('CREATE INDEX IF NOT EXISTS tx_height_index ON tx (height)').run()
this.logger.info('Saving results')
})
}
initializeV6 () {
if (this.db.pragma('user_version')[0].user_version !== 5) return
this.logger.info('Setting up database v6')
this.transaction(() => {
this.db.pragma('user_version = 6')
const height = this.db.prepare('SELECT height FROM crawl WHERE role = \'tip\'').raw(true).all()[0]
const hash = this.db.prepare('SELECT hash FROM crawl WHERE role = \'tip\'').raw(true).all()[0]
this.db.prepare('DROP TABLE crawl').run()
this.db.prepare(
`CREATE TABLE IF NOT EXISTS crawl (
key TEXT UNIQUE,
value TEXT
)`
).run()
this.db.prepare('INSERT INTO crawl (key, value) VALUES (\'height\', ?)').run(height.toString())
this.db.prepare('INSERT INTO crawl (key, value) VALUES (\'hash\', ?)').run(hash)
this.logger.info('Saving results')
})
}
initializeV7 () {
if (this.db.pragma('user_version')[0].user_version !== 6) return
this.logger.info('Setting up database v7')
this.transaction(() => {
this.db.pragma('user_version = 7')
this.logger.info('Getting possible transactions to execute')
const stmt = this.db.prepare(`
SELECT txid
FROM tx
WHERE downloaded = 1
AND executable = 1
AND executed = 0
AND (has_code = 0 OR (SELECT COUNT(*) FROM trust WHERE trust.txid = tx.txid AND trust.value = 1) = 1)
AND txid NOT IN ban
`)
const txids = stmt.raw(true).all().map(x => x[0])
const isReadyToExecuteStmt = this.db.prepare(IS_READY_TO_EXECUTE_SQL)
const ready = []
for (let i = 0; i < txids.length; i++) {
const txid = txids[i]
const row = isReadyToExecuteStmt.get(txid)
if (row && row.ready) ready.push(txid)
if (i % 1000 === 0) console.log('Checking to execute', i, 'of', txids.length)
}
this.logger.info('Marking', ready.length, 'transactions to execute')
this.db.prepare('CREATE TABLE IF NOT EXISTS executing (txid TEXT UNIQUE)').run()
const markExecutingStmt = this.db.prepare('INSERT OR IGNORE INTO executing (txid) VALUES (?)')
ready.forEach(txid => markExecutingStmt.run(txid))
this.logger.info('Saving results')
})
}
async close () {
if (this.worker) {
this.logger.debug('Terminating background loader')
await this.worker.terminate()
this.worker = null
}
if (this.db) {
this.logger.debug('Closing' + (this.readonly ? ' readonly' : '') + ' database')
this.db.close()
this.db = null
}
}
transaction (f) {
if (!this.db) return
this.db.transaction(f)()
}
// --------------------------------------------------------------------------
// tx
// --------------------------------------------------------------------------
addBlock (txids, txhexs, height, hash, time) {
this.transaction(() => {
txids.forEach((txid, i) => {
const txhex = txhexs && txhexs[i]
this.addTransaction(txid, txhex, height, time)
})
this.setHeight(height)
this.setHash(hash)
})
}
addTransaction (txid, txhex, height, time) {
this.transaction(() => {
this.addNewTransaction(txid)
if (height) this.setTransactionHeight(txid, height)
if (time) this.setTransactionTime(txid, time)
})
const downloaded = this.isTransactionDownloaded(txid)
if (downloaded) return
if (txhex) {
this.parseAndStoreTransaction(txid, txhex)
} else {
if (this.onRequestDownload) this.onRequestDownload(txid)
}
}
parseAndStoreTransaction (txid, hex) {
if (this.isTransactionDownloaded(txid)) return
let metadata = null
let bsvtx = null
const inputs = []
const outputs = []
try {
if (!hex) throw new Error('No hex')
bsvtx = new bsv.Transaction(hex)
bsvtx.inputs.forEach(input => {
const location = `${input.prevTxId.toString('hex')}_o${input.outputIndex}`
inputs.push(location)
})
bsvtx.outputs.forEach((output, n) => {
if (output.script.isDataOut() || output.script.isSafeDataOut()) return
outputs.push(`${txid}_o${n}`)
})
metadata = Run.util.metadata(hex)
} catch (e) {
this.logger.error(`${txid} => ${e.message}`)
this.storeParsedNonExecutableTransaction(txid, hex, inputs, outputs)
return
}
const deps = new Set()
for (let i = 0; i < metadata.in; i++) {
const prevtxid = bsvtx.inputs[i].prevTxId.toString('hex')
deps.add(prevtxid)
}
for (const ref of metadata.ref) {
if (ref.startsWith('native://')) {
continue
} else if (ref.includes('berry')) {
const reftxid = ref.slice(0, 64)
deps.add(reftxid)
} else {
const reftxid = ref.slice(0, 64)
deps.add(reftxid)
}
}
const hasCode = metadata.exec.some(cmd => cmd.op === 'DEPLOY' || cmd.op === 'UPGRADE')
this.storeParsedExecutableTransaction(txid, hex, hasCode, deps, inputs, outputs)
for (const deptxid of deps) {
if (!this.isTransactionDownloaded(deptxid)) {
if (this.onRequestDownload) this.onRequestDownload(deptxid)
}
}
}
addNewTransaction (txid) {
if (this.hasTransaction(txid)) return
const time = Math.round(Date.now() / 1000)
this.addNewTransactionStmt.run(txid, time)
if (this.onAddTransaction) this.onAddTransaction(txid)
}
setTransactionHeight (txid, height) {
this.setTransactionHeightStmt.run(height, txid)
}
setTransactionTime (txid, time) {
this.setTransactionTimeStmt.run(time, txid)
}
storeParsedNonExecutableTransaction (txid, hex, inputs, outputs) {
this.transaction(() => {
const bytes = Buffer.from(hex, 'hex')
this.setTransactionBytesStmt.run(bytes, txid)
this.setTransactionExecutableStmt.run(0, txid)
inputs.forEach(location => this.setSpendStmt.run(location, txid))
outputs.forEach(location => this.setUnspentStmt.run(location))
})
// Non-executable might be berry data. We execute once we receive them.
const downstreamReadyToExecute = this.getDownstreamReadyToExecuteStmt.raw(true).all(txid).map(x => x[0])
downstreamReadyToExecute.forEach(downtxid => {
this.markExecutingStmt.run(downtxid)
if (this.onReadyToExecute) this.onReadyToExecute(downtxid)
})
}
storeParsedExecutableTransaction (txid, hex, hasCode, deps, inputs, outputs) {
this.transaction(() => {
const bytes = Buffer.from(hex, 'hex')
this.setTransactionBytesStmt.run(bytes, txid)
this.setTransactionExecutableStmt.run(1, txid)
this.setTransactionHasCodeStmt.run(hasCode ? 1 : 0, txid)
inputs.forEach(location => this.setSpendStmt.run(location, txid))
outputs.forEach(location => this.setUnspentStmt.run(location))
for (const deptxid of deps) {
this.addNewTransaction(deptxid)
this.addDepStmt.run(deptxid, txid)
if (this.getTransactionFailedStmt.get(deptxid).failed) {
this.setTransactionExecutionFailed(txid)
return
}
}
})
this._checkExecutability(txid)
}
storeExecutedTransaction (txid, result) {
const { cache, classes, locks, scripthashes } = result
this.transaction(() => {
this.setTransactionExecutedStmt.run(1, txid)
this.setTransactionIndexedStmt.run(1, txid)
this.unmarkExecutingStmt.run(txid)
for (const key of Object.keys(cache)) {
if (key.startsWith('jig://')) {
const location = key.slice('jig://'.length)
this.setJigStateStmt.run(location, JSON.stringify(cache[key]))
continue
}
if (key.startsWith('berry://')) {
const location = key.slice('berry://'.length)
this.setBerryStateStmt.run(location, JSON.stringify(cache[key]))
continue
}
}
for (const [location, cls] of classes) {
this.setJigClassStmt.run(cls, location)
}
for (const [location, lock] of locks) {
this.setJigLockStmt.run(lock, location)
}
for (const [location, scripthash] of scripthashes) {
this.setJigScripthashStmt.run(scripthash, location)
}
})
const downstreamReadyToExecute = this.getDownstreamReadyToExecuteStmt.raw(true).all(txid).map(x => x[0])
downstreamReadyToExecute.forEach(downtxid => {
this.markExecutingStmt.run(downtxid)
if (this.onReadyToExecute) this.onReadyToExecute(downtxid)
})
}
setTransactionExecutionFailed (txid) {
this.transaction(() => {
this.setTransactionExecutableStmt.run(0, txid)
this.setTransactionExecutedStmt.run(1, txid)
this.setTransactionIndexedStmt.run(0, txid)
this.unmarkExecutingStmt.run(txid)
})
// We try executing downstream transactions if this was marked executable but it wasn't.
// This allows an admin to manually change executable status in the database.
let executable = false
try {
const rawtx = this.getTransactionHex(txid)
Run.util.metadata(rawtx)
executable = true
} catch (e) { }
if (!executable) {
const downstream = this.getDownstreamStmt.raw(true).all(txid).map(x => x[0])
downstream.forEach(downtxid => this._checkExecutability(downtxid))
}
}
getTransactionHex (txid) {
const row = this.getTransactionHexStmt.raw(true).get(txid)
return row && row[0]
}
getTransactionTime (txid) {
const row = this.getTransactionTimeStmt.raw(true).get(txid)
return row && row[0]
}
getTransactionHeight (txid) {
const row = this.getTransactionHeightStmt.raw(true).get(txid)
return row && row[0]
}
deleteTransaction (txid, deleted = new Set()) {
if (deleted.has(txid)) return
const txids = [txid]
deleted.add(txid)
this.transaction(() => {
while (txids.length) {
const txid = txids.shift()
if (this.onDeleteTransaction) this.onDeleteTransaction(txid)
this.deleteTransactionStmt.run(txid)
this.deleteJigStatesStmt.run(txid)
this.deleteBerryStatesStmt.run(txid)
this.deleteSpendsStmt.run(txid)
this.unspendOutputsStmt.run(txid)
this.deleteDepsStmt.run(txid)
const downtxids = this.getDownstreamStmt.raw(true).all(txid).map(row => row[0])
for (const downtxid of downtxids) {
if (deleted.has(downtxid)) continue
deleted.add(downtxid)
txids.push(downtxid)
}
}
})
}
unconfirmTransaction (txid) {
this.unconfirmTransactionStmt.run(txid)
}
unindexTransaction (txid) {
this.transaction(() => {
if (this.getTransactionIndexedStmt.raw(true).get(txid)[0]) {
this.setTransactionExecutedStmt.run(0, txid)
this.setTransactionIndexedStmt.run(0, txid)
this.deleteJigStatesStmt.run(txid)
this.deleteBerryStatesStmt.run(txid)
this.unmarkExecutingStmt.run(txid)
const downtxids = this.getDownstreamStmt.raw(true).all(txid).map(row => row[0])
downtxids.forEach(downtxid => this.unindexTransaction(downtxid))
if (this.onUnindexTransaction) this.onUnindexTransaction(txid)
}
})
}
hasTransaction (txid) { return !!this.hasTransactionStmt.get(txid) }
isTransactionDownloaded (txid) {
const result = this.getTransactionDownloadedStmt.raw(true).get(txid)
return result && !!result[0]
}
getTransactionsAboveHeight (height) { return this.getTransactionsAboveHeightStmt.raw(true).all(height).map(row => row[0]) }
getMempoolTransactionsBeforeTime (time) { return this.getMempoolTransactionsBeforeTimeStmt.raw(true).all(time).map(row => row[0]) }
getTransactionsToDownload () { return this.getTransactionsToDownloadStmt.raw(true).all().map(row => row[0]) }
getDownloadedCount () { return this.getTransactionsDownloadedCountStmt.get().count }
getIndexedCount () { return this.getTransactionsIndexedCountStmt.get().count }
// --------------------------------------------------------------------------
// spends
// --------------------------------------------------------------------------
getSpend (location) {
const row = this.getSpendStmt.raw(true).get(location)
return row && row[0]
}
// --------------------------------------------------------------------------
// deps
// --------------------------------------------------------------------------
addDep (txid, deptxid) {
this.addNewTransaction(deptxid)
this.addDepStmt.run(deptxid, txid)
if (this.getTransactionFailedStmt.get(deptxid).failed) {
this.setTransactionExecutionFailed(deptxid)
}
}
addMissingDeps (txid, deptxids) {
this.transaction(() => deptxids.forEach(deptxid => this.addDep(txid, deptxid)))
this._checkExecutability(txid)
}
// --------------------------------------------------------------------------
// jig
// --------------------------------------------------------------------------
getJigState (location) {
const row = this.getJigStateStmt.raw(true).get(location)
return row && row[0]
}
// --------------------------------------------------------------------------
// unspent
// --------------------------------------------------------------------------
getAllUnspent () {
return this.getAllUnspentStmt.raw(true).all().map(row => row[0])
}
getAllUnspentByClassOrigin (origin) {
return this.getAllUnspentByClassStmt.raw(true).all(origin).map(row => row[0])
}
getAllUnspentByLockOrigin (origin) {
return this.getAllUnspentByLockStmt.raw(true).all(origin).map(row => row[0])
}
getAllUnspentByScripthash (scripthash) {
return this.getAllUnspentByScripthashStmt.raw(true).all(scripthash).map(row => row[0])
}
getAllUnspentByClassOriginAndLockOrigin (clsOrigin, lockOrigin) {
return this.getAllUnspentByClassLockStmt.raw(true).all(clsOrigin, lockOrigin).map(row => row[0])
}
getAllUnspentByClassOriginAndScripthash (clsOrigin, scripthash) {
return this.getAllUnspentByClassScripthashStmt.raw(true).all(clsOrigin, scripthash).map(row => row[0])
}
getAllUnspentByLockOriginAndScripthash (lockOrigin, scripthash) {
return this.getAllUnspentByLockScripthashStmt.raw(true).all(lockOrigin, scripthash).map(row => row[0])
}
getAllUnspentByClassOriginAndLockOriginAndScripthash (clsOrigin, lockOrigin, scripthash) {
return this.getAllUnspentByClassLockScripthashStmt.raw(true).all(clsOrigin, lockOrigin, scripthash).map(row => row[0])
}
getNumUnspent () {
return this.getNumUnspentStmt.get().unspent
}
// --------------------------------------------------------------------------
// berry
// --------------------------------------------------------------------------
getBerryState (location) {
const row = this.getBerryStateStmt.raw(true).get(location)
return row && row[0]
}
// --------------------------------------------------------------------------
// trust
// --------------------------------------------------------------------------
isTrusted (txid) {
const row = this.isTrustedStmt.raw(true).get(txid)
return !!row && !!row[0]
}
trust (txid) {
if (this.isTrusted(txid)) return
const trusted = [txid]
// Recursively trust code parents
const queue = this.getUpstreamUnexecutedCodeStmt.raw(true).all(txid).map(x => x[0])
const visited = new Set()
while (queue.length) {
const uptxid = queue.shift()
if (visited.has(uptxid)) continue
if (this.isTrusted(uptxid)) continue
visited.add(uptxid)
trusted.push(txid)
this.getUpstreamUnexecutedCodeStmt.raw(true).all(txid).forEach(x => queue.push(x[0]))
}
this.transaction(() => trusted.forEach(txid => this.setTrustedStmt.run(txid, 1)))
trusted.forEach(txid => this._checkExecutability(txid))
if (this.onTrustTransaction) trusted.forEach(txid => this.onTrustTransaction(txid))
}
untrust (txid) {
if (!this.isTrusted(txid)) return
this.transaction(() => {
this.unindexTransaction(txid)
this.setTrustedStmt.run(txid, 0)
})
if (this.onUntrustTransaction) this.onUntrustTransaction(txid)
}
getTrustlist () {
return this.getTrustlistStmt.raw(true).all().map(x => x[0])
}
// --------------------------------------------------------------------------
// ban
// --------------------------------------------------------------------------
isBanned (txid) {
const row = this.isBannedStmt.raw(true).get(txid)
return !!row && !!row[0]
}
ban (txid) {
this.transaction(() => {
this.unindexTransaction(txid)
this.banStmt.run(txid)
})
if (this.onBanTransaction) this.onBanTransaction(txid)
}
unban (txid) {
this.unbanStmt.run(txid)
this._checkExecutability(txid)
if (this.onUnbanTransaction) this.onUnbanTransaction(txid)
}
getBanlist () {
return this.getBanlistStmt.raw(true).all().map(x => x[0])
}
// --------------------------------------------------------------------------
// crawl
// --------------------------------------------------------------------------
getHeight () {
const row = this.getHeightStmt.raw(true).all()[0]
return row && parseInt(row[0])
}
getHash () {
const row = this.getHashStmt.raw(true).all()[0]
return row && row[0]
}
setHeight (height) {
this.setHeightStmt.run(height.toString())
}
setHash (hash) {
this.setHashStmt.run(hash)
}
// --------------------------------------------------------------------------
// internal
// --------------------------------------------------------------------------
loadTransactionsToExecute () {
this.logger.debug('Loading transactions to execute')
const txids = this.db.prepare('SELECT txid FROM executing').raw(true).all().map(x => x[0])
txids.forEach(txid => this._checkExecutability(txid))
}
_checkExecutability (txid) {
const row = this.isReadyToExecuteStmt.get(txid)
if (row && row.ready) {
this.markExecutingStmt.run(txid)
if (this.onReadyToExecute) this.onReadyToExecute(txid)
}
}
}
// ------------------------------------------------------------------------------------------------
Database.HEIGHT_MEMPOOL = HEIGHT_MEMPOOL
Database.HEIGHT_UNKNOWN = HEIGHT_UNKNOWN
module.exports = Database