From 83f94f19f25ef76748c75beff5ac87bff562fffc Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:51:28 +0530 Subject: [PATCH 01/15] config: add mainnet gas price support --- config/config.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 3b172ff..613522d 100644 --- a/config/config.go +++ b/config/config.go @@ -17,6 +17,7 @@ const ( DefaultFolderName = ".fairyringclient" DefaultChainID = "fairyring-testnet-3" DefaultDenom = "ufair" + DefaultGasPrice = "0.025" ) type Node struct { @@ -26,6 +27,7 @@ type Node struct { GRPCPort uint64 Denom string ChainID string + GasPrice string } type Config struct { @@ -75,7 +77,7 @@ func (c *Config) SaveConfig() error { updateConfig(*c) if err := viper.WriteConfig(); err != nil { - fmt.Errorf("failed to write config as : %s", err.Error()) + return fmt.Errorf("failed to write config: %w", err) } return nil @@ -84,7 +86,7 @@ func (c *Config) SaveConfig() error { func (c *Config) ExportConfig() error { homeDir, err := os.UserHomeDir() if err != nil { - log.Fatal(err) + return err } if _, err := os.Stat(homeDir + "/" + DefaultFolderName); os.IsNotExist(err) { @@ -97,7 +99,6 @@ func (c *Config) ExportConfig() error { filePath := filepath.Join(homeDir+"/"+DefaultFolderName, "config.yml") _, err = os.Stat(filePath) if os.IsNotExist(err) { - // File does not exist, create it log.Println("Initializing FairyRing Client default config...") file, err := os.Create(filePath) @@ -117,7 +118,7 @@ func (c *Config) ExportConfig() error { setInitialConfig(*c) if err = viper.WriteConfigAs(homeDir + "/" + DefaultFolderName + "/config.yml"); err != nil { - fmt.Errorf("failed to write config as : %s", err.Error()) + return fmt.Errorf("failed to write config: %w", err) } return nil @@ -138,6 +139,7 @@ func DefaultConfig(withCosmosKey bool) Config { GRPCPort: 9090, Denom: DefaultDenom, ChainID: DefaultChainID, + GasPrice: DefaultGasPrice, }, PrivateKey: privateKey, TotalValidatorNum: 0, @@ -155,6 +157,7 @@ func updateConfig(c Config) { viper.Set("FairyRingNode.grpcPort", c.FairyRingNode.GRPCPort) viper.Set("FairyRingNode.denom", c.FairyRingNode.Denom) viper.Set("FairyRingNode.chainID", c.FairyRingNode.ChainID) + viper.Set("FairyRingNode.gasPrice", c.FairyRingNode.GasPrice) viper.Set("PrivateKey", c.PrivateKey) @@ -170,6 +173,7 @@ func setInitialConfig(c Config) { viper.SetDefault("FairyRingNode.grpcPort", c.FairyRingNode.GRPCPort) viper.SetDefault("FairyRingNode.denom", c.FairyRingNode.Denom) viper.SetDefault("FairyRingNode.chainID", c.FairyRingNode.ChainID) + viper.SetDefault("FairyRingNode.gasPrice", c.FairyRingNode.GasPrice) viper.SetDefault("PrivateKey", c.PrivateKey) From 106717259a99024c54374aacea35633de51b05a4 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:51:53 +0530 Subject: [PATCH 02/15] config: expose gas price update flag --- cmd/config_update.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/config_update.go b/cmd/config_update.go index 9ff3db8..f1b6d35 100644 --- a/cmd/config_update.go +++ b/cmd/config_update.go @@ -3,6 +3,7 @@ package cmd import ( "fairyringclient/config" "fmt" + "github.com/spf13/cobra" ) @@ -20,6 +21,7 @@ var configUpdateCmd = &cobra.Command{ chainID, _ := cmd.Flags().GetString("chain-id") chainDenom, _ := cmd.Flags().GetString("denom") + chainGasPrice, _ := cmd.Flags().GetString("gas-price") chainIP, _ := cmd.Flags().GetString("ip") chainProtocol, _ := cmd.Flags().GetString("protocol") chainGrpcPort, _ := cmd.Flags().GetUint64("grpc-port") @@ -35,6 +37,7 @@ var configUpdateCmd = &cobra.Command{ GRPCPort: chainGrpcPort, Denom: chainDenom, ChainID: chainID, + GasPrice: chainGasPrice, } cfg.InvalidSharePauseThreshold = pauseThreshold @@ -59,6 +62,7 @@ func init() { configUpdateCmd.Flags().String("chain-id", cfg.FairyRingNode.ChainID, "Update config chain id") configUpdateCmd.Flags().String("denom", cfg.FairyRingNode.Denom, "Update config denom") + configUpdateCmd.Flags().String("gas-price", cfg.FairyRingNode.GasPrice, "Update config gas price in denom units per gas") configUpdateCmd.Flags().Uint64("grpc-port", cfg.FairyRingNode.GRPCPort, "Update config grpc-port") configUpdateCmd.Flags().String("ip", cfg.FairyRingNode.IP, "Update config node ip address") configUpdateCmd.Flags().Uint64("port", cfg.FairyRingNode.Port, "Update config node port") From 63a629daae303d66ee646b98278b2e71a3453dc8 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:52:09 +0530 Subject: [PATCH 03/15] config: show configured gas price --- cmd/config_show.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/config_show.go b/cmd/config_show.go index 28cbe60..f7b796b 100644 --- a/cmd/config_show.go +++ b/cmd/config_show.go @@ -23,9 +23,10 @@ var configShowCmd = &cobra.Command{ FairyRing Node Endpoint: %s Chain ID: %s Chain Denom: %s +Gas Price: %s InvalidSharePauseThreshold: %d MetricsPort: %d SubmitBlockwiseKeyshares: %t -`, cfg.GetGRPCEndpoint(), cfg.GetFairyRingNodeURI(), cfg.FairyRingNode.ChainID, cfg.FairyRingNode.Denom, cfg.InvalidSharePauseThreshold, cfg.MetricsPort, cfg.SubmitBlockwiseKeyshares) +`, cfg.GetGRPCEndpoint(), cfg.GetFairyRingNodeURI(), cfg.FairyRingNode.ChainID, cfg.FairyRingNode.Denom, cfg.FairyRingNode.GasPrice, cfg.InvalidSharePauseThreshold, cfg.MetricsPort, cfg.SubmitBlockwiseKeyshares) }, } From 017834919283ed30de73be2a35250022b71dbd90 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:52:20 +0530 Subject: [PATCH 04/15] version: report v1.0.1 correctly --- cmd/version.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/version.go b/cmd/version.go index dfac33d..cb486bc 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -2,10 +2,11 @@ package cmd import ( "fmt" + "github.com/spf13/cobra" ) -const ClientVersion = "v0.7.1" +const ClientVersion = "v1.0.1" // configCmd represents the config command var versionCmd = &cobra.Command{ From b0c9d6e961ae5b278f0d991629b83823ec6763fe Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:52:32 +0530 Subject: [PATCH 05/15] test: cover fee and blockwise defaults --- config/config_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 config/config_test.go diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..3689af8 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,17 @@ +package config + +import "testing" + +func TestDefaultConfigIncludesMainnetFeeSettings(t *testing.T) { + cfg := DefaultConfig(false) + + if cfg.FairyRingNode.Denom != DefaultDenom { + t.Fatalf("unexpected default denom: got %q want %q", cfg.FairyRingNode.Denom, DefaultDenom) + } + if cfg.FairyRingNode.GasPrice != DefaultGasPrice { + t.Fatalf("unexpected default gas price: got %q want %q", cfg.FairyRingNode.GasPrice, DefaultGasPrice) + } + if cfg.SubmitBlockwiseKeyshares { + t.Fatal("blockwise keyshare submission must default to disabled") + } +} From 81710ae6d2bdd096e493e19cf537335070be75ae Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:53:27 +0530 Subject: [PATCH 06/15] tx: add fees and serialize account submissions --- pkg/cosmosClient/client.go | 191 ++++++++++++++++++++++--------------- 1 file changed, 114 insertions(+), 77 deletions(-) diff --git a/pkg/cosmosClient/client.go b/pkg/cosmosClient/client.go index 57b5002..46848c0 100644 --- a/pkg/cosmosClient/client.go +++ b/pkg/cosmosClient/client.go @@ -5,18 +5,14 @@ import ( "encoding/base64" "encoding/hex" "fmt" - distIBE "github.com/FairBlock/DistributedIBE" - keysharetypes "github.com/Fairblock/fairyring/x/keyshare/types" - peptypes "github.com/Fairblock/fairyring/x/pep/types" - dcrdSecp256k1 "github.com/decred/dcrd/dcrec/secp256k1" - bls "github.com/drand/kyber-bls12381" - "github.com/skip-mev/block-sdk/v2/testutils" "log" "strings" "time" "cosmossdk.io/math" - + distIBE "github.com/FairBlock/DistributedIBE" + keysharetypes "github.com/Fairblock/fairyring/x/keyshare/types" + peptypes "github.com/Fairblock/fairyring/x/pep/types" clienttx "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" @@ -26,7 +22,10 @@ import ( authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + dcrdSecp256k1 "github.com/decred/dcrd/dcrec/secp256k1" + bls "github.com/drand/kyber-bls12381" "github.com/pkg/errors" + "github.com/skip-mev/block-sdk/v2/testutils" "google.golang.org/grpc" ) @@ -55,6 +54,8 @@ type CosmosClient struct { account authtypes.BaseAccount accAddress cosmostypes.AccAddress chainID string + feeDenom string + gasPrice math.LegacyDec txQueue chan QueuedTx } @@ -62,6 +63,8 @@ func NewCosmosClient( endpoint string, privateKeyHex string, chainID string, + feeDenom string, + gasPriceString string, ) (*CosmosClient, error) { grpcConn, err := grpc.Dial( endpoint, @@ -71,6 +74,18 @@ func NewCosmosClient( return nil, err } + if feeDenom == "" { + return nil, errors.New("fee denom must not be empty") + } + + gasPrice, err := math.LegacyNewDecFromStr(gasPriceString) + if err != nil { + return nil, errors.Wrap(err, "invalid gas price") + } + if !gasPrice.IsPositive() { + return nil, errors.New("gas price must be positive") + } + authClient := authtypes.NewQueryClient(grpcConn) bankClient := banktypes.NewQueryClient(grpcConn) pepeClient := peptypes.NewQueryClient(grpcConn) @@ -80,6 +95,9 @@ func NewCosmosClient( if err != nil { return nil, err } + if len(keyBytes) != 32 { + return nil, errors.New("private key must decode to exactly 32 bytes") + } privateKey := secp256k1.PrivKey{Key: keyBytes} pubKey := privateKey.PubKey() @@ -101,14 +119,12 @@ func NewCosmosClient( context.Background(), &authtypes.QueryAccountRequest{Address: addr}, ) - if err != nil { log.Println(cosmostypes.AccAddress(address).String()) return nil, err } - err = baseAccount.Unmarshal(resp.Account.Value) - if err != nil { + if err = baseAccount.Unmarshal(resp.Account.Value); err != nil { return nil, err } @@ -125,18 +141,22 @@ func NewCosmosClient( accAddress: accAddr, publicKey: pubKey, chainID: chainID, + feeDenom: feeDenom, + gasPrice: gasPrice, txQueue: make(chan QueuedTx, 1), }, nil } func (c *CosmosClient) updateAccSequence() error { - out, err := c.authClient.Account(context.Background(), - &authtypes.QueryAccountRequest{Address: c.accAddress.String()}) + out, err := c.authClient.Account( + context.Background(), + &authtypes.QueryAccountRequest{Address: c.accAddress.String()}, + ) if err != nil { return err } - var baseAccount authtypes.BaseAccount + var baseAccount authtypes.BaseAccount if err = baseAccount.Unmarshal(out.Account.Value); err != nil { return err } @@ -148,9 +168,7 @@ func (c *CosmosClient) updateAccSequence() error { func (c *CosmosClient) IsAddrAuthorized(target string) bool { resp, err := c.keyshareQueryClient.AuthorizedAddress( context.Background(), - &keysharetypes.QueryAuthorizedAddressRequest{ - Target: target, - }, + &keysharetypes.QueryAuthorizedAddressRequest{Target: target}, ) if err != nil { return false @@ -187,7 +205,6 @@ func (c *CosmosClient) GetKeyShare(getPendingShare bool) (*distIBE.Share, uint64 } targetEncKeyShareList := pubKey.ActivePubkey.EncryptedKeyshares - if getPendingShare { targetEncKeyShareList = pubKey.QueuedPubkey.EncryptedKeyshares } @@ -197,26 +214,29 @@ func (c *CosmosClient) GetKeyShare(getPendingShare bool) (*distIBE.Share, uint64 } for index, val := range targetEncKeyShareList { - if val.Validator == c.GetAddress() { - decryptedByte, err := c.decryptShare(val.Data) - if err != nil { - return nil, 0, 0, err - } - keyShareIndex := index + 1 - parsedShare, err := c.parseShare(decryptedByte, int64(keyShareIndex)) - if err != nil { - return nil, 0, 0, err - } + if val.Validator != c.GetAddress() { + continue + } - expiryHeight := pubKey.ActivePubkey.Expiry + decryptedByte, err := c.decryptShare(val.Data) + if err != nil { + return nil, 0, 0, err + } - if getPendingShare { - expiryHeight = pubKey.QueuedPubkey.Expiry - } + keyShareIndex := index + 1 + parsedShare, err := c.parseShare(decryptedByte, int64(keyShareIndex)) + if err != nil { + return nil, 0, 0, err + } - return parsedShare, uint64(keyShareIndex), expiryHeight, nil + expiryHeight := pubKey.ActivePubkey.Expiry + if getPendingShare { + expiryHeight = pubKey.QueuedPubkey.Expiry } + + return parsedShare, uint64(keyShareIndex), expiryHeight, nil } + return nil, 0, 0, errors.New("encrypted share for your validator not found") } @@ -288,33 +308,39 @@ func (c *CosmosClient) HandleTxQueue() error { continue } - go func(qTx QueuedTx) { - txBytes, err := c.signTxMsg(*qTx.Tx, qTx.AdjustGas) - if err != nil { - qTx.TxResultErrHandler(errors.New(fmt.Sprintf("Error signing tx: %s", err.Error()))) - return + txBytes, err := c.signTxMsg(*queuedTx.Tx, queuedTx.AdjustGas) + if err != nil { + if queuedTx.TxResultErrHandler != nil { + queuedTx.TxResultErrHandler(fmt.Errorf("error signing tx: %w", err)) } - resp, err := c.txClient.BroadcastTx( - context.Background(), - &tx.BroadcastTxRequest{ - TxBytes: txBytes, - Mode: tx.BroadcastMode_BROADCAST_MODE_SYNC, - }, - ) - if err != nil { - log.Printf("Error broadcasting tx in Tx queue handler: %v", err) - if qTx.TxResultErrHandler != nil { - qTx.TxResultErrHandler(err) - } - return + continue + } + + resp, err := c.txClient.BroadcastTx( + context.Background(), + &tx.BroadcastTxRequest{ + TxBytes: txBytes, + Mode: tx.BroadcastMode_BROADCAST_MODE_SYNC, + }, + ) + if err != nil { + log.Printf("Error broadcasting tx in Tx queue handler: %v", err) + if queuedTx.TxResultErrHandler != nil { + queuedTx.TxResultErrHandler(err) } - if resp.TxResponse.Code != 0 { - qTx.TxResultErrHandler(errors.New(fmt.Sprintf("Error broadcasting tx: %s", resp.TxResponse.RawLog))) - return + continue + } + + if resp.TxResponse.Code != 0 { + if queuedTx.TxResultErrHandler != nil { + queuedTx.TxResultErrHandler(fmt.Errorf("error broadcasting tx: %s", resp.TxResponse.RawLog)) } - c.WaitForQueuedTx(qTx, resp.TxResponse.TxHash) - }(queuedTx) + continue + } + // Process queued transactions serially all the way through commit so the + // next item observes the incremented account sequence. + c.WaitForQueuedTx(queuedTx, resp.TxResponse.TxHash) } } @@ -325,7 +351,10 @@ func (c *CosmosClient) WaitForQueuedTx(q QueuedTx, txHash string) { if q.TxResultErrHandler != nil { q.TxResultErrHandler(err) } - } else if q.TxSuccessHandler != nil { + return + } + + if q.TxSuccessHandler != nil { q.TxSuccessHandler(getTxResp) } } @@ -350,18 +379,15 @@ func (c *CosmosClient) BroadcastTx(msg cosmostypes.Msg, adjustGas bool) (*tx.Get if err != nil { return nil, err } - - for { - getTxResp, err := c.txClient.GetTx(context.Background(), &tx.GetTxRequest{Hash: resp.TxResponse.TxHash}) - if err != nil { - if strings.Contains(err.Error(), "not found") { - time.Sleep(time.Second) - continue - } - return nil, err - } - return getTxResp, err + if resp.TxResponse.Code != 0 { + return nil, errors.Errorf( + "error code: '%d' msg: '%s'", + resp.TxResponse.Code, + resp.TxResponse.RawLog, + ) } + + return c.WaitForTx(resp.TxResponse.TxHash, time.Second) } func (c *CosmosClient) decryptShare(shareCipher string) ([]byte, error) { @@ -380,10 +406,10 @@ func (c *CosmosClient) decryptShare(shareCipher string) ([]byte, error) { func (c *CosmosClient) parseShare(shareByte []byte, index int64) (*distIBE.Share, error) { parsedShare := bls.NewKyberScalar() - err := parsedShare.UnmarshalBinary(shareByte) - if err != nil { + if err := parsedShare.UnmarshalBinary(shareByte); err != nil { return nil, err } + return &distIBE.Share{ Index: bls.NewKyberScalar().SetInt64(index), Value: parsedShare, @@ -400,26 +426,29 @@ func (c *CosmosClient) WaitForTx(hash string, rate time.Duration) (*tx.GetTxResp } return nil, err } - return resp, err + return resp, nil } } +func calculateFeeAmount(gasPrice math.LegacyDec, gasLimit uint64) math.Int { + return gasPrice.MulInt(math.NewIntFromUint64(gasLimit)).Ceil().TruncateInt() +} + func (c *CosmosClient) signTxMsg(msg cosmostypes.Msg, adjustGas bool) ([]byte, error) { encodingCfg := testutils.CreateTestEncodingConfig() txBuilder := encodingCfg.TxConfig.NewTxBuilder() encodingCfg.TxConfig.SignModeHandler().DefaultMode() - err := txBuilder.SetMsgs(msg) - if err != nil { + if err := txBuilder.SetMsgs(msg); err != nil { return nil, err } - + if err := c.updateAccSequence(); err != nil { log.Printf("Error updating Account sequence in Tx queue handler: %v", err) return nil, err } - var newGasLimit uint64 = defaultGasLimit + newGasLimit := uint64(defaultGasLimit) if adjustGas { txf := clienttx.Factory{}. WithGas(defaultGasLimit). @@ -430,7 +459,7 @@ func (c *CosmosClient) signTxMsg(msg cosmostypes.Msg, adjustGas bool) ([]byte, e WithSequence(c.account.Sequence). WithGasAdjustment(defaultGasAdjustment) - _, newGasLimit, err = clienttx.CalculateGas(c.grpcConn, txf, msg) + _, newGasLimit, err := clienttx.CalculateGas(c.grpcConn, txf, msg) if err != nil { return nil, err } @@ -438,6 +467,12 @@ func (c *CosmosClient) signTxMsg(msg cosmostypes.Msg, adjustGas bool) ([]byte, e txBuilder.SetGasLimit(newGasLimit) + feeAmount := calculateFeeAmount(c.gasPrice, newGasLimit) + if !feeAmount.IsPositive() { + return nil, errors.New("calculated transaction fee must be positive") + } + txBuilder.SetFeeAmount(cosmostypes.NewCoins(cosmostypes.NewCoin(c.feeDenom, feeAmount))) + signerData := authsigning.SignerData{ ChainID: c.chainID, AccountNumber: c.account.AccountNumber, @@ -464,12 +499,14 @@ func (c *CosmosClient) signTxMsg(msg cosmostypes.Msg, adjustGas bool) ([]byte, e context.Background(), 1, signerData, txBuilder, &c.privateKey, encodingCfg.TxConfig, c.account.Sequence, ) - - err = txBuilder.SetSignatures(sigV2) if err != nil { return nil, err } + if err = txBuilder.SetSignatures(sigV2); err != nil { + return nil, err + } + txBytes, err := encodingCfg.TxConfig.TxEncoder()(txBuilder.GetTx()) if err != nil { return nil, err From e1b0ddfc780471b866de3a67e6a0c1210d2df089 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:53:44 +0530 Subject: [PATCH 07/15] test: cover configured fee calculation --- pkg/cosmosClient/client_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 pkg/cosmosClient/client_test.go diff --git a/pkg/cosmosClient/client_test.go b/pkg/cosmosClient/client_test.go new file mode 100644 index 0000000..f9aa879 --- /dev/null +++ b/pkg/cosmosClient/client_test.go @@ -0,0 +1,22 @@ +package cosmosClient + +import ( + "testing" + + "cosmossdk.io/math" +) + +func TestCalculateFeeAmountRoundsUp(t *testing.T) { + gasPrice, err := math.LegacyNewDecFromStr("0.025") + if err != nil { + t.Fatal(err) + } + + if got := calculateFeeAmount(gasPrice, 300000).String(); got != "7500" { + t.Fatalf("unexpected fee: got %s want 7500", got) + } + + if got := calculateFeeAmount(gasPrice, 1).String(); got != "1" { + t.Fatalf("fee must round up: got %s want 1", got) + } +} From 07ee196df4a2c22ac9021b86e116f54c9d305841 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:54:03 +0530 Subject: [PATCH 08/15] delegate: use configured transaction fees --- cmd/delegate_add.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/delegate_add.go b/cmd/delegate_add.go index 868093b..c2855d0 100644 --- a/cmd/delegate_add.go +++ b/cmd/delegate_add.go @@ -4,9 +4,10 @@ import ( "fairyringclient/config" "fairyringclient/pkg/cosmosClient" "fmt" + "log" + "github.com/Fairblock/fairyring/x/keyshare/types" "github.com/spf13/cobra" - "log" ) // delegateAdd represents the delegate add command @@ -16,7 +17,6 @@ var delegateAdd = &cobra.Command{ Long: `Authorize an address for submitting key share`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - cfg, err := config.ReadConfigFromFile() if err != nil { fmt.Printf("Error loading config from file: %s\n", err.Error()) @@ -33,8 +33,9 @@ var delegateAdd = &cobra.Command{ gRPCEndpoint, cfg.PrivateKey, cfg.FairyRingNode.ChainID, + cfg.FairyRingNode.Denom, + cfg.FairyRingNode.GasPrice, ) - if err != nil { log.Fatalf("Error creating custom cosmos client, make sure provided account is activated: %v\n", err) } @@ -49,7 +50,6 @@ var delegateAdd = &cobra.Command{ } txResp, err := eachClient.BroadcastTx(&msg, false) - if err != nil { log.Fatalf("unable to broadcast create authorized address message, ERROR: %s\n", err.Error()) } From f95c0960f2064115df2552d5056b25493f80fafc Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:54:22 +0530 Subject: [PATCH 09/15] delegate: use configured transaction fees --- cmd/delegate_remove.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/delegate_remove.go b/cmd/delegate_remove.go index 0b9d0ae..b0765f4 100644 --- a/cmd/delegate_remove.go +++ b/cmd/delegate_remove.go @@ -4,9 +4,10 @@ import ( "fairyringclient/config" "fairyringclient/pkg/cosmosClient" "fmt" + "log" + "github.com/Fairblock/fairyring/x/keyshare/types" "github.com/spf13/cobra" - "log" ) // delegateRemove represents the delegate add command @@ -16,7 +17,6 @@ var delegateRemove = &cobra.Command{ Long: `Remove an authorized address for submitting key share`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - cfg, err := config.ReadConfigFromFile() if err != nil { fmt.Printf("Error loading config from file: %s\n", err.Error()) @@ -33,8 +33,9 @@ var delegateRemove = &cobra.Command{ gRPCEndpoint, cfg.PrivateKey, cfg.FairyRingNode.ChainID, + cfg.FairyRingNode.Denom, + cfg.FairyRingNode.GasPrice, ) - if err != nil { log.Fatalf("Error creating custom cosmos client, make sure provided account is activated: %v\n", err) } @@ -49,7 +50,6 @@ var delegateRemove = &cobra.Command{ } txResp, err := eachClient.BroadcastTx(&msg, false) - if err != nil { log.Fatalf("unable to broadcast delete authorized address message, ERROR: %s\n", err.Error()) } From a7ef92264f1535ecdfaa744985ff2e4ad8b36a83 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:55:19 +0530 Subject: [PATCH 10/15] client: secure general and private keyshare handling --- internal/fairyringclient/fairyringclient.go | 83 +++++++-------------- 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/internal/fairyringclient/fairyringclient.go b/internal/fairyringclient/fairyringclient.go index e73cf99..c390fe9 100644 --- a/internal/fairyringclient/fairyringclient.go +++ b/internal/fairyringclient/fairyringclient.go @@ -7,32 +7,26 @@ import ( "fairyringclient/config" "fairyringclient/pkg/cosmosClient" "fmt" + "log" "net/http" + "strconv" "strings" + "time" + "github.com/Fairblock/fairyring/x/keyshare/types" "github.com/btcsuite/btcd/btcec" + abciTypes "github.com/cometbft/cometbft/abci/types" + tmclient "github.com/cometbft/cometbft/rpc/client/http" + coretypes "github.com/cometbft/cometbft/rpc/core/types" + tmtypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/types/tx" "github.com/pkg/errors" - - "github.com/Fairblock/fairyring/x/keyshare/types" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" - - "log" - "strconv" - "time" - - tmclient "github.com/cometbft/cometbft/rpc/client/http" - coretypes "github.com/cometbft/cometbft/rpc/core/types" - tmtypes "github.com/cometbft/cometbft/types" - - abciTypes "github.com/cometbft/cometbft/abci/types" ) -var ( - validatorCosmosClient *ValidatorClients -) +var validatorCosmosClient *ValidatorClients var ( invalidShareSubmitted = promauto.NewCounter(prometheus.CounterOpts{ @@ -62,8 +56,7 @@ var ( ) func StartFairyRingClient(cfg config.Config) { - - PauseThreshold := cfg.InvalidSharePauseThreshold + pauseThreshold := cfg.InvalidSharePauseThreshold submitBlockwiseKeyshares := cfg.SubmitBlockwiseKeyshares vCosmosClient, client, err := InitializeValidatorClient(cfg) @@ -187,14 +180,12 @@ func StartFairyRingClient(cfg config.Config) { if hasCoinSpentEvent(txResp.TxResponse.Events) { validatorCosmosClient.IncreaseInvalidShareNum() log.Printf("KeyShare for Height %s is INVALID, Got Slashed, Current number invalid share in a row: %d\n", processHeightStr, validatorCosmosClient.InvalidShareInARow) - defer invalidShareSubmitted.Inc() - if validatorCosmosClient.InvalidShareInARow >= PauseThreshold { + if validatorCosmosClient.InvalidShareInARow >= pauseThreshold { validatorCosmosClient.Pause() - log.Printf("Client paused due to number of invalid share in a row '%d' reaches threshold '%d', Waiting until next round\n", validatorCosmosClient.InvalidShareInARow, PauseThreshold) + log.Printf("Client paused due to number of invalid share in a row '%d' reaches threshold '%d', Waiting until next round\n", validatorCosmosClient.InvalidShareInARow, pauseThreshold) } - return } @@ -203,6 +194,7 @@ func StartFairyRingClient(cfg config.Config) { defer failedShareSubmitted.Inc() return } + log.Printf("Submit KeyShare for Height %s Confirmed\n", processHeightStr) latestSubmitKeyshare.Set(float64(processHeight)) defer validShareSubmitted.Inc() @@ -215,7 +207,6 @@ func StartFairyRingClient(cfg config.Config) { func InitializeValidatorClient(cfg config.Config) (*ValidatorClients, *tmclient.HTTP, error) { denom := cfg.FairyRingNode.Denom - if len(denom) == 0 { return nil, nil, errors.New("denom not found in config") } @@ -247,8 +238,9 @@ func InitializeValidatorClient(cfg config.Config) (*ValidatorClients, *tmclient. gRPCEndpoint, cfg.PrivateKey, cfg.FairyRingNode.ChainID, + cfg.FairyRingNode.Denom, + cfg.FairyRingNode.GasPrice, ) - if err != nil { return nil, nil, errors.Wrap(err, "error creating custom cosmos client, make sure provided account is activated") } @@ -282,10 +274,8 @@ func handleTxEvents(txOut <-chan coretypes.ResultEvent) { switch k { case "queued-pubkey-created.pubkey": handleNewPubKeyEvent(result.Events) - break case "pubkey-overrode.pubkey": handlePubKeyOverrodeEvent(result.Events) - break } } } @@ -297,13 +287,12 @@ func handleEndBlockEvents(events []abciTypes.Event) { if e.Type == "start-send-encrypted-keyshare" { var id, secpPubkey, requester string for _, a := range e.Attributes { - if a.Key == "identity" { + switch a.Key { + case "identity": id = a.Value - } - if a.Key == "requester" { + case "requester": requester = a.Value - } - if a.Key == "secp256k1-pubkey" { + case "secp256k1-pubkey": secpPubkey = a.Value } } @@ -320,8 +309,8 @@ func handleEndBlockEvents(events []abciTypes.Event) { if e.Type != "start-send-general-keyshare" { continue } - for _, a := range e.Attributes { + for _, a := range e.Attributes { if a.Key != "identity" { continue } @@ -329,11 +318,11 @@ func handleEndBlockEvents(events []abciTypes.Event) { identity := a.Value if len(identity) < 1 { log.Printf("Empty Identity detected in start send general key share event") - return + break } handleStartSubmitGeneralKeyShareEvent(identity) - return + break } } } @@ -343,17 +332,15 @@ func handleStartSubmitEncryptedKeyShareEvent( secpPubkey string, requester string, ) { - log.Printf("Start Submitting Encrypted Key Share for identity: %s pubkey: %s requester: %s", identity, secpPubkey, requester) + log.Printf("Start Submitting Encrypted Key Share for identity: %s requester: %s", identity, requester) derivedShare, index, err := validatorCosmosClient.DeriveKeyShare([]byte(identity)) if err != nil { log.Fatal(err) } - log.Printf("Derived Private Key Share: %s\n", derivedShare) - // Encrypt the message encryptedMessage, err := encryptWithPublicKey(derivedShare, secpPubkey) if err != nil { - fmt.Printf("Error encrypting message: %s\n", err) + log.Printf("Error encrypting private key share: %s\n", err) return } @@ -372,21 +359,17 @@ func handleStartSubmitEncryptedKeyShareEvent( if txResp.TxResponse.Code != 0 { log.Printf("Private KeyShare for Identity %s Requester %s Failed: %s\n", identity, requester, txResp.TxResponse.RawLog) return - } else { - log.Printf("Private KeyShare for Identity %s Requester %s Confirmed\n", identity, requester) } + log.Printf("Private KeyShare for Identity %s Requester %s Confirmed\n", identity, requester) }) } -// This function encrypts data using an RSA public key. func encryptWithPublicKey(data string, pubKeyBase64 string) (string, error) { - // Decode the base64 public key pubKeyBytes, err := base64.StdEncoding.DecodeString(pubKeyBase64) if err != nil { return "", err } - // Load the secp256k1 public key pubKey, err := btcec.ParsePubKey(pubKeyBytes, btcec.S256()) if err != nil { return "", err @@ -397,7 +380,6 @@ func encryptWithPublicKey(data string, pubKeyBase64 string) (string, error) { return "", err } - // Encode ciphertext as hex for easy handling return hex.EncodeToString(ciphertext), nil } @@ -407,7 +389,6 @@ func handleStartSubmitGeneralKeyShareEvent(identity string) { if err != nil { log.Fatal(err) } - log.Printf("Derived General Key Share: %s\n", derivedShare) validatorCosmosClient.CosmosClient.AddTxToQueue(&types.MsgSubmitGeneralKeyshare{ Creator: validatorCosmosClient.CosmosClient.GetAddress(), @@ -419,18 +400,15 @@ func handleStartSubmitGeneralKeyShareEvent(identity string) { func(err error) { log.Printf("Submit General KeyShare for Identity %s ERROR: %s\n", identity, err.Error()) if strings.Contains(err.Error(), "account sequence") { - go func(id string) { - handleStartSubmitGeneralKeyShareEvent(id) - }(identity) + go handleStartSubmitGeneralKeyShareEvent(identity) } }, func(txResp *tx.GetTxResponse) { if txResp.TxResponse.Code != 0 { log.Printf("General KeyShare for Identity %s Failed: %s\n", identity, txResp.TxResponse.RawLog) return - } else { - log.Printf("Submit General KeyShare for Identity %s Confirmed\n", identity) } + log.Printf("Submit General KeyShare for Identity %s Confirmed\n", identity) }) } @@ -449,8 +427,7 @@ func handlePubKeyOverrodeEvent(data map[string][]string) { continue } log.Printf( - "Successfully Updated Shares for the current overrode round: %s | Index: %d", - validatorCosmosClient.CurrentShare.Share.Value.String(), + "Successfully Updated Share for the current overrode round | Index: %d", validatorCosmosClient.CurrentShare.Index, ) validatorCosmosClient.RemovePendingShare() @@ -466,7 +443,6 @@ func handleNewPubKeyEvent(data map[string][]string) { log.Printf("New Pubkey found: %s\n", pubKey[0]) - // Get Share & Commits on chain few blocks later for { err := validatorCosmosClient.UpdateKeyShareFromChain(true) if err != nil { @@ -474,8 +450,7 @@ func handleNewPubKeyEvent(data map[string][]string) { continue } log.Printf( - "Successfully Updated Shares for next round: %s | Index: %d", - validatorCosmosClient.PendingShare.Share.Value.String(), + "Successfully Updated Share for next round | Index: %d", validatorCosmosClient.PendingShare.Index, ) break From 0d7c1a612ae6aa8f61b7e79da27861387ebd1180 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:55:58 +0530 Subject: [PATCH 11/15] logging: remove raw keyshare material --- internal/fairyringclient/types.go | 48 +++++++++++-------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/internal/fairyringclient/types.go b/internal/fairyringclient/types.go index 917cfd5..850f4c4 100644 --- a/internal/fairyringclient/types.go +++ b/internal/fairyringclient/types.go @@ -3,13 +3,14 @@ package fairyringclient import ( "encoding/hex" "fairyringclient/pkg/cosmosClient" + "log" + "strings" + distIBE "github.com/FairBlock/DistributedIBE" "github.com/Fairblock/fairyring/x/keyshare/types" "github.com/drand/kyber" bls "github.com/drand/kyber-bls12381" "github.com/pkg/errors" - "log" - "strings" ) type KeyShare struct { @@ -45,20 +46,6 @@ func (v *ValidatorClients) RegisterValidatorSet() { log.Printf("%s Registered as Validator", addr) } -// -//func (v *ValidatorClients) UnregisterValidatorSet() { -// addr := v.CosmosClient.GetAddress() -// _, err := validatorCosmosClient.CosmosClient.BroadcastTx(&types.MsgUnregisterValidator{ -// Creator: addr, -// }, true) -// if err != nil { -// if !strings.Contains(err.Error(), "validator already unregistered") { -// log.Fatal(err) -// } -// } -// log.Printf("%s Unregistered Validator", addr) -//} - func (v *ValidatorClients) Pause() { v.Paused = true } @@ -72,7 +59,7 @@ func (v *ValidatorClients) SetCommitments(c *types.QueryCommitmentsResponse) { } func (v *ValidatorClients) IncreaseInvalidShareNum() { - v.InvalidShareInARow = v.InvalidShareInARow + 1 + v.InvalidShareInARow++ } func (v *ValidatorClients) ResetInvalidShareNum() { @@ -105,18 +92,20 @@ func remainingBlocks(expiry uint64, height uint64) uint64 { func (v *ValidatorClients) logShareState(height uint64) { if v.CurrentShare != nil { - log.Printf("Current Share Expires at: %d, in %d blocks | %v", + log.Printf( + "Current Share Index: %d | Expires at: %d, in %d blocks", + v.CurrentShare.Index, v.CurrentShareExpiryBlock, remainingBlocks(v.CurrentShareExpiryBlock, height), - v.CurrentShare.Share, ) } if v.PendingShare != nil { - log.Printf("Pending Share expires at: %d, in %d blocks | %v", + log.Printf( + "Pending Share Index: %d | Expires at: %d, in %d blocks", + v.PendingShare.Index, v.PendingShareExpiryBlock, remainingBlocks(v.PendingShareExpiryBlock, height), - v.PendingShare.Share, ) } } @@ -153,13 +142,13 @@ func (v *ValidatorClients) SyncCurrentShareWithChain(latestBlockHeight uint64) e if v.PendingShare != nil && v.PendingShareExpiryBlock > latestBlockHeight { v.ActivatePendingShare() - log.Printf("Activated locally cached pending key share, New Share: %v\n", v.CurrentShare.Share.Value.String()) + log.Printf("Activated locally cached pending key share | Index: %d\n", v.CurrentShare.Index) } else { v.RemovePendingShare() if err := v.UpdateKeyShareFromChain(false); err != nil { return err } - log.Printf("Fetched active key share from chain, New Share: %v\n", v.CurrentShare.Share.Value.String()) + log.Printf("Fetched active key share from chain | Index: %d\n", v.CurrentShare.Index) } v.resetAfterShareSwitch() @@ -193,7 +182,7 @@ func (v *ValidatorClients) PrepareShareForTargetHeight(targetHeight uint64) erro v.ActivatePendingShare() v.resetAfterShareSwitch() - log.Printf("Activated pending key share, New Share: %v\n", v.CurrentShare.Share.Value.String()) + log.Printf("Activated pending key share | Index: %d\n", v.CurrentShare.Index) } return nil @@ -206,7 +195,7 @@ func (v *ValidatorClients) UpdateKeyShareFromChain(forNextRound bool) error { } commits, err := v.CosmosClient.GetCommitments() - for err != nil { + if err != nil { return err } @@ -238,7 +227,6 @@ func (v *ValidatorClients) UpdateKeyShareFromChain(forNextRound bool) error { } v.Commitments = commits - return nil } @@ -261,7 +249,6 @@ func (v *ValidatorClients) VerifyShare(commitments *types.Commitments, verifyPen } targetShare := v.CurrentShare - if targetShare == nil { return false, errors.New("active share not found") } @@ -283,8 +270,7 @@ func (v *ValidatorClients) VerifyShare(commitments *types.Commitments, verifyPen } newCommitmentPoint := s.G1().Point() - err = newCommitmentPoint.UnmarshalBinary(newByteCommitment) - if err != nil { + if err = newCommitmentPoint.UnmarshalBinary(newByteCommitment); err != nil { return false, err } @@ -298,7 +284,7 @@ func (v *ValidatorClients) VerifyShare(commitments *types.Commitments, verifyPen return false, errors.New("unable to create hashable G2 point") } - Qid := hG2.Hash([]byte("verifying")) + qid := hG2.Hash([]byte("verifying")) - return distIBE.VerifyShare(s, newCommitment, extracted, Qid), nil + return distIBE.VerifyShare(s, newCommitment, extracted, qid), nil } From 5ce7f9e66318c28becfa3ad48b4aefa40073c2a8 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:56:19 +0530 Subject: [PATCH 12/15] ci: run tests before release builds --- .github/workflows/release-linux-amd64.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-linux-amd64.yml b/.github/workflows/release-linux-amd64.yml index bcd2bdf..19906c1 100644 --- a/.github/workflows/release-linux-amd64.yml +++ b/.github/workflows/release-linux-amd64.yml @@ -6,7 +6,7 @@ on: tag: description: Existing GitHub release tag required: true - default: v1.0.0 + default: v1.0.1 type: string permissions: @@ -60,6 +60,9 @@ jobs: echo "Building tag ${ACTUAL_TAG}" git rev-parse HEAD + - name: Run tests + run: go test ./... + - name: Build fairyringclient run: | set -euo pipefail From f07dcaac2a3c8ad3e15e788207b3f97138843130 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:57:44 +0530 Subject: [PATCH 13/15] ci: add pull request test and build checks --- .github/workflows/ci.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0393858 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run tests + run: go test ./... + + - name: Build + run: go build -mod=readonly -trimpath -o /tmp/fairyringclient . From 911959cc091c5bd848f17df8a434b4c2fb225b53 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:01:23 +0530 Subject: [PATCH 14/15] tx: distinguish fee spending from validator slashing --- internal/fairyringclient/fairyringclient.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/internal/fairyringclient/fairyringclient.go b/internal/fairyringclient/fairyringclient.go index c390fe9..1ec4165 100644 --- a/internal/fairyringclient/fairyringclient.go +++ b/internal/fairyringclient/fairyringclient.go @@ -177,7 +177,7 @@ func StartFairyRingClient(cfg config.Config) { } }, func(txResp *tx.GetTxResponse) { - if hasCoinSpentEvent(txResp.TxResponse.Events) { + if hasSlashingCoinSpentEvent(txResp.TxResponse.Events, validatorCosmosClient.CosmosClient.GetAddress()) { validatorCosmosClient.IncreaseInvalidShareNum() log.Printf("KeyShare for Height %s is INVALID, Got Slashed, Current number invalid share in a row: %d\n", processHeightStr, validatorCosmosClient.InvalidShareInARow) defer invalidShareSubmitted.Inc() @@ -257,10 +257,16 @@ func InitializeValidatorClient(cfg config.Config) (*ValidatorClients, *tmclient. return &ValidatorClients{CosmosClient: vCosmosClient}, client, nil } -func hasCoinSpentEvent(e []abciTypes.Event) bool { - for _, eachEvent := range e { - if eachEvent.Type == "coin_spent" { - return true +func hasSlashingCoinSpentEvent(events []abciTypes.Event, feePayer string) bool { + for _, event := range events { + if event.Type != "coin_spent" { + continue + } + + for _, attribute := range event.Attributes { + if attribute.Key == "spender" && attribute.Value != "" && attribute.Value != feePayer { + return true + } } } return false From f16d7281b98d1b09e5b0b5320dc353de14ad6fd6 Mon Sep 17 00:00:00 2001 From: ap0calypse644 <91111707+ap0calypse644@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:11:03 +0530 Subject: [PATCH 15/15] fix gas estimate variable shadowing --- pkg/cosmosClient/client.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cosmosClient/client.go b/pkg/cosmosClient/client.go index 46848c0..bab0251 100644 --- a/pkg/cosmosClient/client.go +++ b/pkg/cosmosClient/client.go @@ -459,10 +459,11 @@ func (c *CosmosClient) signTxMsg(msg cosmostypes.Msg, adjustGas bool) ([]byte, e WithSequence(c.account.Sequence). WithGasAdjustment(defaultGasAdjustment) - _, newGasLimit, err := clienttx.CalculateGas(c.grpcConn, txf, msg) + _, calculatedGasLimit, err := clienttx.CalculateGas(c.grpcConn, txf, msg) if err != nil { return nil, err } + newGasLimit = calculatedGasLimit } txBuilder.SetGasLimit(newGasLimit)