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

feat: participation key integrity hash #111

Closed
wants to merge 2 commits into from
Closed
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
77 changes: 77 additions & 0 deletions internal/algod/participation/integrity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package participation

import (
"crypto/sha512"
"encoding/base32"
"encoding/binary"
"errors"
"github.com/algorand/go-algorand-sdk/v2/types"
"github.com/algorandfoundation/nodekit/api"
"strings"
)

// concat merges multiple byte slices into a single byte slice by appending all input slices into a single output slice.
func concat(slices [][]byte) []byte {
var all []byte
for _, s := range slices {
all = append(all, s...)
}
return all
}

// hash calculates a shortened base32-encoded SHA-512/256 hash of the input byte slice and returns it as a string.
func hash(rawBytes []byte) string {
hashBytes := sha512.Sum512_256(rawBytes)
return strings.Replace(base32.StdEncoding.EncodeToString(hashBytes[:8]), "===", "", 1)
}

// IntegrityHash computes a unique, deterministic hash based on a ParticipationKey, ensuring data integrity for validation.
// Returns a base32-encoded string and an error if the input data is invalid or hashing fails.
func IntegrityHash(partkey api.ParticipationKey) (string, error) {
address, err := types.DecodeAddress(partkey.Address)
if err != nil {
return "", err
}

encodedFV := make([]byte, 8)
binary.BigEndian.PutUint64(encodedFV, uint64(partkey.Key.VoteFirstValid))

encodedLV := make([]byte, 8)
binary.BigEndian.PutUint64(encodedLV, uint64(partkey.Key.VoteLastValid))

encodedVoteKeyDilution := make([]byte, 8)
binary.BigEndian.PutUint64(encodedVoteKeyDilution, uint64(partkey.Key.VoteKeyDilution))

// raw bytes
rawData := concat([][]byte{
address[:],
partkey.Key.SelectionParticipationKey[:],
partkey.Key.VoteParticipationKey[:],
*partkey.Key.StateProofKey,
encodedFV[:],
encodedLV[:],
encodedVoteKeyDilution[:],
})

if len(rawData) != 184 {
return "", errors.New("invalid raw data length")
}

Check warning on line 58 in internal/algod/participation/integrity.go

View check run for this annotation

Codecov / codecov/patch

internal/algod/participation/integrity.go#L57-L58

Added lines #L57 - L58 were not covered by tests

// Enchode
hashBytes := sha512.Sum512_256(rawData)
return strings.Replace(base32.StdEncoding.EncodeToString(hashBytes[:8]), "===", "", 1), nil
}

func OfflineHash(address string, network string) (string, error) {
addr, err := types.DecodeAddress(address)
if err != nil {
return "", err
}

Check warning on line 69 in internal/algod/participation/integrity.go

View check run for this annotation

Codecov / codecov/patch

internal/algod/participation/integrity.go#L68-L69

Added lines #L68 - L69 were not covered by tests

rawBytes := concat([][]byte{
addr[:],
[]byte(network),
})

return hash(rawBytes), nil
}
58 changes: 58 additions & 0 deletions internal/algod/participation/integrity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package participation

import (
"encoding/base64"
"github.com/algorandfoundation/nodekit/api"
"testing"
)

func Test_IntegrityHash(t *testing.T) {
account := "TUIDKH2C7MUHZDD77MAMUREJRKNK25SYXB7OAFA6JFBB24PEL5UX4S4GUU"
selectionKey, err := base64.StdEncoding.DecodeString("DM9cyZ0oLuVHDtVzhkhLIW06uE0J9faf6aL/FeLFj3o=")
if err != nil {
t.Fatal(err)
}
stateProofKey, err := base64.StdEncoding.DecodeString("+DAZBTXOletJxFUhEaYQaWaNs3Q4DLEwOlJ68gI8IGq9Ss/1szOimQiAt+f6lqk4FxEe/XvaAXkMbv2/9OiE1g==")
if err != nil {
t.Fatal(err)
}

voteKey, err := base64.StdEncoding.DecodeString("dHynahCuNWpeR9BcE+B8VE1GM/KdUj759k9ja8zNY30=")
if err != nil {
t.Fatal(err)
}

var fv = 47733256
var lv = 47861731
var kd = 359

var expectedHash = "4OAJOXKPLUQM2"

res, err := IntegrityHash(api.ParticipationKey{
Address: account,
EffectiveFirstValid: nil,
EffectiveLastValid: nil,
Id: "",
Key: api.AccountParticipation{
SelectionParticipationKey: selectionKey,
StateProofKey: &stateProofKey,
VoteFirstValid: fv,
VoteKeyDilution: kd,
VoteLastValid: lv,
VoteParticipationKey: voteKey,
},
LastBlockProposal: nil,
LastStateProof: nil,
LastVote: nil,
})
if res != expectedHash {
t.Error("expected", expectedHash, "got", res)
}

var expectedOfflineHash = "FFDBPNDG63S7C"
res, err = OfflineHash(account, "testnet")
if res != expectedOfflineHash {
t.Error("expected", expectedOfflineHash, "got", res)
}

}
39 changes: 39 additions & 0 deletions internal/test/mock/fixtures.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,48 @@
package mock

