Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: deterministic state while detecting corrupt keys #133

Merged
merged 2 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
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
}

// 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
}

// 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)
}
}
Comment on lines +87 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to else setsuspended false here? otherwise how is the suspended flag cleared after the user has re-reged online?


// 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"
}
m.HasPrefix = true

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()
}

Expand Down
Loading