Skip to content

Commit

Permalink
feat(keychain-sdk): batch multiple requests into a single transaction (
Browse files Browse the repository at this point in the history
…#92)

* feat(go-client): introduce Msger type and allow to easily build multi-msg transactions

* feat(keychain-sdk): batch multiple requests into a single transaction
  • Loading branch information
Pitasi committed Mar 14, 2024
1 parent 18996e2 commit 79af474
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 134 deletions.
4 changes: 1 addition & 3 deletions go-client/tx_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ import (
// TxClient can read/write transactions to wardend and endpoints provided by the treasury module.
type TxClient struct {
*RawTxClient
*WardenTxClient
}

// NewTxClient returns a TxClient.
func NewTxClient(id Identity, chainID string, c *grpc.ClientConn, accountFetcher AccountFetcher) *TxClient {
raw := NewRawTxClient(id, chainID, c, accountFetcher)
return &TxClient{
RawTxClient: raw,
WardenTxClient: NewWardenTxClient(raw),
RawTxClient: raw,
}
}
3 changes: 0 additions & 3 deletions go-client/tx_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@ func NewIdentityFromSeed(derivationPath, seedPhrase string) (Identity, error) {

// Generate a private key object from the bytes
privKey, _ := btcec.PrivKeyFromBytes(derivedKey)
if err != nil {
return Identity{}, fmt.Errorf("failed to generate private key: %w", err)
}

// Convert the public key to a Cosmos secp256k1.PublicKey
cosmosPrivKey := &secp256k1.PrivKey{
Expand Down
12 changes: 11 additions & 1 deletion go-client/tx_raw_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
db "github.com/cosmos/cosmos-db"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/types"
sdk "github.com/cosmos/cosmos-sdk/types"
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
xauthsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
Expand Down Expand Up @@ -80,9 +81,13 @@ func (c *RawTxClient) SendWaitTx(ctx context.Context, txBytes []byte) error {
return nil
}

type Msger interface {
Msg(creator string) sdk.Msg
}

// Build a transaction with the given messages and sign it.
// Sequence and account numbers will be fetched automatically from the chain.
func (c *RawTxClient) BuildTx(ctx context.Context, gasLimit uint64, fees types.Coins, msgs ...types.Msg) ([]byte, error) {
func (c *RawTxClient) BuildTx(ctx context.Context, gasLimit uint64, fees types.Coins, msgers ...Msger) ([]byte, error) {
account, err := c.accountFetcher.Account(ctx, c.Identity.Address.String())
if err != nil {
return nil, fmt.Errorf("fetch account: %w", err)
Expand All @@ -108,6 +113,11 @@ func (c *RawTxClient) BuildTx(ctx context.Context, gasLimit uint64, fees types.C
txBuilder.SetGasLimit(gasLimit)
txBuilder.SetFeeAmount(fees)

msgs := make([]sdk.Msg, len(msgers))
for i, m := range msgers {
msgs[i] = m.Msg(c.Identity.Address.String())
}

if err = txBuilder.SetMsgs(msgs...); err != nil {
return nil, fmt.Errorf("set msgs: %w", err)
}
Expand Down
159 changes: 45 additions & 114 deletions go-client/tx_warden.go
Original file line number Diff line number Diff line change
@@ -1,137 +1,68 @@
// Copyright 2024
//
// This file includes work covered by the following copyright and permission notices:
//
// Copyright 2023 Qredo Ltd.
// Licensed under the Apache License, Version 2.0;
//
// This file is part of the Warden Protocol library.
//
// The Warden Protocol library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the Warden Protocol library. If not, see https://github.com/warden-protocol/wardenprotocol/blob/main/LICENSE
package client

import (
"context"
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/warden-protocol/wardenprotocol/warden/x/warden/types"
)

// WardenTxClient contains a raw tx client.
type WardenTxClient struct {
c *RawTxClient
}

// NewWardenTxClient returns a WardenTxClient.
func NewWardenTxClient(c *RawTxClient) *WardenTxClient {
return &WardenTxClient{c: c}
type KeyRequestFulfilment struct {
RequestID uint64
PublicKey []byte
}

// FulfilKeyRequest completes a key request writing the public key bytes to wardend. Note that the sender must be authorized to submit transactions
// for the keychain corresponding to the requestID. The transaction will be rejected if the WardenTxClient does not have the correct identity address.
func (c *WardenTxClient) FulfilKeyRequest(ctx context.Context, requestID uint64, publicKey []byte) error {
status := types.KeyRequestStatus_KEY_REQUEST_STATUS_FULFILLED
result := types.NewMsgUpdateKeyRequestKey(publicKey)

msg := &types.MsgUpdateKeyRequest{
Creator: c.c.Identity.Address.String(),
RequestId: requestID,
Status: status,
Result: result,
}

txBytes, err := c.c.BuildTx(ctx, DefaultGasLimit, DefaultFees, msg)
if err != nil {
return fmt.Errorf("build tx: %w", err)
}

if err = c.c.SendWaitTx(ctx, txBytes); err != nil {
return fmt.Errorf("send wait tx: %w", err)
func (r KeyRequestFulfilment) Msg(creator string) sdk.Msg {
return &types.MsgUpdateKeyRequest{
Creator: creator,
RequestId: r.RequestID,
Status: types.KeyRequestStatus_KEY_REQUEST_STATUS_FULFILLED,
Result: types.NewMsgUpdateKeyRequestKey(r.PublicKey),
}

return nil
}

// RejectKeyRequest is similar to FulfilKeyRequest, but instead rejects the key request with the provided reason.
func (c *WardenTxClient) RejectKeyRequest(ctx context.Context, requestID uint64, reason string) error {
status := types.KeyRequestStatus_KEY_REQUEST_STATUS_REJECTED
result := types.NewMsgUpdateKeyRequestReject(reason)

msg := &types.MsgUpdateKeyRequest{
Creator: c.c.Identity.Address.String(),
RequestId: requestID,
Status: status,
Result: result,
}

txBytes, err := c.c.BuildTx(ctx, DefaultGasLimit, DefaultFees, msg)
if err != nil {
return fmt.Errorf("build tx: %w", err)
}
type KeyRequestRejection struct {
RequestID uint64
Reason string
}

if err = c.c.SendWaitTx(ctx, txBytes); err != nil {
return fmt.Errorf("send wait tx: %w", err)
func (r KeyRequestRejection) Msg(creator string) sdk.Msg {
return &types.MsgUpdateKeyRequest{
Creator: creator,
RequestId: r.RequestID,
Status: types.KeyRequestStatus_KEY_REQUEST_STATUS_REJECTED,
Result: types.NewMsgUpdateKeyRequestReject(r.Reason),
}
}

return nil
type SignRequestFulfilment struct {
RequestID uint64
Signature []byte
}

// FulfilSignatureRequest completes a signature request writing the signature bytes to wardend. The sender must be authorized to submit transactions
// for the keychain corresponding to the requestID. The transaction will be rejected if the WardenTxClient does not have the correct identity address.
func (c *WardenTxClient) FulfilSignatureRequest(ctx context.Context, requestID uint64, sig []byte) error {
status := types.SignRequestStatus_SIGN_REQUEST_STATUS_FULFILLED
result := &types.MsgFulfilSignatureRequest_Payload{
Payload: &types.MsgSignedData{
SignedData: sig,
func (r SignRequestFulfilment) Msg(creator string) sdk.Msg {
return &types.MsgFulfilSignatureRequest{
Creator: creator,
RequestId: r.RequestID,
Status: types.SignRequestStatus_SIGN_REQUEST_STATUS_FULFILLED,
Result: &types.MsgFulfilSignatureRequest_Payload{
Payload: &types.MsgSignedData{
SignedData: r.Signature,
},
},
}

msg := &types.MsgFulfilSignatureRequest{
Creator: c.c.Identity.Address.String(),
RequestId: requestID,
Status: status,
Result: result,
}

txBytes, err := c.c.BuildTx(ctx, DefaultGasLimit, DefaultFees, msg)
if err != nil {
return err
}

if err = c.c.SendWaitTx(ctx, txBytes); err != nil {
return err
}

return nil
}

// RejectSignatureRequest notifies wardend that a signature request has been rejected. The sender must be authorized to submit transactions
// for the keychain corresponding to the requestID. The transaction will be rejected if the WardenTxClient does not have the correct identity address.
func (c *WardenTxClient) RejectSignatureRequest(ctx context.Context, requestID uint64, reason string) error {
status := types.SignRequestStatus_SIGN_REQUEST_STATUS_REJECTED
result := &types.MsgFulfilSignatureRequest_RejectReason{RejectReason: reason}

msg := &types.MsgFulfilSignatureRequest{
Creator: c.c.Identity.Address.String(),
RequestId: requestID,
Status: status,
Result: result,
}

txBytes, err := c.c.BuildTx(ctx, DefaultGasLimit, DefaultFees, msg)
if err != nil {
return err
}
type SignRequestRejection struct {
RequestID uint64
Reason string
}

if err = c.c.SendWaitTx(ctx, txBytes); err != nil {
return err
func (r SignRequestRejection) Msg(creator string) sdk.Msg {
return &types.MsgFulfilSignatureRequest{
Creator: creator,
RequestId: r.RequestID,
Status: types.SignRequestStatus_SIGN_REQUEST_STATUS_REJECTED,
Result: &types.MsgFulfilSignatureRequest_RejectReason{
RejectReason: r.Reason,
},
}

return nil
}
33 changes: 29 additions & 4 deletions keychain-sdk/key_requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package keychain

import (
"context"
"encoding/hex"
"log/slog"
"time"

"github.com/warden-protocol/wardenprotocol/go-client"
Expand All @@ -19,16 +21,28 @@ type KeyRequestHandler func(w KeyResponseWriter, req *KeyRequest)

type keyResponseWriter struct {
ctx context.Context
tx *client.TxClient
txWriter *TxWriter
keyRequestID uint64
logger *slog.Logger
onComplete func()
}

func (w *keyResponseWriter) Fulfil(publicKey []byte) error {
return w.tx.FulfilKeyRequest(w.ctx, w.keyRequestID, publicKey)
w.logger.Debug("fulfilling key request", "id", w.keyRequestID, "public_key", hex.EncodeToString(publicKey))
defer w.onComplete()
return w.txWriter.Write(w.ctx, client.KeyRequestFulfilment{
RequestID: w.keyRequestID,
PublicKey: publicKey,
})
}

func (w *keyResponseWriter) Reject(reason string) error {
return w.tx.RejectKeyRequest(w.ctx, w.keyRequestID, reason)
w.logger.Debug("rejecting key request", "id", w.keyRequestID, "reason", reason)
defer w.onComplete()
return w.txWriter.Write(w.ctx, client.KeyRequestRejection{
RequestID: w.keyRequestID,
Reason: reason,
})
}

func (a *App) ingestKeyRequests(keyRequestsCh chan *wardentypes.KeyRequest) {
Expand All @@ -40,7 +54,13 @@ func (a *App) ingestKeyRequests(keyRequestsCh chan *wardentypes.KeyRequest) {
a.logger().Error("failed to get key requests", "error", err)
} else {
for _, keyRequest := range keyRequests {
if !a.keyRequestTracker.IsNew(keyRequest.Id) {
a.logger().Debug("skipping key request", "id", keyRequest.Id)
continue
}

a.logger().Info("got key request", "id", keyRequest.Id)
a.keyRequestTracker.Ingested(keyRequest.Id)
keyRequestsCh <- keyRequest
}
}
Expand All @@ -59,13 +79,18 @@ func (a *App) handleKeyRequest(keyRequest *wardentypes.KeyRequest) {
ctx := context.Background()
w := &keyResponseWriter{
ctx: ctx,
tx: a.tx,
txWriter: a.txWriter,
keyRequestID: keyRequest.Id,
logger: a.logger(),
onComplete: func() {
a.keyRequestTracker.Done(keyRequest.Id)
},
}
defer func() {
if r := recover(); r != nil {
a.logger().Error("panic in key request handler", "error", r)
_ = w.Reject("internal error")
return
}
}()

Expand Down
28 changes: 23 additions & 5 deletions keychain-sdk/keychain.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"fmt"
"io"
"log/slog"
"time"

"github.com/warden-protocol/wardenprotocol/go-client"
wardentypes "github.com/warden-protocol/wardenprotocol/warden/x/warden/types"
Expand All @@ -22,13 +23,17 @@ type App struct {
keyRequestHandler KeyRequestHandler
signRequestHandler SignRequestHandler

query *client.QueryClient
tx *client.TxClient
query *client.QueryClient
txWriter *TxWriter
keyRequestTracker *RequestTracker
signRequestTracker *RequestTracker
}

func NewApp(config Config) *App {
return &App{
config: config,
config: config,
keyRequestTracker: NewRequestTracker(),
signRequestTracker: NewRequestTracker(),
}
}

Expand Down Expand Up @@ -57,15 +62,27 @@ func (a *App) Start(ctx context.Context) error {
}

keyRequestsCh := make(chan *wardentypes.KeyRequest)
defer close(keyRequestsCh)
go a.ingestKeyRequests(keyRequestsCh)

signRequestsCh := make(chan *wardentypes.SignRequest)
defer close(signRequestsCh)
go a.ingestSignRequests(signRequestsCh)

flushErrors := make(chan error)
defer close(flushErrors)
go func() {
if err := a.txWriter.Start(ctx, flushErrors); err != nil {
a.logger().Error("tx writer exited with error", "error", err)
}
}()

for {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-flushErrors:
a.logger().Error("tx writer flush error", "error", err)
case keyRequest := <-keyRequestsCh:
go a.handleKeyRequest(keyRequest)
case signRequest := <-signRequestsCh:
Expand Down Expand Up @@ -95,9 +112,10 @@ func (a *App) initConnections() error {

a.logger().Info("keychain party identity", "address", identity.Address.String())

a.tx = client.NewTxClient(identity, a.config.ChainID, conn, query)
txClient := client.NewTxClient(identity, a.config.ChainID, conn, query)
a.txWriter = NewTxWriter(txClient, 20, 8*time.Second, a.logger())

return nil
}

var defaultPageLimit = uint64(10)
var defaultPageLimit = uint64(20)
Loading

0 comments on commit 79af474

Please sign in to comment.