import (
"encoding/base64"
"github.com/algorandfoundation/nodekit/api"
)

func GetPartKey() (*api.ParticipationKey, error) {
account := "TUIDKH2C7MUHZDD77MAMUREJRKNK25SYXB7OAFA6JFBB24PEL5UX4S4GUU"
selectionKey, err := base64.StdEncoding.DecodeString("DM9cyZ0oLuVHDtVzhkhLIW06uE0J9faf6aL/FeLFj3o=")
if err != nil {
return nil, err
}

Check warning on line 13 in internal/test/mock/fixtures.go

View check run for this annotation

Codecov / codecov/patch

internal/test/mock/fixtures.go#L12-L13

Added lines #L12 - L13 were not covered by tests
stateProofKey, err := base64.StdEncoding.DecodeString("+DAZBTXOletJxFUhEaYQaWaNs3Q4DLEwOlJ68gI8IGq9Ss/1szOimQiAt+f6lqk4FxEe/XvaAXkMbv2/9OiE1g==")
if err != nil {
return nil, err
}

Check warning on line 17 in internal/test/mock/fixtures.go

View check run for this annotation

Codecov / codecov/patch

internal/test/mock/fixtures.go#L16-L17

Added lines #L16 - L17 were not covered by tests

voteKey, err := base64.StdEncoding.DecodeString("dHynahCuNWpeR9BcE+B8VE1GM/KdUj759k9ja8zNY30=")
if err != nil {
return nil, err
}

Check warning on line 22 in internal/test/mock/fixtures.go

View check run for this annotation

Codecov / codecov/patch

internal/test/mock/fixtures.go#L21-L22

Added lines #L21 - L22 were not covered by tests

var fv = 47733256
var lv = 47861731
var kd = 359
return &api.ParticipationKey{
Address: account,
EffectiveFirstValid: nil,
EffectiveLastValid: nil,
Id: "DRINJEQ6PN4GDQYE6ECJFRTVSCXZA4BUWNZMPFL6Q2MFTEFBPXGA",
Key: api.AccountParticipation{
SelectionParticipationKey: selectionKey,
StateProofKey: &stateProofKey,
VoteFirstValid: fv,
VoteKeyDilution: kd,
VoteLastValid: lv,
VoteParticipationKey: voteKey,
},
LastBlockProposal: nil,
LastStateProof: nil,
LastVote: nil,
}, nil
}

var VoteKey = []byte("TESTKEY")
var SelectionKey = []byte("TESTKEY")
var StateProofKey = []byte("TESTKEY")
Expand Down
10 changes: 7 additions & 3 deletions ui/modal/modal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ func Test_Snapshot(t *testing.T) {
golden.RequireEqual(t, []byte(got))
})
t.Run("InfoModal", func(t *testing.T) {
model := New(lipgloss.NewStyle().Width(80).Height(80).Render(""), true, test.GetState(nil))
model.SetKey(&mock.Keys[0])
model := New(lipgloss.NewStyle().Width(200).Height(80).Render(""), true, test.GetState(nil))
key, err := mock.GetPartKey()
if err != nil {
t.Fatal(err)
}
model.SetKey(key)
model.SetType(app.InfoModal)
model, _ = model.HandleMessage(tea.WindowSizeMsg{Width: 80, Height: 40})
model, _ = model.HandleMessage(tea.WindowSizeMsg{Width: 201, Height: 80})
got := ansi.Strip(model.View())
golden.RequireEqual(t, []byte(got))
})
Expand Down
Loading
Loading