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

#683 keychain sdk configure multiple nodes endpoints #810

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7b6d2d2
Added support for multiple node urls
backsapc Sep 9, 2024
8e48821
Changed config
backsapc Sep 9, 2024
eefa6f1
Fixed test
backsapc Sep 9, 2024
cd821e6
Updated config for wardenkms
backsapc Oct 24, 2024
50fffb0
Merge branch 'main' into feature/683-keychain-sdk-configure-multiple-…
backsapc Oct 24, 2024
dc209fd
Fixed locking
backsapc Oct 24, 2024
f53b2ea
Merge branch 'feature/683-keychain-sdk-configure-multiple-nodes-endpo…
backsapc Oct 24, 2024
90d8ca5
Typo
backsapc Oct 24, 2024
ab22126
Simplified a bit
backsapc Oct 24, 2024
9b4c3a7
Code review
backsapc Oct 24, 2024
b1fe1de
Code review
backsapc Oct 24, 2024
fcd8167
Changed status check
backsapc Oct 24, 2024
31bbb3a
Fixed
backsapc Oct 25, 2024
3980feb
Omit err check
backsapc Oct 25, 2024
5ba8904
Merge branch 'main' into feature/683-keychain-sdk-configure-multiple-…
backsapc Oct 29, 2024
a23df86
Merge branch 'main' into feature/683-keychain-sdk-configure-multiple-…
backsapc Oct 30, 2024
bf12a7d
Merge branch 'main' into feature/683-keychain-sdk-configure-multiple-…
backsapc Nov 2, 2024
6b4ea38
Merge branch 'main' into feature/683-keychain-sdk-configure-multiple-…
backsapc Nov 11, 2024
b5923b8
uward revert
backsapc Nov 20, 2024
d4c3937
Fixed wardenkms startup
backsapc Nov 20, 2024
57bd353
Update keychain-sdk/tx_client_pool.go
backsapc Nov 22, 2024
6a545f3
Update keychain-sdk/example_keychain_test.go
backsapc Nov 22, 2024
3b7404f
Merge branch 'main' into feature/683-keychain-sdk-configure-multiple-…
backsapc Nov 27, 2024
3642a15
Code review comments
backsapc Nov 27, 2024
ce27189
Merge branch 'main' into feature/683-keychain-sdk-configure-multiple-…
backsapc Nov 28, 2024
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
23 changes: 23 additions & 0 deletions cmd/wardenkms/health_check_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

type HealthCheckResponse struct {
// The number of nodes that are online
Online uint `json:"online"`

// The total number of nodes
Total uint `json:"total"`

// The consensus threshold
Threshold uint8 `json:"threshold"`

// Node statuses
Nodes []NodeStatus `json:"nodes"`
}
backsapc marked this conversation as resolved.
Show resolved Hide resolved

type NodeStatus struct {
// The address of the node
Address string `json:"address"`

// The status of the node
Status string `json:"status"`
}
backsapc marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's follow Go conventions when documenting types or fields: https://go.dev/wiki/CodeReviewComments#comment-sentences.

Suggested change
type HealthCheckResponse struct {
// The number of nodes that are online
Online uint `json:"online"`
// The total number of nodes
Total uint `json:"total"`
// The consensus threshold
Threshold uint8 `json:"threshold"`
// Node statuses
Nodes []NodeStatus `json:"nodes"`
}
type NodeStatus struct {
// The address of the node
Address string `json:"address"`
// The status of the node
Status string `json:"status"`
}
type HealthCheckResponse struct {
// Online is...
Online uint `json:"online"`
// Total is...
Total uint `json:"total"`
// Threshold is...
Threshold uint8 `json:"threshold"`
// Nodes...
Nodes []NodeStatus `json:"nodes"`
}
type NodeStatus struct {
// Address...
Address string `json:"address"`
// Status...
Status string `json:"status"`
}

114 changes: 97 additions & 17 deletions cmd/wardenkms/wardenkms.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"log"
"log/slog"
Expand All @@ -20,11 +21,10 @@ import (
)

