mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-01-12 16:53:19 +08:00
feat: stx addr encoding LRU cache
This commit is contained in:
3
.env
3
.env
@@ -84,3 +84,6 @@ MAINNET_SEND_MANY_CONTRACT_ID=SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.send-man
|
||||
# IMGIX_DOMAIN=https://<your domain>.imgix.net
|
||||
# IMGIX_TOKEN=<your token>
|
||||
|
||||
# Specify max number of STX address to store in an in-memory LRU cache (CPU optimization).
|
||||
# Defaults to 50,000, which should result in around 25 megabytes of additional memory usage.
|
||||
# STACKS_ADDRESS_CACHE_SIZE=10000
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -2489,6 +2489,11 @@
|
||||
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw=="
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
|
||||
@@ -12436,7 +12441,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
@@ -18025,8 +18029,7 @@
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"yaml": {
|
||||
"version": "1.10.2",
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"@stacks/transactions": "^v2.0.1",
|
||||
"@types/dockerode": "^2.5.34",
|
||||
"@types/express-list-endpoints": "^4.0.1",
|
||||
"@types/lru-cache": "^5.1.1",
|
||||
"@types/ws": "^7.2.5",
|
||||
"big-integer": "^1.6.48",
|
||||
"bignumber.js": "^9.0.1",
|
||||
@@ -131,6 +132,7 @@
|
||||
"http-proxy-middleware": "^2.0.1",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"jsonrpc-lite": "^2.1.0",
|
||||
"lru-cache": "^6.0.0",
|
||||
"micro-base58": "^0.5.0",
|
||||
"nock": "^13.1.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
|
||||
90
src/c32-addr-cache.ts
Normal file
90
src/c32-addr-cache.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
This module is hacky and does things that should generally be avoided in this codebase. We are using a procedure to
|
||||
"re-export" a function on an existing module in order to add some functionality specific to the API. In this case,
|
||||
the `c32check.c32address` function is difficult to override within this codebase. Many entry points into the function
|
||||
are from calls into stacks.js libs. A cleaner solution would involve implementing optional params to many stacks.js
|
||||
functions to provide a stx address encoding function. However, that would be a significant change to the stacks.js libs,
|
||||
and for now this approach is much easier and faster.
|
||||
*/
|
||||
|
||||
import * as c32check from 'c32check';
|
||||
import * as LruCache from 'lru-cache';
|
||||
|
||||
type c32AddressFn = typeof c32check.c32address;
|
||||
|
||||
const MAX_ADDR_CACHE_SIZE = 50_000;
|
||||
export const ADDR_CACHE_ENV_VAR = 'STACKS_ADDRESS_CACHE_SIZE';
|
||||
|
||||
let addressLruCache: LruCache<string, string> | undefined;
|
||||
export function getAddressLruCache() {
|
||||
if (addressLruCache === undefined) {
|
||||
let cacheSize = MAX_ADDR_CACHE_SIZE;
|
||||
const envAddrCacheVar = process.env[ADDR_CACHE_ENV_VAR];
|
||||
if (envAddrCacheVar) {
|
||||
cacheSize = Number.parseInt(envAddrCacheVar);
|
||||
}
|
||||
addressLruCache = new LruCache<string, string>({ max: cacheSize });
|
||||
}
|
||||
return addressLruCache;
|
||||
}
|
||||
|
||||
const c32EncodeInjectedSymbol = Symbol();
|
||||
let origC32AddressProp: PropertyDescriptor | undefined;
|
||||
let origC32AddressFn: c32AddressFn | undefined;
|
||||
|
||||
function createC32AddressCache(origFn: c32AddressFn): c32AddressFn {
|
||||
const c32addressCached: c32AddressFn = (version, hash160hex) => {
|
||||
const cacheKey = `${version}${hash160hex}`;
|
||||
const addrCache = getAddressLruCache();
|
||||
let addrVal = addrCache.get(cacheKey);
|
||||
if (addrVal === undefined) {
|
||||
addrVal = origFn(version, hash160hex);
|
||||
addrCache.set(cacheKey, addrVal);
|
||||
}
|
||||
return addrVal;
|
||||
};
|
||||
Object.defineProperty(c32addressCached, c32EncodeInjectedSymbol, { value: true });
|
||||
return c32addressCached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the `c32address` function on the `c32check` module to use an LRU cache
|
||||
* where commonly used encoded address strings can be cached.
|
||||
*/
|
||||
export function injectC32addressEncodeCache() {
|
||||
// Skip if already injected
|
||||
if (c32EncodeInjectedSymbol in c32check.c32address) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const c32checkModule = require('c32check');
|
||||
const origProp = Object.getOwnPropertyDescriptor(c32checkModule, 'c32address');
|
||||
if (!origProp) {
|
||||
throw new Error(`Could not get property descriptor for 'c32address' on module 'c32check'`);
|
||||
}
|
||||
origC32AddressProp = origProp;
|
||||
const origFn = origProp.get?.();
|
||||
if (!origFn) {
|
||||
throw new Error(`Falsy result for 'c32address' property getter on 'c32check' module`);
|
||||
}
|
||||
origC32AddressFn = origFn;
|
||||
const newFn = createC32AddressCache(origFn);
|
||||
|
||||
// The exported module object specifies a property with a getter and setter (rather than simple indexer value),
|
||||
// so use `defineProperty` to work around errors from trying to set/re-define the property with `c32checkModule.c32address = newFn`.
|
||||
Object.defineProperty(c32checkModule, 'c32address', { get: () => newFn });
|
||||
}
|
||||
|
||||
export function restoreC32AddressModule() {
|
||||
if (addressLruCache !== undefined) {
|
||||
addressLruCache.reset();
|
||||
addressLruCache = undefined;
|
||||
}
|
||||
|
||||
if (origC32AddressProp !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const c32checkModule = require('c32check');
|
||||
Object.defineProperty(c32checkModule, 'c32address', origC32AddressProp);
|
||||
origC32AddressProp = undefined;
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,14 @@ import * as getopts from 'getopts';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as net from 'net';
|
||||
import { injectC32addressEncodeCache } from './c32-addr-cache';
|
||||
|
||||
loadDotEnv();
|
||||
|
||||
sourceMapSupport.install({ handleUncaughtExceptions: false });
|
||||
|
||||
injectC32addressEncodeCache();
|
||||
|
||||
registerShutdownConfig();
|
||||
|
||||
async function monitorCoreRpcConnection(): Promise<void> {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as c32check from 'c32check';
|
||||
import * as c32AddrCache from '../c32-addr-cache';
|
||||
import { ADDR_CACHE_ENV_VAR } from '../c32-addr-cache';
|
||||
import { getCurrentGitTag, has0xPrefix, isValidBitcoinAddress } from '../helpers';
|
||||
|
||||
test('get git tag', () => {
|
||||
@@ -16,6 +18,52 @@ describe('has0xPrefix()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('c32address lru caching', () => {
|
||||
c32AddrCache.restoreC32AddressModule();
|
||||
const origAddrCacheEnvVar = process.env[ADDR_CACHE_ENV_VAR];
|
||||
process.env[ADDR_CACHE_ENV_VAR] = '5';
|
||||
try {
|
||||
// No LRU cache used for c32address fn
|
||||
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(0);
|
||||
const stxAddr1 = 'SP2JKEZC09WVMR33NBSCWQAJC5GS590RP1FR9CK55';
|
||||
const decodedAddr1 = c32check.c32addressDecode(stxAddr1);
|
||||
const encodeResult1 = c32check.c32address(decodedAddr1[0], decodedAddr1[1]);
|
||||
expect(encodeResult1).toBe(stxAddr1);
|
||||
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(0);
|
||||
|
||||
// Inject LRU cache into c32address fn, ensure it gets used
|
||||
c32AddrCache.injectC32addressEncodeCache();
|
||||
expect(c32AddrCache.getAddressLruCache().max).toBe(5);
|
||||
|
||||
const encodeResult2 = c32check.c32address(decodedAddr1[0], decodedAddr1[1]);
|
||||
expect(encodeResult2).toBe(stxAddr1);
|
||||
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(1);
|
||||
|
||||
const encodeResult3 = c32check.c32address(decodedAddr1[0], decodedAddr1[1]);
|
||||
expect(encodeResult3).toBe(stxAddr1);
|
||||
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(1);
|
||||
|
||||
// Test max cache size
|
||||
c32AddrCache.getAddressLruCache().reset();
|
||||
for (let i = 1; i < 10; i++) {
|
||||
// hash160 hex string
|
||||
const buff = Buffer.alloc(20);
|
||||
buff[i] = i;
|
||||
c32check.c32address(1, buff.toString('hex'));
|
||||
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(Math.min(i, 5));
|
||||
}
|
||||
|
||||
// Sanity check: reset c32 lib to original state, ensure no LRU cache used
|
||||
c32AddrCache.restoreC32AddressModule();
|
||||
const encodeResult4 = c32check.c32address(decodedAddr1[0], decodedAddr1[1]);
|
||||
expect(encodeResult4).toBe(stxAddr1);
|
||||
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(0);
|
||||
} finally {
|
||||
process.env[ADDR_CACHE_ENV_VAR] = origAddrCacheEnvVar;
|
||||
c32AddrCache.restoreC32AddressModule();
|
||||
}
|
||||
});
|
||||
|
||||
test('bitcoin<->stacks address', () => {
|
||||
const mainnetStxAddr = 'SP2JKEZC09WVMR33NBSCWQAJC5GS590RP1FR9CK55';
|
||||
const mainnetBtcAddr = '1G4ayBXJvxZMoZpaNdZG6VyWwWq2mHpMjQ';
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"build": "rimraf ./lib && npm run build:node",
|
||||
"build:node": "tsc",
|
||||
"start": "node ./lib/utils/src/index.js"
|
||||
"start": "node ./lib/utils/src/index.js",
|
||||
"address-cache-test": "npm run build && NODE_ENV=production node --expose-gc ./lib/utils/src/addr-lru-cache-test.js"
|
||||
},
|
||||
"prettier": "@stacks/prettier-config",
|
||||
"dependencies": {
|
||||
|
||||
63
utils/src/addr-lru-cache-test.ts
Normal file
63
utils/src/addr-lru-cache-test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import * as util from 'util';
|
||||
import * as assert from 'assert';
|
||||
import * as c32check from 'c32check';
|
||||
import * as c32AddrCache from '../../src/c32-addr-cache';
|
||||
|
||||
if (!global.gc) {
|
||||
throw new Error('Enable --expose-gc');
|
||||
}
|
||||
|
||||
const iters = 500_000;
|
||||
process.env[c32AddrCache.ADDR_CACHE_ENV_VAR] = iters.toString();
|
||||
|
||||
c32AddrCache.injectC32addressEncodeCache();
|
||||
|
||||
const buff = Buffer.alloc(20);
|
||||
c32check.c32address(1, buff.toString('hex'));
|
||||
const startMemory = process.memoryUsage();
|
||||
const startRss = startMemory.rss;
|
||||
const startMemoryStr = util.inspect(startMemory);
|
||||
|
||||
for (let i = 0; i < iters; i++) {
|
||||
// hash160 hex string
|
||||
buff.writeInt32LE(i);
|
||||
c32check.c32address(1, buff.toString('hex'));
|
||||
}
|
||||
|
||||
global.gc();
|
||||
|
||||
const endMemory = process.memoryUsage();
|
||||
const endRss = endMemory.rss;
|
||||
const endMemoryStr = util.inspect(endMemory);
|
||||
console.log('Start memory', startMemoryStr);
|
||||
console.log('End memory', endMemoryStr);
|
||||
|
||||
assert.equal(c32AddrCache.getAddressLruCache().itemCount, iters);
|
||||
|
||||
const rn = (num: number) => Math.round(num * 100) / 100;
|
||||
const megabytes = (bytes: number) => rn(bytes / 1024 / 1024);
|
||||
|
||||
const byteDiff = (endRss - startRss) / (iters / 10_000);
|
||||
|
||||
console.log(`Start RSS: ${megabytes(startRss)}, end RSS: ${megabytes(endRss)}`);
|
||||
console.log(`Around ${megabytes(byteDiff)} megabytes per 10k cache entries`);
|
||||
|
||||
/*
|
||||
Several rounds of running this benchmark show "Around 4.44 megabytes per 10k cache entries":
|
||||
|
||||
Start memory {
|
||||
rss: 26202112,
|
||||
heapTotal: 5578752,
|
||||
heapUsed: 3642392,
|
||||
external: 1147316,
|
||||
arrayBuffers: 59931
|
||||
}
|
||||
End memory {
|
||||
rss: 259125248,
|
||||
heapTotal: 216875008,
|
||||
heapUsed: 181636328,
|
||||
external: 1261038,
|
||||
arrayBuffers: 18090
|
||||
}
|
||||
Start RSS: 24.99, end RSS: 247.12
|
||||
*/
|
||||
Reference in New Issue
Block a user