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.
This commit is contained in:
Nodari Chkuaselidze 2024-03-18 14:53:34 +04:00
parent c38e4b9298
commit f233b76a96
No known key found for this signature in database
GPG key ID: B018A7BB437D1F05
7 changed files with 142 additions and 48 deletions

View file

@ -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<Object[]>}
*/
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);

View file

@ -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<ChainEntry[]>}
*/
async getEntries(start = -1, end = -1) {
return this.node.chain.getEntries(start, end);
}
/**
* Rescan for any missed transactions.
* @param {Number|Hash} start - Start block.

View file

@ -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.

View file

@ -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,19 +167,6 @@ class BlockMeta extends bio.Struct {
return this;
}
/**
* Instantiate block meta from serialized tip data.
* @private
* @param {Buffer} data
*/
read(br) {
this.hash = br.readHash();
this.height = br.readU32();
this.time = br.readU32();
return this;
}
/**
* Instantiate block meta from chain entry.
* @param {ChainEntry} entry
@ -162,13 +177,26 @@ class BlockMeta extends bio.Struct {
return new this().fromEntry(entry);
}
/**
* Instantiate block meta from serialized tip data.
* @private
* @param {Buffer} data
*/
read(br) {
this.hash = br.readHash();
this.height = br.readU32();
this.time = br.readU64();
return this;
}
/**
* 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;

View file

@ -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);
}
/**

View file

@ -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);

View file

@ -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', () => {