From 0745027613699155a441e8ef891b7acbbcfbe9ce Mon Sep 17 00:00:00 2001 From: Aurora Gaffney Date: Fri, 22 Mar 2024 15:09:35 -0500 Subject: [PATCH] feat: localstatequery stake-related queries (#557) This adds support for the StakeDistribution, StakePools, and StakePoolParams queries. Fixes #553 --- cmd/gouroboros/query.go | 27 ++++++ ledger/common.go | 34 ++++++++ protocol/localstatequery/client.go | 11 ++- protocol/localstatequery/queries.go | 124 ++++++++++++++++++++-------- 4 files changed, 155 insertions(+), 41 deletions(-) diff --git a/cmd/gouroboros/query.go b/cmd/gouroboros/query.go index caa0f390..fda1980b 100644 --- a/cmd/gouroboros/query.go +++ b/cmd/gouroboros/query.go @@ -163,6 +163,13 @@ func testQuery(f *globalFlags) { os.Exit(1) } fmt.Printf("stake-distribution: %#v\n", *stakeDistribution) + case "stake-pools": + stakePools, err := o.LocalStateQuery().Client.GetStakePools() + if err != nil { + fmt.Printf("ERROR: failure querying stake pools: %s\n", err) + os.Exit(1) + } + fmt.Printf("stake-pools: %#v\n", *stakePools) case "genesis-config": genesisConfig, err := o.LocalStateQuery().Client.GetGenesisConfig() if err != nil { @@ -170,6 +177,26 @@ func testQuery(f *globalFlags) { os.Exit(1) } fmt.Printf("genesis-config: %#v\n", *genesisConfig) + case "pool-params": + var tmpPools []ledger.PoolId + if len(queryFlags.flagset.Args()) <= 1 { + fmt.Println("No pools specified") + os.Exit(1) + } + for _, pool := range queryFlags.flagset.Args()[1:] { + tmpPoolId, err := ledger.NewPoolIdFromBech32(pool) + if err != nil { + fmt.Printf("Invalid bech32 pool ID %q: %s", pool, err) + os.Exit(1) + } + tmpPools = append(tmpPools, tmpPoolId) + } + poolParams, err := o.LocalStateQuery().Client.GetStakePoolParams(tmpPools) + if err != nil { + fmt.Printf("ERROR: failure querying stake pool params: %s\n", err) + os.Exit(1) + } + fmt.Printf("pool-params: %#v\n", *poolParams) case "utxos-by-address": var tmpAddrs []ledger.Address if len(queryFlags.flagset.Args()) <= 1 { diff --git a/ledger/common.go b/ledger/common.go index dc051092..3c6db37a 100644 --- a/ledger/common.go +++ b/ledger/common.go @@ -187,6 +187,40 @@ func (a AssetFingerprint) String() string { return encoded } +type PoolId [28]byte + +func NewPoolIdFromBech32(poolId string) (PoolId, error) { + var p PoolId + _, data, err := bech32.DecodeNoLimit(poolId) + if err != nil { + return p, err + } + decoded, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return p, err + } + if len(decoded) != len(p) { + return p, fmt.Errorf("invalid pool ID length: %d", len(decoded)) + } + p = PoolId(decoded) + return p, err +} + +func (p PoolId) String() string { + // Convert data to base32 and encode as bech32 + convData, err := bech32.ConvertBits(p[:], 8, 5, true) + if err != nil { + panic( + fmt.Sprintf("unexpected error converting data to base32: %s", err), + ) + } + encoded, err := bech32.Encode("pool", convData) + if err != nil { + panic(fmt.Sprintf("unexpected error encoding data as bech32: %s", err)) + } + return encoded +} + const ( AddressHeaderTypeMask = 0xF0 AddressHeaderNetworkMask = 0x0F diff --git a/protocol/localstatequery/client.go b/protocol/localstatequery/client.go index b38fd7ec..fc54ace9 100644 --- a/protocol/localstatequery/client.go +++ b/protocol/localstatequery/client.go @@ -590,12 +590,8 @@ func (c *Client) GetStakePools() (*StakePoolsResult, error) { return &result, nil } -// TODO -/* -query [17 #6.258([*poolid]) -*/ func (c *Client) GetStakePoolParams( - poolIds []interface{}, + poolIds []ledger.PoolId, ) (*StakePoolParamsResult, error) { c.busyMutex.Lock() defer c.busyMutex.Unlock() @@ -606,7 +602,10 @@ func (c *Client) GetStakePoolParams( query := buildShelleyQuery( currentEra, QueryTypeShelleyStakePoolParams, - // TODO: add params + cbor.Tag{ + Number: cbor.CborTagSet, + Content: poolIds, + }, ) var result StakePoolParamsResult if err := c.runQuery(query, &result); err != nil { diff --git a/protocol/localstatequery/queries.go b/protocol/localstatequery/queries.go index 8fe69c07..49d8c306 100644 --- a/protocol/localstatequery/queries.go +++ b/protocol/localstatequery/queries.go @@ -16,6 +16,7 @@ package localstatequery import ( "fmt" + "net" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger" @@ -156,11 +157,14 @@ type CurrentProtocolParamsResult interface { // TODO type ProposedProtocolParamsUpdatesResult interface{} -// TODO -/* -result [{ *poolid => [[num den] vrf-hash]}] num/den is the quotient representing the stake fractions -*/ -type StakeDistributionResult interface{} +type StakeDistributionResult struct { + cbor.StructAsArray + Results map[ledger.PoolId]struct { + cbor.StructAsArray + StakeFraction *cbor.Rat + VrfHash ledger.Blake2b256 + } +} type UTxOByAddressResult struct { cbor.StructAsArray @@ -295,37 +299,87 @@ type UTxOByTxInResult struct { Results map[UtxoId]ledger.BabbageTransactionOutput } -// TODO -/* -result [#6.258([ *poolid ])] -*/ -type StakePoolsResult interface{} +type StakePoolsResult struct { + cbor.StructAsArray + Results []ledger.PoolId +} -// TODO -/* -result [{ *poolid => [ *pool_param ] }] -pool_param CDDL Comment -operator keyhash -vrf_keyhash keyhash -pledge coin -margin #6.30([num den]) -reward_account -pool_owners set -relays [ *relay ] -pool_metadata pool_metadata/null -relay CDDL Comment -single_host_addr [0 port/null ipv4/null ipv6/null] -single_host_name [1 port/null dns_name] An A or AAAA DNS -multi_host_name [2 dns_name] A SRV DNS record -Type CDDL Comment -port uint .le 65535 -ipv4 bytes .size 4 -ipv6 bytes .size 16 -dns_name tstr .size (0..64) -pool_metadata [url metadata_hash] -url tstr .size (0..64) -*/ -type StakePoolParamsResult interface{} +type StakePoolParamsResult struct { + cbor.StructAsArray + Results map[ledger.PoolId]struct { + cbor.StructAsArray + Operator ledger.Blake2b224 + VrfKeyHash ledger.Blake2b256 + Pledge uint + FixedCost uint + Margin *cbor.Rat + RewardAccount ledger.Address + PoolOwners []ledger.Blake2b224 + Relays []StakePoolParamsResultRelay + PoolMetadata *struct { + cbor.StructAsArray + Url string + MetadataHash ledger.Blake2b256 + } + } +} + +type StakePoolParamsResultRelay struct { + Type int + Port *uint + Ipv4 *net.IP + Ipv6 *net.IP + Hostname *string +} + +func (s *StakePoolParamsResultRelay) UnmarshalCBOR(data []byte) error { + tmpId, err := cbor.DecodeIdFromList(data) + if err != nil { + return err + } + s.Type = tmpId + switch tmpId { + case 0: + var tmpData struct { + cbor.StructAsArray + Type uint + Port *uint + Ipv4 *net.IP + Ipv6 *net.IP + } + if _, err := cbor.Decode(data, &tmpData); err != nil { + return err + } + s.Port = tmpData.Port + s.Ipv4 = tmpData.Ipv4 + s.Ipv6 = tmpData.Ipv6 + case 1: + var tmpData struct { + cbor.StructAsArray + Type uint + Port *uint + Hostname *string + } + if _, err := cbor.Decode(data, &tmpData); err != nil { + return err + } + s.Port = tmpData.Port + s.Hostname = tmpData.Hostname + case 2: + var tmpData struct { + cbor.StructAsArray + Type uint + Hostname *string + } + if _, err := cbor.Decode(data, &tmpData); err != nil { + return err + } + s.Hostname = tmpData.Hostname + default: + return fmt.Errorf("invalid relay type: %d", tmpId) + } + return nil +} // TODO type RewardInfoPoolsResult interface{}