Skip to content

Instantly share code, notes, and snippets.

@junderw
Last active April 19, 2024 07:02
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save junderw/b43af3253ea5865ed52cb51c200ac19c to your computer and use it in GitHub Desktop.
Save junderw/b43af3253ea5865ed52cb51c200ac19c to your computer and use it in GitHub Desktop.
Estimate bytes for bitcoin transactions
// Usage:
// getByteCount({'MULTISIG-P2SH:2-4':45},{'P2PKH':1}) Means "45 inputs of P2SH Multisig and 1 output of P2PKH"
// getByteCount({'P2PKH':1,'MULTISIG-P2SH:2-3':2},{'P2PKH':2}) means "1 P2PKH input and 2 Multisig P2SH (2 of 3) inputs along with 2 P2PKH outputs"
function getByteCount(inputs, outputs) {
var totalWeight = 0
var hasWitness = false
var inputCount = 0
var outputCount = 0
// assumes compressed pubkeys in all cases.
var types = {
// MULTISIG-* do not include pubkeys or signatures yet (this is calculated at runtime)
// sigs = 73 and pubkeys = 34 (these include pushdata byte)
'inputs': {
// Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:3(max))
// + (script_bytes(OP_0,PUSHDATA(max:3),m,n,CHECK_MULTISIG):5)
'MULTISIG-P2SH': 51 * 4,
// Segwit: (push_count:1) + (script_bytes(OP_0,PUSHDATA(max:3),m,n,CHECK_MULTISIG):5)
// Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:1)
'MULTISIG-P2WSH': 8 + (41 * 4),
// Segwit: (push_count:1) + (script_bytes(OP_0,PUSHDATA(max:3),m,n,CHECK_MULTISIG):5)
// Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:1) + (p2wsh:35)
'MULTISIG-P2SH-P2WSH': 8 + (76 * 4),
// Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:1) + (sig:73) + (pubkey:34)
'P2PKH': 148 * 4,
// Segwit: (push_count:1) + (sig:73) + (pubkey:34)
// Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:1)
'P2WPKH': 108 + (41 * 4),
// Segwit: (push_count:1) + (sig:73) + (pubkey:34)
// Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:1) + (p2wpkh:23)
'P2SH-P2WPKH': 108 + (64 * 4)
},
'outputs': {
// (p2sh:24) + (amount:8)
'P2SH': 32 * 4,
// (p2pkh:26) + (amount:8)
'P2PKH': 34 * 4,
// (p2wpkh:23) + (amount:8)
'P2WPKH': 31 * 4,
// (p2wsh:35) + (amount:8)
'P2WSH': 43 * 4
}
}
function checkUInt53 (n) {
if (n < 0 || n > Number.MAX_SAFE_INTEGER || n % 1 !== 0) throw new RangeError('value out of range')
}
function varIntLength (number) {
checkUInt53(number)
return (
number < 0xfd ? 1
: number <= 0xffff ? 3
: number <= 0xffffffff ? 5
: 9
)
}
Object.keys(inputs).forEach(function(key) {
checkUInt53(inputs[key])
if (key.slice(0,8) === 'MULTISIG') {
// ex. "MULTISIG-P2SH:2-3" would mean 2 of 3 P2SH MULTISIG
var keyParts = key.split(':')
if (keyParts.length !== 2) throw new Error('invalid input: ' + key)
var newKey = keyParts[0]
var mAndN = keyParts[1].split('-').map(function (item) { return parseInt(item) })
totalWeight += types.inputs[newKey] * inputs[key]
var multiplyer = (newKey === 'MULTISIG-P2SH') ? 4 : 1
totalWeight += ((73 * mAndN[0]) + (34 * mAndN[1])) * multiplyer * inputs[key]
} else {
totalWeight += types.inputs[key] * inputs[key]
}
inputCount += inputs[key]
if (key.indexOf('W') >= 0) hasWitness = true
})
Object.keys(outputs).forEach(function(key) {
checkUInt53(outputs[key])
totalWeight += types.outputs[key] * outputs[key]
outputCount += outputs[key]
})
if (hasWitness) totalWeight += 2
totalWeight += 8 * 4
totalWeight += varIntLength(inputCount) * 4
totalWeight += varIntLength(outputCount) * 4
return Math.ceil(totalWeight / 4)
}
@BruceChar
Copy link

