Skip to content

Commit 59474c6

Browse files
authored
Merge pull request #36 from algorandfoundation/fix/show-expired-keys
fix: show expired keys
2 parents 5963f82 + 7819f6a commit 59474c6

File tree

8 files changed

+209
-150
lines changed

8 files changed

+209
-150
lines changed

cmd/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ var (
9797
Client: client,
9898
Context: ctx,
9999
}
100-
state.Accounts = internal.AccountsFromState(&state, new(internal.Clock), client)
101-
100+
state.Accounts, err = internal.AccountsFromState(&state, new(internal.Clock), client)
101+
cobra.CheckErr(err)
102102
// Fetch current state
103103
err = state.Status.Fetch(ctx, client, new(internal.HttpPkg))
104104
cobra.CheckErr(err)

internal/accounts.go

Lines changed: 101 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package internal
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
@@ -15,6 +16,8 @@ type Account struct {
1516
Participation *api.AccountParticipation
1617
// IncentiveEligible determines the minimum fee
1718
IncentiveEligible bool
19+
// NonResidentKey finds an online account that is missing locally
20+
NonResidentKey bool
1821
// Account Address is the algorand encoded address
1922
Address string
2023
// Status is the Online/Offline/"NotParticipating" status of the account
@@ -26,10 +29,10 @@ type Account struct {
2629
// A count of how many participation Keys exist on this node for this Account
2730
Keys int
2831
// Expires is the date the participation key will expire
29-
Expires time.Time
32+
Expires *time.Time
3033
}
3134

32-
// Get Online Status of Account
35+
// GetAccount status of api.Account
3336
func GetAccount(client api.ClientWithResponsesInterface, address string) (api.Account, error) {
3437
var format api.AccountInformationParamsFormat = "json"
3538
r, err := client.AccountInformationWithResponse(
@@ -51,73 +54,117 @@ func GetAccount(client api.ClientWithResponsesInterface, address string) (api.Ac
5154
return *r.JSON200, nil
5255
}
5356

54-
func GetExpiresTime(t Time, key api.ParticipationKey, state *StateModel) time.Time {
57+
// GetExpiresTime calculates and returns the expiration time for a participation key based on the current account state.
58+
func GetExpiresTime(t Time, lastRound int, roundTime time.Duration, account Account) *time.Time {
5559
now := t.Now()
56-
var expires = now.Add(-(time.Hour * 24 * 365 * 100))
57-
if key.LastBlockProposal != nil && state.Status.LastRound != 0 && state.Metrics.RoundTime != 0 {
58-
roundDiff := max(0, *key.EffectiveLastValid-int(state.Status.LastRound))
59-
distance := int(state.Metrics.RoundTime) * roundDiff
60+
var expires time.Time
61+
if account.Status == "Online" &&
62+
account.Participation != nil &&
63+
lastRound != 0 &&
64+
roundTime != 0 {
65+
roundDiff := max(0, account.Participation.VoteLastValid-int(lastRound))
66+
distance := int(roundTime) * roundDiff
6067
expires = now.Add(time.Duration(distance))
68+
return &expires
6169
}
62-
return expires
70+
return nil
6371
}
6472

65-
// AccountsFromParticipationKeys maps an array of api.ParticipationKey to a keyed map of Account
66-
func AccountsFromState(state *StateModel, t Time, client api.ClientWithResponsesInterface) map[string]Account {
67-
values := make(map[string]Account)
68-
if state == nil || state.ParticipationKeys == nil {
69-
return values
70-
}
71-
for _, key := range *state.ParticipationKeys {
72-
val, ok := values[key.Address]
73-
if !ok {
74-
var account = api.Account{
75-
Address: key.Address,
76-
Status: "Unknown",
77-
IncentiveEligible: nil,
78-
Amount: 0,
79-
}
80-
if state.Status.State != SyncingState {
81-
var err error
82-
account, err = GetAccount(client, key.Address)
83-
// TODO: handle error
84-
if err != nil {
85-
// TODO: Logging
86-
panic(err)
87-
}
88-
}
73+
// ParticipationKeysToAccounts converts a slice of ParticipationKey objects into a map of Account objects.
74+
// The keys parameter is a slice of pointers to ParticipationKey instances.
75+
// The prev parameter is an optional map that allows merging of existing accounts with new ones.
76+
// Returns a map where each key is an address from a ParticipationKey, and the value is a corresponding Account.
77+
func ParticipationKeysToAccounts(keys *[]api.ParticipationKey) map[string]Account {
78+
// Allow merging of existing accounts
79+
var accounts = make(map[string]Account)
8980

90-
// Check for eligibility
91-
var incentiveEligible = false
92-
if account.IncentiveEligible == nil {
93-
incentiveEligible = false
94-
} else {
95-
incentiveEligible = *account.IncentiveEligible
96-
}
81+
// Must have keys to process
82+
if keys == nil {
83+
return accounts
84+
}
9785

98-
values[key.Address] = Account{
99-
Participation: account.Participation,
86+
// Add missing Accounts
87+
for _, key := range *keys {
88+
if _, ok := accounts[key.Address]; !ok {
89+
accounts[key.Address] = Account{
90+
Participation: nil,
91+
IncentiveEligible: false,
10092
Address: key.Address,
101-
Status: account.Status,
102-
Balance: account.Amount / 1000000,
103-
Expires: GetExpiresTime(t, key, state),
104-
IncentiveEligible: incentiveEligible,
93+
Status: "Unknown",
94+
Balance: 0,
10595
Keys: 1,
96+
Expires: nil,
10697
}
10798
} else {
108-
val.Keys++
109-
if val.Expires.Before(t.Now()) {
110-
now := t.Now()
111-
var expires = GetExpiresTime(t, key, state)
112-
if !expires.Before(now) {
113-
val.Expires = expires
114-
}
99+
acct := accounts[key.Address]
100+
acct.Keys++
101+
accounts[key.Address] = acct
102+
}
103+
}
104+
return accounts
105+
}
106+
107+
func UpdateAccountFromRPC(account Account, rpcAccount api.Account) Account {
108+
account.Status = rpcAccount.Status
109+
account.Balance = rpcAccount.Amount / 1000000
110+
account.Participation = rpcAccount.Participation
111+
112+
var incentiveEligible = false
113+
if rpcAccount.IncentiveEligible == nil {
114+
incentiveEligible = false
115+
} else {
116+
incentiveEligible = *rpcAccount.IncentiveEligible
117+
}
118+
119+
account.IncentiveEligible = incentiveEligible
120+
121+
return account
122+
}
123+
124+
func IsParticipationKeyActive(part api.ParticipationKey, account api.AccountParticipation) bool {
125+
var equal = false
126+
if bytes.Equal(part.Key.VoteParticipationKey, account.VoteParticipationKey) &&
127+
part.Key.VoteLastValid == account.VoteLastValid &&
128+
part.Key.VoteFirstValid == account.VoteFirstValid {
129+
equal = true
130+
}
131+
return equal
132+
}
133+
134+
func UpdateAccountExpiredTime(t Time, account Account, state *StateModel) Account {
135+
var nonResidentKey = true
136+
for _, key := range *state.ParticipationKeys {
137+
// We have the key locally, update the residency
138+
if account.Status == "Offline" || (key.Address == account.Address && account.Participation != nil && IsParticipationKeyActive(key, *account.Participation)) {
139+
nonResidentKey = false
140+
}
141+
}
142+
account.NonResidentKey = nonResidentKey
143+
account.Expires = GetExpiresTime(t, int(state.Status.LastRound), state.Metrics.RoundTime, account)
144+
return account
145+
}
146+
147+
// AccountsFromState maps an array of api.ParticipationKey to a keyed map of Account
148+
func AccountsFromState(state *StateModel, t Time, client api.ClientWithResponsesInterface) (map[string]Account, error) {
149+
if state == nil {
150+
return make(map[string]Account), nil
151+
}
152+
153+
accounts := ParticipationKeysToAccounts(state.ParticipationKeys)
154+
155+
for _, acct := range accounts {
156+
// For each account, update the data from the RPC endpoint
157+
if state.Status.State != SyncingState {
158+
rpcAcct, err := GetAccount(client, acct.Address)
159+
if err != nil {
160+
return nil, err
115161
}
116-
values[key.Address] = val
162+
accounts[acct.Address] = UpdateAccountFromRPC(acct, rpcAcct)
163+
accounts[acct.Address] = UpdateAccountExpiredTime(t, accounts[acct.Address], state)
117164
}
118165
}
119166

120-
return values
167+
return accounts, nil
121168
}
122169

123170
func ValidateAddress(address string) bool {

internal/accounts_test.go

Lines changed: 16 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -62,66 +62,6 @@ func Test_AccountsFromState(t *testing.T) {
6262
t.Fatal("Expected error for invalid address")
6363
}
6464

65-
// Test Account from State
66-
67-
effectiveFirstValid := 0
68-
effectiveLastValid := 10000
69-
lastProposedRound := 1336
70-
// Create mockedPart Keys
71-
var mockedPartKeys = []api.ParticipationKey{
72-
{
73-
Address: onlineAccounts[0].Address,
74-
EffectiveFirstValid: &effectiveFirstValid,
75-
EffectiveLastValid: &effectiveLastValid,
76-
Id: "",
77-
Key: api.AccountParticipation{
78-
SelectionParticipationKey: nil,
79-
StateProofKey: nil,
80-
VoteParticipationKey: nil,
81-
VoteFirstValid: 0,
82-
VoteLastValid: 9999999,
83-
VoteKeyDilution: 0,
84-
},
85-
LastBlockProposal: &lastProposedRound,
86-
LastStateProof: nil,
87-
LastVote: nil,
88-
},
89-
{
90-
Address: onlineAccounts[0].Address,
91-
EffectiveFirstValid: nil,
92-
EffectiveLastValid: nil,
93-
Id: "",
94-
Key: api.AccountParticipation{
95-
SelectionParticipationKey: nil,
96-
StateProofKey: nil,
97-
VoteParticipationKey: nil,
98-
VoteFirstValid: 0,
99-
VoteLastValid: 9999999,
100-
VoteKeyDilution: 0,
101-
},
102-
LastBlockProposal: nil,
103-
LastStateProof: nil,
104-
LastVote: nil,
105-
},
106-
{
107-
Address: onlineAccounts[1].Address,
108-
EffectiveFirstValid: &effectiveFirstValid,
109-
EffectiveLastValid: &effectiveLastValid,
110-
Id: "",
111-
Key: api.AccountParticipation{
112-
SelectionParticipationKey: nil,
113-
StateProofKey: nil,
114-
VoteParticipationKey: nil,
115-
VoteFirstValid: 0,
116-
VoteLastValid: 9999999,
117-
VoteKeyDilution: 0,
118-
},
119-
LastBlockProposal: &lastProposedRound,
120-
LastStateProof: nil,
121-
LastVote: nil,
122-
},
123-
}
124-
12565
// Mock StateModel
12666
state := &StateModel{
12767
Metrics: MetricsModel{
@@ -140,39 +80,35 @@ func Test_AccountsFromState(t *testing.T) {
14080
NeedsUpdate: false,
14181
LastRound: 1337,
14282
},
143-
ParticipationKeys: &mockedPartKeys,
83+
ParticipationKeys: &mock.Keys,
14484
}
14585

14686
// Calculate expiration
14787
clock := new(mock.Clock)
14888
now := clock.Now()
149-
roundDiff := max(0, effectiveLastValid-int(state.Status.LastRound))
89+
roundDiff := max(0, mock.Keys[0].Key.VoteLastValid-int(state.Status.LastRound))
15090
distance := int(state.Metrics.RoundTime) * roundDiff
15191
expires := now.Add(time.Duration(distance))
152-
92+
tClient := test.GetClient(false)
93+
acct, _ = GetAccount(tClient, "ABC")
15394
// Construct expected accounts
15495
expectedAccounts := map[string]Account{
155-
onlineAccounts[0].Address: {
156-
Participation: onlineAccounts[0].Participation,
157-
Address: onlineAccounts[0].Address,
158-
Status: onlineAccounts[0].Status,
159-
Balance: onlineAccounts[0].Amount / 1_000_000,
160-
Keys: 2,
161-
Expires: expires,
162-
},
163-
onlineAccounts[1].Address: {
164-
Participation: onlineAccounts[1].Participation,
165-
Address: onlineAccounts[1].Address,
166-
Status: onlineAccounts[1].Status,
167-
Balance: onlineAccounts[1].Amount / 1_000_000,
168-
Keys: 1,
169-
Expires: expires,
96+
"ABC": {
97+
Participation: acct.Participation,
98+
Address: acct.Address,
99+
Status: acct.Status,
100+
IncentiveEligible: true,
101+
Balance: acct.Amount / 1_000_000,
102+
Keys: 2,
103+
Expires: &expires,
170104
},
171105
}
172106

173107
// Call AccountsFromState
174-
accounts := AccountsFromState(state, clock, client)
175-
108+
accounts, err := AccountsFromState(state, clock, tClient)
109+
if err != nil {
110+
t.Fatal(err)
111+
}
176112
// Assert results
177113
assert.Equal(t, expectedAccounts, accounts)
178114

internal/state.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,10 @@ func (s *StateModel) UpdateMetricsFromRPC(ctx context.Context, client api.Client
125125
s.Metrics.LastRX = res["algod_network_received_bytes_total"]
126126
}
127127
}
128-
func (s *StateModel) UpdateAccounts() {
129-
s.Accounts = AccountsFromState(s, new(Clock), s.Client)
128+
func (s *StateModel) UpdateAccounts() error {
129+
var err error
130+
s.Accounts, err = AccountsFromState(s, new(Clock), s.Client)
131+
return err
130132
}
131133

132134
func (s *StateModel) UpdateKeys() {
@@ -137,6 +139,9 @@ func (s *StateModel) UpdateKeys() {
137139
}
138140
if err == nil {
139141
s.Admin = true
140-
s.UpdateAccounts()
142+
err = s.UpdateAccounts()
143+
if err != nil {
144+
// TODO: Handle error
145+
}
141146
}
142147
}

internal/test/client.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,18 @@ func (c *Client) DeleteParticipationKeyByIDWithResponse(ctx context.Context, par
156156
return &res, nil
157157
}
158158

159+
func (c *Client) AccountInformationWithResponse(ctx context.Context, address string, params *api.AccountInformationParams, reqEditors ...api.RequestEditorFn) (*api.AccountInformationResponse, error) {
160+
httpResponse := http.Response{StatusCode: 200}
161+
return &api.AccountInformationResponse{
162+
Body: nil,
163+
HTTPResponse: &httpResponse,
164+
JSON200: &mock.ABCAccount,
165+
JSON400: nil,
166+
JSON401: nil,
167+
JSON500: nil,
168+
}, nil
169+
}
170+
159171
func (c *Client) GenerateParticipationKeysWithResponse(ctx context.Context, address string, params *api.GenerateParticipationKeysParams, reqEditors ...api.RequestEditorFn) (*api.GenerateParticipationKeysResponse, error) {
160172
mock.Keys = append(mock.Keys, api.ParticipationKey{
161173
Address: "ABC",

0 commit comments

Comments
 (0)