Skip to content

Commit

Permalink
Merge pull request #133 from algorandfoundation/fix/finding
Browse files Browse the repository at this point in the history
fix: deterministic state while detecting corrupt keys
  • Loading branch information
PhearZero authored Jan 23, 2025
2 parents 0c5afba + d87ce8c commit aaf752c
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 67 deletions.
37 changes: 37 additions & 0 deletions internal/algod/participation/participation.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,43 @@ func GenerateKeys(
}
}

type Diff struct {
VoteFirstValid bool
VoteLastValid bool
VoteKeyDilution bool
VoteParticipationKey bool
SelectionParticipationKey bool
StateProofKey bool
}

func boolToInt(input bool) int {
if input {
return 1
}
return 0

Check warning on line 118 in internal/algod/participation/participation.go

View check run for this annotation

Codecov / codecov/patch

internal/algod/participation/participation.go#L114-L118

Added lines #L114 - L118 were not covered by tests
}
func HasChanged(part api.ParticipationKey, account *api.AccountParticipation) (Diff, bool, int) {
diff := Diff{
VoteFirstValid: account.VoteFirstValid != part.Key.VoteFirstValid,
VoteLastValid: account.VoteLastValid != part.Key.VoteLastValid,
VoteKeyDilution: account.VoteKeyDilution != part.Key.VoteKeyDilution,
VoteParticipationKey: !bytes.Equal(account.VoteParticipationKey, part.Key.VoteParticipationKey),
SelectionParticipationKey: !bytes.Equal(account.SelectionParticipationKey, part.Key.SelectionParticipationKey),
StateProofKey: !bytes.Equal(*account.StateProofKey, *part.Key.StateProofKey),
}

// Count matches
fvMatch := boolToInt(diff.VoteFirstValid)
lvMatch := boolToInt(diff.VoteLastValid)
kdMatch := boolToInt(diff.VoteKeyDilution)
selMatch := boolToInt(diff.SelectionParticipationKey)
votMatch := boolToInt(diff.VoteParticipationKey)
spkMatch := boolToInt(diff.StateProofKey)
matchCount := fvMatch + lvMatch + kdMatch + selMatch + votMatch + spkMatch

return diff, diff.VoteFirstValid || diff.VoteLastValid || diff.VoteKeyDilution || diff.VoteParticipationKey || diff.SelectionParticipationKey || diff.StateProofKey, matchCount

Check warning on line 139 in internal/algod/participation/participation.go

View check run for this annotation

Codecov / codecov/patch

internal/algod/participation/participation.go#L120-L139

Added lines #L120 - L139 were not covered by tests
}

