Skip to content

Commit 4a55842

Browse files
committed
feat: add block id, transaction id, and certificate hash
- Block.id: blake2b-256 of header + cert_hash + merkle_root + tx_count - Transaction.id(): blake2b-256 of sign_bytes - Certificate.hash(): blake2b-256 of encoded certificate - Simple merkle tree implementation for TX root - Test updated with expected block id
1 parent 2f5a52c commit 4a55842

4 files changed

Lines changed: 82 additions & 0 deletions

File tree

pactus/block/block.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import annotations
22

3+
import hashlib
4+
import struct
5+
36
from pactus.encoding import encoding
47
from pactus.transaction import Transaction
58

@@ -38,3 +41,53 @@ def decode(cls, data: bytes) -> Block:
3841
transactions.append(tx)
3942

4043
return cls(header, prev_cert, transactions)
44+
45+
@property
46+
def id(self) -> bytes:
47+
"""Return the block ID (blake2b-256 of header + cert_hash + tx_root + tx_count)."""
48+
buf = self._header_bytes()
49+
if self.prev_cert is not None:
50+
buf += self.prev_cert.hash()
51+
buf += self._txs_root()
52+
buf += struct.pack("<i", len(self.transactions))
53+
return hashlib.blake2b(buf, digest_size=32).digest()
54+
55+
def _header_bytes(self) -> bytes:
56+
h = self.header
57+
buf = encoding.append_uint8(b"", h.version)
58+
buf = encoding.append_uint32(buf, h.unix_time)
59+
buf = h.prev_block_hash.encode(buf)
60+
buf = h.state_root.encode(buf)
61+
buf = encoding.append_fixed_bytes(buf, h.sortition_seed)
62+
return h.proposer_address.encode(buf)
63+
64+
def _txs_root(self) -> bytes:
65+
return Block._merkle_root([tx.id() for tx in self.transactions])
66+
67+
@staticmethod
68+
def _merkle_root(hashes: list) -> bytes:
69+
if not hashes:
70+
return bytes(32)
71+
72+
# Build simple merkle tree
73+
n = 1
74+
while n < len(hashes):
75+
n <<= 1
76+
77+
tree = [None] * (n * 2 - 1)
78+
for i, h in enumerate(hashes):
79+
tree[i] = h
80+
81+
offset = n
82+
for i in range(0, len(tree) - 1, 2):
83+
left = tree[i]
84+
right = tree[i + 1]
85+
if left is None:
86+
tree[offset] = None
87+
elif right is None:
88+
tree[offset] = hashlib.blake2b(left + left, digest_size=32).digest()
89+
else:
90+
tree[offset] = hashlib.blake2b(left + right, digest_size=32).digest()
91+
offset += 1
92+
93+
return tree[-1] or bytes(32)

pactus/block/certificate.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import hashlib
2+
13
from pactus.crypto.bls.signature import Signature
24
from pactus.encoding import encoding
35
from pactus.types.height import Height
@@ -44,3 +46,16 @@ def decode(cls, buf: bytes) -> tuple:
4446

4547
cert = cls(height, round_, committers, absentees, signature)
4648
return cert, buf
49+
50+
def hash(self) -> bytes:
51+
"""Return the certificate hash (blake2b-256 of encoded bytes)."""
52+
buf = self.height.encode(b"")
53+
buf = self.round.encode(buf)
54+
buf = encoding.append_var_int(buf, len(self.committers))
55+
for n in self.committers:
56+
buf = encoding.append_var_int(buf, n)
57+
buf = encoding.append_var_int(buf, len(self.absentees))
58+
for n in self.absentees:
59+
buf = encoding.append_var_int(buf, n)
60+
buf = self.signature.encode(buf)
61+
return hashlib.blake2b(buf, digest_size=32).digest()

pactus/transaction/transaction.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import hashlib
4+
35
from pactus.crypto.address import Address, AddressType
46
from pactus.crypto.bls.public_key import PublicKey as BLSPublicKey
57
from pactus.crypto.bls.signature import Signature as BLSSignature
@@ -182,6 +184,10 @@ def sign_bytes(self) -> bytes:
182184
buf = self._get_unsigned_bytes(b"")
183185
return buf[1:]
184186

187+
def id(self) -> bytes:
188+
"""Return the transaction ID (blake2b-256 of sign bytes)."""
189+
return hashlib.blake2b(self.sign_bytes(), digest_size=32).digest()
190+
185191
def sign(self, private_key: PrivateKey) -> bytes:
186192
"""Sign the transaction and return signed bytes."""
187193
buf = self._get_unsigned_bytes(b"")

tests/test_block.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
class TestBlockDecode(unittest.TestCase):
99
def test_decode_block_from_raw(self):
10+
# Block number of 888
11+
# https://pactusscan.com/block/888
1012
raw_hex = (
1113
"019094b165"
1214
"84fdb8aac442735c6d4d4457e2236cd6ca86cb0b898ab37a03be1723b8faac55"
@@ -64,6 +66,12 @@ def test_decode_block_from_raw(self):
6466
"43d6f72983bf25ce856946df1360710f",
6567
)
6668

69+
# --- Block ID ---
70+
self.assertEqual(
71+
block.id.hex(),
72+
"3fad130499227f7a83d4e65873b0f880db275a6c4d1b9dd65005c1f1667ec98f",
73+
)
74+
6775
# --- Transactions ---
6876
self.assertEqual(len(block.transactions), 1)
6977

0 commit comments

Comments
 (0)