diff --git a/docs/developer-docs/docs/build-a-keychain/keychain-sdk.md b/docs/developer-docs/docs/build-a-keychain/keychain-sdk.md deleted file mode 100644 index 191cec3d2..000000000 --- a/docs/developer-docs/docs/build-a-keychain/keychain-sdk.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Keychain SDK - -## Overview - -The **Keychain SDK** is a Go SDK that abstracts the communication with [Warden Protocol nodes](/learn/glossary#warden-protocol-node), facilitating the development of [Keychains](/learn/glossary#keychain). Here is what you can do with the Keychain SDK: - -- Set up and run a Keychain -- Manage [key requests](/learn/request-flow#key-request-flow) and [signature requests](/learn/request-flow#signature-request-flow) -- Create and manage request trackers -- Create and manage [Keychain Writers](/learn/glossary#keychain-writer) - -You can find the SDK in our GitHub repository: - -- [Keychain SDK](https://github.com/warden-protocol/wardenprotocol/tree/main/keychain-sdk) - -To learn about the available types and functions, check the reference page: - -- [KeyChain SDK reference](https://pkg.go.dev/github.com/warden-protocol/wardenprotocol/keychain-sdk) diff --git a/docs/developer-docs/docs/build-a-keychain/sdk/_category_.json b/docs/developer-docs/docs/build-a-keychain/sdk/_category_.json new file mode 100644 index 000000000..38a436868 --- /dev/null +++ b/docs/developer-docs/docs/build-a-keychain/sdk/_category_.json @@ -0,0 +1,11 @@ +{ + "position": 4, + "label": "Keychain SDK", + "collapsible": true, + "collapsed": false, + "link": { + "type": "generated-index", + "title": "Keychain SDK" + }, + "customProps": {} +} \ No newline at end of file diff --git a/docs/developer-docs/docs/build-a-keychain/sdk/keychain-sdk.md b/docs/developer-docs/docs/build-a-keychain/sdk/keychain-sdk.md new file mode 100644 index 000000000..1e99c16ea --- /dev/null +++ b/docs/developer-docs/docs/build-a-keychain/sdk/keychain-sdk.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 1 +--- + +# Keychain SDK + +## Overview + +The **Keychain SDK** offers a robust framework for managing cryptographic operations on the Warden Protocol. It simplifies the development of applications that interact with the Warden Protocol, handling both key requests and sign requests with efficiency and security. + +## Module descriptions + +In this section, we will walk you through different modules of the **Keychain SDK.** + +### Sign requests (`sign_requests.go`) + +**Purpose**: This module handles sign requests, allowing the application to process and respond to cryptographic signing operations. + +**Key components:** + +- **SignResponseWriter interface**: Provides methods to fulfill or reject sign requests. + - `Fulfil(signature []byte) error`: Writes a signature to a sign request. + - `Reject(reason string) error`: Writes a rejection message to a sign request. +- **SignRequestHandler**: A function type that processes individual sign requests. +- **signResponseWriter struct**: Implements the `SignResponseWriter` interface, managing encryption and transaction writing. + +**Functions:** + +- `handleSignRequest`: Processes a sign request using the provided `SignRequestHandler`. +- `ingestSignRequests`: Continuously fetches and handles new sign requests. + +### Keychain (`keychain.go`) + +**Purpose**: Central application management, coordinating key and sign request handling. + +**Key components:** + +- **App struct**: Represents the Keychain application, managing configuration and handlers. + - Key members include `config`, `query`, `txWriter`, `keyRequestTracker`, and `signRequestTracker`. +- **NewApp function**: Initializes a new `App` instance with the provided configuration. +- **Handlers**: These methods set the functions for processing requests. + - `SetKeyRequestHandler`: Sets the handler for key requests. + - `SetSignRequestHandler`: Sets the handler for sign requests. +- **Start method**: Begins the Keychain application's operations, managing request channels and transaction writing. +- `ConnectionState`: Returns the state of the gRPC connection. +- `initConnections`: Establishes connections to the Warden Protocol via gRPC. + +### Key requests (`key_requests.go`) + +**Purpose**: Handles key requests, providing interfaces to fulfill or reject requests for public keys. + +**Key components:** + +- **KeyResponseWriter Interface**: Contains methods to fulfill or reject key requests. + - `Fulfil(publicKey []byte) error`: Sends a public key in response. + - `Reject(reason string) error`: Sends a rejection reason. +- **KeyRequestHandler**: A function type for handling key requests. +- **keyResponseWriter Struct**: Implements the `KeyResponseWriter` interface, processing key requests and writing results. + +**Functions:** + +- `handleKeyRequest`: Processes a key request using the provided `KeyRequestHandler`. +- `ingestKeyRequests`: Continuously fetches and handles new key requests. + +### Configuration (`config.go`) + +**Purpose**: Defines the configuration structure for the Keychain SDK, detailing all necessary parameters for application setup. + +**Key components:** + +- **Config struct**: Specifies settings such as logger, chain ID, gRPC details, and batching options. + +```go +type Config struct { + Logger *slog.Logger + ChainID string + GRPCURL string + GRPCInsecure bool + KeychainID uint64 + DerivationPath string + Mnemonic string + BatchInterval time.Duration + BatchSize int + GasLimit uint64 + TxFees sdk.Coins + TxTimeout time.Duration +} +``` + +### Transaction 2riter (`writer.go`) + +**Purpose**: Manages transaction batching and sending operations. + +**Key components:** + +- **W struct**: Handles batch management and sending transactions to the blockchain. + +**Functions:** + +- `Start`: Begins the transaction writing process. +- `Write`: Adds messages to the batch for processing. +- `Flush`: Sends accumulated transactions in a batch. +- `sendWaitTx`: Handles the actual transaction submission and awaits confirmation. + +### Request tracker (`tracker.go`) + +**Purpose**: Tracks processed request IDs to prevent duplicate processing. + +**Key components:** + +- **T struct**: Manages an ingested map to track processed requests, ensuring each request is only handled once. + +**Functions:** + +- `IsNew`: Checks if a request ID is new. +- `Ingested`: Marks a request as ingested. +- `Done`: Removes a request from the ingested map. + +### Encryption utilities (`enc.go`) + +**Purpose**: Provides encryption utilities for cryptographic operations. + +**Key functions:** + +- **Encrypt**: Encrypts data using a provided ECDSA public key. +- **ValidateEncryptionKey**: Validates an ECDSA public key. + +You can find more details about the Keychain SDK in our GitHub repository: + +- [The Keychain SDK](https://github.com/warden-protocol/wardenprotocol/tree/main/keychain-sdk) + +To learn about the available types and functions, check the reference page: + +- [The Keychain SDK reference](https://pkg.go.dev/github.com/warden-protocol/wardenprotocol/keychain-sdk) diff --git a/docs/developer-docs/docs/build-a-keychain/sdk/tutorial.md b/docs/developer-docs/docs/build-a-keychain/sdk/tutorial.md new file mode 100644 index 000000000..6d3635c21 --- /dev/null +++ b/docs/developer-docs/docs/build-a-keychain/sdk/tutorial.md @@ -0,0 +1,395 @@ +--- +sidebar_position: 2 +--- + +# Building a Keychain service with Warden Protocol + +## Overview + +The Keychain service is a crucial component in the Warden Protocol ecosystem. Keychains are responsible for generating cryptographic keys, securely storing them, and signing transactions. To learn how Keychains process key and signature requests, see [Request flow](/learn/request-flow). + +This tutorial explains how to build a Keychain application in Go using the **Keychain SDK**. We're also going to test the application using mock key and sign requests. + +Note that in a production environment, you'd need to implement actual logic for generating keys and signing transactions, integrate with a secure key storage solution, and add more robust error handling and security measures. + +## Prerequisites + +- `Go` 1.23 or later + +## Setting up the project + +1. Create a new directory for your project: + + ```bash + mkdir warden-keychain-service && cd warden-keychain-service + ``` + +2. Initialize a new Go module: + + ```go + go mod init warden-keychain-service + ``` + +3. Install the required dependencies: + + ```go + go get github.com/warden-protocol/wardenprotocol/keychain-sdk + go get github.com/stretchr/testify + ``` + +## Creating the main application + +1. Create a new file named `main.go` in your project directory and open it in your preferred text editor. + +2. Add the following code to `main.go` with the skeleton: + + ```go + package main + + import ( + "context" + "log/slog" + "os" + "time" + + "github.com/warden-protocol/wardenprotocol/keychain-sdk" + ) + + func main() { + // Set up a logger for debugging + + // Create a new Keychain application + + } + + // Set up handlers for key requests and sign requests + app.SetKeyRequestHandler(handleKeyRequest) + app.SetSignRequestHandler(handleSignRequest) + + + // Start the application + + + // handleKeyRequest processes incoming key requests + func handleKeyRequest(w keychain.KeyResponseWriter, req *keychain.KeyRequest) { + } + + // handleSignRequest processes incoming sign requests + func handleSignRequest(w keychain.SignResponseWriter, req *keychain.SignRequest) { + } + ``` + + Let's first define `handleKeyRequest` function. This function takes in a `KeyResponseWriter` and a `KeyRequest` as parameters.Inside the function, let us create a logger using `slog.Default()` and log informational messages with the request ID and key type. + + **Note:** This function will create a dummy public key as a byte slice. + + Finally, let us call the `Fulfil` method on the `KeyResponseWriter` with the dummy public key. If there is an error, it logs an error message and calls the Reject method on the `KeyResponseWriter` with an error message. + + ```go + // handleKeyRequest processes incoming key requests + func handleKeyRequest(w keychain.KeyResponseWriter, req *keychain.KeyRequest) { + logger := slog.Default() + logger.Info("received a key request", "id", req.Id, "key_type", req.KeyType) + + // In a real application, you would generate a public key here + // For this example, we'll use a dummy public key + publicKey := []byte("dummy_public_key") + + if err := w.Fulfil(publicKey); err != nil { + logger.Error("failed to fulfill the key request", "error", err) + if err := w.Reject("Internal error"); err != nil { + logger.Error("failed to reject the key request", "error", err) + } + } + } + ``` + + Next, let us define `Go` function called `handleSignRequest` that takes in a `SignResponseWriter` and a `SignRequest` as parameters. It logs the received sign request and then generates a dummy signature. If the `Fulfil` method of the ` SignResponseWriter` returns an error, it logs the error and attempts to reject the sign request. + + ```go + func handleSignRequest(w keychain.SignResponseWriter, req *keychain.SignRequest) { + logger := slog.Default() + logger.Info("received a sign request", "id", req.Id, "key_id", req.KeyId) + + // In a real application, you would sign the data here + // For this example, we'll use a dummy signature + signature := []byte("dummy_signature") + + if err := w.Fulfil(signature); err != nil { + logger.Error("failed to fulfill the sign request", "error", err) + if err := w.Reject("Internal error"); err != nil { + logger.Error("failed to reject the sign request", "error", err) + } + } + } + ``` + + OK! Now since our main logic is implemented, let us write the complete `main.go` + + ```go + package main + + import ( + "context" + "log/slog" + "os" + "time" + + "github.com/warden-protocol/wardenprotocol/keychain-sdk" + ) + + func main() { + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + + app := keychain.NewApp(keychain.Config{ + Logger: logger, + ChainID: "warden", + GRPCURL: "localhost:9090", + GRPCInsecure: true, + KeychainID: 1, + Mnemonic: "zebra future seed foil jungle eyebrow rubber spatial measure auction unveil blue toy good lift audit truth obvious voyage inspire gold rule year canyon", + DerivationPath: "m/44'/118'/0'/0/0", + GasLimit: 400000, + BatchInterval: 8 * time.Second, + BatchSize: 10, + }) + + app.SetKeyRequestHandler(handleKeyRequest) + app.SetSignRequestHandler(handleSignRequest) + + if err := app.Start(context.Background()); err != nil { + logger.Error("application error", "error", err) + os.Exit(1) + } + } + + func handleKeyRequest(w keychain.KeyResponseWriter, req *keychain.KeyRequest) { + logger := slog.Default() + logger.Info("received key request", "id", req.Id, "key_type", req.KeyType) + + // In a real application, you would generate a public key here + // For this example, we'll use a dummy public key + publicKey := []byte("dummy_public_key") + + if err := w.Fulfil(publicKey); err != nil { + logger.Error("failed to fulfill key request", "error", err) + if err := w.Reject("Internal error"); err != nil { + logger.Error("failed to reject key request", "error", err) + } + } + } + + func handleSignRequest(w keychain.SignResponseWriter, req *keychain.SignRequest) { + logger := slog.Default() + logger.Info("received sign request", "id", req.Id, "key_id", req.KeyId) + + // In a real application, you would sign the data here + // For this example, we'll use a dummy signature + signature := []byte("dummy_signature") + + if err := w.Fulfil(signature); err != nil { + logger.Error("failed to fulfill sign request", "error", err) + if err := w.Reject("Internal error"); err != nil { + logger.Error("failed to reject sign request", "error", err) + } + } + } + ``` + +## Creating a test + +Now, let us write a test to test our previously written function. + +1. Create a new file named `keychain_test.go` in your project directory and open it in your text editor. + +2. Add the following code to `keychain_test.go`: + + ```go + package main + + import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/warden-protocol/wardenprotocol/keychain-sdk" + wardentypes "github.com/warden-protocol/wardenprotocol/warden/x/warden/types/v1beta3" + ) + + // Mock implementation of KeyResponseWriter + type mockKeyResponseWriter struct { + fulfilled bool + rejected bool + publicKey []byte + reason string + } + + func (m *mockKeyResponseWriter) Fulfil(publicKey []byte) error { + m.fulfilled = true + m.publicKey = publicKey + return nil + } + + func (m *mockKeyResponseWriter) Reject(reason string) error { + m.rejected = true + m.reason = reason + return nil + } + + // Mock implementation of SignResponseWriter + type mockSignResponseWriter struct { + fulfilled bool + rejected bool + signature []byte + reason string + } + + func (m *mockSignResponseWriter) Fulfil(signature []byte) error { + m.fulfilled = true + m.signature = signature + return nil + } + + func (m *mockSignResponseWriter) Reject(reason string) error { + m.rejected = true + m.reason = reason + return nil + } + + // TestKeychain is the main test function + func TestKeychain(t *testing.T) { + // Set up the Keychain app + app := setupKeychainApp(t) + + // Start the app in a goroutine + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + errChan := make(chan error, 1) + go func() { + if err := app.Start(ctx); err != nil { + errChan <- err + } + }() + + // Give the app some time to start + select { + case err := <-errChan: + t.Fatalf("Keychain app error: %v", err) + case <-time.After(10 * time.Second): + t.Log("Keychain app started successfully") + } + + t.Run("TestKeyRequest", func(t *testing.T) { + testKeyRequest(t) + }) + + t.Run("TestSignRequest", func(t *testing.T) { + testSignRequest(t) + }) + } + + // setupKeychainApp creates and configures a new keychain app for testing + func setupKeychainApp(t *testing.T) *keychain.App { + mnemonic := "zebra future seed foil jungle eyebrow rubber spatial measure auction unveil blue toy good lift audit truth obvious voyage inspire gold rule year canyon" + + app := keychain.NewApp(keychain.Config{ + ChainID: "warden", + GRPCURL: "localhost:9090", + GRPCInsecure: true, + KeychainID: 1, + Mnemonic: mnemonic, + DerivationPath: "m/44'/118'/0'/0/0", + GasLimit: 400000, + BatchInterval: 8 * time.Second, + BatchSize: 10, + }) + + t.Logf("Setting up the Keychain app with mnemonic: %s", mnemonic) + + return app + } + + // testKeyRequest tests the key request handling + func testKeyRequest(t *testing.T) { + // Create a new key request + keyRequest := &keychain.KeyRequest{ + Id: 1, + SpaceId: 1, + KeychainId: 1, + KeyType: wardentypes.KeyType_KEY_TYPE_ECDSA_SECP256K1, + RuleId: 1, + } + + writer := &mockKeyResponseWriter{} + handleKeyRequest(writer, keyRequest) + + assert.True(t, writer.fulfilled) + assert.NotEmpty(t, writer.publicKey) + } + + // testSignRequest tests the sign request handling + func testSignRequest(t *testing.T) { + // Create a new sign request + signRequest := &keychain.SignRequest{ + Id: 1, + KeyId: 1, + DataForSigning: []byte("test data to sign"), + EncryptionKey: []byte("test encryption key"), + } + + writer := &mockSignResponseWriter{} + handleSignRequest(writer, signRequest) + + assert.True(t, writer.fulfilled) + assert.NotEmpty(t, writer.signature) + } + ``` + +Here is a brief explanation of the `keychain_test.go` code: + +- We define mock implementations of `KeyResponseWriter` and `SignResponseWriter` for testing purposes. +- The `TestKeychain` function is the main test function. It sets up the keychain app, starts it in a goroutine, and runs two subtests. +- `setupKeychainApp` creates a new Keychain app with the same configuration as in `main.go`. +- `testKeyRequest` creates a mock key request, calls the `handleKeyRequest` function, and asserts that the response is as expected. +- `testSignRequest` does the same for sign requests. + +## Running tests + +To run the tests: + +1. Open a terminal and navigate to your project directory. +2. Run the following command: + + ```bash + go test -v + ``` + +3. You should see output indicating that the tests have run and passed: + + ```bash + RUN TestKeychain + RUN TestKeychain/TestKeyRequest + + 2024/08/21 18:11:14 INFO received key request id=1 key_type=KEY_TYPE_ECDSA_SECP256K1 + + RUN TestKeychain/TestSignRequest + + 2024/08/21 18:11:14 INFO received sign request id=1 key_id=1 + + PASS: TestKeychain (2.00s) + PASS: TestKeychain/TestKeyRequest (0.00s) + PASS: TestKeychain/TestSignRequest (0.00s) + + PASS + ok keychain-sdk 2.990s + ``` + +## Conclusion + +This tutorial has walked you through creating a basic Keychain service using the Warden Protocol. You've set up the main application, implemented placeholder handlers for key and sign requests, and created tests to verify the basic functionality. + +Happy coding! 🚀 diff --git a/docs/developer-docs/docusaurus.config.ts b/docs/developer-docs/docusaurus.config.ts index 2c0b5884c..87adf687f 100644 --- a/docs/developer-docs/docusaurus.config.ts +++ b/docs/developer-docs/docusaurus.config.ts @@ -120,7 +120,7 @@ const config: Config = { { type: "doc", label: "Keychain SDK", - docId: "build-a-keychain/keychain-sdk", + docId: "build-a-keychain/sdk/keychain-sdk", }, { type: "doc",