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

chore: Validate pop cmd #118

Merged
merged 5 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

## Unreleased

### Improvements
- [#111](https://github.com/babylonlabs-io/btc-staker/pull/111) Add CLI command
to create phase-1/phase-2 PoP payload
- [#115](https://github.com/babylonlabs-io/btc-staker/pull/115) Add CLI command
to create payload for phase-1/phase-2 PoP deletion
- [#116](https://github.com/babylonlabs-io/btc-staker/pull/116) Add CLI command
to sign Cosmos ADR-36 messages
- [#118](https://github.com/babylonlabs-io/btc-staker/pull/118) Add CLI command
to validate PoP JSON file

## v0.14.0

Expand Down
176 changes: 170 additions & 6 deletions cmd/stakercli/pop/pop.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ package pop

import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strconv"

"github.com/babylonlabs-io/babylon/crypto/bip322"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/urfave/cli"

"github.com/babylonlabs-io/btc-staker/babylonclient/keyringcontroller"
"github.com/babylonlabs-io/btc-staker/cmd/stakercli/helpers"
"github.com/babylonlabs-io/btc-staker/staker"
"github.com/babylonlabs-io/btc-staker/types"
ut "github.com/babylonlabs-io/btc-staker/utils"
"github.com/babylonlabs-io/btc-staker/walletcontroller"
"github.com/btcsuite/btcd/btcutil"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/urfave/cli"
)

const (
Expand All @@ -29,22 +35,24 @@ const (
babyAddressPrefixFlag = "baby-address-prefix"
keyringDirFlag = "keyring-dir"
keyringBackendFlag = "keyring-backend"
outputFileFlag = "output-file"
)

var PopCommands = []cli.Command{
{
Name: "pop",
Usage: "Commands realted to generation and verification of the Proof of Possession",
Usage: "Commands about proof-of-possession generation and verification",
Category: "PoP commands",
Subcommands: []cli.Command{
generateCreatePopCmd,
GenerateCreatePopCmd,
generateDeletePopCmd,
signCosmosAdr36Cmd,
ValidatePopCmd,
},
},
}

var generateCreatePopCmd = cli.Command{
var GenerateCreatePopCmd = cli.Command{
Name: "generate-create-pop",
ShortName: "gcp",
Usage: "stakercli pop generate-create-pop",
Expand Down Expand Up @@ -104,6 +112,11 @@ var generateCreatePopCmd = cli.Command{
Usage: "Keyring backend",
Value: "test",
},
cli.StringFlag{
Name: outputFileFlag,
Usage: "Path to output JSON file",
Value: "",
},
},
Action: generatePop,
}
Expand Down Expand Up @@ -163,11 +176,162 @@ func generatePop(c *cli.Context) error {
return err
}

if outputPath := c.String(outputFileFlag); outputPath != "" {
// Convert response to JSON
jsonBytes, err := json.MarshalIndent(popResponse, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal response to JSON: %w", err)
}

// Write to file
if err := os.WriteFile(outputPath, jsonBytes, 0644); err != nil {
return fmt.Errorf("failed to write output file: %w", err)
}
}

helpers.PrintRespJSON(popResponse)

return nil
}

var ValidatePopCmd = cli.Command{
Name: "validate",
ShortName: "vp",
Usage: "stakercli pop validate <path-to-pop.json>",
Flags: []cli.Flag{
cli.StringFlag{
Name: btcNetworkFlag,
Usage: "Bitcoin network (testnet3, mainnet, regtest, simnet, signet)",
Value: "testnet3",
},
cli.StringFlag{
Name: babyAddressPrefixFlag,
Usage: "Baby address prefix",
Value: "bbn",
},
},
Action: validatePop,
ArgsUsage: "<path-to-pop.json>",
}

func validatePop(c *cli.Context) error {
if c.NArg() != 1 {
return fmt.Errorf("expected 1 argument (pop file path), got %d", c.NArg())
}

// Read and parse the PoP file
popFilePath := c.Args().First()
popFileBytes, err := os.ReadFile(popFilePath)
if err != nil {
return fmt.Errorf("failed to read pop file: %w", err)
}

var popResponse staker.Response
if err := json.Unmarshal(popFileBytes, &popResponse); err != nil {
return fmt.Errorf("failed to parse pop file: %w", err)
}

// Get network params
network := c.String(btcNetworkFlag)
networkParams, err := ut.GetBtcNetworkParams(network)
if err != nil {
return fmt.Errorf("failed to get btc network params: %w", err)
}

babyAddressPrefix := c.String(babyAddressPrefixFlag)

err = ValidatePop(popResponse, networkParams, babyAddressPrefix)
if err != nil {
return fmt.Errorf("pop validation failed: %w", err)
}

fmt.Println("Proof of Possession is valid!")

return nil
}

func ValidatePop(popResponse staker.Response, btcNetParams *chaincfg.Params, babyPrefix string) error {
err := ValidateBTCSignBaby(popResponse.BTCAddress, popResponse.BabyAddress, popResponse.BTCSignBaby, babyPrefix, btcNetParams)
if err != nil {
return fmt.Errorf("invalid btcSignBaby: %w", err)
}

err = ValidateBabySignBTC(popResponse.BabyPublicKey, popResponse.BabyAddress, popResponse.BTCAddress, popResponse.BabySignBTC)
if err != nil {
return fmt.Errorf("invalid babySignBtc: %w", err)
}

return nil
}

func ValidateBTCSignBaby(btcAddr, babyAddr, btcSignBaby, babyPrefix string, btcNetParams *chaincfg.Params) error {
btcAddress, err := btcutil.DecodeAddress(btcAddr, btcNetParams)
if err != nil {
return fmt.Errorf("failed to decode bitcoin address: %w", err)
}

sdkAddressBytes, err := sdk.GetFromBech32(babyAddr, babyPrefix)
if err != nil {
return fmt.Errorf("failed to decode baby address: %w", err)
}

sdkAddress := sdk.AccAddress(sdkAddressBytes)

bech32cosmosAddressString, err := sdk.Bech32ifyAddressBytes(babyPrefix, sdkAddress.Bytes())
if err != nil {
return fmt.Errorf("failed to get babylon address bytes: %w", err)
}

schnorrSigBase64, err := base64.StdEncoding.DecodeString(btcSignBaby)
if err != nil {
return fmt.Errorf("failed to decode btcSignBaby: %w", err)
}

witness, err := bip322.SimpleSigToWitness(schnorrSigBase64)
if err != nil {
return fmt.Errorf("failed to convert btcSignBaby to witness: %w", err)
}

return bip322.Verify(
[]byte(bech32cosmosAddressString),
witness,
btcAddress,
btcNetParams,
)
}

func ValidateBabySignBTC(babyPk, babyAddr, btcAddress, babySigOverBTCPk string) error {
babyPubKeyBz, err := base64.StdEncoding.DecodeString(babyPk)
if err != nil {
return fmt.Errorf("failed to decode babyPublicKey: %w", err)
}

babyPubKey := &secp256k1.PubKey{
Key: babyPubKeyBz,
}

babySignBTC := []byte(btcAddress)
base64Bytes := base64.StdEncoding.EncodeToString(babySignBTC)
babySignBtcDoc := staker.NewCosmosSignDoc(babyAddr, base64Bytes)
babySignBtcMarshaled, err := json.Marshal(babySignBtcDoc)
if err != nil {
return fmt.Errorf("failed to marshalling cosmos sign doc: %w", err)
}

babySignBtcBz := sdk.MustSortJSON(babySignBtcMarshaled)

secp256SigBase64, err := base64.StdEncoding.DecodeString(babySigOverBTCPk)
if err != nil {
return fmt.Errorf("failed to decode babySignBTC: %w", err)
}

if !babyPubKey.VerifySignature(babySignBtcBz, secp256SigBase64) {
return fmt.Errorf("invalid babySignBtc")
}

return nil
}

var generateDeletePopCmd = cli.Command{
Name: "generate-delete-pop",
ShortName: "gdp",
Expand Down
48 changes: 48 additions & 0 deletions cmd/stakercli/pop/pop_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package pop_test

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/babylonlabs-io/btc-staker/itest/testutil"
"github.com/babylonlabs-io/btc-staker/staker"
)

var popsToVerify = []staker.Response{
{
BabyAddress: "bbn1xjz8fs9vkmefdqaxan5kv2d09vmwzru7jhy424",
Copy link
Collaborator

Choose a reason for hiding this comment

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

just a question, as I assume this data was generated by generate pop command ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ideally yes, but that would require some e2e tests to set up wallet, generate btc account, etc.

BTCAddress: "bc1qcpty6lpueassw9rhfrvkq6h0ufnhmc2nhgvpcr",
BTCPublicKey: "79f71003589158b2579345540b08bbc74974c49dd5e0782e31d0de674540d513",
BTCSignBaby: "AkcwRAIgcrI2IdD2JSFVIeQmtRA3wFjjiy+qEvqbX57rn6xvWWECIDis7vHSJeR8X91uMQReG0pPQFFLpeM0ga4BW+Tt2V54ASEDefcQA1iRWLJXk0VUCwi7x0l0xJ3V4HguMdDeZ0VA1RM=",
BabySignBTC: "FnYTm9ZbhJZY202R9YBkjGEJqeJ/n5McZBpGH38P2pt0YRcjwOh8XgoeVQTU9So7/RHVHHdKNB09DVmtQJ7xtw==",
BabyPublicKey: "Asezdqkvh+kLbuD75DirSwi/QFbJjFe2SquiivMaPS65",
},
}

func TestValidatePoPCmd(t *testing.T) {
t.Parallel()

// Create a temporary JSON file
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "pop.json")

// Marshal the test data to JSON
jsonData, err := json.MarshalIndent(popsToVerify[0], "", " ")
require.NoError(t, err)

// Write JSON to temporary file
err = os.WriteFile(tmpFile, jsonData, 0644)
require.NoError(t, err)

// Test ValidatePopCmd with the JSON file
app := testutil.TestApp()
validatePop := []string{
"stakercli", "pop", "validate", tmpFile,
}
err = app.Run(validatePop)
require.NoError(t, err)
}
2 changes: 2 additions & 0 deletions itest/testutil/appcli.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/babylonlabs-io/babylon/testutil/datagen"
cmdadmin "github.com/babylonlabs-io/btc-staker/cmd/stakercli/admin"
cmddaemon "github.com/babylonlabs-io/btc-staker/cmd/stakercli/daemon"
"github.com/babylonlabs-io/btc-staker/cmd/stakercli/pop"
"github.com/babylonlabs-io/btc-staker/cmd/stakercli/transaction"
"github.com/babylonlabs-io/networks/parameters/parser"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -56,6 +57,7 @@ func TestApp() *cli.App {
app.Commands = append(app.Commands, cmddaemon.DaemonCommands...)
app.Commands = append(app.Commands, cmdadmin.AdminCommands...)
app.Commands = append(app.Commands, transaction.TransactionCommands...)
app.Commands = append(app.Commands, pop.PopCommands...)
return app
}

Expand Down
Loading