2024-03-22 09:52:27 +00:00
import { TransactionFlags } from './filters.utils' ;
import { getVarIntLength , opcodes , parseMultisigScript , isPoint } from './script.utils' ;
import { Transaction } from '../interfaces/electrs.interface' ;
import { CpfpInfo , RbfInfo } from '../interfaces/node-api.interface' ;
// Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2 ;
const MAX_STANDARD_TX_WEIGHT = 400 _000 ;
const MAX_BLOCK_SIGOPS_COST = 80 _000 ;
const MAX_STANDARD_TX_SIGOPS_COST = ( MAX_BLOCK_SIGOPS_COST / 5 ) ;
const MIN_STANDARD_TX_NONWITNESS_SIZE = 65 ;
const MAX_P2SH_SIGOPS = 15 ;
const MAX_STANDARD_P2WSH_STACK_ITEMS = 100 ;
const MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80 ;
const MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE = 80 ;
const MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600 ;
const MAX_STANDARD_SCRIPTSIG_SIZE = 1650 ;
const DUST_RELAY_TX_FEE = 3 ;
const MAX_OP_RETURN_RELAY = 83 ;
const DEFAULT_PERMIT_BAREMULTISIG = true ;
export function countScriptSigops ( script : string , isRawScript : boolean = false , witness : boolean = false ) : number {
if ( ! script ? . length ) {
return 0 ;
}
let sigops = 0 ;
// count OP_CHECKSIG and OP_CHECKSIGVERIFY
sigops += ( script . match ( /OP_CHECKSIG/g ) ? . length || 0 ) ;
// count OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY
if ( isRawScript ) {
// in scriptPubKey or scriptSig, always worth 20
sigops += 20 * ( script . match ( /OP_CHECKMULTISIG/g ) ? . length || 0 ) ;
} else {
// in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
const matches = script . matchAll ( /(?:OP_(?:PUSHNUM_)?(\d+))? OP_CHECKMULTISIG/g ) ;
for ( const match of matches ) {
const n = parseInt ( match [ 1 ] ) ;
if ( Number . isInteger ( n ) ) {
sigops += n ;
} else {
sigops += 20 ;
}
}
}
return witness ? sigops : ( sigops * 4 ) ;
}
export function setSchnorrSighashFlags ( flags : bigint , witness : string [ ] ) : bigint {
// no witness items
if ( ! witness ? . length ) {
return flags ;
}
const hasAnnex = witness . length > 1 && witness [ witness . length - 1 ] . startsWith ( '50' ) ;
if ( witness ? . length === ( hasAnnex ? 2 : 1 ) ) {
// keypath spend, signature is the only witness item
if ( witness [ 0 ] . length === 130 ) {
flags |= setSighashFlags ( flags , witness [ 0 ] ) ;
} else {
flags |= TransactionFlags . sighash_default ;
}
} else {
// scriptpath spend, all items except for the script, control block and annex could be signatures
for ( let i = 0 ; i < witness . length - ( hasAnnex ? 3 : 2 ) ; i ++ ) {
// handle probable signatures
if ( witness [ i ] . length === 130 ) {
flags |= setSighashFlags ( flags , witness [ i ] ) ;
} else if ( witness [ i ] . length === 128 ) {
flags |= TransactionFlags . sighash_default ;
}
}
}
return flags ;
}
export function isDERSig ( w : string ) : boolean {
// heuristic to detect probable DER signatures
return ( w . length >= 18
&& w . startsWith ( '30' ) // minimum DER signature length is 8 bytes + sighash flag (see https://mempool.space/testnet/tx/c6c232a36395fa338da458b86ff1327395a9afc28c5d2daa4273e410089fd433)
&& [ '01' , '02' , '03' , '81' , '82' , '83' ] . includes ( w . slice ( - 2 ) ) // signature must end with a valid sighash flag
&& ( w . length === ( 2 * parseInt ( w . slice ( 2 , 4 ) , 16 ) ) + 6 ) // second byte encodes the combined length of the R and S components
) ;
}
/ * *
* Validates most standardness rules
*
* returns true early if any standardness rule is violated , otherwise false
* ( except for non - mandatory - script - verify - flag and p2sh script evaluation rules which are * not * enforced )
* /
export function isNonStandard ( tx : Transaction ) : boolean {
// version
if ( tx . version > TX_MAX_STANDARD_VERSION ) {
return true ;
}
// tx-size
if ( tx . weight > MAX_STANDARD_TX_WEIGHT ) {
return true ;
}
// tx-size-small
if ( getNonWitnessSize ( tx ) < MIN_STANDARD_TX_NONWITNESS_SIZE ) {
return true ;
}
// bad-txns-too-many-sigops
if ( tx . sigops && tx . sigops > MAX_STANDARD_TX_SIGOPS_COST ) {
return true ;
}
// input validation
for ( const vin of tx . vin ) {
if ( vin . is_coinbase ) {
// standardness rules don't apply to coinbase transactions
return false ;
}
// scriptsig-size
if ( ( vin . scriptsig . length / 2 ) > MAX_STANDARD_SCRIPTSIG_SIZE ) {
return true ;
}
// scriptsig-not-pushonly
if ( vin . scriptsig_asm ) {
for ( const op of vin . scriptsig_asm . split ( ' ' ) ) {
if ( opcodes [ op ] && opcodes [ op ] > opcodes [ 'OP_16' ] ) {
return true ;
}
}
}
// bad-txns-nonstandard-inputs
if ( vin . prevout ? . scriptpubkey_type === 'p2sh' ) {
// TODO: evaluate script (https://github.com/bitcoin/bitcoin/blob/1ac627c485a43e50a9a49baddce186ee3ad4daad/src/policy/policy.cpp#L177)
// countScriptSigops returns the witness-scaled sigops, so divide by 4 before comparison with MAX_P2SH_SIGOPS
const sigops = ( countScriptSigops ( vin . inner_redeemscript_asm || '' ) / 4 ) ;
if ( sigops > MAX_P2SH_SIGOPS ) {
return true ;
}
} else if ( [ 'unknown' , 'provably_unspendable' , 'empty' ] . includes ( vin . prevout ? . scriptpubkey_type || '' ) ) {
return true ;
}
// TODO: bad-witness-nonstandard
}
// output validation
let opreturnCount = 0 ;
for ( const vout of tx . vout ) {
// scriptpubkey
2024-06-16 22:25:40 +00:00
if ( [ 'nonstandard' , 'provably_unspendable' , 'empty' ] . includes ( vout . scriptpubkey_type ) ) {
2024-03-22 09:52:27 +00:00
// (non-standard output type)
return true ;
2024-06-16 22:25:40 +00:00
} else if ( vout . scriptpubkey_type === 'unknown' ) {
// undefined segwit version/length combinations are actually standard in outputs
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/interpreter.cpp#L1950-L1951
if ( vout . scriptpubkey . startsWith ( '00' ) || ! isWitnessProgram ( vout . scriptpubkey ) ) {
return true ;
}
2024-03-22 09:52:27 +00:00
} else if ( vout . scriptpubkey_type === 'multisig' ) {
if ( ! DEFAULT_PERMIT_BAREMULTISIG ) {
// bare-multisig
return true ;
}
const mOfN = parseMultisigScript ( vout . scriptpubkey_asm ) ;
if ( ! mOfN || mOfN . n < 1 || mOfN . n > 3 || mOfN . m < 1 || mOfN . m > mOfN . n ) {
// (non-standard bare multisig threshold)
return true ;
}
} else if ( vout . scriptpubkey_type === 'op_return' ) {
opreturnCount ++ ;
if ( ( vout . scriptpubkey . length / 2 ) > MAX_OP_RETURN_RELAY ) {
// over default datacarrier limit
return true ;
}
}
// dust
// (we could probably hardcode this for the different output types...)
if ( vout . scriptpubkey_type !== 'op_return' ) {
let dustSize = ( vout . scriptpubkey . length / 2 ) ;
// add varint length overhead
dustSize += getVarIntLength ( dustSize ) ;
// add value size
dustSize += 8 ;
2024-06-30 02:06:50 +00:00
if ( isWitnessProgram ( vout . scriptpubkey ) ) {
2024-03-22 09:52:27 +00:00
dustSize += 67 ;
} else {
dustSize += 148 ;
}
if ( vout . value < ( dustSize * DUST_RELAY_TX_FEE ) ) {
// under minimum output size
return true ;
}
}
}
// multi-op-return
if ( opreturnCount > 1 ) {
return true ;
}
// TODO: non-mandatory-script-verify-flag
return false ;
}
2024-06-16 22:25:40 +00:00
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
// followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
function isWitnessProgram ( scriptpubkey : string ) : false | { version : number , program : string } {
if ( scriptpubkey . length < 8 || scriptpubkey . length > 84 ) {
return false ;
}
const version = parseInt ( scriptpubkey . slice ( 0 , 2 ) , 16 ) ;
if ( version !== 0 && version < 0x51 || version > 0x60 ) {
return false ;
}
const push = parseInt ( scriptpubkey . slice ( 2 , 4 ) , 16 ) ;
if ( push + 2 === ( scriptpubkey . length / 2 ) ) {
return {
version : version ? version - 0x50 : 0 ,
program : scriptpubkey.slice ( 4 ) ,
} ;
}
return false ;
}
2024-03-22 09:52:27 +00:00
export function getNonWitnessSize ( tx : Transaction ) : number {
let weight = tx . weight ;
let hasWitness = false ;
for ( const vin of tx . vin ) {
if ( vin . witness ? . length ) {
hasWitness = true ;
// witness count
weight -= getVarIntLength ( vin . witness . length ) ;
for ( const witness of vin . witness ) {
// witness item size + content
weight -= getVarIntLength ( witness . length / 2 ) + ( witness . length / 2 ) ;
}
}
}
if ( hasWitness ) {
// marker & segwit flag
weight -= 2 ;
}
return Math . ceil ( weight / 4 ) ;
}
export function setSegwitSighashFlags ( flags : bigint , witness : string [ ] ) : bigint {
for ( const w of witness ) {
if ( isDERSig ( w ) ) {
flags |= setSighashFlags ( flags , w ) ;
}
}
return flags ;
}
export function setLegacySighashFlags ( flags : bigint , scriptsig_asm : string ) : bigint {
for ( const item of scriptsig_asm . split ( ' ' ) ) {
// skip op_codes
if ( item . startsWith ( 'OP_' ) ) {
continue ;
}
// check pushed data
if ( isDERSig ( item ) ) {
flags |= setSighashFlags ( flags , item ) ;
}
}
return flags ;
}
export function setSighashFlags ( flags : bigint , signature : string ) : bigint {
switch ( signature . slice ( - 2 ) ) {
case '01' : return flags | TransactionFlags . sighash_all ;
case '02' : return flags | TransactionFlags . sighash_none ;
case '03' : return flags | TransactionFlags . sighash_single ;
case '81' : return flags | TransactionFlags . sighash_all | TransactionFlags . sighash_acp ;
case '82' : return flags | TransactionFlags . sighash_none | TransactionFlags . sighash_acp ;
case '83' : return flags | TransactionFlags . sighash_single | TransactionFlags . sighash_acp ;
default : return flags | TransactionFlags . sighash_default ; // taproot only
}
}
export function isBurnKey ( pubkey : string ) : boolean {
return [
'022222222222222222222222222222222222222222222222222222222222222222' ,
'033333333333333333333333333333333333333333333333333333333333333333' ,
'020202020202020202020202020202020202020202020202020202020202020202' ,
'030303030303030303030303030303030303030303030303030303030303030303' ,
] . includes ( pubkey ) ;
}
export function getTransactionFlags ( tx : Transaction , cpfpInfo? : CpfpInfo , replacement? : boolean ) : bigint {
let flags = tx . flags ? BigInt ( tx . flags ) : 0 n ;
// Update variable flags (CPFP, RBF)
if ( cpfpInfo ) {
if ( cpfpInfo . ancestors . length ) {
flags |= TransactionFlags . cpfp_child ;
}
if ( cpfpInfo . descendants ? . length ) {
flags |= TransactionFlags . cpfp_parent ;
}
}
if ( replacement ) {
flags |= TransactionFlags . replacement ;
}
// Already processed static flags, no need to do it again
if ( tx . flags ) {
return flags ;
}
// Process static flags
if ( tx . version === 1 ) {
flags |= TransactionFlags . v1 ;
} else if ( tx . version === 2 ) {
flags |= TransactionFlags . v2 ;
} else if ( tx . version === 3 ) {
flags |= TransactionFlags . v3 ;
}
const reusedInputAddresses : { [ address : string ] : number } = { } ;
const reusedOutputAddresses : { [ address : string ] : number } = { } ;
const inValues = { } ;
const outValues = { } ;
let rbf = false ;
for ( const vin of tx . vin ) {
if ( vin . sequence < 0xfffffffe ) {
rbf = true ;
}
switch ( vin . prevout ? . scriptpubkey_type ) {
case 'p2pk' : flags |= TransactionFlags . p2pk ; break ;
case 'multisig' : flags |= TransactionFlags . p2ms ; break ;
case 'p2pkh' : flags |= TransactionFlags . p2pkh ; break ;
case 'p2sh' : flags |= TransactionFlags . p2sh ; break ;
case 'v0_p2wpkh' : flags |= TransactionFlags . p2wpkh ; break ;
case 'v0_p2wsh' : flags |= TransactionFlags . p2wsh ; break ;
case 'v1_p2tr' : {
if ( ! vin . witness ? . length ) {
throw new Error ( 'Taproot input missing witness data' ) ;
}
flags |= TransactionFlags . p2tr ;
// in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin . witness ? . [ vin . witness . length - 1 ] . startsWith ( '50' ) ;
// script spends have more than one witness item, not counting the annex (if present)
if ( vin . witness . length > ( hasAnnex ? 2 : 1 ) ) {
// the script itself is the second-to-last witness item, not counting the annex
const asm = vin . inner_witnessscript_asm ;
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
if ( asm ? . includes ( 'OP_0 OP_IF' ) ) {
flags |= TransactionFlags . inscription ;
}
}
} break ;
}
// sighash flags
if ( vin . prevout ? . scriptpubkey_type === 'v1_p2tr' ) {
flags |= setSchnorrSighashFlags ( flags , vin . witness ) ;
} else if ( vin . witness ) {
flags |= setSegwitSighashFlags ( flags , vin . witness ) ;
} else if ( vin . scriptsig ? . length ) {
flags |= setLegacySighashFlags ( flags , vin . scriptsig_asm ) ;
}
if ( vin . prevout ? . scriptpubkey_address ) {
reusedInputAddresses [ vin . prevout ? . scriptpubkey_address ] = ( reusedInputAddresses [ vin . prevout ? . scriptpubkey_address ] || 0 ) + 1 ;
}
inValues [ vin . prevout ? . value || Math . random ( ) ] = ( inValues [ vin . prevout ? . value || Math . random ( ) ] || 0 ) + 1 ;
}
if ( rbf ) {
flags |= TransactionFlags . rbf ;
} else {
flags |= TransactionFlags . no_rbf ;
}
let hasFakePubkey = false ;
let P2WSHCount = 0 ;
let olgaSize = 0 ;
for ( const vout of tx . vout ) {
switch ( vout . scriptpubkey_type ) {
case 'p2pk' : {
flags |= TransactionFlags . p2pk ;
// detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve)
hasFakePubkey = hasFakePubkey || ! isPoint ( vout . scriptpubkey ? . slice ( 2 , - 2 ) ) ;
} break ;
case 'multisig' : {
flags |= TransactionFlags . p2ms ;
// detect fake pubkeys (i.e. not valid DER points on the secp256k1 curve)
const asm = vout . scriptpubkey_asm ;
for ( const key of ( asm ? . split ( ' ' ) || [ ] ) ) {
if ( ! hasFakePubkey && ! key . startsWith ( 'OP_' ) ) {
hasFakePubkey = hasFakePubkey || isBurnKey ( key ) || ! isPoint ( key ) ;
}
}
} break ;
case 'p2pkh' : flags |= TransactionFlags . p2pkh ; break ;
case 'p2sh' : flags |= TransactionFlags . p2sh ; break ;
case 'v0_p2wpkh' : flags |= TransactionFlags . p2wpkh ; break ;
case 'v0_p2wsh' : flags |= TransactionFlags . p2wsh ; break ;
case 'v1_p2tr' : flags |= TransactionFlags . p2tr ; break ;
case 'op_return' : flags |= TransactionFlags . op_return ; break ;
}
if ( vout . scriptpubkey_address ) {
reusedOutputAddresses [ vout . scriptpubkey_address ] = ( reusedOutputAddresses [ vout . scriptpubkey_address ] || 0 ) + 1 ;
}
if ( vout . scriptpubkey_type === 'v0_p2wsh' ) {
if ( ! P2WSHCount ) {
olgaSize = parseInt ( vout . scriptpubkey . slice ( 4 , 8 ) , 16 ) ;
}
P2WSHCount ++ ;
if ( P2WSHCount === Math . ceil ( ( olgaSize + 2 ) / 32 ) ) {
const nullBytes = ( P2WSHCount * 32 ) - olgaSize - 2 ;
if ( vout . scriptpubkey . endsWith ( '' . padEnd ( nullBytes * 2 , '0' ) ) ) {
flags |= TransactionFlags . fake_scripthash ;
}
}
} else {
P2WSHCount = 0 ;
}
outValues [ vout . value || Math . random ( ) ] = ( outValues [ vout . value || Math . random ( ) ] || 0 ) + 1 ;
}
if ( hasFakePubkey ) {
flags |= TransactionFlags . fake_pubkey ;
}
// fast but bad heuristic to detect possible coinjoins
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
const addressReuse = Object . keys ( reusedOutputAddresses ) . reduce ( ( acc , key ) = > Math . max ( acc , ( reusedInputAddresses [ key ] || 0 ) + ( reusedOutputAddresses [ key ] || 0 ) ) , 0 ) > 1 ;
if ( ! addressReuse && tx . vin . length >= 5 && tx . vout . length >= 5 && ( Object . keys ( inValues ) . length + Object . keys ( outValues ) . length ) <= ( tx . vin . length + tx . vout . length ) / 2 ) {
flags |= TransactionFlags . coinjoin ;
}
// more than 5:1 input:output ratio
if ( tx . vin . length / tx . vout . length >= 5 ) {
flags |= TransactionFlags . consolidation ;
}
// less than 1:5 input:output ratio
if ( tx . vin . length / tx . vout . length <= 0.2 ) {
flags |= TransactionFlags . batch_payout ;
}
if ( isNonStandard ( tx ) ) {
flags |= TransactionFlags . nonstandard ;
}
return flags ;
2024-05-30 21:22:53 +00:00
}
export function getUnacceleratedFeeRate ( tx : Transaction , accelerated : boolean ) : number {
if ( accelerated ) {
let ancestorVsize = tx . weight / 4 ;
let ancestorFee = tx . fee ;
for ( const ancestor of tx . ancestors || [ ] ) {
ancestorVsize += ( ancestor . weight / 4 ) ;
ancestorFee += ancestor . fee ;
}
return Math . min ( tx . fee / ( tx . weight / 4 ) , ( ancestorFee / ancestorVsize ) ) ;
} else {
return tx . effectiveFeePerVsize ;
}
2024-03-22 09:52:27 +00:00
}