Hi, @junderw. How can I get the type from the address? Thanks!

@junderw
Copy link
Author

junderw commented Aug 4, 2019

it depends on what libraries you have available, but an address can only tell you the output type.

for input type you should already know (since you are signing the inputs)

@educob
Copy link

educob commented Feb 17, 2020

Hi.
I create generic scripts in this way:

let redeemScript = bitcoin.script.compile(script) // REDEEM Script
const { address } = bitcoin.payments.p2sh({ redeem: { output: redeemScript, network: this.bitcoin_network }, network: this.bitcoin_network })

The input to spend an utxo to that script would be of type P2SH-P2WPKH?

Thanks.

@junderw
Copy link
Author

junderw commented Feb 18, 2020

depends on the contents of script

@educob
Copy link

educob commented Feb 18, 2020

A generic script like the one in the Andreas Antonopoulus book.
Not any in particular but a genric one with IF-ELSE, time constraints, etc.
Thanks.

@junderw
Copy link
Author

junderw commented Feb 19, 2020

nope. this function does not support that.

the function literally is just a list of strict script patterns and how many bytes they use.

Your script could have 5 bytes or 1000 bytes. You would have to make your own input type and add it in to the code.

P2SH output size is same regardless of contents.

@educob
Copy link

educob commented Feb 19, 2020

Thanks for your answer.
Let's say the whole transaction has x bytes.
What would be the amount of bytes to use if the transaction uses segwit?

Forgive my poor memory but isn't the case that with segwit the scripts are not included in the trasanction? In that case it wouldn't matter how big they are.
Thanks.

@junderw
Copy link
Author

junderw commented Jul 23, 2020

In the function, anything multiplied by 4 is a non-segwit byte.

Segwit bytes are removed from the transaction ID hash, but they are still included in the wtxid hash, and for fee calculation segwit bytes are counted as 0.25 bytes each using a new unit called weight 1 byte = 4 weight, but segwit data 1 byte = 1 weight, so segwit bytes get a 75% discount. This is why the function has * 4 all over the place.

@bajian
Copy link

bajian commented Dec 29, 2020

'outputs': {
            'P2SH': 32 * 4, //128
            'P2PKH': 34 * 4, //136
            'P2WPKH': 31 * 4, //124
            'P2WSH': 43 * 4 //172
        }

Does p2sh output contain p2sh-p2wsh and p2sh-p2wpkh?
@junderw

@junderw
Copy link
Author

junderw commented Dec 29, 2020

  1. p2sh(p2wsh(xxx))
  2. p2sh(xxx)

So anyhing dealing with p2sh will be 'P2SH'

@junderw
Copy link
Author

junderw commented Dec 29, 2020

Reason: When using p2sh or p2wsh "wrapping" scripts, they are the only one in the output. the redeemscript/witnessscript will be in the input of the spending transaction (which is irrelevant to estimating byte size of THIS transaction)

@junderw
Copy link
Author

junderw commented Dec 29, 2020

@bajian let me know if you have any questions.

@bajian
Copy link

bajian commented Dec 29, 2020

Oh, I see. Thanks ! @junderw

@landabaso
Copy link

landabaso commented Mar 4, 2022

Nice tool!

I understand this covers quite a few common cases. I'm wondering how could one extend this tool to compute sizes for other stuff.
Timelocks for example.

What if the transaction spends a P2WSH (or P2SH) where the locking script is a timelock?

For example, Alice can sign after relative timelock expires: AliceSignature TRUE. Or Both Alice and Bob must agree to sign before time lock expires: AliceSignature BobSignature FALSE.

This would be the locking script:

      OP_IF
          ${relativeLockTime}
          OP_CHECKSEQUENCEVERIFY
          OP_DROP
      OP_ELSE
          ${bobPubKey}
          OP_CHECKSIGVERIFY
      OP_ENDIF
      ${alicePubKey}
      OP_CHECKSIG

How could one estimate the size for AliceSignature TRUE and AliceSignature BobSignature FALSE scripts with P2SH/P2WSH for example? And how compute the size of the script itself?

