From 608f4126c95606e885afb1ccfc3b7054d5cacf0a Mon Sep 17 00:00:00 2001 From: Aurora Gaffney Date: Mon, 6 Jan 2025 14:53:07 -0600 Subject: [PATCH] feat: create genesis UTxOs from Byron genesis config --- ledger/byron/genesis.go | 97 +++++++++++++++++++++++++++++++++++- ledger/byron/genesis_test.go | 86 +++++++++++++++++++++++++++++++- ledger/common/address.go | 33 ++++++++++++ 3 files changed, 213 insertions(+), 3 deletions(-) diff --git a/ledger/byron/genesis.go b/ledger/byron/genesis.go index 627dd916..ef0eaaa0 100644 --- a/ledger/byron/genesis.go +++ b/ledger/byron/genesis.go @@ -15,9 +15,15 @@ package byron import ( + "encoding/base64" "encoding/json" "io" "os" + "slices" + "strconv" + + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/ledger/common" ) type ByronGenesis struct { @@ -28,7 +34,7 @@ type ByronGenesis struct { StartTime int BootStakeholders map[string]int HeavyDelegation map[string]ByronGenesisHeavyDelegation - NonAvvmBalances any + NonAvvmBalances map[string]string VssCerts map[string]ByronGenesisVssCert } @@ -81,6 +87,95 @@ type ByronGenesisVssCert struct { VssKey string } +func (g *ByronGenesis) GenesisUtxos() ([]common.Utxo, error) { + avvmUtxos, err := g.avvmUtxos() + if err != nil { + return nil, err + } + nonAvvmUtxos, err := g.nonAvvmUtxos() + if err != nil { + return nil, err + } + ret := slices.Concat( + avvmUtxos, + nonAvvmUtxos, + ) + return ret, nil +} + +func (g *ByronGenesis) avvmUtxos() ([]common.Utxo, error) { + var ret []common.Utxo + for pubkey, amount := range g.AvvmDistr { + // Build address from redeem pubkey + pubkeyBytes, err := base64.URLEncoding.DecodeString(pubkey) + if err != nil { + return nil, err + } + tmpAddr, err := common.NewByronAddressRedeem( + pubkeyBytes, + // XXX: do we need to specify the network ID? + common.ByronAddressAttributes{}, + ) + if err != nil { + return nil, err + } + tmpAmount, err := strconv.ParseUint(amount, 10, 64) + if err != nil { + return nil, err + } + addrBytes, err := cbor.Encode(tmpAddr) + if err != nil { + return nil, err + } + ret = append( + ret, + common.Utxo{ + Id: ByronTransactionInput{ + TxId: common.Blake2b256Hash(addrBytes), + OutputIndex: 0, + }, + Output: ByronTransactionOutput{ + OutputAddress: tmpAddr, + OutputAmount: tmpAmount, + }, + }, + ) + } + return ret, nil +} + +func (g *ByronGenesis) nonAvvmUtxos() ([]common.Utxo, error) { + var ret []common.Utxo + for address, amount := range g.NonAvvmBalances { + tmpAddr, err := common.NewAddress(address) + if err != nil { + return nil, err + } + tmpAmount, err := strconv.ParseUint(amount, 10, 64) + if err != nil { + return nil, err + } + addrBytes, err := cbor.Encode(tmpAddr) + if err != nil { + return nil, err + } + ret = append( + ret, + common.Utxo{ + Id: ByronTransactionInput{ + TxId: common.Blake2b256Hash(addrBytes), + OutputIndex: 0, + }, + Output: ByronTransactionOutput{ + OutputAddress: tmpAddr, + OutputAmount: tmpAmount, + }, + }, + ) + } + return ret, nil +} + func NewByronGenesisFromReader(r io.Reader) (ByronGenesis, error) { var ret ByronGenesis dec := json.NewDecoder(r) diff --git a/ledger/byron/genesis_test.go b/ledger/byron/genesis_test.go index 182c0aff..5fadadf9 100644 --- a/ledger/byron/genesis_test.go +++ b/ledger/byron/genesis_test.go @@ -15,7 +15,9 @@ package byron_test import ( + "encoding/json" "reflect" + "strconv" "strings" "testing" @@ -97,7 +99,6 @@ const byronGenesisConfig = ` ` var expectedGenesisObj = byron.ByronGenesis{ - // TODO AvvmDistr: map[string]string{ "-0BJDi-gauylk4LptQTgjMeo7kY9lTCbZv12vwOSTZk=": "9999300000000", "-0Np4pyTOWF26iXWVIvu6fhz9QupwWRS2hcCaOEYlw0=": "3760024000000", @@ -188,7 +189,7 @@ var expectedGenesisObj = byron.ByronGenesis{ Omega: 0, }, }, - NonAvvmBalances: map[string]interface{}{}, + NonAvvmBalances: map[string]string{}, VssCerts: map[string]byron.ByronGenesisVssCert{ "0efd6f3b2849d5baf25b3e2bf2d46f88427b4e455fc3dc43f57819c5": { ExpiryEpoch: 5, @@ -250,3 +251,84 @@ func TestGenesisFromJson(t *testing.T) { ) } } + +func TestGenesisNonAvvmUtxos(t *testing.T) { + testAddr := "FHnt4NL7yPXvDWHa8bVs73UEUdJd64VxWXSFNqetECtYfTd9TtJguJ14Lu3feth" + testAmount := uint64(30_000_000_000_000_000) + expectedTxId := "4843cf2e582b2f9ce37600e5ab4cc678991f988f8780fed05407f9537f7712bd" + // Generate genesis config JSON + tmpGenesisData := map[string]any{ + "nonAvvmBalances": map[string]string{ + testAddr: strconv.FormatUint(testAmount, 10), + }, + } + tmpGenesisJson, err := json.Marshal(tmpGenesisData) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + // Parse genesis config JSON + tmpGenesis, err := byron.NewByronGenesisFromReader( + strings.NewReader(string(tmpGenesisJson)), + ) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tmpGenesisUtxos, err := tmpGenesis.GenesisUtxos() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if len(tmpGenesisUtxos) != 1 { + t.Fatalf("did not get expected count of genesis UTxOs") + } + tmpUtxo := tmpGenesisUtxos[0] + if tmpUtxo.Id.Id().String() != expectedTxId { + t.Fatalf("did not get expected TxID: got %s, wanted %s", tmpUtxo.Id.Id().String(), expectedTxId) + } + if tmpUtxo.Output.Address().String() != testAddr { + t.Fatalf("did not get expected address: got %s, wanted %s", tmpUtxo.Output.Address().String(), testAddr) + } + if tmpUtxo.Output.Amount() != testAmount { + t.Fatalf("did not get expected amount: got %d, wanted %d", tmpUtxo.Output.Amount(), testAmount) + } +} + +func TestGenesisAvvmUtxos(t *testing.T) { + testPubkey := "URVk8FxX6Ik9z-Cub09oOxMkp6FwNq27kJUXbjJnfsQ=" + testAmount := uint64(2463071701000000) + expectedTxId := "0ae3da29711600e94a33fb7441d2e76876a9a1e98b5ebdefbf2e3bc535617616" + expectedAddr := "Ae2tdPwUPEZKQuZh2UndEoTKEakMYHGNjJVYmNZgJk2qqgHouxDsA5oT83n" + // Generate genesis config JSON + tmpGenesisData := map[string]any{ + "avvmDistr": map[string]string{ + testPubkey: strconv.FormatUint(testAmount, 10), + }, + } + tmpGenesisJson, err := json.Marshal(tmpGenesisData) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + // Parse genesis config JSON + tmpGenesis, err := byron.NewByronGenesisFromReader( + strings.NewReader(string(tmpGenesisJson)), + ) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tmpGenesisUtxos, err := tmpGenesis.GenesisUtxos() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if len(tmpGenesisUtxos) != 1 { + t.Fatalf("did not get expected count of genesis UTxOs") + } + tmpUtxo := tmpGenesisUtxos[0] + if tmpUtxo.Id.Id().String() != expectedTxId { + t.Fatalf("did not get expected TxID: got %s, wanted %s", tmpUtxo.Id.Id().String(), expectedTxId) + } + if tmpUtxo.Output.Address().String() != expectedAddr { + t.Fatalf("did not get expected address: got %s, wanted %s", tmpUtxo.Output.Address().String(), expectedAddr) + } + if tmpUtxo.Output.Amount() != testAmount { + t.Fatalf("did not get expected amount: got %d, wanted %d", tmpUtxo.Output.Amount(), testAmount) + } +} diff --git a/ledger/common/address.go b/ledger/common/address.go index 7232cf8a..cad37c59 100644 --- a/ledger/common/address.go +++ b/ledger/common/address.go @@ -22,6 +22,7 @@ import ( "github.com/blinklabs-io/gouroboros/base58" "github.com/blinklabs-io/gouroboros/bech32" "github.com/blinklabs-io/gouroboros/cbor" + "golang.org/x/crypto/sha3" ) const ( @@ -135,6 +136,38 @@ func NewByronAddressFromParts( }, nil } +func NewByronAddressRedeem( + pubkey []byte, + attr ByronAddressAttributes, +) (Address, error) { + if len(pubkey) != 32 { + return Address{}, fmt.Errorf( + "invalid redeem pubkey length: %d", + len(pubkey), + ) + } + addrRoot := []any{ + ByronAddressTypeRedeem, + []any{ + ByronAddressTypeRedeem, + pubkey, + }, + attr, + } + addrRootBytes, err := cbor.Encode(addrRoot) + if err != nil { + return Address{}, err + } + sha3Sum := sha3.Sum256(addrRootBytes) + addrHash := Blake2b224Hash(sha3Sum[:]) + return Address{ + addressType: AddressTypeByron, + paymentAddress: addrHash.Bytes(), + byronAddressType: ByronAddressTypeRedeem, + byronAddressAttr: attr, + }, nil +} + func (a *Address) populateFromBytes(data []byte) error { // Extract header info header := data[0]