Skip to content

Commit

Permalink
Feat: Add dust calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
junderw committed Dec 4, 2023
1 parent 31b6c27 commit 074720d
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 0 deletions.
11 changes: 11 additions & 0 deletions src/address.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,15 @@ export declare function fromBech32(address: string): Bech32Result;
export declare function toBase58Check(hash: Buffer, version: number): string;
export declare function toBech32(data: Buffer, version: number, prefix: string): string;
export declare function fromOutputScript(output: Buffer, network?: Network): string;
/**
* This uses the logic from Bitcoin Core to decide what is the dust threshold for a given script.
*
* Ref: https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/policy/policy.cpp#L26-L63
*
* @param {Buffer} script - This is the script to evaluate a dust limit for.
* @param {number} [satPerVb=1] - This is to account for different MIN_RELAY_TX_FEE amounts. Bitcoin Core does not calculate
* dust based on the mempool ejection cutoff, but always by the MIN_RELAY_TX_FEE.
* This argument should be passed in as satoshi per vByte. Not satoshi per kvByte like Core.
*/
export declare function dustAmountFromOutputScript(script: Buffer, satPerVb?: number): number;
export declare function toOutputScript(address: string, network?: Network): Buffer;
42 changes: 42 additions & 0 deletions src/address.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.toOutputScript =
exports.dustAmountFromOutputScript =
exports.fromOutputScript =
exports.toBech32 =
exports.toBase58Check =
exports.fromBech32 =
exports.fromBase58Check =
void 0;
const networks = require('./networks');
const ops_1 = require('./ops');
const payments = require('./payments');
const bscript = require('./script');
const types_1 = require('./types');
const varuint = require('bip174/src/lib/converter/varint');
const bech32_1 = require('bech32');
const bs58check = require('bs58check');
const FUTURE_SEGWIT_MAX_SIZE = 40;
Expand Down Expand Up @@ -116,6 +119,45 @@ function fromOutputScript(output, network) {
throw new Error(bscript.toASM(output) + ' has no matching Address');
}
exports.fromOutputScript = fromOutputScript;
/**
* This uses the logic from Bitcoin Core to decide what is the dust threshold for a given script.
*
* Ref: https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/policy/policy.cpp#L26-L63
*
* @param {Buffer} script - This is the script to evaluate a dust limit for.
* @param {number} [satPerVb=1] - This is to account for different MIN_RELAY_TX_FEE amounts. Bitcoin Core does not calculate
* dust based on the mempool ejection cutoff, but always by the MIN_RELAY_TX_FEE.
* This argument should be passed in as satoshi per vByte. Not satoshi per kvByte like Core.
*/
function dustAmountFromOutputScript(script, satPerVb = 1) {
if (isUnspendableCore(script)) {
return 0;
}
const inputBytes = isSegwit(script) ? 67 : 148;
const outputBytes = script.length + 8 + varuint.encodingLength(script.length);
return Math.ceil((inputBytes + outputBytes) * 3 * satPerVb);
}
exports.dustAmountFromOutputScript = dustAmountFromOutputScript;
function isUnspendableCore(script) {
const startsWithOpReturn =
script.length > 0 && script[0] == ops_1.OPS.OP_RETURN;
const MAX_SCRIPT_SIZE = 10000;
const greaterThanScriptSize = script.length > MAX_SCRIPT_SIZE;
// If unspendable, return 0
// https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/script/script.h#L554C16-L554C84
// (size() > 0 && *begin() == OP_RETURN) || (size() > MAX_SCRIPT_SIZE);
return startsWithOpReturn || greaterThanScriptSize;
}
function isSegwit(script) {
if (script.length < 4 || script.length > 42) return false;
if (
script[0] !== ops_1.OPS.OP_0 &&
(script[0] < ops_1.OPS.OP_1 || script[0] > ops_1.OPS.OP_16)
)
return false;
if (script[1] + 2 !== script.length) return false;
return true;
}
function toOutputScript(address, network) {
network = network || networks.bitcoin;
let decodeBase58;
Expand Down
72 changes: 72 additions & 0 deletions test/address.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,76 @@ describe('address', () => {
});
});
});

