def parse(cls: Type["Sig"], data: BinaryData, check_validity: bool = True) -> "Sig": """Return a Sig by parsing binary data. Deserialize a strict ASN.1 DER representation of an ECDSA signature. """ stream = bytesio_from_binarydata(data) ec = secp256k1 # [0x30] [data-size][0x02][r-size][r][0x02][s-size][s] marker = stream.read(1) if marker != _DER_SIG_MARKER: err_msg = f"invalid compound header: {marker.hex()}" err_msg += f", instead of DER sequence tag {_DER_SIG_MARKER.hex()}" raise BTClibValueError(err_msg) # [data-size][0x02][r-size][r][0x02][s-size][s] sig_data = var_bytes.parse(stream, forbid_zero_size=True) # [0x02][r-size][r][0x02][s-size][s] sig_data_substream = bytesio_from_binarydata(sig_data) r = _deserialize_scalar(sig_data_substream) s = _deserialize_scalar(sig_data_substream) # to prevent malleability # the sig_data_substream must have been consumed entirely if sig_data_substream.read(1) != b"": err_msg = "invalid DER sequence length" raise BTClibValueError(err_msg) return cls(r, s, ec, check_validity)
def parse(cls: Type[_Sig], data: BinaryData, check_validity: bool = True) -> _Sig: stream = bytesio_from_binarydata(data) ec = secp256k1 r = int.from_bytes(stream.read(ec.p_size), byteorder="big", signed=False) s = int.from_bytes(stream.read(ec.n_size), byteorder="big", signed=False) return cls(r, s, ec, check_validity)
def deserialize(cls, data): stream = bytesio_from_binarydata(data) len_addresses = varint.decode(stream) addresses = [] for x in range(len_addresses): addresses.append(NetworkAddress.deserialize(stream)) return cls(addresses=addresses)
def parse(cls: Type[_Tx], data: BinaryData, check_validity: bool = True) -> _Tx: "Return a Tx by parsing binary data." stream = bytesio_from_binarydata(data) # version is a signed int (int32_t, not uint32_t) version = int.from_bytes(stream.read(4), byteorder="little", signed=True) segwit = stream.read(2) == _SEGWIT_MARKER if not segwit: # Change stream position: seek to byte offset relative to position stream.seek(-2, SEEK_CUR) # current position n = var_int.parse(stream) vin = [TxIn.parse(stream) for _ in range(n)] n = var_int.parse(stream) vout = [TxOut.parse(stream) for _ in range(n)] if segwit: for tx_in in vin: tx_in.script_witness = Witness.parse(stream, check_validity) lock_time = int.from_bytes(stream.read(4), byteorder="little", signed=False) return cls(version, lock_time, vin, vout, check_validity)
def deserialize(cls, data): stream = bytesio_from_binarydata(data) header = BlockHeader.deserialize(stream) index = varint.decode(stream) status = BlockStatus.from_bytes(stream.read(1), "little") downloaded = bool(int.from_bytes(stream.read(1), "little")) return cls(header, index, status, downloaded)
def deserialize(cls, data): stream = bytesio_from_binarydata(data) message = stream.read(varint.decode(stream)).decode() code = RejectCode.from_bytes(stream.read(1), "little") reason = stream.read(varint.decode(stream)).decode() data = stream.read(32).hex() return cls(message, code, reason, data)
def parse(cls: Type["BlockHeader"], data: BinaryData, check_validity: bool = True) -> "BlockHeader": "Return a BlockHeader by parsing 80 bytes from binary data." stream = bytesio_from_binarydata(data) # version is a signed int (int32_t, not uint32_t) version = int.from_bytes(stream.read(4), byteorder="little", signed=True) previous_block_hash = stream.read(_HF_LEN)[::-1] merkle_root_ = stream.read(_HF_LEN)[::-1] t = int.from_bytes(stream.read(4), byteorder="little", signed=False) time = datetime.fromtimestamp(t, timezone.utc) bits = stream.read(4)[::-1] nonce = int.from_bytes(stream.read(4), byteorder="little", signed=False) return cls( version, previous_block_hash, merkle_root_, time, bits, nonce, check_validity, )
def deserialize(cls, data): stream = bytesio_from_binarydata(data) blockhash = stream.read(32)[::-1].hex() num_indexes = varint.decode(stream) indexes = [] for x in range(num_indexes): indexes.append(varint.decode(stream)) return cls(blockhash=blockhash, indexes=indexes)
def deserialize(cls, data): stream = bytesio_from_binarydata(data) blockhash = stream.read(32)[::-1].hex() num_transactions = varint.decode(stream) transactions = [] for x in range(num_transactions): transactions.append(TxData.deserialize(stream)) return cls(blockhash=blockhash, transactions=transactions)
def deserialize(cls, data): stream = bytesio_from_binarydata(data) headers_num = varint.decode(stream) headers = [] for x in range(headers_num): header = BlockHeader.deserialize(stream) stream.read(1) headers.append(header) return cls(headers)
def deserialize(cls, data): stream = bytesio_from_binarydata(data) inventory_length = varint.decode(stream) inventory = [] for x in range(inventory_length): item_type = int.from_bytes(stream.read(4), "little") item_hash = stream.read(32)[::-1].hex() inventory.append((item_type, item_hash)) return cls(inventory)
def parse(cls: Type["TxOut"], data: BinaryData, check_validity: bool = True) -> "TxOut": stream = bytesio_from_binarydata(data) value = int.from_bytes(stream.read(8), byteorder="little", signed=False) script = var_bytes.parse(stream) return cls(value, ScriptPubKey(script, "mainnet"), check_validity)
def parse(cls: Type["Witness"], data: BinaryData, check_validity: bool = True) -> "Witness": "Return a Witness by parsing binary data." data = bytesio_from_binarydata(data) n = var_int.parse(data) stack = [var_bytes.parse(data) for _ in range(n)] return cls(stack, check_validity)
def parse(cls: Type["OutPoint"], data: BinaryData, check_validity: bool = True) -> "OutPoint": "Return an OutPoint from the first 36 bytes of the provided data." data = bytesio_from_binarydata(data) tx_id = data.read(32)[::-1] vout = int.from_bytes(data.read(4), "little", signed=False) return cls(tx_id, vout, check_validity)
def deserialize(cls, data): stream = bytesio_from_binarydata(data) version = int.from_bytes(stream.read(4), "little") block_hashes = [] for x in range(varint.decode(stream)): block_hash = stream.read(32)[::-1].hex() block_hashes.append(block_hash) hash_stop = stream.read(32)[::-1].hex() return cls(version=version, block_hashes=block_hashes, hash_stop=hash_stop)
def deserialize(cls, data, version_msg=False): stream = bytesio_from_binarydata(data) if not version_msg: time = int.from_bytes(stream.read(4), "little") else: time = 0 services = int.from_bytes(stream.read(8), "little") a = stream.read(16) ip = IPv6Address(a) port = int.from_bytes(stream.read(2), "big") return cls(time=time, services=services, ip=ip, port=port)
def parse(cls: Type[_Block], data: BinaryData, check_validity: bool = True) -> _Block: "Return a Block by parsing binary data." stream = bytesio_from_binarydata(data) header = BlockHeader.parse(stream) n = var_int.parse(stream) # TODO: is a block required to have a coinbase tx? transactions = [Tx.parse(stream) for _ in range(n)] return cls(header, transactions, check_validity)
def parse(cls: Type["TxIn"], data: BinaryData, check_validity: bool = True) -> "TxIn": stream = bytesio_from_binarydata(data) prev_out = OutPoint.parse(stream) script_sig = var_bytes.parse(stream) sequence = int.from_bytes(stream.read(4), byteorder="little", signed=False) return cls(prev_out, script_sig, sequence, Witness(), check_validity)
def parse(cls: Type["Psbt"], psbt_bin: Octets, check_validity: bool = True) -> "Psbt": "Return a Psbt by parsing binary data." # FIXME: psbt_bin should be BinaryData # stream = bytesio_from_binarydata(psbt_bin) # and the deserialization should happen reading the stream # not slicing bytes tx = Tx(check_validity=False) version = 0 hd_key_paths: Dict[Octets, BIP32KeyOrigin] = {} unknown: Dict[Octets, Octets] = {} # psbt_bin = bytes_from_octets(psbt_bin) stream = bytesio_from_binarydata(psbt_bin) if stream.read(4) != PSBT_MAGIC_BYTES: raise BTClibValueError("malformed psbt: missing magic bytes") if stream.read(1) != PSBT_SEPARATOR: raise BTClibValueError("malformed psbt: missing separator") global_map, stream = deserialize_map(stream) for k, v in global_map.items(): if k[:1] == PSBT_GLOBAL_UNSIGNED_TX: tx = deserialize_tx(k, v, "global unsigned tx", False) elif k[:1] == PSBT_GLOBAL_VERSION: version = deserialize_int(k, v, "global version") elif k[:1] == PSBT_GLOBAL_XPUB: hd_key_paths[k[1:]] = BIP32KeyOrigin.parse(v) else: # unknown unknown[k] = v inputs: List[PsbtIn] = [] for _ in tx.vin: input_map, stream = deserialize_map(stream) inputs.append(PsbtIn.parse(input_map)) outputs: List[PsbtOut] = [] for _ in tx.vout: output_map, stream = deserialize_map(stream) outputs.append(PsbtOut.parse(output_map)) return cls( tx, inputs, outputs, version, hd_key_paths, unknown, check_validity, )
def parse(stream: BinaryData, forbid_zero_size: bool = False) -> bytes: """Return the variable-length octets read from a stream.""" stream = bytesio_from_binarydata(stream) i = var_int.parse(stream) if forbid_zero_size and i == 0: raise BTClibRuntimeError("zero size") result = stream.read(i) if len(result) != i: raise BTClibRuntimeError("not enough binary data") return result
def deserialize(cls, data): stream = bytesio_from_binarydata(data) hash = stream.read(32).hex() to_add = [] for x in range(varint.decode(stream)): out_point = OutPoint.deserialize(stream) tx_out = TxOut.deserialize(stream) to_add.append([out_point, tx_out]) to_remove = [] for x in range(varint.decode(stream)): out_point = OutPoint.deserialize(stream) to_remove.append(out_point) return cls(hash, to_add, to_remove)
def parse(stream: BinaryData, exit_on_op_success: bool = False) -> List[Command]: s = bytesio_from_binarydata(stream) r: List[Command] = [] # initialize the result list while True: t = s.read(1) # get one byte if not t: break i = t[0] # convert the first byte to an integer if 0 < i <= 78: # push data_length = i # 0 < i < 76 -> 1-byte-data-length | data if 75 < i < 79: # i == 76 -> OP_PUSHDATA1 | 1-byte-data-length | data # i == 77 -> OP_PUSHDATA2 | 2-byte-data-length | data x = i - 75 if i == 78: # OP_PUSHDATA4 | 4-byte-data-length | data x = 4 y = s.read(x) if len(y) != x: raise BTClibValueError( "Not enough data for pushdata length") data_length = int.from_bytes(y, byteorder="little") if data_length > 520: raise BTClibValueError( f"Invalid pushdata length: {data_length}") data = s.read(data_length) if len(data) != data_length: raise BTClibValueError("Not enough data for pushdata") command = data.hex().upper() elif i in OP_CODE_NAME_FROM_INT: # OP_CODE command = OP_CODE_NAME_FROM_INT[i] # Opcodes which take integers and bools off the stack require # that they be no more than 4 bytes long. # If this is the case, parse that command as int # t = r[-1] # if isinstance(t, bytes) and len(t) <= 4: # r[-1] = decode_num(t) else: # OP_SUCCESSx command = f"OP_SUCCESS{i}" if exit_on_op_success: return ["OP_SUCCESS"] r.append(command) return r
def deserialize_map(data: BinaryData) -> Tuple[Dict[bytes, bytes], BytesIO]: stream = bytesio_from_binarydata(data) if ( len(stream.getbuffer()) == stream.tell() ): # we are at the end of the stream buffer raise BTClibValueError("malformed psbt: at least a map is missing") partial_map: Dict[bytes, bytes] = {} while True: if stream.read(1)[0] == 0: return partial_map, stream stream.seek(-1, 1) # reset stream position key = stream.read(var_int.parse(stream)) value = stream.read(var_int.parse(stream)) if key in partial_map: raise BTClibValueError(f"duplicated key in psbt map: 0x{key.hex()}") partial_map[key] = value
def parse( cls: Type["TxOut"], data: BinaryData, check_validity: bool = True, ) -> "TxOut": stream = bytesio_from_binarydata(data) value = int.from_bytes(stream.read(8), byteorder="little", signed=False) script = var_bytes.parse(stream) return cls( value, ScriptPubKey(script, "mainnet", check_validity=False ), # https://github.com/bitcoin/bitcoin/issues/320 check_validity, )
def parse(stream: BinaryData, exit_on_op_success: bool = False) -> List[Command]: s = bytesio_from_binarydata(stream) r: List[Command] = [] # initialize the result list while True: t = s.read(1) # get one byte if not t: break i = t[0] # convert the byte to an integer if 0 < i <= 78: # push if 0 < i < 76: # 1-byte-data-length | data data_length = i if 76 <= i <= 78: if i == 76: # OP_PUSHDATA1 | 1-byte-data-length | data x = 1 elif i == 77: # OP_PUSHDATA2 | 2-byte-data-length | data x = 2 elif i == 78: # OP_PUSHDATA4 | 4-byte-data-length | data x = 4 y = s.read(x) if len(y) != x: raise BTClibValueError("Invalid pushdata length") data_length = int.from_bytes(y, byteorder="little") if data_length > 520: raise BTClibValueError("Invalid pushdata length") data = s.read(data_length) if len(data) != data_length: raise BTClibValueError("Invalid pushdata length") # if <= 0xFFFFFFFF, parse it as integer if i < 6 and decode_num(data) <= 0xFFFFFFFF: new_op_code: Command = decode_num(data) else: new_op_code = data.hex().upper() elif i in OP_CODE_NAMES.keys(): # OP_CODE new_op_code = OP_CODE_NAMES[i] else: # OP_SUCCESSx new_op_code = f"OP_SUCCESS{i}" if exit_on_op_success: return ["OP_SUCCESS"] r.append(new_op_code) return r
def parse(stream: BinaryData) -> int: """Return the variable-length integer read from a stream.""" stream = bytesio_from_binarydata(stream) i = stream.read(1)[0] if i < 0xFD: # one byte integer return i if i == 0xFD: # 0xfd marks the next two bytes as the number return int.from_bytes(stream.read(2), byteorder="little", signed=False) if i == 0xFE: # 0xfe marks the next four bytes as the number return int.from_bytes(stream.read(4), byteorder="little", signed=False) # 0xff marks the next eight bytes as the number return int.from_bytes(stream.read(8), byteorder="little", signed=False)
def deserialize(cls, data): stream = bytesio_from_binarydata(data) header = BlockHeader.deserialize(stream) nonce = int.from_bytes(stream.read(8), "little") short_ids = [] short_ids_length = varint.decode(stream) for x in range(short_ids_length): short_ids.append(stream.read(6)[::-1].hex()) prefilled_tx_list = [] prefilled_tx_num = varint.decode(stream) for x in range(prefilled_tx_num): tx_index = varint.decode(stream) tx = Tx.deserialize(stream) prefilled_tx_list.append((tx_index, tx)) return cls( header=header, nonce=nonce, short_ids=short_ids, prefilled_tx_list=prefilled_tx_list, )
def parse( cls: Type["BIP32KeyData"], xkey_bin: BinaryData, check_validity: bool = True ) -> "BIP32KeyData": "Return a BIP32KeyData by parsing 73 bytes from binary data." stream = bytesio_from_binarydata(xkey_bin) xkey_bin = stream.read(_REQUIRED_LENGHT) if check_validity and len(xkey_bin) != _REQUIRED_LENGHT: err_msg = f"invalid decoded length: {len(xkey_bin)}" err_msg += f" instead of {_REQUIRED_LENGHT}" raise BTClibValueError(err_msg) return cls( version=xkey_bin[0:4], depth=xkey_bin[4], parent_fingerprint=xkey_bin[5:9], index=int.from_bytes(xkey_bin[9:13], byteorder="big", signed=False), chain_code=xkey_bin[13:45], key=xkey_bin[45:78], check_validity=check_validity, )
def parse(cls: Type["Sig"], data: BinaryData, check_validity: bool = True) -> "Sig": stream = bytesio_from_binarydata(data) sig_bin = stream.read(_REQUIRED_LENGHT) if check_validity and len(sig_bin) != _REQUIRED_LENGHT: err_msg = f"invalid decoded length: {len(sig_bin)}" err_msg += f" instead of {_REQUIRED_LENGHT}" raise BTClibValueError(err_msg) rf = sig_bin[0] ec = secp256k1 n_size = ec.n_size r = int.from_bytes(sig_bin[1:1 + n_size], "big", signed=False) s = int.from_bytes(sig_bin[1 + n_size:1 + 2 * n_size], "big", signed=False) dsa_sig = dsa.Sig(r, s, ec, check_validity=False) return cls(rf, dsa_sig, check_validity)
def parse(stream: BinaryData) -> List[Command]: s = bytesio_from_binarydata(stream) # initialize the result list r: List[Command] = [] while True: # get one byte t = s.read(1) if not t: break # convert the byte to an integer i = t[0] if 0 < i < 76: # 1-byte-data-length | data data = s.read(i) # if <= 0xFFFFFFFF, parse it as integer as_int = decode_num(data) r.append(as_int if i < 6 and as_int <= 0xFFFFFFFF else data.hex().upper()) elif i == 76: # OP_PUSHDATA1 | 1-byte-data-length | data data_length = int.from_bytes(s.read(1), byteorder="little", signed=False) data = s.read(data_length) r.append(data.hex().upper()) elif i == 77: # OP_PUSHDATA2 | 2-byte-data-length | data data_length = int.from_bytes(s.read(2), byteorder="little", signed=False) data = s.read(data_length) r.append(data.hex().upper()) elif i == 78: # OP_PUSHDATA4 | 4-byte-data-length | data data_length = int.from_bytes(s.read(4), byteorder="little", signed=False) data = s.read(data_length) r.append(data.hex().upper()) else: # OP_CODE r.append(OP_CODE_NAMES[i]) return r