Merge pull request #3670 from mempool/junderw/pushtxantidos
Push TX: Include validation to prevent DoS
This commit is contained in:
commit
8b681f3ba0
@ -29,7 +29,9 @@
|
|||||||
"ADVANCED_GBT_MEMPOOL": false,
|
"ADVANCED_GBT_MEMPOOL": false,
|
||||||
"RUST_GBT": false,
|
"RUST_GBT": false,
|
||||||
"CPFP_INDEXING": false,
|
"CPFP_INDEXING": false,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 6
|
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||||
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
|
"ALLOW_UNREACHABLE": true
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
|
@ -30,7 +30,9 @@
|
|||||||
"RUST_GBT": false,
|
"RUST_GBT": false,
|
||||||
"CPFP_INDEXING": true,
|
"CPFP_INDEXING": true,
|
||||||
"MAX_BLOCKS_BULK_QUERY": 999,
|
"MAX_BLOCKS_BULK_QUERY": 999,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 999
|
"DISK_CACHE_BLOCK_INTERVAL": 999,
|
||||||
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
|
"ALLOW_UNREACHABLE": true
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
|
@ -44,6 +44,8 @@ describe('Mempool Backend Config', () => {
|
|||||||
CPFP_INDEXING: false,
|
CPFP_INDEXING: false,
|
||||||
MAX_BLOCKS_BULK_QUERY: 0,
|
MAX_BLOCKS_BULK_QUERY: 0,
|
||||||
DISK_CACHE_BLOCK_INTERVAL: 6,
|
DISK_CACHE_BLOCK_INTERVAL: 6,
|
||||||
|
MAX_PUSH_TX_SIZE_WEIGHT: 400000,
|
||||||
|
ALLOW_UNREACHABLE: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
||||||
|
@ -723,12 +723,7 @@ class BitcoinRoutes {
|
|||||||
private async $postTransaction(req: Request, res: Response) {
|
private async $postTransaction(req: Request, res: Response) {
|
||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
try {
|
try {
|
||||||
let rawTx;
|
const rawTx = Common.getTransactionFromRequest(req, false);
|
||||||
if (typeof req.body === 'object') {
|
|
||||||
rawTx = Object.keys(req.body)[0];
|
|
||||||
} else {
|
|
||||||
rawTx = req.body;
|
|
||||||
}
|
|
||||||
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
|
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
|
||||||
res.send(txIdResult);
|
res.send(txIdResult);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -739,12 +734,8 @@ class BitcoinRoutes {
|
|||||||
|
|
||||||
private async $postTransactionForm(req: Request, res: Response) {
|
private async $postTransactionForm(req: Request, res: Response) {
|
||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
const matches = /tx=([a-z0-9]+)/.exec(req.body);
|
|
||||||
let txHex = '';
|
|
||||||
if (matches && matches[1]) {
|
|
||||||
txHex = matches[1];
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
const txHex = Common.getTransactionFromRequest(req, true);
|
||||||
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
||||||
res.send(txIdResult);
|
res.send(txIdResult);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
|
import { Request } from 'express';
|
||||||
import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
|
import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||||
@ -511,6 +513,115 @@ export class Common {
|
|||||||
static getNthPercentile(n: number, sortedDistribution: any[]): any {
|
static getNthPercentile(n: number, sortedDistribution: any[]): any {
|
||||||
return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))];
|
return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getTransactionFromRequest(req: Request, form: boolean): string {
|
||||||
|
let rawTx: any = typeof req.body === 'object' && form
|
||||||
|
? Object.values(req.body)[0] as any
|
||||||
|
: req.body;
|
||||||
|
if (typeof rawTx !== 'string') {
|
||||||
|
throw Object.assign(new Error('Non-string request body'), { code: -1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support both upper and lower case hex
|
||||||
|
// Support both txHash= Form and direct API POST
|
||||||
|
const reg = form ? /^txHash=((?:[a-fA-F0-9]{2})+)$/ : /^((?:[a-fA-F0-9]{2})+)$/;
|
||||||
|
const matches = reg.exec(rawTx);
|
||||||
|
if (!matches || !matches[1]) {
|
||||||
|
throw Object.assign(new Error('Non-hex request body'), { code: -2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guaranteed to be a hex string of multiple of 2
|
||||||
|
// Guaranteed to be lower case
|
||||||
|
// Guaranteed to pass validation (see function below)
|
||||||
|
return this.validateTransactionHex(matches[1].toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static validateTransactionHex(txhex: string): string {
|
||||||
|
// Do not mutate txhex
|
||||||
|
|
||||||
|
// We assume txhex to be valid hex (output of getTransactionFromRequest above)
|
||||||
|
|
||||||
|
// Check 1: Valid transaction parse
|
||||||
|
let tx: bitcoinjs.Transaction;
|
||||||
|
try {
|
||||||
|
tx = bitcoinjs.Transaction.fromHex(txhex);
|
||||||
|
} catch(e) {
|
||||||
|
throw Object.assign(new Error('Invalid transaction (could not parse)'), { code: -4 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 2: Simple size check
|
||||||
|
if (tx.weight() > config.MEMPOOL.MAX_PUSH_TX_SIZE_WEIGHT) {
|
||||||
|
throw Object.assign(new Error(`Transaction too large (max ${config.MEMPOOL.MAX_PUSH_TX_SIZE_WEIGHT} weight units)`), { code: -3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 3: Check unreachable script in taproot (if not allowed)
|
||||||
|
if (!config.MEMPOOL.ALLOW_UNREACHABLE) {
|
||||||
|
tx.ins.forEach(input => {
|
||||||
|
const witness = input.witness;
|
||||||
|
// See BIP 341: Script validation rules
|
||||||
|
const hasAnnex = witness.length >= 2 &&
|
||||||
|
witness[witness.length - 1][0] === 0x50;
|
||||||
|
const scriptSpendMinLength = hasAnnex ? 3 : 2;
|
||||||
|
const maybeScriptSpend = witness.length >= scriptSpendMinLength;
|
||||||
|
|
||||||
|
if (maybeScriptSpend) {
|
||||||
|
const controlBlock = witness[witness.length - scriptSpendMinLength + 1];
|
||||||
|
if (controlBlock.length === 0 || !this.isValidLeafVersion(controlBlock[0])) {
|
||||||
|
// Skip this input, it's not taproot
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Definitely taproot. Get script
|
||||||
|
const script = witness[witness.length - scriptSpendMinLength];
|
||||||
|
const decompiled = bitcoinjs.script.decompile(script);
|
||||||
|
if (!decompiled || decompiled.length < 2) {
|
||||||
|
// Skip this input
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Iterate up to second last (will look ahead 1 item)
|
||||||
|
for (let i = 0; i < decompiled.length - 1; i++) {
|
||||||
|
const first = decompiled[i];
|
||||||
|
const second = decompiled[i + 1];
|
||||||
|
if (
|
||||||
|
first === bitcoinjs.opcodes.OP_FALSE &&
|
||||||
|
second === bitcoinjs.opcodes.OP_IF
|
||||||
|
) {
|
||||||
|
throw Object.assign(new Error('Unreachable taproot scripts not allowed'), { code: -5 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass through the input string untouched
|
||||||
|
return txhex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isValidLeafVersion(leafVersion: number): boolean {
|
||||||
|
// See Note 7 in BIP341
|
||||||
|
// https://github.com/bitcoin/bips/blob/66a1a8151021913047934ebab3f8883f2f8ca75b/bip-0341.mediawiki#cite_note-7
|
||||||
|
// "What constraints are there on the leaf version?"
|
||||||
|
|
||||||
|
// Must be an integer between 0 and 255
|
||||||
|
// Since we're parsing a byte
|
||||||
|
if (Math.floor(leafVersion) !== leafVersion || leafVersion < 0 || leafVersion > 255) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// "the leaf version cannot be odd"
|
||||||
|
if ((leafVersion & 0x01) === 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// "The values that comply to this rule are
|
||||||
|
// the 32 even values between 0xc0 and 0xfe
|
||||||
|
if (leafVersion >= 0xc0 && leafVersion <= 0xfe) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// and also 0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe."
|
||||||
|
if ([0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe].includes(leafVersion)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Otherwise, invalid
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,6 +35,8 @@ interface IConfig {
|
|||||||
CPFP_INDEXING: boolean;
|
CPFP_INDEXING: boolean;
|
||||||
MAX_BLOCKS_BULK_QUERY: number;
|
MAX_BLOCKS_BULK_QUERY: number;
|
||||||
DISK_CACHE_BLOCK_INTERVAL: number;
|
DISK_CACHE_BLOCK_INTERVAL: number;
|
||||||
|
MAX_PUSH_TX_SIZE_WEIGHT: number;
|
||||||
|
ALLOW_UNREACHABLE: boolean;
|
||||||
};
|
};
|
||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
@ -165,6 +167,8 @@ const defaults: IConfig = {
|
|||||||
'CPFP_INDEXING': false,
|
'CPFP_INDEXING': false,
|
||||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||||
'DISK_CACHE_BLOCK_INTERVAL': 6,
|
'DISK_CACHE_BLOCK_INTERVAL': 6,
|
||||||
|
'MAX_PUSH_TX_SIZE_WEIGHT': 400000,
|
||||||
|
'ALLOW_UNREACHABLE': true,
|
||||||
},
|
},
|
||||||
'ESPLORA': {
|
'ESPLORA': {
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||||
|
@ -29,6 +29,8 @@
|
|||||||
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
|
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
|
||||||
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
|
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__,
|
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__,
|
||||||
|
"MAX_PUSH_TX_SIZE_WEIGHT": __MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__,
|
||||||
|
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
|
||||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
|
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
|
||||||
},
|
},
|
||||||
|
@ -32,6 +32,9 @@ __MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
|
|||||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
||||||
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
|
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
|
||||||
|
__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000}
|
||||||
|
__MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true}
|
||||||
|
|
||||||
|
|
||||||
# CORE_RPC
|
# CORE_RPC
|
||||||
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
||||||
@ -161,6 +164,8 @@ sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" me
|
|||||||
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__!${__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_ALLOW_UNREACHABLE__!${__MEMPOOL_ALLOW_UNREACHABLE__}!g" mempool-config.json
|
||||||
|
|
||||||
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
|
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
|
||||||
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
|
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
|
||||||
|
@ -16,7 +16,9 @@
|
|||||||
"ADVANCED_GBT_MEMPOOL": true,
|
"ADVANCED_GBT_MEMPOOL": true,
|
||||||
"RUST_GBT": true,
|
"RUST_GBT": true,
|
||||||
"USE_SECOND_NODE_FOR_MINFEE": true,
|
"USE_SECOND_NODE_FOR_MINFEE": true,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 1
|
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
||||||
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
|
"ALLOW_UNREACHABLE": true
|
||||||
},
|
},
|
||||||
"SYSLOG" : {
|
"SYSLOG" : {
|
||||||
"MIN_PRIORITY": "debug"
|
"MIN_PRIORITY": "debug"
|
||||||
|
@ -12,7 +12,9 @@
|
|||||||
"ADVANCED_GBT_MEMPOOL": true,
|
"ADVANCED_GBT_MEMPOOL": true,
|
||||||
"RUST_GBT": true,
|
"RUST_GBT": true,
|
||||||
"POLL_RATE_MS": 1000,
|
"POLL_RATE_MS": 1000,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 1
|
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
||||||
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
|
"ALLOW_UNREACHABLE": true
|
||||||
},
|
},
|
||||||
"SYSLOG" : {
|
"SYSLOG" : {
|
||||||
"MIN_PRIORITY": "debug"
|
"MIN_PRIORITY": "debug"
|
||||||
|
@ -12,7 +12,9 @@
|
|||||||
"ADVANCED_GBT_MEMPOOL": true,
|
"ADVANCED_GBT_MEMPOOL": true,
|
||||||
"RUST_GBT": true,
|
"RUST_GBT": true,
|
||||||
"POLL_RATE_MS": 1000,
|
"POLL_RATE_MS": 1000,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 1
|
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
||||||
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
|
"ALLOW_UNREACHABLE": true
|
||||||
},
|
},
|
||||||
"SYSLOG" : {
|
"SYSLOG" : {
|
||||||
"MIN_PRIORITY": "debug"
|
"MIN_PRIORITY": "debug"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user