Skip to content

Commit e3d00f1

Browse files
committed
fix(localstatequery): support stake address info queries
1 parent c7e54c7 commit e3d00f1

6 files changed

Lines changed: 223 additions & 5 deletions

File tree

ledger/common/certs.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,17 @@ type Drep struct {
142142
Credential []byte
143143
}
144144

145+
func (d Drep) MarshalCBOR() ([]byte, error) {
146+
switch d.Type {
147+
case DrepTypeAddrKeyHash, DrepTypeScriptHash:
148+
return cbor.Encode([]any{d.Type, d.Credential})
149+
case DrepTypeAbstain, DrepTypeNoConfidence:
150+
return cbor.Encode([]any{d.Type})
151+
default:
152+
return nil, fmt.Errorf("unknown drep type: %d", d.Type)
153+
}
154+
}
155+
145156
func (d *Drep) UnmarshalCBOR(data []byte) error {
146157
drepType, err := cbor.DecodeIdFromList(data)
147158
if err != nil {

ledger/common/certs_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
package common
1616

1717
import (
18+
"bytes"
1819
"encoding/hex"
1920
"encoding/json"
2021
"reflect"
2122
"strings"
2223
"testing"
2324

25+
"github.com/blinklabs-io/gouroboros/cbor"
2426
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
2528
)
2629

2730
// TestDrepString tests CIP-0129 bech32 encoding for DRep identifiers.
@@ -164,3 +167,20 @@ func TestDrepUnmarshalJson(t *testing.T) {
164167
}
165168
}
166169
}
170+
171+
func TestDrepCBORRoundTrip(t *testing.T) {
172+
testCases := []Drep{
173+
{Type: DrepTypeAddrKeyHash, Credential: make([]byte, 28)},
174+
{Type: DrepTypeScriptHash, Credential: bytes.Repeat([]byte{0xaa}, 28)},
175+
{Type: DrepTypeAbstain},
176+
{Type: DrepTypeNoConfidence},
177+
}
178+
for _, expected := range testCases {
179+
encoded, err := cbor.Encode(expected)
180+
require.NoError(t, err)
181+
var actual Drep
182+
_, err = cbor.Decode(encoded, &actual)
183+
require.NoError(t, err)
184+
require.Equal(t, expected, actual)
185+
}
186+
}

protocol/localstatequery/client.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,36 @@ func (c *Client) GetFilteredDelegationsAndRewardAccounts(
576576
return &result, nil
577577
}
578578

