Merge pull request #3670 from mempool/junderw/pushtxantidos
Push TX: Include validation to prevent DoS
This commit is contained in:
		
						commit
						67e5f41d8e
					
				@ -29,7 +29,9 @@
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": false,
 | 
			
		||||
    "RUST_GBT": 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": {
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,9 @@
 | 
			
		||||
    "RUST_GBT": false,
 | 
			
		||||
    "CPFP_INDEXING": true,
 | 
			
		||||
    "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": {
 | 
			
		||||
    "HOST": "__CORE_RPC_HOST__",
 | 
			
		||||
@ -120,4 +122,4 @@
 | 
			
		||||
  "CLIGHTNING": {
 | 
			
		||||
    "SOCKET": "__CLIGHTNING_SOCKET__"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,8 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
        CPFP_INDEXING: false,
 | 
			
		||||
        MAX_BLOCKS_BULK_QUERY: 0,
 | 
			
		||||
        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 });
 | 
			
		||||
 | 
			
		||||
@ -723,12 +723,7 @@ class BitcoinRoutes {
 | 
			
		||||
  private async $postTransaction(req: Request, res: Response) {
 | 
			
		||||
    res.setHeader('content-type', 'text/plain');
 | 
			
		||||
    try {
 | 
			
		||||
      let rawTx;
 | 
			
		||||
      if (typeof req.body === 'object') {
 | 
			
		||||
        rawTx = Object.keys(req.body)[0];
 | 
			
		||||
      } else {
 | 
			
		||||
        rawTx = req.body;
 | 
			
		||||
      }
 | 
			
		||||
      const rawTx = Common.getTransactionFromRequest(req, false);
 | 
			
		||||
      const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
 | 
			
		||||
      res.send(txIdResult);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
@ -739,12 +734,8 @@ class BitcoinRoutes {
 | 
			
		||||
 | 
			
		||||
  private async $postTransactionForm(req: Request, res: Response) {
 | 
			
		||||
    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 {
 | 
			
		||||
      const txHex = Common.getTransactionFromRequest(req, true);
 | 
			
		||||
      const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
 | 
			
		||||
      res.send(txIdResult);
 | 
			
		||||
    } 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 config from '../config';
 | 
			
		||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
 | 
			
		||||
@ -511,6 +513,115 @@ export class Common {
 | 
			
		||||
  static getNthPercentile(n: number, sortedDistribution: any[]): any {
 | 
			
		||||
    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;
 | 
			
		||||
    MAX_BLOCKS_BULK_QUERY: number;
 | 
			
		||||
    DISK_CACHE_BLOCK_INTERVAL: number;
 | 
			
		||||
    MAX_PUSH_TX_SIZE_WEIGHT: number;
 | 
			
		||||
    ALLOW_UNREACHABLE: boolean;
 | 
			
		||||
  };
 | 
			
		||||
  ESPLORA: {
 | 
			
		||||
    REST_API_URL: string;
 | 
			
		||||
@ -165,6 +167,8 @@ const defaults: IConfig = {
 | 
			
		||||
    'CPFP_INDEXING': false,
 | 
			
		||||
    'MAX_BLOCKS_BULK_QUERY': 0,
 | 
			
		||||
    'DISK_CACHE_BLOCK_INTERVAL': 6,
 | 
			
		||||
    'MAX_PUSH_TX_SIZE_WEIGHT': 400000,
 | 
			
		||||
    'ALLOW_UNREACHABLE': true,
 | 
			
		||||
  },
 | 
			
		||||
  'ESPLORA': {
 | 
			
		||||
    'REST_API_URL': 'http://127.0.0.1:3000',
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,8 @@
 | 
			
		||||
    "CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
 | 
			
		||||
    "MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
 | 
			
		||||
    "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_URL": "__MEMPOOL_POOLS_JSON_URL__"
 | 
			
		||||
  },
 | 
			
		||||
@ -126,4 +128,4 @@
 | 
			
		||||
    "GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
 | 
			
		||||
    "GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,9 @@ __MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
 | 
			
		||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
 | 
			
		||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
 | 
			
		||||
__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_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_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_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_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,9 @@
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": true,
 | 
			
		||||
    "RUST_GBT": 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" : {
 | 
			
		||||
    "MIN_PRIORITY": "debug"
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,9 @@
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": true,
 | 
			
		||||
    "RUST_GBT": true,
 | 
			
		||||
    "POLL_RATE_MS": 1000,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 1
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 1,
 | 
			
		||||
    "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
 | 
			
		||||
    "ALLOW_UNREACHABLE": true
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG" : {
 | 
			
		||||
    "MIN_PRIORITY": "debug"
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,9 @@
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": true,
 | 
			
		||||
    "RUST_GBT": true,
 | 
			
		||||
    "POLL_RATE_MS": 1000,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 1
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 1,
 | 
			
		||||
    "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
 | 
			
		||||
    "ALLOW_UNREACHABLE": true
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG" : {
 | 
			
		||||
    "MIN_PRIORITY": "debug"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user