2024-10-23 11:34:59 +09:00
import { TransactionFlags } from '@app/shared/filters.utils' ;
2024-11-27 17:54:07 +01:00
import { getVarIntLength , parseMultisigScript , isPoint } from '@app/shared/script.utils' ;
import { Transaction , Vin } from '@interfaces/electrs.interface' ;
2024-10-23 11:09:38 +09:00
import { CpfpInfo , RbfInfo , TransactionStripped } from '@interfaces/node-api.interface' ;
2024-10-22 21:05:01 +09:00
import { StateService } from '@app/services/state.service' ;
2024-11-27 17:54:07 +01:00
import { Hash } from './sha256' ;
2024-03-22 09:52:27 +00:00
// Bitcoin Core default policy settings
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 )
2024-08-30 23:12:24 +00:00
*
* As standardness rules change , we ' ll need to apply the rules in force * at the time * to older blocks .
* For now , just pull out individual rules into versioned functions where necessary .
2024-03-22 09:52:27 +00:00
* /
2024-08-30 23:12:24 +00:00
export function isNonStandard ( tx : Transaction , height? : number , network? : string ) : boolean {
2024-03-22 09:52:27 +00:00
// version
2024-08-30 23:12:24 +00:00
if ( isNonStandardVersion ( tx , height , network ) ) {
2024-03-22 09:52:27 +00:00
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 ;
2024-08-30 23:12:24 +00:00
} else if ( isNonStandardAnchor ( tx , height , network ) ) {
return true ;
2024-03-22 09:52:27 +00:00
}
// 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-08-30 23:12:24 +00:00
// Individual versioned standardness rules
const V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4' : 42 _000 ,
'testnet' : 2 _900_000 ,
'signet' : 211 _000 ,
'' : 863 _500 ,
} ;
function isNonStandardVersion ( tx : Transaction , height? : number , network? : string ) : boolean {
let TX_MAX_STANDARD_VERSION = 3 ;
if (
height != null
&& network != null
&& V3_STANDARDNESS_ACTIVATION_HEIGHT [ network ]
&& height <= V3_STANDARDNESS_ACTIVATION_HEIGHT [ network ]
) {
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
TX_MAX_STANDARD_VERSION = 2 ;
}
if ( tx . version > TX_MAX_STANDARD_VERSION ) {
return true ;
}
return false ;
}
const ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4' : 42 _000 ,
'testnet' : 2 _900_000 ,
'signet' : 211 _000 ,
'' : 863 _500 ,
} ;
function isNonStandardAnchor ( tx : Transaction , height? : number , network? : string ) : boolean {
if (
height != null
&& network != null
&& ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT [ network ]
&& height <= ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT [ network ]
) {
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
return true ;
}
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 ) ;
}
2024-08-30 23:12:24 +00:00
export function getTransactionFlags ( tx : Transaction , cpfpInfo? : CpfpInfo , replacement? : boolean , height? : number , network? : string ) : bigint {
2024-03-22 09:52:27 +00:00
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' : {
flags |= TransactionFlags . p2tr ;
2024-07-01 02:02:53 +02:00
// every valid taproot input has at least one witness item, however transactions
// created before taproot activation don't need to have any witness data
// (see https://mempool.space/tx/b10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d41)
if ( vin . witness ? . length ) {
// 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 ;
}
2024-03-22 09:52:27 +00:00
}
}
} 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 ;
}
2024-08-30 23:12:24 +00:00
if ( isNonStandard ( tx , height , network ) ) {
2024-03-22 09:52:27 +00:00
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-08-17 00:14:33 +00:00
}
export function identifyPrioritizedTransactions ( transactions : TransactionStripped [ ] ) : { prioritized : string [ ] , deprioritized : string [ ] } {
// find the longest increasing subsequence of transactions
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
// should be O(n log n)
const X = transactions . slice ( 1 ) . reverse ( ) ; // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
if ( X . length < 2 ) {
return { prioritized : [ ] , deprioritized : [ ] } ;
}
const N = X . length ;
const P : number [ ] = new Array ( N ) ;
const M : number [ ] = new Array ( N + 1 ) ;
M [ 0 ] = - 1 ; // undefined so can be set to any value
let L = 0 ;
for ( let i = 0 ; i < N ; i ++ ) {
// Binary search for the smallest positive l ≤ L
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
let lo = 1 ;
let hi = L + 1 ;
while ( lo < hi ) {
const mid = lo + Math . floor ( ( hi - lo ) / 2 ) ; // lo <= mid < hi
if ( X [ M [ mid ] ] . rate > X [ i ] . rate ) {
hi = mid ;
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
lo = mid + 1 ;
}
}
// After searching, lo == hi is 1 greater than the
// length of the longest prefix of X[i]
const newL = lo ;
// The predecessor of X[i] is the last index of
// the subsequence of length newL-1
P [ i ] = M [ newL - 1 ] ;
M [ newL ] = i ;
if ( newL > L ) {
// If we found a subsequence longer than any we've
// found yet, update L
L = newL ;
}
}
// Reconstruct the longest increasing subsequence
// It consists of the values of X at the L indices:
// ..., P[P[M[L]]], P[M[L]], M[L]
const LIS : TransactionStripped [ ] = new Array ( L ) ;
let k = M [ L ] ;
for ( let j = L - 1 ; j >= 0 ; j -- ) {
LIS [ j ] = X [ k ] ;
k = P [ k ] ;
}
const lisMap = new Map < string , number > ( ) ;
LIS . forEach ( ( tx , index ) = > lisMap . set ( tx . txid , index ) ) ;
const prioritized : string [ ] = [ ] ;
const deprioritized : string [ ] = [ ] ;
let lastRate = 0 ;
for ( const tx of X ) {
if ( lisMap . has ( tx . txid ) ) {
lastRate = tx . rate ;
} else {
if ( Math . abs ( tx . rate - lastRate ) < 0.1 ) {
// skip if the rate is almost the same as the previous transaction
} else if ( tx . rate <= lastRate ) {
prioritized . push ( tx . txid ) ;
} else {
deprioritized . push ( tx . txid ) ;
}
}
}
return { prioritized , deprioritized } ;
2024-10-22 21:05:01 +09:00
}
2024-11-27 17:54:07 +01:00
2024-11-28 12:07:05 +01:00
// Adapted from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L254
// Converts hex bitcoin script to ASM
2024-11-27 17:54:07 +01:00
function convertScriptSigAsm ( hex : string ) : string {
const buf = new Uint8Array ( hex . length / 2 ) ;
for ( let i = 0 ; i < buf . length ; i ++ ) {
buf [ i ] = parseInt ( hex . substr ( i * 2 , 2 ) , 16 ) ;
}
const b = [ ] ;
let i = 0 ;
while ( i < buf . length ) {
const op = buf [ i ] ;
if ( op >= 0x01 && op <= 0x4e ) {
i ++ ;
let push ;
if ( op === 0x4c ) {
push = buf [ i ] ;
b . push ( 'OP_PUSHDATA1' ) ;
i += 1 ;
} else if ( op === 0x4d ) {
push = buf [ i ] | ( buf [ i + 1 ] << 8 ) ;
b . push ( 'OP_PUSHDATA2' ) ;
i += 2 ;
} else if ( op === 0x4e ) {
push = buf [ i ] | ( buf [ i + 1 ] << 8 ) | ( buf [ i + 2 ] << 16 ) | ( buf [ i + 3 ] << 24 ) ;
b . push ( 'OP_PUSHDATA4' ) ;
i += 4 ;
} else {
push = op ;
b . push ( 'OP_PUSHBYTES_' + push ) ;
}
const data = buf . slice ( i , i + push ) ;
if ( data . length !== push ) {
break ;
}
b . push ( uint8ArrayToHexString ( data ) ) ;
i += data . length ;
} else {
if ( op === 0x00 ) {
b . push ( 'OP_0' ) ;
} else if ( op === 0x4f ) {
b . push ( 'OP_PUSHNUM_NEG1' ) ;
} else if ( op === 0xb1 ) {
b . push ( 'OP_CLTV' ) ;
} else if ( op === 0xb2 ) {
b . push ( 'OP_CSV' ) ;
} else if ( op === 0xba ) {
b . push ( 'OP_CHECKSIGADD' ) ;
} else {
const opcode = opcodes [ op ] ;
if ( opcode ) {
b . push ( opcode ) ;
} else {
b . push ( 'OP_RETURN_' + op ) ;
}
}
i += 1 ;
}
}
return b . join ( ' ' ) ;
}
2024-11-28 12:07:05 +01:00
// Copied from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L327
2024-11-27 17:54:07 +01:00
/ * *
* This function must only be called when we know the witness we are parsing
* is a taproot witness .
* @param witness An array of hex strings that represents the witness stack of
* the input .
* @returns null if the witness is not a script spend , and the hex string of
* the script item if it is a script spend .
* /
function witnessToP2TRScript ( witness : string [ ] ) : string | null {
if ( witness . length < 2 ) return null ;
// Note: see BIP341 for parsing details of witness stack
// If there are at least two witness elements, and the first byte of the
// last element is 0x50, this last element is called annex a and
// is removed from the witness stack.
const hasAnnex = witness [ witness . length - 1 ] . substring ( 0 , 2 ) === '50' ;
// If there are at least two witness elements left, script path spending is used.
// Call the second-to-last stack element s, the script.
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
if ( hasAnnex && witness . length < 3 ) return null ;
const positionOfScript = hasAnnex ? witness . length - 3 : witness.length - 2 ;
return witness [ positionOfScript ] ;
}
2024-11-28 12:07:05 +01:00
// Copied from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L227
// Fills inner_redeemscript_asm and inner_witnessscript_asm fields of fetched prevouts for decoded transactions
2024-11-27 17:54:07 +01:00
export function addInnerScriptsToVin ( vin : Vin ) : void {
if ( ! vin . prevout ) {
return ;
}
if ( vin . prevout . scriptpubkey_type === 'p2sh' ) {
const redeemScript = vin . scriptsig_asm . split ( ' ' ) . reverse ( ) [ 0 ] ;
vin . inner_redeemscript_asm = convertScriptSigAsm ( redeemScript ) ;
if ( vin . witness && vin . witness . length > 2 ) {
const witnessScript = vin . witness [ vin . witness . length - 1 ] ;
vin . inner_witnessscript_asm = convertScriptSigAsm ( witnessScript ) ;
}
}
if ( vin . prevout . scriptpubkey_type === 'v0_p2wsh' && vin . witness ) {
const witnessScript = vin . witness [ vin . witness . length - 1 ] ;
vin . inner_witnessscript_asm = convertScriptSigAsm ( witnessScript ) ;
}
if ( vin . prevout . scriptpubkey_type === 'v1_p2tr' && vin . witness ) {
const witnessScript = witnessToP2TRScript ( vin . witness ) ;
if ( witnessScript !== null ) {
vin . inner_witnessscript_asm = convertScriptSigAsm ( witnessScript ) ;
}
}
}
2024-11-28 12:07:05 +01:00
// Adapted from bitcoinjs-lib at https://github.com/bitcoinjs/bitcoinjs-lib/blob/32e08aa57f6a023e995d8c4f0c9fbdc5f11d1fa0/ts_src/transaction.ts#L78
// Reads buffer of raw transaction data
2024-11-27 17:54:07 +01:00
function fromBuffer ( buffer : Uint8Array , network : string ) : Transaction {
let offset = 0 ;
function readInt8 ( ) : number {
if ( offset + 1 > buffer . length ) {
throw new Error ( 'Buffer out of bounds' ) ;
}
return buffer [ offset ++ ] ;
}
function readInt16() {
if ( offset + 2 > buffer . length ) {
throw new Error ( 'Buffer out of bounds' ) ;
}
const value = buffer [ offset ] | ( buffer [ offset + 1 ] << 8 ) ;
offset += 2 ;
return value ;
}
function readInt32 ( unsigned = false ) : number {
if ( offset + 4 > buffer . length ) {
throw new Error ( 'Buffer out of bounds' ) ;
}
const value = buffer [ offset ] | ( buffer [ offset + 1 ] << 8 ) | ( buffer [ offset + 2 ] << 16 ) | ( buffer [ offset + 3 ] << 24 ) ;
offset += 4 ;
if ( unsigned ) {
return value >>> 0 ;
}
return value ;
}
function readInt64 ( ) : bigint {
if ( offset + 8 > buffer . length ) {
throw new Error ( 'Buffer out of bounds' ) ;
}
const low = BigInt ( buffer [ offset ] | ( buffer [ offset + 1 ] << 8 ) | ( buffer [ offset + 2 ] << 16 ) | ( buffer [ offset + 3 ] << 24 ) ) ;
const high = BigInt ( buffer [ offset + 4 ] | ( buffer [ offset + 5 ] << 8 ) | ( buffer [ offset + 6 ] << 16 ) | ( buffer [ offset + 7 ] << 24 ) ) ;
offset += 8 ;
return ( high << 32 n ) | ( low & 0xffffffff n ) ;
}
function readVarInt ( ) : bigint {
const first = readInt8 ( ) ;
if ( first < 0xfd ) {
return BigInt ( first ) ;
} else if ( first === 0xfd ) {
return BigInt ( readInt16 ( ) ) ;
} else if ( first === 0xfe ) {
return BigInt ( readInt32 ( true ) ) ;
} else if ( first === 0xff ) {
return readInt64 ( ) ;
} else {
throw new Error ( "Invalid VarInt prefix" ) ;
}
}
function readSlice ( n : number | bigint ) : Uint8Array {
const length = Number ( n ) ;
if ( offset + length > buffer . length ) {
throw new Error ( 'Cannot read slice out of bounds' ) ;
}
const slice = buffer . slice ( offset , offset + length ) ;
offset += length ;
return slice ;
}
function readVarSlice ( ) : Uint8Array {
return readSlice ( readVarInt ( ) ) ;
}
function readVector ( ) : Uint8Array [ ] {
const count = readVarInt ( ) ;
const vector = [ ] ;
for ( let i = 0 ; i < count ; i ++ ) {
vector . push ( readVarSlice ( ) ) ;
}
return vector ;
}
// Parse raw transaction
const tx = {
status : {
confirmed : null ,
block_height : null ,
block_hash : null ,
block_time : null ,
}
} as Transaction ;
tx . version = readInt32 ( ) ;
const marker = readInt8 ( ) ;
const flag = readInt8 ( ) ;
let hasWitnesses = false ;
if (
marker === 0x00 &&
flag === 0x01
) {
hasWitnesses = true ;
} else {
offset -= 2 ;
}
const vinLen = readVarInt ( ) ;
tx . vin = [ ] ;
for ( let i = 0 ; i < vinLen ; ++ i ) {
const txid = uint8ArrayToHexString ( readSlice ( 32 ) . reverse ( ) ) ;
const vout = readInt32 ( true ) ;
const scriptsig = uint8ArrayToHexString ( readVarSlice ( ) ) ;
const sequence = readInt32 ( true ) ;
const is_coinbase = txid === '0' . repeat ( 64 ) ;
const scriptsig_asm = convertScriptSigAsm ( scriptsig ) ;
tx . vin . push ( { txid , vout , scriptsig , sequence , is_coinbase , scriptsig_asm , prevout : null } ) ;
}
const voutLen = readVarInt ( ) ;
tx . vout = [ ] ;
for ( let i = 0 ; i < voutLen ; ++ i ) {
const value = Number ( readInt64 ( ) ) ;
const scriptpubkeyArray = readVarSlice ( ) ;
const scriptpubkey = uint8ArrayToHexString ( scriptpubkeyArray )
const scriptpubkey_asm = convertScriptSigAsm ( scriptpubkey ) ;
const toAddress = scriptPubKeyToAddress ( scriptpubkey , network ) ;
const scriptpubkey_type = toAddress . type ;
const scriptpubkey_address = toAddress ? . address ;
tx . vout . push ( { value , scriptpubkey , scriptpubkey_asm , scriptpubkey_type , scriptpubkey_address } ) ;
}
let witnessSize = 0 ;
if ( hasWitnesses ) {
const startOffset = offset ;
for ( let i = 0 ; i < vinLen ; ++ i ) {
tx . vin [ i ] . witness = readVector ( ) . map ( uint8ArrayToHexString ) ;
}
witnessSize = offset - startOffset + 2 ;
}
tx . locktime = readInt32 ( true ) ;
if ( offset !== buffer . length ) {
throw new Error ( 'Transaction has unexpected data' ) ;
}
tx . size = buffer . length ;
tx . weight = ( tx . size - witnessSize ) * 3 + tx . size ;
tx . txid = txid ( tx ) ;
return tx ;
}
export function decodeRawTransaction ( rawtx : string , network : string ) : Transaction {
if ( ! rawtx . length || rawtx . length % 2 !== 0 || ! /^[0-9a-fA-F]*$/ . test ( rawtx ) ) {
throw new Error ( 'Invalid hex string' ) ;
}
const buffer = new Uint8Array ( rawtx . length / 2 ) ;
for ( let i = 0 ; i < rawtx . length ; i += 2 ) {
buffer [ i / 2 ] = parseInt ( rawtx . substring ( i , i + 2 ) , 16 ) ;
}
return fromBuffer ( buffer , network ) ;
}
function serializeTransaction ( tx : Transaction ) : Uint8Array {
const result : number [ ] = [ ] ;
// Add version
result . push ( . . . intToBytes ( tx . version , 4 ) ) ;
// Add input count and inputs
result . push ( . . . varIntToBytes ( tx . vin . length ) ) ;
for ( const input of tx . vin ) {
result . push ( . . . hexStringToUint8Array ( input . txid ) . reverse ( ) ) ;
result . push ( . . . intToBytes ( input . vout , 4 ) ) ;
const scriptSig = hexStringToUint8Array ( input . scriptsig ) ;
result . push ( . . . varIntToBytes ( scriptSig . length ) ) ;
result . push ( . . . scriptSig ) ;
result . push ( . . . intToBytes ( input . sequence , 4 ) ) ;
}
// Add output count and outputs
result . push ( . . . varIntToBytes ( tx . vout . length ) ) ;
for ( const output of tx . vout ) {
result . push ( . . . bigIntToBytes ( BigInt ( output . value ) , 8 ) ) ;
const scriptPubKey = hexStringToUint8Array ( output . scriptpubkey ) ;
result . push ( . . . varIntToBytes ( scriptPubKey . length ) ) ;
result . push ( . . . scriptPubKey ) ;
}
// Add locktime
result . push ( . . . intToBytes ( tx . locktime , 4 ) ) ;
return new Uint8Array ( result ) ;
}
function txid ( tx : Transaction ) : string {
const serializedTx = serializeTransaction ( tx ) ;
const hash1 = new Hash ( ) . update ( serializedTx ) . digest ( ) ;
const hash2 = new Hash ( ) . update ( hash1 ) . digest ( ) ;
return uint8ArrayToHexString ( hash2 . reverse ( ) ) ;
}
2024-11-28 12:07:05 +01:00
// Copied from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L177
2024-11-27 17:54:07 +01:00
export function countSigops ( transaction : Transaction ) : number {
let sigops = 0 ;
for ( const input of transaction . vin ) {
if ( input . scriptsig_asm ) {
sigops += countScriptSigops ( input . scriptsig_asm , true ) ;
}
if ( input . prevout ) {
switch ( true ) {
case input . prevout . scriptpubkey_type === 'p2sh' && input . witness ? . length === 2 && input . scriptsig && input . scriptsig . startsWith ( '160014' ) :
case input . prevout . scriptpubkey_type === 'v0_p2wpkh' :
sigops += 1 ;
break ;
case input . prevout ? . scriptpubkey_type === 'p2sh' && input . witness ? . length && input . scriptsig && input . scriptsig . startsWith ( '220020' ) :
case input . prevout . scriptpubkey_type === 'v0_p2wsh' :
if ( input . witness ? . length ) {
sigops += countScriptSigops ( convertScriptSigAsm ( input . witness [ input . witness . length - 1 ] ) , false , true ) ;
}
break ;
case input . prevout . scriptpubkey_type === 'p2sh' :
if ( input . inner_redeemscript_asm ) {
sigops += countScriptSigops ( input . inner_redeemscript_asm ) ;
}
break ;
}
}
}
for ( const output of transaction . vout ) {
if ( output . scriptpubkey_asm ) {
sigops += countScriptSigops ( output . scriptpubkey_asm , true ) ;
}
}
return sigops ;
}
function scriptPubKeyToAddress ( scriptPubKey : string , network : string ) : { address : string , type : string } {
// P2PKH
if ( /^76a914[0-9a-f]{40}88ac$/ . test ( scriptPubKey ) ) {
return { address : p2pkh ( scriptPubKey . substring ( 6 , 6 + 40 ) , network ) , type : 'p2pkh' } ;
}
// P2PK
if ( /^21[0-9a-f]{66}ac$/ . test ( scriptPubKey ) || /^41[0-9a-f]{130}ac$/ . test ( scriptPubKey ) ) {
return { address : null , type : 'p2pk' } ;
}
// P2SH
if ( /^a914[0-9a-f]{40}87$/ . test ( scriptPubKey ) ) {
return { address : p2sh ( scriptPubKey . substring ( 4 , 4 + 40 ) , network ) , type : 'p2sh' } ;
}
// P2WPKH
if ( /^0014[0-9a-f]{40}$/ . test ( scriptPubKey ) ) {
return { address : p2wpkh ( scriptPubKey . substring ( 4 , 4 + 40 ) , network ) , type : 'v0_p2wpkh' } ;
}
// P2WSH
if ( /^0020[0-9a-f]{64}$/ . test ( scriptPubKey ) ) {
return { address : p2wsh ( scriptPubKey . substring ( 4 , 4 + 64 ) , network ) , type : 'v0_p2wsh' } ;
}
// P2TR
if ( /^5120[0-9a-f]{64}$/ . test ( scriptPubKey ) ) {
return { address : p2tr ( scriptPubKey . substring ( 4 , 4 + 64 ) , network ) , type : 'v1_p2tr' } ;
}
// multisig
if ( /^[0-9a-f]+ae$/ . test ( scriptPubKey ) ) {
return { address : null , type : 'multisig' } ;
}
// anchor
if ( scriptPubKey === '51024e73' ) {
2025-01-08 15:25:19 +01:00
return { address : p2a ( network ) , type : 'anchor' } ;
2024-11-27 17:54:07 +01:00
}
// op_return
if ( /^6a/ . test ( scriptPubKey ) ) {
return { address : null , type : 'op_return' } ;
}
return { address : null , type : 'unknown' } ;
}
function p2pkh ( pubKeyHash : string , network : string ) : string {
const pubkeyHashArray = hexStringToUint8Array ( pubKeyHash ) ;
const version = [ 'testnet' , 'testnet4' , 'signet' ] . includes ( network ) ? 0x6f : 0x00 ;
const versionedPayload = Uint8Array . from ( [ version , . . . pubkeyHashArray ] ) ;
const hash1 = new Hash ( ) . update ( versionedPayload ) . digest ( ) ;
const hash2 = new Hash ( ) . update ( hash1 ) . digest ( ) ;
const checksum = hash2 . slice ( 0 , 4 ) ;
const finalPayload = Uint8Array . from ( [ . . . versionedPayload , . . . checksum ] ) ;
const bitcoinAddress = base58Encode ( finalPayload ) ;
return bitcoinAddress ;
}
function p2sh ( scriptHash : string , network : string ) : string {
const scriptHashArray = hexStringToUint8Array ( scriptHash ) ;
const version = [ 'testnet' , 'testnet4' , 'signet' ] . includes ( network ) ? 0xc4 : 0x05 ;
const versionedPayload = Uint8Array . from ( [ version , . . . scriptHashArray ] ) ;
const hash1 = new Hash ( ) . update ( versionedPayload ) . digest ( ) ;
const hash2 = new Hash ( ) . update ( hash1 ) . digest ( ) ;
const checksum = hash2 . slice ( 0 , 4 ) ;
const finalPayload = Uint8Array . from ( [ . . . versionedPayload , . . . checksum ] ) ;
const bitcoinAddress = base58Encode ( finalPayload ) ;
return bitcoinAddress ;
}
function p2wpkh ( pubKeyHash : string , network : string ) : string {
const pubkeyHashArray = hexStringToUint8Array ( pubKeyHash ) ;
const hrp = [ 'testnet' , 'testnet4' , 'signet' ] . includes ( network ) ? 'tb' : 'bc' ;
const version = 0 ;
const words = [ version ] . concat ( toWords ( pubkeyHashArray ) ) ;
const bech32Address = bech32Encode ( hrp , words ) ;
return bech32Address ;
}
function p2wsh ( scriptHash : string , network : string ) : string {
const scriptHashArray = hexStringToUint8Array ( scriptHash ) ;
const hrp = [ 'testnet' , 'testnet4' , 'signet' ] . includes ( network ) ? 'tb' : 'bc' ;
const version = 0 ;
const words = [ version ] . concat ( toWords ( scriptHashArray ) ) ;
const bech32Address = bech32Encode ( hrp , words ) ;
return bech32Address ;
}
function p2tr ( pubKeyHash : string , network : string ) : string {
const pubkeyHashArray = hexStringToUint8Array ( pubKeyHash ) ;
const hrp = [ 'testnet' , 'testnet4' , 'signet' ] . includes ( network ) ? 'tb' : 'bc' ;
const version = 1 ;
const words = [ version ] . concat ( toWords ( pubkeyHashArray ) ) ;
const bech32Address = bech32Encode ( hrp , words , 0x2bc830a3 ) ;
2025-01-08 15:25:19 +01:00
return bech32Address ;
}
function p2a ( network : string ) : string {
const pubkeyHashArray = hexStringToUint8Array ( '4e73' ) ;
const hrp = [ 'testnet' , 'testnet4' , 'signet' ] . includes ( network ) ? 'tb' : 'bc' ;
const version = 1 ;
const words = [ version ] . concat ( toWords ( pubkeyHashArray ) ) ;
const bech32Address = bech32Encode ( hrp , words , 0x2bc830a3 ) ;
2024-11-27 17:54:07 +01:00
return bech32Address ;
}
// base58 encoding
function base58Encode ( data : Uint8Array ) : string {
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" ;
let hexString = Array . from ( data )
. map ( byte = > byte . toString ( 16 ) . padStart ( 2 , '0' ) )
. join ( '' ) ;
let num = BigInt ( "0x" + hexString ) ;
let encoded = "" ;
while ( num > 0 ) {
const remainder = Number ( num % 58 n ) ;
num = num / 58 n ;
encoded = BASE58_ALPHABET [ remainder ] + encoded ;
}
for ( let byte of data ) {
if ( byte === 0 ) {
encoded = "1" + encoded ;
} else {
break ;
}
}
return encoded ;
}
// bech32 encoding
// Adapted from https://github.com/bitcoinjs/bech32/blob/5ceb0e3d4625561a459c85643ca6947739b2d83c/src/index.ts
function bech32Encode ( prefix : string , words : number [ ] , constant : number = 1 ) {
const BECH32_ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" ;
const checksum = createChecksum ( prefix , words , constant ) ;
const combined = words . concat ( checksum ) ;
let result = prefix + '1' ;
for ( let i = 0 ; i < combined . length ; ++ i ) {
result += BECH32_ALPHABET . charAt ( combined [ i ] ) ;
}
return result ;
}
function polymodStep ( pre ) {
const GENERATORS = [ 0x3b6a57b2 , 0x26508e6d , 0x1ea119fa , 0x3d4233dd , 0x2a1462b3 ] ;
const b = pre >> 25 ;
return (
( ( pre & 0x1ffffff ) << 5 ) ^
( ( b & 1 ? GENERATORS [ 0 ] : 0 ) ^
( b & 2 ? GENERATORS [ 1 ] : 0 ) ^
( b & 4 ? GENERATORS [ 2 ] : 0 ) ^
( b & 8 ? GENERATORS [ 3 ] : 0 ) ^
( b & 16 ? GENERATORS [ 4 ] : 0 ) )
) ;
}
function prefixChk ( prefix ) {
let chk = 1 ;
for ( let i = 0 ; i < prefix . length ; ++ i ) {
const c = prefix . charCodeAt ( i ) ;
chk = polymodStep ( chk ) ^ ( c >> 5 ) ;
}
chk = polymodStep ( chk ) ;
for ( let i = 0 ; i < prefix . length ; ++ i ) {
const c = prefix . charCodeAt ( i ) ;
chk = polymodStep ( chk ) ^ ( c & 0x1f ) ;
}
return chk ;
}
function createChecksum ( prefix : string , words : number [ ] , constant : number ) {
const POLYMOD_CONST = constant ;
let chk = prefixChk ( prefix ) ;
for ( let i = 0 ; i < words . length ; ++ i ) {
const x = words [ i ] ;
chk = polymodStep ( chk ) ^ x ;
}
for ( let i = 0 ; i < 6 ; ++ i ) {
chk = polymodStep ( chk ) ;
}
chk ^= POLYMOD_CONST ;
const checksum = [ ] ;
for ( let i = 0 ; i < 6 ; ++ i ) {
checksum . push ( ( chk >> ( 5 * ( 5 - i ) ) ) & 31 ) ;
}
return checksum ;
}
function convertBits ( data , fromBits , toBits , pad ) {
let acc = 0 ;
let bits = 0 ;
const ret = [ ] ;
const maxV = ( 1 << toBits ) - 1 ;
for ( let i = 0 ; i < data . length ; ++ i ) {
const value = data [ i ] ;
if ( value < 0 || value >> fromBits ) throw new Error ( 'Invalid value' ) ;
acc = ( acc << fromBits ) | value ;
bits += fromBits ;
while ( bits >= toBits ) {
bits -= toBits ;
ret . push ( ( acc >> bits ) & maxV ) ;
}
}
if ( pad ) {
if ( bits > 0 ) {
ret . push ( ( acc << ( toBits - bits ) ) & maxV ) ;
}
} else if ( bits >= fromBits || ( ( acc << ( toBits - bits ) ) & maxV ) ) {
throw new Error ( 'Invalid data' ) ;
}
return ret ;
}
function toWords ( bytes ) {
return convertBits ( bytes , 8 , 5 , true ) ;
}
// Helper functions
function uint8ArrayToHexString ( uint8Array : Uint8Array ) : string {
return Array . from ( uint8Array ) . map ( byte = > byte . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
}
function hexStringToUint8Array ( hex : string ) : Uint8Array {
const buf = new Uint8Array ( hex . length / 2 ) ;
for ( let i = 0 ; i < buf . length ; i ++ ) {
buf [ i ] = parseInt ( hex . substr ( i * 2 , 2 ) , 16 ) ;
}
return buf ;
}
function intToBytes ( value : number , byteLength : number ) : number [ ] {
const bytes = [ ] ;
for ( let i = 0 ; i < byteLength ; i ++ ) {
bytes . push ( ( value >> ( 8 * i ) ) & 0xff ) ;
}
return bytes ;
}
function bigIntToBytes ( value : bigint , byteLength : number ) : number [ ] {
const bytes = [ ] ;
for ( let i = 0 ; i < byteLength ; i ++ ) {
bytes . push ( Number ( ( value >> BigInt ( 8 * i ) ) & 0xff n ) ) ;
}
return bytes ;
}
function varIntToBytes ( value : number | bigint ) : number [ ] {
const bytes = [ ] ;
if ( typeof value === 'number' ) {
if ( value < 0xfd ) {
bytes . push ( value ) ;
} else if ( value <= 0xffff ) {
bytes . push ( 0xfd , value & 0xff , ( value >> 8 ) & 0xff ) ;
} else if ( value <= 0xffffffff ) {
bytes . push ( 0xfe , . . . intToBytes ( value , 4 ) ) ;
}
} else {
if ( value < 0xfd n ) {
bytes . push ( Number ( value ) ) ;
} else if ( value <= 0xffff n ) {
bytes . push ( 0xfd , Number ( value & 0xff n ) , Number ( ( value >> 8 n ) & 0xff n ) ) ;
} else if ( value <= 0xffffffff n ) {
bytes . push ( 0xfe , . . . intToBytes ( Number ( value ) , 4 ) ) ;
} else {
bytes . push ( 0xff , . . . bigIntToBytes ( value , 8 ) ) ;
}
}
return bytes ;
}
2024-11-28 12:07:05 +01:00
// Inversed the opcodes object from https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/utils/bitcoin-script.ts#L1
2024-11-27 17:54:07 +01:00
const opcodes = {
0 : 'OP_0' ,
76 : 'OP_PUSHDATA1' ,
77 : 'OP_PUSHDATA2' ,
78 : 'OP_PUSHDATA4' ,
79 : 'OP_PUSHNUM_NEG1' ,
80 : 'OP_RESERVED' ,
81 : 'OP_PUSHNUM_1' ,
82 : 'OP_PUSHNUM_2' ,
83 : 'OP_PUSHNUM_3' ,
84 : 'OP_PUSHNUM_4' ,
85 : 'OP_PUSHNUM_5' ,
86 : 'OP_PUSHNUM_6' ,
87 : 'OP_PUSHNUM_7' ,
88 : 'OP_PUSHNUM_8' ,
89 : 'OP_PUSHNUM_9' ,
90 : 'OP_PUSHNUM_10' ,
91 : 'OP_PUSHNUM_11' ,
92 : 'OP_PUSHNUM_12' ,
93 : 'OP_PUSHNUM_13' ,
94 : 'OP_PUSHNUM_14' ,
95 : 'OP_PUSHNUM_15' ,
96 : 'OP_PUSHNUM_16' ,
97 : 'OP_NOP' ,
98 : 'OP_VER' ,
99 : 'OP_IF' ,
100 : 'OP_NOTIF' ,
101 : 'OP_VERIF' ,
102 : 'OP_VERNOTIF' ,
103 : 'OP_ELSE' ,
104 : 'OP_ENDIF' ,
105 : 'OP_VERIFY' ,
106 : 'OP_RETURN' ,
107 : 'OP_TOALTSTACK' ,
108 : 'OP_FROMALTSTACK' ,
109 : 'OP_2DROP' ,
110 : 'OP_2DUP' ,
111 : 'OP_3DUP' ,
112 : 'OP_2OVER' ,
113 : 'OP_2ROT' ,
114 : 'OP_2SWAP' ,
115 : 'OP_IFDUP' ,
116 : 'OP_DEPTH' ,
117 : 'OP_DROP' ,
118 : 'OP_DUP' ,
119 : 'OP_NIP' ,
120 : 'OP_OVER' ,
121 : 'OP_PICK' ,
122 : 'OP_ROLL' ,
123 : 'OP_ROT' ,
124 : 'OP_SWAP' ,
125 : 'OP_TUCK' ,
126 : 'OP_CAT' ,
127 : 'OP_SUBSTR' ,
128 : 'OP_LEFT' ,
129 : 'OP_RIGHT' ,
130 : 'OP_SIZE' ,
131 : 'OP_INVERT' ,
132 : 'OP_AND' ,
133 : 'OP_OR' ,
134 : 'OP_XOR' ,
135 : 'OP_EQUAL' ,
136 : 'OP_EQUALVERIFY' ,
137 : 'OP_RESERVED1' ,
138 : 'OP_RESERVED2' ,
139 : 'OP_1ADD' ,
140 : 'OP_1SUB' ,
141 : 'OP_2MUL' ,
142 : 'OP_2DIV' ,
143 : 'OP_NEGATE' ,
144 : 'OP_ABS' ,
145 : 'OP_NOT' ,
146 : 'OP_0NOTEQUAL' ,
147 : 'OP_ADD' ,
148 : 'OP_SUB' ,
149 : 'OP_MUL' ,
150 : 'OP_DIV' ,
151 : 'OP_MOD' ,
152 : 'OP_LSHIFT' ,
153 : 'OP_RSHIFT' ,
154 : 'OP_BOOLAND' ,
155 : 'OP_BOOLOR' ,
156 : 'OP_NUMEQUAL' ,
157 : 'OP_NUMEQUALVERIFY' ,
158 : 'OP_NUMNOTEQUAL' ,
159 : 'OP_LESSTHAN' ,
160 : 'OP_GREATERTHAN' ,
161 : 'OP_LESSTHANOREQUAL' ,
162 : 'OP_GREATERTHANOREQUAL' ,
163 : 'OP_MIN' ,
164 : 'OP_MAX' ,
165 : 'OP_WITHIN' ,
166 : 'OP_RIPEMD160' ,
167 : 'OP_SHA1' ,
168 : 'OP_SHA256' ,
169 : 'OP_HASH160' ,
170 : 'OP_HASH256' ,
171 : 'OP_CODESEPARATOR' ,
172 : 'OP_CHECKSIG' ,
173 : 'OP_CHECKSIGVERIFY' ,
174 : 'OP_CHECKMULTISIG' ,
175 : 'OP_CHECKMULTISIGVERIFY' ,
176 : 'OP_NOP1' ,
177 : 'OP_CHECKLOCKTIMEVERIFY' ,
178 : 'OP_CHECKSEQUENCEVERIFY' ,
179 : 'OP_NOP4' ,
180 : 'OP_NOP5' ,
181 : 'OP_NOP6' ,
182 : 'OP_NOP7' ,
183 : 'OP_NOP8' ,
184 : 'OP_NOP9' ,
185 : 'OP_NOP10' ,
186 : 'OP_CHECKSIGADD' ,
253 : 'OP_PUBKEYHASH' ,
254 : 'OP_PUBKEY' ,
255 : 'OP_INVALIDOPCODE' ,
} ;