describe('dustAmountFromOutputScript', () => {
it('gets correct values', () => {
const vectors = [
// OP_RETURN is always 0 regardless of size
[Buffer.from('6a04deadbeef', 'hex'), 1, 0],
[Buffer.from('6a08deadbeefdeadbeef', 'hex'), 1, 0],
// 3 byte non-segwit output is 3 + 1 + 8 + 148 = 160 * 3 = 480
[Buffer.from('020102', 'hex'), 1, 480],
// * 2 the feerate, * 2 the result
[Buffer.from('020102', 'hex'), 2, 960],
// P2PKH is 546 (well known)
[
Buffer.from(
'76a914b6211d1f14f26ea4aed0e4a55e56e82656c7233d88ac',
'hex',
),
1,
546,
],
// P2WPKH is 294 (mentioned in Core comments)
[
Buffer.from('00145f72106b919817aa740fc655cce1a59f2d804e16', 'hex'),
1,
294,
],
// P2TR (and P2WSH) is 330
[
Buffer.from(
'51208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963',
'hex',
),
1,
330,
],
// P2TR (and P2WSH) with OP_16 for some reason is still 330
[
Buffer.from(
'60208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963',
'hex',
),
1,
330,
],
// P2TR (and P2WSH) with 0x61 instead of OP number for some reason is now 573
[
Buffer.from(
'61208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963',
'hex',
),
1,
573,
],
// P2TR (and P2WSH) with 0x50 instead of OP 1-16 for some reason is now 573
[
Buffer.from(
'50208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963',
'hex',
),
1,
573,
],
] as const;

for (const [script, feeRatekvB, expected] of vectors) {
assert.strictEqual(
baddress.dustAmountFromOutputScript(script, feeRatekvB),
expected,
);
}
});
});
});
44 changes: 44 additions & 0 deletions ts_src/address.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Network } from './networks';
import * as networks from './networks';
import { OPS } from './ops';
import * as payments from './payments';
import * as bscript from './script';
import { typeforce, tuple, Hash160bit, UInt8 } from './types';
import * as varuint from 'bip174/src/lib/converter/varint';
import { bech32, bech32m } from 'bech32';
import * as bs58check from 'bs58check';
export interface Base58CheckResult {
Expand Down Expand Up @@ -139,6 +141,48 @@ export function fromOutputScript(output: Buffer, network?: Network): string {
throw new Error(bscript.toASM(output) + ' has no matching Address');
}

/**
* This uses the logic from Bitcoin Core to decide what is the dust threshold for a given script.
*
* Ref: https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/policy/policy.cpp#L26-L63
*
* @param {Buffer} script - This is the script to evaluate a dust limit for.
* @param {number} [satPerVb=1] - This is to account for different MIN_RELAY_TX_FEE amounts. Bitcoin Core does not calculate
* dust based on the mempool ejection cutoff, but always by the MIN_RELAY_TX_FEE.
* This argument should be passed in as satoshi per vByte. Not satoshi per kvByte like Core.
*/
export function dustAmountFromOutputScript(
script: Buffer,
satPerVb: number = 1,
): number {
if (isUnspendableCore(script)) {
return 0;
}

const inputBytes = isSegwit(script) ? 67 : 148;
const outputBytes = script.length + 8 + varuint.encodingLength(script.length);

return Math.ceil((inputBytes + outputBytes) * 3 * satPerVb);
}

function isUnspendableCore(script: Buffer): boolean {
const startsWithOpReturn = script.length > 0 && script[0] == OPS.OP_RETURN;
const MAX_SCRIPT_SIZE = 10000;
const greaterThanScriptSize = script.length > MAX_SCRIPT_SIZE;
// If unspendable, return 0
// https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/script/script.h#L554C16-L554C84
// (size() > 0 && *begin() == OP_RETURN) || (size() > MAX_SCRIPT_SIZE);
return startsWithOpReturn || greaterThanScriptSize;
}

function isSegwit(script: Buffer): boolean {
if (script.length < 4 || script.length > 42) return false;
if (script[0] !== OPS.OP_0 && (script[0] < OPS.OP_1 || script[0] > OPS.OP_16))
return false;
if (script[1] + 2 !== script.length) return false;
return true;
}

export function toOutputScript(address: string, network?: Network): Buffer {
network = network || networks.bitcoin;

Expand Down

0 comments on commit 074720d

Please sign in to comment.