From f233b76a96fb2ac6f794a097666c53a8d7eec436 Mon Sep 17 00:00:00 2001 From: Nodari Chkuaselidze Date: Mon, 18 Mar 2024 14:53:34 +0400 Subject: [PATCH] walletdb: Write time with blockmeta record. Update wdb version to 3. This requires full wdb block entry wipe and rescan. That is handled by PR #889. `layout.h` is looked up by height, so only missing data was time. Now we can implement walletdb only median time past calculation. --- lib/wallet/client.js | 13 +++++++ lib/wallet/nodeclient.js | 11 ++++++ lib/wallet/nullclient.js | 18 ++++++++++ lib/wallet/records.js | 62 ++++++++++++++++++++++++---------- lib/wallet/walletdb.js | 45 ++++++++++++++++++------ test/wallet-chainstate-test.js | 28 +++++++-------- test/wallet-records-test.js | 9 ++--- 7 files changed, 140 insertions(+), 46 deletions(-) diff --git a/lib/wallet/client.js b/lib/wallet/client.js index 0f7e3a729..7c4616aa0 100644 --- a/lib/wallet/client.js +++ b/lib/wallet/client.js @@ -64,6 +64,18 @@ class WalletClient extends NodeClient { return parseEntry(await super.getEntry(block)); } + /** + * Get entries. + * @param {Number} [start=-1] + * @param {Number} [end=-1] + * @returns {Promise} + */ + + async getEntries(start, end) { + const entries = await super.getEntries(start, end); + return entries.map(parseEntry); + } + async send(tx) { return super.send(tx.encode()); } @@ -138,6 +150,7 @@ function parseEntry(data) { const hash = data.slice(0, 32); const height = encoding.readU32(data, 32); + // skip nonce 4. const time = encoding.readU64(data, 40); const prevBlock = data.slice(48, 80); diff --git a/lib/wallet/nodeclient.js b/lib/wallet/nodeclient.js index cf5a705f3..75b5119e6 100644 --- a/lib/wallet/nodeclient.js +++ b/lib/wallet/nodeclient.js @@ -268,6 +268,17 @@ class NodeClient extends AsyncEmitter { return this.node.chain.getHashes(start, end); } + /** + * Get entries range. + * @param {Number} start + * @param {Number} end + * @returns {Promise} + */ + + async getEntries(start = -1, end = -1) { + return this.node.chain.getEntries(start, end); + } + /** * Rescan for any missed transactions. * @param {Number|Hash} start - Start block. diff --git a/lib/wallet/nullclient.js b/lib/wallet/nullclient.js index 6a8030f8d..2dbd06ef1 100644 --- a/lib/wallet/nullclient.js +++ b/lib/wallet/nullclient.js @@ -9,6 +9,7 @@ const assert = require('bsert'); const EventEmitter = require('events'); const NameState = require('../covenants/namestate'); +const Block = require('../primitives/block'); const util = require('../utils/util'); /** @@ -173,6 +174,23 @@ class NullClient extends EventEmitter { return [this.network.genesis.hash]; } + /** + * Get entries. + * @param {Number} [start=-1] + * @param {Number} [end=-1] + * @returns {Promise} + */ + + async getEntries(start = -1, end = -1) { + const genesisBlock = Block.decode(this.network.genesisBlock); + const entry = { + hash: genesisBlock.hash(), + height: 0, + time: genesisBlock.time + }; + return [entry]; + } + /** * Rescan for any missed transactions. * @param {Number|Hash} start - Start block. diff --git a/lib/wallet/records.js b/lib/wallet/records.js index a36f39d0e..91da78780 100644 --- a/lib/wallet/records.js +++ b/lib/wallet/records.js @@ -118,12 +118,40 @@ class BlockMeta extends bio.Struct { } /** - * Get block meta hash as a buffer. + * Encode hash and time. * @returns {Buffer} */ - toHash() { - return this.hash; + toHashAndTime() { + const data = Buffer.allocUnsafe(32 + 8); + bio.writeBytes(data, this.hash, 0); + bio.writeU64(data, this.time, 32); + return data; + } + + /** + * Decode hash and time. + * @param {Buffer} data + * @param {Number} height + * @returns {BlockMeta} + */ + + fromHashAndTime(data, height) { + this.hash = data.slice(0, 32); + this.time = bio.readU64(data, 32); + this.height = height; + return this; + } + + /** + * Instantiate block meta from hash and time. + * @param {Buffer} data + * @param {Number} height + * @returns {BlockMeta} + */ + + static fromHashAndTime(data, height) { + return new this().fromHashAndTime(data, height); } /** @@ -139,6 +167,16 @@ class BlockMeta extends bio.Struct { return this; } + /** + * Instantiate block meta from chain entry. + * @param {ChainEntry} entry + * @returns {BlockMeta} + */ + + static fromEntry(entry) { + return new this().fromEntry(entry); + } + /** * Instantiate block meta from serialized tip data. * @private @@ -148,27 +186,17 @@ class BlockMeta extends bio.Struct { read(br) { this.hash = br.readHash(); this.height = br.readU32(); - this.time = br.readU32(); + this.time = br.readU64(); return this; } - /** - * Instantiate block meta from chain entry. - * @param {ChainEntry} entry - * @returns {BlockMeta} - */ - - static fromEntry(entry) { - return new this().fromEntry(entry); - } - /** * Calculate size. * @returns {Number} */ getSize() { - return 40; + return 44; } /** @@ -179,7 +207,7 @@ class BlockMeta extends bio.Struct { write(bw) { bw.writeHash(this.hash); bw.writeU32(this.height); - bw.writeU32(this.time); + bw.writeU64(this.time); return bw; } @@ -462,5 +490,3 @@ exports.ChainState = ChainState; exports.BlockMeta = BlockMeta; exports.TXRecord = TXRecord; exports.MapRecord = MapRecord; - -module.exports = exports; diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 5fb9d8b16..3333dba96 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -17,10 +17,12 @@ const Logger = require('blgr'); const {safeEqual} = require('bcrypto/lib/safe'); const aes = require('bcrypto/lib/aes'); const Network = require('../protocol/network'); +const consensus = require('../protocol/consensus'); const Path = require('./path'); const common = require('./common'); const Wallet = require('./wallet'); const Account = require('./account'); +const Block = require('../primitives/block'); const Outpoint = require('../primitives/outpoint'); const layouts = require('./layout'); const records = require('./records'); @@ -342,6 +344,25 @@ class WalletDB extends EventEmitter { return undefined; } + /** + * Add genesis block. + * @returns {Promise} + */ + + async saveGenesis() { + // Write genesis block. + const network = this.network; + const block = Block.decode(network.genesisBlock); + const entry = { + hash: block.hash(), + height: 0, + time: block.time, + prevBlock: consensus.ZERO_HASH + }; + + await this.addBlock(entry, []); + } + /** * Close the walletdb, wait for the database to close. * @returns {Promise} @@ -453,6 +474,7 @@ class WalletDB extends EventEmitter { await this.syncInitState(); await this.syncFilter(); await this.syncChain(); + this.rescanning = false; await this.resend(); } finally { this.rescanning = false; @@ -468,8 +490,10 @@ class WalletDB extends EventEmitter { async loadState() { const cache = await this.getState(); - if (!cache) + if (!cache) { + await this.saveGenesis(); return; + } this.logger.info('Initialized chain state from the database.'); this.hasStateCache = true; @@ -490,14 +514,15 @@ class WalletDB extends EventEmitter { this.logger.info('Initializing database state from server.'); const b = this.db.batch(); - const hashes = await this.client.getHashes(); + const entries = await this.client.getEntries(); let tip = null; - for (let height = 0; height < hashes.length; height++) { - const hash = hashes[height]; - const meta = new BlockMeta(hash, height); - b.put(layout.h.encode(height), meta.toHash()); + for (let height = 0; height < entries.length; height++) { + const entry = entries[height]; + assert(entry.height === height); + const meta = new BlockMeta(entry.hash, entry.height, entry.time); + b.put(layout.h.encode(height), meta.toHashAndTime()); tip = meta; } @@ -1969,7 +1994,7 @@ class WalletDB extends EventEmitter { } // Save tip and state. - b.put(layout.h.encode(tip.height), tip.toHash()); + b.put(layout.h.encode(tip.height), tip.toHashAndTime()); b.put(layout.R.encode(), state.encode()); await b.write(); @@ -2257,12 +2282,12 @@ class WalletDB extends EventEmitter { */ async getBlock(height) { - const hash = await this.db.get(layout.h.encode(height)); + const data = await this.db.get(layout.h.encode(height)); - if (!hash) + if (!data) return null; - return new BlockMeta(hash, height); + return BlockMeta.fromHashAndTime(data, height); } /** diff --git a/test/wallet-chainstate-test.js b/test/wallet-chainstate-test.js index 3adfe8f51..43f6b16d4 100644 --- a/test/wallet-chainstate-test.js +++ b/test/wallet-chainstate-test.js @@ -396,7 +396,7 @@ describe('WalletDB ChainState', function() { } assert.strictEqual(wdb.state.startHeight, firstBlock.height); - assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction * 2); assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction * 2); assert.strictEqual(wdb.state.marked, true); @@ -407,7 +407,7 @@ describe('WalletDB ChainState', function() { const tip = await wdb.getTip(); assert.strictEqual(wdb.state.startHeight, tip.height); - assert.strictEqual(wdb.state.startHash, tip.hash); + assert.bufferEqual(wdb.state.startHash, tip.hash); assert.strictEqual(wdb.height, noTXBuffer); assert.strictEqual(wdb.state.height, noTXBuffer); assert.strictEqual(wdb.state.marked, false); @@ -422,14 +422,14 @@ describe('WalletDB ChainState', function() { firstBlock = blockAndTXs.block; assert.strictEqual(wdb.state.startHeight, firstBlock.height); - assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); assert.strictEqual(wdb.height, noTXBuffer + i + 1); assert.strictEqual(wdb.state.height, noTXBuffer + i + 1); assert.strictEqual(wdb.state.marked, true); } assert.strictEqual(wdb.state.startHeight, firstBlock.height); - assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction); assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction); assert.strictEqual(wdb.state.marked, true); @@ -452,7 +452,7 @@ describe('WalletDB ChainState', function() { } assert.strictEqual(wdb.state.startHeight, firstBlock.height); - assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction * 2); assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction * 2); assert.strictEqual(wdb.state.marked, true); @@ -463,7 +463,7 @@ describe('WalletDB ChainState', function() { const tip = await wdb.getTip(); assert.strictEqual(wdb.state.startHeight, tip.height); - assert.strictEqual(wdb.state.startHash, tip.hash); + assert.bufferEqual(wdb.state.startHash, tip.hash); assert.strictEqual(wdb.height, noTXBuffer); assert.strictEqual(wdb.state.height, noTXBuffer); assert.strictEqual(wdb.state.marked, false); @@ -472,14 +472,14 @@ describe('WalletDB ChainState', function() { await progressWithNoTX(wdb); assert.strictEqual(wdb.state.startHeight, tip.height); - assert.strictEqual(wdb.state.startHash, tip.hash); + assert.bufferEqual(wdb.state.startHash, tip.hash); assert.strictEqual(wdb.height, noTXBuffer + i + 1); assert.strictEqual(wdb.state.height, noTXBuffer + i + 1); assert.strictEqual(wdb.state.marked, false); } assert.strictEqual(wdb.state.startHeight, tip.height); - assert.strictEqual(wdb.state.startHash, tip.hash); + assert.bufferEqual(wdb.state.startHash, tip.hash); assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction); assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction); assert.strictEqual(wdb.state.marked, false); @@ -493,14 +493,14 @@ describe('WalletDB ChainState', function() { removeBlocks.push(blockAndTXs); assert.strictEqual(wdb.state.startHeight, firstBlock.height); - assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction + i + 1); assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction + i + 1); assert.strictEqual(wdb.state.marked, true); } assert.strictEqual(wdb.state.startHeight, firstBlock.height); - assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction * 2); assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction * 2); assert.strictEqual(wdb.state.marked, true); @@ -517,7 +517,7 @@ describe('WalletDB ChainState', function() { await progressWithNoTX(wdb); assert.strictEqual(wdb.state.startHeight, 0); - assert.strictEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.bufferEqual(wdb.state.startHash, consensus.ZERO_HASH); assert.strictEqual(wdb.height, noTXBuffer); assert.strictEqual(wdb.state.height, noTXBuffer); assert.strictEqual(wdb.state.marked, false); @@ -539,7 +539,7 @@ describe('WalletDB ChainState', function() { assert.strictEqual(err.message, 'Corruption'); assert.strictEqual(wdb.state.startHeight, 0); - assert.strictEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.bufferEqual(wdb.state.startHash, consensus.ZERO_HASH); assert.strictEqual(wdb.height, noTXBuffer); assert.strictEqual(wdb.state.height, noTXBuffer); assert.strictEqual(wdb.state.marked, false); @@ -551,7 +551,7 @@ describe('WalletDB ChainState', function() { await progressWithNoTX(wdb); assert.strictEqual(wdb.state.startHeight, 0); - assert.strictEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.bufferEqual(wdb.state.startHash, consensus.ZERO_HASH); assert.strictEqual(wdb.height, noTXBuffer + i + 1); assert.strictEqual(wdb.state.height, noTXBuffer + i + 1); assert.strictEqual(wdb.state.marked, false); @@ -559,7 +559,7 @@ describe('WalletDB ChainState', function() { const {block} = await progressWithTX(wdb); assert.strictEqual(wdb.state.startHeight, block.height); - assert.strictEqual(wdb.state.startHash, block.hash); + assert.bufferEqual(wdb.state.startHash, block.hash); assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction + 1); assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction + 1); assert.strictEqual(wdb.state.marked, true); diff --git a/test/wallet-records-test.js b/test/wallet-records-test.js index 744282551..d3e3adc65 100644 --- a/test/wallet-records-test.js +++ b/test/wallet-records-test.js @@ -227,12 +227,13 @@ describe('Wallet Records', function() { compareBlockMeta(decoded, data); }); - it('should return block hash', () => { + it('should return block hash and time and decode', () => { const data = getRandomBlockMetaData(); - const meta = new BlockMeta(); - meta.inject(data); + const meta = BlockMeta.fromEntry(data); - assert.bufferEqual(meta.toHash(), data.hash); + const hashAndTime = meta.toHashAndTime(); + const meta2 = BlockMeta.fromHashAndTime(hashAndTime, data.height); + compareBlockMeta(meta, meta2); }); it('should be created from ChainEntry', () => {