Quick start
Overview
This guide will walk you through the process of building a Keychain in Go with the Keychain SDK.
Prerequisites
You need to install the following:
- Go 1.22 or later
make
1. Prepare the chain
1.1. Run a node
You can:
For the rest of this guide, we'll assume you have a running Warden Protocol node with a local account that has a few WARD tokens. The local account will be used to fund the Keychain and its Writers and referenced as <your-key>
in the following commands.
Check the list of available accounts by running this command:
wardend keys list
Check the local account balance:
wardend query bank balances <your-key>
In development genesis files, you'll typically find an account named shulgin
that is ready to be used.
1.2. Create a Keychain entity
You need to register your Keychain entity on-chain.
-
Initiate a
MsgNewKeychain
transaction by running this command:wardend tx warden new-keychain \
--description 'My Keychain' \
--keychain-fees \
'{"key_req": [{"amount": "100", "denom": "uward"}], \
"sig_req": [{"amount": "0", "denom": "uward"}]}' \
--from <your-key> \
--chain-id wardenprotocolSpecify the following details:
description
(optional): The Keychain descriptionkeychainFees
(optional):key_req
: A fee in uWARD for creating a key pairkey_req
: A fee in uWARD for signing a transaction
from
: Your local accountchain-id
: The chain ID –wardenprotocol
-
A new Keychain object will be created on-chain with its dedicated Keychain ID. Get the ID:
wardend query warden keychains
This ID will be used in the next steps, referenced as
<keychain-id>
.
1.3. Add a Keychain Writer
A Keychain Writer is an account that can write Keychain results (public keys and signatures) to the chain. The Keychain Writers list is essentially an allowlist of accounts that can interact on behalf of the Keychain.
To add a Keychain Writer, take these steps:
-
Initiate a
MsgAddKeychainWriter
transaction by running the following command:wardend keys add my-keychain-writer
-
The output will be similar to the following:
- address: warden18my6wqsrf5ek85znp8x202wwyg8rw4fqhy54k2
name: my-keychain-writer
pubkey: '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A2cECb3ziw5/LzUBUZIChyek3bnGQv/PSXHAH28xd9/Q"}'
type: local
**Important** write this mnemonic phrase in a safe place. It is the only way to recover your account if you ever forget your password.
virus boat radio apple pilot ask vault exhaust again state doll stereo slide exhibit scissors miss attack boat budget egg bird mask more tricktipOnly the Keychain Writer address, returned as
address
, will be able to publish signatures and public keys on behalf of the Keychain. -
Note down the mnemonic phrase and the address of the new account, it'll be needed to configure the Keychain SDK and interact with the chain using this account.
-
Fund the new account with some tokens (1 WARD in this example):
wardend tx bank send <your-key> \
$(wardend keys show -a my-keychain-writer) \
1000000uward \
--chain-id wardenprotocol
2. Build a Go app
2.1. Scaffold a Go app
-
Create a new Go app using the following command:
mkdir my-keychain
cd my-keychain
go mod init my-keychainThis will create a new Go module called
my-keychain
. -
Create a new Go file called
main.go
with the following content:// main.go
package main
func main() {
// ...
}
2.2. Import the Keychain SDK
-
Add the Keychain SDK to your Go module by running this:
go get github.com/warden-protocol/wardenprotocol/keychain-sdk
-
Import and use the SDK in your
main.go
file to create anApp
instance:// main.go
package main
import (
"context"
"github.com/warden-protocol/wardenprotocol/keychain-sdk"
)
func main() {
app := keychain.NewApp(keychain.Config{ })
}
2.3. Configure the app
Before starting the app, you need to configure it with the necessary information.
You'll find a basic configuration for connecting to a local Warden Protocol node below. Make the following adjustments:
- Replace
<your-keychain-id>
with the ID of the Keychain entity you created earlier - Replace the mnemonic phrase with the one for the Keychain Writer.
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, // not required, but recommended
// setup the connection to the Warden Protocol node
ChainID: "wardenprotocol",
GRPCURL: "localhost:9090",
GRPCInsecure: true,
// setup the account used to write txs
KeychainId: <your-keychain-id>,
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",
DerivationPath: "m/44'/118'/0'/0/0",
// setup throughput for batching responses
GasLimit: 400000,
BatchTimeout: 8 * time.Second,
BatchSize: 10,
})
}
2.4. Start the app
Finally, start the app by calling app.Start
:
func main() {
// ...
if err := app.Start(context.TODO()); err != nil {
panic(err)
}
}
2.5. Run the app
You can try running the app using the following command:
go run main.go
If everything is set up correctly, you'll see the app connecting to the Warden Protocol node and starting to process incoming requests.
The output will be similar to the following:
time=2024-03-26T12:01:38.020+01:00 level=INFO msg="starting keychain" keychain_id=1
time=2024-03-26T12:01:38.020+01:00 level=INFO msg="connecting to the Warden Protocol using gRPC" url=localhost:9090 insecure=true
time=2024-03-26T12:01:38.027+01:00 level=INFO msg="keychain writer identity" address=warden18my6wqsrf5ek85znp8x202wwyg8rw4fqhy54k2
time=2024-03-26T12:01:38.027+01:00 level=INFO msg="starting tx writer"
You can try to request a new ECDSA Key from SpaceWard of from the CLI.
In the following command, replace space-id
with the ID of the Space you want to use and keychain-id
with the ID of the Keychain entity you created earlier:
wardend tx warden new-key-request \
--space-id 1 \
--keychain-id 1 \
--key-type ecdsa-secp256-k1 \
--from <your-key> \
--chain-id wardenprotocol
You haven't implemented a key request handler yet, so you'll get an error:
time=2024-03-26T12:01:38.047+01:00 level=INFO msg="got key request" id=25579
time=2024-03-26T12:01:38.048+01:00 level=ERROR msg="key request handler not set"
3. Implement request handlers
3.1. Implement KeyRequestHandler
You're only one step away from generating new keys and writing them back to the chain. To do this, you need to implement a KeyRequestHandler
that will be called when a new key request is received.
In this example, the Keychain will generate ECDSA secp256k1 keys using the github.com/ethereum/go-ethereum/crypto
package. Private keys will be stored in-memory.
-
Add the following code to your
main.go
file:func main() {
app := ...
app.SetKeyRequestHandler(func(w keychain.KeyResponseWriter, req *keychain.KeyRequest) {
// your custom logic goes here
})
}tipThe
SetKeyRequestHandler()
function receives the following:KeyResponseWriter
that can be used to write the response back to the chainKeyRequest
with the details of the request, such as the key type (for example, ECDSA secp256k1)
-
Write a simple in-memory storage:
import (
...
"crypto/ecdsa"
"sync"
)
type Store struct {
mutex sync.Mutex
keys map[uint64]*ecdsa.PrivateKey
}
func (s *Store) Save(id uint64, key *ecdsa.PrivateKey) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.keys[id] = key
}
func (s *Store) Get(id uint64) *ecdsa.PrivateKey {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.keys[id]
} -
Implement a functioning
KeyRequestHandler
:import (
...
"github.com/ethereum/go-ethereum/crypto"
"github.com/warden-protocol/wardenprotocol/warden/x/warden/types/v1beta2"
)
func main() {
// ...
store := &Store{
keys: make(map[uint64]*ecdsa.PrivateKey),
}
app.SetKeyRequestHandler(func(w keychain.KeyResponseWriter, req *keychain.KeyRequest) {
if req.KeyType != v1beta2.KeyType_KEY_TYPE_ECDSA_SECP256K1 {
logger.Error("unsupported key type", "type", req.KeyType)
w.Reject("unsupported key type")
return
}
key, err := crypto.GenerateKey()
if err != nil {
logger.Error("failed to generate key", "error", err)
w.Reject("failed to generate key")
return
}
store.Save(req.Id, key)
pubKey := crypto.CompressPubkey(&key.PublicKey)
if err := w.Fulfil(pubKey); err != nil {
logger.Error("failed to fulfil key request", "error", err)
return
}
})
// ...
3.2. Implement SignRequestHandler
Now you need to implement a SignRequestHandler
. It functions similarly to the KeyRequestHandler
, but instead of generating new keys, it signs data using the private key associated with the request.
It's important to be able to recover the private key associated with a specific KeyRequest ID, so you should use the same Store
you created earlier.
Add the following code to your main.go
file:
func main() {
// ...
app.SetSignRequestHandler(func(w keychain.SignResponseWriter, req *keychain.SignRequest) {
key := store.Get(req.KeyId)
if key == nil {
logger.Error("key not found", "id", req.KeyId)
w.Reject("key not found")
return
}
sig, err := crypto.Sign(req.DataForSigning, key)
if err != nil {
logger.Error("failed to sign", "error", err)
w.Reject("failed to sign")
return
}
if err := w.Fulfil(sig); err != nil {
logger.Error("failed to fulfil sign request", "error", err)
return
}
})
// ...
}
4. Result
You have now built a simple Keychain in Go using the Keychain SDK. This Keychain stores ECDSA private keys in-memory and uses them to sign data. You can now run the app again and interact with it using the SpaceWard UI or the CLI.
By implementing request handlers, you can plug in any key generation and signing logic you need. Your Keychain can interact with external APIs, hardware security modules, or other key management systems such as MPC networks.
If you have any questions or need help, feel free to ask in the #keychain-operators channel on Discord.
Happy coding! 🚀