From 780960ae5fd143a73eadb9f954ff34456c20e6ae Mon Sep 17 00:00:00 2001 From: Mostafa Date: Wed, 1 Jul 2026 03:20:44 +0800 Subject: [PATCH 1/4] refactor: add Header.encode() and simplify Block._header_bytes --- pactus/block/block.py | 8 +------- pactus/block/header.py | 8 ++++++++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pactus/block/block.py b/pactus/block/block.py index 8695c8f..2d2f454 100644 --- a/pactus/block/block.py +++ b/pactus/block/block.py @@ -53,13 +53,7 @@ def id(self) -> bytes: return hashlib.blake2b(buf, digest_size=32).digest() def _header_bytes(self) -> bytes: - h = self.header - buf = encoding.append_uint8(b"", h.version) - buf = encoding.append_uint32(buf, h.unix_time) - buf = h.prev_block_hash.encode(buf) - buf = h.state_root.encode(buf) - buf = encoding.append_fixed_bytes(buf, h.sortition_seed) - return h.proposer_address.encode(buf) + return self.header.encode(b"") def _txs_root(self) -> bytes: return Block._merkle_root([tx.id() for tx in self.transactions]) diff --git a/pactus/block/header.py b/pactus/block/header.py index d70cf6f..92e0cf9 100644 --- a/pactus/block/header.py +++ b/pactus/block/header.py @@ -42,3 +42,11 @@ def decode(cls, buf: bytes) -> tuple: 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) From 76ad6d7e096fff42a929577354cb46d1556fbc30 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Wed, 1 Jul 2026 03:25:44 +0800 Subject: [PATCH 2/4] refactor: add Certificate.encode() and simplify hash() --- pactus/block/certificate.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pactus/block/certificate.py b/pactus/block/certificate.py index f04b3d9..f5bab3e 100644 --- a/pactus/block/certificate.py +++ b/pactus/block/certificate.py @@ -47,9 +47,8 @@ def decode(cls, buf: bytes) -> tuple: cert = cls(height, round_, committers, absentees, signature) return cert, buf - def hash(self) -> bytes: - """Return the certificate hash (blake2b-256 of encoded bytes).""" - buf = self.height.encode(b"") + 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)) for n in self.committers: @@ -57,5 +56,8 @@ def hash(self) -> bytes: buf = encoding.append_var_int(buf, len(self.absentees)) for n in self.absentees: buf = encoding.append_var_int(buf, n) - buf = self.signature.encode(buf) - return hashlib.blake2b(buf, digest_size=32).digest() + return 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() From 3b97028d2a9cef70fdd8e7d3dcf146021c2350d4 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Wed, 1 Jul 2026 03:31:58 +0800 Subject: [PATCH 3/4] test: add block 88888 decode test with multiple tx types Block 88888 contains 5 transactions: subsidy transfer + 3 sortitions + bond. Validates header, certificate (51 committers), block ID, and all tx types. --- tests/test_block.py | 107 ++++++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/tests/test_block.py b/tests/test_block.py index c5bc68b..3b9d8bb 100644 --- a/tests/test_block.py +++ b/tests/test_block.py @@ -6,21 +6,31 @@ class TestBlockDecode(unittest.TestCase): - def test_decode_block_from_raw(self): - # Block number of 888 - # https://pactusscan.com/block/888 + def test_decode_block_88888(self): raw_hex = ( - "019094b165" - "84fdb8aac442735c6d4d4457e2236cd6ca86cb0b898ab37a03be1723b8faac55" - "d24bf4f116090735a366d1818ec48f5d91b4ff1deab091ecef5fc8bbae94cdd5" - "a50eac535372c874d7150a60cb8a988dde1a60b7ebda1729cc14f6d4d2575ed4" - "b72089c36ffe9f7ecc60d8be09912feb" - "010c4cbdcae2ba23a3335f994eaaf104a4f53981a2" - "770300000000040001020300" - "8da0b181eaa433b5c9855a89de0f0c7530ec4d2c3aecff290af7ae02b33dd968" - "43d6f72983bf25ce856946df1360710f" - "0102017803000000000100" - "020c8ce957b484397ab248c25ce135e2d0aff0c8e48094ebdc03" + "01d802bf65f805b91ce08e5ee84b4707248fd602db5364da6ba9b273a200228ce9c62be4ac" + "a846b739df34099e5345088e66ded3f63762cc766c5380c58f788fb134c9467c" + "b366e61d7dc9f9a63d0b4ce69aa1cfc91cc11436eb43220b4db00ac39b8056e47f6ee34f1846" + "83626ba43a0f85d7d82b01468a4e63084ff773a7750a52872c665b19b1174c375b" + "010000003324ee0365e004a605ab079403c305910780089406379203b0077622d6049201bd07e" + "903fb07bf072cc806c8053fc8014a0f81018901a701870835b301cb05850126b101b00120e2" + "051d5dae05c506d405a10534b502df0400" + "8a95d277dd80203ca7b964711f819bdd19b9e028f46d3416c43b1f3f28f746936b9c309960b9" + "6e01fceb4917e6bd95a3" + "050201385b01000000010002f9682245d323f7c4b72131cd6d0fac6f1cfc6783a0d689dd0301" + "01375b010000000301a2aa63337fad6bac1936dc563ada569c7a12e2aa93bf4a3be75443b0c7" + "8e2e342984bb89c55542cb0d1f09c4ec9ebae998f9c42f4d7385261793325fe1cbdd3141c07d" + "288252f8c291497f04e572d56bcc7997d19f634880d4c9f238e0791f062e5bc64af160b4c976" + "6c2c2f3cae588314909fe40101375b01000000030127179f1ae3a28030972f30b0a848053a2c" + "752bedb4bab53872a3d52e40bc04de319b0a92a70f49983379f113502c2e0e34f7321c9b31b4" + "eb8ce63c623e468e4ba401e6daadd19f9d254e0894b82f50fef40d1b4ad969b26894e43237c6" + "8b527571418cee783b5821e5f96234a8dcc53fea17266b0101375b01000000030180e9161eaa" + "d5285558caa382f1eec2d2642c13eeae3698cdedf32d066e1946b2d92881c0378aaa8155ad3b" + "52de4eeee2d106b4d0ef0bb69f0d6b2e6e8ce573652553bf0eadd458f64081f3c0bd3cec9566" + "1188a004a5f2ce81d40007238639a2babe6ba4347616b848098094e1c0c4f396196478010137" + "5b0100a0c21e000202b9abf7f9a0406e4a9f7262b3d3d23d449d161cad0165d2d2c74fb4cc35" + "09cdd646f4eeefacb1a9cf720080e497d0128c7d2655bfd876417fb7b18efd83374bd9cacebb" + "990453ba153defd4c41fb97bcc074552307f6c7194aeeb6e10588358" ) raw = bytes.fromhex(raw_hex) @@ -29,70 +39,81 @@ def test_decode_block_from_raw(self): # --- Header --- h = block.header self.assertEqual(h.version, 1) - self.assertEqual(h.unix_time, 1706136720) + self.assertEqual(h.unix_time, 1707016920) self.assertEqual( str(h.prev_block_hash), - "84fdb8aac442735c6d4d4457e2236cd6ca86cb0b898ab37a03be1723b8faac55", + "f805b91ce08e5ee84b4707248fd602db5364da6ba9b273a200228ce9c62be4ac", ) self.assertEqual( str(h.state_root), - "d24bf4f116090735a366d1818ec48f5d91b4ff1deab091ecef5fc8bbae94cdd5", - ) - self.assertEqual( - h.sortition_seed.hex(), - "a50eac535372c874d7150a60cb8a988dde1a60b7ebda1729cc14f6d4d2575ed4" - "b72089c36ffe9f7ecc60d8be09912feb", + "a846b739df34099e5345088e66ded3f63762cc766c5380c58f788fb134c9467c", ) self.assertEqual(h.proposer_address.address_type(), AddressType.VALIDATOR) - self.assertEqual( - h.proposer_address.raw_bytes().hex(), - "010c4cbdcae2ba23a3335f994eaaf104a4f53981a2", - ) self.assertEqual( h.proposer_address.string(), - "pc1pp3xtmjhzhg36xv6ln9824ugy5n6nnqdzugzu3j", + "pc1pg69yuccgflmh8fm4pffgwtrxtvvmz96vzjuvev", ) # --- Certificate --- cert = block.prev_cert self.assertIsNotNone(cert) - self.assertEqual(cert.height.value, 887) + self.assertEqual(cert.height.value, 88887) self.assertEqual(cert.round.value, 0) - self.assertEqual(cert.committers, [0, 1, 2, 3]) + self.assertEqual(len(cert.committers), 51) self.assertEqual(cert.absentees, []) self.assertEqual( - cert.signature.string(), - "8da0b181eaa433b5c9855a89de0f0c7530ec4d2c3aecff290af7ae02b33dd968" - "43d6f72983bf25ce856946df1360710f", + cert.hash().hex(), + "14aa7049254af312d2f8c5515989271a709a758e552801fe34ac68efa11581ef", ) # --- Block ID --- self.assertEqual( block.id.hex(), - "3fad130499227f7a83d4e65873b0f880db275a6c4d1b9dd65005c1f1667ec98f", + "5a12881b1d5fd9bea3a61f24d7555d8900e85d02e9606b5a3176edf3c355a32d", ) # --- Transactions --- - self.assertEqual(len(block.transactions), 1) + self.assertEqual(len(block.transactions), 5) + # Tx 0: Subsidy (transfer from treasury) tx = block.transactions[0] self.assertEqual(tx.version, 1) - self.assertEqual(tx.flags, 0x02) # FLAG_NOT_SIGNED - self.assertEqual(tx.lock_time.value, 888) + self.assertEqual(tx.flags, 0x02) + self.assertEqual(tx.lock_time.value, 88888) self.assertEqual(tx.fee.value, 0) - self.assertEqual(tx.memo, "") self.assertEqual(tx.payload.get_type(), PayloadType.TRANSFER) - - # Subsidy transaction: signer is treasury address self.assertTrue(tx.payload.signer().is_treasury_address()) - self.assertEqual(tx.signature, None) - self.assertEqual(tx.public_key, None) + self.assertIsNone(tx.signature) + self.assertIsNone(tx.public_key) + self.assertEqual(tx.payload.amount.value, 1_000_500_000) + self.assertEqual( + tx.payload.receiver.string(), + "pc1zl95zy3wny0mufdepx8xk6ravduw0ceurudtqqq", + ) + + # Tx 1-3: Sortition + for i in range(1, 4): + tx = block.transactions[i] + self.assertEqual(tx.flags, 0x01) + self.assertEqual(tx.lock_time.value, 88887) + self.assertEqual(tx.fee.value, 0) + self.assertEqual(tx.payload.get_type(), PayloadType.SORTITION) + self.assertIsNotNone(tx.signature) + self.assertIsNone(tx.public_key) # stripped - self.assertEqual(tx.payload.amount.value, 1_000_000_000) + # Tx 4: Bond + tx = block.transactions[4] + self.assertEqual(tx.flags, 0x01) + self.assertEqual(tx.lock_time.value, 88887) + self.assertEqual(tx.fee.value, 500_000) + self.assertEqual(tx.payload.get_type(), PayloadType.BOND) + self.assertEqual(tx.payload.stake.value, 5_000_000_000) self.assertEqual( tx.payload.receiver.string(), - "pc1zpjxwj4a5ssuh4vjgcfwwzd0z6zhlpj8ylnhdl8", + "pc1pvhfd9360knxr2zwd6er0fmh04jc6nnmjulhkcp", ) + self.assertIsNotNone(tx.signature) + self.assertIsNone(tx.public_key) # stripped if __name__ == "__main__": From ec4cd11297b588b38a45f951e3545728fc9ac47d Mon Sep 17 00:00:00 2001 From: Mostafa Date: Wed, 1 Jul 2026 10:55:08 +0800 Subject: [PATCH 4/4] chore: update hash decode func --- pactus/crypto/hash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pactus/crypto/hash.py b/pactus/crypto/hash.py index 018b024..f7e5451 100644 --- a/pactus/crypto/hash.py +++ b/pactus/crypto/hash.py @@ -32,7 +32,7 @@ def encode(self, buf: bytes) -> bytes: return encoding.append_fixed_bytes(buf, self.data) @classmethod - def decode(cls, buf: bytes) -> tuple[Hash, bytes]: + def decode(cls, buf: bytes) -> tuple: """ Decode a Hash from bytes. Returns (Hash, remaining_buf).