Skip to content

Commit 95e5b02

Browse files
authored
Byron address (#464)
* Byron address * More test * Format
1 parent e53aa55 commit 95e5b02

File tree

8 files changed

+590
-15
lines changed

8 files changed

+590
-15
lines changed

docs/source/guides/address.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ a stake verification key::
8585
An address object could also be created from an address string directly::
8686

8787
>>> address = Address.from_primitive("addr_test1vr2p8st5t5cxqglyjky7vk98k7jtfhdpvhl4e97cezuhn0cqcexl7")
88+
>>> byron_address = Address.from_primitive("Ae2tdPwUPEZFRbyhz3cpfC2CumGzNkFBN2L42rcUc2yjQpEkxDbkPodpMAi")
8889

8990

9091
An enterprise address does not have staking functionalities, it is created from a payment verification key only::

poetry.lock

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pycardano/address.py

Lines changed: 233 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88

99
from __future__ import annotations
1010

11+
import binascii
1112
import os
1213
from enum import Enum
1314
from typing import Optional, Type, Union
1415

16+
import base58
17+
import cbor2
18+
from cbor2 import CBORTag
1519
from typing_extensions import override
1620

1721
from pycardano.crypto.bech32 import decode, encode
@@ -202,12 +206,32 @@ def __init__(
202206
self._payment_part = payment_part
203207
self._staking_part = staking_part
204208
self._network = network
209+
210+
# Byron address fields (only populated when decoding Byron addresses)
211+
self._byron_payload_hash: Optional[bytes] = None
212+
self._byron_attributes: Optional[dict] = None
213+
self._byron_type: Optional[int] = None
214+
self._byron_crc32: Optional[int] = None
215+
205216
self._address_type = self._infer_address_type()
206-
self._header_byte = self._compute_header_byte()
207-
self._hrp = self._compute_hrp()
217+
self._header_byte = self._compute_header_byte() if not self.is_byron else None
218+
self._hrp = self._compute_hrp() if not self.is_byron else None
219+
220+
@property
221+
def is_byron(self) -> bool:
222+
"""Check if this is a Byron-era address.
223+
224+
Returns:
225+
bool: True if this is a Byron address, False if Shelley/later.
226+
"""
227+
return self._byron_payload_hash is not None
208228

209229
def _infer_address_type(self):
210230
"""Guess address type from the combination of payment part and staking part."""
231+
# Check if this is a Byron address
232+
if self.is_byron:
233+
return AddressType.BYRON
234+
211235
payment_type = type(self.payment_part)
212236
staking_type = type(self.staking_part)
213237
if payment_type == VerificationKeyHash:
@@ -263,15 +287,35 @@ def address_type(self) -> AddressType:
263287
return self._address_type
264288

265289
@property
266-
def header_byte(self) -> bytes:
267-
"""Header byte that identifies the type of address."""
290+
def header_byte(self) -> Optional[bytes]:
291+
"""Header byte that identifies the type of address. None for Byron addresses."""
268292
return self._header_byte
269293

270294
@property
271-
def hrp(self) -> str:
272-
"""Human-readable prefix for bech32 encoder."""
295+
def hrp(self) -> Optional[str]:
296+
"""Human-readable prefix for bech32 encoder. None for Byron addresses."""
273297
return self._hrp
274298

299+
@property
300+
def payload_hash(self) -> Optional[bytes]:
301+
"""Byron address payload hash (28 bytes). None for Shelley addresses."""
302+
return self._byron_payload_hash if self.is_byron else None
303+
304+
@property
305+
def byron_attributes(self) -> Optional[dict]:
306+
"""Byron address attributes. None for Shelley addresses."""
307+
return self._byron_attributes if self.is_byron else None
308+
309+
@property
310+
def byron_type(self) -> Optional[int]:
311+
"""Byron address type (0=Public Key, 2=Redemption). None for Shelley addresses."""
312+
return self._byron_type if self.is_byron else None
313+
314+
@property
315+
def crc32_checksum(self) -> Optional[int]:
316+
"""Byron address CRC32 checksum. None for Shelley addresses."""
317+
return self._byron_crc32 if self.is_byron else None
318+
275319
def _compute_header_byte(self) -> bytes:
276320
"""Compute the header byte."""
277321
return (self.address_type.value << 4 | self.network.value).to_bytes(
@@ -294,6 +338,16 @@ def _compute_hrp(self) -> str:
294338
return prefix + suffix
295339

296340
def __bytes__(self):
341+
if self.is_byron:
342+
payload = cbor2.dumps(
343+
[
344+
self._byron_payload_hash,
345+
self._byron_attributes,
346+
self._byron_type,
347+
]
348+
)
349+
return cbor2.dumps([CBORTag(24, payload), self._byron_crc32])
350+
297351
payment = self.payment_part or bytes()
298352
if self.staking_part is None:
299353
staking = bytes()
@@ -304,19 +358,21 @@ def __bytes__(self):
304358
return self.header_byte + bytes(payment) + bytes(staking)
305359

306360
def encode(self) -> str:
307-
"""Encode the address in Bech32 format.
361+
"""Encode the address in Bech32 format (Shelley) or Base58 format (Byron).
308362
309363
More info about Bech32 `here <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32>`_.
310364
311365
Returns:
312-
str: Encoded address in Bech32.
366+
str: Encoded address in Bech32 (Shelley) or Base58 (Byron).
313367
314368
Examples:
315369
>>> payment_hash = VerificationKeyHash(
316370
... bytes.fromhex("cc30497f4ff962f4c1dca54cceefe39f86f1d7179668009f8eb71e59"))
317371
>>> print(Address(payment_hash).encode())
318372
addr1v8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3ukgqdsn8w
319373
"""
374+
if self.is_byron:
375+
return base58.b58encode(bytes(self)).decode("ascii")
320376
return encode(self.hrp, bytes(self))
321377

322378
@classmethod
@@ -345,8 +401,38 @@ def to_primitive(self) -> bytes:
345401
@classmethod
346402
@limit_primitive_type(bytes, str)
347403
def from_primitive(cls: Type[Address], value: Union[bytes, str]) -> Address:
404+
# Convert string to bytes
348405
if isinstance(value, str):
349-
value = bytes(decode(value))
406+
# Check for Byron Base58 prefixes (common Byron patterns)
407+
if value.startswith(("Ae2td", "Ddz")):
408+
return cls._from_byron_base58(value)
409+
410+
# Try Bech32 decode for Shelley addresses
411+
original_str = value
412+
try:
413+
value = bytes(decode(value))
414+
except Exception:
415+
try:
416+
return cls._from_byron_base58(original_str)
417+
except Exception as e:
418+
raise DecodingException(f"Failed to decode address string: {e}")
419+
420+
# At this point, value is always bytes
421+
# Check if it's a Byron address (CBOR with tag 24)
422+
try:
423+
decoded = cbor2.loads(value)
424+
if isinstance(decoded, (tuple, list)) and len(decoded) == 2:
425+
if isinstance(decoded[0], CBORTag) and decoded[0].tag == 24:
426+
# This is definitely a Byron address - validate and decode it
427+
return cls._from_byron_cbor(value)
428+
except DecodingException:
429+
# Byron decoding failed with validation error - re-raise it
430+
raise
431+
except Exception:
432+
# Not Byron CBOR (general CBOR decode error), continue with Shelley decoding
433+
pass
434+
435+
# Shelley address decoding (existing logic)
350436
header = value[0]
351437
payload = value[1:]
352438
addr_type = AddressType((header & 0xF0) >> 4)
@@ -397,16 +483,150 @@ def from_primitive(cls: Type[Address], value: Union[bytes, str]) -> Address:
397483
return cls(None, ScriptHash(payload), network)
398484
raise DeserializeException(f"Error in deserializing bytes: {value}")
399485

486+
@classmethod
487+
def _from_byron_base58(cls: Type[Address], base58_str: str) -> Address:
488+
"""Decode a Byron address from Base58 string.
489+
490+
Args:
491+
base58_str: Base58-encoded Byron address string.
492+
493+
Returns:
494+
Address: Decoded Byron address instance.
495+
496+
Raises:
497+
DecodingException: When decoding fails.
498+
"""
499+
try:
500+
cbor_bytes = base58.b58decode(base58_str)
501+
except Exception as e:
502+
raise DecodingException(f"Failed to decode Base58 string: {e}")
503+
504+
return cls._from_byron_cbor(cbor_bytes)
505+
506+
@classmethod
507+
def _from_byron_cbor(cls: Type[Address], cbor_bytes: bytes) -> Address:
508+
"""Decode a Byron address from CBOR bytes.
509+
510+
Args:
511+
cbor_bytes: CBOR-encoded Byron address bytes.
512+
513+
Returns:
514+
Address: Decoded Byron address instance.
515+
516+
Raises:
517+
DecodingException: When decoding fails.
518+
"""
519+
try:
520+
decoded = cbor2.loads(cbor_bytes)
521+
except Exception as e:
522+
raise DecodingException(f"Failed to decode CBOR bytes: {e}")
523+
524+
# Byron address structure: [CBORTag(24, payload), crc32]
525+
if not isinstance(decoded, (tuple, list)) or len(decoded) != 2:
526+
raise DecodingException(
527+
f"Byron address must be a 2-element array, got {type(decoded)}"
528+
)
529+
530+
tagged_payload, crc32_checksum = decoded
531+
532+
if not isinstance(tagged_payload, CBORTag) or tagged_payload.tag != 24:
533+
raise DecodingException(
534+
f"Byron address must use CBOR tag 24, got {tagged_payload}"
535+
)
536+
537+
payload_cbor = tagged_payload.value
538+
if not isinstance(payload_cbor, bytes):
539+
raise DecodingException(
540+
f"Tag 24 must contain bytes, got {type(payload_cbor)}"
541+
)
542+
543+
computed_crc32 = binascii.crc32(payload_cbor) & 0xFFFFFFFF
544+
if computed_crc32 != crc32_checksum:
545+
raise DecodingException(
546+
f"CRC32 checksum mismatch: expected {crc32_checksum}, got {computed_crc32}"
547+
)
548+
549+
try:
550+
payload = cbor2.loads(payload_cbor)
551+
except Exception as e:
552+
raise DecodingException(f"Failed to decode Byron address payload: {e}")
553+
554+
if not isinstance(payload, (tuple, list)) or len(payload) != 3:
555+
raise DecodingException(
556+
f"Byron address payload must be a 3-element array, got {payload}"
557+
)
558+
559+
payload_hash, attributes, byron_type = payload
560+
561+
if not isinstance(payload_hash, bytes) or len(payload_hash) != 28:
562+
size = (
563+
len(payload_hash)
564+
if isinstance(payload_hash, bytes)
565+
else f"type {type(payload_hash).__name__}"
566+
)
567+
raise DecodingException(f"Payload hash must be 28 bytes, got {size}")
568+
569+
if not isinstance(attributes, dict):
570+
raise DecodingException(
571+
f"Attributes must be a dict, got {type(attributes)}"
572+
)
573+
574+
if byron_type not in (0, 2):
575+
raise DecodingException(f"Byron type must be 0 or 2, got {byron_type}")
576+
577+
# Create Address instance with Byron fields
578+
addr = cls.__new__(cls)
579+
addr._payment_part = None
580+
addr._staking_part = None
581+
addr._byron_payload_hash = payload_hash
582+
addr._byron_attributes = attributes
583+
addr._byron_type = byron_type
584+
addr._byron_crc32 = crc32_checksum
585+
addr._network = addr._infer_byron_network()
586+
addr._address_type = AddressType.BYRON
587+
addr._header_byte = None
588+
addr._hrp = None
589+
return addr
590+
591+
def _infer_byron_network(self) -> Network:
592+
"""Infer network from Byron address attributes.
593+
594+
Returns:
595+
Network: MAINNET or TESTNET (defaults to MAINNET).
596+
"""
597+
if self._byron_attributes and 2 in self._byron_attributes:
598+
network_bytes = self._byron_attributes[2]
599+
if isinstance(network_bytes, bytes):
600+
try:
601+
network_discriminant = cbor2.loads(network_bytes)
602+
# Mainnet: 764824073 (0x2D964A09), Testnet: 1097911063 (0x42659F17)
603+
if network_discriminant == 1097911063:
604+
return Network.TESTNET
605+
except Exception:
606+
pass
607+
return Network.MAINNET
608+
400609
def __eq__(self, other):
401610
if not isinstance(other, Address):
402611
return False
403-
else:
612+
613+
if self.is_byron != other.is_byron:
614+
return False
615+
616+
if self.is_byron:
404617
return (
405-
other.payment_part == self.payment_part
406-
and other.staking_part == self.staking_part
407-
and other.network == self.network
618+
self._byron_payload_hash == other._byron_payload_hash
619+
and self._byron_attributes == other._byron_attributes
620+
and self._byron_type == other._byron_type
621+
and self._byron_crc32 == other._byron_crc32
408622
)
409623

624+
return (
625+
self.payment_part == other.payment_part
626+
and self.staking_part == other.staking_part
627+
and self.network == other.network
628+
)
629+
410630
def __repr__(self):
411631
return f"{self.encode()}"
412632

pycardano/transaction.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ class Transaction(ArrayCBORSerializable):
690690

691691
transaction_witness_set: TransactionWitnessSet
692692

693-
valid: bool = True
693+
valid: Optional[bool] = field(default=True, metadata={"optional": True})
694694

695695
auxiliary_data: Optional[AuxiliaryData] = None
696696

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ docker = ">=7.1.0"
3939
ogmios = ">=1.4.2"
4040
requests = ">=2.32.3"
4141
websockets = ">=13.0"
42+
base58 = ">=2.1.0"
4243

4344
[tool.poetry.group.dev.dependencies]
4445
pytest = ">=8.2.0"

0 commit comments

Comments
 (0)