I imagine each signature has a fixed size and the TRUE or FALSE OPs also have a fixed size. Also one could compile the script and see the size. Is that correct? I have the feeling it's not as easy as adding these values. Is it?

@junderw
Copy link
Author

junderw commented Mar 4, 2022

Well, first you'd count the bytes of the script:

1 OP_IF
5    ${relativeLockTime}
1    OP_CHECKSEQUENCEVERIFY
1    OP_DROP
1 OP_ELSE
34    ${bobPubKey}
1    OP_CHECKSIGVERIFY
1 OP_ENDIF
34 ${alicePubKey}
1 OP_CHECKSIG

80 bytes for the script.

Now for the spend paths:

73 ${aliceSig}
1 OP_TRUE

OR

73 ${aliceSig}
73 ${bobSig}
1 OP_FALSE

So 74 or 147 bytes.

Then look at the comments to get an idea of what other things to include in the calculation.

@landabaso
Copy link

I get the idea. Thanks @junderw !

@educob
Copy link

educob commented Oct 30, 2022

Hi Jonathan.

I have run the method with inputs: { P2WPKH: 2 } & outputs: { P2PKH: 1, P2WPKH: 1} and it returns 181 bytes.
But when I create the tx and call: const size = tx.virtualSize() I get 211.

Why are they different?

Thanks.

@junderw
Copy link
Author

junderw commented Oct 31, 2022

Show me the raw transaction, please.

@educob
Copy link

educob commented Nov 22, 2022

Hi.

This is my take on computing bytes for a SCRIPT-P2WSH.
I've run the method for 3 scripts and for 2 it gave the exact amount. In the 3rd if fails 11 bytes short.
The script that failed was the andreas antonopoulos one with script bytes: 196 bytes and path bytes (IF-IF path): 149 bytes

it has to be called:
getByteCount( { "SCRIPT:xxx-yyy": 1}, outputs)
where xxx is the locking script bytes taken from:

const script = bitcoin.script.compile(script) 
const scriptBytes = script.length

yyy: is the path (unlocking script) bytes.

These are the updates on the getCountBytes method.

var types = {
        // MULTISIG-* do not include pubkeys or signatures yet (this is calculated at runtime)
        // sigs = 73 and pubkeys = 34 (these include pushdata byte)
        'inputs': {
          // Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:1) = 41. utxo 
          'SCRIPT': 41 * 4 + 4, // + 4 comes from real txs.

...

Object.keys(inputs).forEach(function(key) {
      checkUInt53(inputs[key])
      if (key.slice(0,6) === 'SCRIPT') {
        var keyParts = key.split(':')
        if (keyParts.length !== 2) throw new Error('invalid input: ' + key)
        var newKey = keyParts[0]
        var scriptAndPath = keyParts[1].split('-').map(function (item) { return parseInt(item) })
        totalWeight += types.inputs[newKey] * inputs[key]
        totalWeight += (scriptAndPath[0] + scriptAndPath[1]) * inputs[key]
      } else if (key.slice(0,8) === 'MULTISIG') {

I would appreciate comments.
Thanks.

@winktool
Copy link

plz how to getByteCount with PT2R

@landabaso
Copy link

landabaso commented Nov 14, 2023

I've been reviewing this script for using it to calculate the total transaction weight for combining SegWit + non-Segwit inputs. My understanding from reading the BIPs is that in a transaction with hasWitness, a single byte is then also required for non-witness inputs to encode the length of the empty witness stack: encodeLength(0) + 0 = 1.

bitcoinjs-lib seems to have specific handling for this: bitcoinjs-lib transaction.ts L228.

Should the weight calculation be modified to better account for the size of the witness data for each input? Something along this (at the end of the snippet):

if (hasWitness) totalWeight += inputs.length;

Note this might not be entirely correct since the witness data size for SegWit inputs might vary, requiring more than one byte for encoding the length of the witness stack.

(EDIT: The witness stack size is already taken into account but only for segwit inputs. I believe the +1 byte is still missing for non-Segwit inputs if hasWitness).

I'm aware there could be nuances I'm missing, and I'd value your perspective on this matter...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment