diff --git a/ledger/common/address.go b/ledger/common/address.go index a013c944..7232cf8a 100644 --- a/ledger/common/address.go +++ b/ledger/common/address.go @@ -16,6 +16,7 @@ package common import ( "fmt" + "hash/crc32" "strings" "github.com/blinklabs-io/gouroboros/base58" @@ -42,16 +43,22 @@ const ( AddressTypeByron = 0b1000 AddressTypeNoneKey = 0b1110 AddressTypeNoneScript = 0b1111 + + ByronAddressTypePubkey = 0 + ByronAddressTypeScript = 1 + ByronAddressTypeRedeem = 2 ) type AddrKeyHash Blake2b224 type Address struct { - addressType uint8 - networkId uint8 - paymentAddress []byte - stakingAddress []byte - extraData []byte + addressType uint8 + networkId uint8 + paymentAddress []byte + stakingAddress []byte + extraData []byte + byronAddressType uint64 + byronAddressAttr ByronAddressAttributes } // NewAddress returns an Address based on the provided bech32/base58 address string @@ -109,15 +116,59 @@ func NewAddressFromParts( }, nil } +func NewByronAddressFromParts( + byronAddrType uint64, + paymentAddr []byte, + attr ByronAddressAttributes, +) (Address, error) { + if len(paymentAddr) != AddressHashSize { + return Address{}, fmt.Errorf( + "invalid payment address hash length: %d", + len(paymentAddr), + ) + } + return Address{ + addressType: AddressTypeByron, + paymentAddress: paymentAddr, + byronAddressType: byronAddrType, + byronAddressAttr: attr, + }, nil +} + func (a *Address) populateFromBytes(data []byte) error { // Extract header info header := data[0] a.addressType = (header & AddressHeaderTypeMask) >> 4 a.networkId = header & AddressHeaderNetworkMask + // Byron Addresses + if a.addressType == AddressTypeByron { + var rawAddr byronAddress + if _, err := cbor.Decode(data, &rawAddr); err != nil { + return err + } + payloadBytes, ok := rawAddr.Payload.Content.([]byte) + if !ok || rawAddr.Payload.Number != 24 { + return fmt.Errorf("invalid Byron address data: unexpected payload content") + } + payloadChecksum := crc32.ChecksumIEEE(payloadBytes) + if rawAddr.Checksum != payloadChecksum { + return fmt.Errorf("invalid Byron address data: checksum does not match") + } + var byronAddr byronAddressPayload + if _, err := cbor.Decode(payloadBytes, &byronAddr); err != nil { + return err + } + if len(byronAddr.Hash) != AddressHashSize { + return fmt.Errorf("invalid Byron address data: hash is not expected length") + } + a.byronAddressType = byronAddr.AddrType + a.byronAddressAttr = byronAddr.Attr + a.paymentAddress = byronAddr.Hash + return nil + } // Check length // We exclude a few address types without fixed sizes that we don't properly support yet - if a.addressType != AddressTypeByron && - a.addressType != AddressTypeKeyPointer && + if a.addressType != AddressTypeKeyPointer && a.addressType != AddressTypeScriptPointer { dataLen := len(data) // Addresses must be at least the address hash size plus header byte @@ -135,7 +186,6 @@ func (a *Address) populateFromBytes(data []byte) error { } } // Extract payload - // NOTE: this is definitely incorrect for Byron payload := data[1:] a.paymentAddress = payload[:AddressHashSize] payload = payload[AddressHashSize:] @@ -264,6 +314,31 @@ func (a Address) generateHRP() string { // Bytes returns the underlying bytes for the address func (a Address) Bytes() []byte { + if a.addressType == AddressTypeByron { + tmpPayload := []any{ + a.paymentAddress, + a.byronAddressAttr, + a.byronAddressType, + } + rawPayload, err := cbor.Encode(tmpPayload) + if err != nil { + // TODO: handle error + return nil + } + tmpData := []any{ + cbor.Tag{ + Number: 24, + Content: rawPayload, + }, + crc32.ChecksumIEEE(rawPayload), + } + ret, err := cbor.Encode(tmpData) + if err != nil { + // TODO: handle error + return nil + } + return ret + } ret := []byte{} ret = append( ret, @@ -301,3 +376,55 @@ func (a Address) String() string { func (a Address) MarshalJSON() ([]byte, error) { return []byte(`"` + a.String() + `"`), nil } + +type byronAddress struct { + cbor.StructAsArray + Payload cbor.Tag + Checksum uint32 +} + +type byronAddressPayload struct { + cbor.StructAsArray + Hash []byte + Attr ByronAddressAttributes + AddrType uint64 +} + +type ByronAddressAttributes struct { + Payload []byte + Network *uint8 +} + +func (a *ByronAddressAttributes) UnmarshalCBOR(data []byte) error { + var tmpData struct { + Payload []byte `cbor:"1,keyasint,omitempty"` + NetworkRaw []byte `cbor:"2,keyasint,omitempty"` + } + if _, err := cbor.Decode(data, &tmpData); err != nil { + return err + } + a.Payload = tmpData.Payload + if len(tmpData.NetworkRaw) > 0 { + var tmpNetwork uint8 + if _, err := cbor.Decode(tmpData.NetworkRaw, &tmpNetwork); err != nil { + return err + } + a.Network = &tmpNetwork + } + return nil +} + +func (a *ByronAddressAttributes) MarshalCBOR() ([]byte, error) { + tmpData := make(map[int]any) + if len(a.Payload) > 0 { + tmpData[1] = a.Payload + } + if a.Network != nil { + networkRaw, err := cbor.Encode(a.Network) + if err != nil { + return nil, err + } + tmpData[2] = networkRaw + } + return cbor.Encode(tmpData) +} diff --git a/ledger/common/address_test.go b/ledger/common/address_test.go index caf54b87..7fc3200f 100644 --- a/ledger/common/address_test.go +++ b/ledger/common/address_test.go @@ -53,11 +53,16 @@ func TestAddressFromBytes(t *testing.T) { addressBytesHex: "61549b5a20e449a3e394b762705f64b9a26b99013003a2bfdba239967c00", expectedAddress: "addr1v92fkk3qu3y68cu5ka38qhmyhx3xhxgpxqp6907m5guevlqqjd7xgj", }, - // Byron address + // Byron address, mainnet with derivation { addressBytesHex: "82d818584283581caf56de241bcca83d72c51e74d18487aa5bc68b45e2caa170fa329d3aa101581e581cea1425ccdd649b25af5deb7e6335da2eb8167353a55e77925122e95f001a3a858621", expectedAddress: "DdzFFzCqrht2ii4Vc7KRchSkVvQtCqdGkQt4nF4Yxg1NpsubFBity2Tpt2eSEGrxBH1eva8qCFKM2Y5QkwM1SFBizRwZgz1N452WYvgG", }, + // Byron address, preview + { + addressBytesHex: "82d818582483581c5d5e698eba3dd9452add99a1af9461beb0ba61b8bece26e7399878dda1024102001a36d41aba", + expectedAddress: "FHnt4NL7yPXvDWHa8bVs73UEUdJd64VxWXSFNqetECtYfTd9TtJguJ14Lu3feth", + }, } for _, testDef := range testDefs { addr := Address{} @@ -149,6 +154,58 @@ func TestAddressFromParts(t *testing.T) { } } +func TestByronAddressFromParts(t *testing.T) { + testDefs := []struct { + addressType uint64 + paymentAddr []byte + addressAttr ByronAddressAttributes + expectedAddress string + }{ + // Byron address, mainnet with derivation + { + addressType: ByronAddressTypePubkey, + paymentAddr: test.DecodeHexString( + "af56de241bcca83d72c51e74d18487aa5bc68b45e2caa170fa329d3a", + ), + addressAttr: ByronAddressAttributes{ + Payload: test.DecodeHexString( + "581cea1425ccdd649b25af5deb7e6335da2eb8167353a55e77925122e95f", + ), + }, + expectedAddress: "DdzFFzCqrht2ii4Vc7KRchSkVvQtCqdGkQt4nF4Yxg1NpsubFBity2Tpt2eSEGrxBH1eva8qCFKM2Y5QkwM1SFBizRwZgz1N452WYvgG", + }, + // Byron address, preview + { + addressType: ByronAddressTypePubkey, + paymentAddr: test.DecodeHexString( + "5d5e698eba3dd9452add99a1af9461beb0ba61b8bece26e7399878dd", + ), + addressAttr: ByronAddressAttributes{ + // We have to jump through this hoop to get an inline pointer to a uint8 + Network: func() *uint8 { ret := uint8(2); return &ret }(), + }, + expectedAddress: "FHnt4NL7yPXvDWHa8bVs73UEUdJd64VxWXSFNqetECtYfTd9TtJguJ14Lu3feth", + }, + } + for _, testDef := range testDefs { + addr, err := NewByronAddressFromParts( + testDef.addressType, + testDef.paymentAddr, + testDef.addressAttr, + ) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if addr.String() != testDef.expectedAddress { + t.Fatalf( + "address did not match expected value, got: %s, wanted: %s", + addr.String(), + testDef.expectedAddress, + ) + } + } +} + func TestAddressPaymentAddress(t *testing.T) { testDefs := []struct { address string