type Config struct {
ChainID string `env:"CHAIN_ID, default=warden_1337-1"`
GRPCURL string `env:"GRPC_URL, default=localhost:9090"`
GRPCInsecure bool `env:"GRPC_INSECURE, default=true"`
Mnemonic string `env:"MNEMONIC, default=exclude try nephew main caught favorite tone degree lottery device tissue tent ugly mouse pelican gasp lava flush pen river noise remind balcony emerge"`
KeychainId uint64 `env:"KEYCHAIN_ID, default=1"`
ChainID string `env:"CHAIN_ID, default=warden_1337-1"`
GRPCURLs GrpcNodeConfigDecoder `env:"GRPC_URLS, default=[{\"GRPCUrl\":\"localhost:9090\",\"GRPCInsecure\":true}] "`
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix inconsistent field naming in default JSON config

The default JSON config uses GRPCUrl while the struct field is named GRPCURL. This inconsistency could cause confusion.

Update the default JSON to match the struct field name:

-GRPCURLs   GrpcNodeConfigDecoder `env:"GRPC_URLS, default=[{\"GRPCUrl\":\"localhost:9090\",\"GRPCInsecure\":true}] "`
+GRPCURLs   GrpcNodeConfigDecoder `env:"GRPC_URLS, default=[{\"GRPCURL\":\"localhost:9090\",\"GRPCInsecure\":true}] "`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
GRPCURLs GrpcNodeConfigDecoder `env:"GRPC_URLS, default=[{\"GRPCUrl\":\"localhost:9090\",\"GRPCInsecure\":true}] "`
GRPCURLs GrpcNodeConfigDecoder `env:"GRPC_URLS, default=[{\"GRPCURL\":\"localhost:9090\",\"GRPCInsecure\":true}] "`

Mnemonic string `env:"MNEMONIC, default=exclude try nephew main caught favorite tone degree lottery device tissue tent ugly mouse pelican gasp lava flush pen river noise remind balcony emerge"`
KeychainId uint64 `env:"KEYCHAIN_ID, default=1"`

KeyringMnemonic string `env:"KEYRING_MNEMONIC, required"`
KeyringPassword string `env:"KEYRING_PASSWORD, required"`
Expand All @@ -38,6 +38,27 @@ type Config struct {
HttpAddr string `env:"HTTP_ADDR, default=:8080"`

LogLevel slog.Level `env:"LOG_LEVEL, default=debug"`

ConsensusNodeThreshold uint8 `env:"CONSENSUS_NODE_THRESHOLD, default=1"`
}

type GrpcNodeConfig struct {
GRPCUrl string
backsapc marked this conversation as resolved.
Show resolved Hide resolved
GRPCInsecure bool
}

type GrpcNodeConfigDecoder []GrpcNodeConfig
backsapc marked this conversation as resolved.
Show resolved Hide resolved

func (sd *GrpcNodeConfigDecoder) Decode(value string) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

let's avoid having a pointer to a pointer (= slice)

Suggested change
func (sd *GrpcNodeConfigDecoder) Decode(value string) error {
func (sd GrpcNodeConfigDecoder) Decode(value string) error {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Isn't it how the thing supposed to be?

nodeConfigs := make([]GrpcNodeConfig, 0)

if err := json.Unmarshal([]byte(value), &nodeConfigs); err != nil {
return fmt.Errorf("invalid map json: %w", err)
}

*sd = nodeConfigs

return nil
}

func main() {
Expand All @@ -56,18 +77,26 @@ func main() {
return
}

grpcConfigs, err := mapGrpcConfig(cfg.GRPCURLs)
Copy link
Contributor

Choose a reason for hiding this comment

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

since wardenkms is an user of the keychain-sdk, having to do this sort of mapping "manually" shows that the configuration of the keychain-sdk is not very ergonomic

i.e. keychain-sdk is not a library that I would enjoy using

i think we can explore different configuration patterns such as the option pattern maybe, or even treating the URL string as "special", like if it starts with "http://" or "tcp://" turn on the insecure option, while if it starts with "https://" turn insecure off

but, if you have better ideas please share them!

if err != nil {
logger.Error("failed to initialize grpc configs", "error", err)
return
}

app := keychain.NewApp(keychain.Config{
Logger: logger,
ChainID: cfg.ChainID,
GRPCURL: cfg.GRPCURL,
GRPCInsecure: cfg.GRPCInsecure,
Mnemonic: cfg.Mnemonic,
KeychainID: cfg.KeychainId,
GasLimit: cfg.GasLimit,
BatchInterval: cfg.BatchInterval,
BatchSize: cfg.BatchSize,
TxTimeout: cfg.TxTimeout,
TxFees: sdk.NewCoins(sdk.NewCoin("award", math.NewInt(cfg.TxFee))),
BasicConfig: keychain.BasicConfig{
Logger: logger,
ChainID: cfg.ChainID,
Mnemonic: cfg.Mnemonic,
KeychainID: cfg.KeychainId,
GasLimit: cfg.GasLimit,
BatchInterval: cfg.BatchInterval,
BatchSize: cfg.BatchSize,
TxTimeout: cfg.TxTimeout,
TxFees: sdk.NewCoins(sdk.NewCoin("award", math.NewInt(cfg.TxFee))),
},
GRPCConfigs: grpcConfigs,
ConsensusNodeThreshold: cfg.ConsensusNodeThreshold,
Pitasi marked this conversation as resolved.
Show resolved Hide resolved
})

app.SetKeyRequestHandler(func(w keychain.KeyResponseWriter, req *keychain.KeyRequest) {
Expand Down Expand Up @@ -120,11 +149,41 @@ func main() {
if cfg.HttpAddr != "" {
logger.Info("starting HTTP server", "addr", cfg.HttpAddr)
http.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
if app.ConnectionState() == connectivity.Ready {
connectionStates := app.ConnectionState()

readyConnectionsCount := uint(0)
nodes := make([]NodeStatus, 0, len(connectionStates))

for url, state := range connectionStates {
if state == connectivity.Ready {
readyConnectionsCount += 1
}

nodes = append(nodes, NodeStatus{
Address: url,
Status: state.String(),
})
}

bytes, err := json.Marshal(HealthCheckResponse{
Online: readyConnectionsCount,
Total: uint(len(connectionStates)),
Nodes: nodes,
Threshold: cfg.ConsensusNodeThreshold,
})

if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

if readyConnectionsCount >= uint(cfg.ConsensusNodeThreshold) {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}

_, _ = w.Write(bytes)
})
go func() { _ = http.ListenAndServe(cfg.HttpAddr, nil) }()
}
Expand All @@ -144,3 +203,24 @@ func bigEndianBytesFromUint32(n uint64) ([4]byte, error) {
binary.BigEndian.PutUint32(b, uint32(n))
return [4]byte(b), nil
}

func mapGrpcConfig(value GrpcNodeConfigDecoder) ([]keychain.GrpcNodeConfig, error) {
var nodesLength = len(value)
if nodesLength == 0 {
return nil, fmt.Errorf("GRPCUrls must be specified")
}

result := make([]keychain.GrpcNodeConfig, 0, nodesLength)
for _, item := range value {
if item.GRPCUrl == "" {
return nil, fmt.Errorf("GRPCUrl must be specified")
}

result = append(result, keychain.GrpcNodeConfig{
backsapc marked this conversation as resolved.
Show resolved Hide resolved
GRPCInsecure: item.GRPCInsecure,
GRPCURL: item.GRPCUrl,
})
backsapc marked this conversation as resolved.
Show resolved Hide resolved
}

return result, nil
}
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ services:
WARDEND_GRPC_ADDRESS: 0.0.0.0:9090
WARDEND_RPC_LADDR: tcp://0.0.0.0:26657
WARDEND_RPC_CORS_ALLOWED_ORIGINS: "*"
WARDEND_MINIMUM_GAS_PRICES: "0uward"
WARDEND_MINIMUM_GAS_PRICES: "0award"
Copy link
Contributor

Choose a reason for hiding this comment

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

let's fix this in another PR since it's not really related to the keychain sdk


faucet:
build:
Expand Down
33 changes: 23 additions & 10 deletions keychain-sdk/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,14 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// Config is the configuration for the Keychain.
type Config struct {
type BasicConfig struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

what's the rationale behind splitting Config into Config and BasicConfig? I think I might be missing something

// Logger is the logger to use for the Keychain.
// If nil, no logging will be done.
Logger *slog.Logger

// ChainID is the chain ID of the chain to connect to.
ChainID string

// GRPCURL is the URL of the gRPC server to connect to.
// e.g. "localhost:9090"
GRPCURL string

// GRPCInsecure determines whether to allow an insecure connection to the
// gRPC server.
GRPCInsecure bool

// KeychainID is the ID of the keychain this instance will fetch requests
// for.
KeychainID uint64
Expand Down Expand Up @@ -58,3 +49,25 @@ type Config struct {
// failed (but the blockchain might still include in a block later).
TxTimeout time.Duration
}

// Config is the configuration for the Keychain.
type Config struct {
BasicConfig

// ConsensusNodeThreshold represents the number of nodes required to execute a pending key/sign request.
ConsensusNodeThreshold uint8
backsapc marked this conversation as resolved.
Show resolved Hide resolved

// GRPCURLs is the list of URLs of the gRPC server to connect to.
// e.g. "localhost:9090"
GRPCConfigs []GrpcNodeConfig
Copy link
Contributor

Choose a reason for hiding this comment

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

since we call them "nodes" (ConsensusNodeThreshold represents the number of nodes...) we should be consistent. Also, the doc comment for GRPCConfigs is outdated.

Suggested change
// ConsensusNodeThreshold represents the number of nodes required to execute a pending key/sign request.
ConsensusNodeThreshold uint8
// GRPCURLs is the list of URLs of the gRPC server to connect to.
// e.g. "localhost:9090"
GRPCConfigs []GrpcNodeConfig
// ConsensusNodeThreshold represents the number of nodes required to execute a pending key/sign request.
ConsensusNodeThreshold uint8
// Nodes is the list of gRPC nodes to connect to for fetching incoming requests and broadcasting transactions.
Nodes []GrpcNodeConfig

}

type GrpcNodeConfig struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

the case of the word grpc is inconsistent

Suggested change
type GrpcNodeConfig struct {
type GRPCNodeConfig struct {

// GRPCInsecure determines whether to allow an insecure connection to the
// gRPC server.
GRPCInsecure bool

// GRPCURL is the URL of the gRPC server to connect to.
// e.g. "localhost:9090"
GRPCURL string
}
Copy link
Contributor

Choose a reason for hiding this comment

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

since we are inside a struct called "GrpcNodeConfig", let's avoid repeating the word GRPC everywhere:

Suggested change
type GrpcNodeConfig struct {
// GRPCInsecure determines whether to allow an insecure connection to the
// gRPC server.
GRPCInsecure bool
// GRPCURL is the URL of the gRPC server to connect to.
// e.g. "localhost:9090"
GRPCURL string
}
type GrpcNodeConfig struct {
// Insecure determines whether to allow an insecure connection to the
// gRPC server.
Insecure bool
// Host is the address of the gRPC server to connect to.
// e.g. "localhost:9090"
Host string
}

technically, the second field is not a URL since it doesn't contain a protocol scheme, i renamed it into "host"

27 changes: 15 additions & 12 deletions keychain-sdk/example_keychain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@ func Main() {
}))

app := keychain.NewApp(keychain.Config{
Logger: logger, // not required, but recommended
BasicConfig: keychain.BasicConfig{
Logger: logger, // not required, but recommended

// setup the connection to the Warden Protocol node
ChainID: "warden",
GRPCURL: "localhost:9090",
GRPCInsecure: true,
// setup the connection to the Warden Protocol node
ChainID: "warden",
backsapc marked this conversation as resolved.
Show resolved Hide resolved

// setup the account used to write txs
KeychainID: 1,
Mnemonic: "virus boat radio apple pilot ask vault exhaust again state doll stereo slide exhibit scissors miss attack boat budget egg bird mask more trick",
// setup the account used to write txs
KeychainID: 1,
Mnemonic: "virus boat radio apple pilot ask vault exhaust again state doll stereo slide exhibit scissors miss attack boat budget egg bird mask more trick",
backsapc marked this conversation as resolved.
Show resolved Hide resolved

// setup throughput for batching responses
GasLimit: 400000,
BatchInterval: 8 * time.Second,
BatchSize: 10,
// setup throughput for batching responses
GasLimit: 400000,
BatchInterval: 8 * time.Second,
BatchSize: 10,
},

GRPCConfigs: []keychain.GrpcNodeConfig{{GRPCURL: "localhost:9090", GRPCInsecure: false}},
ConsensusNodeThreshold: 1,
backsapc marked this conversation as resolved.
Show resolved Hide resolved
})

app.SetKeyRequestHandler(func(w keychain.KeyResponseWriter, req *keychain.KeyRequest) {
Expand Down
59 changes: 48 additions & 11 deletions keychain-sdk/internal/tracker/tracker.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,72 @@
package tracker

import (
"fmt"
"sync"
)

type Action int
backsapc marked this conversation as resolved.
Show resolved Hide resolved

const (
ActionSkip Action = iota
ActionProcess
)

type stringSet map[string]struct{}

// add safely adds a string to the set
func (s stringSet) add(str string) bool {
if _, exists := s[str]; exists {
return false
}
s[str] = struct{}{}
return true
}

type T struct {
rw sync.RWMutex
ingested map[uint64]struct{}
threshold uint8
rw sync.RWMutex
ingested map[uint64]stringSet
}

func New() *T {
func New(threshold uint8) *T {
return &T{
ingested: make(map[uint64]struct{}),
threshold: threshold,
ingested: make(map[uint64]stringSet),
}
}

func (t *T) IsNew(id uint64) bool {
t.rw.RLock()
defer t.rw.RUnlock()
_, ok := t.ingested[id]
return !ok
func (t *T) ingestTracker(id uint64) stringSet {
value, ok := t.ingested[id]
if !ok {
t.ingested[id] = make(stringSet)

return t.ingested[id]
}

return value
}

func (t *T) Ingested(id uint64) {
func (t *T) Ingest(id uint64, ingesterId string) (Action, error) {
t.rw.Lock()
defer t.rw.Unlock()
t.ingested[id] = struct{}{}

value := t.ingestTracker(id)

if !value.add(ingesterId) {
return ActionSkip, fmt.Errorf("already ingested")
}

if uint64(len(value)) == uint64(t.threshold) {
return ActionProcess, nil
}

return ActionSkip, nil
backsapc marked this conversation as resolved.
Show resolved Hide resolved
}

func (t *T) Done(id uint64) {
t.rw.Lock()
defer t.rw.Unlock()

delete(t.ingested, id)
}
Loading
Loading