579+
// GetStakeDelegDeposits returns the deposits currently locked by the requested
580+
// registered stake credentials.
581+
func (c *Client) GetStakeDelegDeposits(
582+
creds []StakeCredential,
583+
) (*StakeDelegDepositsResult, error) {
584+
c.Protocol.Logger().
585+
Debug(fmt.Sprintf("calling GetStakeDelegDeposits(creds: %+v)", creds),
586+
"component", "network",
587+
"protocol", ProtocolName,
588+
"role", "client",
589+
"connection_id", c.callbackContext.ConnectionId.String(),
590+
)
591+
c.busyMutex.Lock()
592+
defer c.busyMutex.Unlock()
593+
currentEra, err := c.getCurrentEra()
594+
if err != nil {
595+
return nil, err
596+
}
597+
query := buildShelleyQuery(
598+
currentEra,
599+
QueryTypeShelleyStakeDelegDeposits,
600+
cbor.NewSetType(creds, true),
601+
)
602+
var result StakeDelegDepositsResult
603+
if err := c.runQuery(query, &result); err != nil {
604+
return nil, err
605+
}
606+
return &result, nil
607+
}
608+
579609
func (c *Client) GetGenesisConfig() (*GenesisConfigResult, error) {
580610
c.Protocol.Logger().
581611
Debug("calling GetGenesisConfig()",
@@ -1392,6 +1422,7 @@ func (c *Client) GetProposals() (*ProposalsResult, error) {
13921422
query := buildShelleyQuery(
13931423
currentEra,
13941424
QueryTypeShelleyGetProposals,
1425+
cbor.NewSetType([]lcommon.GovActionId{}, true),
13951426
)
13961427
var result ProposalsResult
13971428
if err := c.runQuery(query, &result); err != nil {

protocol/localstatequery/client_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,7 @@ func TestGetDRepStatePreConway(t *testing.T) {
943943
func TestGetProposalsEmpty(t *testing.T) {
944944
// Empty proposals list
945945
expectedResult := localstatequery.ProposalsResult{}
946-
cborData, err := cbor.Encode(expectedResult)
946+
cborData, err := cbor.Encode([]any{expectedResult})
947947
if err != nil {
948948
t.Fatalf("unexpected error encoding: %s", err)
949949
}
@@ -1100,7 +1100,7 @@ func TestGetProposalsSingleProposal(t *testing.T) {
11001100
ExpiresAfter: 110,
11011101
}
11021102
expectedResult := localstatequery.ProposalsResult{proposal}
1103-
cborData, err := cbor.Encode(expectedResult)
1103+
cborData, err := cbor.Encode([]any{expectedResult})
11041104
if err != nil {
11051105
t.Fatalf("unexpected error encoding: %s", err)
11061106
}
@@ -1211,7 +1211,7 @@ func TestFilteredVoteDelegateesResultDecode(t *testing.T) {
12111211
Credential: drepCredBytes,
12121212
},
12131213
}
1214-
cborData, err := cbor.Encode(encodableMap)
1214+
cborData, err := cbor.Encode([]any{encodableMap})
12151215
require.NoError(t, err, "unexpected error encoding map")
12161216

12171217
// Decode into FilteredVoteDelegateesResult
@@ -1247,7 +1247,7 @@ func TestGetFilteredVoteDelegatees(t *testing.T) {
12471247
Credential: drepCredBytes,
12481248
},
12491249
}
1250-
cborData, err := cbor.Encode(encodableMap)
1250+
cborData, err := cbor.Encode([]any{encodableMap})
12511251
require.NoError(t, err, "unexpected error encoding result")
12521252

12531253
conversation := append(

protocol/localstatequery/queries.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const (
6363
QueryTypeShelleyPoolState = 19
6464
QueryTypeShelleyStakeSnapshots = 20
6565
QueryTypeShelleyPoolDistr = 21
66+
QueryTypeShelleyStakeDelegDeposits = 22
6667

6768
// Conway governance queries (v8+)
6869
QueryTypeShelleyConstitution = 23
@@ -236,6 +237,7 @@ func shelleyQueryTypes() map[int]any {
236237
QueryTypeShelleyPoolState: &ShelleyPoolStateQuery{},
237238
QueryTypeShelleyStakeSnapshots: &ShelleyStakeSnapshotsQuery{},
238239
QueryTypeShelleyPoolDistr: &ShelleyPoolDistrQuery{},
240+
QueryTypeShelleyStakeDelegDeposits: &ShelleyStakeDelegDepositsQuery{},
239241
// Conway governance queries
240242
QueryTypeShelleyConstitution: &ShelleyConstitutionQuery{},
241243
QueryTypeShelleyGovState: &ShelleyGovStateQuery{},
@@ -377,6 +379,15 @@ type ShelleyFilteredDelegationAndRewardAccountsQuery struct {
377379
Creds cbor.SetType[StakeCredential]
378380
}
379381

382+
// ShelleyStakeDelegDepositsQuery returns the deposit locked by each requested
383+
// registered stake credential. It was added with node-to-client version 16 and
384+
// is used by cardano-cli query stake-address-info.
385+
type ShelleyStakeDelegDepositsQuery struct {
386+
cbor.StructAsArray
387+
Type int
388+
Creds cbor.SetType[StakeCredential]
389+
}
390+
380391
type ShelleyGenesisConfigQuery struct {
381392
simpleQueryBase
382393
}
@@ -741,6 +752,22 @@ type FilteredDelegationsAndRewardAccountsResult struct {
741752
Rewards map[StakeCredential]uint64
742753
}
743754

755+
// StakeDelegDepositsResult is the result of GetStakeDelegDeposits.
756+
// The map is wrapped in a one-element array by the Shelley era codec.
757+
type StakeDelegDepositsResult map[StakeCredential]uint64
758+
759+
func (r *StakeDelegDepositsResult) UnmarshalCBOR(data []byte) error {
760+
var tmp struct {
761+
cbor.StructAsArray
762+
Deposits map[StakeCredential]uint64
763+
}
764+
if _, err := cbor.Decode(data, &tmp); err != nil {
765+
return err
766+
}
767+
*r = tmp.Deposits
768+
return nil
769+
}
770+
744771
func (r *FilteredDelegationsAndRewardAccountsResult) UnmarshalCBOR(data []byte) error {
745772
var tmp struct {
746773
cbor.StructAsArray
@@ -1099,7 +1126,9 @@ type ShelleyAccountStateQuery struct {
10991126
}
11001127

11011128
type ShelleyGetProposalsQuery struct {
1102-
simpleQueryBase
1129+
cbor.StructAsArray
1130+
Type int
1131+
ActionIds cbor.SetType[lcommon.GovActionId]
11031132
}
11041133

11051134
type ShelleyGetRatifyStateQuery struct {
@@ -1465,8 +1494,21 @@ type CommitteeMembersStateResult struct {
14651494
// FilteredVoteDelegateesResult represents the vote delegatees
14661495
// for stake credentials.
14671496
// The result maps stake credentials to their DRep delegations
1497+
// and is wrapped in a one-element array by the Shelley era codec.
14681498
type FilteredVoteDelegateesResult map[StakeCredential]lcommon.Drep
14691499

1500+
func (r *FilteredVoteDelegateesResult) UnmarshalCBOR(data []byte) error {
1501+
var tmp struct {
1502+
cbor.StructAsArray
1503+
Delegatees map[StakeCredential]lcommon.Drep
1504+
}
1505+
if _, err := cbor.Decode(data, &tmp); err != nil {
1506+
return err
1507+
}
1508+
*r = tmp.Delegatees
1509+
return nil
1510+
}
1511+
14701512
// SPOStakeDistrResult represents the SPO stake distribution for governance
14711513
// Maps pool IDs to their governance voting power
14721514
type SPOStakeDistrResult struct {
@@ -1476,8 +1518,21 @@ type SPOStakeDistrResult struct {
14761518

14771519
// ProposalsResult represents the result of a GetProposals query.
14781520
// It contains a list of governance action states for all active proposals.
1521+
// The list is wrapped in a one-element array by the Shelley era codec.
14791522
type ProposalsResult []GovActionState
14801523

1524+
func (r *ProposalsResult) UnmarshalCBOR(data []byte) error {
1525+
var tmp struct {
1526+
cbor.StructAsArray
1527+
Proposals []GovActionState
1528+
}
1529+
if _, err := cbor.Decode(data, &tmp); err != nil {
1530+
return err
1531+
}
1532+
*r = tmp.Proposals
1533+
return nil
1534+
}
1535+
14811536
// EnactState represents the enactment state within a ratify state result.
14821537
// It contains the current committee, constitution, protocol parameters,
14831538
// treasury, withdrawals, and previous governance action IDs.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package localstatequery
16+
17+
import (
18+
"encoding/hex"
19+
"testing"
20+
21+
"github.com/blinklabs-io/gouroboros/cbor"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
// This is the MsgQuery shape emitted by cardano-cli for
26+
// query stake-address-info:
27+
//
28+
// MsgQuery(Block(Shelley(era=6, GetStakeDelegDeposits({key credential})))))
29+
//
30+
// Before query 22 was registered in shelleyQueryTypes, decoding this message
31+
// failed and the node-to-client connection closed without returning a result.
32+
func TestDecodeStakeDelegDepositsQuery(t *testing.T) {
33+
const payloadHex = "82038200820082068216d90102818200581c" +
34+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
35+
payload, err := hex.DecodeString(payloadHex)
36+
require.NoError(t, err)
37+
38+
msg, err := NewMsgFromCbor(MessageTypeQuery, payload)
39+
require.NoError(t, err)
40+
msgQuery, ok := msg.(*MsgQuery)
41+
require.True(t, ok)
42+
blockQuery, ok := msgQuery.Query.Query.(*BlockQuery)
43+
require.True(t, ok)
44+
shelleyQuery, ok := blockQuery.Query.(*ShelleyQuery)
45+
require.True(t, ok)
46+
require.Equal(t, uint(6), shelleyQuery.Era)
47+
query, ok := shelleyQuery.Query.(*ShelleyStakeDelegDepositsQuery)
48+
require.True(t, ok)
49+
require.Equal(t, QueryTypeShelleyStakeDelegDeposits, query.Type)
50+
require.Len(t, query.Creds.Items(), 1)
51+
require.Equal(t, uint64(0), query.Creds.Items()[0].Tag)
52+
require.Equal(
53+
t,
54+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
55+
hex.EncodeToString(query.Creds.Items()[0].Bytes[:]),
56+
)
57+
}
58+
59+
func TestDecodeGetProposalsQueryWithFilter(t *testing.T) {
60+
const payloadHex = "820382008200820682181fd9010280"
61+
payload, err := hex.DecodeString(payloadHex)
62+
require.NoError(t, err)
63+
64+
msg, err := NewMsgFromCbor(MessageTypeQuery, payload)
65+
require.NoError(t, err)
66+
msgQuery := msg.(*MsgQuery)
67+
blockQuery := msgQuery.Query.Query.(*BlockQuery)
68+
shelleyQuery := blockQuery.Query.(*ShelleyQuery)
69+
query, ok := shelleyQuery.Query.(*ShelleyGetProposalsQuery)
70+
require.True(t, ok)
71+
require.Equal(t, QueryTypeShelleyGetProposals, query.Type)
72+
require.Empty(t, query.ActionIds.Items())
73+
}
74+
75+
func TestDecodeFilteredDelegationsAndRewardAccountsResult(t *testing.T) {
76+
payload, err := hex.DecodeString("8182a0a0")
77+
require.NoError(t, err)
78+
79+
var result FilteredDelegationsAndRewardAccountsResult
80+
_, err = cbor.Decode(payload, &result)
81+
require.NoError(t, err)
82+
require.Empty(t, result.Delegations)
83+
require.Empty(t, result.Rewards)
84+
}
85+
86+
func TestDecodeStakeAddressInfoEraResults(t *testing.T) {
87+
var deposits StakeDelegDepositsResult
88+
_, err := cbor.Decode([]byte{0x81, 0xa0}, &deposits)
89+
require.NoError(t, err)
90+
require.Empty(t, deposits)
91+
92+
var delegatees FilteredVoteDelegateesResult
93+
_, err = cbor.Decode([]byte{0x81, 0xa0}, &delegatees)
94+
require.NoError(t, err)
95+
require.Empty(t, delegatees)
96+
97+
var proposals ProposalsResult
98+
_, err = cbor.Decode([]byte{0x81, 0x80}, &proposals)
99+
require.NoError(t, err)
100+
require.Empty(t, proposals)
101+
}

0 commit comments

Comments
 (0)