diff --git a/examples/example_decode_block.py b/examples/example_decode_block.py index d172f05..8d24871 100644 --- a/examples/example_decode_block.py +++ b/examples/example_decode_block.py @@ -11,6 +11,7 @@ """ import asyncio +import io import random from pactus.block import Block @@ -18,6 +19,7 @@ TESTNET_RPC = "https://testnet1.pactus.org/jsonrpc" + async def fetch_and_decode_block(): client = PactusOpenRPCClient( headers={}, @@ -34,7 +36,7 @@ async def fetch_and_decode_block(): print(f"\nFetching block at height {height}...") res = await client.pactus.blockchain.get_block(height=height, verbosity=0) - block = Block.decode(bytes.fromhex(res["data"])) + block = Block.decode(io.BytesIO(bytes.fromhex(res["data"]))) print(f" - Hash: {block.id}") print(f" - Version: {block.header.version}") @@ -48,10 +50,10 @@ async def fetch_and_decode_block(): print(f" - Absentees: {block.prev_cert.absentees}") print(f" - Signature: {block.prev_cert.signature.string()}") - print(" - Transactions:") for i, tx in enumerate(block.transactions): print(f" - {i}: {tx.id()}") + if __name__ == "__main__": asyncio.run(fetch_and_decode_block()) diff --git a/pactus/block/block.py b/pactus/block/block.py index a0c0006..dab5b5d 100644 --- a/pactus/block/block.py +++ b/pactus/block/block.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +import io import struct from pactus.crypto.hash import Hash @@ -23,22 +24,22 @@ def __init__( self.transactions = transactions @classmethod - def decode(cls, data: bytes) -> Block: - """Decode a Block from raw bytes.""" - header, data = Header.decode(data) + def decode(cls, buf: io.BufferedReader) -> Block: + """Decode a Block from the stream.""" + header = Header.decode(buf) # Genesis block has no certificate is_genesis = header.prev_block_hash.is_undef() prev_cert = None if not is_genesis: - prev_cert, data = Certificate.decode(data) + prev_cert = Certificate.decode(buf) - num_txs, data = encoding.read_var_int(data) + num_txs = encoding.read_var_int(buf) transactions = [] for _ in range(num_txs): - tx, data = Transaction.decode(data) + tx = Transaction.decode(buf) transactions.append(tx) return cls(header, prev_cert, transactions) @@ -54,7 +55,9 @@ def id(self) -> Hash: return Hash(hashlib.blake2b(buf, digest_size=32).digest()) def _header_bytes(self) -> bytes: - return self.header.encode(b"") + buf = io.BytesIO() + self.header.encode(buf) + return buf.getvalue() def _txs_root(self) -> bytes: return Block._merkle_root([tx.id().data for tx in self.transactions]) diff --git a/pactus/block/certificate.py b/pactus/block/certificate.py index f5bab3e..5ed669d 100644 --- a/pactus/block/certificate.py +++ b/pactus/block/certificate.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import hashlib +import io from pactus.crypto.bls.signature import Signature from pactus.encoding import encoding @@ -22,42 +25,40 @@ def __init__( self.signature = signature @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Certificate from bytes. - Returns (Certificate, remaining_buf). - """ - height, buf = Height.decode(buf) - round_, buf = Round.decode(buf) - - num_committers, buf = encoding.read_var_int(buf) + def decode(cls, buf: io.BufferedReader) -> Certificate: + """Decode a Certificate from the stream.""" + height = Height.decode(buf) + round_ = Round.decode(buf) + + num_committers = encoding.read_var_int(buf) committers = [] for _ in range(num_committers): - n, buf = encoding.read_var_int(buf) + n = encoding.read_var_int(buf) committers.append(n) - num_absentees, buf = encoding.read_var_int(buf) + num_absentees = encoding.read_var_int(buf) absentees = [] for _ in range(num_absentees): - n, buf = encoding.read_var_int(buf) + n = encoding.read_var_int(buf) absentees.append(n) - signature, buf = Signature.decode(buf) + signature = Signature.decode(buf) - cert = cls(height, round_, committers, absentees, signature) - return cert, buf + return cls(height, round_, committers, absentees, signature) - def encode(self, buf: bytes) -> bytes: - buf = self.height.encode(buf) - buf = self.round.encode(buf) - buf = encoding.append_var_int(buf, len(self.committers)) + def encode(self, buf: io.Writer) -> None: + self.height.encode(buf) + self.round.encode(buf) + encoding.append_var_int(buf, len(self.committers)) for n in self.committers: - buf = encoding.append_var_int(buf, n) - buf = encoding.append_var_int(buf, len(self.absentees)) + encoding.append_var_int(buf, n) + encoding.append_var_int(buf, len(self.absentees)) for n in self.absentees: - buf = encoding.append_var_int(buf, n) - return self.signature.encode(buf) + encoding.append_var_int(buf, n) + self.signature.encode(buf) def hash(self) -> bytes: """Return the certificate hash (blake2b-256 of encoded bytes).""" - return hashlib.blake2b(self.encode(b""), digest_size=32).digest() + buf = io.BytesIO() + self.encode(buf) + return hashlib.blake2b(buf.getvalue(), digest_size=32).digest() diff --git a/pactus/block/header.py b/pactus/block/header.py index 92e0cf9..57d3426 100644 --- a/pactus/block/header.py +++ b/pactus/block/header.py @@ -1,3 +1,10 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io + from pactus.crypto.address import Address from pactus.crypto.hash import Hash from pactus.encoding import encoding @@ -21,19 +28,16 @@ def __init__( self.proposer_address = proposer_address @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Header from bytes. - Returns (Header, remaining_buf). - """ - version, buf = encoding.read_uint8(buf) - unix_time, buf = encoding.read_uint32(buf) - prev_block_hash, buf = Hash.decode(buf) - state_root, buf = Hash.decode(buf) - sortition_seed, buf = encoding.read_fixed_bytes(buf, 48) - proposer_address, buf = Address.decode(buf) - - header = cls( + def decode(cls, buf: io.BufferedReader) -> Header: + """Decode a Header from the stream.""" + version = encoding.read_uint8(buf) + unix_time = encoding.read_uint32(buf) + prev_block_hash = Hash.decode(buf) + state_root = Hash.decode(buf) + sortition_seed = encoding.read_fixed_bytes(buf, 48) + proposer_address = Address.decode(buf) + + return cls( version, unix_time, prev_block_hash, @@ -41,12 +45,11 @@ def decode(cls, buf: bytes) -> tuple: sortition_seed, proposer_address, ) - return header, buf - - def encode(self, buf: bytes) -> bytes: - buf = encoding.append_uint8(buf, self.version) - buf = encoding.append_uint32(buf, self.unix_time) - buf = self.prev_block_hash.encode(buf) - buf = self.state_root.encode(buf) - buf = encoding.append_fixed_bytes(buf, self.sortition_seed) - return self.proposer_address.encode(buf) + + def encode(self, buf: io.Writer) -> None: + encoding.append_uint8(buf, self.version) + encoding.append_uint32(buf, self.unix_time) + self.prev_block_hash.encode(buf) + self.state_root.encode(buf) + encoding.append_fixed_bytes(buf, self.sortition_seed) + self.proposer_address.encode(buf) diff --git a/pactus/crypto/address.py b/pactus/crypto/address.py index 4738c8e..51a7537 100644 --- a/pactus/crypto/address.py +++ b/pactus/crypto/address.py @@ -1,11 +1,15 @@ from __future__ import annotations from enum import Enum +from typing import TYPE_CHECKING from pactus.crypto.hrp import HRP from pactus.encoding import encoding from pactus.utils import utils +if TYPE_CHECKING: + import io + # Address format: hrp + `1` + type + data + checksum ADDRESS_SIZE = 21 @@ -86,20 +90,18 @@ def is_account_address(self) -> bool: def is_validator_address(self) -> bool: return self.address_type() == AddressType.VALIDATOR - def encode(self, buf: bytes) -> bytes: + def encode(self, buf: io.Writer) -> None: if self.is_treasury_address(): - return encoding.append_uint8(buf, AddressType.TREASURY.value) - return encoding.append_fixed_bytes(buf, self.raw_bytes()) + encoding.append_uint8(buf, AddressType.TREASURY.value) + else: + encoding.append_fixed_bytes(buf, self.raw_bytes()) @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode an Address from bytes. - Returns (Address, remaining_buf). - """ - addr_type, buf = encoding.read_uint8(buf) + def decode(cls, buf: io.BufferedReader) -> Address: + """Decode an Address from the stream.""" + addr_type = encoding.read_uint8(buf) if addr_type == AddressType.TREASURY.value: - return cls(AddressType.TREASURY, bytes(20)), buf + return cls(AddressType.TREASURY, bytes(20)) - data, buf = encoding.read_fixed_bytes(buf, 20) - return cls(AddressType(addr_type), data), buf + data = encoding.read_fixed_bytes(buf, 20) + return cls(AddressType(addr_type), data) diff --git a/pactus/crypto/bls/public_key.py b/pactus/crypto/bls/public_key.py index 16e03b7..22d055f 100644 --- a/pactus/crypto/bls/public_key.py +++ b/pactus/crypto/bls/public_key.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +from typing import TYPE_CHECKING from ripemd.ripemd160 import ripemd160 @@ -13,6 +14,9 @@ from .bls12_381.serdesZ import deserialize, serialize from .signature import DST, SIGNATURE_TYPE_BLS, Signature +if TYPE_CHECKING: + import io + PUBLIC_KEY_SIZE = 96 @@ -61,13 +65,13 @@ def string(self) -> str: self.raw_bytes(), ) - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.raw_bytes()) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.raw_bytes()) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) - return cls.from_bytes(data), buf + def decode(cls, buf: io.BufferedReader) -> PublicKey: + data = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) + return cls.from_bytes(data) def account_address(self) -> Address: return self._make_address(AddressType.BLS_ACCOUNT) diff --git a/pactus/crypto/bls/signature.py b/pactus/crypto/bls/signature.py index 3441ef9..f2ad220 100644 --- a/pactus/crypto/bls/signature.py +++ b/pactus/crypto/bls/signature.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pactus.encoding import encoding from .bls12_381.bls_sig_g1 import aggregate_sigs from .bls12_381.serdesZ import deserialize, serialize +if TYPE_CHECKING: + import io + SIGNATURE_SIZE = 48 SIGNATURE_TYPE_BLS = 1 DST = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" @@ -39,11 +44,11 @@ def raw_bytes(self) -> bytes: def string(self) -> str: return self.raw_bytes().hex() - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.raw_bytes()) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.raw_bytes()) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) + def decode(cls, buf: io.BufferedReader) -> Signature: + data = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) point_g1 = deserialize(data, is_ell2=False) - return cls(point_g1), buf + return cls(point_g1) diff --git a/pactus/crypto/ed25519/public_key.py b/pactus/crypto/ed25519/public_key.py index 7af7055..f1238da 100644 --- a/pactus/crypto/ed25519/public_key.py +++ b/pactus/crypto/ed25519/public_key.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +from typing import TYPE_CHECKING from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric import ed25519 @@ -13,6 +14,9 @@ from .signature import SIGNATURE_TYPE_ED25519, Signature +if TYPE_CHECKING: + import io + PUBLIC_KEY_SIZE = 32 @@ -50,13 +54,13 @@ def string(self) -> str: self.raw_bytes(), ) - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.raw_bytes()) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.raw_bytes()) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) - return cls(ed25519.Ed25519PublicKey.from_public_bytes(data)), buf + def decode(cls, buf: io.BufferedReader) -> PublicKey: + data = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) + return cls(ed25519.Ed25519PublicKey.from_public_bytes(data)) def account_address(self) -> Address: blake2b = hashlib.blake2b(digest_size=32) diff --git a/pactus/crypto/ed25519/signature.py b/pactus/crypto/ed25519/signature.py index 68f7e3c..e828e0e 100644 --- a/pactus/crypto/ed25519/signature.py +++ b/pactus/crypto/ed25519/signature.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pactus.encoding import encoding +if TYPE_CHECKING: + import io + SIGNATURE_SIZE = 64 SIGNATURE_TYPE_ED25519 = 3 @@ -26,10 +31,10 @@ def raw_bytes(self) -> bytes: def string(self) -> str: return self.sig.hex() - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.sig) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.sig) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) - return cls(data), buf + def decode(cls, buf: io.BufferedReader) -> Signature: + data = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) + return cls(data) diff --git a/pactus/crypto/hash.py b/pactus/crypto/hash.py index f7e5451..5369c3a 100644 --- a/pactus/crypto/hash.py +++ b/pactus/crypto/hash.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pactus.encoding import encoding +if TYPE_CHECKING: + import io + HASH_SIZE = 32 @@ -28,14 +33,11 @@ def __str__(self) -> str: def is_undef(self) -> bool: return self.data == bytes(HASH_SIZE) - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.data) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.data) @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Hash from bytes. - Returns (Hash, remaining_buf). - """ - data, buf = encoding.read_fixed_bytes(buf, HASH_SIZE) - return cls(data), buf + def decode(cls, buf: io.BufferedReader) -> Hash: + """Decode a Hash from the stream.""" + data = encoding.read_fixed_bytes(buf, HASH_SIZE) + return cls(data) diff --git a/pactus/crypto/public_key.py b/pactus/crypto/public_key.py index f50cf33..40a7851 100644 --- a/pactus/crypto/public_key.py +++ b/pactus/crypto/public_key.py @@ -4,6 +4,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: + import io + from .signature import Signature @@ -22,12 +24,12 @@ def string(self) -> str: pass @abstractmethod - def encode(self, buf: bytes) -> bytes: + def encode(self, buf: io.Writer) -> None: pass @classmethod @abstractmethod - def decode(cls, buf: bytes) -> tuple: + def decode(cls, buf: io.BufferedReader) -> PublicKey: pass @abstractmethod diff --git a/pactus/crypto/secp256k1/public_key.py b/pactus/crypto/secp256k1/public_key.py index 95cfafc..3ea36d3 100644 --- a/pactus/crypto/secp256k1/public_key.py +++ b/pactus/crypto/secp256k1/public_key.py @@ -2,6 +2,7 @@ import hashlib from functools import partial +from typing import TYPE_CHECKING import secp256k1 from ripemd.ripemd160 import ripemd160 @@ -13,6 +14,9 @@ from .signature import SIGNATURE_TYPE_SECP256K1, Signature +if TYPE_CHECKING: + import io + PUBLIC_KEY_SIZE = 33 # Compressed public key @@ -50,13 +54,13 @@ def string(self) -> str: self.raw_bytes(), ) - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.raw_bytes()) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.raw_bytes()) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) - return cls(secp256k1.PublicKey(data, raw=True)), buf + def decode(cls, buf: io.BufferedReader) -> PublicKey: + data = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) + return cls(secp256k1.PublicKey(data, raw=True)) def account_address(self) -> Address: blake2b = hashlib.blake2b(digest_size=32) diff --git a/pactus/crypto/secp256k1/signature.py b/pactus/crypto/secp256k1/signature.py index 6b18c5e..dc5ec95 100644 --- a/pactus/crypto/secp256k1/signature.py +++ b/pactus/crypto/secp256k1/signature.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pactus.encoding import encoding +if TYPE_CHECKING: + import io + SIGNATURE_SIZE = 64 SIGNATURE_TYPE_SECP256K1 = 4 @@ -26,10 +31,10 @@ def raw_bytes(self) -> bytes: def string(self) -> str: return self.sig.hex() - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.sig) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.sig) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) - return cls(data), buf + def decode(cls, buf: io.BufferedReader) -> Signature: + data = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) + return cls(data) diff --git a/pactus/crypto/signature.py b/pactus/crypto/signature.py index ac50935..f2d0ec7 100644 --- a/pactus/crypto/signature.py +++ b/pactus/crypto/signature.py @@ -1,6 +1,10 @@ from __future__ import annotations from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io class Signature(ABC): @@ -18,10 +22,10 @@ def string(self) -> str: pass @abstractmethod - def encode(self, buf: bytes) -> bytes: + def encode(self, buf: io.Writer) -> None: pass @classmethod @abstractmethod - def decode(cls, buf: bytes) -> tuple: + def decode(cls, buf: io.BufferedReader) -> Signature: pass diff --git a/pactus/encoding/encoding.py b/pactus/encoding/encoding.py index 91c0a92..78f23ed 100644 --- a/pactus/encoding/encoding.py +++ b/pactus/encoding/encoding.py @@ -1,93 +1,82 @@ from __future__ import annotations +from typing import TYPE_CHECKING -def append_uint8(buf: bytes, val: int) -> bytes: - buf += val.to_bytes(1, "little") - return buf +if TYPE_CHECKING: + import io -def append_uint16(buf: bytes, val: int) -> bytes: - buf += val.to_bytes(2, "little") - return buf +def append_uint8(buf: io.Writer, val: int) -> None: + buf.write(val.to_bytes(1, "little")) -def append_uint32(buf: bytes, val: int) -> bytes: - buf += val.to_bytes(4, "little") - return buf +def append_uint16(buf: io.Writer, val: int) -> None: + buf.write(val.to_bytes(2, "little")) -def append_str(buf: bytes, val: str) -> bytes: - buf = append_var_int(buf, len(val)) - return append_fixed_bytes(buf, bytes(val, "utf-8")) +def append_uint32(buf: io.Writer, val: int) -> None: + buf.write(val.to_bytes(4, "little")) -def append_var_int(buf: bytes, val: int) -> bytes: +def append_str(buf: io.Writer, val: str) -> None: + append_var_int(buf, len(val)) + append_fixed_bytes(buf, bytes(val, "utf-8")) + + +def append_var_int(buf: io.Writer, val: int) -> None: while val >= 0x80: n = (val & 0x7F) | 0x80 - buf += bytes([n]) + buf.write(bytes([n])) val >>= 7 - buf += bytes([val]) - return buf + buf.write(bytes([val])) -def append_fixed_bytes(buf: bytes, data: bytes) -> bytes: - buf += data - return buf +def append_fixed_bytes(buf: io.Writer, data: bytes) -> None: + buf.write(data) -def read_uint8(buf: bytes) -> tuple[int, bytes]: - val = int.from_bytes(buf[0:1], "little", signed=False) - return val, buf[1:] +def _read(buf: io.BufferedReader, size: int) -> bytes: + data = buf.read(size) + if len(data) != size: + msg = "unexpected end of data while reading" + raise ValueError(msg) + + return data -def read_uint16(buf: bytes) -> tuple[int, bytes]: - val = int.from_bytes(buf[0:2], "little", signed=False) - return val, buf[2:] +def read_uint8(buf: io.BufferedReader) -> int: + return int.from_bytes(_read(buf, 1), "little", signed=False) -def read_uint32(buf: bytes) -> tuple[int, bytes]: - val = int.from_bytes(buf[0:4], "little", signed=False) - return val, buf[4:] +def read_uint16(buf: io.BufferedReader) -> int: + return int.from_bytes(_read(buf, 2), "little", signed=False) -def read_var_int(buf: bytes) -> tuple[int, bytes]: - """ - Read a variable-length integer from bytes at given offset. - Returns (value, new_offset). - """ +def read_uint32(buf: io.BufferedReader) -> int: + return int.from_bytes(_read(buf, 4), "little", signed=False) + + +def read_var_int(buf: io.BytesIO) -> int: + """Read a variable-length integer from the stream.""" result = 0 shift = 0 - offset = 0 while True: - if offset >= len(buf): - msg = "unexpected end of data while reading varint" - raise ValueError(msg) - byte = buf[offset] - offset += 1 + byte = read_uint8(buf) result |= (byte & 0x7F) << shift if (byte & 0x80) == 0: break shift += 7 - return result, buf[offset:] + return result -def read_fixed_bytes(buf: bytes, size: int) -> tuple[bytes, bytes]: - """ - Read a fixed number of bytes from data at given offset. - Returns (bytes_read, new_offset). - """ - if size > len(buf): - msg = "unexpected end of data while reading fixed bytes" - raise ValueError(msg) - return buf[0:size], buf[size:] +def read_fixed_bytes(buf: io.BytesIO, size: int) -> bytes: + """Read a fixed number of bytes from the stream.""" + return _read(buf, size) -def read_str(buf: bytes) -> tuple[str, bytes]: - """ - Read a variable-length string (varint length + utf8 data). - Returns (string, new_offset). - """ - length, buf = read_var_int(buf) - raw, buf = read_fixed_bytes(buf, length) - return raw.decode("utf-8"), buf +def read_str(buf: io.BytesIO) -> str: + """Read a variable-length string (varint length + utf8 data) from the stream.""" + length = read_var_int(buf) + raw = read_fixed_bytes(buf, length) + return raw.decode("utf-8") diff --git a/pactus/transaction/payload/bond.py b/pactus/transaction/payload/bond.py index 67c4664..be44fed 100644 --- a/pactus/transaction/payload/bond.py +++ b/pactus/transaction/payload/bond.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pactus.crypto.address import Address from pactus.crypto.bls.public_key import PublicKey as BLSPublicKey from pactus.encoding import encoding @@ -7,6 +9,9 @@ from ._payload import PayloadType +if TYPE_CHECKING: + import io + class BondPayload: def __init__( @@ -21,15 +26,15 @@ def __init__( self.public_key = public_key self.stake = stake - def encode(self, buf: bytes) -> bytes: - buf = self.sender.encode(buf) - buf = self.receiver.encode(buf) + def encode(self, buf: io.Writer) -> None: + self.sender.encode(buf) + self.receiver.encode(buf) if self.public_key is not None: - buf = encoding.append_var_int(buf, 96) - buf = self.public_key.encode(buf) + encoding.append_var_int(buf, 96) + self.public_key.encode(buf) else: - buf = encoding.append_var_int(buf, 0) - return self.stake.encode(buf) + encoding.append_var_int(buf, 0) + self.stake.encode(buf) def get_type(self) -> PayloadType: return PayloadType.BOND @@ -38,17 +43,17 @@ def signer(self) -> Address: return self.sender @classmethod - def decode(cls, buf: bytes) -> tuple: - sender, buf = Address.decode(buf) - receiver, buf = Address.decode(buf) + def decode(cls, buf: io.BufferedReader) -> BondPayload: + sender = Address.decode(buf) + receiver = Address.decode(buf) - pub_key_size, buf = encoding.read_var_int(buf) + pub_key_size = encoding.read_var_int(buf) public_key = None if pub_key_size == 96: - public_key, buf = BLSPublicKey.decode(buf) + public_key = BLSPublicKey.decode(buf) elif pub_key_size != 0: msg = f"invalid public key size: {pub_key_size}" raise ValueError(msg) - stake, buf = Amount.decode(buf) - return cls(sender, receiver, public_key, stake), buf + stake = Amount.decode(buf) + return cls(sender, receiver, public_key, stake) diff --git a/pactus/transaction/payload/sortition.py b/pactus/transaction/payload/sortition.py index f8a007d..f22f131 100644 --- a/pactus/transaction/payload/sortition.py +++ b/pactus/transaction/payload/sortition.py @@ -1,3 +1,10 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io + from pactus.crypto.address import Address from pactus.encoding import encoding @@ -9,9 +16,9 @@ def __init__(self, address: Address, proof: bytes) -> None: self.address = address self.proof = proof - def encode(self, buf: bytes) -> bytes: - buf = self.address.encode(buf) - return encoding.append_fixed_bytes(buf, self.proof) + def encode(self, buf: io.Writer) -> None: + self.address.encode(buf) + encoding.append_fixed_bytes(buf, self.proof) def get_type(self) -> PayloadType: return PayloadType.SORTITION @@ -20,7 +27,7 @@ def signer(self) -> Address: return self.address @classmethod - def decode(cls, buf: bytes) -> tuple: - address, buf = Address.decode(buf) - proof, buf = encoding.read_fixed_bytes(buf, 48) - return cls(address, proof), buf + def decode(cls, buf: io.BufferedReader) -> SortitionPayload: + address = Address.decode(buf) + proof = encoding.read_fixed_bytes(buf, 48) + return cls(address, proof) diff --git a/pactus/transaction/payload/transfer.py b/pactus/transaction/payload/transfer.py index 44c06a8..2fda5ec 100644 --- a/pactus/transaction/payload/transfer.py +++ b/pactus/transaction/payload/transfer.py @@ -1,3 +1,10 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io + from pactus.crypto.address import Address from pactus.types.amount import Amount @@ -10,10 +17,10 @@ def __init__(self, sender: Address, receiver: Address, amount: Amount) -> None: self.receiver = receiver self.amount = amount - def encode(self, buf: bytes) -> bytes: - buf = self.sender.encode(buf) - buf = self.receiver.encode(buf) - return self.amount.encode(buf) + def encode(self, buf: io.Writer) -> None: + self.sender.encode(buf) + self.receiver.encode(buf) + self.amount.encode(buf) def get_type(self) -> PayloadType: return PayloadType.TRANSFER @@ -22,8 +29,8 @@ def signer(self) -> Address: return self.sender @classmethod - def decode(cls, buf: bytes) -> tuple: - sender, buf = Address.decode(buf) - receiver, buf = Address.decode(buf) - amount, buf = Amount.decode(buf) - return cls(sender, receiver, amount), buf + def decode(cls, buf: io.BufferedReader) -> TransferPayload: + sender = Address.decode(buf) + receiver = Address.decode(buf) + amount = Amount.decode(buf) + return cls(sender, receiver, amount) diff --git a/pactus/transaction/payload/unbond.py b/pactus/transaction/payload/unbond.py index f98a49c..69bc18c 100644 --- a/pactus/transaction/payload/unbond.py +++ b/pactus/transaction/payload/unbond.py @@ -1,3 +1,10 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io + from pactus.crypto.address import Address from ._payload import PayloadType @@ -7,8 +14,8 @@ class UnbondPayload: def __init__(self, validator: Address) -> None: self.validator = validator - def encode(self, buf: bytes) -> bytes: - return self.validator.encode(buf) + def encode(self, buf: io.Writer) -> None: + self.validator.encode(buf) def get_type(self) -> PayloadType: return PayloadType.UNBOND @@ -17,6 +24,6 @@ def signer(self) -> Address: return self.validator @classmethod - def decode(cls, buf: bytes) -> tuple: - validator, buf = Address.decode(buf) - return cls(validator), buf + def decode(cls, buf: io.BufferedReader) -> UnbondPayload: + validator = Address.decode(buf) + return cls(validator) diff --git a/pactus/transaction/payload/withdraw.py b/pactus/transaction/payload/withdraw.py index 50e9525..e56494c 100644 --- a/pactus/transaction/payload/withdraw.py +++ b/pactus/transaction/payload/withdraw.py @@ -1,3 +1,10 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io + from pactus.crypto.address import Address from pactus.types.amount import Amount @@ -10,10 +17,10 @@ def __init__(self, from_addr: Address, to_addr: Address, amount: Amount) -> None self.to_addr = to_addr self.amount = amount - def encode(self, buf: bytes) -> bytes: - buf = self.from_addr.encode(buf) - buf = self.to_addr.encode(buf) - return self.amount.encode(buf) + def encode(self, buf: io.Writer) -> None: + self.from_addr.encode(buf) + self.to_addr.encode(buf) + self.amount.encode(buf) def get_type(self) -> PayloadType: return PayloadType.WITHDRAW @@ -22,8 +29,8 @@ def signer(self) -> Address: return self.from_addr @classmethod - def decode(cls, buf: bytes) -> tuple: - from_addr, buf = Address.decode(buf) - to_addr, buf = Address.decode(buf) - amount, buf = Amount.decode(buf) - return cls(from_addr, to_addr, amount), buf + def decode(cls, buf: io.BufferedReader) -> WithdrawPayload: + from_addr = Address.decode(buf) + to_addr = Address.decode(buf) + amount = Amount.decode(buf) + return cls(from_addr, to_addr, amount) diff --git a/pactus/transaction/transaction.py b/pactus/transaction/transaction.py index 6ee4902..321e46d 100644 --- a/pactus/transaction/transaction.py +++ b/pactus/transaction/transaction.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +import io from pactus.crypto.address import Address, AddressType from pactus.crypto.bls.public_key import PublicKey as BLSPublicKey @@ -49,28 +50,25 @@ def __init__( self.signature: Signature = None @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Transaction from bytes. - Returns (Transaction, remaining_buf). - """ - flags, buf = encoding.read_uint8(buf) - version, buf = encoding.read_uint8(buf) - lock_time, buf = Height.decode(buf) - fee, buf = Amount.decode(buf) - memo, buf = encoding.read_str(buf) - payload_type, buf = encoding.read_uint8(buf) + def decode(cls, buf: io.BufferedReader) -> Transaction: + """Decode a Transaction from the stream.""" + flags = encoding.read_uint8(buf) + version = encoding.read_uint8(buf) + lock_time = Height.decode(buf) + fee = Amount.decode(buf) + memo = encoding.read_str(buf) + payload_type = encoding.read_uint8(buf) if payload_type == PayloadType.TRANSFER.value: - payload, buf = TransferPayload.decode(buf) + payload = TransferPayload.decode(buf) elif payload_type == PayloadType.BOND.value: - payload, buf = BondPayload.decode(buf) + payload = BondPayload.decode(buf) elif payload_type == PayloadType.SORTITION.value: - payload, buf = SortitionPayload.decode(buf) + payload = SortitionPayload.decode(buf) elif payload_type == PayloadType.UNBOND.value: - payload, buf = UnbondPayload.decode(buf) + payload = UnbondPayload.decode(buf) elif payload_type == PayloadType.WITHDRAW.value: - payload, buf = WithdrawPayload.decode(buf) + payload = WithdrawPayload.decode(buf) else: msg = f"unknown payload type: {payload_type}" raise ValueError(msg) @@ -86,20 +84,18 @@ def decode(cls, buf: bytes) -> tuple: tx.signature = None if flags & FLAG_NOT_SIGNED: - return tx, buf + return tx signer_type = payload.signer().address_type() - sig, buf = Transaction._decode_signature(buf, signer_type) - tx.signature = sig + tx.signature = Transaction._decode_signature(buf, signer_type) if (flags & FLAG_STRIPPED_PUBLIC_KEY) == 0: - pub, buf = Transaction._decode_public_key(buf, signer_type) - tx.public_key = pub + tx.public_key = Transaction._decode_public_key(buf, signer_type) - return tx, buf + return tx @staticmethod - def _decode_signature(buf: bytes, signer_type: AddressType) -> tuple: + def _decode_signature(buf: io.BufferedReader, signer_type: AddressType) -> Signature: if signer_type in (AddressType.VALIDATOR, AddressType.BLS_ACCOUNT): return BLSSignature.decode(buf) if signer_type == AddressType.ED25519_ACCOUNT: @@ -110,7 +106,7 @@ def _decode_signature(buf: bytes, signer_type: AddressType) -> tuple: raise ValueError(msg) @staticmethod - def _decode_public_key(buf: bytes, signer_type: AddressType) -> tuple: + def _decode_public_key(buf: io.BufferedReader, signer_type: AddressType) -> PublicKey: if signer_type in (AddressType.VALIDATOR, AddressType.BLS_ACCOUNT): return BLSPublicKey.decode(buf) if signer_type == AddressType.ED25519_ACCOUNT: @@ -170,20 +166,21 @@ def create_withdraw_tx( payload = WithdrawPayload(from_addr, to_addr, amount) return cls(lock_time, fee, memo, payload) - def _get_unsigned_bytes(self, buf: bytes) -> bytes: - """Generate the unsigned bytes of the transaction for signing.""" - buf = encoding.append_uint8(buf, self.flags) - buf = encoding.append_uint8(buf, self.version) - buf = self.lock_time.encode(buf) - buf = self.fee.encode(buf) - buf = encoding.append_str(buf, self.memo) - buf = encoding.append_uint8(buf, self.payload.get_type().value) - return self.payload.encode(buf) + def _get_unsigned_bytes(self, buf: io.Writer) -> None: + """Write the unsigned bytes of the transaction to the buffer.""" + encoding.append_uint8(buf, self.flags) + encoding.append_uint8(buf, self.version) + self.lock_time.encode(buf) + self.fee.encode(buf) + encoding.append_str(buf, self.memo) + encoding.append_uint8(buf, self.payload.get_type().value) + self.payload.encode(buf) def sign_bytes(self) -> bytes: """Return the bytes to be signed (everything except flags).""" - buf = self._get_unsigned_bytes(b"") - return buf[1:] + buf = io.BytesIO() + self._get_unsigned_bytes(buf) + return buf.getvalue()[1:] def id(self) -> Hash: """Return the transaction ID (blake2b-256 of sign bytes).""" @@ -191,15 +188,16 @@ def id(self) -> Hash: def sign(self, private_key: PrivateKey) -> bytes: """Sign the transaction and return signed bytes.""" - buf = self._get_unsigned_bytes(b"") - sig = private_key.sign(buf[1:]) + buf = io.BytesIO() + self._get_unsigned_bytes(buf) + sig = private_key.sign(buf.getvalue()[1:]) pub = private_key.public_key() - buf = sig.encode(buf) - buf = pub.encode(buf) + sig.encode(buf) + pub.encode(buf) self.public_key = pub self.signature = sig self.flags |= FLAG_NOT_SIGNED - return buf + return buf.getvalue() diff --git a/pactus/types/amount.py b/pactus/types/amount.py index 6fe044b..662d9c7 100644 --- a/pactus/types/amount.py +++ b/pactus/types/amount.py @@ -1,3 +1,10 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io + import math from pactus.encoding import encoding @@ -22,7 +29,7 @@ class Amount: def __init__(self, value: int = 0) -> None: self.value = value - def __eq__(self, other: "Amount") -> bool: + def __eq__(self, other: Amount) -> bool: if isinstance(other, Amount): return self.value == other.value @@ -45,12 +52,12 @@ def to_nano_pac(self) -> int: return self.value @classmethod - def from_nano_pac(cls, a: int) -> "Amount": + def from_nano_pac(cls, a: int) -> Amount: """Store the value as NanoPAC in the Amount instance.""" return cls(a) @classmethod - def from_pac(cls, f: float) -> "Amount": + def from_pac(cls, f: float) -> Amount: """ Convert a floating-point value in PAC to NanoPAC and store it in the Amount instance. @@ -64,7 +71,7 @@ def from_pac(cls, f: float) -> "Amount": return cls.from_nano_pac(int(cls._round(f * NANO_PAC_PER_PAC))) @classmethod - def from_string(cls, s: str) -> "Amount": + def from_string(cls, s: str) -> Amount: """ Parse a string representing a value in PAC, converts it to NanoPAC, and updates the Amount object. @@ -79,17 +86,14 @@ def from_string(cls, s: str) -> "Amount": return cls.from_pac(f) - def encode(self, buf: bytes) -> bytes: - return encoding.append_var_int(buf, self.value) + def encode(self, buf: io.Writer) -> None: + encoding.append_var_int(buf, self.value) @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode an Amount from bytes. - Returns (Amount, remaining_buf). - """ - val, buf = encoding.read_var_int(buf) - return cls(val), buf + def decode(cls, buf: io.BufferedReader) -> Amount: + """Decode an Amount from the stream.""" + val = encoding.read_var_int(buf) + return cls(val) @staticmethod def _round(f: float) -> float: diff --git a/pactus/types/height.py b/pactus/types/height.py index 192b3a9..f2e6b10 100644 --- a/pactus/types/height.py +++ b/pactus/types/height.py @@ -1,3 +1,10 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io + from pactus.encoding import encoding @@ -12,7 +19,7 @@ class Height: def __init__(self, value: int = 0) -> None: self.value = value - def __eq__(self, other: "Height") -> bool: + def __eq__(self, other: Height) -> bool: if isinstance(other, Height): return self.value == other.value @@ -24,14 +31,11 @@ def __hash__(self) -> int: def __str__(self) -> str: return str(self.value) - def encode(self, buf: bytes) -> bytes: - return encoding.append_uint32(buf, self.value) + def encode(self, buf: io.Writer) -> None: + encoding.append_uint32(buf, self.value) @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Height from bytes. - Returns (Height, remaining_buf). - """ - val, buf = encoding.read_uint32(buf) - return cls(val), buf + def decode(cls, buf: io.BufferedReader) -> Height: + """Decode a Height from the stream.""" + val = encoding.read_uint32(buf) + return cls(val) diff --git a/pactus/types/round.py b/pactus/types/round.py index 47f7343..3be4c25 100644 --- a/pactus/types/round.py +++ b/pactus/types/round.py @@ -1,3 +1,10 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io + from pactus.encoding import encoding @@ -11,7 +18,7 @@ class Round: def __init__(self, value: int = 0) -> None: self.value = value - def __eq__(self, other: "Round") -> bool: + def __eq__(self, other: Round) -> bool: if isinstance(other, Round): return self.value == other.value @@ -23,14 +30,11 @@ def __hash__(self) -> int: def __str__(self) -> str: return str(self.value) - def encode(self, buf: bytes) -> bytes: - return encoding.append_uint16(buf, self.value) + def encode(self, buf: io.Writer) -> None: + encoding.append_uint16(buf, self.value) @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Round from bytes. - Returns (Round, remaining_buf). - """ - val, buf = encoding.read_uint16(buf) - return cls(val), buf + def decode(cls, buf: io.BufferedReader) -> Round: + """Decode a Round from the stream.""" + val = encoding.read_uint16(buf) + return cls(val) diff --git a/tests/test_amount.py b/tests/test_amount.py index e92e954..476233f 100644 --- a/tests/test_amount.py +++ b/tests/test_amount.py @@ -1,3 +1,4 @@ +import io import unittest from pactus.types.amount import NANO_PAC_PER_PAC, Amount @@ -219,6 +220,43 @@ def test_round_trip_conversion(self): self.assertEqual(amount, amount_from_nano) self.assertAlmostEqual(back_to_pac, pac_value, places=9) + def test_encode_decode(self): + """Test that encoding and decoding an Amount round-trips correctly.""" + test_cases = [ + Amount(0), + Amount.from_pac(42.5), + Amount.from_pac(1.0), + Amount.from_pac(0.5), + Amount.from_pac(0.000000001), + Amount.from_nano_pac(1000000000), + Amount.from_nano_pac(500000000), + Amount.from_nano_pac(1), + Amount.from_nano_pac(123456789), + ] + + for original in test_cases: + buf = io.BytesIO() + original.encode(buf) + + buf.seek(0) + decoded = Amount.decode(buf) + self.assertEqual(decoded, original) + + def test_encode_known_values(self): + """Test that encoding produces expected byte sequences.""" + test_cases = [ + (Amount(0), b"\x00"), + (Amount(1), b"\x01"), + (Amount(0xFF), b"\xff\x01"), + (Amount.from_nano_pac(1000000000), b"\x80\x94\xeb\xdc\x03"), + (Amount.from_nano_pac(42_000_000_000), b"\x80\xc8\x94\xbb\x9c\x01"), + ] + + for amount, expected_bytes in test_cases: + buf = io.BytesIO() + amount.encode(buf) + self.assertEqual(buf.getvalue(), expected_bytes) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_block.py b/tests/test_block.py index 8cad8d8..63b7834 100644 --- a/tests/test_block.py +++ b/tests/test_block.py @@ -1,3 +1,4 @@ +import io import unittest from pactus.block import Block @@ -34,7 +35,7 @@ def test_decode_block_88888(self): ) raw = bytes.fromhex(raw_hex) - block = Block.decode(raw) + block = Block.decode(io.BytesIO(raw)) # --- Header --- h = block.header diff --git a/tests/test_encoding.py b/tests/test_encoding.py index 19637bb..53eb221 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -1,4 +1,5 @@ import unittest +import io from pactus.encoding.encoding import ( append_uint8, @@ -23,13 +24,13 @@ def test_uint8(self): ] for _, (value, expected_bytes) in enumerate(tests): - buf = b"" - buf = append_uint8(buf, value) - self.assertEqual(buf, expected_bytes) + buf = io.BytesIO() + append_uint8(buf, value) + self.assertEqual(buf.getvalue(), expected_bytes) - read, buf = read_uint8(buf) + buf.seek(0) + read = read_uint8(buf) self.assertEqual(read, value) - self.assertEqual(len(buf), 0) def test_uint16(self): tests = [ @@ -41,13 +42,13 @@ def test_uint16(self): ] for _, (value, expected_bytes) in enumerate(tests): - buf = b"" - buf = append_uint16(buf, value) - self.assertEqual(buf, expected_bytes) + buf = io.BytesIO() + append_uint16(buf, value) + self.assertEqual(buf.getvalue(), expected_bytes) - read, buf = read_uint16(buf) + buf.seek(0) + read = read_uint16(buf) self.assertEqual(read, value) - self.assertEqual(len(buf), 0) def test_uint32(self): tests = [ @@ -59,13 +60,13 @@ def test_uint32(self): ] for _, (value, expected_bytes) in enumerate(tests): - buf = b"" - buf = append_uint32(buf, value) - self.assertEqual(buf, expected_bytes) + buf = io.BytesIO() + append_uint32(buf, value) + self.assertEqual(buf.getvalue(), expected_bytes) - read, buf = read_uint32(buf) + buf.seek(0) + read = read_uint32(buf) self.assertEqual(read, value) - self.assertEqual(len(buf), 0) def test_var_int(self): tests = [ @@ -89,13 +90,13 @@ def test_var_int(self): ] for i, (value, expected_bytes) in enumerate(tests): - buf = b"" - buf = append_var_int(buf, value) - self.assertEqual(buf, expected_bytes) + buf = io.BytesIO() + append_var_int(buf, value) + self.assertEqual(buf.getvalue(), expected_bytes) - read, buf = read_var_int(buf) + buf.seek(0) + read = read_var_int(buf) self.assertEqual(read, value) - self.assertEqual(len(buf), 0) def test_str(self): # str256 is a string that takes a 2-byte varint to encode. @@ -108,13 +109,13 @@ def test_str(self): ] for i, (value, expected_bytes) in enumerate(tests): - buf = b"" - buf = append_str(buf, value) - self.assertEqual(buf, expected_bytes) + buf = io.BytesIO() + append_str(buf, value) + self.assertEqual(buf.getvalue(), expected_bytes) - read, buf = read_str(buf) + buf.seek(0) + read = read_str(buf) self.assertEqual(read, value) - self.assertEqual(len(buf), 0) if __name__ == "__main__": diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 4f0286f..bd120e9 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -1,3 +1,4 @@ +import io import unittest from pactus.crypto.address import Address @@ -67,7 +68,9 @@ def test_decode_transfer_tx(self): "8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda8506" "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa" ) - expected_txid = "1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f" + expected_txid = ( + "1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f" + ) expected_sign_bytes = ( "0124a3230080ade2040b77616c6c65742d636f726501" "037098338e0b6808119dfd4457ab806b9c2059b89b" @@ -76,7 +79,7 @@ def test_decode_transfer_tx(self): ) raw = bytes.fromhex(signed_data_hex) - tx, _ = Transaction.decode(raw) + tx = Transaction.decode(io.BytesIO(raw)) self.assertEqual(tx.version, 1) self.assertEqual(tx.lock_time.value, 2335524) @@ -118,7 +121,9 @@ def test_decode_bond_tx(self): "715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d" "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa" ) - expected_txid = "f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80" + expected_txid = ( + "f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80" + ) expected_sign_bytes = ( "015ca3230080ade2040b77616c6c65742d636f726502" "037098338e0b6808119dfd4457ab806b9c2059b89b" @@ -128,7 +133,7 @@ def test_decode_bond_tx(self): ) raw = bytes.fromhex(signed_data_hex) - tx, _ = Transaction.decode(raw) + tx = Transaction.decode(io.BytesIO(raw)) self.assertEqual(tx.version, 1) self.assertEqual(tx.lock_time.value, 2335580)