├── x └── btcbridge │ ├── module_simulation.go │ ├── types │ ├── asset.go │ ├── merkle_proof.go │ ├── message_update_params.go │ ├── message_submit_headers.go │ ├── expected_keepers.go │ ├── message_submit_signature.go │ ├── message_submit_withdraw_status.go │ ├── message_update_senders.go │ ├── message_withdraw_bitcoin.go │ ├── message_submit_withdraw_transaction.go │ ├── codec.go │ ├── message_submit_deposit_transaction.go │ ├── runes_test.go │ ├── varint.go │ ├── errors.go │ ├── keys.go │ ├── genesis.go │ ├── signature.go │ ├── params.go │ ├── deposit_policy.go │ └── runes.go │ ├── keeper │ ├── event.go │ ├── util.go │ ├── queries.go │ ├── keeper.go │ ├── keeper_test.go │ ├── msg_server.go │ └── keeper_deposit.go │ ├── genesis.go │ ├── genesis_test.go │ ├── client │ └── cli │ │ ├── tx.go │ │ └── query.go │ └── module.go ├── .gitignore ├── testutil ├── sample │ └── sample.go ├── nullify │ └── nullify.go ├── keeper │ └── btc_bridge.go └── network │ └── network.go ├── tools └── tools.go ├── proto ├── buf.gen.swagger.yaml ├── buf.gen.sta.yaml ├── buf.gen.ts.yaml ├── buf.gen.gogo.yaml ├── side │ └── btcbridge │ │ ├── genesis.proto │ │ ├── params.proto │ │ ├── bitcoin.proto │ │ ├── tx.proto │ │ └── query.proto ├── buf.gen.pulsar.yaml ├── buf.yaml └── buf.lock ├── signet.block ├── cmd └── sided │ ├── main.go │ └── cmd │ ├── config.go │ └── genaccounts.go ├── app ├── params │ └── encoding.go ├── genesis.go ├── wasm_config.go ├── encoding.go ├── test_helper.go ├── export.go └── test_setup.go ├── docs ├── docs.go └── template │ └── index.tpl ├── config.yml ├── create_validator.sh ├── README.md ├── .github └── workflows │ └── release.yml ├── second_node.sh ├── local_node_dev.sh ├── local_node.sh └── go.mod /x/btcbridge/module_simulation.go: -------------------------------------------------------------------------------- 1 | package btcbridge 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vue/node_modules 2 | vue/dist 3 | release/ 4 | .idea/ 5 | .vscode/ 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /testutil/sample/sample.go: -------------------------------------------------------------------------------- 1 | package sample 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | ) 7 | 8 | // AccAddress returns a sample account address 9 | func AccAddress() string { 10 | pk := ed25519.GenPrivKey().PubKey() 11 | addr := pk.Address() 12 | return sdk.AccAddress(addr).String() 13 | } 14 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/cosmos/gogoproto/protoc-gen-gocosmos" 7 | _ "github.com/golang/protobuf/protoc-gen-go" 8 | _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway" 9 | _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger" 10 | _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2" 11 | ) 12 | -------------------------------------------------------------------------------- /proto/buf.gen.swagger.yaml: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from Ignite. You can edit 2 | # the file content but do not change the file name or path. 3 | # 4 | # buf.gen.swagger.yaml 5 | # 6 | version: v1 7 | plugins: 8 | - name: openapiv2 9 | out: . 10 | opt: 11 | - logtostderr=true 12 | - openapi_naming_strategy=fqn 13 | - json_names_for_fields=false 14 | - generate_unbound_methods=true -------------------------------------------------------------------------------- /proto/buf.gen.sta.yaml: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from Ignite. You can edit 2 | # the file content but do not change the file name or path. 3 | # 4 | # buf.gen.sta.yaml 5 | # 6 | version: v1 7 | plugins: 8 | - name: openapiv2 9 | out: . 10 | opt: 11 | - logtostderr=true 12 | - openapi_naming_strategy=simple 13 | - ignore_comments=true 14 | - simple_operation_ids=false 15 | - json_names_for_fields=false 16 | -------------------------------------------------------------------------------- /x/btcbridge/types/asset.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // AssetTypeFromDenom returns the asset type according to the denom 9 | func AssetTypeFromDenom(denom string, p Params) AssetType { 10 | if denom == p.BtcVoucherDenom { 11 | return AssetType_ASSET_TYPE_BTC 12 | } 13 | 14 | if strings.HasPrefix(denom, fmt.Sprintf("%s/", RunesProtocolName)) { 15 | return AssetType_ASSET_TYPE_RUNE 16 | } 17 | 18 | return AssetType_ASSET_TYPE_UNSPECIFIED 19 | } 20 | -------------------------------------------------------------------------------- /signet.block: -------------------------------------------------------------------------------- 1 | "best_block_header": { 2 | "version": "667459584", 3 | "hash": "0000000000000009fb68da72e8994f014fafb455c72978233b94580b12af778c", 4 | "height": "2815023", 5 | "previous_block_hash": "0000000000000004a29c20eb32532718de8072665620edb4c657b22b4d463967", 6 | "merkle_root": "9e219423eadce80e882cdff04b3026c9bbc994fd08a774f34a705ca3e710a332", 7 | "nonce": "3913166971", 8 | "bits": "191881b8", 9 | "time": "1715566066", 10 | "ntx": "6236" 11 | } 12 | -------------------------------------------------------------------------------- /proto/buf.gen.ts.yaml: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from Ignite. You can edit 2 | # the file content but do not change the file name or path. 3 | # 4 | # buf.gen.ts.yaml 5 | # 6 | version: v1 7 | managed: 8 | enabled: true 9 | plugins: 10 | - plugin: buf.build/community/stephenh-ts-proto 11 | out: . 12 | opt: 13 | - logtostderr=true 14 | - allow_merge=true 15 | - json_names_for_fields=false 16 | - ts_proto_opt=snakeToCamel=true 17 | - ts_proto_opt=esModuleInterop=true 18 | - ts_proto_out=. 19 | -------------------------------------------------------------------------------- /cmd/sided/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/cosmos/cosmos-sdk/server" 7 | svrcmd "github.com/cosmos/cosmos-sdk/server/cmd" 8 | 9 | "github.com/sideprotocol/side/app" 10 | "github.com/sideprotocol/side/cmd/sided/cmd" 11 | ) 12 | 13 | func main() { 14 | rootCmd, _ := cmd.NewRootCmd() 15 | if err := svrcmd.Execute(rootCmd, "", app.DefaultNodeHome); err != nil { 16 | switch e := err.(type) { 17 | case server.ErrorCode: 18 | os.Exit(e.Code) 19 | 20 | default: 21 | os.Exit(1) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /proto/buf.gen.gogo.yaml: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from Ignite. You can edit 2 | # the file content but do not change the file name or path. 3 | # 4 | # buf.gen.gogo.yaml 5 | # 6 | version: v1 7 | plugins: 8 | - name: gocosmos 9 | out: . 10 | opt: 11 | - plugins=grpc 12 | - Mgoogle/protobuf/any.proto=github.com/cosmos/cosmos-sdk/codec/types 13 | - Mcosmos/orm/v1/orm.proto=cosmossdk.io/orm 14 | - name: grpc-gateway 15 | out: . 16 | opt: 17 | - logtostderr=true 18 | - allow_colon_final_segments=true 19 | -------------------------------------------------------------------------------- /x/btcbridge/keeper/event.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | "github.com/sideprotocol/side/x/btcbridge/types" 6 | ) 7 | 8 | func (k Keeper) EmitEvent(ctx sdk.Context, sender string, attr ...sdk.Attribute) { 9 | headerAttr := []sdk.Attribute{ 10 | { 11 | Key: "sender", 12 | Value: sender, 13 | }, 14 | } 15 | 16 | headerAttr = append(headerAttr, attr...) 17 | ctx.EventManager().EmitEvent( 18 | sdk.NewEvent( 19 | types.ModuleName, 20 | // attr..., 21 | headerAttr..., 22 | ), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/params/encoding.go: -------------------------------------------------------------------------------- 1 | package params 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/client" 5 | "github.com/cosmos/cosmos-sdk/codec" 6 | "github.com/cosmos/cosmos-sdk/codec/types" 7 | ) 8 | 9 | // EncodingConfig specifies the concrete encoding types to use for a given app. 10 | // This is provided for compatibility between protobuf and amino implementations. 11 | type EncodingConfig struct { 12 | InterfaceRegistry types.InterfaceRegistry 13 | Marshaler codec.Codec 14 | TxConfig client.TxConfig 15 | Amino *codec.LegacyAmino 16 | } 17 | -------------------------------------------------------------------------------- /proto/side/btcbridge/genesis.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package side.btcbridge; 3 | 4 | import "gogoproto/gogo.proto"; 5 | import "side/btcbridge/params.proto"; 6 | import "side/btcbridge/bitcoin.proto"; 7 | 8 | option go_package = "github.com/sideprotocol/side/x/btcbridge/types"; 9 | 10 | // GenesisState defines the btc light client module's genesis state. 11 | message GenesisState { 12 | Params params = 1 [(gogoproto.nullable) = false]; 13 | // the chain tip of the bitcoin chain 14 | BlockHeader best_block_header = 2; 15 | repeated BlockHeader block_headers = 3; 16 | repeated UTXO utxos = 4; 17 | } 18 | -------------------------------------------------------------------------------- /proto/buf.gen.pulsar.yaml: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from Ignite. You can edit 2 | # the file content but do not change the file name or path. 3 | # 4 | # buf.gen.pulsar.yaml 5 | # 6 | version: v1 7 | managed: 8 | enabled: true 9 | go_package_prefix: 10 | default: cosmossdk.io/api 11 | except: 12 | - buf.build/googleapis/googleapis 13 | - buf.build/cosmos/gogo-proto 14 | - buf.build/cosmos/cosmos-proto 15 | override: 16 | plugins: 17 | - name: go-pulsar 18 | out: ./api 19 | opt: paths=source_relative 20 | - name: go-grpc 21 | out: ./api 22 | opt: paths=source_relative 23 | -------------------------------------------------------------------------------- /proto/buf.yaml: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from Ignite. You can edit 2 | # the file content but do not change the file name or path. 3 | # 4 | # buf.yaml 5 | # 6 | version: v1 7 | deps: 8 | - buf.build/protocolbuffers/wellknowntypes 9 | - buf.build/cosmos/cosmos-sdk 10 | - buf.build/cosmos/cosmos-proto 11 | - buf.build/cosmos/gogo-proto 12 | - buf.build/googleapis/googleapis 13 | - buf.build/cosmos/ics23 14 | breaking: 15 | use: 16 | - FILE 17 | lint: 18 | use: 19 | - DEFAULT 20 | - COMMENTS 21 | - FILE_LOWER_SNAKE_CASE 22 | except: 23 | - UNARY_RPC 24 | - COMMENT_FIELD 25 | - SERVICE_SUFFIX 26 | - PACKAGE_VERSION_SUFFIX 27 | - RPC_REQUEST_STANDARD_NAME 28 | ignore: 29 | - tendermint 30 | -------------------------------------------------------------------------------- /app/genesis.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/cosmos/cosmos-sdk/codec" 7 | ) 8 | 9 | // The genesis state of the blockchain is represented here as a map of raw json 10 | // messages key'd by a identifier string. 11 | // The identifier is used to determine which module genesis information belongs 12 | // to so it may be appropriately routed during init chain. 13 | // Within this application default genesis information is retrieved from 14 | // the ModuleBasicManager which populates json from each BasicModule 15 | // object provided to it during init. 16 | type GenesisState map[string]json.RawMessage 17 | 18 | // NewDefaultGenesisState generates the default state for the application. 19 | func NewDefaultGenesisState(cdc codec.JSONCodec) GenesisState { 20 | return ModuleBasics.DefaultGenesis(cdc) 21 | } 22 | -------------------------------------------------------------------------------- /proto/buf.lock: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from Ignite. 2 | # DO NOT EDIT 3 | # 4 | # buf.lock 5 | # 6 | version: v1 7 | deps: 8 | - remote: buf.build 9 | owner: cosmos 10 | repository: cosmos-proto 11 | commit: 1935555c206d4afb9e94615dfd0fad31 12 | - remote: buf.build 13 | owner: cosmos 14 | repository: cosmos-sdk 15 | commit: 954f7b05f38440fc8250134b15adec47 16 | - remote: buf.build 17 | owner: cosmos 18 | repository: gogo-proto 19 | commit: 34d970b699f84aa382f3c29773a60836 20 | - remote: buf.build 21 | owner: cosmos 22 | repository: ics23 23 | commit: 3c44d8daa8b44059ac744cd17d4a49d7 24 | - remote: buf.build 25 | owner: googleapis 26 | repository: googleapis 27 | commit: 75b4300737fb4efca0831636be94e517 28 | - remote: buf.build 29 | owner: protocolbuffers 30 | repository: wellknowntypes 31 | commit: 44e83bc050a4497fa7b36b34d95ca156 32 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "embed" 5 | httptemplate "html/template" 6 | "net/http" 7 | 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | const ( 12 | apiFile = "/static/openapi.yml" 13 | indexFile = "template/index.tpl" 14 | ) 15 | 16 | //go:embed static 17 | var Static embed.FS 18 | 19 | //go:embed template 20 | var template embed.FS 21 | 22 | func RegisterOpenAPIService(appName string, rtr *mux.Router) { 23 | rtr.Handle(apiFile, http.FileServer(http.FS(Static))) 24 | rtr.HandleFunc("/", handler(appName)) 25 | } 26 | 27 | // handler returns an http handler that servers OpenAPI console for an OpenAPI spec at specURL. 28 | func handler(title string) http.HandlerFunc { 29 | t, _ := httptemplate.ParseFS(template, indexFile) 30 | 31 | return func(w http.ResponseWriter, req *http.Request) { 32 | t.Execute(w, struct { 33 | Title string 34 | URL string 35 | }{ 36 | title, 37 | apiFile, 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /x/btcbridge/types/merkle_proof.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/btcsuite/btcd/blockchain" 7 | "github.com/btcsuite/btcd/chaincfg/chainhash" 8 | ) 9 | 10 | // VerifyMerkleProof verifies a Merkle proof 11 | func VerifyMerkleProof(proofs []string, txHash, root *chainhash.Hash) bool { 12 | current := txHash 13 | for _, proof := range proofs { 14 | 15 | bytes, err := base64.StdEncoding.DecodeString(proof) 16 | if err != nil { 17 | return false 18 | } 19 | position := bytes[0] 20 | p := current 21 | if len(bytes) > 1 { 22 | p, err = chainhash.NewHash(bytes[1:]) 23 | if err != nil { 24 | return false 25 | } 26 | } 27 | 28 | var temp chainhash.Hash 29 | if position == 0 { 30 | temp = blockchain.HashMerkleBranches(current, p) 31 | } else { 32 | temp = blockchain.HashMerkleBranches(p, current) 33 | } 34 | current = &temp 35 | } 36 | 37 | return current.IsEqual(root) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/sided/cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/chaincfg" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | 7 | "github.com/sideprotocol/side/app" 8 | ) 9 | 10 | func initSDKConfig() { 11 | // Set prefixes 12 | accountPubKeyPrefix := app.AccountAddressPrefix + "pub" 13 | validatorAddressPrefix := app.AccountAddressPrefix + "valoper" 14 | validatorPubKeyPrefix := app.AccountAddressPrefix + "valoperpub" 15 | consNodeAddressPrefix := app.AccountAddressPrefix + "valcons" 16 | consNodePubKeyPrefix := app.AccountAddressPrefix + "valconspub" 17 | 18 | // Set and seal config 19 | config := sdk.GetConfig() 20 | config.SetBech32PrefixForAccount(app.AccountAddressPrefix, accountPubKeyPrefix) 21 | config.SetBech32PrefixForValidator(validatorAddressPrefix, validatorPubKeyPrefix) 22 | config.SetBech32PrefixForConsensusNode(consNodeAddressPrefix, consNodePubKeyPrefix) 23 | config.SetBtcChainCfg(&chaincfg.SigNetParams) 24 | config.Seal() 25 | } 26 | -------------------------------------------------------------------------------- /app/wasm_config.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" 5 | ) 6 | 7 | const ( 8 | // DefaultInstanceCost is initially set the same as in wasmd 9 | DefaultInstanceCost uint64 = 60_000 10 | // DefaultCompileCost set to a large number for testing 11 | DefaultCompileCost uint64 = 100 12 | ) 13 | 14 | // MunGasRegisterConfig is defaults plus a custom compile amount 15 | func GasRegisterConfig() wasmtypes.WasmGasRegisterConfig { 16 | gasConfig := wasmtypes.DefaultGasRegisterConfig() 17 | gasConfig.InstanceCost = DefaultInstanceCost 18 | gasConfig.CompileCost = DefaultCompileCost 19 | 20 | return gasConfig 21 | } 22 | 23 | func NewSideWasmGasRegister() wasmtypes.WasmGasRegister { 24 | return wasmtypes.NewWasmGasRegister(GasRegisterConfig()) 25 | } 26 | 27 | func AllCapabilities() []string { 28 | return []string{ 29 | "iterator", 30 | "staking", 31 | "stargate", 32 | "cosmwasm_1_1", 33 | "cosmwasm_1_2", 34 | "cosmwasm_1_3", 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /x/btcbridge/keeper/util.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | 7 | "github.com/btcsuite/btcd/chaincfg/chainhash" 8 | "github.com/btcsuite/btcd/wire" 9 | "github.com/sideprotocol/side/x/btcbridge/types" 10 | ) 11 | 12 | func HeaderConvert(header *types.BlockHeader) *wire.BlockHeader { 13 | prehash, _ := chainhash.NewHashFromStr(header.PreviousBlockHash) 14 | root, _ := chainhash.NewHashFromStr(header.MerkleRoot) 15 | n := new(big.Int) 16 | n.SetString(header.Bits, 16) 17 | return &wire.BlockHeader{ 18 | Version: int32(header.Version), 19 | PrevBlock: *prehash, 20 | MerkleRoot: *root, 21 | Timestamp: time.Unix(int64(header.Time), 0), 22 | Bits: uint32(n.Uint64()), 23 | Nonce: uint32(header.Nonce), 24 | } 25 | } 26 | 27 | func BitsToTarget(bits string) *big.Int { 28 | n := new(big.Int) 29 | n.SetString(bits, 16) 30 | return n 31 | } 32 | 33 | func BitsToTargetUint32(bits string) uint32 { 34 | return uint32(BitsToTarget(bits).Uint64()) 35 | } 36 | -------------------------------------------------------------------------------- /docs/template/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ .Title }} 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 24 | 25 | 26 | Footer 27 | © 2022 GitHub, Inc. 28 | Footer navigation 29 | -------------------------------------------------------------------------------- /app/encoding.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/codec" 5 | "github.com/cosmos/cosmos-sdk/codec/types" 6 | "github.com/cosmos/cosmos-sdk/std" 7 | "github.com/cosmos/cosmos-sdk/x/auth/tx" 8 | 9 | "github.com/sideprotocol/side/app/params" 10 | ) 11 | 12 | // makeEncodingConfig creates an EncodingConfig for an amino based test configuration. 13 | func makeEncodingConfig() params.EncodingConfig { 14 | amino := codec.NewLegacyAmino() 15 | interfaceRegistry := types.NewInterfaceRegistry() 16 | marshaler := codec.NewProtoCodec(interfaceRegistry) 17 | txCfg := tx.NewTxConfig(marshaler, tx.DefaultSignModes) 18 | 19 | return params.EncodingConfig{ 20 | InterfaceRegistry: interfaceRegistry, 21 | Marshaler: marshaler, 22 | TxConfig: txCfg, 23 | Amino: amino, 24 | } 25 | } 26 | 27 | // MakeEncodingConfig creates an EncodingConfig for testing 28 | func MakeEncodingConfig() params.EncodingConfig { 29 | encodingConfig := makeEncodingConfig() 30 | std.RegisterLegacyAminoCodec(encodingConfig.Amino) 31 | std.RegisterInterfaces(encodingConfig.InterfaceRegistry) 32 | ModuleBasics.RegisterLegacyAminoCodec(encodingConfig.Amino) 33 | ModuleBasics.RegisterInterfaces(encodingConfig.InterfaceRegistry) 34 | return encodingConfig 35 | } 36 | -------------------------------------------------------------------------------- /x/btcbridge/genesis.go: -------------------------------------------------------------------------------- 1 | package btcbridge 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | "github.com/sideprotocol/side/x/btcbridge/keeper" 6 | "github.com/sideprotocol/side/x/btcbridge/types" 7 | ) 8 | 9 | // InitGenesis initializes the module's state from a provided genesis state. 10 | func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { 11 | // this line is used by starport scaffolding # genesis/module/init 12 | k.SetParams(ctx, genState.Params) 13 | k.SetBestBlockHeader(ctx, genState.BestBlockHeader) 14 | if len(genState.BlockHeaders) > 0 { 15 | err := k.SetBlockHeaders(ctx, genState.BlockHeaders) 16 | if err != nil { 17 | panic(err) 18 | } 19 | } 20 | // import utxos 21 | for _, utxo := range genState.Utxos { 22 | k.SetUTXO(ctx, utxo) 23 | } 24 | } 25 | 26 | // ExportGenesis returns the module's exported genesis 27 | func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { 28 | genesis := types.DefaultGenesis() 29 | genesis.Params = k.GetParams(ctx) 30 | genesis.BestBlockHeader = k.GetBestBlockHeader(ctx) 31 | genesis.BlockHeaders = k.GetAllBlockHeaders(ctx) 32 | genesis.Utxos = k.GetAllUTXOs(ctx) 33 | 34 | // this line is used by starport scaffolding # genesis/module/export 35 | 36 | return genesis 37 | } 38 | -------------------------------------------------------------------------------- /x/btcbridge/types/message_update_params.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 6 | ) 7 | 8 | const TypeMsgUpdateParams = "update_params" 9 | 10 | // Route returns the route of MsgUpdateParams. 11 | func (msg *MsgUpdateParamsRequest) Route() string { 12 | return RouterKey 13 | } 14 | 15 | // Type returns the type of MsgUpdateParams. 16 | func (msg *MsgUpdateParamsRequest) Type() string { 17 | return TypeMsgUpdateParams 18 | } 19 | 20 | // GetSignBytes implements the LegacyMsg interface. 21 | func (m MsgUpdateParamsRequest) GetSignBytes() []byte { 22 | return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) 23 | } 24 | 25 | // GetSigners returns the expected signers for a MsgUpdateParams message. 26 | func (m *MsgUpdateParamsRequest) GetSigners() []sdk.AccAddress { 27 | addr, _ := sdk.AccAddressFromBech32(m.Authority) 28 | return []sdk.AccAddress{addr} 29 | } 30 | 31 | // ValidateBasic performs basic MsgUpdateParams message validation. 32 | func (m *MsgUpdateParamsRequest) ValidateBasic() error { 33 | if _, err := sdk.AccAddressFromBech32(m.Authority); err != nil { 34 | return sdkerrors.Wrap(err, "invalid authority address") 35 | } 36 | 37 | if err := m.Params.Validate(); err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /proto/side/btcbridge/params.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package side.btcbridge; 3 | 4 | import "gogoproto/gogo.proto"; 5 | 6 | option go_package = "github.com/sideprotocol/side/x/btcbridge/types"; 7 | 8 | // Params defines the parameters for the module. 9 | message Params { 10 | // Only accept blocks sending from these addresses 11 | repeated string authorized_relayers = 1; 12 | // The minimum number of confirmations required for a block to be accepted 13 | int32 confirmations = 2; 14 | // Indicates the maximum depth or distance from the latest block up to which transactions are considered for acceptance. 15 | uint64 max_acceptable_block_depth = 3; 16 | // the denomanation of the voucher 17 | string btc_voucher_denom = 4; 18 | repeated Vault vaults = 5; 19 | } 20 | 21 | // AssetType defines the type of asset 22 | enum AssetType { 23 | // Unspecified asset type 24 | ASSET_TYPE_UNSPECIFIED = 0; 25 | // BTC 26 | ASSET_TYPE_BTC = 1; 27 | // BRC20: ordi, sats 28 | ASSET_TYPE_BRC20 = 2; 29 | // RUNE, dog*go*to*the*moon 30 | ASSET_TYPE_RUNE = 3; 31 | } 32 | 33 | // Vault defines the parameters for the module. 34 | message Vault { 35 | // the depositor should send their btc to this address 36 | string address = 1; 37 | // the pub key to which the voucher is sent 38 | string pub_key = 2; 39 | // the address to which the voucher is sent 40 | AssetType asset_type = 4; 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /x/btcbridge/types/message_submit_headers.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdkerrors "cosmossdk.io/errors" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | ) 7 | 8 | const TypeMsgSubmitBlockHeader = "submit_block_header" 9 | 10 | func NewMsgSubmitBlockHeaderRequest( 11 | sender string, 12 | headers []*BlockHeader, 13 | ) *MsgSubmitBlockHeaderRequest { 14 | return &MsgSubmitBlockHeaderRequest{ 15 | Sender: sender, 16 | BlockHeaders: headers, 17 | } 18 | } 19 | 20 | func (msg *MsgSubmitBlockHeaderRequest) Route() string { 21 | return RouterKey 22 | } 23 | 24 | func (msg *MsgSubmitBlockHeaderRequest) Type() string { 25 | return TypeMsgSubmitBlockHeader 26 | } 27 | 28 | func (msg *MsgSubmitBlockHeaderRequest) GetSigners() []sdk.AccAddress { 29 | Sender, err := sdk.AccAddressFromBech32(msg.Sender) 30 | if err != nil { 31 | panic(err) 32 | } 33 | return []sdk.AccAddress{Sender} 34 | } 35 | 36 | func (msg *MsgSubmitBlockHeaderRequest) GetSignBytes() []byte { 37 | bz := ModuleCdc.MustMarshalJSON(msg) 38 | return sdk.MustSortJSON(bz) 39 | } 40 | 41 | func (msg *MsgSubmitBlockHeaderRequest) ValidateBasic() error { 42 | _, err := sdk.AccAddressFromBech32(msg.Sender) 43 | if err != nil { 44 | return sdkerrors.Wrapf(err, "invalid Sender address (%s)", err) 45 | } 46 | 47 | if len(msg.BlockHeaders) == 0 { 48 | return sdkerrors.Wrap(ErrInvalidHeader, "block headers cannot be empty") 49 | } 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /x/btcbridge/types/expected_keepers.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | "github.com/cosmos/cosmos-sdk/x/auth/types" 6 | banktype "github.com/cosmos/cosmos-sdk/x/bank/types" 7 | ) 8 | 9 | // AccountKeeper defines the expected account keeper used for simulations (noalias) 10 | type AccountKeeper interface { 11 | GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI 12 | // Methods imported from account should be defined here 13 | } 14 | 15 | // BankKeeper defines the expected interface needed to retrieve account balances. 16 | type BankKeeper interface { 17 | SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins 18 | // Methods imported from bank should be defined here 19 | 20 | SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error 21 | 22 | SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error 23 | SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error 24 | SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error 25 | SetDenomMetaData(ctx sdk.Context, denomMetaData banktype.Metadata) 26 | 27 | MintCoins(ctx sdk.Context, moduleName string, amounts sdk.Coins) error 28 | BurnCoins(ctx sdk.Context, moduleName string, amounts sdk.Coins) error 29 | 30 | HasSupply(ctx sdk.Context, denom string) bool 31 | GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin 32 | } 33 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | build: 3 | binary: sided 4 | proto: 5 | path: proto 6 | third_party_paths: 7 | - third_party/proto 8 | - proto_vendor 9 | accounts: 10 | - name: alice 11 | coins: 12 | - 10000000000000000000000000000000uside 13 | - 10000000000000000000000000000000uusdc 14 | - 10000000000000000000000000000000uusdt 15 | 16 | - name: bob 17 | mnemonic: "furnace wild gravity resist heavy beef bundle deliver design service cycle monkey" 18 | coins: 19 | - 10000000000000000000000000000000uside 20 | - 10000000000000000000000000000000uusdc 21 | - 10000000000000000000000000000000uusdt 22 | # faucet: 23 | # name: bob 24 | # coins: 25 | # - 10uside 26 | # host: 0.0.0.0:4500 27 | genesis: 28 | app_state: 29 | crisis: 30 | constant_fee: 31 | denom: uside 32 | gov: 33 | deposit_params: 34 | min_deposit: 35 | - amount: "10000000" 36 | denom: uside 37 | params: 38 | min_deposit: 39 | - amount: "10000000" 40 | denom: uside 41 | voting_period: "60s" 42 | mint: 43 | params: 44 | mint_denom: uside 45 | staking: 46 | params: 47 | bond_denom: uside 48 | chain_id: grimoria-testnet-1 49 | consensus_params: 50 | block: 51 | max_gas: "100000000" 52 | validators: 53 | - name: alice 54 | bonded: 10000000000000000000000uside 55 | home: $HOME/.side 56 | client: 57 | openapi: 58 | path: "docs/static/openapi.yml" 59 | prefix: 60 | address: "side" 61 | -------------------------------------------------------------------------------- /x/btcbridge/types/message_submit_signature.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdkerrors "cosmossdk.io/errors" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | ) 7 | 8 | const TypeMsgSubmitSignature = "submit_signature" 9 | 10 | func NewMsgSubmitWithdrawSignaturesRequest( 11 | sender string, 12 | txid string, 13 | pbst string, 14 | ) *MsgSubmitWithdrawSignaturesRequest { 15 | return &MsgSubmitWithdrawSignaturesRequest{ 16 | Sender: sender, 17 | Txid: txid, 18 | Psbt: pbst, 19 | } 20 | } 21 | 22 | func (msg *MsgSubmitWithdrawSignaturesRequest) Route() string { 23 | return RouterKey 24 | } 25 | 26 | func (msg *MsgSubmitWithdrawSignaturesRequest) Type() string { 27 | return TypeMsgSubmitSignature 28 | } 29 | 30 | func (msg *MsgSubmitWithdrawSignaturesRequest) GetSigners() []sdk.AccAddress { 31 | Sender, err := sdk.AccAddressFromBech32(msg.Sender) 32 | if err != nil { 33 | panic(err) 34 | } 35 | return []sdk.AccAddress{Sender} 36 | } 37 | 38 | func (msg *MsgSubmitWithdrawSignaturesRequest) GetSignBytes() []byte { 39 | bz := ModuleCdc.MustMarshalJSON(msg) 40 | return sdk.MustSortJSON(bz) 41 | } 42 | 43 | func (msg *MsgSubmitWithdrawSignaturesRequest) ValidateBasic() error { 44 | _, err := sdk.AccAddressFromBech32(msg.Sender) 45 | if err != nil { 46 | return sdkerrors.Wrapf(err, "invalid Sender address (%s)", err) 47 | } 48 | 49 | if len(msg.Txid) == 0 { 50 | return sdkerrors.Wrap(ErrInvalidSignatures, "txid cannot be empty") 51 | } 52 | 53 | if len(msg.Psbt) == 0 { 54 | return sdkerrors.Wrap(ErrInvalidSignatures, "sigatures cannot be empty") 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /x/btcbridge/types/message_submit_withdraw_status.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdkerrors "cosmossdk.io/errors" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | ) 7 | 8 | const TypeMsgSubmitWithdrawStatus = "submit_withdraw_status" 9 | 10 | func NewMsgSubmitWithdrawStatusRequest( 11 | sender string, 12 | txid string, 13 | status SigningStatus, 14 | ) *MsgSubmitWithdrawStatusRequest { 15 | return &MsgSubmitWithdrawStatusRequest{ 16 | Sender: sender, 17 | Txid: txid, 18 | Status: status, 19 | } 20 | } 21 | 22 | func (msg *MsgSubmitWithdrawStatusRequest) Route() string { 23 | return RouterKey 24 | } 25 | 26 | func (msg *MsgSubmitWithdrawStatusRequest) Type() string { 27 | return TypeMsgSubmitWithdrawStatus 28 | } 29 | 30 | func (msg *MsgSubmitWithdrawStatusRequest) GetSigners() []sdk.AccAddress { 31 | Sender, err := sdk.AccAddressFromBech32(msg.Sender) 32 | if err != nil { 33 | panic(err) 34 | } 35 | return []sdk.AccAddress{Sender} 36 | } 37 | 38 | func (msg *MsgSubmitWithdrawStatusRequest) GetSignBytes() []byte { 39 | bz := ModuleCdc.MustMarshalJSON(msg) 40 | return sdk.MustSortJSON(bz) 41 | } 42 | 43 | func (msg *MsgSubmitWithdrawStatusRequest) ValidateBasic() error { 44 | _, err := sdk.AccAddressFromBech32(msg.Sender) 45 | if err != nil { 46 | return sdkerrors.Wrapf(err, "invalid Sender address (%s)", err) 47 | } 48 | 49 | if len(msg.Txid) == 0 { 50 | return sdkerrors.Wrap(ErrSigningRequestNotExist, "txid cannot be empty") 51 | } 52 | 53 | if msg.Status != SigningStatus_SIGNING_STATUS_BROADCASTED { 54 | return sdkerrors.Wrap(ErrInvalidStatus, "invalid status") 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /x/btcbridge/types/message_update_senders.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdkerrors "cosmossdk.io/errors" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | ) 7 | 8 | const TypeMsgUpdateSenders = "update_qualifed_relayers" 9 | 10 | func NewMsgUpdateSendersRequest( 11 | sender string, 12 | relayers []string, 13 | ) *MsgUpdateQualifiedRelayersRequest { 14 | return &MsgUpdateQualifiedRelayersRequest{ 15 | Sender: sender, 16 | Relayers: relayers, 17 | } 18 | } 19 | 20 | func (msg *MsgUpdateQualifiedRelayersRequest) Route() string { 21 | return RouterKey 22 | } 23 | 24 | func (msg *MsgUpdateQualifiedRelayersRequest) Type() string { 25 | return TypeMsgUpdateSenders 26 | } 27 | 28 | func (msg *MsgUpdateQualifiedRelayersRequest) GetSigners() []sdk.AccAddress { 29 | Sender, err := sdk.AccAddressFromBech32(msg.Sender) 30 | if err != nil { 31 | panic(err) 32 | } 33 | return []sdk.AccAddress{Sender} 34 | } 35 | 36 | func (msg *MsgUpdateQualifiedRelayersRequest) GetSignBytes() []byte { 37 | bz := ModuleCdc.MustMarshalJSON(msg) 38 | return sdk.MustSortJSON(bz) 39 | } 40 | 41 | func (msg *MsgUpdateQualifiedRelayersRequest) ValidateBasic() error { 42 | _, err := sdk.AccAddressFromBech32(msg.Sender) 43 | if err != nil { 44 | return sdkerrors.Wrapf(err, "invalid sender address (%s)", err) 45 | } 46 | 47 | if len(msg.Relayers) == 0 { 48 | return sdkerrors.Wrap(ErrInvalidSenders, "relayers cannot be empty") 49 | } 50 | 51 | for _, sender := range msg.Relayers { 52 | _, err := sdk.AccAddressFromBech32(sender) 53 | if err != nil { 54 | return sdkerrors.Wrapf(ErrInvalidSenders, "address (%s) is invalid", sender) 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /testutil/nullify/nullify.go: -------------------------------------------------------------------------------- 1 | // Package nullify provides methods to init nil values structs for test assertion. 2 | package nullify 3 | 4 | import ( 5 | "reflect" 6 | "unsafe" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | ) 10 | 11 | var ( 12 | coinType = reflect.TypeOf(sdk.Coin{}) 13 | coinsType = reflect.TypeOf(sdk.Coins{}) 14 | ) 15 | 16 | // Fill analyze all struct fields and slices with 17 | // reflection and initialize the nil and empty slices, 18 | // structs, and pointers. 19 | func Fill(x interface{}) interface{} { 20 | v := reflect.Indirect(reflect.ValueOf(x)) 21 | switch v.Kind() { 22 | case reflect.Slice: 23 | for i := 0; i < v.Len(); i++ { 24 | obj := v.Index(i) 25 | objPt := reflect.NewAt(obj.Type(), unsafe.Pointer(obj.UnsafeAddr())).Interface() 26 | objPt = Fill(objPt) 27 | obj.Set(reflect.ValueOf(objPt)) 28 | } 29 | case reflect.Struct: 30 | for i := 0; i < v.NumField(); i++ { 31 | f := reflect.Indirect(v.Field(i)) 32 | if !f.CanSet() { 33 | continue 34 | } 35 | switch f.Kind() { 36 | case reflect.Slice: 37 | f.Set(reflect.MakeSlice(f.Type(), 0, 0)) 38 | case reflect.Struct: 39 | switch f.Type() { 40 | case coinType: 41 | coin := reflect.New(coinType).Interface() 42 | s := reflect.ValueOf(coin).Elem() 43 | f.Set(s) 44 | case coinsType: 45 | coins := reflect.New(coinsType).Interface() 46 | s := reflect.ValueOf(coins).Elem() 47 | f.Set(s) 48 | default: 49 | objPt := reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Interface() 50 | s := Fill(objPt) 51 | f.Set(reflect.ValueOf(s)) 52 | } 53 | } 54 | } 55 | } 56 | return reflect.Indirect(v).Interface() 57 | } 58 | -------------------------------------------------------------------------------- /x/btcbridge/types/message_withdraw_bitcoin.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strconv" 5 | 6 | sdkerrors "cosmossdk.io/errors" 7 | sdk "github.com/cosmos/cosmos-sdk/types" 8 | ) 9 | 10 | const TypeMsgWithdrawBitcoin = "withdraw_bitcoin" 11 | 12 | func NewMsgWithdrawBitcoinRequest( 13 | sender string, 14 | amount string, 15 | feeRate string, 16 | ) *MsgWithdrawBitcoinRequest { 17 | return &MsgWithdrawBitcoinRequest{ 18 | Sender: sender, 19 | Amount: amount, 20 | FeeRate: feeRate, 21 | } 22 | } 23 | 24 | func (msg *MsgWithdrawBitcoinRequest) Route() string { 25 | return RouterKey 26 | } 27 | 28 | func (msg *MsgWithdrawBitcoinRequest) Type() string { 29 | return TypeMsgWithdrawBitcoin 30 | } 31 | 32 | func (msg *MsgWithdrawBitcoinRequest) GetSigners() []sdk.AccAddress { 33 | Sender, err := sdk.AccAddressFromBech32(msg.Sender) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return []sdk.AccAddress{Sender} 38 | } 39 | 40 | func (msg *MsgWithdrawBitcoinRequest) GetSignBytes() []byte { 41 | bz := ModuleCdc.MustMarshalJSON(msg) 42 | return sdk.MustSortJSON(bz) 43 | } 44 | 45 | func (msg *MsgWithdrawBitcoinRequest) ValidateBasic() error { 46 | _, err := sdk.AccAddressFromBech32(msg.Sender) 47 | if err != nil { 48 | return sdkerrors.Wrapf(err, "invalid Sender address (%s)", err) 49 | } 50 | 51 | _, err = sdk.ParseCoinNormalized(msg.Amount) 52 | if err != nil { 53 | return sdkerrors.Wrapf(ErrInvalidAmount, "invalid amount %s", msg.Amount) 54 | } 55 | 56 | feeRate, err := strconv.ParseInt(msg.FeeRate, 10, 64) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if feeRate <= 0 { 62 | return sdkerrors.Wrap(ErrInvalidFeeRate, "fee rate must be greater than zero") 63 | } 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /testutil/keeper/btc_bridge.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "testing" 5 | 6 | tmdb "github.com/cometbft/cometbft-db" 7 | "github.com/cometbft/cometbft/libs/log" 8 | tmproto "github.com/cometbft/cometbft/proto/tendermint/types" 9 | "github.com/cosmos/cosmos-sdk/codec" 10 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 11 | "github.com/cosmos/cosmos-sdk/store" 12 | storetypes "github.com/cosmos/cosmos-sdk/store/types" 13 | sdk "github.com/cosmos/cosmos-sdk/types" 14 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 15 | govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 16 | "github.com/sideprotocol/side/app" 17 | "github.com/sideprotocol/side/x/btcbridge/keeper" 18 | "github.com/sideprotocol/side/x/btcbridge/types" 19 | "github.com/stretchr/testify/require" 20 | ) 21 | 22 | func BtcBridgeKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { 23 | app := app.InitSideTestApp(false) 24 | 25 | storeKey := sdk.NewKVStoreKey(types.StoreKey) 26 | memStoreKey := storetypes.NewMemoryStoreKey(types.MemStoreKey) 27 | 28 | db := tmdb.NewMemDB() 29 | stateStore := store.NewCommitMultiStore(db) 30 | stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeIAVL, db) 31 | stateStore.MountStoreWithDB(memStoreKey, storetypes.StoreTypeMemory, nil) 32 | require.NoError(t, stateStore.LoadLatestVersion()) 33 | 34 | registry := codectypes.NewInterfaceRegistry() 35 | cdc := codec.NewProtoCodec(registry) 36 | 37 | authority := authtypes.NewModuleAddress(govtypes.ModuleName).String() 38 | 39 | k := keeper.NewKeeper( 40 | cdc, 41 | storeKey, 42 | memStoreKey, 43 | authority, 44 | app.BankKeeper, 45 | ) 46 | 47 | ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) 48 | 49 | // Initialize params 50 | k.SetParams(ctx, types.DefaultParams()) 51 | 52 | return k, ctx 53 | } 54 | -------------------------------------------------------------------------------- /x/btcbridge/types/message_submit_withdraw_transaction.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdkerrors "cosmossdk.io/errors" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | ) 7 | 8 | const TypeMsgSubmitWithdrawTransaction = "submit_withdraw_transaction" 9 | 10 | func NewMsgSubmitWithdrawTransactionRequest( 11 | sender string, 12 | blockhash string, 13 | transaction string, 14 | proof []string, 15 | ) *MsgSubmitWithdrawTransactionRequest { 16 | return &MsgSubmitWithdrawTransactionRequest{ 17 | Sender: sender, 18 | Blockhash: blockhash, 19 | TxBytes: transaction, 20 | Proof: proof, 21 | } 22 | } 23 | 24 | func (msg *MsgSubmitWithdrawTransactionRequest) Route() string { 25 | return RouterKey 26 | } 27 | 28 | func (msg *MsgSubmitWithdrawTransactionRequest) Type() string { 29 | return TypeMsgSubmitDepositTransaction 30 | } 31 | 32 | func (msg *MsgSubmitWithdrawTransactionRequest) GetSigners() []sdk.AccAddress { 33 | Sender, err := sdk.AccAddressFromBech32(msg.Sender) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return []sdk.AccAddress{Sender} 38 | } 39 | 40 | func (msg *MsgSubmitWithdrawTransactionRequest) GetSignBytes() []byte { 41 | bz := ModuleCdc.MustMarshalJSON(msg) 42 | return sdk.MustSortJSON(bz) 43 | } 44 | 45 | func (msg *MsgSubmitWithdrawTransactionRequest) ValidateBasic() error { 46 | _, err := sdk.AccAddressFromBech32(msg.Sender) 47 | if err != nil { 48 | return sdkerrors.Wrapf(err, "invalid Sender address (%s)", err) 49 | } 50 | 51 | if len(msg.Blockhash) == 0 { 52 | return sdkerrors.Wrap(ErrInvalidBtcTransaction, "blockhash cannot be empty") 53 | } 54 | 55 | if len(msg.TxBytes) == 0 { 56 | return sdkerrors.Wrap(ErrInvalidBtcTransaction, "transaction cannot be empty") 57 | } 58 | 59 | if len(msg.Proof) == 0 { 60 | return sdkerrors.Wrap(ErrInvalidBtcTransaction, "proof cannot be empty") 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /x/btcbridge/types/codec.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/codec" 5 | cdctypes "github.com/cosmos/cosmos-sdk/codec/types" 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | "github.com/cosmos/cosmos-sdk/types/msgservice" 8 | ) 9 | 10 | func RegisterCodec(cdc *codec.LegacyAmino) { 11 | cdc.RegisterConcrete(&MsgSubmitBlockHeaderRequest{}, "btcbridge/MsgSubmitBlockHeaderRequest", nil) 12 | cdc.RegisterConcrete(&MsgUpdateQualifiedRelayersRequest{}, "btcbridge/MsgUpdateQualifiedRelayersRequest", nil) 13 | cdc.RegisterConcrete(&MsgSubmitDepositTransactionRequest{}, "btcbridge/MsgSubmitDepositTransactionRequest", nil) 14 | cdc.RegisterConcrete(&MsgSubmitWithdrawSignaturesRequest{}, "btcbridge/MsgSubmitWithdrawSignaturesRequest", nil) 15 | cdc.RegisterConcrete(&MsgSubmitWithdrawTransactionRequest{}, "btcbridge/MsgSubmitWithdrawTransactionRequest", nil) 16 | cdc.RegisterConcrete(&MsgUpdateParamsRequest{}, "btcbridge/MsgUpdateParamsRequest", nil) 17 | // this line is used by starport scaffolding # 2 18 | } 19 | 20 | func RegisterInterfaces(registry cdctypes.InterfaceRegistry) { 21 | registry.RegisterImplementations((*sdk.Msg)(nil), &MsgSubmitBlockHeaderRequest{}) 22 | registry.RegisterImplementations((*sdk.Msg)(nil), &MsgUpdateQualifiedRelayersRequest{}) 23 | registry.RegisterImplementations((*sdk.Msg)(nil), &MsgSubmitDepositTransactionRequest{}) 24 | registry.RegisterImplementations((*sdk.Msg)(nil), &MsgSubmitWithdrawSignaturesRequest{}) 25 | registry.RegisterImplementations((*sdk.Msg)(nil), &MsgSubmitWithdrawTransactionRequest{}) 26 | registry.RegisterImplementations((*sdk.Msg)(nil), &MsgUpdateParamsRequest{}) 27 | // this line is used by starport scaffolding # 3 28 | 29 | msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) 30 | } 31 | 32 | var ( 33 | Amino = codec.NewLegacyAmino() 34 | ModuleCdc = codec.NewProtoCodec(cdctypes.NewInterfaceRegistry()) 35 | ) 36 | -------------------------------------------------------------------------------- /create_validator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | KEYS=("dev0" "dev1") 4 | CHAINID="grimoria-testnet-1" 5 | MONIKER="Side Labs" 6 | BINARY="$HOME/go/bin/sided" 7 | 8 | # Remember to change to other types of keyring like 'file' in-case exposing to outside world, 9 | # otherwise your balance will be wiped quickly 10 | # The keyring test does not require private key to steal tokens from you 11 | KEYRING="test" 12 | #KEYALGO="secp256k1" 13 | KEYALGO="segwit" 14 | LOGLEVEL="info" 15 | # Set dedicated home directory for the $BINARY instance 16 | HOMEDIR="$HOME/.side2" 17 | 18 | # Path variables 19 | CONFIG=$HOMEDIR/config/config.toml 20 | APP_TOML=$HOMEDIR/config/app.toml 21 | GENESIS=$HOMEDIR/config/genesis.json 22 | TMP_GENESIS=$HOMEDIR/config/tmp_genesis.json 23 | PEER_ID="" 24 | 25 | # validate dependencies are installed 26 | command -v jq >/dev/null 2>&1 || { 27 | echo >&2 "jq not installed. More info: https://stedolan.github.io/jq/download/" 28 | exit 1 29 | } 30 | 31 | # used to exit on first error (any non-zero exit code) 32 | set -e 33 | 34 | # Set client config 35 | $BINARY config keyring-backend $KEYRING --home "$HOMEDIR" 36 | $BINARY config chain-id $CHAINID --home "$HOMEDIR" 37 | 38 | # Start the node (remove the --pruning=nothing flag if historical queries are not needed) 39 | $BINARY tx staking create-validator -y --moniker segwit --pubkey=$($BINARY tendermint show-validator --home $HOMEDIR) --amount="10000000uside" --commission-rate 0.1 --commission-max-rate 0.1 --commission-max-change-rate 0.1 --min-self-delegation 1 --chain-id $CHAINID --fees 2000uside --home "$HOMEDIR" --from segwit_key 40 | sleep 6 41 | $BINARY tx staking create-validator -y --moniker secp256 --pubkey=$($BINARY tendermint show-validator --home $HOMEDIR) --amount="15000000uside" --commission-rate 0.1 --commission-max-rate 0.1 --commission-max-change-rate 0.1 --min-self-delegation 1 --chain-id $CHAINID --fees 2000uside --home "$HOMEDIR" --from secp256k1_key 42 | -------------------------------------------------------------------------------- /x/btcbridge/types/message_submit_deposit_transaction.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdkerrors "cosmossdk.io/errors" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | ) 7 | 8 | const TypeMsgSubmitDepositTransaction = "submit_deposit_transaction" 9 | 10 | func NewMsgSubmitTransactionRequest( 11 | sender string, 12 | blockhash string, 13 | transaction string, 14 | proof []string, 15 | ) *MsgSubmitDepositTransactionRequest { 16 | return &MsgSubmitDepositTransactionRequest{ 17 | Sender: sender, 18 | Blockhash: blockhash, 19 | TxBytes: transaction, 20 | Proof: proof, 21 | } 22 | } 23 | 24 | func (msg *MsgSubmitDepositTransactionRequest) Route() string { 25 | return RouterKey 26 | } 27 | 28 | func (msg *MsgSubmitDepositTransactionRequest) Type() string { 29 | return TypeMsgSubmitDepositTransaction 30 | } 31 | 32 | func (msg *MsgSubmitDepositTransactionRequest) GetSigners() []sdk.AccAddress { 33 | Sender, err := sdk.AccAddressFromBech32(msg.Sender) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return []sdk.AccAddress{Sender} 38 | } 39 | 40 | func (msg *MsgSubmitDepositTransactionRequest) GetSignBytes() []byte { 41 | bz := ModuleCdc.MustMarshalJSON(msg) 42 | return sdk.MustSortJSON(bz) 43 | } 44 | 45 | func (msg *MsgSubmitDepositTransactionRequest) ValidateBasic() error { 46 | _, err := sdk.AccAddressFromBech32(msg.Sender) 47 | if err != nil { 48 | return sdkerrors.Wrapf(err, "invalid Sender address (%s)", err) 49 | } 50 | 51 | if len(msg.Blockhash) == 0 { 52 | return sdkerrors.Wrap(ErrInvalidBtcTransaction, "blockhash cannot be empty") 53 | } 54 | 55 | if len(msg.PrevTxBytes) == 0 { 56 | return sdkerrors.Wrap(ErrInvalidBtcTransaction, "transaction cannot be empty") 57 | } 58 | 59 | if len(msg.TxBytes) == 0 { 60 | return sdkerrors.Wrap(ErrInvalidBtcTransaction, "transaction cannot be empty") 61 | } 62 | 63 | if len(msg.Proof) == 0 { 64 | return sdkerrors.Wrap(ErrInvalidBtcTransaction, "proof cannot be empty") 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sidechain 2 | **sidechain** is a blockchain built using Cosmos SDK and Tendermint and created with [Ignite CLI](https://ignite.com/cli). 3 | 4 | ## Get started 5 | 6 | ``` 7 | ignite chain serve 8 | ``` 9 | `serve` command installs dependencies, builds, initializes, and starts your blockchain in development. 10 | 11 | ### Requirements 12 | 13 | - `ignite version: 0.27.1` 14 | - `go version >= 1.20.x` 15 | 16 | ### Configure 17 | 18 | Your blockchain in development can be configured with `config.yml`. To learn more, see the [Ignite CLI docs](https://docs.ignite.com). 19 | 20 | ### Web Frontend 21 | 22 | Ignite CLI has scaffolded a Vue.js-based web app in the `vue` directory. Run the following commands to install dependencies and start the app: 23 | 24 | ``` 25 | cd vue 26 | npm install 27 | npm run serve 28 | ``` 29 | 30 | The frontend app is built using the `@starport/vue` and `@starport/vuex` packages. For details, see the [monorepo for Ignite front-end development](https://github.com/ignite/web). 31 | 32 | ## Release 33 | To release a new version of your blockchain, create and push a new tag with `v` prefix. A new draft release with the configured targets will be created. 34 | 35 | ``` 36 | git tag v0.1 37 | git push origin v0.1 38 | ``` 39 | 40 | After a draft release is created, make your final changes from the release page and publish it. 41 | 42 | ### Install 43 | To install the latest version of your blockchain node's binary, execute the following command on your machine: 44 | 45 | ``` 46 | curl https://get.ignite.com/username/sidechain@latest! | sudo bash 47 | ``` 48 | `username/sidechain` should match the `username` and `repo_name` of the Github repository to which the source code was pushed. Learn more about [the install process](https://github.com/allinbits/starport-installer). 49 | 50 | ## Learn more 51 | 52 | - [Ignite CLI](https://ignite.com/cli) 53 | - [Tutorials](https://docs.ignite.com/guide) 54 | - [Ignite CLI docs](https://docs.ignite.com) 55 | - [Cosmos SDK docs](https://docs.cosmos.network) 56 | - [Developer Chat](https://discord.gg/ignite) 57 | -------------------------------------------------------------------------------- /x/btcbridge/types/runes_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/btcsuite/btcd/wire" 10 | 11 | "github.com/sideprotocol/side/x/btcbridge/types" 12 | ) 13 | 14 | func TestParseRunes(t *testing.T) { 15 | testCases := []struct { 16 | name string 17 | pkScriptHex string 18 | edicts []*types.Edict 19 | expectPass bool 20 | }{ 21 | { 22 | name: "valid runes edict", 23 | pkScriptHex: "6a5d0b00c0a2330380cab5ee0101", 24 | edicts: []*types.Edict{ 25 | { 26 | Id: &types.RuneId{Block: 840000, Tx: 3}, 27 | Amount: "500000000", 28 | Output: 1, 29 | }, 30 | }, 31 | expectPass: true, 32 | }, 33 | { 34 | name: "output index is out of range", 35 | pkScriptHex: "6a5d0b00c0a2330380cab5ee0102", 36 | expectPass: false, 37 | }, 38 | { 39 | name: "no OP_RETURN", 40 | pkScriptHex: "615d0b00c0a2330380cab5ee0102", 41 | expectPass: true, 42 | edicts: nil, 43 | }, 44 | { 45 | name: "no runes magic number", 46 | pkScriptHex: "6a5c0b00c0a2330380cab5ee0102", 47 | expectPass: true, 48 | edicts: nil, 49 | }, 50 | { 51 | name: "non data push op", 52 | pkScriptHex: "6a5d4f00c0a2330380cab5ee0102", 53 | expectPass: false, 54 | }, 55 | { 56 | name: "no tag body for edicts", 57 | pkScriptHex: "6a5d0b01c0a2330380cab5ee0102", 58 | expectPass: false, 59 | }, 60 | { 61 | name: "invalid edict", 62 | pkScriptHex: "6a5d0b00c0a2330380cab5ee01", 63 | expectPass: false, 64 | }, 65 | } 66 | 67 | for _, tc := range testCases { 68 | t.Run(tc.name, func(t *testing.T) { 69 | pkScript, err := hex.DecodeString(tc.pkScriptHex) 70 | require.NoError(t, err) 71 | 72 | tx := wire.NewMsgTx(types.TxVersion) 73 | tx.AddTxOut(wire.NewTxOut(0, pkScript)) 74 | 75 | edicts, err := types.ParseRunes(tx) 76 | if tc.expectPass { 77 | require.NoError(t, err) 78 | require.EqualValues(t, tc.edicts, edicts) 79 | } else { 80 | require.NotNil(t, err) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /x/btcbridge/types/varint.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "errors" 5 | "math/big" 6 | 7 | "lukechampine.com/uint128" 8 | ) 9 | 10 | func EncodeUint32(n uint32) []byte { 11 | var result []byte 12 | 13 | for n >= 128 { 14 | result = append(result, byte(n&0x7F|0x80)) 15 | n >>= 7 16 | } 17 | 18 | result = append(result, byte(n)) 19 | return result 20 | } 21 | 22 | func EncodeUint64(n uint64) []byte { 23 | var result []byte 24 | 25 | for n >= 128 { 26 | result = append(result, byte(n&0x7F|0x80)) 27 | n >>= 7 28 | } 29 | 30 | result = append(result, byte(n)) 31 | return result 32 | } 33 | 34 | func EncodeUint128(n *uint128.Uint128) []byte { 35 | return EncodeBigInt(n.Big()) 36 | } 37 | 38 | func EncodeBigInt(n *big.Int) []byte { 39 | var result []byte 40 | 41 | for n.Cmp(big.NewInt(128)) >= 0 { 42 | temp := new(big.Int).Set(n) 43 | last := temp.And(n, new(big.Int).SetUint64(0b0111_1111)) 44 | result = append(result, last.Or(last, new(big.Int).SetUint64(0b1000_0000)).Bytes()[0]) 45 | n.Rsh(n, 7) 46 | } 47 | 48 | if len(n.Bytes()) == 0 { 49 | result = append(result, 0) 50 | } else { 51 | result = append(result, n.Bytes()...) 52 | } 53 | 54 | return result 55 | } 56 | 57 | func Decode(bz []byte) (uint128.Uint128, int, error) { 58 | n := big.NewInt(0) 59 | 60 | for i, b := range bz { 61 | if i > 18 { 62 | return uint128.Zero, 0, errors.New("varint overflow") 63 | } 64 | 65 | value := uint64(b) & 0b0111_1111 66 | if i == 18 && value&0b0111_1100 != 0 { 67 | return uint128.Zero, 0, errors.New("varint too large") 68 | } 69 | 70 | temp := new(big.Int).SetUint64(value) 71 | n.Or(n, temp.Lsh(temp, uint(7*i))) 72 | 73 | if b&0b1000_0000 == 0 { 74 | return uint128.FromBig(n), i + 1, nil 75 | } 76 | } 77 | 78 | return uint128.Zero, 0, errors.New("varint too short") 79 | } 80 | 81 | func DecodeVec(payload []byte) ([]uint128.Uint128, error) { 82 | vec := make([]uint128.Uint128, 0) 83 | i := 0 84 | 85 | for i < len(payload) { 86 | value, length, err := Decode(payload[i:]) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | vec = append(vec, value) 92 | i += length 93 | } 94 | 95 | return vec, nil 96 | } 97 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow is useful if you want to automate the process of: 2 | # 3 | # a) Creating a new prelease when you push a new tag with a "v" prefix (version). 4 | # 5 | # This type of prerelease is meant to be used for production: alpha, beta, rc, etc. types of releases. 6 | # After the prerelease is created, you need to make your changes on the release page at the relevant 7 | # Github page and publish your release. 8 | # 9 | # b) Creating/updating the "latest" prerelease when you push to your default branch. 10 | # 11 | # This type of prelease is useful to make your bleeding-edge binaries available to advanced users. 12 | # 13 | # The workflow will not run if there is no tag pushed with a "v" prefix and no change pushed to your 14 | # default branch. 15 | on: push 16 | 17 | jobs: 18 | might_release: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Prepare Release Variables 27 | id: vars 28 | uses: ignite/cli/actions/release/vars@main 29 | 30 | - name: Issue Release Assets 31 | uses: ignite/cli/actions/cli@main 32 | if: ${{ steps.vars.outputs.should_release == 'true' }} 33 | with: 34 | args: chain build --release --release.prefix ${{ steps.vars.outputs.tarball_prefix }} -t linux:amd64 -t darwin:amd64 -t darwin:arm64 35 | 36 | - name: Delete the "latest" Release 37 | uses: dev-drprasad/delete-tag-and-release@v0.2.1 38 | if: ${{ steps.vars.outputs.is_release_type_latest == 'true' }} 39 | with: 40 | tag_name: ${{ steps.vars.outputs.tag_name }} 41 | delete_release: true 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Publish the Release 46 | uses: softprops/action-gh-release@v1 47 | if: ${{ steps.vars.outputs.should_release == 'true' }} 48 | with: 49 | tag_name: ${{ steps.vars.outputs.tag_name }} 50 | files: release/* 51 | prerelease: true 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | -------------------------------------------------------------------------------- /proto/side/btcbridge/bitcoin.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package side.btcbridge; 3 | 4 | import "gogoproto/gogo.proto"; 5 | 6 | option go_package = "github.com/sideprotocol/side/x/btcbridge/types"; 7 | 8 | // Bitcoin Block Header 9 | message BlockHeader { 10 | uint64 version = 1; 11 | string hash = 2; 12 | uint64 height = 3; 13 | string previous_block_hash = 4; 14 | string merkle_root = 5; 15 | uint64 nonce = 6; 16 | string bits = 7; 17 | uint64 time = 8; 18 | uint64 ntx = 9; 19 | } 20 | 21 | // Bitcoin Signing Status 22 | enum SigningStatus { 23 | // SIGNING_STATUS_UNSPECIFIED - Default value, should not be used 24 | SIGNING_STATUS_UNSPECIFIED = 0; 25 | // SIGNING_STATUS_CREATED - The signing request is created 26 | SIGNING_STATUS_CREATED = 1; 27 | // SIGNING_STATUS_SIGNED - The signing request is signed 28 | SIGNING_STATUS_SIGNED = 2; 29 | // SIGNING_STATUS_BROADCASTED - The signing request is broadcasted 30 | SIGNING_STATUS_BROADCASTED = 3; 31 | // SIGNING_STATUS_CONFIRMED - The signing request is confirmed 32 | SIGNING_STATUS_CONFIRMED = 4; 33 | // SIGNING_STATUS_REJECTED - The signing request is rejected 34 | SIGNING_STATUS_REJECTED = 5; 35 | } 36 | 37 | // Bitcoin Signing Request 38 | message BitcoinSigningRequest { 39 | string address = 1; 40 | string txid = 2; 41 | string psbt = 3; 42 | SigningStatus status = 4; 43 | uint64 sequence = 5; 44 | // The vault address that the request is associated with 45 | string vault_address = 6; 46 | } 47 | 48 | // Bitcoin UTXO 49 | message UTXO { 50 | string txid = 1; 51 | uint64 vout = 2; 52 | string address = 3; 53 | uint64 amount = 4; 54 | // height is used for calculating confirmations 55 | uint64 height = 5; 56 | bytes pub_key_script = 6; 57 | bool is_coinbase = 7; 58 | bool is_locked = 8; 59 | // rune balances associated with the UTXO 60 | repeated RuneBalance runes = 9; 61 | } 62 | 63 | // Rune Balance 64 | message RuneBalance { 65 | // serialized rune id 66 | string id = 1; 67 | // rune amount 68 | string amount = 2; 69 | } 70 | 71 | // Rune ID 72 | message RuneId { 73 | // block height 74 | uint64 block = 1; 75 | // tx index 76 | uint32 tx = 2; 77 | } 78 | 79 | // Rune Edict 80 | message Edict { 81 | RuneId id = 1; 82 | string amount = 2; 83 | uint32 output = 3; 84 | } 85 | -------------------------------------------------------------------------------- /x/btcbridge/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // DONTCOVER 4 | 5 | import ( 6 | errorsmod "cosmossdk.io/errors" 7 | ) 8 | 9 | // x/btcbridge module sentinel errors 10 | var ( 11 | ErrSenderAddressNotAuthorized = errorsmod.Register(ModuleName, 1000, "sender address not authorized") 12 | ErrInvalidHeader = errorsmod.Register(ModuleName, 1100, "invalid block header") 13 | ErrReorgFailed = errorsmod.Register(ModuleName, 1101, "failed to reorg chain") 14 | ErrForkedBlockHeader = errorsmod.Register(ModuleName, 1102, "Invalid forked block header") 15 | 16 | ErrInvalidSenders = errorsmod.Register(ModuleName, 2100, "invalid allowed senders") 17 | 18 | ErrInvalidBtcTransaction = errorsmod.Register(ModuleName, 3100, "invalid bitcoin transaction") 19 | ErrBlockNotFound = errorsmod.Register(ModuleName, 3101, "block not found") 20 | ErrTransactionNotIncluded = errorsmod.Register(ModuleName, 3102, "transaction not included in block") 21 | ErrNotConfirmed = errorsmod.Register(ModuleName, 3200, "transaction not confirmed") 22 | ErrExceedMaxAcceptanceDepth = errorsmod.Register(ModuleName, 3201, "exceed max acceptance block depth") 23 | ErrUnsupportedScriptType = errorsmod.Register(ModuleName, 3202, "unsupported script type") 24 | ErrTransactionAlreadyMinted = errorsmod.Register(ModuleName, 3203, "transaction already minted") 25 | ErrInvalidDepositTransaction = errorsmod.Register(ModuleName, 3204, "invalid deposit transaction") 26 | 27 | ErrInvalidSignatures = errorsmod.Register(ModuleName, 4200, "invalid signatures") 28 | ErrInsufficientBalance = errorsmod.Register(ModuleName, 4201, "insufficient balance") 29 | ErrSigningRequestNotExist = errorsmod.Register(ModuleName, 4202, "signing request does not exist") 30 | ErrInvalidStatus = errorsmod.Register(ModuleName, 4203, "invalid status") 31 | 32 | ErrUTXODoesNotExist = errorsmod.Register(ModuleName, 5100, "utxo does not exist") 33 | ErrUTXOLocked = errorsmod.Register(ModuleName, 5101, "utxo locked") 34 | ErrUTXOUnlocked = errorsmod.Register(ModuleName, 5102, "utxo unlocked") 35 | 36 | ErrInvalidAmount = errorsmod.Register(ModuleName, 6100, "invalid amount") 37 | ErrInvalidFeeRate = errorsmod.Register(ModuleName, 6101, "invalid fee rate") 38 | ErrAssetNotSupported = errorsmod.Register(ModuleName, 6102, "asset not supported") 39 | ErrDustOutput = errorsmod.Register(ModuleName, 6103, "too small output amount") 40 | ErrInsufficientUTXOs = errorsmod.Register(ModuleName, 6104, "insufficient utxos") 41 | ErrFailToSerializePsbt = errorsmod.Register(ModuleName, 6105, "failed to serialize psbt") 42 | 43 | ErrInvalidRunes = errorsmod.Register(ModuleName, 7100, "invalid runes") 44 | ErrInvalidRuneId = errorsmod.Register(ModuleName, 7101, "invalid rune id") 45 | 46 | ErrInvalidParams = errorsmod.Register(ModuleName, 8100, "invalid module params") 47 | ) 48 | -------------------------------------------------------------------------------- /x/btcbridge/types/keys.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "math/big" 5 | ) 6 | 7 | const ( 8 | // ModuleName defines the module name 9 | ModuleName = "btcbridge" 10 | 11 | // StoreKey defines the primary module store key 12 | StoreKey = ModuleName 13 | 14 | // RouterKey defines the module's message routing key 15 | RouterKey = ModuleName 16 | 17 | // MemStoreKey defines the in-memory store key 18 | MemStoreKey = "mem_btcbridge" 19 | ) 20 | 21 | func KeyPrefix(p string) []byte { 22 | return []byte(p) 23 | } 24 | 25 | var ( 26 | ParamsStoreKey = []byte{0x1} 27 | SequenceKey = []byte{0x2} 28 | 29 | // Host chain keys prefix the HostChain structs 30 | BtcBlockHeaderHashPrefix = []byte{0x11} // prefix for each key to a block header, for a hash 31 | BtcBlockHeaderHeightPrefix = []byte{0x12} // prefix for each key to a block hash, for a height 32 | BtcBestBlockHeaderKey = []byte{0x13} // key for the best block height 33 | BtcSigningRequestPrefix = []byte{0x14} // prefix for each key to a signing request 34 | 35 | BtcUtxoKeyPrefix = []byte{0x15} // prefix for each key to a utxo 36 | BtcOwnerUtxoKeyPrefix = []byte{0x16} // prefix for each key to an owned utxo 37 | BtcOwnerRunesUtxoKeyPrefix = []byte{0x17} // prefix for each key to an owned runes utxo 38 | 39 | BtcMintedTxHashKeyPrefix = []byte{0x18} // prefix for each key to a minted tx hash 40 | 41 | BtcLockedAssetKeyPrefix = []byte{0x19} // prefix for each key to the locked asset 42 | ) 43 | 44 | func Int64ToBytes(number uint64) []byte { 45 | big := new(big.Int) 46 | big.SetUint64(number) 47 | return big.Bytes() 48 | } 49 | 50 | func BtcUtxoKey(hash string, vout uint64) []byte { 51 | return append(append(BtcUtxoKeyPrefix, []byte(hash)...), Int64ToBytes(vout)...) 52 | } 53 | 54 | func BtcOwnerUtxoKey(owner string, hash string, vout uint64) []byte { 55 | key := append(append(BtcOwnerUtxoKeyPrefix, []byte(owner)...), []byte(hash)...) 56 | key = append(key, Int64ToBytes(vout)...) 57 | 58 | return key 59 | } 60 | 61 | func BtcOwnerRunesUtxoKey(owner string, id string, hash string, vout uint64) []byte { 62 | key := append(append(BtcOwnerRunesUtxoKeyPrefix, []byte(owner)...), []byte(id)...) 63 | key = append(key, []byte(hash)...) 64 | key = append(key, Int64ToBytes(vout)...) 65 | 66 | return key 67 | } 68 | 69 | func BtcBlockHeaderHashKey(hash string) []byte { 70 | return append(BtcBlockHeaderHashPrefix, []byte(hash)...) 71 | } 72 | 73 | func BtcBlockHeaderHeightKey(height uint64) []byte { 74 | return append(BtcBlockHeaderHeightPrefix, Int64ToBytes(height)...) 75 | } 76 | 77 | // @deprecated, use BtcSigningRequestHashKey instead 78 | func BtcSigningRequestKey(sequence uint64) []byte { 79 | return append(BtcSigningRequestPrefix, Int64ToBytes(sequence)...) 80 | } 81 | 82 | func BtcSigningRequestHashKey(txid string) []byte { 83 | return append(BtcSigningRequestPrefix, []byte(txid)...) 84 | } 85 | 86 | func BtcMintedTxHashKey(hash string) []byte { 87 | return append(BtcMintedTxHashKeyPrefix, []byte(hash)...) 88 | } 89 | 90 | func BtcLockedAssetKey(txHash string, coin []byte) []byte { 91 | return append(append(BtcLockedAssetKeyPrefix, []byte(txHash)...), coin...) 92 | } 93 | -------------------------------------------------------------------------------- /x/btcbridge/types/genesis.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/chaincfg" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | ) 7 | 8 | // this line is used by starport scaffolding # genesis/types/import 9 | 10 | func DefaultBestBlockHeader() *BlockHeader { 11 | config := sdk.GetConfig().GetBtcChainCfg() 12 | switch config.Name { 13 | case chaincfg.MainNetParams.Name: 14 | return DefaultMainNetBestBlockHeader() 15 | case chaincfg.SigNetParams.Name: 16 | return DefaultSignetBestBlockHeader() 17 | } 18 | return DefaultTestnetBestBlockHeader() 19 | } 20 | 21 | func DefaultSignetBestBlockHeader() *BlockHeader { 22 | // testnet3 block 2815023 23 | return &BlockHeader{ 24 | Version: 536870912, 25 | Hash: "0000017317a7dfa637773406765d308e93cb5a8e5e266bb21687e120bf0e13d3", 26 | Height: 1, 27 | PreviousBlockHash: "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6", 28 | MerkleRoot: "f1192075c6416b02df1487f1f302d925a875bec7e37cf38079e00b1cd831898a", 29 | Time: 1718707794, 30 | Bits: "1e0377ae", 31 | Nonce: 13603325, 32 | Ntx: 1, 33 | } 34 | } 35 | 36 | func DefaultTestnetBestBlockHeader() *BlockHeader { 37 | // testnet3 block 2815023 38 | return &BlockHeader{ 39 | Version: 667459584, 40 | Hash: "0000000000000009fb68da72e8994f014fafb455c72978233b94580b12af778c", 41 | Height: 2815023, 42 | PreviousBlockHash: "0000000000000004a29c20eb32532718de8072665620edb4c657b22b4d463967", 43 | MerkleRoot: "9e219423eadce80e882cdff04b3026c9bbc994fd08a774f34a705ca3e710a332", 44 | Time: 1715566066, 45 | Bits: "191881b8", 46 | Nonce: 3913166971, 47 | Ntx: 6236, 48 | } 49 | } 50 | 51 | func DefaultMainNetBestBlockHeader() *BlockHeader { 52 | // testnet3 block 2815023 53 | return &BlockHeader{ 54 | Version: 667459584, 55 | Hash: "0000000000000009fb68da72e8994f014fafb455c72978233b94580b12af778c", 56 | Height: 2815023, 57 | PreviousBlockHash: "0000000000000004a29c20eb32532718de8072665620edb4c657b22b4d463967", 58 | MerkleRoot: "9e219423eadce80e882cdff04b3026c9bbc994fd08a774f34a705ca3e710a332", 59 | Time: 1715566066, 60 | Bits: "191881b8", 61 | Nonce: 3913166971, 62 | Ntx: 6236, 63 | } 64 | } 65 | 66 | // DefaultGenesis returns the default genesis state 67 | func DefaultGenesis() *GenesisState { 68 | return &GenesisState{ 69 | // this line is used by starport scaffolding # genesis/types/default 70 | Params: DefaultParams(), 71 | BestBlockHeader: DefaultBestBlockHeader(), 72 | BlockHeaders: []*BlockHeader{}, 73 | Utxos: []*UTXO{}, 74 | } 75 | } 76 | 77 | // Validate performs basic genesis state validation returning an error upon any 78 | // failure. 79 | func (gs GenesisState) Validate() error { 80 | // this line is used by starport scaffolding # genesis/types/validate 81 | // need to be improved by checking the block headers & best block header 82 | if gs.BestBlockHeader == nil || gs.BestBlockHeader.Hash == "" || gs.BestBlockHeader.PreviousBlockHash == "" || gs.BestBlockHeader.MerkleRoot == "" { 83 | return ErrInvalidHeader 84 | } 85 | return gs.Params.Validate() 86 | } 87 | -------------------------------------------------------------------------------- /second_node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | KEYS=("dev0" "dev1") 4 | CHAINID="grimoria-testnet-1" 5 | MONIKER="Side Labs" 6 | BINARY="$HOME/go/bin/sided" 7 | DENOM_STR="uside,uusdt,uusdc" 8 | 9 | set -f 10 | IFS=, 11 | DENOMS=($DENOM_STR) 12 | 13 | IFS=";" 14 | 15 | 16 | INITIAL_SUPPLY="500000000000000" 17 | BLOCK_GAS=10000000 18 | MAX_GAS=10000000000 19 | 20 | # Remember to change to other types of keyring like 'file' in-case exposing to outside world, 21 | # otherwise your balance will be wiped quickly 22 | # The keyring test does not require private key to steal tokens from you 23 | KEYRING="test" 24 | #KEYALGO="secp256k1" 25 | KEYALGO="segwit" 26 | LOGLEVEL="info" 27 | # Set dedicated home directory for the $BINARY instance 28 | HOMEDIR="$HOME/.side2" 29 | 30 | # Path variables 31 | CONFIG=$HOMEDIR/config/config.toml 32 | APP_TOML=$HOMEDIR/config/app.toml 33 | GENESIS=$HOMEDIR/config/genesis.json 34 | TMP_GENESIS=$HOMEDIR/config/tmp_genesis.json 35 | PEER_ID="peer" 36 | 37 | # validate dependencies are installed 38 | command -v jq >/dev/null 2>&1 || { 39 | echo >&2 "jq not installed. More info: https://stedolan.github.io/jq/download/" 40 | exit 1 41 | } 42 | 43 | # used to exit on first error (any non-zero exit code) 44 | set -e 45 | 46 | # Reinstall daemon 47 | # make install 48 | 49 | # User prompt if an existing local node configuration is found. 50 | if [ -d "$HOMEDIR" ]; then 51 | printf "\nAn existing folder at '%s' was found. You can choose to delete this folder and start a new local node with new keys from genesis. When declined, the existing local node is started. \n" "$HOMEDIR" 52 | echo "Overwrite the existing configuration and start a new local node? [y/n]" 53 | read -r overwrite 54 | else 55 | overwrite="Y" 56 | fi 57 | 58 | 59 | # Setup local node if overwrite is set to Yes, otherwise skip setup 60 | if [[ $overwrite == "y" || $overwrite == "Y" ]]; then 61 | # Remove the previous folder 62 | rm -rf "$HOMEDIR" 63 | 64 | $BINARY init $MONIKER -o --chain-id $CHAINID --home "$HOMEDIR" 65 | 66 | # Set client config 67 | cp -r ~/.side/config/genesis.json $HOMEDIR/config/genesis.json 68 | $BINARY config keyring-backend $KEYRING --home "$HOMEDIR" 69 | $BINARY config chain-id $CHAINID --home "$HOMEDIR" 70 | 71 | sed -i.bak 's/127.0.0.1:26657/0.0.0.0:16657/g' "$CONFIG" 72 | sed -i.bak 's/127.0.0.1:26658/0.0.0.0:16658/g' "$CONFIG" 73 | sed -i.bak 's/0.0.0.0:26656/0.0.0.0:16656/g' "$CONFIG" 74 | sed -i.bak 's/persistent_peers = ""/persistent_peers = "dfd3e3c99414aa850f6e269cf4a674a66062cd49@127.0.0.1:26656"/g' "$CONFIG" 75 | #sed -i 's/persistent_peers = "$PEERID"/g' "$CONFIG" 76 | 77 | sed -i.bak 's/swagger = false/swagger = true/g' $APP_TOML 78 | sed -i.bak 's/localhost:9090/localhost:8090/g' $APP_TOML 79 | 80 | $BINARY keys add secp256k1_key --key-type segwit --home "$HOMEDIR" 81 | $BINARY tx bank send dev1 $($BINARY keys show secp256k1_key -a --home "$HOMEDIR") 2000000000uside --chain-id $CHAINID --fees 200uside --yes 82 | sleep 6 83 | $BINARY keys add segwit_key --key-type segwit --home "$HOMEDIR" 84 | $BINARY tx bank send dev1 $($BINARY keys show segwit_key -a --home "$HOMEDIR") 2000000000uside --chain-id $CHAINID --fees 200uside --yes 85 | 86 | 87 | fi 88 | 89 | # Start the node (remove the --pruning=nothing flag if historical queries are not needed) 90 | $BINARY start --log_level info --minimum-gas-prices=0.0001${DENOMS[0]} --home "$HOMEDIR" --pruning="everything" 91 | -------------------------------------------------------------------------------- /testutil/network/network.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | tmdb "github.com/cometbft/cometbft-db" 9 | tmrand "github.com/cometbft/cometbft/libs/rand" 10 | "github.com/cosmos/cosmos-sdk/baseapp" 11 | "github.com/cosmos/cosmos-sdk/crypto/hd" 12 | "github.com/cosmos/cosmos-sdk/crypto/keyring" 13 | servertypes "github.com/cosmos/cosmos-sdk/server/types" 14 | pruningtypes "github.com/cosmos/cosmos-sdk/store/pruning/types" 15 | "github.com/cosmos/cosmos-sdk/testutil/network" 16 | simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" 17 | sdk "github.com/cosmos/cosmos-sdk/types" 18 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 19 | "github.com/stretchr/testify/require" 20 | 21 | "github.com/sideprotocol/side/app" 22 | ) 23 | 24 | type ( 25 | Network = network.Network 26 | Config = network.Config 27 | ) 28 | 29 | // New creates instance with fully configured cosmos network. 30 | // Accepts optional config, that will be used in place of the DefaultConfig() if provided. 31 | func New(t *testing.T, configs ...Config) *Network { 32 | if len(configs) > 1 { 33 | panic("at most one config should be provided") 34 | } 35 | var cfg network.Config 36 | if len(configs) == 0 { 37 | cfg = DefaultConfig() 38 | } else { 39 | cfg = configs[0] 40 | } 41 | net, err := network.New(t, t.TempDir(), cfg) 42 | require.NoError(t, err) 43 | _, err = net.WaitForHeight(1) 44 | require.NoError(t, err) 45 | t.Cleanup(net.Cleanup) 46 | return net 47 | } 48 | 49 | // DefaultConfig will initialize config for the network with custom application, 50 | // genesis and single validator. All other parameters are inherited from cosmos-sdk/testutil/network.DefaultConfig 51 | func DefaultConfig() network.Config { 52 | var ( 53 | encoding = app.MakeEncodingConfig() 54 | chainID = "chain-" + tmrand.NewRand().Str(6) 55 | ) 56 | return network.Config{ 57 | Codec: encoding.Marshaler, 58 | TxConfig: encoding.TxConfig, 59 | LegacyAmino: encoding.Amino, 60 | InterfaceRegistry: encoding.InterfaceRegistry, 61 | AccountRetriever: authtypes.AccountRetriever{}, 62 | AppConstructor: func(val network.ValidatorI) servertypes.Application { 63 | return app.New( 64 | val.GetCtx().Logger, 65 | tmdb.NewMemDB(), 66 | nil, 67 | true, 68 | map[int64]bool{}, 69 | val.GetCtx().Config.RootDir, 70 | 0, 71 | encoding, 72 | simtestutil.EmptyAppOptions{}, 73 | baseapp.SetPruning(pruningtypes.NewPruningOptionsFromString(val.GetAppConfig().Pruning)), 74 | baseapp.SetMinGasPrices(val.GetAppConfig().MinGasPrices), 75 | baseapp.SetChainID(chainID), 76 | ) 77 | }, 78 | GenesisState: app.ModuleBasics.DefaultGenesis(encoding.Marshaler), 79 | TimeoutCommit: 2 * time.Second, 80 | ChainID: chainID, 81 | NumValidators: 1, 82 | BondDenom: sdk.DefaultBondDenom, 83 | MinGasPrices: fmt.Sprintf("0.000006%s", sdk.DefaultBondDenom), 84 | AccountTokens: sdk.TokensFromConsensusPower(1000, sdk.DefaultPowerReduction), 85 | StakingTokens: sdk.TokensFromConsensusPower(500, sdk.DefaultPowerReduction), 86 | BondedTokens: sdk.TokensFromConsensusPower(100, sdk.DefaultPowerReduction), 87 | PruningStrategy: pruningtypes.PruningOptionNothing, 88 | CleanupDir: true, 89 | SigningAlgo: string(hd.Secp256k1Type), 90 | KeyringOptions: []keyring.Option{}, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /x/btcbridge/types/signature.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | secp256k1 "github.com/btcsuite/btcd/btcec/v2" 5 | "github.com/btcsuite/btcd/btcec/v2/ecdsa" 6 | "github.com/btcsuite/btcd/btcec/v2/schnorr" 7 | "github.com/btcsuite/btcd/btcutil/psbt" 8 | "github.com/btcsuite/btcd/txscript" 9 | "github.com/btcsuite/btcd/wire" 10 | ) 11 | 12 | // VerifyPsbtSignatures verifies the signatures of the given psbt 13 | // Note: assume that the psbt is finalized and all inputs are witness type 14 | func VerifyPsbtSignatures(p *psbt.Packet) bool { 15 | // extract signed tx 16 | signedTx, err := psbt.Extract(p) 17 | if err != nil { 18 | return false 19 | } 20 | 21 | // build previous output fetcher 22 | prevOutputFetcher := txscript.NewMultiPrevOutFetcher(nil) 23 | 24 | for i, txIn := range p.UnsignedTx.TxIn { 25 | prevOutput := p.Inputs[i].WitnessUtxo 26 | if prevOutput == nil { 27 | return false 28 | } 29 | 30 | prevOutputFetcher.AddPrevOut(txIn.PreviousOutPoint, prevOutput) 31 | } 32 | 33 | // verify signatures 34 | for i := range p.Inputs { 35 | prevOutput := p.Inputs[i].WitnessUtxo 36 | hashType := DefaultSigHashType 37 | 38 | switch { 39 | case txscript.IsPayToWitnessPubKeyHash(prevOutput.PkScript): 40 | if !verifyWitnessSignature(signedTx, i, prevOutput, prevOutputFetcher, hashType) { 41 | return false 42 | } 43 | 44 | case txscript.IsPayToTaproot(prevOutput.PkScript): 45 | if !verifyTaprootSignature(signedTx, i, prevOutput, prevOutputFetcher, hashType) { 46 | return false 47 | } 48 | 49 | default: 50 | return false 51 | } 52 | } 53 | 54 | return true 55 | } 56 | 57 | // verifyWitnessSignature verifies the signature of the witness v0 input 58 | func verifyWitnessSignature(tx *wire.MsgTx, idx int, prevOutput *wire.TxOut, prevOutputFetcher txscript.PrevOutputFetcher, hashType txscript.SigHashType) bool { 59 | witness := tx.TxIn[idx].Witness 60 | if len(witness) != 2 { 61 | return false 62 | } 63 | 64 | sigBytes := witness[0] 65 | pkBytes := witness[1] 66 | 67 | sig, err := ecdsa.ParseDERSignature(sigBytes) 68 | if err != nil { 69 | return false 70 | } 71 | 72 | pk, err := secp256k1.ParsePubKey(pkBytes) 73 | if err != nil { 74 | return false 75 | } 76 | 77 | if sigBytes[len(sigBytes)-1] != byte(hashType) { 78 | return false 79 | } 80 | 81 | sigHash, err := txscript.CalcWitnessSigHash(prevOutput.PkScript, txscript.NewTxSigHashes(tx, prevOutputFetcher), 82 | hashType, tx, idx, prevOutput.Value) 83 | if err != nil { 84 | return false 85 | } 86 | 87 | return sig.Verify(sigHash, pk) 88 | } 89 | 90 | // verifyTaprootSignature verifies the signature of the taproot input 91 | func verifyTaprootSignature(tx *wire.MsgTx, idx int, prevOutput *wire.TxOut, prevOutputFetcher txscript.PrevOutputFetcher, hashType txscript.SigHashType) bool { 92 | witness := tx.TxIn[idx].Witness 93 | if len(witness) != 1 || len(witness[0]) == 0 { 94 | return false 95 | } 96 | 97 | sigBytes := witness[0] 98 | 99 | if hashType != txscript.SigHashDefault { 100 | if sigBytes[len(sigBytes)-1] != byte(hashType) { 101 | return false 102 | } 103 | 104 | sigBytes = sigBytes[0 : len(sigBytes)-1] 105 | } 106 | 107 | sig, err := schnorr.ParseSignature(sigBytes) 108 | if err != nil { 109 | return false 110 | } 111 | 112 | pk, err := schnorr.ParsePubKey(prevOutput.PkScript[2:34]) 113 | if err != nil { 114 | return false 115 | } 116 | 117 | sigHash, err := txscript.CalcTaprootSignatureHash(txscript.NewTxSigHashes(tx, prevOutputFetcher), 118 | hashType, tx, idx, prevOutputFetcher) 119 | if err != nil { 120 | return false 121 | } 122 | 123 | return sig.Verify(sigHash, pk) 124 | } 125 | -------------------------------------------------------------------------------- /x/btcbridge/types/params.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | 7 | secp256k1 "github.com/btcsuite/btcd/btcec/v2" 8 | "github.com/btcsuite/btcd/btcutil" 9 | "github.com/btcsuite/btcd/txscript" 10 | 11 | sdk "github.com/cosmos/cosmos-sdk/types" 12 | ) 13 | 14 | // NewParams creates a new Params instance 15 | func NewParams(relayers []string) Params { 16 | return Params{ 17 | AuthorizedRelayers: relayers, 18 | Confirmations: 2, 19 | MaxAcceptableBlockDepth: 100, 20 | BtcVoucherDenom: "sat", 21 | Vaults: []*Vault{{ 22 | Address: "", 23 | PubKey: "", 24 | AssetType: AssetType_ASSET_TYPE_BTC, 25 | }, { 26 | Address: "", 27 | PubKey: "", 28 | AssetType: AssetType_ASSET_TYPE_RUNE, 29 | }}, 30 | } 31 | } 32 | 33 | // DefaultParams returns a default set of parameters 34 | func DefaultParams() Params { 35 | return NewParams([]string{}) 36 | } 37 | 38 | // Validate validates the set of params 39 | func (p Params) Validate() error { 40 | for _, sender := range p.AuthorizedRelayers { 41 | _, err := sdk.AccAddressFromBech32(sender) 42 | if err != nil { 43 | return err 44 | } 45 | } 46 | 47 | if err := sdk.ValidateDenom(p.BtcVoucherDenom); err != nil { 48 | return err 49 | } 50 | 51 | vaults := make(map[string]bool) 52 | 53 | for _, vault := range p.Vaults { 54 | _, ok := vaults[vault.Address] 55 | if ok { 56 | return ErrInvalidParams 57 | } 58 | 59 | _, err := sdk.AccAddressFromBech32(vault.Address) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | if len(vault.PubKey) != 0 { 65 | pkBytes, err := hex.DecodeString(vault.PubKey) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | _, err = secp256k1.ParsePubKey(pkBytes) 71 | if err != nil { 72 | return err 73 | } 74 | } 75 | 76 | if vault.AssetType == AssetType_ASSET_TYPE_UNSPECIFIED { 77 | return ErrInvalidParams 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // checks if the given address is an authorized sender 85 | func (p Params) IsAuthorizedSender(sender string) bool { 86 | for _, s := range p.AuthorizedRelayers { 87 | if s == sender { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | // SelectVaultByBitcoinAddress returns the vault if the address is found 95 | // returns the vault if the address is found 96 | func SelectVaultByBitcoinAddress(vaults []*Vault, address string) *Vault { 97 | for _, v := range vaults { 98 | if v.Address == address { 99 | return v 100 | } 101 | } 102 | return nil 103 | } 104 | 105 | // SelectVaultByPubKey returns the vault if the public key is found 106 | // returns the vault if the public key is found 107 | func SelectVaultByPubKey(vaults []*Vault, pubKey string) *Vault { 108 | for _, v := range vaults { 109 | if v.PubKey == pubKey { 110 | return v 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | 117 | // SelectVaultByAssetType returns the vault by the asset type 118 | func SelectVaultByAssetType(vaults []*Vault, assetType AssetType) *Vault { 119 | for _, v := range vaults { 120 | if v.AssetType == assetType { 121 | return v 122 | } 123 | } 124 | 125 | return nil 126 | } 127 | 128 | // SelectVaultByPkScript returns the vault by the pk script 129 | func SelectVaultByPkScript(vaults []*Vault, pkScript []byte) *Vault { 130 | chainCfg := sdk.GetConfig().GetBtcChainCfg() 131 | 132 | for _, v := range vaults { 133 | addr, err := btcutil.DecodeAddress(v.Address, chainCfg) 134 | if err != nil { 135 | continue 136 | } 137 | 138 | addrScript, err := txscript.PayToAddrScript(addr) 139 | if err != nil { 140 | continue 141 | } 142 | 143 | if bytes.Equal(addrScript, pkScript) { 144 | return v 145 | } 146 | } 147 | 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /x/btcbridge/genesis_test.go: -------------------------------------------------------------------------------- 1 | package btcbridge_test 2 | 3 | // Path: x/btcbridge/genesis_test.go 4 | 5 | import ( 6 | "bytes" 7 | "encoding/hex" 8 | "testing" 9 | 10 | "github.com/btcsuite/btcd/btcutil" 11 | "github.com/btcsuite/btcd/wire" 12 | keepertest "github.com/sideprotocol/side/testutil/keeper" 13 | "github.com/sideprotocol/side/testutil/nullify" 14 | "github.com/sideprotocol/side/x/btcbridge" 15 | "github.com/sideprotocol/side/x/btcbridge/types" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestGenesis(t *testing.T) { 20 | mnemonic := "sunny bamboo garlic fold reopen exile letter addict forest vessel square lunar shell number deliver cruise calm artist fire just kangaroo suit wheel extend" 21 | println(mnemonic) 22 | 23 | genesisState := types.GenesisState{ 24 | Params: types.DefaultParams(), 25 | 26 | // this line is used by starport scaffolding # genesis/test/state 27 | } 28 | 29 | k, ctx := keepertest.BtcBridgeKeeper(t) 30 | btcbridge.InitGenesis(ctx, *k, genesisState) 31 | got := btcbridge.ExportGenesis(ctx, *k) 32 | require.NotNil(t, got) 33 | 34 | nullify.Fill(&genesisState) 35 | nullify.Fill(got) 36 | 37 | // this line is used by starport scaffolding # genesis/test/assert 38 | } 39 | 40 | // TestSubmitTx tests the SubmitTx function 41 | // func TestSubmitTx(t *testing.T) { 42 | 43 | // // test tx: https://blockchain.info/tx/b657e22827039461a9493ede7bdf55b01579254c1630b0bfc9185ec564fc05ab?format=json 44 | 45 | // k, ctx := keepertest.BtcbridgeClientKeeper(t) 46 | 47 | // txHex := "02000000000101e5df234dbeff74d6da14754ebeea8ab0e2f60b1884566846cf6b36e8ceb5f5350100000000fdffffff02f0490200000000001600142ac13073e8d491428790481321a636696d00107681d7e205000000001600142bf3aa186cbdcbe88b70a67edcd5a32ce5e8e6d8024730440220081ee61d749ce8cedcf6eedde885579af2eb65ca67d29e6ae2c37109d81cbbb202203a1891ce45f91f159ccf04348ef37a3d1a12d89e5e01426e061326057e6c128d012103036bbdd77c9a932f37bd66175967c7fb7eb75ece06b87c1ad1716770cb3ca4ee79fc2a00" 48 | // prevTxHex := "0200000000010183372652f2af9ab34b3a003efada6b054c75583185ac130de72599dfdf4e462b0100000000fdffffff02f0490200000000001600142ac13073e8d491428790481321a636696d001076a64ee50500000000160014a03614eef338681373de94a2dc2574de55da1980024730440220274250f6036bea0947daf4455ab4976f81721257d163fd952fb5b0c70470edc602202fba816be260219bbc40a8983c459cf05cf2209bf1e62e7ccbf78aec54db607f0121031cee21ef69fe68b240c3032616fa310c6a60a856c0a7e0c1298815c92fb2c61788fb2a00" 49 | 50 | // msg := &types.MsgSubmitTransactionRequest{ 51 | // Sender: "", 52 | // Blockhash: "000000000d73ecf25d3bf8e6ae65c35aa2a90e3271edff8bab90d87ed875f13b", 53 | // TxBytes: "0100000001b3f7", 54 | // PrevTxBytes: "0100000001b3f7", 55 | // Proof: []string{"0100000001b3f7"}, 56 | // } 57 | // // this line is used by starport scaffolding # handler/test/submit 58 | // err := k.ProcessBitcoinDepositTransaction(ctx, msg) 59 | // require.NoError(t, err) 60 | // } 61 | 62 | // Decode transaction 63 | func TestDecodeTransaction(t *testing.T) { 64 | hexStr := "02000000000101e5df234dbeff74d6da14754ebeea8ab0e2f60b1884566846cf6b36e8ceb5f5350100000000fdffffff02f0490200000000001600142ac13073e8d491428790481321a636696d00107681d7e205000000001600142bf3aa186cbdcbe88b70a67edcd5a32ce5e8e6d8024730440220081ee61d749ce8cedcf6eedde885579af2eb65ca67d29e6ae2c37109d81cbbb202203a1891ce45f91f159ccf04348ef37a3d1a12d89e5e01426e061326057e6c128d012103036bbdd77c9a932f37bd66175967c7fb7eb75ece06b87c1ad1716770cb3ca4ee79fc2a00" 65 | 66 | // Decode the hex string to transaction 67 | txBytes, err := hex.DecodeString(hexStr) 68 | require.NoError(t, err) 69 | 70 | // Create a new transaction 71 | var tx wire.MsgTx 72 | err = tx.Deserialize(bytes.NewReader(txBytes)) 73 | require.NoError(t, err) 74 | 75 | uTx := btcutil.NewTx(&tx) 76 | 77 | for _, input := range uTx.MsgTx().TxIn { 78 | t.Log(input.PreviousOutPoint.String()) 79 | } 80 | 81 | require.GreaterOrEqual(t, len(uTx.MsgTx().TxIn), 1) 82 | } 83 | -------------------------------------------------------------------------------- /x/btcbridge/keeper/queries.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "context" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | "github.com/sideprotocol/side/x/btcbridge/types" 8 | "google.golang.org/grpc/codes" 9 | "google.golang.org/grpc/status" 10 | ) 11 | 12 | var _ types.QueryServer = Keeper{} 13 | 14 | func (k Keeper) QueryParams(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { 15 | if req == nil { 16 | return nil, status.Error(codes.InvalidArgument, "invalid request") 17 | } 18 | ctx := sdk.UnwrapSDKContext(goCtx) 19 | 20 | return &types.QueryParamsResponse{Params: k.GetParams(ctx)}, nil 21 | } 22 | 23 | func (k Keeper) QueryChainTip(goCtx context.Context, req *types.QueryChainTipRequest) (*types.QueryChainTipResponse, error) { 24 | if req == nil { 25 | return nil, status.Error(codes.InvalidArgument, "invalid request") 26 | } 27 | ctx := sdk.UnwrapSDKContext(goCtx) 28 | 29 | best := k.GetBestBlockHeader(ctx) 30 | 31 | return &types.QueryChainTipResponse{ 32 | Hash: best.Hash, 33 | Height: best.Height, 34 | }, nil 35 | } 36 | 37 | // BlockHeaderByHash queries the block header by hash. 38 | func (k Keeper) QueryBlockHeaderByHash(goCtx context.Context, req *types.QueryBlockHeaderByHashRequest) (*types.QueryBlockHeaderByHashResponse, error) { 39 | if req == nil { 40 | return nil, status.Error(codes.InvalidArgument, "invalid request") 41 | } 42 | ctx := sdk.UnwrapSDKContext(goCtx) 43 | 44 | header := k.GetBlockHeader(ctx, req.Hash) 45 | if header == nil { 46 | return nil, status.Error(codes.NotFound, "block header not found") 47 | } 48 | 49 | return &types.QueryBlockHeaderByHashResponse{BlockHeader: header}, nil 50 | } 51 | 52 | func (k Keeper) QueryBlockHeaderByHeight(goCtx context.Context, req *types.QueryBlockHeaderByHeightRequest) (*types.QueryBlockHeaderByHeightResponse, error) { 53 | if req == nil { 54 | return nil, status.Error(codes.InvalidArgument, "invalid request") 55 | } 56 | ctx := sdk.UnwrapSDKContext(goCtx) 57 | 58 | header := k.GetBlockHeaderByHeight(ctx, req.Height) 59 | if header == nil { 60 | return nil, status.Error(codes.NotFound, "block header not found") 61 | } 62 | 63 | return &types.QueryBlockHeaderByHeightResponse{BlockHeader: header}, nil 64 | } 65 | 66 | func (k Keeper) QuerySigningRequest(goCtx context.Context, req *types.QuerySigningRequestRequest) (*types.QuerySigningRequestResponse, error) { 67 | if req == nil { 68 | return nil, status.Error(codes.InvalidArgument, "invalid request") 69 | } 70 | if req.Status == types.SigningStatus_SIGNING_STATUS_UNSPECIFIED { 71 | return nil, status.Error(codes.InvalidArgument, "invalid status") 72 | } 73 | 74 | ctx := sdk.UnwrapSDKContext(goCtx) 75 | 76 | requests := k.FilterSigningRequestsByStatus(ctx, req) 77 | 78 | return &types.QuerySigningRequestResponse{Requests: requests}, nil 79 | } 80 | 81 | func (k Keeper) QuerySigningRequestByAddress(goCtx context.Context, req *types.QuerySigningRequestByAddressRequest) (*types.QuerySigningRequestByAddressResponse, error) { 82 | if req == nil { 83 | return nil, status.Error(codes.InvalidArgument, "invalid request") 84 | } 85 | 86 | ctx := sdk.UnwrapSDKContext(goCtx) 87 | 88 | requests := k.FilterSigningRequestsByAddr(ctx, req) 89 | 90 | return &types.QuerySigningRequestByAddressResponse{Requests: requests}, nil 91 | } 92 | 93 | func (k Keeper) QuerySigningRequestByTxHash(goCtx context.Context, req *types.QuerySigningRequestByTxHashRequest) (*types.QuerySigningRequestByTxHashResponse, error) { 94 | if req == nil { 95 | return nil, status.Error(codes.InvalidArgument, "invalid request") 96 | } 97 | 98 | ctx := sdk.UnwrapSDKContext(goCtx) 99 | 100 | var request *types.BitcoinSigningRequest 101 | 102 | if k.HasSigningRequest(ctx, req.Txid) { 103 | request = k.GetSigningRequest(ctx, req.Txid) 104 | } 105 | 106 | return &types.QuerySigningRequestByTxHashResponse{Request: request}, nil 107 | } 108 | 109 | func (k Keeper) QueryUTXOs(goCtx context.Context, req *types.QueryUTXOsRequest) (*types.QueryUTXOsResponse, error) { 110 | if req == nil { 111 | return nil, status.Error(codes.InvalidArgument, "invalid request") 112 | } 113 | ctx := sdk.UnwrapSDKContext(goCtx) 114 | 115 | utxos := k.GetAllUTXOs(ctx) 116 | 117 | return &types.QueryUTXOsResponse{Utxos: utxos}, nil 118 | } 119 | 120 | func (k Keeper) QueryUTXOsByAddress(goCtx context.Context, req *types.QueryUTXOsByAddressRequest) (*types.QueryUTXOsByAddressResponse, error) { 121 | if req == nil { 122 | return nil, status.Error(codes.InvalidArgument, "invalid request") 123 | } 124 | ctx := sdk.UnwrapSDKContext(goCtx) 125 | 126 | _, err := sdk.AccAddressFromBech32(req.Address) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | utxos := k.GetUTXOsByAddr(ctx, req.Address) 132 | 133 | return &types.QueryUTXOsByAddressResponse{Utxos: utxos}, nil 134 | } 135 | -------------------------------------------------------------------------------- /x/btcbridge/types/deposit_policy.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/btcutil" 5 | "github.com/btcsuite/btcd/chaincfg" 6 | "github.com/btcsuite/btcd/txscript" 7 | "github.com/btcsuite/btcd/wire" 8 | ) 9 | 10 | const ( 11 | // maximum allowed number of the non-vault outputs for the btc deposit transaction 12 | MaxNonVaultOutNum = 1 13 | 14 | // maximum allowed number of the non-vault outputs for the runes deposit transaction 15 | RunesMaxNonVaultOutNum = 3 16 | 17 | // allowed number of edicts in the runes payload for the runes deposit transaction 18 | RunesEdictNum = 1 19 | ) 20 | 21 | // ExtractRecipientAddr extracts the recipient address for minting voucher token by the type of the asset to be deposited 22 | func ExtractRecipientAddr(tx *wire.MsgTx, prevTx *wire.MsgTx, vaults []*Vault, isRunes bool, chainCfg *chaincfg.Params) (btcutil.Address, error) { 23 | if isRunes { 24 | return ExtractRunesRecipientAddr(tx, prevTx, vaults, chainCfg) 25 | } 26 | 27 | return ExtractCommonRecipientAddr(tx, prevTx, vaults, chainCfg) 28 | } 29 | 30 | // ExtractCommonRecipientAddr extracts the recipient address for minting voucher token in the common case. 31 | // First, extract the recipient from the tx out which is a non-vault address; 32 | // Then fall back to the first input 33 | func ExtractCommonRecipientAddr(tx *wire.MsgTx, prevTx *wire.MsgTx, vaults []*Vault, chainCfg *chaincfg.Params) (btcutil.Address, error) { 34 | var recipient btcutil.Address 35 | 36 | nonVaultOutCount := 0 37 | 38 | // extract from the tx out which is a non-vault address 39 | for _, out := range tx.TxOut { 40 | pkScript, err := txscript.ParsePkScript(out.PkScript) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | addr, err := pkScript.Address(chainCfg) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | vault := SelectVaultByBitcoinAddress(vaults, addr.EncodeAddress()) 51 | if vault == nil { 52 | recipient = addr 53 | nonVaultOutCount++ 54 | } 55 | } 56 | 57 | // exceed allowed non vault out number 58 | if nonVaultOutCount > MaxNonVaultOutNum { 59 | return nil, ErrInvalidDepositTransaction 60 | } 61 | 62 | if recipient != nil { 63 | return recipient, nil 64 | } 65 | 66 | // fall back to extract from the first input 67 | pkScript, err := txscript.ParsePkScript(prevTx.TxOut[tx.TxIn[0].PreviousOutPoint.Index].PkScript) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return pkScript.Address(chainCfg) 73 | } 74 | 75 | // ExtractRunesRecipientAddr extracts the recipient address for minting runes voucher token. 76 | // First, extract the recipient from the tx out which is a non-vault and non-OP_RETURN output; 77 | // Then fall back to the first input 78 | func ExtractRunesRecipientAddr(tx *wire.MsgTx, prevTx *wire.MsgTx, vaults []*Vault, chainCfg *chaincfg.Params) (btcutil.Address, error) { 79 | var recipient btcutil.Address 80 | 81 | nonVaultOutCount := 0 82 | 83 | // extract from the tx out which is a non-vault and non-OP_RETURN output 84 | for _, out := range tx.TxOut { 85 | if IsOpReturnOutput(out) { 86 | nonVaultOutCount++ 87 | continue 88 | } 89 | 90 | pkScript, err := txscript.ParsePkScript(out.PkScript) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | addr, err := pkScript.Address(chainCfg) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | vault := SelectVaultByBitcoinAddress(vaults, addr.EncodeAddress()) 101 | if vault == nil { 102 | recipient = addr 103 | nonVaultOutCount++ 104 | } 105 | } 106 | 107 | // exceed allowed non vault out number 108 | if nonVaultOutCount > RunesMaxNonVaultOutNum { 109 | return nil, ErrInvalidDepositTransaction 110 | } 111 | 112 | if recipient != nil { 113 | return recipient, nil 114 | } 115 | 116 | // fall back to extract from the first input 117 | pkScript, err := txscript.ParsePkScript(prevTx.TxOut[tx.TxIn[0].PreviousOutPoint.Index].PkScript) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | return pkScript.Address(chainCfg) 123 | } 124 | 125 | // CheckRunesDepositTransaction checks if the given tx is valid runes deposit tx 126 | func CheckRunesDepositTransaction(tx *wire.MsgTx, vaults []*Vault) (*Edict, error) { 127 | edicts, err := ParseRunes(tx) 128 | if err != nil { 129 | return nil, ErrInvalidDepositTransaction 130 | } 131 | 132 | if len(edicts) == 0 { 133 | return nil, nil 134 | } 135 | 136 | if len(edicts) != RunesEdictNum { 137 | return nil, ErrInvalidDepositTransaction 138 | } 139 | 140 | // even split is not supported 141 | if edicts[0].Output == uint32(len(tx.TxOut)) { 142 | return nil, ErrInvalidDepositTransaction 143 | } 144 | 145 | vault := SelectVaultByPkScript(vaults, tx.TxOut[edicts[0].Output].PkScript) 146 | if vault == nil || vault.AssetType != AssetType_ASSET_TYPE_RUNE { 147 | return nil, ErrInvalidDepositTransaction 148 | } 149 | 150 | return edicts[0], nil 151 | } 152 | -------------------------------------------------------------------------------- /proto/side/btcbridge/tx.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package side.btcbridge; 4 | 5 | import "gogoproto/gogo.proto"; 6 | import "side/btcbridge/params.proto"; 7 | import "side/btcbridge/bitcoin.proto"; 8 | 9 | option go_package = "github.com/sideprotocol/side/x/btcbridge/types"; 10 | 11 | // Msg defines the Msg service. 12 | service Msg { 13 | // SubmitBlockHeaders submits bitcoin block headers to the side chain. 14 | rpc SubmitBlockHeaders (MsgSubmitBlockHeaderRequest) returns (MsgSubmitBlockHeadersResponse); 15 | // SubmitDepositTransaction submits bitcoin transaction to the side chain. 16 | rpc SubmitDepositTransaction (MsgSubmitDepositTransactionRequest) returns (MsgSubmitDepositTransactionResponse); 17 | // SubmitWithdrawalTransaction submits bitcoin transaction to the side chain. 18 | rpc SubmitWithdrawTransaction (MsgSubmitWithdrawTransactionRequest) returns (MsgSubmitWithdrawTransactionResponse); 19 | // UpdateSenders updates the senders of the side chain. 20 | rpc UpdateQualifiedRelayers (MsgUpdateQualifiedRelayersRequest) returns (MsgUpdateQualifiedRelayersResponse); 21 | // WithdrawBitcoin withdraws the bitcoin from the side chain. 22 | rpc WithdrawBitcoin (MsgWithdrawBitcoinRequest) returns (MsgWithdrawBitcoinResponse); 23 | // SubmitWithdrawSignatures submits the signatures of the withdraw transaction. 24 | rpc SubmitWithdrawSignatures (MsgSubmitWithdrawSignaturesRequest) returns (MsgSubmitWithdrawSignaturesResponse); 25 | // SubmitWithdrawStatus submits the status of the withdraw transaction. 26 | rpc SubmitWithdrawStatus (MsgSubmitWithdrawStatusRequest) returns (MsgSubmitWithdrawStatusResponse); 27 | // UpdateParams defines a governance operation for updating the x/btcbridge module 28 | // parameters. The authority defaults to the x/gov module account. 29 | // 30 | // Since: cosmos-sdk 0.47 31 | rpc UpdateParams(MsgUpdateParamsRequest) returns (MsgUpdateParamsResponse); 32 | } 33 | 34 | // MsgSubmitWithdrawStatusRequest defines the Msg/SubmitWithdrawStatus request type. 35 | message MsgSubmitWithdrawStatusRequest { 36 | string sender = 1; 37 | string txid = 2; 38 | SigningStatus status = 3; 39 | } 40 | 41 | // MsgSubmitWithdrawStatusResponse defines the Msg/SubmitWithdrawStatus response type. 42 | message MsgSubmitWithdrawStatusResponse { 43 | } 44 | 45 | // MsgBlockHeaderRequest defines the Msg/SubmitBlockHeaders request type. 46 | message MsgSubmitBlockHeaderRequest { 47 | string sender = 1; 48 | repeated BlockHeader block_headers = 2; 49 | } 50 | 51 | // MsgSubmitBlockHeadersResponse defines the Msg/SubmitBlockHeaders response type. 52 | message MsgSubmitBlockHeadersResponse { 53 | } 54 | 55 | // MsgSubmitTransactionRequest defines the Msg/SubmitTransaction request type. 56 | message MsgSubmitDepositTransactionRequest { 57 | // this is relayer address who submit the bitcoin transaction to the side chain 58 | string sender = 1; 59 | string blockhash = 2; 60 | // the tx bytes in base64 format 61 | // used for parsing the sender of the transaction 62 | string prev_tx_bytes = 3; 63 | // the tx bytes in base64 format 64 | string tx_bytes = 4; 65 | repeated string proof = 5; 66 | } 67 | 68 | // MsgSubmitTransactionResponse defines the Msg/SubmitTransaction response type. 69 | message MsgSubmitDepositTransactionResponse { 70 | } 71 | 72 | // MsgSubmitTransactionRequest defines the Msg/SubmitTransaction request type. 73 | message MsgSubmitWithdrawTransactionRequest { 74 | // this is relayer address who submit the bitcoin transaction to the side chain 75 | string sender = 1; 76 | string blockhash = 2; 77 | // the tx bytes in base64 format 78 | string tx_bytes = 4; 79 | repeated string proof = 5; 80 | } 81 | 82 | // MsgSubmitTransactionResponse defines the Msg/SubmitTransaction response type. 83 | message MsgSubmitWithdrawTransactionResponse { 84 | } 85 | 86 | // Msg defines the MsgUpdateSender service. 87 | message MsgUpdateQualifiedRelayersRequest { 88 | string sender = 1; 89 | // update senders who can send block headers to the side chain 90 | repeated string relayers = 2; 91 | } 92 | 93 | // MsgUpdateSenderResponse defines the Msg/UpdateSender response type. 94 | message MsgUpdateQualifiedRelayersResponse { 95 | } 96 | 97 | // MsgWithdrawBitcoinRequest defines the Msg/WithdrawBitcoin request type. 98 | message MsgWithdrawBitcoinRequest { 99 | string sender = 1; 100 | // withdraw amount in satoshi, etc: 100000000sat = 1btc 101 | string amount = 2; 102 | // fee rate in sats/vB 103 | string fee_rate = 3; 104 | } 105 | 106 | // MsgWithdrawBitcoinResponse defines the Msg/WithdrawBitcoin response type. 107 | message MsgWithdrawBitcoinResponse { 108 | } 109 | 110 | // MsgSubmitWithdrawSignaturesRequest defines the Msg/SubmitWithdrawSignatures request type. 111 | message MsgSubmitWithdrawSignaturesRequest { 112 | string sender = 1; 113 | string txid = 2; 114 | string psbt = 3; 115 | } 116 | 117 | // MsgSubmitWithdrawSignaturesResponse defines the Msg/SubmitWithdrawSignatures response type. 118 | message MsgSubmitWithdrawSignaturesResponse { 119 | } 120 | 121 | // MsgUpdateParamsRequest is the Msg/UpdateParams request type. 122 | // 123 | // Since: cosmos-sdk 0.47 124 | message MsgUpdateParamsRequest { 125 | // authority is the address that controls the module (defaults to x/gov unless overwritten). 126 | string authority = 1; 127 | 128 | // params defines the x/btcbridge parameters to be updated. 129 | // 130 | // NOTE: All parameters must be supplied. 131 | Params params = 2 [(gogoproto.nullable) = false]; 132 | } 133 | 134 | // MsgUpdateParamsResponse defines the Msg/UpdateParams response type. 135 | // 136 | // Since: cosmos-sdk 0.47 137 | message MsgUpdateParamsResponse {} 138 | -------------------------------------------------------------------------------- /x/btcbridge/types/runes.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "lukechampine.com/uint128" 9 | 10 | "github.com/btcsuite/btcd/txscript" 11 | "github.com/btcsuite/btcd/wire" 12 | ) 13 | 14 | const ( 15 | // runes protocol name 16 | RunesProtocolName = "runes" 17 | 18 | // runes magic number 19 | MagicNumber = txscript.OP_13 20 | 21 | // tag indicating that the following are edicts 22 | TagBody = 0 23 | 24 | // the number of components of each edict 25 | EdictLen = 4 26 | 27 | // sats in the runes output by default 28 | RunesOutValue = 546 29 | ) 30 | 31 | // ParseRunes parses the potential runes protocol from the given tx; 32 | // If no OP_RETURN found, no error returned 33 | // Only support edicts for now 34 | func ParseRunes(tx *wire.MsgTx) ([]*Edict, error) { 35 | for _, out := range tx.TxOut { 36 | tokenizer := txscript.MakeScriptTokenizer(0, out.PkScript) 37 | if !tokenizer.Next() || tokenizer.Err() != nil || tokenizer.Opcode() != txscript.OP_RETURN { 38 | continue 39 | } 40 | 41 | if !tokenizer.Next() || tokenizer.Err() != nil || tokenizer.Opcode() != MagicNumber { 42 | continue 43 | } 44 | 45 | var payload []byte 46 | 47 | for tokenizer.Next() { 48 | if txscript.IsSmallInt(tokenizer.Opcode()) || tokenizer.Opcode() <= txscript.OP_PUSHDATA4 { 49 | payload = append(payload, tokenizer.Data()...) 50 | } else { 51 | return nil, ErrInvalidRunes 52 | } 53 | } 54 | 55 | if tokenizer.Err() != nil { 56 | return nil, ErrInvalidRunes 57 | } 58 | 59 | return ParseEdicts(tx, payload) 60 | } 61 | 62 | return nil, nil 63 | } 64 | 65 | // ParseEdicts parses the given payload to a set of edicts 66 | func ParseEdicts(tx *wire.MsgTx, payload []byte) ([]*Edict, error) { 67 | integers, err := DecodeVec(payload) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | if len(integers) < EdictLen+1 || len(integers[1:])%EdictLen != 0 || !integers[0].Equals(uint128.From64(TagBody)) { 73 | return nil, ErrInvalidRunes 74 | } 75 | 76 | integers = integers[1:] 77 | 78 | edicts := make([]*Edict, 0) 79 | 80 | for i := 0; i < len(integers); i = i + 4 { 81 | output := uint32(integers[i+3].Big().Uint64()) 82 | if output > uint32(len(tx.TxOut)) { 83 | return nil, ErrInvalidRunes 84 | } 85 | 86 | edict := Edict{ 87 | Id: &RuneId{ 88 | Block: integers[i].Big().Uint64(), 89 | Tx: uint32(integers[i+1].Big().Uint64()), 90 | }, 91 | Amount: integers[i+2].String(), 92 | Output: output, 93 | } 94 | 95 | edicts = append(edicts, &edict) 96 | } 97 | 98 | return edicts, nil 99 | } 100 | 101 | // ParseEdict parses the given payload to edict 102 | func ParseEdict(payload []byte) (*Edict, error) { 103 | integers, err := DecodeVec(payload) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | if len(integers) != EdictLen+1 && !integers[0].Equals(uint128.From64(TagBody)) { 109 | return nil, ErrInvalidRunes 110 | } 111 | 112 | return &Edict{ 113 | Id: &RuneId{ 114 | Block: integers[1].Big().Uint64(), 115 | Tx: uint32(integers[2].Big().Uint64()), 116 | }, 117 | Amount: integers[3].String(), 118 | Output: uint32(integers[4].Big().Uint64()), 119 | }, nil 120 | } 121 | 122 | // BuildEdictScript builds the edict script 123 | func BuildEdictScript(runeId string, amount uint128.Uint128, output uint32) ([]byte, error) { 124 | var id RuneId 125 | id.MustUnmarshal([]byte(runeId)) 126 | 127 | edict := Edict{ 128 | Id: &id, 129 | Amount: amount.String(), 130 | Output: output, 131 | } 132 | 133 | payload := []byte{TagBody} 134 | payload = append(payload, edict.MustMarshalLEB128()...) 135 | 136 | scriptBuilder := txscript.NewScriptBuilder() 137 | scriptBuilder.AddOp(txscript.OP_RETURN).AddOp(MagicNumber).AddData(payload) 138 | 139 | return scriptBuilder.Script() 140 | } 141 | 142 | func (id *RuneId) ToString() string { 143 | return fmt.Sprintf("%d:%d", id.Block, id.Tx) 144 | } 145 | 146 | func (id *RuneId) FromString(idStr string) error { 147 | parts := strings.Split(idStr, ":") 148 | if len(parts) != 2 { 149 | return ErrInvalidRuneId 150 | } 151 | 152 | block, err := strconv.ParseUint(parts[0], 10, 64) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | tx, err := strconv.ParseUint(parts[1], 10, 32) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | id.Block = block 163 | id.Tx = uint32(tx) 164 | 165 | return nil 166 | } 167 | 168 | func (id *RuneId) MustUnmarshal(bz []byte) { 169 | err := id.FromString(string(bz)) 170 | if err != nil { 171 | panic(err) 172 | } 173 | } 174 | 175 | // Denom returns the corresponding denom for the runes voucher token 176 | func (id *RuneId) Denom() string { 177 | return fmt.Sprintf("%s/%s", RunesProtocolName, id.ToString()) 178 | } 179 | 180 | // FromDenom converts the denom to the rune id 181 | func (id *RuneId) FromDenom(denom string) { 182 | idStr := strings.TrimPrefix(denom, fmt.Sprintf("%s/", RunesProtocolName)) 183 | 184 | id.MustUnmarshal([]byte(idStr)) 185 | } 186 | 187 | func (e *Edict) MustMarshalLEB128() []byte { 188 | amount := RuneAmountFromString(e.Amount) 189 | 190 | payload := make([]byte, 0) 191 | 192 | payload = append(payload, EncodeUint64(e.Id.Block)...) 193 | payload = append(payload, EncodeUint32(e.Id.Tx)...) 194 | payload = append(payload, EncodeUint128(&amount)...) 195 | payload = append(payload, EncodeUint32(e.Output)...) 196 | 197 | return payload 198 | } 199 | 200 | // RuneAmountFromString converts the given string to the rune amount 201 | // Panic if any error occurred 202 | func RuneAmountFromString(str string) uint128.Uint128 { 203 | amount, err := uint128.FromString(str) 204 | if err != nil { 205 | panic(err) 206 | } 207 | 208 | return amount 209 | } 210 | -------------------------------------------------------------------------------- /x/btcbridge/client/cli/tx.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/btcsuite/btcd/btcutil/psbt" 11 | "github.com/spf13/cobra" 12 | 13 | "github.com/cosmos/cosmos-sdk/client" 14 | "github.com/cosmos/cosmos-sdk/client/flags" 15 | "github.com/cosmos/cosmos-sdk/client/tx" 16 | sdk "github.com/cosmos/cosmos-sdk/types" 17 | 18 | // "github.com/cosmos/cosmos-sdk/client/flags" 19 | "github.com/sideprotocol/side/x/btcbridge/types" 20 | ) 21 | 22 | var DefaultRelativePacketTimeoutTimestamp = uint64((time.Duration(10) * time.Minute).Nanoseconds()) 23 | 24 | // const ( 25 | // flagPacketTimeoutTimestamp = "packet-timeout-timestamp" 26 | // listSeparator = "," 27 | // ) 28 | 29 | // GetTxCmd returns the transaction commands for this module 30 | func GetTxCmd() *cobra.Command { 31 | cmd := &cobra.Command{ 32 | Use: types.ModuleName, 33 | Short: fmt.Sprintf("%s transactions subcommands", types.ModuleName), 34 | DisableFlagParsing: true, 35 | SuggestionsMinimumDistance: 2, 36 | RunE: client.ValidateCmd, 37 | } 38 | 39 | cmd.AddCommand(CmdSubmitBlocks()) 40 | cmd.AddCommand(CmdUpdateSenders()) 41 | cmd.AddCommand(CmdWithdrawBitcoin()) 42 | cmd.AddCommand(CmdSubmitWithdrawSignatures()) 43 | 44 | return cmd 45 | } 46 | 47 | func CmdSubmitBlocks() *cobra.Command { 48 | cmd := &cobra.Command{ 49 | Use: "submit-blocks [file-path-to-block-headers.json]", 50 | Short: "Submit Bitcoin block headers to the chain", 51 | Args: cobra.ExactArgs(1), 52 | RunE: func(cmd *cobra.Command, args []string) (err error) { 53 | clientCtx, err := client.GetClientTxContext(cmd) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // read the block headers from the file 59 | blockHeaders, err := readBlockHeadersFromFile(args[0]) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | msg := types.NewMsgSubmitBlockHeaderRequest( 65 | clientCtx.GetFromAddress().String(), 66 | blockHeaders, 67 | ) 68 | if err := msg.ValidateBasic(); err != nil { 69 | return err 70 | } 71 | return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) 72 | }, 73 | } 74 | 75 | flags.AddTxFlagsToCmd(cmd) 76 | 77 | return cmd 78 | } 79 | 80 | // Update Authorized Senders 81 | func CmdUpdateSenders() *cobra.Command { 82 | cmd := &cobra.Command{ 83 | Use: "update-senders [senders]", 84 | Short: "Update authorized senders", 85 | Args: cobra.ExactArgs(1), 86 | RunE: func(cmd *cobra.Command, args []string) (err error) { 87 | clientCtx, err := client.GetClientTxContext(cmd) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | // split the senders from args[0] 93 | senders := strings.Split(args[0], ",") 94 | if len(senders) == 0 { 95 | return fmt.Errorf("senders can not be empty") 96 | } 97 | 98 | msg := types.NewMsgUpdateSendersRequest( 99 | clientCtx.GetFromAddress().String(), 100 | senders, 101 | ) 102 | if err := msg.ValidateBasic(); err != nil { 103 | return err 104 | } 105 | return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) 106 | }, 107 | } 108 | 109 | flags.AddTxFlagsToCmd(cmd) 110 | 111 | return cmd 112 | } 113 | 114 | // Withdraw Bitcoin 115 | func CmdWithdrawBitcoin() *cobra.Command { 116 | cmd := &cobra.Command{ 117 | Use: "withdraw [amount] [fee-rate]", 118 | Short: "Withdraw bitcoin to the given sender", 119 | Args: cobra.ExactArgs(2), 120 | RunE: func(cmd *cobra.Command, args []string) (err error) { 121 | clientCtx, err := client.GetClientTxContext(cmd) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | _, err = sdk.ParseCoinsNormalized(args[0]) 127 | if err != nil { 128 | return fmt.Errorf("invalid amount") 129 | } 130 | 131 | msg := types.NewMsgWithdrawBitcoinRequest( 132 | clientCtx.GetFromAddress().String(), 133 | args[0], 134 | args[1], 135 | ) 136 | 137 | if err := msg.ValidateBasic(); err != nil { 138 | return err 139 | } 140 | 141 | return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) 142 | }, 143 | } 144 | 145 | flags.AddTxFlagsToCmd(cmd) 146 | 147 | return cmd 148 | } 149 | 150 | func CmdSubmitWithdrawSignatures() *cobra.Command { 151 | cmd := &cobra.Command{ 152 | Use: "submit-signature [psbt]", 153 | Short: "Submit signed withdrawal psbt", 154 | Args: cobra.ExactArgs(1), 155 | RunE: func(cmd *cobra.Command, args []string) (err error) { 156 | clientCtx, err := client.GetClientTxContext(cmd) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | p, err := psbt.NewFromRawBytes(strings.NewReader(args[0]), true) 162 | if err != nil { 163 | return fmt.Errorf("invalid psbt") 164 | } 165 | 166 | signedTx, err := psbt.Extract(p) 167 | if err != nil { 168 | return fmt.Errorf("failed to extract tx from psbt") 169 | } 170 | 171 | msg := types.NewMsgSubmitWithdrawSignaturesRequest( 172 | clientCtx.GetFromAddress().String(), 173 | signedTx.TxHash().String(), 174 | args[0], 175 | ) 176 | 177 | if err := msg.ValidateBasic(); err != nil { 178 | return err 179 | } 180 | 181 | return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) 182 | }, 183 | } 184 | 185 | flags.AddTxFlagsToCmd(cmd) 186 | 187 | return cmd 188 | } 189 | 190 | // readBlockHeadersFromFile reads the block headers from the file 191 | func readBlockHeadersFromFile(filePath string) ([]*types.BlockHeader, error) { 192 | // read the file 193 | file, err := os.Open(filePath) 194 | if err != nil { 195 | return nil, err 196 | } 197 | defer file.Close() 198 | 199 | // read the block headers from the file 200 | var blockHeaders []*types.BlockHeader 201 | decoder := json.NewDecoder(file) 202 | if err := decoder.Decode(&blockHeaders); err != nil { 203 | return nil, err 204 | } 205 | return blockHeaders, nil 206 | } 207 | -------------------------------------------------------------------------------- /x/btcbridge/keeper/keeper.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/btcsuite/btcd/blockchain" 7 | "github.com/cometbft/cometbft/libs/log" 8 | "github.com/cosmos/cosmos-sdk/codec" 9 | storetypes "github.com/cosmos/cosmos-sdk/store/types" 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | "github.com/sideprotocol/side/x/btcbridge/types" 12 | ) 13 | 14 | type ( 15 | Keeper struct { 16 | BaseUTXOKeeper 17 | 18 | cdc codec.BinaryCodec 19 | storeKey storetypes.StoreKey 20 | memKey storetypes.StoreKey 21 | 22 | authority string 23 | 24 | bankKeeper types.BankKeeper 25 | } 26 | ) 27 | 28 | func NewKeeper( 29 | cdc codec.BinaryCodec, 30 | storeKey, 31 | memKey storetypes.StoreKey, 32 | authority string, 33 | bankKeeper types.BankKeeper, 34 | ) *Keeper { 35 | return &Keeper{ 36 | cdc: cdc, 37 | storeKey: storeKey, 38 | memKey: memKey, 39 | authority: authority, 40 | bankKeeper: bankKeeper, 41 | BaseUTXOKeeper: *NewBaseUTXOKeeper(cdc, storeKey), 42 | } 43 | } 44 | 45 | func (k Keeper) Logger(ctx sdk.Context) log.Logger { 46 | return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) 47 | } 48 | 49 | func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { 50 | store := ctx.KVStore(k.storeKey) 51 | bz := k.cdc.MustMarshal(¶ms) 52 | store.Set(types.ParamsStoreKey, bz) 53 | } 54 | 55 | func (k Keeper) GetParams(ctx sdk.Context) types.Params { 56 | store := ctx.KVStore(k.storeKey) 57 | var params types.Params 58 | bz := store.Get(types.ParamsStoreKey) 59 | k.cdc.MustUnmarshal(bz, ¶ms) 60 | return params 61 | } 62 | 63 | func (k Keeper) GetBestBlockHeader(ctx sdk.Context) *types.BlockHeader { 64 | store := ctx.KVStore(k.storeKey) 65 | var blockHeader types.BlockHeader 66 | bz := store.Get(types.BtcBestBlockHeaderKey) 67 | k.cdc.MustUnmarshal(bz, &blockHeader) 68 | return &blockHeader 69 | } 70 | 71 | func (k Keeper) SetBestBlockHeader(ctx sdk.Context, header *types.BlockHeader) { 72 | store := ctx.KVStore(k.storeKey) 73 | bz := k.cdc.MustMarshal(header) 74 | store.Set(types.BtcBestBlockHeaderKey, bz) 75 | } 76 | 77 | func (k Keeper) SetBlockHeaders(ctx sdk.Context, blockHeader []*types.BlockHeader) error { 78 | store := ctx.KVStore(k.storeKey) 79 | // check if the previous block header exists 80 | best := k.GetBestBlockHeader(ctx) 81 | for _, header := range blockHeader { 82 | 83 | // check the block header sanity 84 | err := blockchain.CheckBlockHeaderSanity( 85 | HeaderConvert(header), 86 | sdk.GetConfig().GetBtcChainCfg().PowLimit, 87 | blockchain.NewMedianTime(), 88 | blockchain.BFNone, 89 | ) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | // check whether it's next block header or not 95 | if best.Hash != header.PreviousBlockHash { 96 | // check if the block header already exists 97 | // if exists, then it is a forked block header 98 | if !store.Has(types.BtcBlockHeaderHeightKey(header.Height)) { 99 | return types.ErrInvalidHeader 100 | } 101 | 102 | // a forked block header is detected 103 | // check if the new block header has more work than the old one 104 | oldNode := k.GetBlockHeaderByHeight(ctx, header.Height) 105 | worksOld := blockchain.CalcWork(BitsToTargetUint32(oldNode.Bits)) 106 | worksNew := blockchain.CalcWork(BitsToTargetUint32(header.Bits)) 107 | if worksNew.Cmp(worksOld) <= 0 { 108 | return types.ErrForkedBlockHeader 109 | } 110 | 111 | // remove the block headers after the forked block header 112 | // and consider the forked block header as the best block header 113 | for i := header.Height; i <= best.Height; i++ { 114 | ctx.Logger().Info("Removing block header: ", i) 115 | thash := k.GetBlockHashByHeight(ctx, i) 116 | store.Delete(types.BtcBlockHeaderHashKey(thash)) 117 | store.Delete(types.BtcBlockHeaderHeightKey(i)) 118 | } 119 | } 120 | 121 | // store the block header 122 | bz := k.cdc.MustMarshal(header) 123 | store.Set(types.BtcBlockHeaderHashKey(header.Hash), bz) 124 | // store the height to hash mapping 125 | store.Set(types.BtcBlockHeaderHeightKey(header.Height), []byte(header.Hash)) 126 | // update the best block header 127 | best = header 128 | } 129 | 130 | if len(blockHeader) > 0 { 131 | // set the best block header 132 | k.SetBestBlockHeader(ctx, best) 133 | } 134 | 135 | return nil 136 | } 137 | 138 | func (k Keeper) GetBlockHeader(ctx sdk.Context, hash string) *types.BlockHeader { 139 | store := ctx.KVStore(k.storeKey) 140 | var blockHeader types.BlockHeader 141 | bz := store.Get(types.BtcBlockHeaderHashKey(hash)) 142 | k.cdc.MustUnmarshal(bz, &blockHeader) 143 | return &blockHeader 144 | } 145 | 146 | func (k Keeper) GetBlockHashByHeight(ctx sdk.Context, height uint64) string { 147 | store := ctx.KVStore(k.storeKey) 148 | hash := store.Get(types.BtcBlockHeaderHeightKey(height)) 149 | return string(hash) 150 | } 151 | 152 | func (k Keeper) GetBlockHeaderByHeight(ctx sdk.Context, height uint64) *types.BlockHeader { 153 | store := ctx.KVStore(k.storeKey) 154 | hash := store.Get(types.BtcBlockHeaderHeightKey(height)) 155 | return k.GetBlockHeader(ctx, string(hash)) 156 | } 157 | 158 | // GetAllBlockHeaders returns all block headers 159 | func (k Keeper) GetAllBlockHeaders(ctx sdk.Context) []*types.BlockHeader { 160 | var headers []*types.BlockHeader 161 | k.IterateBlockHeaders(ctx, func(header types.BlockHeader) (stop bool) { 162 | headers = append(headers, &header) 163 | return false 164 | }) 165 | return headers 166 | } 167 | 168 | // IterateBlockHeaders iterates through all block headers 169 | func (k Keeper) IterateBlockHeaders(ctx sdk.Context, process func(header types.BlockHeader) (stop bool)) { 170 | store := ctx.KVStore(k.storeKey) 171 | iterator := sdk.KVStorePrefixIterator(store, types.BtcBlockHeaderHashPrefix) 172 | defer iterator.Close() 173 | for ; iterator.Valid(); iterator.Next() { 174 | var header types.BlockHeader 175 | k.cdc.MustUnmarshal(iterator.Value(), &header) 176 | if process(header) { 177 | break 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /proto/side/btcbridge/query.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package side.btcbridge; 3 | 4 | import "gogoproto/gogo.proto"; 5 | import "google/api/annotations.proto"; 6 | import "cosmos/base/query/v1beta1/pagination.proto"; 7 | import "side/btcbridge/params.proto"; 8 | import "side/btcbridge/bitcoin.proto"; 9 | 10 | option go_package = "github.com/sideprotocol/side/x/btcbridge/types"; 11 | 12 | // Query defines the gRPC querier service. 13 | service Query { 14 | // Parameters queries the parameters of the module. 15 | rpc QueryParams(QueryParamsRequest) returns (QueryParamsResponse) { 16 | option (google.api.http).get = "/side/btcbridge/params"; 17 | } 18 | // ChainTip queries the chain tip of the module. 19 | rpc QueryChainTip(QueryChainTipRequest) returns (QueryChainTipResponse) { 20 | option (google.api.http).get = "/side/btcbridge/tip"; 21 | } 22 | // BlockHeaderByHeight queries the block header by height. 23 | rpc QueryBlockHeaderByHeight(QueryBlockHeaderByHeightRequest) returns (QueryBlockHeaderByHeightResponse) { 24 | option (google.api.http).get = "/side/btcbridge/height/{height}"; 25 | } 26 | // BlockHeaderByHash queries the block header by hash. 27 | rpc QueryBlockHeaderByHash(QueryBlockHeaderByHashRequest) returns (QueryBlockHeaderByHashResponse) { 28 | option (google.api.http).get = "/side/btcbridge/hash/{hash}"; 29 | } 30 | // QuerySigningRequest queries the request to sign. 31 | rpc QuerySigningRequest(QuerySigningRequestRequest) returns (QuerySigningRequestResponse) { 32 | option (google.api.http).get = "/side/btcbridge/signing/request"; 33 | } 34 | // QuerySigningRequestByAddress queries the signing request by the given address. 35 | rpc QuerySigningRequestByAddress(QuerySigningRequestByAddressRequest) returns (QuerySigningRequestByAddressResponse) { 36 | option (google.api.http).get = "/side/btcbridge/signing/request/address/{address}"; 37 | } 38 | // QuerySigningRequestByTxHash queries the signing request by the given tx hash. 39 | rpc QuerySigningRequestByTxHash(QuerySigningRequestByTxHashRequest) returns (QuerySigningRequestByTxHashResponse) { 40 | option (google.api.http).get = "/side/btcbridge/signing/request/tx/{txid}"; 41 | } 42 | // UTXOs queries all utxos. 43 | rpc QueryUTXOs(QueryUTXOsRequest) returns (QueryUTXOsResponse) { 44 | option (google.api.http).get = "/side/btcbridge/utxos"; 45 | } 46 | // UTXOsByAddress queries the utxos of the given address. 47 | rpc QueryUTXOsByAddress(QueryUTXOsByAddressRequest) returns (QueryUTXOsByAddressResponse) { 48 | option (google.api.http).get = "/side/btcbridge/utxos/{address}"; 49 | } 50 | } 51 | 52 | // QuerySigningRequestRequest is request type for the Query/SigningRequest RPC method. 53 | message QuerySigningRequestRequest { 54 | SigningStatus status = 1; 55 | cosmos.base.query.v1beta1.PageResponse pagination = 2; 56 | } 57 | 58 | // QuerySigningRequestResponse is response type for the Query/SigningRequest RPC method. 59 | message QuerySigningRequestResponse { 60 | repeated BitcoinSigningRequest requests = 1; 61 | cosmos.base.query.v1beta1.PageResponse pagination = 2; 62 | } 63 | 64 | // QuerySigningRequestByAddressRequest is request type for the Query/SigningRequestByAddress RPC method. 65 | message QuerySigningRequestByAddressRequest { 66 | string address = 1; 67 | cosmos.base.query.v1beta1.PageResponse pagination = 2; 68 | } 69 | 70 | // QuerySigningRequestByAddressResponse is response type for the Query/SigningRequestByAddress RPC method. 71 | message QuerySigningRequestByAddressResponse { 72 | repeated BitcoinSigningRequest requests = 1; 73 | cosmos.base.query.v1beta1.PageResponse pagination = 2; 74 | } 75 | 76 | // QuerySigningRequestByTxHashRequest is request type for the Query/SigningRequestByTxHash RPC method. 77 | message QuerySigningRequestByTxHashRequest { 78 | string txid = 1; 79 | } 80 | 81 | // QuerySigningRequestByTxHashResponse is response type for the Query/SigningRequestByTxHash RPC method. 82 | message QuerySigningRequestByTxHashResponse { 83 | BitcoinSigningRequest request = 1; 84 | } 85 | 86 | // QueryParamsRequest is request type for the Query/Params RPC method. 87 | message QueryParamsRequest {} 88 | 89 | // QueryParamsResponse is response type for the Query/Params RPC method. 90 | message QueryParamsResponse { 91 | // params holds all the parameters of this module. 92 | Params params = 1 [(gogoproto.nullable) = false]; 93 | } 94 | 95 | // QueryChainTipRequest is request type for the Query/ChainTip RPC method. 96 | message QueryChainTipRequest {} 97 | 98 | // QueryChainTipResponse is response type for the Query/ChainTip RPC method. 99 | message QueryChainTipResponse { 100 | string hash = 1; 101 | uint64 height = 2; 102 | } 103 | 104 | // QueryBlockHeaderByHeightRequest is the request type for the Query/BlockHeaderByHeight RPC method. 105 | message QueryBlockHeaderByHeightRequest { 106 | uint64 height = 1; 107 | } 108 | 109 | // QueryBlockHeaderByHeightResponse is the response type for the Query/BlockHeaderByHeight RPC method. 110 | message QueryBlockHeaderByHeightResponse { 111 | BlockHeader block_header = 1; 112 | } 113 | 114 | // QueryBlockHeaderByHashRequest is the request type for the Query/BlockHeaderByHash RPC method. 115 | message QueryBlockHeaderByHashRequest { 116 | string hash = 1; 117 | } 118 | 119 | // QueryBlockHeaderByHashResponse is the response type for the Query/BlockHeaderByHash RPC method. 120 | message QueryBlockHeaderByHashResponse { 121 | BlockHeader block_header = 1; 122 | } 123 | 124 | // QueryUTXOsRequest is the request type for the Query/UTXOs RPC method. 125 | message QueryUTXOsRequest {} 126 | 127 | // QueryUTXOsResponse is the response type for the Query/UTXOs RPC method. 128 | message QueryUTXOsResponse { 129 | repeated UTXO utxos = 1; 130 | } 131 | 132 | // QueryUTXOsByAddressRequest is the request type for the Query/UTXOsByAddress RPC method. 133 | message QueryUTXOsByAddressRequest { 134 | string address = 1; 135 | } 136 | 137 | // QueryUTXOsByAddressResponse is the response type for the Query/UTXOsByAddress RPC method. 138 | message QueryUTXOsByAddressResponse { 139 | repeated UTXO utxos = 1; 140 | } 141 | -------------------------------------------------------------------------------- /x/btcbridge/module.go: -------------------------------------------------------------------------------- 1 | package btcbridge 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | // this line is used by starport scaffolding # 1 9 | 10 | "github.com/grpc-ecosystem/grpc-gateway/runtime" 11 | "github.com/spf13/cobra" 12 | 13 | abci "github.com/cometbft/cometbft/abci/types" 14 | 15 | "github.com/cosmos/cosmos-sdk/client" 16 | "github.com/cosmos/cosmos-sdk/codec" 17 | cdctypes "github.com/cosmos/cosmos-sdk/codec/types" 18 | sdk "github.com/cosmos/cosmos-sdk/types" 19 | "github.com/cosmos/cosmos-sdk/types/module" 20 | "github.com/sideprotocol/side/x/btcbridge/client/cli" 21 | "github.com/sideprotocol/side/x/btcbridge/keeper" 22 | "github.com/sideprotocol/side/x/btcbridge/types" 23 | ) 24 | 25 | var ( 26 | _ module.AppModule = AppModule{} 27 | _ module.AppModuleBasic = AppModuleBasic{} 28 | ) 29 | 30 | // ---------------------------------------------------------------------------- 31 | // AppModuleBasic 32 | // ---------------------------------------------------------------------------- 33 | 34 | // AppModuleBasic implements the AppModuleBasic interface that defines the independent methods a Cosmos SDK module needs to implement. 35 | type AppModuleBasic struct { 36 | cdc codec.BinaryCodec 37 | } 38 | 39 | func NewAppModuleBasic(cdc codec.BinaryCodec) AppModuleBasic { 40 | return AppModuleBasic{cdc: cdc} 41 | } 42 | 43 | // Name returns the name of the module as a string 44 | func (AppModuleBasic) Name() string { 45 | return types.ModuleName 46 | } 47 | 48 | // RegisterLegacyAminoCodec registers the amino codec for the module, which is used to marshal and unmarshal structs to/from []byte in order to persist them in the module's KVStore 49 | func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { 50 | types.RegisterCodec(cdc) 51 | } 52 | 53 | // RegisterInterfaces registers a module's interface types and their concrete implementations as proto.Message 54 | func (a AppModuleBasic) RegisterInterfaces(reg cdctypes.InterfaceRegistry) { 55 | types.RegisterInterfaces(reg) 56 | } 57 | 58 | // DefaultGenesis returns a default GenesisState for the module, marshaled to json.RawMessage. The default GenesisState need to be defined by the module developer and is primarily used for testing 59 | func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { 60 | return cdc.MustMarshalJSON(types.DefaultGenesis()) 61 | } 62 | 63 | // ValidateGenesis used to validate the GenesisState, given in its json.RawMessage form 64 | func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error { 65 | var genState types.GenesisState 66 | if err := cdc.UnmarshalJSON(bz, &genState); err != nil { 67 | return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) 68 | } 69 | return genState.Validate() 70 | } 71 | 72 | // RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module 73 | func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { 74 | if err := types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)); err != nil { 75 | panic(err) 76 | } 77 | } 78 | 79 | // GetTxCmd returns the root Tx command for the module. The subcommands of this root command are used by end-users to generate new transactions containing messages defined in the module 80 | func (a AppModuleBasic) GetTxCmd() *cobra.Command { 81 | return cli.GetTxCmd() 82 | } 83 | 84 | // GetQueryCmd returns the root query command for the module. The subcommands of this root command are used by end-users to generate new queries to the subset of the state defined by the module 85 | func (AppModuleBasic) GetQueryCmd() *cobra.Command { 86 | return cli.GetQueryCmd(types.StoreKey) 87 | } 88 | 89 | // ---------------------------------------------------------------------------- 90 | // AppModule 91 | // ---------------------------------------------------------------------------- 92 | 93 | // AppModule implements the AppModule interface that defines the inter-dependent methods that modules need to implement 94 | type AppModule struct { 95 | AppModuleBasic 96 | 97 | keeper keeper.Keeper 98 | } 99 | 100 | func NewAppModule( 101 | cdc codec.Codec, 102 | keeper keeper.Keeper, 103 | ) AppModule { 104 | return AppModule{ 105 | AppModuleBasic: NewAppModuleBasic(cdc), 106 | keeper: keeper, 107 | } 108 | } 109 | 110 | // RegisterServices registers a gRPC query service to respond to the module-specific gRPC queries 111 | func (am AppModule) RegisterServices(cfg module.Configurator) { 112 | types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) 113 | types.RegisterQueryServer(cfg.QueryServer(), am.keeper) 114 | } 115 | 116 | // RegisterInvariants registers the invariants of the module. If an invariant deviates from its predicted value, the InvariantRegistry triggers appropriate logic (most often the chain will be halted) 117 | func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} 118 | 119 | // InitGenesis performs the module's genesis initialization. It returns no validator updates. 120 | func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, gs json.RawMessage) []abci.ValidatorUpdate { 121 | var genState types.GenesisState 122 | // Initialize global index to index in genesis state 123 | cdc.MustUnmarshalJSON(gs, &genState) 124 | 125 | InitGenesis(ctx, am.keeper, genState) 126 | 127 | return []abci.ValidatorUpdate{} 128 | } 129 | 130 | // ExportGenesis returns the module's exported genesis state as raw JSON bytes. 131 | func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { 132 | genState := ExportGenesis(ctx, am.keeper) 133 | return cdc.MustMarshalJSON(genState) 134 | } 135 | 136 | // ConsensusVersion is a sequence number for state-breaking change of the module. It should be incremented on each consensus-breaking change introduced by the module. To avoid wrong/empty versions, the initial version should be set to 1 137 | func (AppModule) ConsensusVersion() uint64 { return 1 } 138 | 139 | // BeginBlock contains the logic that is automatically triggered at the beginning of each block 140 | func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} 141 | 142 | // EndBlock contains the logic that is automatically triggered at the end of each block 143 | func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { 144 | return []abci.ValidatorUpdate{} 145 | } 146 | -------------------------------------------------------------------------------- /app/test_helper.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/cometbft/cometbft/crypto/secp256k1" 9 | tmproto "github.com/cometbft/cometbft/proto/tendermint/types" 10 | tmtypes "github.com/cometbft/cometbft/types" 11 | "github.com/cosmos/cosmos-sdk/testutil/mock" 12 | "github.com/stretchr/testify/require" 13 | 14 | sdk "github.com/cosmos/cosmos-sdk/types" 15 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 16 | banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" 17 | stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" 18 | 19 | dbm "github.com/cometbft/cometbft-db" 20 | abci "github.com/cometbft/cometbft/abci/types" 21 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 22 | cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" 23 | 24 | "github.com/cometbft/cometbft/libs/log" 25 | simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" 26 | ) 27 | 28 | const ( 29 | DefaultBondDenom = "uside" 30 | USDC = "uusdc" 31 | WBTC = "wbtc" 32 | WDAI = "wdai" 33 | WUSDT = "wusdt" 34 | ) 35 | 36 | // Setup initializes a new App. A Nop logger is set in App. 37 | func Setup(t *testing.T) *App { 38 | t.Helper() 39 | 40 | privVal := mock.NewPV() 41 | pubKey, err := privVal.GetPubKey() 42 | require.NoError(t, err) 43 | 44 | // create validator set with single validator 45 | validator := tmtypes.NewValidator(pubKey, 1) 46 | valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{validator}) 47 | 48 | // generate genesis account 49 | senderPrivKey := secp256k1.GenPrivKey() 50 | acc := authtypes.NewBaseAccountWithAddress(senderPrivKey.PubKey().Address().Bytes()) 51 | balance := banktypes.Balance{ 52 | Address: acc.GetAddress().String(), 53 | Coins: sdk.NewCoins( 54 | sdk.NewCoin(DefaultBondDenom, sdk.NewInt(100000000000000)), 55 | sdk.NewCoin(USDC, sdk.NewInt(100000000000000)), 56 | ), 57 | } 58 | 59 | app := SetupWithGenesisValSet(t, valSet, []authtypes.GenesisAccount{acc}, balance) 60 | 61 | return app 62 | } 63 | 64 | func genesisStateWithValSet(t *testing.T, 65 | app *App, genesisState GenesisState, 66 | valSet *tmtypes.ValidatorSet, genAccs []authtypes.GenesisAccount, 67 | balances ...banktypes.Balance, 68 | ) GenesisState { 69 | // set genesis accounts 70 | 71 | authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), genAccs) 72 | genesisState[authtypes.ModuleName] = app.AppCodec().MustMarshalJSON(authGenesis) 73 | 74 | validators := make([]stakingtypes.Validator, 0, len(valSet.Validators)) 75 | delegations := make([]stakingtypes.Delegation, 0, len(valSet.Validators)) 76 | 77 | bondAmt := sdk.DefaultPowerReduction 78 | 79 | for _, val := range valSet.Validators { 80 | pk, err := cryptocodec.FromTmPubKeyInterface(val.PubKey) 81 | require.NoError(t, err) 82 | pkAny, err := codectypes.NewAnyWithValue(pk) 83 | require.NoError(t, err) 84 | validator := stakingtypes.Validator{ 85 | OperatorAddress: sdk.ValAddress(val.Address).String(), 86 | ConsensusPubkey: pkAny, 87 | Jailed: false, 88 | Status: stakingtypes.Bonded, 89 | Tokens: bondAmt, 90 | DelegatorShares: sdk.OneDec(), 91 | Description: stakingtypes.Description{}, 92 | UnbondingHeight: int64(0), 93 | UnbondingTime: time.Unix(0, 0).UTC(), 94 | Commission: stakingtypes.NewCommission(sdk.OneDec(), sdk.OneDec(), sdk.OneDec()), 95 | MinSelfDelegation: sdk.ZeroInt(), 96 | } 97 | validators = append(validators, validator) 98 | delegations = append(delegations, stakingtypes.NewDelegation(genAccs[0].GetAddress(), val.Address.Bytes(), sdk.OneDec())) 99 | 100 | } 101 | // set validators and delegations 102 | stakingParams := stakingtypes.DefaultParams() 103 | stakingParams.BondDenom = DefaultBondDenom 104 | stakingParams.MinCommissionRate = sdk.OneDec() 105 | stakingGenesis := stakingtypes.NewGenesisState(stakingParams, validators, delegations) 106 | genesisState[stakingtypes.ModuleName] = app.AppCodec().MustMarshalJSON(stakingGenesis) 107 | 108 | totalSupply := sdk.NewCoins() 109 | for _, b := range balances { 110 | // add genesis acc tokens to total supply 111 | totalSupply = totalSupply.Add(b.Coins...) 112 | } 113 | 114 | for range delegations { 115 | // add delegated tokens to total supply 116 | totalSupply = totalSupply.Add(sdk.NewCoin(DefaultBondDenom, bondAmt)) 117 | } 118 | 119 | // add bonded amount to bonded pool module account 120 | balances = append(balances, banktypes.Balance{ 121 | Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String(), 122 | Coins: sdk.Coins{sdk.NewCoin(DefaultBondDenom, bondAmt)}, 123 | }) 124 | 125 | // update total supply 126 | bankGenesis := banktypes.NewGenesisState(banktypes.DefaultGenesisState().Params, balances, totalSupply, []banktypes.Metadata{}, []banktypes.SendEnabled{}) 127 | genesisState[banktypes.ModuleName] = app.AppCodec().MustMarshalJSON(bankGenesis) 128 | 129 | return genesisState 130 | } 131 | 132 | // SetupWithGenesisValSet initializes a new App with a validator set and genesis accounts 133 | // that also act as delegators. For simplicity, each validator is bonded with a delegation 134 | // of one consensus engine unit (10^6) in the default token of the simapp from first genesis 135 | // account. A Nop logger is set in App. 136 | func SetupWithGenesisValSet(t *testing.T, valSet *tmtypes.ValidatorSet, genAccs []authtypes.GenesisAccount, balances ...banktypes.Balance) *App { 137 | t.Helper() 138 | 139 | app, genesisState := setup(true, 5) 140 | genesisState = genesisStateWithValSet(t, app, genesisState, valSet, genAccs, balances...) 141 | 142 | stateBytes, err := json.MarshalIndent(genesisState, "", " ") 143 | require.NoError(t, err) 144 | 145 | // init chain will set the validator set and initialize the genesis accounts 146 | app.InitChain( 147 | abci.RequestInitChain{ 148 | Validators: []abci.ValidatorUpdate{}, 149 | ConsensusParams: simtestutil.DefaultConsensusParams, 150 | AppStateBytes: stateBytes, 151 | }, 152 | ) 153 | 154 | // commit genesis changes 155 | app.Commit() 156 | app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{ 157 | Height: app.LastBlockHeight() + 1, 158 | AppHash: app.LastCommitID().Hash, 159 | ValidatorsHash: valSet.Hash(), 160 | NextValidatorsHash: valSet.Hash(), 161 | }}) 162 | 163 | return app 164 | } 165 | 166 | func setup(withGenesis bool, invCheckPeriod uint) (*App, GenesisState) { 167 | db := dbm.NewMemDB() 168 | encCdc := MakeEncodingConfig() 169 | appOptions := make(simtestutil.AppOptionsMap, 0) 170 | 171 | app := New( 172 | log.NewNopLogger(), 173 | db, 174 | nil, true, map[int64]bool{}, DefaultNodeHome, invCheckPeriod, encCdc, appOptions) 175 | if withGenesis { 176 | return app, NewDefaultGenesisState(encCdc.Marshaler) 177 | } 178 | return app, GenesisState{} 179 | } 180 | -------------------------------------------------------------------------------- /x/btcbridge/client/cli/query.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | 8 | // "strings" 9 | 10 | "github.com/btcsuite/btcd/chaincfg/chainhash" 11 | "github.com/sideprotocol/side/x/btcbridge/types" 12 | "github.com/spf13/cobra" 13 | 14 | "github.com/cosmos/cosmos-sdk/client" 15 | "github.com/cosmos/cosmos-sdk/client/flags" 16 | sdk "github.com/cosmos/cosmos-sdk/types" 17 | ) 18 | 19 | // GetQueryCmd returns the cli query commands for this module 20 | func GetQueryCmd(_ string) *cobra.Command { 21 | // Group yield queries under a subcommand 22 | cmd := &cobra.Command{ 23 | Use: types.ModuleName, 24 | Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName), 25 | DisableFlagParsing: true, 26 | SuggestionsMinimumDistance: 2, 27 | RunE: client.ValidateCmd, 28 | } 29 | 30 | cmd.AddCommand(CmdQueryParams()) 31 | cmd.AddCommand(CmdBestBlock()) 32 | cmd.AddCommand(CmdQueryBlock()) 33 | cmd.AddCommand(CmdQueryUTXOs()) 34 | cmd.AddCommand(CmdQuerySigningRequest()) 35 | // this line is used by starport scaffolding # 1 36 | 37 | return cmd 38 | } 39 | 40 | func CmdQueryParams() *cobra.Command { 41 | cmd := &cobra.Command{ 42 | Use: "params", 43 | Short: "shows the parameters of the module", 44 | Args: cobra.NoArgs, 45 | RunE: func(cmd *cobra.Command, args []string) error { 46 | clientCtx, err := client.GetClientQueryContext(cmd) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | queryClient := types.NewQueryClient(clientCtx) 52 | 53 | res, err := queryClient.QueryParams(cmd.Context(), &types.QueryParamsRequest{}) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | return clientCtx.PrintProto(res) 59 | }, 60 | } 61 | 62 | flags.AddQueryFlagsToCmd(cmd) 63 | 64 | return cmd 65 | } 66 | 67 | func CmdBestBlock() *cobra.Command { 68 | cmd := &cobra.Command{ 69 | Use: "best-block", 70 | Short: "shows the best block header of the light client", 71 | Args: cobra.NoArgs, 72 | RunE: func(cmd *cobra.Command, args []string) error { 73 | clientCtx, err := client.GetClientQueryContext(cmd) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | queryClient := types.NewQueryClient(clientCtx) 79 | 80 | res, err := queryClient.QueryChainTip(cmd.Context(), &types.QueryChainTipRequest{}) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | return clientCtx.PrintProto(res) 86 | }, 87 | } 88 | 89 | flags.AddQueryFlagsToCmd(cmd) 90 | 91 | return cmd 92 | } 93 | 94 | // CmdQueryBlock returns the command to query the heights of the light client 95 | func CmdQueryBlock() *cobra.Command { 96 | cmd := &cobra.Command{ 97 | Use: "block [hash or height]", 98 | Short: "Query block by hash or height", 99 | Args: cobra.ExactArgs(1), 100 | RunE: func(cmd *cobra.Command, args []string) error { 101 | clientCtx, err := client.GetClientQueryContext(cmd) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | queryClient := types.NewQueryClient(clientCtx) 107 | 108 | height, err := strconv.ParseUint(args[0], 10, 64) 109 | if err != nil { 110 | res, err := queryClient.QueryBlockHeaderByHash(cmd.Context(), &types.QueryBlockHeaderByHashRequest{Hash: args[0]}) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | return clientCtx.PrintProto(res) 116 | } 117 | 118 | res, err := queryClient.QueryBlockHeaderByHeight(cmd.Context(), &types.QueryBlockHeaderByHeightRequest{Height: height}) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | return clientCtx.PrintProto(res) 124 | }, 125 | } 126 | 127 | flags.AddQueryFlagsToCmd(cmd) 128 | 129 | return cmd 130 | } 131 | 132 | // CmdQuerySigningRequest returns the command to query signing request 133 | func CmdQuerySigningRequest() *cobra.Command { 134 | cmd := &cobra.Command{ 135 | Use: "signing-request [status | address | tx hash]", 136 | Short: "Query signing requests by status, address or tx hash", 137 | Args: cobra.ExactArgs(1), 138 | RunE: func(cmd *cobra.Command, args []string) error { 139 | clientCtx, err := client.GetClientQueryContext(cmd) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | queryClient := types.NewQueryClient(clientCtx) 145 | 146 | status, err := strconv.ParseInt(args[0], 10, 32) 147 | if err != nil { 148 | _, err = sdk.AccAddressFromBech32(args[0]) 149 | if err != nil { 150 | _, err := chainhash.NewHashFromStr(args[0]) 151 | if err != nil { 152 | return fmt.Errorf("invalid arg, neither status, address nor tx hash: %s", args[0]) 153 | } 154 | 155 | res, err := queryClient.QuerySigningRequestByTxHash(cmd.Context(), &types.QuerySigningRequestByTxHashRequest{Txid: args[0]}) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | return clientCtx.PrintProto(res) 161 | } 162 | 163 | res, err := queryClient.QuerySigningRequestByAddress(cmd.Context(), &types.QuerySigningRequestByAddressRequest{Address: args[0]}) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | return clientCtx.PrintProto(res) 169 | } 170 | 171 | res, err := queryClient.QuerySigningRequest(cmd.Context(), &types.QuerySigningRequestRequest{Status: types.SigningStatus(status)}) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | return clientCtx.PrintProto(res) 177 | }, 178 | } 179 | 180 | flags.AddQueryFlagsToCmd(cmd) 181 | 182 | return cmd 183 | } 184 | 185 | func CmdQueryUTXOs() *cobra.Command { 186 | cmd := &cobra.Command{ 187 | Use: "utxos [address]", 188 | Short: "query utxos with an optional address", 189 | Args: cobra.MaximumNArgs(1), 190 | RunE: func(cmd *cobra.Command, args []string) error { 191 | clientCtx, err := client.GetClientQueryContext(cmd) 192 | if err != nil { 193 | return err 194 | } 195 | 196 | if len(args) == 0 { 197 | return queryUTXOs(cmd.Context(), &clientCtx) 198 | } 199 | 200 | return queryUTXOsByAddr(cmd.Context(), &clientCtx, args[0]) 201 | }, 202 | } 203 | 204 | flags.AddQueryFlagsToCmd(cmd) 205 | 206 | return cmd 207 | } 208 | 209 | func queryUTXOs(cmdCtx context.Context, clientCtx *client.Context) error { 210 | queryClient := types.NewQueryClient(clientCtx) 211 | 212 | res, err := queryClient.QueryUTXOs(cmdCtx, &types.QueryUTXOsRequest{}) 213 | if err != nil { 214 | return err 215 | } 216 | 217 | return clientCtx.PrintProto(res) 218 | } 219 | 220 | func queryUTXOsByAddr(cmdCtx context.Context, clientCtx *client.Context, addr string) error { 221 | queryClient := types.NewQueryClient(clientCtx) 222 | 223 | _, err := sdk.AccAddressFromBech32(addr) 224 | if err != nil { 225 | return err 226 | } 227 | 228 | res, err := queryClient.QueryUTXOsByAddress(cmdCtx, &types.QueryUTXOsByAddressRequest{ 229 | Address: addr, 230 | }) 231 | if err != nil { 232 | return err 233 | } 234 | 235 | return clientCtx.PrintProto(res) 236 | } 237 | -------------------------------------------------------------------------------- /cmd/sided/cmd/genaccounts.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/cosmos/cosmos-sdk/client" 10 | "github.com/cosmos/cosmos-sdk/client/flags" 11 | "github.com/cosmos/cosmos-sdk/crypto/keyring" 12 | "github.com/cosmos/cosmos-sdk/server" 13 | sdk "github.com/cosmos/cosmos-sdk/types" 14 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 15 | authvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" 16 | banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" 17 | "github.com/cosmos/cosmos-sdk/x/genutil" 18 | genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | const ( 23 | flagVestingStart = "vesting-start-time" 24 | flagVestingEnd = "vesting-end-time" 25 | flagVestingAmt = "vesting-amount" 26 | ) 27 | 28 | // AddGenesisAccountCmd returns add-genesis-account cobra Command. 29 | func AddGenesisAccountCmd(defaultNodeHome string) *cobra.Command { 30 | cmd := &cobra.Command{ 31 | Use: "add-genesis-account [address_or_key_name] [coin][,[coin]]", 32 | Short: "Add a genesis account to genesis.json", 33 | Long: `Add a genesis account to genesis.json. The provided account must specify 34 | the account address or key name and a list of initial coins. If a key name is given, 35 | the address will be looked up in the local Keybase. The list of initial tokens must 36 | contain valid denominations. Accounts may optionally be supplied with vesting parameters. 37 | `, 38 | Args: cobra.ExactArgs(2), 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | clientCtx := client.GetClientContextFromCmd(cmd) 41 | cdc := clientCtx.Codec 42 | 43 | serverCtx := server.GetServerContextFromCmd(cmd) 44 | config := serverCtx.Config 45 | 46 | config.SetRoot(clientCtx.HomeDir) 47 | 48 | coins, err := sdk.ParseCoinsNormalized(args[1]) 49 | if err != nil { 50 | return fmt.Errorf("failed to parse coins: %w", err) 51 | } 52 | 53 | addr, err := sdk.AccAddressFromBech32(args[0]) 54 | if err != nil { 55 | inBuf := bufio.NewReader(cmd.InOrStdin()) 56 | keyringBackend, err := cmd.Flags().GetString(flags.FlagKeyringBackend) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | // attempt to lookup address from Keybase if no address was provided 62 | kb, err := keyring.New(sdk.KeyringServiceName(), keyringBackend, clientCtx.HomeDir, inBuf, cdc) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | info, err := kb.Key(args[0]) 68 | if err != nil { 69 | return fmt.Errorf("failed to get address from Keybase: %w", err) 70 | } 71 | 72 | addr, err = info.GetAddress() 73 | if err != nil { 74 | return fmt.Errorf("failed to get address from Keybase: %w", err) 75 | } 76 | } 77 | 78 | vestingStart, err := cmd.Flags().GetInt64(flagVestingStart) 79 | if err != nil { 80 | return err 81 | } 82 | vestingEnd, err := cmd.Flags().GetInt64(flagVestingEnd) 83 | if err != nil { 84 | return err 85 | } 86 | vestingAmtStr, err := cmd.Flags().GetString(flagVestingAmt) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | vestingAmt, err := sdk.ParseCoinsNormalized(vestingAmtStr) 92 | if err != nil { 93 | return fmt.Errorf("failed to parse vesting amount: %w", err) 94 | } 95 | 96 | // create concrete account type based on input parameters 97 | var genAccount authtypes.GenesisAccount 98 | 99 | balances := banktypes.Balance{Address: addr.String(), Coins: coins.Sort()} 100 | baseAccount := authtypes.NewBaseAccount(addr, nil, 0, 0) 101 | 102 | if !vestingAmt.IsZero() { 103 | baseVestingAccount := authvesting.NewBaseVestingAccount(baseAccount, vestingAmt.Sort(), vestingEnd) 104 | 105 | if (balances.Coins.IsZero() && !baseVestingAccount.OriginalVesting.IsZero()) || 106 | baseVestingAccount.OriginalVesting.IsAnyGT(balances.Coins) { 107 | return errors.New("vesting amount cannot be greater than total amount") 108 | } 109 | 110 | switch { 111 | case vestingStart != 0 && vestingEnd != 0: 112 | genAccount = authvesting.NewContinuousVestingAccountRaw(baseVestingAccount, vestingStart) 113 | 114 | case vestingEnd != 0: 115 | genAccount = authvesting.NewDelayedVestingAccountRaw(baseVestingAccount) 116 | 117 | default: 118 | return errors.New("invalid vesting parameters; must supply start and end time or end time") 119 | } 120 | } else { 121 | genAccount = baseAccount 122 | } 123 | 124 | if err := genAccount.Validate(); err != nil { 125 | return fmt.Errorf("failed to validate new genesis account: %w", err) 126 | } 127 | 128 | genFile := config.GenesisFile() 129 | appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile) 130 | if err != nil { 131 | return fmt.Errorf("failed to unmarshal genesis state: %w", err) 132 | } 133 | 134 | authGenState := authtypes.GetGenesisStateFromAppState(cdc, appState) 135 | 136 | accs, err := authtypes.UnpackAccounts(authGenState.Accounts) 137 | if err != nil { 138 | return fmt.Errorf("failed to get accounts from any: %w", err) 139 | } 140 | 141 | if accs.Contains(addr) { 142 | return fmt.Errorf("cannot add account at existing address %s", addr) 143 | } 144 | 145 | // Add the new account to the set of genesis accounts and sanitize the 146 | // accounts afterwards. 147 | accs = append(accs, genAccount) 148 | accs = authtypes.SanitizeGenesisAccounts(accs) 149 | 150 | genAccs, err := authtypes.PackAccounts(accs) 151 | if err != nil { 152 | return fmt.Errorf("failed to convert accounts into any's: %w", err) 153 | } 154 | authGenState.Accounts = genAccs 155 | 156 | authGenStateBz, err := cdc.MarshalJSON(&authGenState) 157 | if err != nil { 158 | return fmt.Errorf("failed to marshal auth genesis state: %w", err) 159 | } 160 | 161 | appState[authtypes.ModuleName] = authGenStateBz 162 | 163 | bankGenState := banktypes.GetGenesisStateFromAppState(cdc, appState) 164 | bankGenState.Balances = append(bankGenState.Balances, balances) 165 | bankGenState.Balances = banktypes.SanitizeGenesisBalances(bankGenState.Balances) 166 | 167 | bankGenStateBz, err := cdc.MarshalJSON(bankGenState) 168 | if err != nil { 169 | return fmt.Errorf("failed to marshal bank genesis state: %w", err) 170 | } 171 | 172 | appState[banktypes.ModuleName] = bankGenStateBz 173 | 174 | appStateJSON, err := json.Marshal(appState) 175 | if err != nil { 176 | return fmt.Errorf("failed to marshal application genesis state: %w", err) 177 | } 178 | 179 | genDoc.AppState = appStateJSON 180 | return genutil.ExportGenesisFile(genDoc, genFile) 181 | }, 182 | } 183 | 184 | cmd.Flags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|kwallet|pass|test)") 185 | cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory") 186 | cmd.Flags().String(flagVestingAmt, "", "amount of coins for vesting accounts") 187 | cmd.Flags().Int64(flagVestingStart, 0, "schedule start time (unix epoch) for vesting accounts") 188 | cmd.Flags().Int64(flagVestingEnd, 0, "schedule end time (unix epoch) for vesting accounts") 189 | flags.AddQueryFlagsToCmd(cmd) 190 | 191 | return cmd 192 | } 193 | -------------------------------------------------------------------------------- /app/export.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | 8 | tmproto "github.com/cometbft/cometbft/proto/tendermint/types" 9 | servertypes "github.com/cosmos/cosmos-sdk/server/types" 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" 12 | "github.com/cosmos/cosmos-sdk/x/staking" 13 | stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" 14 | ) 15 | 16 | // ExportAppStateAndValidators exports the state of the application for a genesis 17 | // file. 18 | func (app *App) ExportAppStateAndValidators( 19 | forZeroHeight bool, 20 | jailAllowedAddrs []string, 21 | modulesToExport []string, 22 | ) (servertypes.ExportedApp, error) { 23 | // as if they could withdraw from the start of the next block 24 | ctx := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) 25 | 26 | // We export at last height + 1, because that's the height at which 27 | // Tendermint will start InitChain. 28 | height := app.LastBlockHeight() + 1 29 | if forZeroHeight { 30 | height = 0 31 | app.prepForZeroHeightGenesis(ctx, jailAllowedAddrs) 32 | } 33 | 34 | genState := app.mm.ExportGenesisForModules(ctx, app.appCodec, modulesToExport) 35 | appState, err := json.MarshalIndent(genState, "", " ") 36 | if err != nil { 37 | return servertypes.ExportedApp{}, err 38 | } 39 | 40 | validators, err := staking.WriteValidators(ctx, app.StakingKeeper) 41 | return servertypes.ExportedApp{ 42 | AppState: appState, 43 | Validators: validators, 44 | Height: height, 45 | ConsensusParams: app.BaseApp.GetConsensusParams(ctx), 46 | }, err 47 | } 48 | 49 | // prepForZeroHeightGenesis prepares for a fresh genesis 50 | // 51 | // NOTE zero height genesis is a temporary feature which will be deprecated 52 | // in favour of export at a block height 53 | func (app *App) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs []string) { 54 | applyAllowedAddrs := false 55 | 56 | // check if there is a allowed address list 57 | if len(jailAllowedAddrs) > 0 { 58 | applyAllowedAddrs = true 59 | } 60 | 61 | allowedAddrsMap := make(map[string]bool) 62 | 63 | for _, addr := range jailAllowedAddrs { 64 | _, err := sdk.ValAddressFromBech32(addr) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | allowedAddrsMap[addr] = true 69 | } 70 | 71 | /* Just to be safe, assert the invariants on current state. */ 72 | app.CrisisKeeper.AssertInvariants(ctx) 73 | 74 | /* Handle fee distribution state. */ 75 | 76 | // withdraw all validator commission 77 | app.StakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { 78 | _, _ = app.DistrKeeper.WithdrawValidatorCommission(ctx, val.GetOperator()) 79 | return false 80 | }) 81 | 82 | // withdraw all delegator rewards 83 | dels := app.StakingKeeper.GetAllDelegations(ctx) 84 | for _, delegation := range dels { 85 | valAddr, err := sdk.ValAddressFromBech32(delegation.ValidatorAddress) 86 | if err != nil { 87 | panic(err) 88 | } 89 | 90 | delAddr := sdk.MustAccAddressFromBech32(delegation.DelegatorAddress) 91 | 92 | _, _ = app.DistrKeeper.WithdrawDelegationRewards(ctx, delAddr, valAddr) 93 | } 94 | 95 | // clear validator slash events 96 | app.DistrKeeper.DeleteAllValidatorSlashEvents(ctx) 97 | 98 | // clear validator historical rewards 99 | app.DistrKeeper.DeleteAllValidatorHistoricalRewards(ctx) 100 | 101 | // set context height to zero 102 | height := ctx.BlockHeight() 103 | ctx = ctx.WithBlockHeight(0) 104 | 105 | // reinitialize all validators 106 | app.StakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { 107 | // donate any unwithdrawn outstanding reward fraction tokens to the community pool 108 | scraps := app.DistrKeeper.GetValidatorOutstandingRewardsCoins(ctx, val.GetOperator()) 109 | feePool := app.DistrKeeper.GetFeePool(ctx) 110 | feePool.CommunityPool = feePool.CommunityPool.Add(scraps...) 111 | app.DistrKeeper.SetFeePool(ctx, feePool) 112 | 113 | if err := app.DistrKeeper.Hooks().AfterValidatorCreated(ctx, val.GetOperator()); err != nil { 114 | panic(err) 115 | } 116 | return false 117 | }) 118 | 119 | // reinitialize all delegations 120 | for _, del := range dels { 121 | valAddr, err := sdk.ValAddressFromBech32(del.ValidatorAddress) 122 | if err != nil { 123 | panic(err) 124 | } 125 | delAddr := sdk.MustAccAddressFromBech32(del.DelegatorAddress) 126 | 127 | if err := app.DistrKeeper.Hooks().BeforeDelegationCreated(ctx, delAddr, valAddr); err != nil { 128 | // never called as BeforeDelegationCreated always returns nil 129 | panic(fmt.Errorf("error while incrementing period: %w", err)) 130 | } 131 | 132 | if err := app.DistrKeeper.Hooks().AfterDelegationModified(ctx, delAddr, valAddr); err != nil { 133 | // never called as AfterDelegationModified always returns nil 134 | panic(fmt.Errorf("error while creating a new delegation period record: %w", err)) 135 | } 136 | } 137 | 138 | // reset context height 139 | ctx = ctx.WithBlockHeight(height) 140 | 141 | /* Handle staking state. */ 142 | 143 | // iterate through redelegations, reset creation height 144 | app.StakingKeeper.IterateRedelegations(ctx, func(_ int64, red stakingtypes.Redelegation) (stop bool) { 145 | for i := range red.Entries { 146 | red.Entries[i].CreationHeight = 0 147 | } 148 | app.StakingKeeper.SetRedelegation(ctx, red) 149 | return false 150 | }) 151 | 152 | // iterate through unbonding delegations, reset creation height 153 | app.StakingKeeper.IterateUnbondingDelegations(ctx, func(_ int64, ubd stakingtypes.UnbondingDelegation) (stop bool) { 154 | for i := range ubd.Entries { 155 | ubd.Entries[i].CreationHeight = 0 156 | } 157 | app.StakingKeeper.SetUnbondingDelegation(ctx, ubd) 158 | return false 159 | }) 160 | 161 | // Iterate through validators by power descending, reset bond heights, and 162 | // update bond intra-tx counters. 163 | store := ctx.KVStore(app.GetKey(stakingtypes.StoreKey)) 164 | iter := sdk.KVStoreReversePrefixIterator(store, stakingtypes.ValidatorsKey) 165 | counter := int16(0) 166 | 167 | for ; iter.Valid(); iter.Next() { 168 | addr := sdk.ValAddress(stakingtypes.AddressFromValidatorsKey(iter.Key())) 169 | validator, found := app.StakingKeeper.GetValidator(ctx, addr) 170 | if !found { 171 | panic("expected validator, not found") 172 | } 173 | 174 | validator.UnbondingHeight = 0 175 | if applyAllowedAddrs && !allowedAddrsMap[addr.String()] { 176 | validator.Jailed = true 177 | } 178 | 179 | app.StakingKeeper.SetValidator(ctx, validator) 180 | counter++ 181 | } 182 | 183 | if err := iter.Close(); err != nil { 184 | app.Logger().Error("error while closing the key-value store reverse prefix iterator: ", err) 185 | return 186 | } 187 | 188 | _, err := app.StakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx) 189 | if err != nil { 190 | log.Fatal(err) 191 | } 192 | 193 | /* Handle slashing state. */ 194 | 195 | // reset start height on signing infos 196 | app.SlashingKeeper.IterateValidatorSigningInfos( 197 | ctx, 198 | func(addr sdk.ConsAddress, info slashingtypes.ValidatorSigningInfo) (stop bool) { 199 | info.StartHeight = 0 200 | app.SlashingKeeper.SetValidatorSigningInfo(ctx, addr, info) 201 | return false 202 | }, 203 | ) 204 | } 205 | -------------------------------------------------------------------------------- /app/test_setup.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | sdkmath "cosmossdk.io/math" 8 | "github.com/Stride-Labs/stride/v16/testutil" 9 | cometbftdb "github.com/cometbft/cometbft-db" 10 | abci "github.com/cometbft/cometbft/abci/types" 11 | "github.com/cometbft/cometbft/crypto/secp256k1" 12 | "github.com/cometbft/cometbft/libs/log" 13 | tmtypes "github.com/cometbft/cometbft/types" 14 | "github.com/cosmos/cosmos-sdk/codec" 15 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 16 | cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" 17 | "github.com/cosmos/cosmos-sdk/testutil/mock" 18 | simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" 19 | sdk "github.com/cosmos/cosmos-sdk/types" 20 | "github.com/cosmos/cosmos-sdk/types/address" 21 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 22 | banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" 23 | stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" 24 | 25 | // cmdcfg "github.com/Stride-Labs/stride/v16/cmd/sided/config" 26 | errorsmod "cosmossdk.io/errors" 27 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 28 | ) 29 | 30 | const bech32Prefix = "cosmos" 31 | 32 | func init() { 33 | SetupConfig() 34 | } 35 | 36 | func SetupConfig() { 37 | config := sdk.GetConfig() 38 | valoper := sdk.PrefixValidator + sdk.PrefixOperator 39 | valoperpub := sdk.PrefixValidator + sdk.PrefixOperator + sdk.PrefixPublic 40 | config.SetBech32PrefixForAccount(bech32Prefix, bech32Prefix+sdk.PrefixPublic) 41 | config.SetBech32PrefixForValidator(bech32Prefix+valoper, bech32Prefix+valoperpub) 42 | 43 | config.SetCoinType(84) // use the same coin type as bitcoin 44 | 45 | // This is copied from the cosmos sdk v0.43.0-beta1 46 | // source: https://github.com/cosmos/cosmos-sdk/blob/v0.43.0-beta1/types/address.go#L141 47 | config.SetAddressVerifier(func(bytes []byte) error { 48 | if len(bytes) == 0 { 49 | return errorsmod.Wrap(sdkerrors.ErrUnknownAddress, "addresses cannot be empty") 50 | } 51 | 52 | if len(bytes) > address.MaxAddrLen { 53 | return errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "address max length is %d, got %d", address.MaxAddrLen, len(bytes)) 54 | } 55 | 56 | // TODO: Do we want to allow addresses of lengths other than 20 and 32 bytes? 57 | // if len(bytes) != 20 && len(bytes) != 32 { 58 | // return errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "address length must be 20 or 32 bytes, got %d", len(bytes)) 59 | // } 60 | 61 | return nil 62 | }) 63 | } 64 | 65 | // Initializes a new SideApp without IBC functionality 66 | func InitSideTestApp(initChain bool) *App { 67 | db := cometbftdb.NewMemDB() 68 | app := New( 69 | log.NewNopLogger(), 70 | db, 71 | nil, 72 | true, 73 | map[int64]bool{}, 74 | DefaultNodeHome, 75 | 5, 76 | MakeEncodingConfig(), 77 | simtestutil.EmptyAppOptions{}, 78 | ) 79 | if initChain { 80 | genesisState := GenesisStateWithValSet(app) 81 | stateBytes, err := json.MarshalIndent(genesisState, "", " ") 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | app.InitChain( 87 | abci.RequestInitChain{ 88 | Validators: []abci.ValidatorUpdate{}, 89 | ConsensusParams: simtestutil.DefaultConsensusParams, 90 | AppStateBytes: stateBytes, 91 | }, 92 | ) 93 | } 94 | 95 | return app 96 | } 97 | 98 | func GenesisStateWithValSet(app *App) GenesisState { 99 | privVal := mock.NewPV() 100 | pubKey, _ := privVal.GetPubKey() 101 | validator := tmtypes.NewValidator(pubKey, 1) 102 | valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{validator}) 103 | 104 | // generate genesis account 105 | senderPrivKey := secp256k1.GenPrivKey() 106 | senderPrivKey.PubKey().Address() 107 | acc := authtypes.NewBaseAccountWithAddress(senderPrivKey.PubKey().Address().Bytes()) 108 | balance := banktypes.Balance{ 109 | Address: acc.GetAddress().String(), 110 | Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100000000000000))), 111 | } 112 | registry := codectypes.NewInterfaceRegistry() 113 | cdc := codec.NewProtoCodec(registry) 114 | 115 | ////////////////////// 116 | balances := []banktypes.Balance{balance} 117 | genesisState := NewDefaultGenesisState(cdc) 118 | genAccs := []authtypes.GenesisAccount{acc} 119 | authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), genAccs) 120 | genesisState[authtypes.ModuleName] = app.AppCodec().MustMarshalJSON(authGenesis) 121 | 122 | validators := make([]stakingtypes.Validator, 0, len(valSet.Validators)) 123 | delegations := make([]stakingtypes.Delegation, 0, len(valSet.Validators)) 124 | 125 | bondAmt := sdk.DefaultPowerReduction 126 | initValPowers := []abci.ValidatorUpdate{} 127 | 128 | for _, val := range valSet.Validators { 129 | pk, _ := cryptocodec.FromTmPubKeyInterface(val.PubKey) 130 | pkAny, _ := codectypes.NewAnyWithValue(pk) 131 | validator := stakingtypes.Validator{ 132 | OperatorAddress: sdk.ValAddress(val.Address).String(), 133 | ConsensusPubkey: pkAny, 134 | Jailed: false, 135 | Status: stakingtypes.Bonded, 136 | Tokens: bondAmt, 137 | DelegatorShares: sdk.OneDec(), 138 | Description: stakingtypes.Description{}, 139 | UnbondingHeight: int64(0), 140 | UnbondingTime: time.Unix(0, 0).UTC(), 141 | Commission: stakingtypes.NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), 142 | MinSelfDelegation: sdkmath.ZeroInt(), 143 | } 144 | validators = append(validators, validator) 145 | delegations = append(delegations, stakingtypes.NewDelegation(genAccs[0].GetAddress(), val.Address.Bytes(), sdk.OneDec())) 146 | 147 | // add initial validator powers so consumer InitGenesis runs correctly 148 | pub, _ := val.ToProto() 149 | initValPowers = append(initValPowers, abci.ValidatorUpdate{ 150 | Power: val.VotingPower, 151 | PubKey: pub.PubKey, 152 | }) 153 | } 154 | // set validators and delegations 155 | stakingGenesis := stakingtypes.NewGenesisState(stakingtypes.DefaultParams(), validators, delegations) 156 | genesisState[stakingtypes.ModuleName] = app.AppCodec().MustMarshalJSON(stakingGenesis) 157 | 158 | totalSupply := sdk.NewCoins() 159 | for _, b := range balances { 160 | // add genesis acc tokens to total supply 161 | totalSupply = totalSupply.Add(b.Coins...) 162 | } 163 | 164 | for range delegations { 165 | // add delegated tokens to total supply 166 | totalSupply = totalSupply.Add(sdk.NewCoin(sdk.DefaultBondDenom, bondAmt)) 167 | } 168 | 169 | // add bonded amount to bonded pool module account 170 | balances = append(balances, banktypes.Balance{ 171 | Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String(), 172 | Coins: sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, bondAmt)}, 173 | }) 174 | 175 | // update total supply 176 | bankGenesis := banktypes.NewGenesisState( 177 | banktypes.DefaultGenesisState().Params, 178 | balances, 179 | totalSupply, 180 | []banktypes.Metadata{}, 181 | []banktypes.SendEnabled{}, 182 | ) 183 | genesisState[banktypes.ModuleName] = app.AppCodec().MustMarshalJSON(bankGenesis) 184 | 185 | vals, err := tmtypes.PB2TM.ValidatorUpdates(initValPowers) 186 | if err != nil { 187 | panic("failed to get vals") 188 | } 189 | 190 | consumerGenesisState := testutil.CreateMinimalConsumerTestGenesis() 191 | consumerGenesisState.InitialValSet = initValPowers 192 | consumerGenesisState.ProviderConsensusState.NextValidatorsHash = tmtypes.NewValidatorSet(vals).Hash() 193 | consumerGenesisState.Params.Enabled = true 194 | 195 | return genesisState 196 | } 197 | 198 | // Initializes a new Side App casted as a TestingApp for IBC support 199 | // func InitSideIBCTestingApp() (ibctesting.TestingApp, map[string]json.RawMessage) { 200 | // app := InitSideTestApp(false) 201 | // registry := codectypes.NewInterfaceRegistry() 202 | // cdc := codec.NewProtoCodec(registry) 203 | // return app, NewDefaultGenesisState(cdc) 204 | // } 205 | -------------------------------------------------------------------------------- /local_node_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | KEYS=("validator" "test" "relayer" "btc-vault" "runes-vault") 4 | CHAINID="devnet" 5 | MONIKER="Side Labs" 6 | BINARY="$HOME/go/bin/sided" 7 | DENOM_STR="uside,sat,uusdc,uusdt" 8 | INITIAL_ACCOUNT_STR="" 9 | set -f 10 | IFS=, 11 | DENOMS=($DENOM_STR) 12 | INITIAL_ACCOUNTS=($INITIAL_ACCOUNT_STR) 13 | 14 | IFS=";" 15 | 16 | INITIAL_SUPPLY="500000000000000" 17 | BLOCK_GAS=10000000 18 | MAX_GAS=10000000000 19 | 20 | # Remember to change to other types of keyring like 'file' in-case exposing to outside world, 21 | # otherwise your balance will be wiped quickly 22 | # The keyring test does not require private key to steal tokens from you 23 | KEYRING="test" 24 | #KEYALGO="secp256k1" 25 | KEYALGO="segwit" 26 | LOGLEVEL="info" 27 | # Set dedicated home directory for the $BINARY instance 28 | HOMEDIR="$HOME/.side" 29 | 30 | # Path variables 31 | CONFIG=$HOMEDIR/config/config.toml 32 | APP_TOML=$HOMEDIR/config/app.toml 33 | GENESIS=$HOMEDIR/config/genesis.json 34 | TMP_GENESIS=$HOMEDIR/config/tmp_genesis.json 35 | 36 | # validate dependencies are installed 37 | command -v jq >/dev/null 2>&1 || { 38 | echo >&2 "jq not installed. More info: https://stedolan.github.io/jq/download/" 39 | exit 1 40 | } 41 | 42 | # used to exit on first error (any non-zero exit code) 43 | set -e 44 | 45 | # Reinstall daemon 46 | make install 47 | 48 | # User prompt if an existing local node configuration is found. 49 | if [ -d "$HOMEDIR" ]; then 50 | printf "\nAn existing folder at '%s' was found. You can choose to delete this folder and start a new local node with new keys from genesis. When declined, the existing local node is started. \n" "$HOMEDIR" 51 | echo "Overwrite the existing configuration and start a new local node? [y/n]" 52 | read -r overwrite 53 | else 54 | overwrite="Y" 55 | fi 56 | 57 | 58 | # Setup local node if overwrite is set to Yes, otherwise skip setup 59 | if [[ $overwrite == "y" || $overwrite == "Y" ]]; then 60 | # Remove the previous folder 61 | rm -rf "$HOMEDIR" 62 | 63 | # Set client config 64 | $BINARY config keyring-backend $KEYRING --home "$HOMEDIR" 65 | $BINARY config chain-id $CHAINID --home "$HOMEDIR" 66 | 67 | # If keys exist they should be deleted 68 | for KEY in "${KEYS[@]}"; do 69 | $BINARY keys add "$KEY" --keyring-backend $KEYRING --algo $KEYALGO --home "$HOMEDIR" 70 | done 71 | # for KEY in "${KEYS[@]}"; do 72 | # # Add the --recover flag to initiate recovery mode 73 | # $BINARY keys add "$KEY" --keyring-backend $KEYRING --algo $KEYALGO --recover --home "$HOMEDIR" 74 | # done 75 | 76 | echo "" 77 | echo "☝️ Copy the above mnemonic phrases and import them to relayer! Press [Enter] to continue..." 78 | read -r continue 79 | 80 | # Set moniker and chain-id for Cascadia (Moniker can be anything, chain-id must be an integer) 81 | $BINARY init $MONIKER -o --chain-id $CHAINID --home "$HOMEDIR" 82 | 83 | jq --arg denom "${DENOMS[0]}" '.app_state["staking"]["params"]["bond_denom"]=$denom' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 84 | jq --arg denom "${DENOMS[0]}" '.app_state["mint"]["params"]["mint_denom"]=$denom' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 85 | jq --arg denom "${DENOMS[0]}" '.app_state["crisis"]["constant_fee"]["denom"]=$denom' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 86 | jq --arg denom "${DENOMS[0]}" '.app_state["gov"]["deposit_params"]["min_deposit"][0]["denom"]=$denom' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 87 | jq --arg denom "${DENOMS[0]}" '.app_state["gov"]["params"]["min_deposit"][0]["denom"]=$denom' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 88 | jq --arg gas "$BLOCK_GAS" '.app_state["feemarket"]["block_gas"]=$gas' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 89 | # Set gas limit in genesis 90 | jq --arg max_gas "$MAX_GAS" '.consensus_params["block"]["max_gas"]=$max_gas' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 91 | # setup relayers 92 | RELAYER=$($BINARY keys show "${KEYS[2]}" -a --keyring-backend $KEYRING --home "$HOMEDIR") 93 | jq --arg relayer "$RELAYER" '.app_state["btcbridge"]["params"]["authorized_relayers"][0]=$relayer' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 94 | # setup vaults 95 | BTC_VAULT=$($BINARY keys show "${KEYS[3]}" -a --keyring-backend $KEYRING --home "$HOMEDIR") 96 | jq --arg btc_vault "$BTC_VAULT" '.app_state["btcbridge"]["params"]["vaults"][0]["address"]=$btc_vault' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 97 | PUKEY=$($BINARY keys show "${KEYS[3]}" --pubkeyhex --keyring-backend $KEYRING --home "$HOMEDIR") 98 | jq --arg pubkey "$PUKEY" '.app_state["btcbridge"]["params"]["vaults"][0]["pub_key"]=$pubkey' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 99 | RUNES_VAULT=$($BINARY keys show "${KEYS[4]}" -a --keyring-backend $KEYRING --home "$HOMEDIR") 100 | jq --arg runes_vault "$RUNES_VAULT" '.app_state["btcbridge"]["params"]["vaults"][1]["address"]=$runes_vault' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 101 | PUKEY=$($BINARY keys show "${KEYS[4]}" --pubkeyhex --keyring-backend $KEYRING --home "$HOMEDIR") 102 | jq --arg pubkey "$PUKEY" '.app_state["btcbridge"]["params"]["vaults"][1]["pub_key"]=$pubkey' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 103 | 104 | # set custom pruning settings 105 | sed -i.bak 's/pruning = "default"/pruning = "custom"/g' "$APP_TOML" 106 | sed -i.bak 's/pruning-keep-recent = "0"/pruning-keep-recent = "2"/g' "$APP_TOML" 107 | sed -i.bak 's/pruning-interval = "0"/pruning-interval = "10"/g' "$APP_TOML" 108 | 109 | sed -i.bak 's/127.0.0.1:26657/0.0.0.0:26657/g' "$CONFIG" 110 | sed -i.bak 's/cors_allowed_origins\s*=\s*\[\]/cors_allowed_origins = ["*",]/g' "$CONFIG" 111 | sed -i.bak 's/swagger = false/swagger = true/g' $APP_TOML 112 | 113 | # Allocate genesis accounts (cosmos formatted addresses) 114 | for KEY in "${KEYS[@]}"; do 115 | BALANCES="" 116 | for key in "${!DENOMS[@]}"; do 117 | BALANCES+=",${INITIAL_SUPPLY}${DENOMS[$key]}" 118 | done 119 | echo ${BALANCES:1} 120 | $BINARY add-genesis-account "$KEY" ${BALANCES:1} --keyring-backend $KEYRING --home "$HOMEDIR" 121 | done 122 | 123 | echo "Genesis accounts allocated for local accounts" 124 | 125 | # Allocate genesis accounts (cosmos formatted addresses) 126 | for ADDR in "${INITIAL_ACCOUNTS[@]}"; do 127 | BALANCES="" 128 | for key in "${!DENOMS[@]}"; do 129 | BALANCES+=",${INITIAL_SUPPLY}${DENOMS[$key]}" 130 | done 131 | echo ${BALANCES:1} 132 | $BINARY add-genesis-account "$ADDR" ${BALANCES:1} --home "$HOMEDIR" 133 | done 134 | echo "Genesis accounts allocated for initial accounts" 135 | 136 | 137 | 138 | # Sign genesis transaction 139 | # echo $INITIAL_SUPPLY${DENOMS[0]} 140 | $BINARY gentx "${KEYS[0]}" $INITIAL_SUPPLY${DENOMS[0]} --keyring-backend $KEYRING --chain-id $CHAINID --identity "666AC57CC678BEC4" --website="https://side.one" --home "$HOMEDIR" 141 | echo "Genesis transaction signed" 142 | 143 | ## In case you want to create multiple validators at genesis 144 | ## 1. Back to `$BINARY keys add` step, init more keys 145 | ## 2. Back to `$BINARY add-genesis-account` step, add balance for those 146 | ## 3. Clone this ~/.$BINARY home directory into some others, let's say `~/.clonedCascadiad` 147 | ## 4. Run `gentx` in each of those folders 148 | ## 5. Copy the `gentx-*` folders under `~/.clonedCascadiad/config/gentx/` folders into the original `~/.$BINARY/config/gentx` 149 | 150 | # Collect genesis tx 151 | $BINARY collect-gentxs --home "$HOMEDIR" 152 | echo "Genesis transactions collected" 153 | 154 | # Run this to ensure everything worked and that the genesis file is setup correctly 155 | $BINARY validate-genesis --home "$HOMEDIR" 156 | echo "Genesis file validated" 157 | 158 | if [[ $1 == "pending" ]]; then 159 | echo "pending mode is on, please wait for the first block committed." 160 | fi 161 | fi 162 | 163 | 164 | # Start the node (remove the --pruning=nothing flag if historical queries are not needed) 165 | $BINARY start --log_level info --minimum-gas-prices=0.0001${DENOMS[0]} --home "$HOMEDIR" 166 | -------------------------------------------------------------------------------- /local_node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | KEYS=("validator" "test" "relayer" "btc-vault" "runes-vault") 4 | CHAINID="grimoria-testnet-1" 5 | MONIKER="Side Labs" 6 | BINARY="$HOME/go/bin/sided" 7 | DENOM_STR="uside,uusdc,uusdt" 8 | INITIAL_ACCOUNT_STR="tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0" 9 | set -f 10 | IFS=, 11 | DENOMS=($DENOM_STR) 12 | INITIAL_ACCOUNTS=($INITIAL_ACCOUNT_STR) 13 | 14 | IFS=";" 15 | 16 | INITIAL_SUPPLY="500000000000000" 17 | BLOCK_GAS=10000000 18 | MAX_GAS=10000000000 19 | 20 | # Remember to change to other types of keyring like 'file' in-case exposing to outside world, 21 | # otherwise your balance will be wiped quickly 22 | # The keyring test does not require private key to steal tokens from you 23 | KEYRING="test" 24 | #KEYALGO="secp256k1" 25 | KEYALGO="segwit" 26 | LOGLEVEL="info" 27 | # Set dedicated home directory for the $BINARY instance 28 | HOMEDIR="$HOME/.side" 29 | 30 | # Path variables 31 | CONFIG=$HOMEDIR/config/config.toml 32 | APP_TOML=$HOMEDIR/config/app.toml 33 | GENESIS=$HOMEDIR/config/genesis.json 34 | TMP_GENESIS=$HOMEDIR/config/tmp_genesis.json 35 | 36 | # validate dependencies are installed 37 | command -v jq >/dev/null 2>&1 || { 38 | echo >&2 "jq not installed. More info: https://stedolan.github.io/jq/download/" 39 | exit 1 40 | } 41 | 42 | # used to exit on first error (any non-zero exit code) 43 | set -e 44 | 45 | # Reinstall daemon 46 | make install 47 | 48 | # User prompt if an existing local node configuration is found. 49 | if [ -d "$HOMEDIR" ]; then 50 | printf "\nAn existing folder at '%s' was found. You can choose to delete this folder and start a new local node with new keys from genesis. When declined, the existing local node is started. \n" "$HOMEDIR" 51 | echo "Overwrite the existing configuration and start a new local node? [y/n]" 52 | read -r overwrite 53 | else 54 | overwrite="Y" 55 | fi 56 | 57 | 58 | # Setup local node if overwrite is set to Yes, otherwise skip setup 59 | if [[ $overwrite == "y" || $overwrite == "Y" ]]; then 60 | # Remove the previous folder 61 | rm -rf "$HOMEDIR" 62 | 63 | # Set client config 64 | $BINARY config keyring-backend $KEYRING --home "$HOMEDIR" 65 | $BINARY config chain-id $CHAINID --home "$HOMEDIR" 66 | 67 | # If keys exist they should be deleted 68 | for KEY in "${KEYS[@]}"; do 69 | $BINARY keys add "$KEY" --keyring-backend $KEYRING --algo $KEYALGO --home "$HOMEDIR" 70 | done 71 | # for KEY in "${KEYS[@]}"; do 72 | # # Add the --recover flag to initiate recovery mode 73 | # $BINARY keys add "$KEY" --keyring-backend $KEYRING --algo $KEYALGO --recover --home "$HOMEDIR" 74 | # done 75 | 76 | echo "" 77 | echo "☝️ Copy the above mnemonic phrases and import them to relayer! Press [Enter] to continue..." 78 | read -r continue 79 | 80 | # Set moniker and chain-id for Cascadia (Moniker can be anything, chain-id must be an integer) 81 | $BINARY init $MONIKER -o --chain-id $CHAINID --home "$HOMEDIR" 82 | 83 | jq --arg denom "${DENOMS[0]}" '.app_state["staking"]["params"]["bond_denom"]=$denom' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 84 | jq --arg denom "${DENOMS[0]}" '.app_state["mint"]["params"]["mint_denom"]=$denom' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 85 | jq --arg denom "${DENOMS[0]}" '.app_state["crisis"]["constant_fee"]["denom"]=$denom' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 86 | jq --arg denom "${DENOMS[0]}" '.app_state["gov"]["deposit_params"]["min_deposit"][0]["denom"]=$denom' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 87 | jq --arg denom "${DENOMS[0]}" '.app_state["gov"]["params"]["min_deposit"][0]["denom"]=$denom' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 88 | jq --arg gas "$BLOCK_GAS" '.app_state["feemarket"]["block_gas"]=$gas' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 89 | # Set gas limit in genesis 90 | jq --arg max_gas "$MAX_GAS" '.consensus_params["block"]["max_gas"]=$max_gas' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 91 | # setup relayers 92 | RELAYER=$($BINARY keys show "${KEYS[2]}" -a --keyring-backend $KEYRING --home "$HOMEDIR") 93 | jq --arg relayer "$RELAYER" '.app_state["btcbridge"]["params"]["authorized_relayers"][0]=$relayer' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 94 | # setup vaults 95 | BTC_VAULT=$($BINARY keys show "${KEYS[3]}" -a --keyring-backend $KEYRING --home "$HOMEDIR") 96 | jq --arg btc_vault "$BTC_VAULT" '.app_state["btcbridge"]["params"]["vaults"][0]["address"]=$btc_vault' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 97 | PUKEY=$($BINARY keys show "${KEYS[3]}" --pubkeyhex --keyring-backend $KEYRING --home "$HOMEDIR") 98 | jq --arg pubkey "$PUKEY" '.app_state["btcbridge"]["params"]["vaults"][0]["pub_key"]=$pubkey' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 99 | RUNES_VAULT=$($BINARY keys show "${KEYS[4]}" -a --keyring-backend $KEYRING --home "$HOMEDIR") 100 | jq --arg runes_vault "$RUNES_VAULT" '.app_state["btcbridge"]["params"]["vaults"][1]["address"]=$runes_vault' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 101 | PUKEY=$($BINARY keys show "${KEYS[4]}" --pubkeyhex --keyring-backend $KEYRING --home "$HOMEDIR") 102 | jq --arg pubkey "$PUKEY" '.app_state["btcbridge"]["params"]["vaults"][1]["pub_key"]=$pubkey' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" 103 | 104 | # set custom pruning settings 105 | sed -i.bak 's/pruning = "default"/pruning = "custom"/g' "$APP_TOML" 106 | sed -i.bak 's/pruning-keep-recent = "0"/pruning-keep-recent = "2"/g' "$APP_TOML" 107 | sed -i.bak 's/pruning-interval = "0"/pruning-interval = "10"/g' "$APP_TOML" 108 | 109 | sed -i.bak 's/127.0.0.1:26657/0.0.0.0:26657/g' "$CONFIG" 110 | sed -i.bak 's/cors_allowed_origins\s*=\s*\[\]/cors_allowed_origins = ["*",]/g' "$CONFIG" 111 | sed -i.bak 's/swagger = false/swagger = true/g' $APP_TOML 112 | 113 | # Allocate genesis accounts (cosmos formatted addresses) 114 | for KEY in "${KEYS[@]}"; do 115 | BALANCES="" 116 | for key in "${!DENOMS[@]}"; do 117 | BALANCES+=",${INITIAL_SUPPLY}${DENOMS[$key]}" 118 | done 119 | echo ${BALANCES:1} 120 | $BINARY add-genesis-account "$KEY" ${BALANCES:1} --keyring-backend $KEYRING --home "$HOMEDIR" 121 | done 122 | 123 | echo "Genesis accounts allocated for local accounts" 124 | 125 | # Allocate genesis accounts (cosmos formatted addresses) 126 | for ADDR in "${INITIAL_ACCOUNTS[@]}"; do 127 | BALANCES="" 128 | for key in "${!DENOMS[@]}"; do 129 | BALANCES+=",${INITIAL_SUPPLY}${DENOMS[$key]}" 130 | done 131 | echo ${BALANCES:1} 132 | $BINARY add-genesis-account "$ADDR" ${BALANCES:1} --home "$HOMEDIR" 133 | done 134 | echo "Genesis accounts allocated for initial accounts" 135 | 136 | 137 | 138 | # Sign genesis transaction 139 | # echo $INITIAL_SUPPLY${DENOMS[0]} 140 | $BINARY gentx "${KEYS[0]}" $INITIAL_SUPPLY${DENOMS[0]} --keyring-backend $KEYRING --chain-id $CHAINID --identity "666AC57CC678BEC4" --website="https://side.one" --home "$HOMEDIR" 141 | echo "Genesis transaction signed" 142 | 143 | ## In case you want to create multiple validators at genesis 144 | ## 1. Back to `$BINARY keys add` step, init more keys 145 | ## 2. Back to `$BINARY add-genesis-account` step, add balance for those 146 | ## 3. Clone this ~/.$BINARY home directory into some others, let's say `~/.clonedCascadiad` 147 | ## 4. Run `gentx` in each of those folders 148 | ## 5. Copy the `gentx-*` folders under `~/.clonedCascadiad/config/gentx/` folders into the original `~/.$BINARY/config/gentx` 149 | 150 | # Collect genesis tx 151 | $BINARY collect-gentxs --home "$HOMEDIR" 152 | echo "Genesis transactions collected" 153 | 154 | # Run this to ensure everything worked and that the genesis file is setup correctly 155 | $BINARY validate-genesis --home "$HOMEDIR" 156 | echo "Genesis file validated" 157 | 158 | if [[ $1 == "pending" ]]; then 159 | echo "pending mode is on, please wait for the first block committed." 160 | fi 161 | fi 162 | 163 | 164 | # Start the node (remove the --pruning=nothing flag if historical queries are not needed) 165 | $BINARY start --log_level info --minimum-gas-prices=0.0001${DENOMS[0]} --home "$HOMEDIR" 166 | -------------------------------------------------------------------------------- /x/btcbridge/keeper/keeper_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/suite" 10 | "lukechampine.com/uint128" 11 | 12 | "github.com/btcsuite/btcd/btcutil" 13 | "github.com/btcsuite/btcd/btcutil/psbt" 14 | "github.com/btcsuite/btcd/chaincfg" 15 | "github.com/btcsuite/btcd/chaincfg/chainhash" 16 | "github.com/btcsuite/btcd/txscript" 17 | "github.com/btcsuite/btcd/wire" 18 | 19 | tmproto "github.com/cometbft/cometbft/proto/tendermint/types" 20 | "github.com/cosmos/btcutil/bech32" 21 | "github.com/cosmos/cosmos-sdk/crypto/keys/segwit" 22 | sdk "github.com/cosmos/cosmos-sdk/types" 23 | 24 | simapp "github.com/sideprotocol/side/app" 25 | "github.com/sideprotocol/side/x/btcbridge/types" 26 | ) 27 | 28 | type KeeperTestSuite struct { 29 | suite.Suite 30 | 31 | ctx sdk.Context 32 | app *simapp.App 33 | 34 | btcVault string 35 | runesVault string 36 | sender string 37 | 38 | btcVaultPkScript []byte 39 | runesVaultPkScript []byte 40 | senderPkScript []byte 41 | } 42 | 43 | func (suite *KeeperTestSuite) SetupTest() { 44 | app := simapp.Setup(suite.T()) 45 | ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Now().UTC()}) 46 | 47 | suite.ctx = ctx 48 | suite.app = app 49 | 50 | chainCfg := sdk.GetConfig().GetBtcChainCfg() 51 | 52 | suite.btcVault, _ = bech32.Encode(chainCfg.Bech32HRPSegwit, segwit.GenPrivKey().PubKey().Address().Bytes()) 53 | suite.runesVault, _ = bech32.Encode(chainCfg.Bech32HRPSegwit, segwit.GenPrivKey().PubKey().Address()) 54 | suite.sender, _ = bech32.Encode(chainCfg.Bech32HRPSegwit, segwit.GenPrivKey().PubKey().Address()) 55 | 56 | suite.btcVaultPkScript = MustPkScriptFromAddress(suite.btcVault, chainCfg) 57 | suite.runesVaultPkScript = MustPkScriptFromAddress(suite.runesVault, chainCfg) 58 | suite.senderPkScript = MustPkScriptFromAddress(suite.sender, chainCfg) 59 | 60 | suite.setupParams(suite.btcVault, suite.runesVault) 61 | } 62 | 63 | func TestKeeperSuite(t *testing.T) { 64 | suite.Run(t, new(KeeperTestSuite)) 65 | } 66 | 67 | func (suite *KeeperTestSuite) setupParams(btcVault string, runesVault string) { 68 | suite.app.BtcBridgeKeeper.SetParams(suite.ctx, types.Params{Vaults: []*types.Vault{ 69 | { 70 | Address: btcVault, 71 | AssetType: types.AssetType_ASSET_TYPE_BTC, 72 | }, 73 | { 74 | Address: runesVault, 75 | AssetType: types.AssetType_ASSET_TYPE_RUNE, 76 | }, 77 | }}) 78 | } 79 | 80 | func (suite *KeeperTestSuite) setupUTXOs(utxos []*types.UTXO) { 81 | for _, utxo := range utxos { 82 | suite.app.BtcBridgeKeeper.SetUTXO(suite.ctx, utxo) 83 | suite.app.BtcBridgeKeeper.SetOwnerUTXO(suite.ctx, utxo) 84 | 85 | for _, r := range utxo.Runes { 86 | suite.app.BtcBridgeKeeper.SetOwnerRunesUTXO(suite.ctx, utxo, r.Id, r.Amount) 87 | } 88 | } 89 | } 90 | 91 | func (suite *KeeperTestSuite) TestMintRunes() { 92 | runeId := "840000:3" 93 | runeAmount := 500000000 94 | runeOutputIndex := 2 95 | 96 | runesScript, err := types.BuildEdictScript(runeId, uint128.From64(uint64(runeAmount)), uint32(runeOutputIndex)) 97 | suite.NoError(err) 98 | 99 | tx := wire.NewMsgTx(types.TxVersion) 100 | tx.AddTxOut(wire.NewTxOut(0, runesScript)) 101 | tx.AddTxOut(wire.NewTxOut(types.RunesOutValue, suite.senderPkScript)) 102 | tx.AddTxOut(wire.NewTxOut(types.RunesOutValue, suite.runesVaultPkScript)) 103 | 104 | denom := fmt.Sprintf("%s/%s", types.RunesProtocolName, runeId) 105 | 106 | balanceBefore := suite.app.BankKeeper.GetBalance(suite.ctx, sdk.MustAccAddressFromBech32(suite.sender), denom) 107 | suite.True(balanceBefore.Amount.IsZero(), "%s balance before mint should be zero", denom) 108 | 109 | recipient, err := suite.app.BtcBridgeKeeper.Mint(suite.ctx, btcutil.NewTx(tx), btcutil.NewTx(tx), 0) 110 | suite.NoError(err) 111 | suite.Equal(suite.sender, recipient.EncodeAddress(), "incorrect recipient") 112 | 113 | balanceAfter := suite.app.BankKeeper.GetBalance(suite.ctx, sdk.MustAccAddressFromBech32(suite.sender), denom) 114 | suite.Equal(uint64(runeAmount), balanceAfter.Amount.Uint64(), "%s balance after mint should be %d", denom, runeAmount) 115 | 116 | utxos := suite.app.BtcBridgeKeeper.GetAllUTXOs(suite.ctx) 117 | suite.Len(utxos, 1, "there should be 1 utxo(s)") 118 | 119 | expectedUTXO := &types.UTXO{ 120 | Txid: tx.TxHash().String(), 121 | Vout: uint64(runeOutputIndex), 122 | Address: suite.runesVault, 123 | Amount: types.RunesOutValue, 124 | PubKeyScript: suite.runesVaultPkScript, 125 | IsLocked: false, 126 | Runes: []*types.RuneBalance{ 127 | { 128 | Id: runeId, 129 | Amount: fmt.Sprintf("%d", runeAmount), 130 | }, 131 | }, 132 | } 133 | 134 | suite.Equal(expectedUTXO, utxos[0], "utxos do not match") 135 | } 136 | 137 | func (suite *KeeperTestSuite) TestWithdrawRunes() { 138 | runeId := "840000:3" 139 | runeAmount := 500000000 140 | 141 | runesUTXOs := []*types.UTXO{ 142 | { 143 | Txid: chainhash.HashH([]byte("runes")).String(), 144 | Vout: 1, 145 | Address: suite.runesVault, 146 | Amount: types.RunesOutValue, 147 | PubKeyScript: suite.runesVaultPkScript, 148 | IsLocked: false, 149 | Runes: []*types.RuneBalance{ 150 | { 151 | Id: runeId, 152 | Amount: fmt.Sprintf("%d", runeAmount), 153 | }, 154 | }, 155 | }, 156 | } 157 | suite.setupUTXOs(runesUTXOs) 158 | 159 | feeRate := 100 160 | amount := runeAmount + 1 161 | 162 | denom := fmt.Sprintf("%s/%s", types.RunesProtocolName, runeId) 163 | coin := sdk.NewCoin(denom, sdk.NewInt(int64(amount))) 164 | 165 | _, err := suite.app.BtcBridgeKeeper.NewSigningRequest(suite.ctx, suite.sender, coin, int64(feeRate)) 166 | suite.ErrorIs(err, types.ErrInsufficientUTXOs, "should fail due to insufficient runes utxos") 167 | 168 | amount = 100000000 169 | coin = sdk.NewCoin(denom, sdk.NewInt(int64(amount))) 170 | 171 | _, err = suite.app.BtcBridgeKeeper.NewSigningRequest(suite.ctx, suite.sender, coin, int64(feeRate)) 172 | suite.ErrorIs(err, types.ErrInsufficientUTXOs, "should fail due to insufficient payment utxos") 173 | 174 | paymentUTXOs := []*types.UTXO{ 175 | { 176 | Txid: chainhash.HashH([]byte("payment")).String(), 177 | Vout: 1, 178 | Address: suite.btcVault, 179 | Amount: 100000, 180 | PubKeyScript: suite.btcVaultPkScript, 181 | IsLocked: false, 182 | }, 183 | } 184 | suite.setupUTXOs(paymentUTXOs) 185 | 186 | req, err := suite.app.BtcBridgeKeeper.NewSigningRequest(suite.ctx, suite.sender, coin, int64(feeRate)) 187 | suite.NoError(err) 188 | 189 | suite.True(suite.app.BtcBridgeKeeper.IsUTXOLocked(suite.ctx, runesUTXOs[0].Txid, runesUTXOs[0].Vout), "runes utxo should be locked") 190 | suite.True(suite.app.BtcBridgeKeeper.IsUTXOLocked(suite.ctx, paymentUTXOs[0].Txid, paymentUTXOs[0].Vout), "payment utxo should be locked") 191 | 192 | runesUTXOs = suite.app.BtcBridgeKeeper.GetUnlockedUTXOsByAddr(suite.ctx, suite.runesVault) 193 | suite.Len(runesUTXOs, 1, "there should be 1 unlocked runes utxo(s)") 194 | 195 | suite.Len(runesUTXOs[0].Runes, 1, "there should be 1 rune in the runes utxo") 196 | suite.Equal(runeId, runesUTXOs[0].Runes[0].Id, "incorrect rune id") 197 | suite.Equal(uint64(runeAmount-amount), types.RuneAmountFromString(runesUTXOs[0].Runes[0].Amount).Big().Uint64(), "incorrect rune amount") 198 | 199 | p, err := psbt.NewFromRawBytes(bytes.NewReader([]byte(req.Psbt)), true) 200 | suite.NoError(err) 201 | 202 | suite.Len(p.Inputs, 2, "there should be 2 inputs") 203 | suite.Equal(suite.runesVaultPkScript, p.Inputs[0].WitnessUtxo.PkScript, "the first input should be runes vault") 204 | suite.Equal(suite.btcVaultPkScript, p.Inputs[1].WitnessUtxo.PkScript, "the second input should be btc vault") 205 | 206 | expectedRunesScript, err := types.BuildEdictScript(runeId, uint128.From64(uint64(amount)), 2) 207 | suite.NoError(err) 208 | 209 | suite.Len(p.UnsignedTx.TxOut, 4, "there should be 4 outputs") 210 | suite.Equal(expectedRunesScript, p.UnsignedTx.TxOut[0].PkScript, "incorrect runes script") 211 | suite.Equal(suite.runesVaultPkScript, p.UnsignedTx.TxOut[1].PkScript, "the second output should be runes change output") 212 | suite.Equal(suite.senderPkScript, p.UnsignedTx.TxOut[2].PkScript, "the third output should be sender output") 213 | suite.Equal(suite.btcVaultPkScript, p.UnsignedTx.TxOut[3].PkScript, "the fouth output should be btc change output") 214 | } 215 | 216 | func MustPkScriptFromAddress(addr string, chainCfg *chaincfg.Params) []byte { 217 | address, err := btcutil.DecodeAddress(addr, chainCfg) 218 | if err != nil { 219 | panic(err) 220 | } 221 | 222 | pkScript, err := txscript.PayToAddrScript(address) 223 | if err != nil { 224 | panic(err) 225 | } 226 | 227 | return pkScript 228 | } 229 | -------------------------------------------------------------------------------- /x/btcbridge/keeper/msg_server.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "strconv" 8 | 9 | "github.com/btcsuite/btcd/btcutil/psbt" 10 | 11 | "cosmossdk.io/errors" 12 | sdk "github.com/cosmos/cosmos-sdk/types" 13 | govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 14 | 15 | "github.com/sideprotocol/side/x/btcbridge/types" 16 | ) 17 | 18 | type msgServer struct { 19 | Keeper 20 | } 21 | 22 | // SubmitBlockHeaders implements types.MsgServer. 23 | func (m msgServer) SubmitBlockHeaders(goCtx context.Context, msg *types.MsgSubmitBlockHeaderRequest) (*types.MsgSubmitBlockHeadersResponse, error) { 24 | ctx := sdk.UnwrapSDKContext(goCtx) 25 | if err := msg.ValidateBasic(); err != nil { 26 | return nil, err 27 | } 28 | 29 | // check if the sender is one of the authorized senders 30 | param := m.GetParams(ctx) 31 | if !param.IsAuthorizedSender(msg.Sender) { 32 | return nil, types.ErrSenderAddressNotAuthorized 33 | } 34 | 35 | // Set block headers 36 | err := m.SetBlockHeaders(ctx, msg.BlockHeaders) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | // Emit events 42 | // m.EmitEvent( 43 | // ctx, 44 | // msg.Sender, 45 | // sdk.Attribute{ 46 | // Key: types.AttributeKeyPoolCreator, 47 | // Value: msg.Sender, 48 | // }, 49 | // ) 50 | return &types.MsgSubmitBlockHeadersResponse{}, nil 51 | } 52 | 53 | // SubmitTransaction implements types.MsgServer. 54 | // No Permission check required for this message 55 | // Since everyone can submit a transaction to mint voucher tokens 56 | // This message is usually sent by relayers 57 | func (m msgServer) SubmitDepositTransaction(goCtx context.Context, msg *types.MsgSubmitDepositTransactionRequest) (*types.MsgSubmitDepositTransactionResponse, error) { 58 | ctx := sdk.UnwrapSDKContext(goCtx) 59 | 60 | if err := msg.ValidateBasic(); err != nil { 61 | ctx.Logger().Error("Error validating basic", "error", err) 62 | return nil, err 63 | } 64 | 65 | txHash, recipient, err := m.ProcessBitcoinDepositTransaction(ctx, msg) 66 | if err != nil { 67 | ctx.Logger().Error("Error processing bitcoin deposit transaction", "error", err) 68 | return nil, err 69 | } 70 | 71 | // Emit Events 72 | m.EmitEvent(ctx, msg.Sender, 73 | sdk.NewAttribute("blockhash", msg.Blockhash), 74 | sdk.NewAttribute("txBytes", msg.TxBytes), 75 | sdk.NewAttribute("txid", txHash.String()), 76 | sdk.NewAttribute("recipient", recipient.EncodeAddress()), 77 | ) 78 | 79 | return &types.MsgSubmitDepositTransactionResponse{}, nil 80 | } 81 | 82 | // SubmitTransaction implements types.MsgServer. 83 | // No Permission check required for this message 84 | // Since everyone can submit a transaction to mint voucher tokens 85 | // This message is usually sent by relayers 86 | func (m msgServer) SubmitWithdrawTransaction(goCtx context.Context, msg *types.MsgSubmitWithdrawTransactionRequest) (*types.MsgSubmitWithdrawTransactionResponse, error) { 87 | ctx := sdk.UnwrapSDKContext(goCtx) 88 | 89 | if err := msg.ValidateBasic(); err != nil { 90 | ctx.Logger().Error("Error validating basic", "error", err) 91 | return nil, err 92 | } 93 | 94 | txHash, err := m.ProcessBitcoinWithdrawTransaction(ctx, msg) 95 | if err != nil { 96 | ctx.Logger().Error("Error processing bitcoin withdraw transaction", "error", err) 97 | return nil, err 98 | } 99 | 100 | // Emit Events 101 | m.EmitEvent(ctx, msg.Sender, 102 | sdk.NewAttribute("blockhash", msg.Blockhash), 103 | sdk.NewAttribute("txBytes", msg.TxBytes), 104 | sdk.NewAttribute("txid", txHash.String()), 105 | ) 106 | 107 | return &types.MsgSubmitWithdrawTransactionResponse{}, nil 108 | } 109 | 110 | // UpdateSenders implements types.MsgServer. 111 | func (m msgServer) UpdateQualifiedRelayers(goCtx context.Context, msg *types.MsgUpdateQualifiedRelayersRequest) (*types.MsgUpdateQualifiedRelayersResponse, error) { 112 | ctx := sdk.UnwrapSDKContext(goCtx) 113 | if err := msg.ValidateBasic(); err != nil { 114 | return nil, err 115 | } 116 | 117 | // check if the sender is one of the authorized senders 118 | param := m.GetParams(ctx) 119 | if !param.IsAuthorizedSender(msg.Sender) { 120 | return nil, types.ErrSenderAddressNotAuthorized 121 | } 122 | 123 | // Set block headers 124 | m.SetParams(ctx, types.NewParams(msg.Relayers)) 125 | 126 | // Emit events 127 | 128 | return &types.MsgUpdateQualifiedRelayersResponse{}, nil 129 | } 130 | 131 | func (m msgServer) WithdrawBitcoin(goCtx context.Context, msg *types.MsgWithdrawBitcoinRequest) (*types.MsgWithdrawBitcoinResponse, error) { 132 | if err := msg.ValidateBasic(); err != nil { 133 | return nil, err 134 | } 135 | 136 | ctx := sdk.UnwrapSDKContext(goCtx) 137 | 138 | sender := sdk.MustAccAddressFromBech32(msg.Sender) 139 | 140 | coin, err := sdk.ParseCoinNormalized(msg.Amount) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | if coin.Denom == m.GetParams(ctx).BtcVoucherDenom { 146 | if err := types.CheckOutputAmount(msg.Sender, coin.Amount.Int64()); err != nil { 147 | return nil, err 148 | } 149 | } 150 | 151 | feeRate, err := strconv.ParseInt(msg.FeeRate, 10, 64) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | req, err := m.Keeper.NewSigningRequest(ctx, msg.Sender, coin, feeRate) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | if err = m.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, sdk.NewCoins(coin)); err != nil { 162 | return nil, err 163 | } 164 | 165 | m.lockAsset(ctx, req.Txid, coin) 166 | 167 | // Emit events 168 | m.EmitEvent(ctx, msg.Sender, 169 | sdk.NewAttribute("amount", msg.Amount), 170 | sdk.NewAttribute("txid", req.Txid), 171 | ) 172 | 173 | return &types.MsgWithdrawBitcoinResponse{}, nil 174 | } 175 | 176 | // SubmitWithdrawSignatures submits the signatures of the withdraw transaction. 177 | func (m msgServer) SubmitWithdrawSignatures(goCtx context.Context, msg *types.MsgSubmitWithdrawSignaturesRequest) (*types.MsgSubmitWithdrawSignaturesResponse, error) { 178 | if err := msg.ValidateBasic(); err != nil { 179 | return nil, err 180 | } 181 | ctx := sdk.UnwrapSDKContext(goCtx) 182 | exist := m.HasSigningRequest(ctx, msg.Txid) 183 | if !exist { 184 | return nil, types.ErrSigningRequestNotExist 185 | } 186 | 187 | b, err := base64.StdEncoding.DecodeString(msg.Psbt) 188 | if err != nil { 189 | return nil, types.ErrInvalidSignatures 190 | } 191 | 192 | packet, err := psbt.NewFromRawBytes(bytes.NewReader(b), false) 193 | if err != nil { 194 | return nil, err 195 | } 196 | 197 | if packet.UnsignedTx.TxHash().String() != msg.Txid { 198 | return nil, types.ErrInvalidSignatures 199 | } 200 | 201 | if err = packet.SanityCheck(); err != nil { 202 | return nil, err 203 | } 204 | if !packet.IsComplete() { 205 | return nil, types.ErrInvalidSignatures 206 | } 207 | 208 | // verify the signatures 209 | if !types.VerifyPsbtSignatures(packet) { 210 | return nil, types.ErrInvalidSignatures 211 | } 212 | 213 | // Set the signing request status to signed 214 | request := m.GetSigningRequest(ctx, msg.Txid) 215 | request.Psbt = msg.Psbt 216 | request.Status = types.SigningStatus_SIGNING_STATUS_SIGNED 217 | m.SetSigningRequest(ctx, request) 218 | 219 | return &types.MsgSubmitWithdrawSignaturesResponse{}, nil 220 | } 221 | 222 | func (m msgServer) SubmitWithdrawStatus(goCtx context.Context, msg *types.MsgSubmitWithdrawStatusRequest) (*types.MsgSubmitWithdrawStatusResponse, error) { 223 | if err := msg.ValidateBasic(); err != nil { 224 | return nil, err 225 | } 226 | param := m.GetParams(sdk.UnwrapSDKContext(goCtx)) 227 | if !param.IsAuthorizedSender(msg.Sender) { 228 | return nil, types.ErrSenderAddressNotAuthorized 229 | } 230 | ctx := sdk.UnwrapSDKContext(goCtx) 231 | exist := m.HasSigningRequest(ctx, msg.Txid) 232 | if !exist { 233 | return nil, types.ErrSigningRequestNotExist 234 | } 235 | 236 | request := m.GetSigningRequest(ctx, msg.Txid) 237 | request.Status = msg.Status 238 | m.SetSigningRequest(ctx, request) 239 | 240 | return &types.MsgSubmitWithdrawStatusResponse{}, nil 241 | } 242 | 243 | // UpdateParams updates the module params. 244 | func (m msgServer) UpdateParams(goCtx context.Context, msg *types.MsgUpdateParamsRequest) (*types.MsgUpdateParamsResponse, error) { 245 | if m.authority != msg.Authority { 246 | return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", m.authority, msg.Authority) 247 | } 248 | 249 | if err := msg.ValidateBasic(); err != nil { 250 | return nil, err 251 | } 252 | 253 | ctx := sdk.UnwrapSDKContext(goCtx) 254 | m.SetParams(ctx, msg.Params) 255 | 256 | return &types.MsgUpdateParamsResponse{}, nil 257 | } 258 | 259 | // NewMsgServerImpl returns an implementation of the MsgServer interface 260 | // for the provided Keeper. 261 | func NewMsgServerImpl(keeper Keeper) types.MsgServer { 262 | return &msgServer{Keeper: keeper} 263 | } 264 | 265 | var _ types.MsgServer = msgServer{} 266 | -------------------------------------------------------------------------------- /x/btcbridge/keeper/keeper_deposit.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | 8 | "github.com/btcsuite/btcd/blockchain" 9 | "github.com/btcsuite/btcd/btcutil" 10 | "github.com/btcsuite/btcd/chaincfg/chainhash" 11 | "github.com/btcsuite/btcd/txscript" 12 | "github.com/btcsuite/btcd/wire" 13 | 14 | sdk "github.com/cosmos/cosmos-sdk/types" 15 | 16 | "github.com/sideprotocol/side/x/btcbridge/types" 17 | ) 18 | 19 | // Process Bitcoin Deposit Transaction 20 | func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.MsgSubmitDepositTransactionRequest) (*chainhash.Hash, btcutil.Address, error) { 21 | ctx.Logger().Info("accept bitcoin deposit tx", "blockhash", msg.Blockhash) 22 | 23 | params := k.GetParams(ctx) 24 | if !params.IsAuthorizedSender(msg.Sender) { 25 | return nil, nil, types.ErrSenderAddressNotAuthorized 26 | } 27 | 28 | tx, prevTx, err := k.ValidateDepositTransaction(ctx, msg.TxBytes, msg.PrevTxBytes, msg.Blockhash, msg.Proof) 29 | if err != nil { 30 | return nil, nil, err 31 | } 32 | 33 | recipient, err := k.Mint(ctx, tx, prevTx, k.GetBlockHeader(ctx, msg.Blockhash).Height) 34 | if err != nil { 35 | return nil, nil, err 36 | } 37 | 38 | return tx.Hash(), recipient, nil 39 | } 40 | 41 | // validateDepositTransaction validates the deposit transaction 42 | func (k Keeper) ValidateDepositTransaction(ctx sdk.Context, txBytes string, prevTxBytes string, blockHash string, proof []string) (*btcutil.Tx, *btcutil.Tx, error) { 43 | params := k.GetParams(ctx) 44 | 45 | header := k.GetBlockHeader(ctx, blockHash) 46 | // Check if block confirmed 47 | if header == nil || header.Height == 0 { 48 | return nil, nil, types.ErrBlockNotFound 49 | } 50 | 51 | best := k.GetBestBlockHeader(ctx) 52 | // Check if the block is confirmed 53 | if best.Height-header.Height < uint64(params.Confirmations) { 54 | return nil, nil, types.ErrNotConfirmed 55 | } 56 | // Check if the block is within the acceptable depth 57 | // if best.Height-header.Height > param.MaxAcceptableBlockDepth { 58 | // return types.ErrExceedMaxAcceptanceDepth 59 | // } 60 | 61 | // Decode the base64 transaction 62 | rawTx, err := base64.StdEncoding.DecodeString(txBytes) 63 | if err != nil { 64 | fmt.Println("Error decoding transaction from base64:", err) 65 | return nil, nil, err 66 | } 67 | 68 | // Create a new transaction 69 | var tx wire.MsgTx 70 | err = tx.Deserialize(bytes.NewReader(rawTx)) 71 | if err != nil { 72 | fmt.Println("Error deserializing transaction:", err) 73 | return nil, nil, err 74 | } 75 | 76 | uTx := btcutil.NewTx(&tx) 77 | 78 | // Validate the transaction 79 | if err := blockchain.CheckTransactionSanity(uTx); err != nil { 80 | fmt.Println("Transaction is not valid:", err) 81 | return nil, nil, err 82 | } 83 | 84 | // Decode the previous transaction 85 | rawPrevTx, err := base64.StdEncoding.DecodeString(prevTxBytes) 86 | if err != nil { 87 | fmt.Println("Error decoding transaction from base64:", err) 88 | return nil, nil, err 89 | } 90 | 91 | // Create a new transaction 92 | var prevMsgTx wire.MsgTx 93 | err = prevMsgTx.Deserialize(bytes.NewReader(rawPrevTx)) 94 | if err != nil { 95 | fmt.Println("Error deserializing transaction:", err) 96 | return nil, nil, err 97 | } 98 | 99 | prevTx := btcutil.NewTx(&prevMsgTx) 100 | 101 | // Validate the transaction 102 | if err := blockchain.CheckTransactionSanity(prevTx); err != nil { 103 | fmt.Println("Transaction is not valid:", err) 104 | return nil, nil, err 105 | } 106 | 107 | if uTx.MsgTx().TxIn[0].PreviousOutPoint.Hash.String() != prevTx.Hash().String() { 108 | return nil, nil, types.ErrInvalidBtcTransaction 109 | } 110 | 111 | // check if the proof is valid 112 | root, err := chainhash.NewHashFromStr(header.MerkleRoot) 113 | if err != nil { 114 | return nil, nil, err 115 | } 116 | 117 | if !types.VerifyMerkleProof(proof, uTx.Hash(), root) { 118 | k.Logger(ctx).Error("Invalid merkle proof", "txhash", tx, "root", root, "proof", proof) 119 | return nil, nil, types.ErrTransactionNotIncluded 120 | } 121 | 122 | return uTx, prevTx, nil 123 | } 124 | 125 | // mint performs the minting operation of the voucher token 126 | func (k Keeper) Mint(ctx sdk.Context, tx *btcutil.Tx, prevTx *btcutil.Tx, height uint64) (btcutil.Address, error) { 127 | params := k.GetParams(ctx) 128 | chainCfg := sdk.GetConfig().GetBtcChainCfg() 129 | 130 | // check if this is a valid runes deposit tx 131 | // if any error encountered, this tx is illegal runes deposit 132 | // if the edict is not nil, it indicates that this is a legal runes deposit tx 133 | edict, err := types.CheckRunesDepositTransaction(tx.MsgTx(), params.Vaults) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | isRunes := edict != nil 139 | 140 | // extract the recipient for minting voucher token 141 | recipient, err := types.ExtractRecipientAddr(tx.MsgTx(), prevTx.MsgTx(), params.Vaults, isRunes, chainCfg) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | // mint voucher token and save utxo if the receiver is a vault address 147 | for i, out := range tx.MsgTx().TxOut { 148 | if types.IsOpReturnOutput(out) { 149 | continue 150 | } 151 | 152 | // check if the output is a valid address 153 | pks, err := txscript.ParsePkScript(out.PkScript) 154 | if err != nil { 155 | return nil, err 156 | } 157 | addr, err := pks.Address(chainCfg) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | // check if the receiver is one of the voucher addresses 163 | vault := types.SelectVaultByBitcoinAddress(params.Vaults, addr.EncodeAddress()) 164 | if vault == nil { 165 | continue 166 | } 167 | 168 | // mint the voucher token by asset type and save utxos 169 | // skip if the asset type of the sender address is unspecified 170 | switch vault.AssetType { 171 | case types.AssetType_ASSET_TYPE_BTC: 172 | err := k.mintBTC(ctx, tx, height, recipient.EncodeAddress(), vault, out, i, params.BtcVoucherDenom) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | case types.AssetType_ASSET_TYPE_RUNE: 178 | if isRunes && edict.Output == uint32(i) { 179 | if err := k.mintRunes(ctx, tx, height, recipient.EncodeAddress(), vault, out, i, edict.Id, edict.Amount); err != nil { 180 | return nil, err 181 | } 182 | } 183 | } 184 | } 185 | 186 | return recipient, nil 187 | } 188 | 189 | func (k Keeper) mintBTC(ctx sdk.Context, tx *btcutil.Tx, height uint64, sender string, vault *types.Vault, out *wire.TxOut, vout int, denom string) error { 190 | // save the hash of the transaction to prevent double minting 191 | hash := tx.Hash().String() 192 | if k.existsInHistory(ctx, hash) { 193 | return types.ErrTransactionAlreadyMinted 194 | } 195 | k.addToMintHistory(ctx, hash) 196 | 197 | // mint the voucher token 198 | if len(denom) == 0 { 199 | denom = "sat" 200 | } 201 | coins := sdk.NewCoins(sdk.NewCoin(denom, sdk.NewInt(out.Value))) 202 | 203 | receipient, err := sdk.AccAddressFromBech32(sender) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, coins); err != nil { 209 | return err 210 | } 211 | 212 | if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receipient, coins); err != nil { 213 | return err 214 | } 215 | 216 | utxo := types.UTXO{ 217 | Txid: hash, 218 | Vout: uint64(vout), 219 | Amount: uint64(out.Value), 220 | PubKeyScript: out.PkScript, 221 | Height: height, 222 | Address: vault.Address, 223 | IsCoinbase: false, 224 | IsLocked: false, 225 | } 226 | 227 | k.saveUTXO(ctx, &utxo) 228 | 229 | return nil 230 | } 231 | 232 | func (k Keeper) mintRunes(ctx sdk.Context, tx *btcutil.Tx, height uint64, recipient string, vault *types.Vault, out *wire.TxOut, vout int, id *types.RuneId, amount string) error { 233 | // save the hash of the transaction to prevent double minting 234 | hash := tx.Hash().String() 235 | if k.existsInHistory(ctx, hash) { 236 | return types.ErrTransactionAlreadyMinted 237 | } 238 | k.addToMintHistory(ctx, hash) 239 | 240 | coins := sdk.NewCoins(sdk.NewCoin(id.Denom(), sdk.NewIntFromBigInt(types.RuneAmountFromString(amount).Big()))) 241 | 242 | receipientAddr, err := sdk.AccAddressFromBech32(recipient) 243 | if err != nil { 244 | return err 245 | } 246 | 247 | if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, coins); err != nil { 248 | return err 249 | } 250 | 251 | if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receipientAddr, coins); err != nil { 252 | return err 253 | } 254 | 255 | utxo := types.UTXO{ 256 | Txid: hash, 257 | Vout: uint64(vout), 258 | Amount: uint64(out.Value), 259 | PubKeyScript: out.PkScript, 260 | Height: height, 261 | Address: vault.Address, 262 | IsCoinbase: false, 263 | IsLocked: false, 264 | Runes: []*types.RuneBalance{{ 265 | Id: id.ToString(), 266 | Amount: amount, 267 | }}, 268 | } 269 | 270 | k.saveUTXO(ctx, &utxo) 271 | 272 | return nil 273 | } 274 | 275 | func (k Keeper) existsInHistory(ctx sdk.Context, txHash string) bool { 276 | store := ctx.KVStore(k.storeKey) 277 | 278 | return store.Has(types.BtcMintedTxHashKey(txHash)) 279 | } 280 | 281 | func (k Keeper) addToMintHistory(ctx sdk.Context, txHash string) { 282 | store := ctx.KVStore(k.storeKey) 283 | 284 | store.Set(types.BtcMintedTxHashKey(txHash), []byte{1}) 285 | } 286 | 287 | // need a query all history for exporting 288 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sideprotocol/side 2 | 3 | go 1.19 4 | 5 | require ( 6 | cosmossdk.io/api v0.3.1 7 | cosmossdk.io/errors v1.0.1 8 | cosmossdk.io/math v1.3.0 9 | github.com/CosmWasm/wasmd v0.45.0 10 | github.com/Stride-Labs/stride/v16 v16.0.0 11 | github.com/btcsuite/btcd v0.24.0 12 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 13 | github.com/btcsuite/btcd/btcutil v1.1.5 14 | github.com/btcsuite/btcd/btcutil/psbt v1.1.9 15 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 16 | github.com/cometbft/cometbft v0.37.4 17 | github.com/cometbft/cometbft-db v0.8.0 18 | github.com/cosmos/btcutil v1.0.5 19 | github.com/cosmos/cosmos-sdk v0.47.5 20 | github.com/cosmos/gogoproto v1.4.10 21 | github.com/cosmos/ibc-go/v7 v7.3.0 22 | github.com/golang/protobuf v1.5.3 23 | github.com/gorilla/mux v1.8.0 24 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 25 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 26 | github.com/prometheus/client_golang v1.16.0 27 | github.com/spf13/cast v1.5.1 28 | github.com/spf13/cobra v1.7.0 29 | github.com/spf13/pflag v1.0.5 30 | github.com/stretchr/testify v1.8.4 31 | google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 32 | google.golang.org/grpc v1.60.1 33 | lukechampine.com/uint128 v1.3.0 34 | ) 35 | 36 | require ( 37 | cloud.google.com/go v0.111.0 // indirect 38 | cloud.google.com/go/compute v1.23.3 // indirect 39 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 40 | cloud.google.com/go/iam v1.1.5 // indirect 41 | cloud.google.com/go/storage v1.30.1 // indirect 42 | cosmossdk.io/core v0.5.1 // indirect 43 | cosmossdk.io/depinject v1.0.0-alpha.4 // indirect 44 | cosmossdk.io/log v1.3.1 // indirect 45 | cosmossdk.io/tools/rosetta v0.2.1 // indirect 46 | filippo.io/edwards25519 v1.0.0 // indirect 47 | github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect 48 | github.com/99designs/keyring v1.2.1 // indirect 49 | github.com/ChainSafe/go-schnorrkel v1.0.0 // indirect 50 | github.com/CosmWasm/wasmvm v1.5.2 // indirect 51 | github.com/aead/siphash v1.0.1 // indirect 52 | github.com/armon/go-metrics v0.4.1 // indirect 53 | github.com/aws/aws-sdk-go v1.44.203 // indirect 54 | github.com/beorn7/perks v1.0.1 // indirect 55 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect 56 | github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect 57 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect 58 | github.com/cenkalti/backoff/v4 v4.1.3 // indirect 59 | github.com/cespare/xxhash v1.1.0 // indirect 60 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 61 | github.com/chzyer/readline v1.5.1 // indirect 62 | github.com/cockroachdb/apd/v2 v2.0.2 // indirect 63 | github.com/cockroachdb/errors v1.10.0 // indirect 64 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect 65 | github.com/cockroachdb/redact v1.1.5 // indirect 66 | github.com/coinbase/rosetta-sdk-go/types v1.0.0 // indirect 67 | github.com/confio/ics23/go v0.9.0 // indirect 68 | github.com/cosmos/cosmos-proto v1.0.0-beta.4 // indirect 69 | github.com/cosmos/go-bip39 v1.0.0 // indirect 70 | github.com/cosmos/gogogateway v1.2.0 // indirect 71 | github.com/cosmos/iavl v0.20.1 // indirect 72 | github.com/cosmos/ics23/go v0.10.0 // indirect 73 | github.com/cosmos/interchain-security/v3 v3.1.0 // indirect 74 | github.com/cosmos/ledger-cosmos-go v0.12.4 // indirect 75 | github.com/cosmos/rosetta-sdk-go v0.10.0 // indirect 76 | github.com/creachadair/taskgroup v0.4.2 // indirect 77 | github.com/danieljoos/wincred v1.1.2 // indirect 78 | github.com/davecgh/go-spew v1.1.1 // indirect 79 | github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect 80 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 81 | github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect 82 | github.com/dgraph-io/badger/v2 v2.2007.4 // indirect 83 | github.com/dgraph-io/ristretto v0.1.1 // indirect 84 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect 85 | github.com/docker/distribution v2.8.2+incompatible // indirect 86 | github.com/dustin/go-humanize v1.0.1 // indirect 87 | github.com/dvsekhvalnov/jose2go v1.6.0 // indirect 88 | github.com/felixge/httpsnoop v1.0.2 // indirect 89 | github.com/fsnotify/fsnotify v1.6.0 // indirect 90 | github.com/getsentry/sentry-go v0.23.0 // indirect 91 | github.com/ghodss/yaml v1.0.0 // indirect 92 | github.com/go-kit/kit v0.12.0 // indirect 93 | github.com/go-kit/log v0.2.1 // indirect 94 | github.com/go-logfmt/logfmt v0.6.0 // indirect 95 | github.com/go-logr/logr v1.2.4 // indirect 96 | github.com/go-logr/stdr v1.2.2 // indirect 97 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect 98 | github.com/gogo/googleapis v1.4.1 // indirect 99 | github.com/gogo/protobuf v1.3.3 // indirect 100 | github.com/golang/glog v1.1.2 // indirect 101 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 102 | github.com/golang/mock v1.6.0 // indirect 103 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect 104 | github.com/google/btree v1.1.2 // indirect 105 | github.com/google/go-cmp v0.6.0 // indirect 106 | github.com/google/gofuzz v1.2.0 // indirect 107 | github.com/google/orderedcode v0.0.1 // indirect 108 | github.com/google/s2a-go v0.1.7 // indirect 109 | github.com/google/uuid v1.4.0 // indirect 110 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 111 | github.com/googleapis/gax-go/v2 v2.12.0 // indirect 112 | github.com/gorilla/handlers v1.5.1 // indirect 113 | github.com/gorilla/websocket v1.5.0 // indirect 114 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 115 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect 116 | github.com/gtank/merlin v0.1.1 // indirect 117 | github.com/gtank/ristretto255 v0.1.2 // indirect 118 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 119 | github.com/hashicorp/go-getter v1.7.1 // indirect 120 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 121 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 122 | github.com/hashicorp/go-version v1.6.0 // indirect 123 | github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect 124 | github.com/hashicorp/hcl v1.0.0 // indirect 125 | github.com/hdevalence/ed25519consensus v0.1.0 // indirect 126 | github.com/huandu/skiplist v1.2.0 // indirect 127 | github.com/improbable-eng/grpc-web v0.15.0 // indirect 128 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 129 | github.com/jmespath/go-jmespath v0.4.0 // indirect 130 | github.com/jmhodges/levigo v1.0.0 // indirect 131 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect 132 | github.com/klauspost/compress v1.16.7 // indirect 133 | github.com/kr/pretty v0.3.1 // indirect 134 | github.com/kr/text v0.2.0 // indirect 135 | github.com/lib/pq v1.10.7 // indirect 136 | github.com/libp2p/go-buffer-pool v0.1.0 // indirect 137 | github.com/linxGnu/grocksdb v1.7.16 // indirect 138 | github.com/magiconair/properties v1.8.7 // indirect 139 | github.com/manifoldco/promptui v0.9.0 // indirect 140 | github.com/mattn/go-colorable v0.1.13 // indirect 141 | github.com/mattn/go-isatty v0.0.20 // indirect 142 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 143 | github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect 144 | github.com/minio/highwayhash v1.0.2 // indirect 145 | github.com/mitchellh/go-homedir v1.1.0 // indirect 146 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 147 | github.com/mitchellh/mapstructure v1.5.0 // indirect 148 | github.com/mtibben/percent v0.2.1 // indirect 149 | github.com/opencontainers/go-digest v1.0.0 // indirect 150 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 151 | github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect 152 | github.com/pkg/errors v0.9.1 // indirect 153 | github.com/pmezard/go-difflib v1.0.0 // indirect 154 | github.com/prometheus/client_model v0.3.0 // indirect 155 | github.com/prometheus/common v0.42.0 // indirect 156 | github.com/prometheus/procfs v0.10.1 // indirect 157 | github.com/rakyll/statik v0.1.7 // indirect 158 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 159 | github.com/rogpeppe/go-internal v1.11.0 // indirect 160 | github.com/rs/cors v1.8.3 // indirect 161 | github.com/rs/zerolog v1.32.0 // indirect 162 | github.com/sasha-s/go-deadlock v0.3.1 // indirect 163 | github.com/spf13/afero v1.9.5 // indirect 164 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 165 | github.com/spf13/viper v1.16.0 // indirect 166 | github.com/subosito/gotenv v1.4.2 // indirect 167 | github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect 168 | github.com/tendermint/go-amino v0.16.0 // indirect 169 | github.com/tidwall/btree v1.6.0 // indirect 170 | github.com/ulikunitz/xz v0.5.11 // indirect 171 | github.com/zondax/hid v0.9.2 // indirect 172 | github.com/zondax/ledger-go v0.14.3 // indirect 173 | go.etcd.io/bbolt v1.3.7 // indirect 174 | go.opencensus.io v0.24.0 // indirect 175 | go.opentelemetry.io/otel v1.19.0 // indirect 176 | go.opentelemetry.io/otel/metric v1.19.0 // indirect 177 | go.opentelemetry.io/otel/trace v1.19.0 // indirect 178 | golang.org/x/crypto v0.21.0 // indirect 179 | golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb // indirect 180 | golang.org/x/net v0.21.0 // indirect 181 | golang.org/x/oauth2 v0.13.0 // indirect 182 | golang.org/x/sync v0.4.0 // indirect 183 | golang.org/x/sys v0.18.0 // indirect 184 | golang.org/x/term v0.18.0 // indirect 185 | golang.org/x/text v0.14.0 // indirect 186 | google.golang.org/api v0.149.0 // indirect 187 | google.golang.org/appengine v1.6.8 // indirect 188 | google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 // indirect 189 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect 190 | google.golang.org/protobuf v1.32.0 // indirect 191 | gopkg.in/ini.v1 v1.67.0 // indirect 192 | gopkg.in/yaml.v2 v2.4.0 // indirect 193 | gopkg.in/yaml.v3 v3.0.1 // indirect 194 | nhooyr.io/websocket v1.8.6 // indirect 195 | pgregory.net/rapid v1.1.0 // indirect 196 | sigs.k8s.io/yaml v1.4.0 // indirect 197 | ) 198 | 199 | replace ( 200 | github.com/cosmos/cosmos-sdk => github.com/sideprotocol/cosmos-sdk v0.47.11-btc1 201 | // github.com/cosmos/cosmos-sdk => ../cosmos-sdk 202 | github.com/cosmos/interchain-security/v3 => github.com/Stride-Labs/interchain-security/v3 v3.1.0-remove-validation-bug-7d3d9d 203 | github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 204 | github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 205 | ) 206 | --------------------------------------------------------------------------------