diff --git a/.circleci/config.yml b/.circleci/config.yml index 5c02092..80f1c99 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,7 +11,7 @@ jobs: resource_class: large steps: - go/install: - version: "1.21.4" + version: "1.22.3" - checkout - run: name: Print Go environment diff --git a/Dockerfile b/Dockerfile index 41159e7..b3e17d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21 as builder +FROM golang:1.22.3 as builder # Install cli tools for building and final image RUN apt-get update && apt-get install -y make git bash gcc curl jq diff --git a/cmd/stakercli/helpers/flags.go b/cmd/stakercli/helpers/flags.go index de70c00..84dddeb 100644 --- a/cmd/stakercli/helpers/flags.go +++ b/cmd/stakercli/helpers/flags.go @@ -3,6 +3,4 @@ package helpers const ( StakingAmountFlag = "staking-amount" StakingTimeBlocksFlag = "staking-time" - UnbondingFee = "unbonding-fee" - UnbondingTimeFlag = "unbonding-time" ) diff --git a/cmd/stakercli/transaction/parsers.go b/cmd/stakercli/transaction/parsers.go new file mode 100644 index 0000000..9893a71 --- /dev/null +++ b/cmd/stakercli/transaction/parsers.go @@ -0,0 +1,99 @@ +package transaction + +import ( + "encoding/hex" + "fmt" + "math" + + "github.com/babylonchain/babylon/btcstaking" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/urfave/cli" +) + +func parseSchnorPubKeyFromCliCtx(ctx *cli.Context, flagName string) (*btcec.PublicKey, error) { + pkHex := ctx.String(flagName) + return parseSchnorPubKeyFromHex(pkHex) +} + +func parseSchnorPubKeyFromHex(pkHex string) (*btcec.PublicKey, error) { + pkBytes, err := hex.DecodeString(pkHex) + if err != nil { + return nil, err + } + + pk, err := schnorr.ParsePubKey(pkBytes) + if err != nil { + return nil, err + } + + return pk, nil +} + +func parseAmountFromCliCtx(ctx *cli.Context, flagName string) (btcutil.Amount, error) { + amt := ctx.Int64(flagName) + + if amt <= 0 { + return 0, fmt.Errorf("staking amount should be greater than 0") + } + + return btcutil.Amount(amt), nil +} + +func parseLockTimeBlocksFromCliCtx(ctx *cli.Context, flagName string) (uint16, error) { + timeBlocks := ctx.Int64(flagName) + + if timeBlocks <= 0 { + return 0, fmt.Errorf("staking time blocks should be greater than 0") + } + + if timeBlocks > math.MaxUint16 { + return 0, fmt.Errorf("staking time blocks should be less or equal to %d", math.MaxUint16) + } + + return uint16(timeBlocks), nil +} + +func parseCovenantKeysFromCliCtx(ctx *cli.Context) ([]*btcec.PublicKey, error) { + covenantMembersPks := ctx.StringSlice(covenantMembersPksFlag) + return parseCovenantKeysFromSlice(covenantMembersPks) +} + +func parseCovenantKeysFromSlice(covenantMembersPks []string) ([]*btcec.PublicKey, error) { + covenantPubKeys := make([]*btcec.PublicKey, len(covenantMembersPks)) + + for i, fpPk := range covenantMembersPks { + fpPkBytes, err := hex.DecodeString(fpPk) + if err != nil { + return nil, err + } + + fpSchnorrKey, err := schnorr.ParsePubKey(fpPkBytes) + if err != nil { + return nil, err + } + + covenantPubKeys[i] = fpSchnorrKey + } + + return covenantPubKeys, nil +} + +func parseMagicBytesFromCliCtx(ctx *cli.Context) ([]byte, error) { + magicBytesHex := ctx.String(magicBytesFlag) + return parseMagicBytesFromHex(magicBytesHex) +} + +func parseMagicBytesFromHex(magicBytesHex string) ([]byte, error) { + magicBytes, err := hex.DecodeString(magicBytesHex) + if err != nil { + return nil, err + } + + if len(magicBytes) != btcstaking.MagicBytesLen { + return nil, fmt.Errorf("magic bytes should be of length %d", btcstaking.MagicBytesLen) + } + + return magicBytes, nil +} diff --git a/cmd/stakercli/transaction/staking.go b/cmd/stakercli/transaction/staking.go deleted file mode 100644 index c49ba82..0000000 --- a/cmd/stakercli/transaction/staking.go +++ /dev/null @@ -1,69 +0,0 @@ -package transaction - -import ( - "fmt" - - "github.com/babylonchain/btc-staker/utils" - "github.com/btcsuite/btcd/btcutil" -) - -// InputBtcStakingTx json input structure to create a staking tx from json -type InputBtcStakingTx struct { - // BtcNetwork type of btc network to use - // Needs to be one of "testnet3", "mainnet", "regtest", "simnet", "signet". - BtcNetwork string `json:"btc_network"` - // StakerPublicKeyHex SchnorPubKey hex encoded. - StakerPublicKeyHex string `json:"staker_public_key_hex"` - // CovenantMembersPkHex covenant members SchnorPubKey hex encoded. - CovenantMembersPkHex []string `json:"covenant_members_pk_hex"` - // FinalityProviderPublicKeyHex SchnorPubKey hex encoded. - FinalityProviderPublicKeyHex string `json:"finality_provider_public_key_hex"` - // StakingAmount the amount to be staked in satoshi. - // A single StakingAmount is equal to 1e-8 of a bitcoin. - StakingAmount int64 `json:"staking_amount"` - // StakingTimeBlocks number of blocks to keep the staking amount locked. - StakingTimeBlocks uint16 `json:"staking_time_blocks"` - // MagicBytesHex magic bytes hex encoded. - MagicBytesHex string `json:"magic_bytes"` - // CovenantQuorum the number of covenant required as quorum. - CovenantQuorum uint32 `json:"covenant_quorum"` -} - -// ToCreatePhase1StakingTxResponse from the data input parses and build parameters to create and serialize response tx structure. -func (tx InputBtcStakingTx) ToCreatePhase1StakingTxResponse() (*CreatePhase1StakingTxResponse, error) { - magicBytes, err := parseMagicBytesFromHex(tx.MagicBytesHex) - if err != nil { - return nil, fmt.Errorf("error parsing magic bytes %s: %w", tx.MagicBytesHex, err) - } - - stakerPk, err := parseSchnorPubKeyFromHex(tx.StakerPublicKeyHex) - if err != nil { - return nil, fmt.Errorf("error parsing staker pub key %s: %w", tx.StakerPublicKeyHex, err) - } - - fpPk, err := parseSchnorPubKeyFromHex(tx.FinalityProviderPublicKeyHex) - if err != nil { - return nil, fmt.Errorf("error parsing finality provider pub key %s: %w", tx.FinalityProviderPublicKeyHex, err) - } - - covenantMembersPks, err := parseCovenantKeysFromSlice(tx.CovenantMembersPkHex) - if err != nil { - return nil, fmt.Errorf("error parsing covenant members pub key %s: %w", tx.CovenantMembersPkHex, err) - } - - btcNetworkParams, err := utils.GetBtcNetworkParams(tx.BtcNetwork) - if err != nil { - return nil, fmt.Errorf("error parsing btc network %s: %w", tx.BtcNetwork, err) - } - - return MakeCreatePhase1StakingTxResponse( - magicBytes, - stakerPk, - fpPk, - covenantMembersPks, - tx.CovenantQuorum, - tx.StakingTimeBlocks, - btcutil.Amount(tx.StakingAmount), - btcNetworkParams, - ) -} diff --git a/cmd/stakercli/transaction/transactions.go b/cmd/stakercli/transaction/transactions.go index fcf0859..181aa0b 100644 --- a/cmd/stakercli/transaction/transactions.go +++ b/cmd/stakercli/transaction/transactions.go @@ -2,18 +2,15 @@ package transaction import ( "encoding/hex" - "encoding/json" "errors" "fmt" - "math" - "strings" "github.com/babylonchain/babylon/btcstaking" bbn "github.com/babylonchain/babylon/types" "github.com/babylonchain/btc-staker/cmd/stakercli/helpers" "github.com/babylonchain/btc-staker/utils" + "github.com/babylonchain/networks/parameters/parser" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" @@ -25,14 +22,13 @@ import ( const ( stakingTransactionFlag = "staking-transaction" - magicBytesFlag = "magic-bytes" - covenantMembersPksFlag = "covenant-committee-pks" - covenantQuorumFlag = "covenant-quorum" networkNameFlag = "network" stakerPublicKeyFlag = "staker-pk" finalityProviderKeyFlag = "finality-provider-pk" - minStakingAmountFlag = "min-staking-amount" - maxStakingAmountFlag = "max-staking-amount" + txInclusionHeightFlag = "tx-inclusion-height" + magicBytesFlag = "magic-bytes" + covenantMembersPksFlag = "covenant-committee-pks" + covenantQuorumFlag = "covenant-quorum" ) var TransactionCommands = []cli.Command{ @@ -43,112 +39,162 @@ var TransactionCommands = []cli.Command{ Category: "transaction commands", Subcommands: []cli.Command{ checkPhase1StakingTransactionCmd, - createPhase1StakingTransactionCmd, createPhase1UnbondingTransactionCmd, - createPhase1StakingTransactionFromJsonCmd, + createPhase1StakingTransactionCmd, + createPhase1StakingTransactionWithParamsCmd, }, }, } -func parseSchnorPubKeyFromCliCtx(ctx *cli.Context, flagName string) (*btcec.PublicKey, error) { - pkHex := ctx.String(flagName) - return parseSchnorPubKeyFromHex(pkHex) +var checkPhase1StakingTransactionCmd = cli.Command{ + Name: "check-phase1-staking-transaction", + ShortName: "cpst", + Usage: "stakercli transaction check-phase1-staking-transaction [fullpath/to/parameters.json]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: stakingTransactionFlag, + Usage: "Staking transaction in hex", + Required: true, + }, + cli.StringFlag{ + Name: networkNameFlag, + Usage: "Bitcoin network on which staking should take place one of (mainnet, testnet3, regtest, simnet, signet)", + Required: true, + }, + }, + Action: checkPhase1StakingTransaction, } -func parseSchnorPubKeyFromHex(pkHex string) (*btcec.PublicKey, error) { - pkBytes, err := hex.DecodeString(pkHex) - if err != nil { - return nil, err - } - - pk, err := schnorr.ParsePubKey(pkBytes) - if err != nil { - return nil, err - } - - return pk, nil +type StakingTxData struct { + StakerPublicKeyHex string `json:"staker_public_key_hex"` + FinalityProviderPublicKeyHex string `json:"finality_provider_public_key_hex"` + StakingAmount int64 `json:"staking_amount"` + StakingTimeBlocks int64 `json:"staking_time_blocks"` + // ParamsVersion is the version of the global parameters aginst which is valid + ParamsVersion int64 `json:"params_version"` } -func parseCovenantKeysFromCliCtx(ctx *cli.Context) ([]*btcec.PublicKey, error) { - covenantMembersPks := ctx.StringSlice(covenantMembersPksFlag) - return parseCovenantKeysFromSlice(covenantMembersPks) +type CheckPhase1StakingTxResponse struct { + IsValid bool `json:"is_valid"` + // StakingData will only be populated if the transaction is valid + StakingData *StakingTxData `json:"staking_data"` } -func parseCovenantKeysFromSlice(covenantMembersPks []string) ([]*btcec.PublicKey, error) { - covenantPubKeys := make([]*btcec.PublicKey, len(covenantMembersPks)) - - for i, fpPk := range covenantMembersPks { - fpPkBytes, err := hex.DecodeString(fpPk) +func validateTxAgainstParams( + tx *wire.MsgTx, + globalParams *parser.ParsedGlobalParams, + net *chaincfg.Params) *CheckPhase1StakingTxResponse { + + for i := len(globalParams.Versions) - 1; i >= 0; i-- { + params := globalParams.Versions[i] + + parsed, err := btcstaking.ParseV0StakingTx( + tx, + params.Tag, + params.CovenantPks, + params.CovenantQuorum, + net, + ) if err != nil { - return nil, err + continue } - fpSchnorrKey, err := schnorr.ParsePubKey(fpPkBytes) - if err != nil { - return nil, err + if parsed.OpReturnData.StakingTime < params.MinStakingTime || parsed.OpReturnData.StakingTime > params.MaxStakingTime { + continue + } + + if btcutil.Amount(parsed.StakingOutput.Value) < params.MinStakingAmount || btcutil.Amount(parsed.StakingOutput.Value) > params.MaxStakingAmount { + continue } - covenantPubKeys[i] = fpSchnorrKey + // At this point we know staking transaciton is valid against this version of global params + return &CheckPhase1StakingTxResponse{ + IsValid: true, + StakingData: &StakingTxData{ + StakerPublicKeyHex: hex.EncodeToString(parsed.OpReturnData.StakerPublicKey.Marshall()), + FinalityProviderPublicKeyHex: hex.EncodeToString(parsed.OpReturnData.FinalityProviderPublicKey.Marshall()), + StakingAmount: int64(parsed.StakingOutput.Value), + StakingTimeBlocks: int64(parsed.OpReturnData.StakingTime), + ParamsVersion: int64(params.Version), + }, + } } - return covenantPubKeys, nil + return &CheckPhase1StakingTxResponse{ + IsValid: false, + } } -func parseMagicBytesFromCliCtx(ctx *cli.Context) ([]byte, error) { - magicBytesHex := ctx.String(magicBytesFlag) - return parseMagicBytesFromHex(magicBytesHex) -} +func checkPhase1StakingTransaction(ctx *cli.Context) error { + inputFilePath := ctx.Args().First() + if len(inputFilePath) == 0 { + return errors.New("json file input is empty") + } -func parseMagicBytesFromHex(magicBytesHex string) ([]byte, error) { - magicBytes, err := hex.DecodeString(magicBytesHex) - if err != nil { - return nil, err + if !os.FileExists(inputFilePath) { + return fmt.Errorf("json file input %s does not exist", inputFilePath) } - if len(magicBytes) != btcstaking.MagicBytesLen { - return nil, fmt.Errorf("magic bytes should be of length %d", btcstaking.MagicBytesLen) + globalParams, err := parser.NewParsedGlobalParamsFromFile(inputFilePath) + + if err != nil { + return fmt.Errorf("error parsing file %s: %w", inputFilePath, err) } - return magicBytes, nil -} + net := ctx.String(networkNameFlag) -func parseAmountFromCliCtx(ctx *cli.Context, flagName string) (btcutil.Amount, error) { - amt := ctx.Int64(flagName) + currentNetwork, err := utils.GetBtcNetworkParams(net) - if amt <= 0 { - return 0, fmt.Errorf("staking amount should be greater than 0") + if err != nil { + return err } - return btcutil.Amount(amt), nil -} + stakingTxHex := ctx.String(stakingTransactionFlag) -func parseLockTimeBlocksFromCliCtx(ctx *cli.Context, flagName string) (uint16, error) { - timeBlocks := ctx.Int64(flagName) + stakingTx, _, err := bbn.NewBTCTxFromHex(stakingTxHex) - if timeBlocks <= 0 { - return 0, fmt.Errorf("staking time blocks should be greater than 0") + if err != nil { + return err } - if timeBlocks > math.MaxUint16 { - return 0, fmt.Errorf("staking time blocks should be less or equal to %d", math.MaxUint16) - } + resp := validateTxAgainstParams(stakingTx, globalParams, currentNetwork) + + helpers.PrintRespJSON(resp) - return uint16(timeBlocks), nil + return nil } -var checkPhase1StakingTransactionCmd = cli.Command{ - Name: "check-phase1-staking-transaction", - ShortName: "cpst", - Usage: "Checks whether provided staking transactions is valid staking transaction (tx must be funded/have inputs)", +var createPhase1StakingTransactionCmd = cli.Command{ + Name: "create-phase1-staking-transaction", + ShortName: "crpst", + Usage: "Creates unsigned and unfunded phase 1 staking transaction", + Description: "Creates unsigned and unfunded phase 1 staking transaction." + + "This method does not validate tx against global parameters, and is dedicated " + + "for advanced use cases. For most cases use safer `create-phase1-staking-transaction-with-params`", Flags: []cli.Flag{ cli.StringFlag{ - Name: stakingTransactionFlag, - Usage: "Staking transaction in hex", + Name: stakerPublicKeyFlag, + Usage: "staker public key in schnorr format (32 byte) in hex", + Required: true, + }, + cli.StringFlag{ + Name: finalityProviderKeyFlag, + Usage: "finality provider public key in schnorr format (32 byte) in hex", + Required: true, + }, + cli.Int64Flag{ + Name: helpers.StakingAmountFlag, + Usage: "Staking amount in satoshis", + Required: true, + }, + cli.Int64Flag{ + Name: helpers.StakingTimeBlocksFlag, + Usage: "Staking time in BTC blocks", Required: true, }, cli.StringFlag{ Name: magicBytesFlag, - Usage: "Magic bytes in op return output in hex", + Usage: "Magic bytes in op_return output in hex", Required: true, }, cli.StringSliceFlag{ @@ -166,31 +212,11 @@ var checkPhase1StakingTransactionCmd = cli.Command{ Usage: "Bitcoin network on which staking should take place one of (mainnet, testnet3, regtest, simnet, signet)", Required: true, }, - cli.StringFlag{ - Name: stakerPublicKeyFlag, - Usage: "Optional staker pub key hex to match the staker pub key in tx", - }, - cli.StringFlag{ - Name: finalityProviderKeyFlag, - Usage: "Optional finality provider public key hex to match the finality provider public key in tx", - }, - cli.Int64Flag{ - Name: minStakingAmountFlag, - Usage: "Optional minimum staking amount in satoshis to check if the amount spent in tx is higher than the flag", - }, - cli.Int64Flag{ - Name: maxStakingAmountFlag, - Usage: "Optional maximum staking amount in satoshis to check if the amount spent in tx is lower than the flag", - }, - cli.Int64Flag{ - Name: helpers.StakingTimeBlocksFlag, - Usage: "Optional staking time in BTC blocks to match how long it was locked for", - }, }, - Action: checkPhase1StakingTransaction, + Action: createPhase1StakingTransaction, } -func checkPhase1StakingTransaction(ctx *cli.Context) error { +func createPhase1StakingTransaction(ctx *cli.Context) error { net := ctx.String(networkNameFlag) currentParams, err := utils.GetBtcNetworkParams(net) @@ -199,13 +225,30 @@ func checkPhase1StakingTransaction(ctx *cli.Context) error { return err } - stakingTxHex := ctx.String(stakingTransactionFlag) + stakerPk, err := parseSchnorPubKeyFromCliCtx(ctx, stakerPublicKeyFlag) + + if err != nil { + return err + } + + fpPk, err := parseSchnorPubKeyFromCliCtx(ctx, finalityProviderKeyFlag) + + if err != nil { + return err + } - tx, _, err := bbn.NewBTCTxFromHex(stakingTxHex) + stakingAmount, err := parseAmountFromCliCtx(ctx, helpers.StakingAmountFlag) + + if err != nil { + return err + } + + stakingTimeBlocks, err := parseLockTimeBlocksFromCliCtx(ctx, helpers.StakingTimeBlocksFlag) if err != nil { return err } + magicBytes, err := parseMagicBytesFromCliCtx(ctx) if err != nil { @@ -220,60 +263,39 @@ func checkPhase1StakingTransaction(ctx *cli.Context) error { covenantQuorum := uint32(ctx.Uint64(covenantQuorumFlag)) - stakingTx, err := btcstaking.ParseV0StakingTx( - tx, + _, tx, err := btcstaking.BuildV0IdentifiableStakingOutputsAndTx( magicBytes, + stakerPk, + fpPk, covenantMembersPks, covenantQuorum, + stakingTimeBlocks, + stakingAmount, currentParams, ) if err != nil { return err } - // verify if optional flags match. - stakerPk := ctx.String(stakerPublicKeyFlag) - if len(stakerPk) > 0 { - stakerPkFromTx := schnorr.SerializePubKey(stakingTx.OpReturnData.StakerPublicKey.PubKey) - stakerPkHexFromTx := hex.EncodeToString(stakerPkFromTx) - if !strings.EqualFold(stakerPk, stakerPkHexFromTx) { - return fmt.Errorf("staker pk in tx %s do not match with flag %s", stakerPkHexFromTx, stakerPk) - } - } - - fpPk := ctx.String(finalityProviderKeyFlag) - if len(fpPk) > 0 { - fpPkFromTx := schnorr.SerializePubKey(stakingTx.OpReturnData.FinalityProviderPublicKey.PubKey) - fpPkHexFromTx := hex.EncodeToString(fpPkFromTx) - if !strings.EqualFold(fpPk, fpPkHexFromTx) { - return fmt.Errorf("finality provider pk in tx %s do not match with flag %s", fpPkHexFromTx, fpPk) - } - } - - timeBlocks := ctx.Int64(helpers.StakingTimeBlocksFlag) - if timeBlocks > 0 && uint16(timeBlocks) != stakingTx.OpReturnData.StakingTime { - return fmt.Errorf("staking time in tx %d do not match with flag %d", stakingTx.OpReturnData.StakingTime, timeBlocks) - } - - txAmount := stakingTx.StakingOutput.Value - minAmount := ctx.Int64(minStakingAmountFlag) - if minAmount > 0 && txAmount < minAmount { - return fmt.Errorf("staking amount in tx %d is less than the min-staking-amount in flag %d", txAmount, minAmount) + serializedTx, err := utils.SerializeBtcTransaction(tx) + if err != nil { + return err } - maxAmount := ctx.Int64(maxStakingAmountFlag) - if maxAmount > 0 && txAmount > maxAmount { - return fmt.Errorf("staking amount in tx %d is more than the max-staking-amount in flag %d", txAmount, maxAmount) + resp := &CreatePhase1StakingTxResponse{ + StakingTxHex: hex.EncodeToString(serializedTx), } - fmt.Println("Provided transaction is valid staking transaction!") + helpers.PrintRespJSON(*resp) return nil } -var createPhase1StakingTransactionCmd = cli.Command{ - Name: "create-phase1-staking-transaction", - ShortName: "crpst", - Usage: "Creates unsigned and unfunded phase 1 staking transaction", +var createPhase1StakingTransactionWithParamsCmd = cli.Command{ + Name: "create-phase1-staking-transaction-with-params", + ShortName: "crpst", + Usage: "stakercli transaction create-phase1-staking-transaction-with-params [fullpath/to/parameters.json]", + Description: "Creates unsigned and unfunded phase 1 staking transaction. It also validates the transaction against provided global parameters", + Action: createPhase1StakingTransactionWithParams, Flags: []cli.Flag{ cli.StringFlag{ Name: stakerPublicKeyFlag, @@ -295,19 +317,9 @@ var createPhase1StakingTransactionCmd = cli.Command{ Usage: "Staking time in BTC blocks", Required: true, }, - cli.StringFlag{ - Name: magicBytesFlag, - Usage: "Magic bytes in op_return output in hex", - Required: true, - }, - cli.StringSliceFlag{ - Name: covenantMembersPksFlag, - Usage: "BTC public keys of the covenant committee members", - Required: true, - }, cli.Uint64Flag{ - Name: covenantQuorumFlag, - Usage: "Required quorum for the covenant members", + Name: txInclusionHeightFlag, + Usage: "Expected BTC height at which transaction will be included. This value is use important to chose correct global parameters for transaction", Required: true, }, cli.StringFlag{ @@ -316,109 +328,73 @@ var createPhase1StakingTransactionCmd = cli.Command{ Required: true, }, }, - Action: createPhase1StakingTransaction, -} - -var createPhase1StakingTransactionFromJsonCmd = cli.Command{ - Name: "create-phase1-staking-transaction-json", - ShortName: "crpstjson", - Usage: "stakercli transaction create-phase1-staking-transaction-json [fullpath/to/inputBtcStakingTx.json]", - Description: "Creates unsigned and unfunded phase 1 staking transaction", - Action: createPhase1StakingTransactionFromJson, } type CreatePhase1StakingTxResponse struct { StakingTxHex string `json:"staking_tx_hex"` } -func createPhase1StakingTransaction(ctx *cli.Context) error { - net := ctx.String(networkNameFlag) - - currentParams, err := utils.GetBtcNetworkParams(net) +func createPhase1StakingTransactionWithParams(ctx *cli.Context) error { + inputFilePath := ctx.Args().First() + if len(inputFilePath) == 0 { + return errors.New("json file input is empty") + } - if err != nil { - return err + if !os.FileExists(inputFilePath) { + return fmt.Errorf("json file input %s does not exist", inputFilePath) } - stakerPk, err := parseSchnorPubKeyFromCliCtx(ctx, stakerPublicKeyFlag) + params, err := parser.NewParsedGlobalParamsFromFile(inputFilePath) if err != nil { - return err + return fmt.Errorf("error parsing file %s: %w", inputFilePath, err) + } - fpPk, err := parseSchnorPubKeyFromCliCtx(ctx, finalityProviderKeyFlag) + currentNetwork, err := utils.GetBtcNetworkParams(ctx.String(networkNameFlag)) if err != nil { return err } - stakingAmount, err := parseAmountFromCliCtx(ctx, helpers.StakingAmountFlag) + stakerPk, err := parseSchnorPubKeyFromCliCtx(ctx, stakerPublicKeyFlag) if err != nil { return err } - stakingTimeBlocks, err := parseLockTimeBlocksFromCliCtx(ctx, helpers.StakingTimeBlocksFlag) + fpPk, err := parseSchnorPubKeyFromCliCtx(ctx, finalityProviderKeyFlag) if err != nil { return err } - magicBytes, err := parseMagicBytesFromCliCtx(ctx) + stakingAmount, err := parseAmountFromCliCtx(ctx, helpers.StakingAmountFlag) if err != nil { return err } - covenantMembersPks, err := parseCovenantKeysFromCliCtx(ctx) + stakingTimeBlocks, err := parseLockTimeBlocksFromCliCtx(ctx, helpers.StakingTimeBlocksFlag) if err != nil { return err } - covenantQuorum := uint32(ctx.Uint64(covenantQuorumFlag)) + expectedHeight := ctx.Uint64(txInclusionHeightFlag) resp, err := MakeCreatePhase1StakingTxResponse( - magicBytes, stakerPk, fpPk, - covenantMembersPks, - covenantQuorum, stakingTimeBlocks, stakingAmount, - currentParams, + params, + expectedHeight, + currentNetwork, ) - if err != nil { - return err - } - helpers.PrintRespJSON(*resp) - return nil -} - -func createPhase1StakingTransactionFromJson(ctx *cli.Context) error { - inputFilePath := ctx.Args().First() - if len(inputFilePath) == 0 { - return errors.New("json file input is empty") - } - - if !os.FileExists(inputFilePath) { - return fmt.Errorf("json file input %s does not exist", inputFilePath) - } - - bz, err := os.ReadFile(inputFilePath) if err != nil { - return fmt.Errorf("error reading file %s: %w", inputFilePath, err) - } - - var input InputBtcStakingTx - if err := json.Unmarshal(bz, &input); err != nil { - return fmt.Errorf("error parsing file content %s to struct %+v: %w", bz, input, err) - } - - resp, err := input.ToCreatePhase1StakingTxResponse() - if err != nil { - return err + return fmt.Errorf("error building staking tx: %w", err) } helpers.PrintRespJSON(*resp) @@ -427,21 +403,34 @@ func createPhase1StakingTransactionFromJson(ctx *cli.Context) error { // MakeCreatePhase1StakingTxResponse builds and serialize staking tx as hex response. func MakeCreatePhase1StakingTxResponse( - magicBytes []byte, stakerPk *btcec.PublicKey, fpPk *btcec.PublicKey, - covenantMembersPks []*btcec.PublicKey, - covenantQuorum uint32, stakingTimeBlocks uint16, stakingAmount btcutil.Amount, + gp *parser.ParsedGlobalParams, + expectedInclusionHeight uint64, net *chaincfg.Params, ) (*CreatePhase1StakingTxResponse, error) { + params := gp.GetVersionedGlobalParamsByHeight(expectedInclusionHeight) + + if params == nil { + return nil, fmt.Errorf("no global params found for height %d", expectedInclusionHeight) + } + + if stakingTimeBlocks < params.MinStakingTime || stakingTimeBlocks > params.MaxStakingTime { + return nil, fmt.Errorf("provided staking time %d is out of bounds for params active at height %d", stakingTimeBlocks, expectedInclusionHeight) + } + + if stakingAmount < params.MinStakingAmount || stakingAmount > params.MaxStakingAmount { + return nil, fmt.Errorf("provided staking amount %d is out of bounds for params active at height %d", stakingAmount, expectedInclusionHeight) + } + _, tx, err := btcstaking.BuildV0IdentifiableStakingOutputsAndTx( - magicBytes, + params.Tag, stakerPk, fpPk, - covenantMembersPks, - covenantQuorum, + params.CovenantPks, + params.CovenantQuorum, stakingTimeBlocks, stakingAmount, net, @@ -465,36 +454,16 @@ func MakeCreatePhase1StakingTxResponse( var createPhase1UnbondingTransactionCmd = cli.Command{ Name: "create-phase1-unbonding-transaction", ShortName: "crput", - Usage: "Creates unsigned phase 1 unbonding transaction", + Usage: "stakercli transaction create-phase1-unbonding-transaction [fullpath/to/parameters.json]", Flags: []cli.Flag{ cli.StringFlag{ Name: stakingTransactionFlag, Usage: "hex encoded staking transaction for which unbonding transaction will be created", Required: true, }, - cli.Int64Flag{ - Name: helpers.UnbondingFee, - Usage: "unbonding fee in satoshis", - Required: true, - }, - cli.Int64Flag{ - Name: helpers.UnbondingTimeFlag, - Usage: "Unbonding time in BTC blocks", - Required: true, - }, - cli.StringFlag{ - Name: magicBytesFlag, - Usage: "Hex encoded magic bytes in staking transaction op return output", - Required: true, - }, - cli.StringSliceFlag{ - Name: covenantMembersPksFlag, - Usage: "BTC public keys of the covenant committee members", - Required: true, - }, cli.Uint64Flag{ - Name: covenantQuorumFlag, - Usage: "Required quorum for the covenant members", + Name: txInclusionHeightFlag, + Usage: "Inclusion height of the staking transactions. Necessary to chose correct global parameters for transaction", Required: true, }, cli.StringFlag{ @@ -515,6 +484,21 @@ type CreatePhase1UnbondingTxResponse struct { } func createPhase1UnbondingTransaction(ctx *cli.Context) error { + inputFilePath := ctx.Args().First() + if len(inputFilePath) == 0 { + return errors.New("json file input is empty") + } + + if !os.FileExists(inputFilePath) { + return fmt.Errorf("json file input %s does not exist", inputFilePath) + } + + globalParams, err := parser.NewParsedGlobalParamsFromFile(inputFilePath) + + if err != nil { + return fmt.Errorf("error parsing file %s: %w", inputFilePath, err) + } + net := ctx.String(networkNameFlag) currentParams, err := utils.GetBtcNetworkParams(net) @@ -531,57 +515,43 @@ func createPhase1UnbondingTransaction(ctx *cli.Context) error { return err } - magicBytes, err := parseMagicBytesFromCliCtx(ctx) + stakingTxInclusionHeight := ctx.Uint64(txInclusionHeightFlag) - if err != nil { - return err - } - - covenantMembersPks, err := parseCovenantKeysFromCliCtx(ctx) + paramsForHeight := globalParams.GetVersionedGlobalParamsByHeight(stakingTxInclusionHeight) - if err != nil { - return err + if paramsForHeight == nil { + return fmt.Errorf("no global params found for height %d", stakingTxInclusionHeight) } - covenantQuorum := uint32(ctx.Uint64(covenantQuorumFlag)) - stakingTxInfo, err := btcstaking.ParseV0StakingTx( stakingTx, - magicBytes, - covenantMembersPks, - covenantQuorum, + paramsForHeight.Tag, + paramsForHeight.CovenantPks, + paramsForHeight.CovenantQuorum, currentParams, ) if err != nil { - return fmt.Errorf("invalid staking transaction: %w", err) + return fmt.Errorf("provided staking transaction is not valid: %w, for params at height %d", err, stakingTxInclusionHeight) } - unbondingFee, err := parseAmountFromCliCtx(ctx, helpers.UnbondingFee) + unbondingAmount := stakingTxInfo.StakingOutput.Value - int64(paramsForHeight.UnbondingFee) - if err != nil { - return err - } - - unbondingTimeBlocks, err := parseLockTimeBlocksFromCliCtx(ctx, helpers.UnbondingTimeFlag) - - if err != nil { - return err - } - - unbondingAmout := stakingTxInfo.StakingOutput.Value - int64(unbondingFee) - - if unbondingAmout <= 0 { - return fmt.Errorf("invalid unbonding amount %d", unbondingAmout) + if unbondingAmount <= 0 { + return fmt.Errorf( + "staking output value is too low to create unbonding transaction. Stake amount: %d, Unbonding fee: %d", + stakingTxInfo.StakingOutput.Value, + paramsForHeight.UnbondingFee, + ) } unbondingInfo, err := btcstaking.BuildUnbondingInfo( stakingTxInfo.OpReturnData.StakerPublicKey.PubKey, []*btcec.PublicKey{stakingTxInfo.OpReturnData.FinalityProviderPublicKey.PubKey}, - covenantMembersPks, - covenantQuorum, - unbondingTimeBlocks, - btcutil.Amount(unbondingAmout), + paramsForHeight.CovenantPks, + paramsForHeight.CovenantQuorum, + paramsForHeight.UnbondingTime, + btcutil.Amount(unbondingAmount), currentParams, ) @@ -616,8 +586,8 @@ func createPhase1UnbondingTransaction(ctx *cli.Context) error { stakingScriptInfo, err := btcstaking.BuildStakingInfo( stakingTxInfo.OpReturnData.StakerPublicKey.PubKey, []*btcec.PublicKey{stakingTxInfo.OpReturnData.FinalityProviderPublicKey.PubKey}, - covenantMembersPks, - covenantQuorum, + paramsForHeight.CovenantPks, + paramsForHeight.CovenantQuorum, stakingTxInfo.OpReturnData.StakingTime, btcutil.Amount(stakingTxInfo.StakingOutput.Value), currentParams, diff --git a/cmd/stakercli/transaction/transactions_test.go b/cmd/stakercli/transaction/transactions_test.go index d231c74..5a7af61 100644 --- a/cmd/stakercli/transaction/transactions_test.go +++ b/cmd/stakercli/transaction/transactions_test.go @@ -7,13 +7,13 @@ import ( "encoding/hex" "encoding/json" "fmt" - "math" "math/rand" "os" "path/filepath" "testing" bbn "github.com/babylonchain/babylon/types" + "github.com/babylonchain/networks/parameters/parser" "github.com/babylonchain/babylon/btcstaking" "github.com/babylonchain/babylon/testutil/datagen" @@ -42,6 +42,40 @@ const ( unspendableKeyPathSchnor = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" ) +var ( + defaultParam = parser.VersionedGlobalParams{ + Version: 0, + ActivationHeight: 100, + StakingCap: 3000000, + CapHeight: 0, + Tag: "01020304", + CovenantPks: []string{ + "03ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "03a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "0359d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + "0357349e985e742d5131e1e2b227b5170f6350ac2e2feb72254fcc25b3cee21a18", + "03c8ccb03c379e452f10c81232b41a1ca8b63d0baf8387e57d302c987e5abb8527", + }, + CovenantQuorum: 3, + UnbondingTime: 1000, + UnbondingFee: 1000, + MaxStakingAmount: 300000, + MinStakingAmount: 3000, + MaxStakingTime: 10000, + MinStakingTime: 100, + ConfirmationDepth: 10, + } + + globalParams = parser.GlobalParams{ + Versions: []*parser.VersionedGlobalParams{&defaultParam}, + } + + paramsMarshalled, _ = json.Marshal(globalParams) + + parsedGlobalParams, _ = parser.ParseGlobalParams(&globalParams) + lastParams = parsedGlobalParams.Versions[len(parsedGlobalParams.Versions)-1] +) + func TestVerifyUnspendableKeyPath(t *testing.T) { bz, err := hex.DecodeString(unspendableKeyPath) require.NoError(t, err) @@ -79,6 +113,18 @@ func FuzzFinalityProviderDeposit(f *testing.F) { }) } +func appRunCreatePhase1StakingTxWithParams(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CreatePhase1StakingTxResponse { + args := []string{"stakercli", "transaction", "create-phase1-staking-transaction-with-params"} + args = append(args, arguments...) + output := appRunWithOutput(r, t, app, args) + + var data transaction.CreatePhase1StakingTxResponse + err := json.Unmarshal([]byte(output), &data) + require.NoError(t, err) + + return data +} + func appRunCreatePhase1StakingTx(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CreatePhase1StakingTxResponse { args := []string{"stakercli", "transaction", "create-phase1-staking-transaction"} args = append(args, arguments...) @@ -91,6 +137,18 @@ func appRunCreatePhase1StakingTx(r *rand.Rand, t *testing.T, app *cli.App, argum return data } +func appRunCheckPhase1StakingTx(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CheckPhase1StakingTxResponse { + args := []string{"stakercli", "transaction", "check-phase1-staking-transaction"} + args = append(args, arguments...) + output := appRunWithOutput(r, t, app, args) + + var data transaction.CheckPhase1StakingTxResponse + err := json.Unmarshal([]byte(output), &data) + require.NoError(t, err) + + return data +} + func genRandomPubKey(t *testing.T) *btcec.PublicKey { privKey, err := btcec.NewPrivateKey() require.NoError(t, err) @@ -140,111 +198,6 @@ func testApp() *cli.App { return app } -func TestCheckPhase1StakingTransactionCmd(t *testing.T) { - app := testApp() - stakerCliCheckP1StkTx := []string{ - "stakercli", "transaction", "check-phase1-staking-transaction", - "--covenant-quorum=1", - "--covenant-committee-pks=50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", - "--magic-bytes=01020304", - "--network=regtest", - "--staking-transaction=02000000000101ffa5874fdf64a535a4beae47ba0e66278b046baf7b3f3855dbf0413060aaeef90000000000fdffffff03404b4c00000000002251207c2649dc890238fada228d52a4c25fcef82e1cf3d7f53895ca0fcfb15dd142bb0000000000000000496a470102030400b91ea4619bc7b3f93e5015976f52f666ae4eb5c98018a6c8e41424905fa8591fa89e7caf57360bc8b791df72abc3fb6d2ddc0e06e171c9f17c4ea1299e677565cd50c876f7f70d0000001600141b9b57f4d4555e65ceb98c465c9580b0d6b0d0f60247304402200ae05daea3dc62ee7f2720c87705da28077ab19e420538eea5b92718271b4356022026c8367ac8bcd0b6d011842159cd525db672b234789a8d37725b247858c90a120121020721ef511b0faee2a487a346fdb96425d9dd7fa79210adbe7b47f0bcdc7e29de00000000", - } - // should pass without opt flags set - err := app.Run(stakerCliCheckP1StkTx) - require.NoError(t, err) - - validBtcPk := "b91ea4619bc7b3f93e5015976f52f666ae4eb5c98018a6c8e41424905fa8591f" - validFpPk := "a89e7caf57360bc8b791df72abc3fb6d2ddc0e06e171c9f17c4ea1299e677565" - validStakingTime := 52560 - realStakingAmount := 5000000 - validCheckArgs := append(stakerCliCheckP1StkTx, - fmt.Sprintf("--staker-pk=%s", validBtcPk), - fmt.Sprintf("--finality-provider-pk=%s", validFpPk), - fmt.Sprintf("--staking-time=%d", validStakingTime), - fmt.Sprintf("--min-staking-amount=%d", realStakingAmount), - fmt.Sprintf("--max-staking-amount=%d", realStakingAmount), - ) - err = app.Run(validCheckArgs) - require.NoError(t, err) - - err = app.Run([]string{ - "stakercli", "transaction", "check-phase1-staking-transaction", - "--covenant-quorum=1", - "--covenant-committee-pks=50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", - "--magic-bytes=62627434", - "--network=signet", - "--staking-transaction=02000000000101b8eba8646e5fdb240af853d52c37b6159984c34bebb55c6097c4f0d276e536c80000000000fdffffff0344770d000000000016001461e09f8a6e653c6bdec644874dc119be1b60f27a404b4c00000000002251204a4b057a9fa0510ccdce480fdac5a3cd12329993bac2517afb784a64d11fc1b40000000000000000496a4762627434002dedbb66510d56b11f7a611e290f044e24dd48fd9c8a76d103ba05c8e95f3558a89e7caf57360bc8b791df72abc3fb6d2ddc0e06e171c9f17c4ea1299e677565cd500247304402203bae17ac05c211e3c849595ef211f9a23ffc6d32d089e53cfaf81b94353f9e0c022063676b789a3fd85842552cd54408a8e92a1d37f51e0f4765ac29ef89ed707b750121032dedbb66510d56b11f7a611e290f044e24dd48fd9c8a76d103ba05c8e95f355800000000", - "--staker-pk=2dedbb66510d56b11f7a611e290f044e24dd48fd9c8a76d103ba05c8e95f3558", - "--finality-provider-pk=a89e7caf57360bc8b791df72abc3fb6d2ddc0e06e171c9f17c4ea1299e677565", - fmt.Sprintf("--staking-time=%d", validStakingTime), - fmt.Sprintf("--min-staking-amount=%d", realStakingAmount), - }) - require.NoError(t, err) - - err = app.Run([]string{ - "stakercli", "transaction", "check-phase1-staking-transaction", - "--covenant-quorum=1", - "--covenant-committee-pks=50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", - "--magic-bytes=62627434", - "--network=signet", - "--staking-transaction=02000000000101b8eba8646e5fdb240af853d52c37b6159984c34bebb55c6097c4f0d276e536c80000000000fdffffff0344770d000000000016001461e09f8a6e653c6bdec644874dc119be1b60f27a404b4c00000000002251204a4b057a9fa0510ccdce480fdac5a3cd12329993bac2517afb784a64d11fc1b40000000000000000496a4762627434002dedbb66510d56b11f7a611e290f044e24dd48fd9c8a76d103ba05c8e95f3558a89e7caf57360bc8b791df72abc3fb6d2ddc0e06e171c9f17c4ea1299e677565cd500247304402203bae17ac05c211e3c849595ef211f9a23ffc6d32d089e53cfaf81b94353f9e0c022063676b789a3fd85842552cd54408a8e92a1d37f51e0f4765ac29ef89ed707b750121032dedbb66510d56b11f7a611e290f044e24dd48fd9c8a76d103ba05c8e95f355800000000", - "--staker-pk=2dedbb66510d56b11f7a611e290f044e24dd48fd9c8a76d103ba05c8e95f3558", - "--finality-provider-pk=a89e7caf57360bc8b791df72abc3fb6d2ddc0e06e171c9f17c4ea1299e677565", - "--staking-time=52560", "--min-staking-amount=50000000", - }) - require.EqualError(t, err, fmt.Errorf("staking amount in tx %d is less than the min-staking-amount in flag 50000000", realStakingAmount).Error()) - - err = app.Run([]string{ - "stakercli", "transaction", "check-phase1-staking-transaction", - "--covenant-quorum=1", - "--covenant-committee-pks=50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", - "--magic-bytes=62627434", - "--network=signet", - "--staking-transaction=02000000000101b8eba8646e5fdb240af853d52c37b6159984c34bebb55c6097c4f0d276e536c80000000000fdffffff0344770d000000000016001461e09f8a6e653c6bdec644874dc119be1b60f27a404b4c00000000002251204a4b057a9fa0510ccdce480fdac5a3cd12329993bac2517afb784a64d11fc1b40000000000000000496a4762627434002dedbb66510d56b11f7a611e290f044e24dd48fd9c8a76d103ba05c8e95f3558a89e7caf57360bc8b791df72abc3fb6d2ddc0e06e171c9f17c4ea1299e677565cd500247304402203bae17ac05c211e3c849595ef211f9a23ffc6d32d089e53cfaf81b94353f9e0c022063676b789a3fd85842552cd54408a8e92a1d37f51e0f4765ac29ef89ed707b750121032dedbb66510d56b11f7a611e290f044e24dd48fd9c8a76d103ba05c8e95f355800000000", - "--staker-pk=2dedbb66510d56b11f7a611e290f044e24dd48fd9c8a76d103ba05c8e95f3558", - "--finality-provider-pk=a89e7caf57360bc8b791df72abc3fb6d2ddc0e06e171c9f17c4ea1299e677565", - "--staking-time=52560", "--min-staking-amount=0", "--max-staking-amount=10", - }) - require.EqualError(t, err, fmt.Errorf("staking amount in tx %d is more than the max-staking-amount in flag 10", realStakingAmount).Error()) - - // check if errors are caught in flags --staker-pk, --finality-provider-pk, --staking-time, --min-staking-amount - invalidStakerPk := "badstakerpk" - invalidBtcStakerArgs := append(stakerCliCheckP1StkTx, - fmt.Sprintf("--staker-pk=%s", invalidStakerPk), - ) - err = app.Run(invalidBtcStakerArgs) - require.EqualError(t, err, fmt.Errorf("staker pk in tx %s do not match with flag %s", validBtcPk, invalidStakerPk).Error()) - - invalidFpPk := "badfppk" - invalidFpPkArgs := append(stakerCliCheckP1StkTx, - fmt.Sprintf("--finality-provider-pk=%s", invalidFpPk), - ) - err = app.Run(invalidFpPkArgs) - require.EqualError(t, err, fmt.Errorf("finality provider pk in tx %s do not match with flag %s", validFpPk, invalidFpPk).Error()) - - invalidStakingTime := 50 - invalidStakingTimeArgs := append(stakerCliCheckP1StkTx, - fmt.Sprintf("--staking-time=%d", invalidStakingTime), - ) - err = app.Run(invalidStakingTimeArgs) - require.EqualError(t, err, fmt.Errorf("staking time in tx %d do not match with flag %d", validStakingTime, invalidStakingTime).Error()) - - invalidMinStakingAmount := realStakingAmount + 1 - invalidMinStakingAmountArgs := append(stakerCliCheckP1StkTx, - fmt.Sprintf("--min-staking-amount=%d", invalidMinStakingAmount), - ) - err = app.Run(invalidMinStakingAmountArgs) - require.EqualError(t, err, fmt.Errorf("staking amount in tx %d is less than the min-staking-amount in flag %d", realStakingAmount, invalidMinStakingAmount).Error()) - - invalidMaxStakingAmount := realStakingAmount - 1 - invalidMaxStakingAmountArgs := append(stakerCliCheckP1StkTx, - fmt.Sprintf("--max-staking-amount=%d", invalidMaxStakingAmount), - ) - err = app.Run(invalidMaxStakingAmountArgs) - require.EqualError(t, err, fmt.Errorf("staking amount in tx %d is more than the max-staking-amount in flag %d", realStakingAmount, invalidMaxStakingAmount).Error()) -} - func appRunCreatePhase1UnbondingTx(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CreatePhase1UnbondingTxResponse { args := []string{"stakercli", "transaction", "create-phase1-unbonding-transaction"} args = append(args, arguments...) @@ -256,32 +209,153 @@ func appRunCreatePhase1UnbondingTx(r *rand.Rand, t *testing.T, app *cli.App, arg return data } -func genRandomUint16(r *rand.Rand) uint16 { - return uint16(r.Intn(math.MaxUint16-1) + 1) +func randRange(r *rand.Rand, min, max int) int { + return rand.Intn(max+1-min) + min +} + +func createTempFileWithParams(f *testing.F) string { + file, err := os.CreateTemp("", "tmpParams-*.json") + require.NoError(f, err) + defer file.Close() + _, err = file.Write(paramsMarshalled) + require.NoError(f, err) + info, err := file.Stat() + require.NoError(f, err) + return filepath.Join(os.TempDir(), info.Name()) +} + +type StakeParameters struct { + StakerPk *btcec.PublicKey + FinalityProviderPk *btcec.PublicKey + StakingTime uint16 + StakingAmount btcutil.Amount + InclusionHeight uint64 +} + +func createCustomValidStakeParams( + t *testing.T, + r *rand.Rand, + p *parser.GlobalParams, + net *chaincfg.Params, +) (*StakeParameters, []string) { + lastParams := p.Versions[len(p.Versions)-1] + inclusionHeight := lastParams.ActivationHeight + 1 + + stakerKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + fpKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + stakingTime := randRange(r, int(lastParams.MinStakingTime), int(lastParams.MaxStakingTime)) + stakingAmount := btcutil.Amount(randRange(r, int(lastParams.MinStakingAmount), int(lastParams.MaxStakingAmount))) + + var args []string + args = append(args, fmt.Sprintf("--staker-pk=%s", hex.EncodeToString(schnorr.SerializePubKey(stakerKey.PubKey())))) + args = append(args, fmt.Sprintf("--finality-provider-pk=%s", hex.EncodeToString(schnorr.SerializePubKey(fpKey.PubKey())))) + args = append(args, fmt.Sprintf("--staking-time=%d", stakingTime)) + args = append(args, fmt.Sprintf("--staking-amount=%d", stakingAmount)) + args = append(args, fmt.Sprintf("--tx-inclusion-height=%d", inclusionHeight)) + args = append(args, fmt.Sprintf("--network=%s", net.Name)) + return &StakeParameters{ + StakerPk: stakerKey.PubKey(), + FinalityProviderPk: fpKey.PubKey(), + StakingTime: uint16(stakingTime), + StakingAmount: stakingAmount, + InclusionHeight: inclusionHeight, + }, args +} + +// Property: Every create should end without error for valid params +func FuzzCreatPhase1Tx(f *testing.F) { + paramsFilePath := createTempFileWithParams(f) + + datagen.AddRandomSeedsToFuzzer(f, 5) + f.Fuzz(func(t *testing.T, seed int64) { + r := rand.New(rand.NewSource(seed)) + app := testApp() + + var args []string + args = append(args, paramsFilePath) + + _, createArgs := createCustomValidStakeParams(t, r, &globalParams, &chaincfg.RegressionNetParams) + + args = append(args, createArgs...) + + resCreate := appRunCreatePhase1StakingTxWithParams( + r, t, app, args, + ) + require.NotNil(t, resCreate) + }) } -func genRandomInt64(r *rand.Rand) int64 { - return int64(r.Intn(100000-1000) + 1000) +func keyToSchnorrHex(key *btcec.PublicKey) string { + return hex.EncodeToString(schnorr.SerializePubKey(key)) +} + +func FuzzCheckPhase1Tx(f *testing.F) { + paramsFilePath := createTempFileWithParams(f) + + datagen.AddRandomSeedsToFuzzer(f, 5) + f.Fuzz(func(t *testing.T, seed int64) { + r := rand.New(rand.NewSource(seed)) + app := testApp() + + stakerParams, _ := createCustomValidStakeParams(t, r, &globalParams, &chaincfg.RegressionNetParams) + + _, tx, err := btcstaking.BuildV0IdentifiableStakingOutputsAndTx( + lastParams.Tag, + stakerParams.StakerPk, + stakerParams.FinalityProviderPk, + lastParams.CovenantPks, + lastParams.CovenantQuorum, + stakerParams.StakingTime, + stakerParams.StakingAmount, + &chaincfg.RegressionNetParams, + ) + require.NoError(t, err) + + fakeInputHash := sha256.Sum256([]byte{0x01}) + tx.AddTxIn(wire.NewTxIn(&wire.OutPoint{Hash: fakeInputHash, Index: 0}, nil, nil)) + + serializedStakingTx, err := utils.SerializeBtcTransaction(tx) + require.NoError(t, err) + + checkArgs := []string{ + paramsFilePath, + fmt.Sprintf("--staking-transaction=%s", hex.EncodeToString(serializedStakingTx)), + fmt.Sprintf("--network=%s", chaincfg.RegressionNetParams.Name), + } + + resCheck := appRunCheckPhase1StakingTx( + r, t, app, checkArgs, + ) + require.NotNil(t, resCheck) + require.True(t, resCheck.IsValid) + require.NotNil(t, resCheck.StakingData) + require.Equal(t, globalParams.Versions[0].Version, uint64(resCheck.StakingData.ParamsVersion)) + require.Equal(t, stakerParams.StakingAmount, btcutil.Amount(resCheck.StakingData.StakingAmount)) + require.Equal(t, stakerParams.StakingTime, uint16(resCheck.StakingData.StakingTimeBlocks)) + require.Equal(t, keyToSchnorrHex(stakerParams.StakerPk), resCheck.StakingData.StakerPublicKeyHex) + require.Equal(t, keyToSchnorrHex(stakerParams.FinalityProviderPk), resCheck.StakingData.FinalityProviderPublicKeyHex) + }) } func FuzzCreateUnbondingTx(f *testing.F) { + paramsFilePath := createTempFileWithParams(f) + datagen.AddRandomSeedsToFuzzer(f, 10) f.Fuzz(func(t *testing.T, seed int64) { r := rand.New(rand.NewSource(seed)) - mb := datagen.GenRandomByteArray(r, btcstaking.MagicBytesLen) - stakerKey := genRandomPubKey(t) - fpKey := genRandomPubKey(t) - cov1Key := genRandomPubKey(t) - cov2Key := genRandomPubKey(t) + + stakerParams, _ := createCustomValidStakeParams(t, r, &globalParams, &chaincfg.RegressionNetParams) _, tx, err := btcstaking.BuildV0IdentifiableStakingOutputsAndTx( - mb, - stakerKey, - fpKey, - []*btcec.PublicKey{cov1Key, cov2Key}, - 1, - genRandomUint16(r), - btcutil.Amount(genRandomInt64(r)), + lastParams.Tag, + stakerParams.StakerPk, + stakerParams.FinalityProviderPk, + lastParams.CovenantPks, + lastParams.CovenantQuorum, + stakerParams.StakingTime, + stakerParams.StakingAmount, &chaincfg.RegressionNetParams, ) require.NoError(t, err) @@ -292,17 +366,11 @@ func FuzzCreateUnbondingTx(f *testing.F) { serializedStakingTx, err := utils.SerializeBtcTransaction(tx) require.NoError(t, err) - unbondingTime := genRandomUint16(r) - createTxCmdArgs := []string{ + paramsFilePath, fmt.Sprintf("--staking-transaction=%s", hex.EncodeToString(serializedStakingTx)), - fmt.Sprintf("--unbonding-fee=%d", 100), - fmt.Sprintf("--unbonding-time=%d", unbondingTime), - fmt.Sprintf("--magic-bytes=%s", hex.EncodeToString(mb)), - fmt.Sprintf("--covenant-committee-pks=%s", hex.EncodeToString(schnorr.SerializePubKey(cov1Key))), - fmt.Sprintf("--covenant-committee-pks=%s", hex.EncodeToString(schnorr.SerializePubKey(cov2Key))), - "--covenant-quorum=1", - "--network=regtest", + fmt.Sprintf("--tx-inclusion-height=%d", stakerParams.InclusionHeight), + fmt.Sprintf("--network=%s", chaincfg.RegressionNetParams.Name), } app := testApp() diff --git a/example/global-params.json b/example/global-params.json new file mode 100644 index 0000000..76183a2 --- /dev/null +++ b/example/global-params.json @@ -0,0 +1,26 @@ +{ + "versions": [ + { + "version": 0, + "activation_height": 1, + "staking_cap": 50000000000, + "cap_height": 0, + "tag": "01020304", + "covenant_pks": [ + "0205149a0c7a95320adf210e47bca8b363b7bd966be86be6392dd6cf4f96995869", + "02e8d503cb52715249f32f3ee79cee88dfd48c2565cb0c79cf9640d291f46fd518", + "02fe81b2409a32ddfd8ec1556557e8dd949b6e4fd37047523cb7f5fefca283d542", + "02bc4a1ff485d7b44faeec320b81ad31c3cad4d097813c21fcf382b4305e4cfc82", + "02001e50601a4a1c003716d7a1ee7fe25e26e55e24e909b3642edb60d30e3c40c1" + ], + "covenant_quorum": 3, + "unbonding_time": 1000, + "unbonding_fee": 20000, + "max_staking_amount": 1000000000, + "min_staking_amount": 1000000, + "max_staking_time": 64000, + "min_staking_time": 64000, + "confirmation_depth": 6 + } + ] +} diff --git a/go.mod b/go.mod index 205043c..0711203 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,13 @@ module github.com/babylonchain/btc-staker -go 1.21 - -toolchain go1.21.4 +go 1.22.3 require ( cosmossdk.io/errors v1.0.1 cosmossdk.io/math v1.3.0 github.com/avast/retry-go/v4 v4.5.1 github.com/babylonchain/babylon v0.8.6-0.20240619103849-013f733e9537 + github.com/babylonchain/networks/parameters v0.2.0 github.com/btcsuite/btcd v0.24.0 github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcd/btcutil v1.1.5 diff --git a/go.sum b/go.sum index 0d53347..c752368 100644 --- a/go.sum +++ b/go.sum @@ -287,6 +287,8 @@ github.com/aws/aws-sdk-go v1.44.312/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/babylonchain/babylon v0.8.6-0.20240619103849-013f733e9537 h1:jbUK7ooiB8Syfrn3bFxueCS3pxbA8ezx3hG+OaEEkrE= github.com/babylonchain/babylon v0.8.6-0.20240619103849-013f733e9537/go.mod h1:YFALTW+Kp/b5jSDoA7Z70RggJjAedlmQTrpdeU8c3hY= +github.com/babylonchain/networks/parameters v0.2.0 h1:f3e1MwMFm33mIKji7AgQk39aO9YIfbCbbjXtQHEQ99E= +github.com/babylonchain/networks/parameters v0.2.0/go.mod h1:nejhvrL7Iwh5Vunvkg7pnomQZlHnyNzOY9lQaDp6tOA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= diff --git a/tools/go.mod b/tools/go.mod index f83966e..09ded4f 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -1,8 +1,6 @@ module github.com/babylonchain/vigilante/tools -go 1.21 - -toolchain go1.21.4 +go 1.22.3 require github.com/babylonchain/babylon v0.8.6-0.20240619103849-013f733e9537