// Delete remove a key from the node
func Delete(ctx context.Context, client api.ClientWithResponsesInterface, participationId string) error {
deletion, err := client.DeleteParticipationKeyByIDWithResponse(ctx, participationId)
Expand Down
129 changes: 67 additions & 62 deletions ui/modal/controller.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package modal

import (
"bytes"
"fmt"
"github.com/algorandfoundation/nodekit/internal/algod"
"github.com/algorandfoundation/nodekit/internal/algod/participation"
Expand Down Expand Up @@ -32,7 +31,7 @@ func boolToInt(input bool) int {
}

// HandleMessage processes the given message, updates the ViewModel state, and returns any commands to execute.
func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
func (m *ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
Expand Down Expand Up @@ -76,66 +75,72 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
m.borderColor = "7"
m.controls = ""
m.title = "Fast Catchup"
// Return early, skip any checks
m.exceptionModal, cmd = m.exceptionModal.HandleMessage(msg)
return m, cmd
}

Check warning on line 81 in ui/modal/controller.go

View check run for this annotation

Codecov / codecov/patch

ui/modal/controller.go#L78-L81

Added lines #L78 - L81 were not covered by tests

// Get the existing account from the state
acct, ok := msg.Accounts[m.Address]

// Handle suspensions
if ok {
if acct.Participation != nil && acct.Status == "Offline" {
m.SetSuspended(true)
}

Check warning on line 90 in ui/modal/controller.go

View check run for this annotation

Codecov / codecov/patch

ui/modal/controller.go#L84-L90

Added lines #L84 - L90 were not covered by tests
}

// We found the account, and we are on one of the modals
if ok && m.Type == app.TransactionModal || m.Type == app.InfoModal {
// Make sure the transaction modal is set to the current address
if m.transactionModal.Participation != nil && m.transactionModal.Participation.Address == acct.Address {
// Actual State
isOnline := acct.Status == "Online"
isActive := acct.Participation != nil

// Derived State
isValid := isOnline && isActive
diff, isDifferent, count := participation.HasChanged(*m.transactionModal.Participation, acct.Participation)

// The account is valid and we registered
if isValid && !isDifferent && m.Type == app.TransactionModal && !m.transactionModal.Active {
m.SetActive(true)
m.infoModal.Prefix = "Successfully registered online!\n"
m.HasPrefix = true
m.SetType(app.InfoModal)
// For the love of all that is good, please lets refactor this. Preferably with a daemon
} else if isValid && isDifferent && count != 6 && (m.Type == app.InfoModal || (m.Type == app.TransactionModal && !m.transactionModal.Active)) {
// It is online, has a participation key but not the one we are looking at AND all the keys are not different
// (AND it's the info modal (this case we are checking on enter) OR we are waiting to register a key, and we made a mistake

// You know it's getting bad when the plugin recommendation is Grazie
// TODO: refactor this beast to have isolated state from the modal controller

} else if m.Type == app.TransactionModal && m.transactionModal.Participation != nil {
// Get the existing account from the state
acct, ok := msg.Accounts[m.Address]
// If the previous state is not active
if ok {
if !m.transactionModal.Active {
if acct.Participation != nil && acct.Status == "Online" {
// comparing values to detect corrupted/non-resident keys
fvMatch := boolToInt(acct.Participation.VoteFirstValid == m.transactionModal.Participation.Key.VoteFirstValid)
lvMatch := boolToInt(acct.Participation.VoteLastValid == m.transactionModal.Participation.Key.VoteLastValid)
kdMatch := boolToInt(acct.Participation.VoteKeyDilution == m.transactionModal.Participation.Key.VoteKeyDilution)
selMatch := boolToInt(bytes.Equal(acct.Participation.SelectionParticipationKey, m.transactionModal.Participation.Key.SelectionParticipationKey))
votMatch := boolToInt(bytes.Equal(acct.Participation.VoteParticipationKey, m.transactionModal.Participation.Key.VoteParticipationKey))
spkMatch := boolToInt(bytes.Equal(*acct.Participation.StateProofKey, *m.transactionModal.Participation.Key.StateProofKey))
matchCount := fvMatch + lvMatch + kdMatch + selMatch + votMatch + spkMatch
if matchCount == 6 {
m.SetActive(true)
m.infoModal.Active = true
m.infoModal.Prefix = "Successfully registered online!\n"
m.HasPrefix = true
m.SetType(app.InfoModal)
} else if matchCount >= 4 {
// We use 4 as the "non resident key" threshold here
// because it would be valid to re-reg with a key that has the same fv / lv / kd
// but it would trigger the non resident condition
// TOOD: refactor this beast to have {previous state} -> compare with next state
m.SetActive(true)
m.infoModal.Active = true
m.infoModal.Prefix = "***WARNING***\nRegistered online but keys do not fully match\nCheck your registered keys carefully against the node keys\n\n"
if fvMatch == 0 {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote First Valid\n"
}
if lvMatch == 0 {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Last Valid\n"
}
if kdMatch == 0 {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Key Dilution\n"
}
if votMatch == 0 {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Key\n"
}
if selMatch == 0 {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Selection Key\n"
}
if spkMatch == 0 {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: State Proof Key\n"
}
m.HasPrefix = true
m.SetType(app.InfoModal)
// Ahh yes, classic "Set Active to the inverse then only navigate when there is no prefix"
// This is the closest thing we have to state, between this and the transaction modal state it works
m.SetActive(false)
if m.infoModal.Prefix == "" {
m.infoModal.Prefix = "***WARNING***\nRegistered online but keys do not fully match\nCheck your registered keys carefully against the node keys\n\n"
if diff.VoteFirstValid {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote First Valid\n"
}
}
} else {
// TODO: This includes suspended keys, where Status == offline but .Participation is set
// Detect and display this
if acct.Participation == nil {
m.SetActive(false)
m.SetType(app.InfoModal)
} else {
m.SetSuspended()
if diff.VoteLastValid {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Last Valid\n"
}
if diff.VoteKeyDilution {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Key Dilution\n"
}
if diff.VoteParticipationKey {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Key\n"
}
if diff.SelectionParticipationKey {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Selection Key\n"
}
if diff.StateProofKey {
m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: State Proof Key\n"
}

Check warning on line 141 in ui/modal/controller.go

View check run for this annotation

Codecov / codecov/patch

ui/modal/controller.go#L94-L141

Added lines #L94 - L141 were not covered by tests
m.HasPrefix = true

Check warning on line 143 in ui/modal/controller.go

View check run for this annotation

Codecov / codecov/patch

ui/modal/controller.go#L143

Added line #L143 was not covered by tests
m.SetType(app.InfoModal)
}
}
Expand Down Expand Up @@ -223,7 +228,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
cmds = append(cmds, cmd)
m.exceptionModal, cmd = m.exceptionModal.HandleMessage(modalMsg)
cmds = append(cmds, cmd)
return &m, tea.Batch(cmds...)
return m, tea.Batch(cmds...)
}

// Only trigger modal commands when they are active
Expand All @@ -242,7 +247,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
}
cmds = append(cmds, cmd)

return &m, tea.Batch(cmds...)
return m, tea.Batch(cmds...)
}

// Update processes the given message, updates the ViewModel state, and returns the updated model and accompanying commands.
Expand Down
8 changes: 3 additions & 5 deletions ui/modal/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,10 @@ func (m *ViewModel) SetActive(active bool) {
}

// SetSuspended sets the active state to false both infoModal and transactionModal, and sets suspended state to true, also for both modals.
func (m *ViewModel) SetSuspended() {
m.infoModal.Active = false
m.infoModal.Suspended = true
func (m *ViewModel) SetSuspended(sus bool) {
m.infoModal.Suspended = sus
m.infoModal.UpdateState()
m.transactionModal.Active = false
m.transactionModal.Suspended = true
m.transactionModal.Suspended = sus
m.transactionModal.UpdateState()

Check warning on line 77 in ui/modal/model.go

View check run for this annotation

Codecov / codecov/patch

ui/modal/model.go#L73-L77

Added lines #L73 - L77 were not covered by tests
}

Expand Down

0 comments on commit aaf752c

Please sign in to comment.