wallet: add by height indexes for credits.

This commit is contained in:
Nodari Chkuaselidze 2025-05-01 17:28:19 +04:00
parent 3c94eb369d
commit b8ce31e9d8
No known key found for this signature in database
GPG key ID: B018A7BB437D1F05
4 changed files with 229 additions and 106 deletions

View file

@ -162,8 +162,10 @@ exports.txdb = {
s: bdb.key('s', ['hash256', 'uint32']),
// Coin Selector
// by value + account
// by account + value
SV: bdb.key('SV', ['uint32', 'uint64', 'hash256', 'uint32']),
// by account + height
SH: bdb.key('SH', ['uint32', 'uint32', 'hash256', 'uint32']),
// Transaction
t: bdb.key('t', ['hash256']),

View file

@ -202,14 +202,25 @@ class TXDB {
* @param {Batch} b
* @param {Credit} credit
* @param {Path} path
* @param {Number?} oldHeight
*/
indexCSCredit(b, credit, path) {
indexCSCredit(b, credit, path, oldHeight) {
const {coin} = credit;
// index coin by value + account.
b.put(layout.SV.encode(
path.account, coin.value, coin.hash, coin.index), null);
const height = coin.height === -1 ? UNCONFIRMED_HEIGHT : coin.height;
b.put(layout.SH.encode(
path.account, height, coin.hash, coin.index), null);
if (oldHeight != null) {
const height = oldHeight === -1 ? UNCONFIRMED_HEIGHT : oldHeight;
b.del(layout.SH.encode(
path.account, height, coin.hash, coin.index));
}
}
/**
@ -222,8 +233,12 @@ class TXDB {
unindexCSCredit(b, credit, path) {
const {coin} = credit;
// Remove coin by value + account.
// Remove coin by account + value.
b.del(layout.SV.encode(path.account, coin.value, coin.hash, coin.index));
// Remove coin by account + height
const height = coin.height === -1 ? UNCONFIRMED_HEIGHT : coin.height;
b.del(layout.SH.encode(path.account, height, coin.hash, coin.index));
}
/**
@ -1192,7 +1207,7 @@ class TXDB {
}
await this.saveCredit(b, credit, path);
this.indexCSCredit(b, credit, path);
this.indexCSCredit(b, credit, path, null);
await this.watchOpensEarly(b, output);
}
@ -1429,7 +1444,7 @@ class TXDB {
credit.coin.height = height;
await this.saveCredit(b, credit, path);
this.indexCSCredit(b, credit, path);
this.indexCSCredit(b, credit, path, -1);
}
// Handle names.
@ -1554,7 +1569,7 @@ class TXDB {
credit.spent = false;
await this.saveCredit(b, credit, path);
this.indexCSCredit(b, credit, path);
this.indexCSCredit(b, credit, path, null);
}
}
@ -1818,7 +1833,7 @@ class TXDB {
credit.spent = true;
own = true;
await this.saveCredit(b, credit, path);
this.indexCSCredit(b, credit, path);
this.indexCSCredit(b, credit, path, null);
}
}
@ -1870,6 +1885,7 @@ class TXDB {
// Update coin height and confirmed
// balance. Save once again.
const oldHeight = credit.coin.height;
credit.coin.height = -1;
// If the coin was not discovered now, it means
@ -1882,7 +1898,7 @@ class TXDB {
}
await this.saveCredit(b, credit, path);
this.indexCSCredit(b, credit, path);
this.indexCSCredit(b, credit, path, oldHeight);
}
// Unconfirm will also index OPENs as the transaction is now part of the
@ -3815,6 +3831,58 @@ class TXDB {
}
}
/**
* Get credits iterator sorted by height.
* @param {Number} acct
* @param {Object} [options]
* @param {Number} [options.minHeight=0]
* @param {Number} [options.maxHeight=UNCONFIRMED_HEIGHT]
* @param {Number} [options.limit=-1]
* @param {Boolean} [options.reverse=false]
* @returns {AsyncGenerator<Credit>}
*/
async *getAccountCreditIterByHeight(acct, options = {}) {
assert(typeof acct === 'number');
assert(acct >= 0);
assert(options && typeof options === 'object');
let minHeight = 0;
let maxHeight = UNCONFIRMED_HEIGHT;
if (options.minHeight != null) {
assert(typeof options.minHeight === 'number');
minHeight = options.minHeight;
}
if (options.maxHeight != null) {
assert(typeof options.maxHeight === 'number');
maxHeight = options.maxHeight;
}
assert(minHeight <= maxHeight);
const min = layout.SH.min(acct, minHeight);
const max = layout.SH.max(acct, maxHeight);
const iter = this.bucket.iterator({
gte: min,
lte: max,
limit: options.limit,
reverse: options.reverse,
keys: true,
values: false
});
for await (const {key} of iter) {
const [,, hash, index] = layout.SH.decode(key);
const credit = await this.getCredit(hash, index);
assert(credit);
yield credit;
}
}
/**
* Get a coin viewpoint.
* @param {TX} tx

View file

@ -5163,6 +5163,21 @@ class Wallet extends EventEmitter {
return this.txdb.getAccountCreditIterByValue(acct, options);
}
/**
* Get credits iterator sorted by height.
* @param {Number} acct
* @param {Object} [options]
* @param {Number} [options.minHeight=0]
* @param {Number} [options.maxHeight=UNCONFIRMED_HEIGHT]
* @param {Number} [options.limit=-1]
* @param {Boolean} [options.reverse=false]
* @returns {AsyncGenerator<Credit>}
*/
getAccountCreditIterByHeight(acct, options = {}) {
return this.txdb.getAccountCreditIterByHeight(acct, options);
}
/**
* Get "smart" coins.
* @param {(String|Number)?} acct

View file

@ -44,7 +44,13 @@ describe('Wallet Coin Selection', function() {
wallet = null;
};
describe('Coin Selection Indexes', function() {
const indexes = [
'value',
'height'
];
for (const indexType of indexes) {
describe(`Coin Selection Indexes (${indexType})`, function() {
const TX_OPTIONS = [
{ value: 2e6, address: primutils.randomP2PKAddress() },
// address will be generated using wallet.
@ -57,33 +63,36 @@ describe('Wallet Coin Selection', function() {
const TOTAL_COINS = 4;
const TOTAL_FUNDS = 1e6 + 2e6 + 3e6 + 4e6;
let isSorted, getIterMethodName;
before(() => {
switch (indexType) {
case 'value':
isSorted = isSortedByValue;
getIterMethodName = 'getAccountCreditIterByValue';
break;
case 'height':
isSorted = isSortedByHeight;
getIterMethodName = 'getAccountCreditIterByHeight';
break;
default:
throw new Error('Invalid index type.');
}
});
beforeEach(beforeFn);
afterEach(afterFn);
/**
* @param {Credit[]} credits
* @returns {Boolean}
*/
const isSortedByValue = (credit) => {
for (let i = 1; i < credit.length; i++) {
if (credit[i].value > credit[i - 1].value)
return false;
}
return true;
};
it('should index unconfirmed tx output', async () => {
const txs = await createInboundTXs(wallet, TX_OPTIONS, false);
await wallet.wdb.addTX(txs[0]);
const iter = wallet.getAccountCreditIterByValue(0);
const creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
const iter = wallet[getIterMethodName](0);
const credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, -1);
assert.strictEqual(credit.spent, false);
}
@ -100,12 +109,12 @@ describe('Wallet Coin Selection', function() {
await wdb.addTX(spendAll.toTX());
// We still have the coin, even thought it is flagged: .spent = true
const iter = wallet.getAccountCreditIterByValue(0);
const creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
const iter = wallet[getIterMethodName](0);
const credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, curBlock(wdb).height);
assert.strictEqual(credit.spent, true);
}
@ -115,12 +124,12 @@ describe('Wallet Coin Selection', function() {
await fundWallet(wallet, TX_OPTIONS, false);
const currentBlock = curBlock(wdb);
const iter = wallet.getAccountCreditIterByValue(0);
const creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
const iter = wallet[getIterMethodName](0);
const credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, currentBlock.height);
assert.strictEqual(credit.spent, false);
}
@ -135,33 +144,33 @@ describe('Wallet Coin Selection', function() {
outputs: [{ value: TOTAL_FUNDS, address: primutils.randomP2PKAddress() }]
});
let iter = wallet.getAccountCreditIterByValue(0);
let creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
let iter = wallet[getIterMethodName](0);
let credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, currentBlock.height);
assert.strictEqual(credit.spent, false);
}
await wdb.addBlock(nextBlock(wdb), [spendAll.toTX()]);
iter = wallet.getAccountCreditIterByValue(0);
creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, 0);
iter = wallet[getIterMethodName](0);
credits = await collectIter(iter);
assert.strictEqual(credits.length, 0);
});
it('should index confirm tx output', async () => {
const txs = await createInboundTXs(wallet, TX_OPTIONS, false);
await wdb.addTX(txs[0]);
let iter = wallet.getAccountCreditIterByValue(0);
let creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
let iter = wallet[getIterMethodName](0);
let credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, -1);
assert.strictEqual(credit.spent, false);
}
@ -169,12 +178,12 @@ describe('Wallet Coin Selection', function() {
await wdb.addBlock(nextBlock(wdb), txs);
const currentBlock = curBlock(wdb);
iter = wallet.getAccountCreditIterByValue(0);
creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
iter = wallet[getIterMethodName](0);
credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, currentBlock.height);
assert.strictEqual(credit.spent, false);
}
@ -192,21 +201,21 @@ describe('Wallet Coin Selection', function() {
await wdb.addTX(spendAllTX);
let iter = wallet.getAccountCreditIterByValue(0);
let creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
let iter = wallet[getIterMethodName](0);
let credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, currentBlock.height);
assert.strictEqual(credit.spent, true);
}
await wdb.addBlock(nextBlock(wdb), [spendAllTX]);
iter = wallet.getAccountCreditIterByValue(0);
creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, 0);
iter = wallet[getIterMethodName](0);
credits = await collectIter(iter);
assert.strictEqual(credits.length, 0);
});
it('should index disconnect tx output', async () => {
@ -214,12 +223,12 @@ describe('Wallet Coin Selection', function() {
const currentBlock = curBlock(wdb);
let iter = wallet.getAccountCreditIterByValue(0);
let creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
let iter = wallet[getIterMethodName](0);
let credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, currentBlock.height);
assert.strictEqual(credit.spent, false);
}
@ -228,12 +237,12 @@ describe('Wallet Coin Selection', function() {
await wdb.removeBlock(currentBlock);
// Only thing that must change is the HEIGHT.
iter = wallet.getAccountCreditIterByValue(0);
creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
iter = wallet[getIterMethodName](0);
credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, -1);
assert.strictEqual(credit.spent, false);
}
@ -251,18 +260,18 @@ describe('Wallet Coin Selection', function() {
const spendAllTX = spendAll.toTX();
await wdb.addBlock(nextBlock(wdb), [spendAllTX]);
let iter = wallet.getAccountCreditIterByValue(0);
let creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, 0);
let iter = wallet[getIterMethodName](0);
let credits = await collectIter(iter);
assert.strictEqual(credits.length, 0);
await wdb.removeBlock(curBlock(wdb));
iter = wallet.getAccountCreditIterByValue(0);
creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
iter = wallet[getIterMethodName](0);
credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, createCoinHeight);
assert.strictEqual(credit.spent, true);
}
@ -272,12 +281,12 @@ describe('Wallet Coin Selection', function() {
const txs = await createInboundTXs(wallet, TX_OPTIONS, false);
await wdb.addTX(txs[0]);
let iter = wallet.getAccountCreditIterByValue(0);
let creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
let iter = wallet[getIterMethodName](0);
let credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, -1);
assert.strictEqual(credit.spent, false);
}
@ -289,9 +298,9 @@ describe('Wallet Coin Selection', function() {
await wdb.addBlock(nextBlock(wdb), [mtx.toTX()]);
iter = wallet.getAccountCreditIterByValue(0);
creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, 0);
iter = wallet[getIterMethodName](0);
credits = await collectIter(iter);
assert.strictEqual(credits.length, 0);
});
it('should index erase tx input', async () => {
@ -305,12 +314,12 @@ describe('Wallet Coin Selection', function() {
await wdb.addTX(spendAll.toTX());
let iter = wallet.getAccountCreditIterByValue(0);
let creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
let iter = wallet[getIterMethodName](0);
let credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, -1);
assert.strictEqual(credit.spent, true);
}
@ -322,9 +331,9 @@ describe('Wallet Coin Selection', function() {
await wdb.addBlock(nextBlock(wdb), [mtx.toTX()]);
iter = wallet.getAccountCreditIterByValue(0);
creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, 0);
iter = wallet[getIterMethodName](0);
credits = await collectIter(iter);
assert.strictEqual(credits.length, 0);
});
it('should index erase (block) tx output', async () => {
@ -336,23 +345,24 @@ describe('Wallet Coin Selection', function() {
const currentBlock = curBlock(wdb);
let iter = wallet.getAccountCreditIterByValue(0);
let creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, TOTAL_COINS);
assert(isSortedByValue(creditsByValue), 'Credits not sorted.');
let iter = wallet[getIterMethodName](0);
let credits = await collectIter(iter);
assert.strictEqual(credits.length, TOTAL_COINS);
assert(isSorted(credits), 'Credits not sorted.');
for (const credit of creditsByValue) {
for (const credit of credits) {
assert.strictEqual(credit.coin.height, currentBlock.height);
assert.strictEqual(credit.spent, false);
}
await wdb.removeBlock(currentBlock);
iter = wallet.getAccountCreditIterByValue(0);
creditsByValue = await collectIter(iter);
assert.strictEqual(creditsByValue.length, 0);
iter = wallet[getIterMethodName](0);
credits = await collectIter(iter);
assert.strictEqual(credits.length, 0);
});
});
}
describe('Selection types', function() {
beforeEach(beforeFn);
@ -652,3 +662,31 @@ async function collectIter(iter) {
return items;
}
/**
* @param {Credit[]} credits
* @returns {Boolean}
*/
function isSortedByValue(credits) {
for (let i = 1; i < credits.length; i++) {
if (credits[i].value > credits[i - 1].value)
return false;
}
return true;
};
/**
* @param {Credit[]} credits
* @returns {Boolean}
*/
function isSortedByHeight(credits) {
for (let i = 1; i < credits.length; i++) {
if (credits[i].coin.height > credits[i - 1].coin.height)
return false;
}
return true;
};