|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import json |
| 4 | +import time |
| 5 | +from bitcoinutils.setup import setup |
| 6 | +from bitcoinutils.transactions import Transaction, TxInput, TxOutput, TxWitnessInput |
| 7 | +from bitcoinutils.script import Script |
| 8 | +from bitcoinutils.keys import P2trAddress |
| 9 | +from bitcoinutils.utils import to_satoshis |
| 10 | +from bitcoinutils.block import Block, BlockHeader |
| 11 | +import hashlib |
| 12 | + |
| 13 | + |
| 14 | +def calculate_merkle_root(txid_list): |
| 15 | + """ |
| 16 | + Calculates merkel root by hashing pairwise txids till only 1 is left. |
| 17 | +
|
| 18 | + Args: |
| 19 | + txid_list: List of the transaction ids in the block |
| 20 | +
|
| 21 | + Returns: |
| 22 | + (bytes): The merkle root of the block. |
| 23 | + """ |
| 24 | + if len(txid_list) == 1: |
| 25 | + return bytes.fromhex(txid_list[0]) |
| 26 | + |
| 27 | + # Convert each txid from big-endian hex to little-endian bytes. |
| 28 | + current_layer = [bytes.fromhex(txid)[::-1] for txid in txid_list] |
| 29 | + |
| 30 | + while len(current_layer) > 1: |
| 31 | + next_layer = [] |
| 32 | + for i in range(0, len(current_layer), 2): |
| 33 | + # If there's an odd number, duplicate the last one. |
| 34 | + if i + 1 < len(current_layer): |
| 35 | + pair = current_layer[i] + current_layer[i + 1] |
| 36 | + else: |
| 37 | + pair = current_layer[i] + current_layer[i] |
| 38 | + # Perform double SHA-256 on the concatenated pair. |
| 39 | + next_layer.append(hashlib.sha256(hashlib.sha256(pair).digest()).digest()) |
| 40 | + current_layer = next_layer |
| 41 | + |
| 42 | + # Reverse back to big-endian and return the bytes. |
| 43 | + return current_layer[0][::-1] |
| 44 | + |
| 45 | + |
| 46 | +def calculate_witness_root_hash(wtxid_list): |
| 47 | + """ |
| 48 | + Calculates witness root hash by hashing pairwise wtxids till only 1 is left. |
| 49 | +
|
| 50 | + Args: |
| 51 | + wtxid_list: List of the witness transaction ids in the block |
| 52 | +
|
| 53 | + Returns: |
| 54 | + (bytes): The witness root hash of the block. |
| 55 | + """ |
| 56 | + if len(wtxid_list) == 1: |
| 57 | + # For a single txid, return its bytes (big-endian) representation. |
| 58 | + return bytes.fromhex(wtxid_list[0]) |
| 59 | + |
| 60 | + # Convert each txid from big-endian hex to little-endian bytes. |
| 61 | + current_layer = [bytes.fromhex(wtxid)[::-1] for wtxid in wtxid_list] |
| 62 | + |
| 63 | + while len(current_layer) > 1: |
| 64 | + next_layer = [] |
| 65 | + for i in range(0, len(current_layer), 2): |
| 66 | + # If there's an odd number, duplicate the last one. |
| 67 | + if i + 1 < len(current_layer): |
| 68 | + pair = current_layer[i] + current_layer[i + 1] |
| 69 | + else: |
| 70 | + pair = current_layer[i] + current_layer[i] |
| 71 | + # Perform double SHA-256 on the concatenated pair. |
| 72 | + next_layer.append(hashlib.sha256(hashlib.sha256(pair).digest()).digest()) |
| 73 | + current_layer = next_layer |
| 74 | + |
| 75 | + return current_layer[0] |
| 76 | + |
| 77 | + |
| 78 | +def calculate_witness_commitment(witness_root_hash, witness_reserved_value): |
| 79 | + """ |
| 80 | + Calculates the witness commitment by performing a double SHA-256 |
| 81 | + on the concatenation of the witness_root_hash and witness_reserved_value. |
| 82 | +
|
| 83 | + Args: |
| 84 | + witness_root_hash(hex): witness root hash of the block |
| 85 | + witness_reserved_value(hex): the reserved value stored in the witness. |
| 86 | +
|
| 87 | + Returns: |
| 88 | + (hex): hex of the witness commitment |
| 89 | +
|
| 90 | + """ |
| 91 | + # Convert both hex strings to bytes and concatenate them |
| 92 | + combined = bytes.fromhex(witness_root_hash + witness_reserved_value) |
| 93 | + |
| 94 | + # Perform double SHA256 hashing |
| 95 | + hash1 = hashlib.sha256(combined).digest() |
| 96 | + hash2 = hashlib.sha256(hash1).digest() |
| 97 | + return hash2.hex() |
| 98 | + |
| 99 | + |
| 100 | +def create_block(coinbase_tx, tx1): |
| 101 | + # Note: Here I have for simplicity taken just the coinbase tx and another transaction (the test_tx the mempool), |
| 102 | + # more generally we just take the list of tx. |
| 103 | + |
| 104 | + txid_list = [coinbase_tx.get_txid(), tx1.get_txid()] |
| 105 | + merkle_root = calculate_merkle_root(txid_list) |
| 106 | + prev_block_hash = bytes.fromhex( |
| 107 | + "0000000000000000000000000000000000000000000000000000000000000000" |
| 108 | + ) |
| 109 | + # version https://learnmeabitcoin.com/technical/block/version/ |
| 110 | + version = "40000000" |
| 111 | + version = int.from_bytes(bytes.fromhex(version), byteorder="little") |
| 112 | + timestamp = int(time.time()) |
| 113 | + # resource: https://learnmeabitcoin.com/technical/block/bits/ |
| 114 | + bits = "1f00ffff" |
| 115 | + bits = int.from_bytes(bytes.fromhex(bits), byteorder="big") |
| 116 | + |
| 117 | + nonce = 0 |
| 118 | + |
| 119 | + block_header = BlockHeader( |
| 120 | + version, |
| 121 | + previous_block_hash=prev_block_hash, |
| 122 | + merkle_root=merkle_root, |
| 123 | + timestamp=timestamp, |
| 124 | + target_bits=bits, |
| 125 | + nonce=nonce, |
| 126 | + ) |
| 127 | + magic = bytes.fromhex("f9beb4d9") |
| 128 | + block_size = None |
| 129 | + transaction_count = len([coinbase_tx, tx1]) |
| 130 | + block = Block( |
| 131 | + magic=magic, |
| 132 | + block_size=block_size, |
| 133 | + header=block_header, |
| 134 | + transactions=[coinbase_tx, tx1], |
| 135 | + transaction_count=transaction_count, |
| 136 | + ) |
| 137 | + return block |
| 138 | + |
| 139 | + |
| 140 | +def mine_block(block_header_bytes, target_hex): |
| 141 | + """ |
| 142 | + Mine a block by iterating through nonce values. |
| 143 | +
|
| 144 | + Args: |
| 145 | + block_header_bytes (bytes): The 80-byte block header with a placeholder nonce. |
| 146 | + target_hex (str): The difficulty target as a hex string (256-bit number). |
| 147 | +
|
| 148 | + Returns: |
| 149 | + (int, bytes): The nonce value and the corresponding block header hash that meets the target. |
| 150 | + """ |
| 151 | + target_int = int(target_hex, 16) |
| 152 | + print("Target (int):", target_int) |
| 153 | + |
| 154 | + for nonce in range(2**32): |
| 155 | + # Replace the last 4 bytes of the header with the current nonce in little-endian format. |
| 156 | + header_with_nonce = block_header_bytes[:-4] + nonce.to_bytes( |
| 157 | + 4, byteorder="little" |
| 158 | + ) |
| 159 | + |
| 160 | + hash_once = hashlib.sha256(header_with_nonce).digest() |
| 161 | + hash_twice = hashlib.sha256(hash_once).digest() |
| 162 | + |
| 163 | + # Bitcoin displays the block hash in big-endian order. |
| 164 | + block_hash = hash_twice[::-1] |
| 165 | + block_hash_int = int.from_bytes(block_hash, byteorder="big") |
| 166 | + |
| 167 | + if block_hash_int < target_int: |
| 168 | + print("Success! Nonce found:", nonce) |
| 169 | + return nonce, block_hash |
| 170 | + |
| 171 | + # Optional: print progress every so often |
| 172 | + if nonce % 1000000 == 0: |
| 173 | + print("Tried nonce:", nonce) |
| 174 | + |
| 175 | + return None, None |
| 176 | + |
| 177 | + |
| 178 | +def main(): |
| 179 | + |
| 180 | + # mock transaction details, this transaction would be the first transaction |
| 181 | + # of the block after the coinbase transaction |
| 182 | + tx_details = { |
| 183 | + "txid": "00000a2d1a9e29116b539b85b6e893213b1ed95a08b7526a8d59a4b088fc6571", |
| 184 | + "version": 1, |
| 185 | + "locktime": 0, |
| 186 | + "vin": [ |
| 187 | + { |
| 188 | + "txid": "2e4843d552ca9487efd9e69c0359f05375b7de5449eb49510d17a25bb5b15ec0", |
| 189 | + "vout": 1, |
| 190 | + "prevout": { |
| 191 | + "scriptpubkey": "512065fd3d423ea46a70505248db989e7302bfbbdd64ee4193dd9a59f69894f0de48", |
| 192 | + "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 65fd3d423ea46a70505248db989e7302bfbbdd64ee4193dd9a59f69894f0de48", |
| 193 | + "scriptpubkey_type": "v1_p2tr", |
| 194 | + "scriptpubkey_address": "bc1pvh7n6s375348q5zjfrde38nnq2lmhhtyaeqe8hv6t8mf398smeyqnug47s", |
| 195 | + "value": 13413 |
| 196 | + }, |
| 197 | + "scriptsig": "", |
| 198 | + "scriptsig_asm": "", |
| 199 | + "witness": [ |
| 200 | + "29783b151d376d5178451ce14f62b091059021680bff36aec2814e33ecacf130e8aa92d6da23f35be7a8c2245b8f910261d4e6a5169f79d6ff7a3f412981f486" |
| 201 | + ], |
| 202 | + "is_coinbase": False, |
| 203 | + "sequence": 1610616404 |
| 204 | + } |
| 205 | + ], |
| 206 | + "vout": [ |
| 207 | + { |
| 208 | + "scriptpubkey": "51204b918d31f22461021ed54e354ac9dcbbe94b98edcfd3615b76c068b08222a87f", |
| 209 | + "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 4b918d31f22461021ed54e354ac9dcbbe94b98edcfd3615b76c068b08222a87f", |
| 210 | + "scriptpubkey_type": "v1_p2tr", |
| 211 | + "scriptpubkey_address": "bc1pfwgc6v0jy3ssy8k4fc654jwuh055hx8delfkzkmkcp5tpq3z4pls7tx8q3", |
| 212 | + "value": 2908 |
| 213 | + }, |
| 214 | + { |
| 215 | + "scriptpubkey": "512065fd3d423ea46a70505248db989e7302bfbbdd64ee4193dd9a59f69894f0de48", |
| 216 | + "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 65fd3d423ea46a70505248db989e7302bfbbdd64ee4193dd9a59f69894f0de48", |
| 217 | + "scriptpubkey_type": "v1_p2tr", |
| 218 | + "scriptpubkey_address": "bc1pvh7n6s375348q5zjfrde38nnq2lmhhtyaeqe8hv6t8mf398smeyqnug47s", |
| 219 | + "value": 8503 |
| 220 | + } |
| 221 | + ], |
| 222 | + "size": 205, |
| 223 | + "weight": 616, |
| 224 | + "fee": 2002, |
| 225 | + "status": { |
| 226 | + "confirmed": True, |
| 227 | + "block_height": 834552, |
| 228 | + "block_hash": "00000000000000000001dd0468a70c94f619251d286585cff57aeb4bd9ede330", |
| 229 | + "block_time": 1710355598 |
| 230 | + }, |
| 231 | + "hex": "01000000000101c05eb1b55ba2170d5149eb4954deb77553f059039ce6d9ef8794ca52d543482e0100000000540e0060025c0b0000000000002251204b918d31f22461021ed54e354ac9dcbbe94b98edcfd3615b76c068b08222a87f372100000000000022512065fd3d423ea46a70505248db989e7302bfbbdd64ee4193dd9a59f69894f0de48014029783b151d376d5178451ce14f62b091059021680bff36aec2814e33ecacf130e8aa92d6da23f35be7a8c2245b8f910261d4e6a5169f79d6ff7a3f412981f48600000000" |
| 232 | + } |
| 233 | + |
| 234 | + |
| 235 | + setup("mainnet") |
| 236 | + # mock requirements |
| 237 | + from_txid = "0000000000000000000000000000000000000000000000000000000000000000" |
| 238 | + to_addr = "bc1pvh7n6s375348q5zjfrde38nnq2lmhhtyaeqe8hv6t8mf398smeyqnug47s" |
| 239 | + to_addr = P2trAddress(to_addr) |
| 240 | + |
| 241 | + # First create a coinbase transaction which is the first transaction of the block |
| 242 | + # The witness reserved value is a 32-byte value that is reserved for future use. |
| 243 | + # and the coinbase transaction must have it in its input's witness |
| 244 | + witness_reserved_value = ( |
| 245 | + "0000000000000000000000000000000000000000000000000000000000000000" |
| 246 | + ) |
| 247 | + |
| 248 | + # Constructing the coinbase transaction |
| 249 | + tx1 = Transaction() |
| 250 | + tx1 = tx1.from_raw(tx_details["hex"]) |
| 251 | + txinp = TxInput( |
| 252 | + txid=from_txid, |
| 253 | + txout_index=0, |
| 254 | + script_sig=Script([witness_reserved_value]), |
| 255 | + ) |
| 256 | + |
| 257 | + # Witness stack contains the list of witness data for the transaction |
| 258 | + # learn more: https://learnmeabitcoin.com/technical/upgrades/segregated-witness/ |
| 259 | + witness_stack = [witness_reserved_value] |
| 260 | + |
| 261 | + # wtxid is different from txid |
| 262 | + wtxid = tx1.get_wtxid(); |
| 263 | + print("Wtx id :", wtxid) |
| 264 | + |
| 265 | + # Coinbase wtxid must be set to all zeros to avoid circular reference |
| 266 | + # Learn more: https://learnmeabitcoin.com/technical/transaction/wtxid/ |
| 267 | + coinbase_wtxid = "0" * 64 |
| 268 | + |
| 269 | + witness_root_hash = calculate_witness_root_hash([coinbase_wtxid, wtxid]) |
| 270 | + witness_root_hash = witness_root_hash.hex() |
| 271 | + print("Witness root hash ", witness_root_hash) |
| 272 | + |
| 273 | + # Calculating the witness commitment hash (double SHA256 of witness_root_hash and witness_reserved_value) |
| 274 | + witness_commitment_hash = calculate_witness_commitment( |
| 275 | + witness_root_hash, witness_reserved_value |
| 276 | + ) |
| 277 | + |
| 278 | + # The commitment is constructed as: |
| 279 | + # commitment = "6a24aa21a9ed" + doublesha256(witness_root_hash, witness_reserved_value) |
| 280 | + # where commitment is made of (source BIP 141): |
| 281 | + # 1-byte - OP_RETURN (0x6a) |
| 282 | + # 1-byte - Push the following 36 bytes (0x24) |
| 283 | + # 4-byte - Commitment header (0xaa21a9ed) |
| 284 | + # 32-byte - Commitment hash: Double-SHA256(witness root hash|witness reserved value) |
| 285 | + commitment = "6a24"+ "aa21a9ed" + witness_commitment_hash |
| 286 | + |
| 287 | + print("Witness Commitment :", commitment) |
| 288 | + # note: to_addr defined at the top. Taken from the first transaction |
| 289 | + # coinbase transactions must contain at least two outputs |
| 290 | + # the first output is the block reward and the second output is the commitment |
| 291 | + # The block reward is reward for the miner for mining the block + fees |
| 292 | + # block reward = block subsidy (currently 3.125 BTC) + transaction fees |
| 293 | + # Learn more: https://learnmeabitcoin.com/technical/mining/block-reward/ |
| 294 | + txout1 = TxOutput(to_satoshis(3.125+0.01), to_addr.to_script_pub_key()) |
| 295 | + |
| 296 | + # The commitment script is described above |
| 297 | + # and is added to the second output of the coinbase transaction |
| 298 | + witness_commitment_script = Script(["OP_RETURN", "aa21a9ed"+witness_commitment_hash]); |
| 299 | + print("witness commitment script : ", witness_commitment_script) |
| 300 | + txout2 = TxOutput(to_satoshis(0), witness_commitment_script) |
| 301 | + coinbase_tx = Transaction( |
| 302 | + [txinp], |
| 303 | + [txout1, txout2], |
| 304 | + has_segwit=True, |
| 305 | + witnesses=[TxWitnessInput(witness_stack)], |
| 306 | + ) |
| 307 | + # Example difficulty target |
| 308 | + # learn more: https://learnmeabitcoin.com/technical/mining/target/ |
| 309 | + # Note, this is even higher then the genesis block example |
| 310 | + # to prevent the mining process from running for a long time |
| 311 | + difficulty_target = ( |
| 312 | + "0000ffff00000000000000000000000000000000000000000000000000000000" |
| 313 | + ) |
| 314 | + # Creating a block that includes the coinbase transaction and tx1. |
| 315 | + block = create_block(coinbase_tx, tx1) |
| 316 | + |
| 317 | + print("block header: ", block.header) |
| 318 | + # getting the block header bytes |
| 319 | + # Note: only the header is hashed and compared to the difficulty in mining |
| 320 | + serialized_header = block.header.serialize_header() |
| 321 | + print("Serialized header : ", serialized_header) |
| 322 | + |
| 323 | + # mining the block using mine_block returns the nonce |
| 324 | + # nonce is the value in the header that can be changed iteratively |
| 325 | + # to get a hash that is less than the target |
| 326 | + nonce, mined_hash = mine_block(serialized_header, difficulty_target) |
| 327 | + print("NONCE: ", nonce) |
| 328 | + |
| 329 | + # Replace the last 4 bytes of the header with the current nonce in little-endian format. |
| 330 | + serialized_header = serialized_header[:-4] + nonce.to_bytes(4, byteorder="little") |
| 331 | + |
| 332 | + print("hex of serialized header ", serialized_header.hex()) |
| 333 | + print("coinbase transaction :", coinbase_tx.to_hex()) |
| 334 | + print("Block: ", block) |
| 335 | + |
| 336 | + |
| 337 | +if __name__ == "__main__": |
| 338 | + main() |
0 commit comments