From 5785f921f2908149c127786bbe0c3aa60fa7be9b Mon Sep 17 00:00:00 2001 From: sufay Date: Mon, 1 Jul 2024 01:34:28 +0800 Subject: [PATCH] add runes support --- go.mod | 7 +- go.sum | 9 +- local_node_dev.sh | 10 +- proto/side/btcbridge/bitcoin.proto | 24 + x/btcbridge/keeper/keeper_deposit.go | 170 ++-- x/btcbridge/keeper/keeper_test.go | 224 ++++- x/btcbridge/keeper/keeper_withdraw.go | 95 +- x/btcbridge/keeper/msg_server.go | 9 +- x/btcbridge/keeper/utxo.go | 69 +- x/btcbridge/types/asset.go | 19 + x/btcbridge/types/bitcoin.pb.go | 831 +++++++++++++++++- x/btcbridge/types/bitcoin_transaction.go | 205 ++++- x/btcbridge/types/deposit_policy.go | 103 ++- x/btcbridge/types/errors.go | 10 +- x/btcbridge/types/keys.go | 16 +- x/btcbridge/types/message_withdraw_bitcoin.go | 7 +- x/btcbridge/types/params.go | 43 +- x/btcbridge/types/runes.go | 209 +++++ x/btcbridge/types/runes_test.go | 84 ++ x/btcbridge/types/varint.go | 96 ++ 20 files changed, 2060 insertions(+), 180 deletions(-) create mode 100644 x/btcbridge/types/asset.go create mode 100644 x/btcbridge/types/runes.go create mode 100644 x/btcbridge/types/runes_test.go create mode 100644 x/btcbridge/types/varint.go diff --git a/go.mod b/go.mod index d4d983f1..c1be8c65 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/spf13/cast v1.5.1 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 ) require ( @@ -56,7 +56,6 @@ require ( github.com/confio/ics23/go v0.9.0 // indirect github.com/containerd/containerd v1.6.8 // indirect github.com/containerd/typeurl v1.0.2 // indirect - github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v0.20.1 // indirect @@ -172,7 +171,7 @@ require ( github.com/spf13/afero v1.9.5 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.16.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect @@ -221,6 +220,7 @@ require ( github.com/btcsuite/btcd/btcutil/psbt v1.1.9 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/bufbuild/buf v1.7.0 + github.com/cosmos/btcutil v1.0.5 github.com/cosmos/cosmos-proto v1.0.0-beta.4 github.com/cosmos/ics23/go v0.10.0 github.com/prometheus/client_golang v1.16.0 @@ -231,6 +231,7 @@ require ( google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 google.golang.org/protobuf v1.32.0 gopkg.in/yaml.v2 v2.4.0 + lukechampine.com/uint128 v1.3.0 ) replace ( diff --git a/go.sum b/go.sum index a3dcade9..31e201a6 100644 --- a/go.sum +++ b/go.sum @@ -1018,8 +1018,9 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1030,8 +1031,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= @@ -1748,6 +1749,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= +lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= diff --git a/local_node_dev.sh b/local_node_dev.sh index 60933ad5..d4635f3e 100755 --- a/local_node_dev.sh +++ b/local_node_dev.sh @@ -91,9 +91,15 @@ if [[ $overwrite == "y" || $overwrite == "Y" ]]; then # setup relayers RELAYER=$($BINARY keys show "${KEYS[2]}" -a --keyring-backend $KEYRING --home "$HOMEDIR") jq --arg relayer "$RELAYER" '.app_state["btcbridge"]["params"]["authorized_relayers"][0]=$relayer' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" - jq --arg relayer "$RELAYER" '.app_state["btcbridge"]["params"]["vaults"][0]["address"]=$relayer' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" - PUKEY=$($BINARY keys show "${KEYS[2]}" --pubkeyhex --keyring-backend $KEYRING --home "$HOMEDIR") + # setup vaults + VAULT1=$($BINARY keys show "${KEYS[1]}" -a --keyring-backend $KEYRING --home "$HOMEDIR") + jq --arg vault1 "$VAULT1" '.app_state["btcbridge"]["params"]["vaults"][0]["address"]=$vault1' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" + PUKEY=$($BINARY keys show "${KEYS[1]}" --pubkeyhex --keyring-backend $KEYRING --home "$HOMEDIR") jq --arg pubkey "$PUKEY" '.app_state["btcbridge"]["params"]["vaults"][0]["pub_key"]=$pubkey' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" + VAULT2=$RELAYER + jq --arg vault2 "$VAULT2" '.app_state["btcbridge"]["params"]["vaults"][1]["address"]=$vault2' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" + PUKEY=$($BINARY keys show "${KEYS[2]}" --pubkeyhex --keyring-backend $KEYRING --home "$HOMEDIR") + jq --arg pubkey "$PUKEY" '.app_state["btcbridge"]["params"]["vaults"][1]["pub_key"]=$pubkey' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" # set custom pruning settings sed -i.bak 's/pruning = "default"/pruning = "custom"/g' "$APP_TOML" diff --git a/proto/side/btcbridge/bitcoin.proto b/proto/side/btcbridge/bitcoin.proto index 775eb904..6b532e64 100644 --- a/proto/side/btcbridge/bitcoin.proto +++ b/proto/side/btcbridge/bitcoin.proto @@ -56,5 +56,29 @@ message UTXO { bytes pub_key_script = 6; bool is_coinbase = 7; bool is_locked = 8; + // rune balances associated with the UTXO + repeated RuneBalance runes = 9; } +// Rune Balance +message RuneBalance { + // serialized rune id + string id = 1; + // rune amount + string amount = 2; +} + +// Rune ID +message RuneId { + // block height + uint64 block = 1; + // tx index + uint32 tx = 2; +} + +// Rune Edict +message Edict { + RuneId id = 1; + string amount = 2; + uint32 output = 3; +} diff --git a/x/btcbridge/keeper/keeper_deposit.go b/x/btcbridge/keeper/keeper_deposit.go index dd02d04a..c1ea2d00 100644 --- a/x/btcbridge/keeper/keeper_deposit.go +++ b/x/btcbridge/keeper/keeper_deposit.go @@ -10,22 +10,39 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/sideprotocol/side/x/btcbridge/types" ) // Process Bitcoin Deposit Transaction func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.MsgSubmitDepositTransactionRequest) (*chainhash.Hash, btcutil.Address, error) { - ctx.Logger().Info("accept bitcoin deposit tx", "blockhash", msg.Blockhash) - param := k.GetParams(ctx) - - if !param.IsAuthorizedSender(msg.Sender) { + params := k.GetParams(ctx) + if !params.IsAuthorizedSender(msg.Sender) { return nil, nil, types.ErrSenderAddressNotAuthorized } - header := k.GetBlockHeader(ctx, msg.Blockhash) + tx, prevTx, err := k.ValidateDepositTransaction(ctx, msg.TxBytes, msg.PrevTxBytes, msg.Blockhash, msg.Proof) + if err != nil { + return nil, nil, err + } + + recipient, err := k.Mint(ctx, tx, prevTx, k.GetBlockHeader(ctx, msg.Blockhash).Height) + if err != nil { + return nil, nil, err + } + + return tx.Hash(), recipient, nil +} + +// validateDepositTransaction validates the deposit transaction +func (k Keeper) ValidateDepositTransaction(ctx sdk.Context, txBytes string, prevTxBytes string, blockHash string, proof []string) (*btcutil.Tx, *btcutil.Tx, error) { + params := k.GetParams(ctx) + + header := k.GetBlockHeader(ctx, blockHash) // Check if block confirmed if header == nil || header.Height == 0 { return nil, nil, types.ErrBlockNotFound @@ -33,7 +50,7 @@ func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.Msg best := k.GetBestBlockHeader(ctx) // Check if the block is confirmed - if best.Height-header.Height < uint64(param.Confirmations) { + if best.Height-header.Height < uint64(params.Confirmations) { return nil, nil, types.ErrNotConfirmed } // Check if the block is within the acceptable depth @@ -42,7 +59,7 @@ func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.Msg // } // Decode the base64 transaction - txBytes, err := base64.StdEncoding.DecodeString(msg.TxBytes) + rawTx, err := base64.StdEncoding.DecodeString(txBytes) if err != nil { fmt.Println("Error decoding transaction from base64:", err) return nil, nil, err @@ -50,15 +67,13 @@ func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.Msg // Create a new transaction var tx wire.MsgTx - err = tx.Deserialize(bytes.NewReader(txBytes)) + err = tx.Deserialize(bytes.NewReader(rawTx)) if err != nil { fmt.Println("Error deserializing transaction:", err) return nil, nil, err } + uTx := btcutil.NewTx(&tx) - if len(uTx.MsgTx().TxIn) < 1 { - return nil, nil, types.ErrInvalidBtcTransaction - } // Validate the transaction if err := blockchain.CheckTransactionSanity(uTx); err != nil { @@ -67,7 +82,7 @@ func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.Msg } // Decode the previous transaction - prevTxBytes, err := base64.StdEncoding.DecodeString(msg.PrevTxBytes) + rawPrevTx, err := base64.StdEncoding.DecodeString(prevTxBytes) if err != nil { fmt.Println("Error decoding transaction from base64:", err) return nil, nil, err @@ -75,16 +90,14 @@ func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.Msg // Create a new transaction var prevMsgTx wire.MsgTx - err = prevMsgTx.Deserialize(bytes.NewReader(prevTxBytes)) + err = prevMsgTx.Deserialize(bytes.NewReader(rawPrevTx)) if err != nil { fmt.Println("Error deserializing transaction:", err) return nil, nil, err } prevTx := btcutil.NewTx(&prevMsgTx) - if len(prevTx.MsgTx().TxOut) < 1 { - return nil, nil, types.ErrInvalidBtcTransaction - } + // Validate the transaction if err := blockchain.CheckTransactionSanity(prevTx); err != nil { fmt.Println("Transaction is not valid:", err) @@ -95,44 +108,59 @@ func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.Msg return nil, nil, types.ErrInvalidBtcTransaction } - chainCfg := sdk.GetConfig().GetBtcChainCfg() - - // Extract the recipient address - recipient, err := types.ExtractRecipientAddr(&tx, &prevMsgTx, param.Vaults, chainCfg) + // check if the proof is valid + root, err := chainhash.NewHashFromStr(header.MerkleRoot) if err != nil { return nil, nil, err } - // if pk.Class() != txscript.WitnessV1TaprootTy || pk.Class() != txscript.WitnessV0PubKeyHashTy || pk.Class() != txscript.WitnessV0ScriptHashTy { - // ctx.Logger().Error("Unsupported script type", "script", pk.Class(), "address", sender.EncodeAddress()) - // return types.ErrUnsupportedScriptType - // } + if !types.VerifyMerkleProof(proof, uTx.Hash(), root) { + k.Logger(ctx).Error("Invalid merkle proof", "txhash", tx, "root", root, "proof", proof) + return nil, nil, types.ErrTransactionNotIncluded + } - // check if the proof is valid - root, err := chainhash.NewHashFromStr(header.MerkleRoot) + return uTx, prevTx, nil +} + +// mint performs the minting operation of the voucher token +func (k Keeper) Mint(ctx sdk.Context, tx *btcutil.Tx, prevTx *btcutil.Tx, height uint64) (btcutil.Address, error) { + params := k.GetParams(ctx) + chainCfg := sdk.GetConfig().GetBtcChainCfg() + + // check if this is a valid runes deposit tx + // if any error encountered, this tx is illegal runes deposit + // if the edict is not nil, it indicates that this is a legal runes deposit tx + edict, err := types.CheckRunesDepositTransaction(tx.MsgTx(), params.Vaults) if err != nil { - return nil, nil, err + return nil, err } - txhash := uTx.MsgTx().TxHash() - if !types.VerifyMerkleProof(msg.Proof, &txhash, root) { - k.Logger(ctx).Error("Invalid merkle proof", "txhash", tx, "root", root, "proof", msg.Proof) - return nil, nil, types.ErrTransactionNotIncluded + isRunes := edict != nil + + // extract the recipient for minting voucher token + recipient, err := types.ExtractRecipientAddr(tx.MsgTx(), prevTx.MsgTx(), params.Vaults, isRunes, chainCfg) + if err != nil { + return nil, err } // mint voucher token and save utxo if the receiver is a vault address - for i, out := range uTx.MsgTx().TxOut { + for i, out := range tx.MsgTx().TxOut { + if types.IsOpReturnOutput(out) { + continue + } + // check if the output is a valid address pks, err := txscript.ParsePkScript(out.PkScript) if err != nil { - return nil, nil, err + return nil, err } addr, err := pks.Address(chainCfg) if err != nil { - return nil, nil, err + return nil, err } + // check if the receiver is one of the voucher addresses - vault := types.SelectVaultByBitcoinAddress(param.Vaults, addr.EncodeAddress()) + vault := types.SelectVaultByBitcoinAddress(params.Vaults, addr.EncodeAddress()) if vault == nil { continue } @@ -141,22 +169,26 @@ func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.Msg // skip if the asset type of the sender address is unspecified switch vault.AssetType { case types.AssetType_ASSET_TYPE_BTC: - err := k.mintBTC(ctx, uTx, header.Height, recipient.EncodeAddress(), vault, out, i, param.BtcVoucherDenom) + err := k.mintBTC(ctx, tx, height, recipient.EncodeAddress(), vault, out, i, params.BtcVoucherDenom) if err != nil { - return nil, nil, err + return nil, err } + case types.AssetType_ASSET_TYPE_RUNE: - k.mintRUNE(ctx, uTx, header.Height, recipient.EncodeAddress(), vault, out, i, "rune") + if isRunes && edict.Output == uint32(i) { + if err := k.mintRunes(ctx, tx, height, recipient.EncodeAddress(), vault, out, i, edict.Id, edict.Amount); err != nil { + return nil, err + } + } } } - return &txhash, recipient, nil + return recipient, nil } -func (k Keeper) mintBTC(ctx sdk.Context, uTx *btcutil.Tx, height uint64, sender string, vault *types.Vault, out *wire.TxOut, vout int, denom string) error { - +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 { // save the hash of the transaction to prevent double minting - hash := uTx.Hash().String() + hash := tx.Hash().String() if k.existsInHistory(ctx, hash) { return types.ErrTransactionAlreadyMinted } @@ -182,7 +214,7 @@ func (k Keeper) mintBTC(ctx sdk.Context, uTx *btcutil.Tx, height uint64, sender } utxo := types.UTXO{ - Txid: uTx.Hash().String(), + Txid: hash, Vout: uint64(vout), Amount: uint64(out.Value), PubKeyScript: out.PkScript, @@ -197,17 +229,47 @@ func (k Keeper) mintBTC(ctx sdk.Context, uTx *btcutil.Tx, height uint64, sender return nil } -func (k Keeper) mintRUNE(ctx sdk.Context, uTx *btcutil.Tx, height uint64, sender string, vault *types.Vault, out *wire.TxOut, vout int, denom string) { - // TODO - - _ = ctx - _ = uTx - _ = height - _ = sender - _ = vault - _ = out - _ = vout - _ = denom +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 { + // save the hash of the transaction to prevent double minting + hash := tx.Hash().String() + if k.existsInHistory(ctx, hash) { + return types.ErrTransactionAlreadyMinted + } + k.addToMintHistory(ctx, hash) + + coins := sdk.NewCoins(sdk.NewCoin(id.Denom(), sdk.NewIntFromBigInt(types.RuneAmountFromString(amount).Big()))) + + receipientAddr, err := sdk.AccAddressFromBech32(recipient) + if err != nil { + return err + } + + if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, coins); err != nil { + return err + } + + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receipientAddr, coins); err != nil { + return err + } + + utxo := types.UTXO{ + Txid: hash, + Vout: uint64(vout), + Amount: uint64(out.Value), + PubKeyScript: out.PkScript, + Height: height, + Address: vault.Address, + IsCoinbase: false, + IsLocked: false, + Runes: []*types.RuneBalance{{ + Id: id.ToString(), + Amount: amount, + }}, + } + + k.saveUTXO(ctx, &utxo) + + return nil } func (k Keeper) existsInHistory(ctx sdk.Context, txHash string) bool { diff --git a/x/btcbridge/keeper/keeper_test.go b/x/btcbridge/keeper/keeper_test.go index 970bc76d..d8bf5cf2 100644 --- a/x/btcbridge/keeper/keeper_test.go +++ b/x/btcbridge/keeper/keeper_test.go @@ -1,14 +1,228 @@ package keeper_test import ( + "bytes" + "fmt" "testing" + "time" + + "github.com/stretchr/testify/suite" + "lukechampine.com/uint128" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/btcutil/bech32" + "github.com/cosmos/cosmos-sdk/crypto/keys/segwit" + sdk "github.com/cosmos/cosmos-sdk/types" + + simapp "github.com/sideprotocol/side/app" + "github.com/sideprotocol/side/x/btcbridge/types" ) -func TestCreatePsbt(t *testing.T) { - // create BitCoin Transaction +type KeeperTestSuite struct { + suite.Suite + + ctx sdk.Context + app *simapp.App + + btcVault string + runesVault string + sender string + + btcVaultPkScript []byte + runesVaultPkScript []byte + senderPkScript []byte +} + +func (suite *KeeperTestSuite) SetupTest() { + app := simapp.Setup(suite.T()) + ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Now().UTC()}) + + suite.ctx = ctx + suite.app = app + + chainCfg := sdk.GetConfig().GetBtcChainCfg() + + suite.btcVault, _ = bech32.Encode(chainCfg.Bech32HRPSegwit, segwit.GenPrivKey().PubKey().Address().Bytes()) + suite.runesVault, _ = bech32.Encode(chainCfg.Bech32HRPSegwit, segwit.GenPrivKey().PubKey().Address()) + suite.sender, _ = bech32.Encode(chainCfg.Bech32HRPSegwit, segwit.GenPrivKey().PubKey().Address()) + + suite.btcVaultPkScript = MustPkScriptFromAddress(suite.btcVault, chainCfg) + suite.runesVaultPkScript = MustPkScriptFromAddress(suite.runesVault, chainCfg) + suite.senderPkScript = MustPkScriptFromAddress(suite.sender, chainCfg) + + suite.setupParams(suite.btcVault, suite.runesVault) +} + +func TestKeeperSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} + +func (suite *KeeperTestSuite) setupParams(btcVault string, runesVault string) { + suite.app.BtcBridgeKeeper.SetParams(suite.ctx, types.Params{Vaults: []*types.Vault{ + { + Address: btcVault, + AssetType: types.AssetType_ASSET_TYPE_BTC, + }, + { + Address: runesVault, + AssetType: types.AssetType_ASSET_TYPE_RUNE, + }, + }}) +} + +func (suite *KeeperTestSuite) setupUTXOs(utxos []*types.UTXO) { + for _, utxo := range utxos { + suite.app.BtcBridgeKeeper.SetUTXO(suite.ctx, utxo) + suite.app.BtcBridgeKeeper.SetOwnerUTXO(suite.ctx, utxo) + + for _, r := range utxo.Runes { + suite.app.BtcBridgeKeeper.SetOwnerRunesUTXO(suite.ctx, utxo, r.Id, r.Amount) + } + } +} + +func (suite *KeeperTestSuite) TestMintRunes() { + runeId := "840000:3" + runeAmount := 500000000 + runeOutputIndex := 2 + + runesScript, err := types.BuildEdictScript(runeId, uint128.From64(uint64(runeAmount)), uint32(runeOutputIndex)) + suite.NoError(err) + + tx := wire.NewMsgTx(types.TxVersion) + tx.AddTxOut(wire.NewTxOut(0, runesScript)) + tx.AddTxOut(wire.NewTxOut(types.RunesOutValue, suite.senderPkScript)) + tx.AddTxOut(wire.NewTxOut(types.RunesOutValue, suite.runesVaultPkScript)) + + denom := fmt.Sprintf("%s/%s", types.RunesProtocolName, runeId) + + balanceBefore := suite.app.BankKeeper.GetBalance(suite.ctx, sdk.MustAccAddressFromBech32(suite.sender), denom) + suite.True(balanceBefore.Amount.IsZero(), "%s balance before mint should be zero", denom) + + recipient, err := suite.app.BtcBridgeKeeper.Mint(suite.ctx, btcutil.NewTx(tx), btcutil.NewTx(tx), 0) + suite.NoError(err) + suite.Equal(suite.sender, recipient.EncodeAddress(), "incorrect recipient") + + balanceAfter := suite.app.BankKeeper.GetBalance(suite.ctx, sdk.MustAccAddressFromBech32(suite.sender), denom) + suite.Equal(uint64(runeAmount), balanceAfter.Amount.Uint64(), "%s balance after mint should be %d", denom, runeAmount) + + utxos := suite.app.BtcBridgeKeeper.GetAllUTXOs(suite.ctx) + suite.Len(utxos, 1, "there should be 1 utxo(s)") + + expectedUTXO := &types.UTXO{ + Txid: tx.TxHash().String(), + Vout: uint64(runeOutputIndex), + Address: suite.runesVault, + Amount: types.RunesOutValue, + PubKeyScript: suite.runesVaultPkScript, + IsLocked: false, + Runes: []*types.RuneBalance{ + { + Id: runeId, + Amount: fmt.Sprintf("%d", runeAmount), + }, + }, + } + + suite.Equal(expectedUTXO, utxos[0], "utxos do not match") +} + +func (suite *KeeperTestSuite) TestWithdrawRunes() { + runeId := "840000:3" + runeAmount := 500000000 + + runesUTXOs := []*types.UTXO{ + { + Txid: chainhash.HashH([]byte("runes")).String(), + Vout: 1, + Address: suite.runesVault, + Amount: types.RunesOutValue, + PubKeyScript: suite.runesVaultPkScript, + IsLocked: false, + Runes: []*types.RuneBalance{ + { + Id: runeId, + Amount: fmt.Sprintf("%d", runeAmount), + }, + }, + }, + } + suite.setupUTXOs(runesUTXOs) + + feeRate := 100 + amount := runeAmount + 1 + + denom := fmt.Sprintf("%s/%s", types.RunesProtocolName, runeId) + coin := sdk.NewCoin(denom, sdk.NewInt(int64(amount))) + + _, err := suite.app.BtcBridgeKeeper.NewSigningRequest(suite.ctx, suite.sender, coin, int64(feeRate)) + suite.ErrorIs(err, types.ErrInsufficientUTXOs, "should fail due to insufficient runes utxos") + + amount = 100000000 + coin = sdk.NewCoin(denom, sdk.NewInt(int64(amount))) + + _, err = suite.app.BtcBridgeKeeper.NewSigningRequest(suite.ctx, suite.sender, coin, int64(feeRate)) + suite.ErrorIs(err, types.ErrInsufficientUTXOs, "should fail due to insufficient payment utxos") + + paymentUTXOs := []*types.UTXO{ + { + Txid: chainhash.HashH([]byte("payment")).String(), + Vout: 1, + Address: suite.btcVault, + Amount: 100000, + PubKeyScript: suite.btcVaultPkScript, + IsLocked: false, + }, + } + suite.setupUTXOs(paymentUTXOs) + + req, err := suite.app.BtcBridgeKeeper.NewSigningRequest(suite.ctx, suite.sender, coin, int64(feeRate)) + suite.NoError(err) + + suite.True(suite.app.BtcBridgeKeeper.IsUTXOLocked(suite.ctx, runesUTXOs[0].Txid, runesUTXOs[0].Vout), "runes utxo should be locked") + suite.True(suite.app.BtcBridgeKeeper.IsUTXOLocked(suite.ctx, paymentUTXOs[0].Txid, paymentUTXOs[0].Vout), "payment utxo should be locked") + + runesUTXOs = suite.app.BtcBridgeKeeper.GetUnlockedUTXOsByAddr(suite.ctx, suite.runesVault) + suite.Len(runesUTXOs, 1, "there should be 1 unlocked runes utxo(s)") + + suite.Len(runesUTXOs[0].Runes, 1, "there should be 1 rune in the runes utxo") + suite.Equal(runeId, runesUTXOs[0].Runes[0].Id, "incorrect rune id") + suite.Equal(uint64(runeAmount-amount), types.RuneAmountFromString(runesUTXOs[0].Runes[0].Amount).Big().Uint64(), "incorrect rune amount") + + p, err := psbt.NewFromRawBytes(bytes.NewReader([]byte(req.Psbt)), true) + suite.NoError(err) + + suite.Len(p.Inputs, 2, "there should be 2 inputs") + suite.Equal(suite.runesVaultPkScript, p.Inputs[0].WitnessUtxo.PkScript, "the first input should be runes vault") + suite.Equal(suite.btcVaultPkScript, p.Inputs[1].WitnessUtxo.PkScript, "the second input should be btc vault") + + expectedRunesScript, err := types.BuildEdictScript(runeId, uint128.From64(uint64(amount)), 2) + suite.NoError(err) + + suite.Len(p.UnsignedTx.TxOut, 4, "there should be 4 outputs") + suite.Equal(expectedRunesScript, p.UnsignedTx.TxOut[0].PkScript, "incorrect runes script") + suite.Equal(suite.runesVaultPkScript, p.UnsignedTx.TxOut[1].PkScript, "the second output should be runes change output") + suite.Equal(suite.senderPkScript, p.UnsignedTx.TxOut[2].PkScript, "the third output should be sender output") + suite.Equal(suite.btcVaultPkScript, p.UnsignedTx.TxOut[3].PkScript, "the fouth output should be btc change output") +} + +func MustPkScriptFromAddress(addr string, chainCfg *chaincfg.Params) []byte { + address, err := btcutil.DecodeAddress(addr, chainCfg) + if err != nil { + panic(err) + } - // use btcwallet to create a transaction - // use btcwallet to create a psbt + pkScript, err := txscript.PayToAddrScript(address) + if err != nil { + panic(err) + } - // pbst := txauthor.NewUnsignedTransaction() + return pkScript } diff --git a/x/btcbridge/keeper/keeper_withdraw.go b/x/btcbridge/keeper/keeper_withdraw.go index 812c41c9..b3e386b7 100644 --- a/x/btcbridge/keeper/keeper_withdraw.go +++ b/x/btcbridge/keeper/keeper_withdraw.go @@ -6,6 +6,8 @@ import ( "encoding/hex" "fmt" + "lukechampine.com/uint128" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -34,24 +36,26 @@ func (k Keeper) IncrementRequestSequence(ctx sdk.Context) uint64 { return seq } -// New signing request -// sender: the address of the sender -// txBytes: the transaction bytes -// vault: the address of the vault, default is empty. -// If empty, the vault will be Bitcoin vault, otherwise it will be Ordinals or Runes vault -func (k Keeper) NewSigningRequest(ctx sdk.Context, sender string, coin sdk.Coin, feeRate int64, vault string) (*types.BitcoinSigningRequest, error) { - if len(vault) == 0 { - // default to the first vault in the params for now - // TODO: select an appropriate vault according to the utxos - p := k.GetParams(ctx) - for i, v := range p.Vaults { - if v.AssetType == types.AssetType_ASSET_TYPE_BTC { - vault = p.Vaults[i].Address - break - } - } +// NewSigningRequest creates a new signing request +func (k Keeper) NewSigningRequest(ctx sdk.Context, sender string, coin sdk.Coin, feeRate int64) (*types.BitcoinSigningRequest, error) { + p := k.GetParams(ctx) + btcVault := types.SelectVaultByAssetType(p.Vaults, types.AssetType_ASSET_TYPE_BTC) + + switch types.AssetTypeFromDenom(coin.Denom, p) { + case types.AssetType_ASSET_TYPE_BTC: + return k.NewBtcSigningRequest(ctx, sender, coin, feeRate, btcVault.Address) + + case types.AssetType_ASSET_TYPE_RUNE: + runesVault := types.SelectVaultByAssetType(p.Vaults, types.AssetType_ASSET_TYPE_RUNE) + return k.NewRunesSigningRequest(ctx, sender, coin, feeRate, runesVault.Address, btcVault.Address) + + default: + return nil, types.ErrAssetNotSupported } +} +// NewBtcSigningRequest creates a signing request for btc withdrawal +func (k Keeper) NewBtcSigningRequest(ctx sdk.Context, sender string, coin sdk.Coin, feeRate int64, vault string) (*types.BitcoinSigningRequest, error) { utxos := k.GetOrderedUTXOsByAddr(ctx, vault) if len(utxos) == 0 { return nil, types.ErrInsufficientUTXOs @@ -90,6 +94,63 @@ func (k Keeper) NewSigningRequest(ctx sdk.Context, sender string, coin sdk.Coin, return signingRequest, nil } +// NewBtcSigningRequest creates a signing request for runes withdrawal +func (k Keeper) NewRunesSigningRequest(ctx sdk.Context, sender string, coin sdk.Coin, feeRate int64, vault string, btcVault string) (*types.BitcoinSigningRequest, error) { + var runeId types.RuneId + runeId.FromDenom(coin.Denom) + + amount := uint128.FromBig(coin.Amount.BigInt()) + + runesUTXOs, amountDelta := k.GetTargetRunesUTXOs(ctx, vault, runeId.ToString(), amount) + if len(runesUTXOs) == 0 { + return nil, types.ErrInsufficientUTXOs + } + + paymentUTXOs := k.GetOrderedUTXOsByAddr(ctx, btcVault) + if len(paymentUTXOs) == 0 { + return nil, types.ErrInsufficientUTXOs + } + + psbt, selectedUTXOs, changeUTXO, runesChangeUTXO, err := types.BuildRunesPsbt(runesUTXOs, paymentUTXOs, sender, runeId.ToString(), amount, feeRate, amountDelta, vault, btcVault) + if err != nil { + return nil, err + } + + psbtB64, err := psbt.B64Encode() + if err != nil { + return nil, types.ErrFailToSerializePsbt + } + + // lock the involved utxos + _ = k.LockUTXOs(ctx, runesUTXOs) + _ = k.LockUTXOs(ctx, selectedUTXOs) + + // save the change utxo and mark minted + if changeUTXO != nil { + k.saveUTXO(ctx, changeUTXO) + k.addToMintHistory(ctx, psbt.UnsignedTx.TxHash().String()) + } + + // save the runes change utxo and mark minted + if runesChangeUTXO != nil { + k.saveUTXO(ctx, runesChangeUTXO) + k.addToMintHistory(ctx, psbt.UnsignedTx.TxHash().String()) + } + + signingRequest := &types.BitcoinSigningRequest{ + Address: sender, + Txid: psbt.UnsignedTx.TxHash().String(), + Psbt: psbtB64, + Status: types.SigningStatus_SIGNING_STATUS_CREATED, + Sequence: k.IncrementRequestSequence(ctx), + VaultAddress: vault, + } + + k.SetSigningRequest(ctx, signingRequest) + + return signingRequest, nil +} + // GetSigningRequest returns the signing request func (k Keeper) HasSigningRequest(ctx sdk.Context, hash string) bool { store := ctx.KVStore(k.storeKey) @@ -162,11 +223,9 @@ func (k Keeper) FilterSigningRequestsByAddr(ctx sdk.Context, req *types.QuerySig // Process Bitcoin Withdraw Transaction func (k Keeper) ProcessBitcoinWithdrawTransaction(ctx sdk.Context, msg *types.MsgSubmitWithdrawTransactionRequest) (*chainhash.Hash, error) { - ctx.Logger().Info("accept bitcoin withdraw tx", "blockhash", msg.Blockhash) param := k.GetParams(ctx) - if !param.IsAuthorizedSender(msg.Sender) { return nil, types.ErrSenderAddressNotAuthorized } diff --git a/x/btcbridge/keeper/msg_server.go b/x/btcbridge/keeper/msg_server.go index 88dcebc3..ed107828 100644 --- a/x/btcbridge/keeper/msg_server.go +++ b/x/btcbridge/keeper/msg_server.go @@ -130,6 +130,7 @@ func (m msgServer) WithdrawBitcoin(goCtx context.Context, msg *types.MsgWithdraw if err := msg.ValidateBasic(); err != nil { return nil, err } + ctx := sdk.UnwrapSDKContext(goCtx) sender := sdk.MustAccAddressFromBech32(msg.Sender) @@ -143,12 +144,18 @@ func (m msgServer) WithdrawBitcoin(goCtx context.Context, msg *types.MsgWithdraw return nil, err } + if coin.Denom == m.GetParams(ctx).BtcVoucherDenom { + if err := types.CheckOutputAmount(msg.Sender, coin.Amount.Int64()); err != nil { + return nil, err + } + } + feeRate, err := strconv.ParseInt(msg.FeeRate, 10, 64) if err != nil { return nil, err } - req, err := m.Keeper.NewSigningRequest(ctx, msg.Sender, coin, feeRate, "") + req, err := m.Keeper.NewSigningRequest(ctx, msg.Sender, coin, feeRate) if err != nil { return nil, err } diff --git a/x/btcbridge/keeper/utxo.go b/x/btcbridge/keeper/utxo.go index f2a4461f..25671c87 100644 --- a/x/btcbridge/keeper/utxo.go +++ b/x/btcbridge/keeper/utxo.go @@ -4,6 +4,8 @@ import ( "math/big" "sort" + "lukechampine.com/uint128" + "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -22,6 +24,8 @@ type UTXOViewKeeper interface { GetUnlockedUTXOsByAddr(ctx sdk.Context, addr string) []*types.UTXO GetOrderedUTXOsByAddr(ctx sdk.Context, addr string) []*types.UTXO + GetTargetRunesUTXOs(ctx sdk.Context, addr string, runeId string, targetAmount uint128.Uint128) ([]*types.UTXO, uint128.Uint128) + IterateAllUTXOs(ctx sdk.Context, cb func(utxo *types.UTXO) (stop bool)) IterateUTXOsByAddr(ctx sdk.Context, addr string, cb func(addr string, utxo *types.UTXO) (stop bool)) } @@ -136,6 +140,31 @@ func (bvk *BaseUTXOViewKeeper) GetOrderedUTXOsByAddr(ctx sdk.Context, addr strin return utxos } +// GetTargetRunesUTXOs gets the unlocked runes utxos targeted by the given params +func (bvk *BaseUTXOViewKeeper) GetTargetRunesUTXOs(ctx sdk.Context, addr string, runeId string, targetAmount uint128.Uint128) ([]*types.UTXO, uint128.Uint128) { + utxos := make([]*types.UTXO, 0) + + totalAmount := uint128.Zero + + bvk.IterateRunesUTXOs(ctx, addr, runeId, func(addr string, id string, amount uint128.Uint128, utxo *types.UTXO) (stop bool) { + if utxo.IsLocked { + return false + } + + utxos = append(utxos, utxo) + + totalAmount = totalAmount.Add(amount) + + return totalAmount.Cmp(targetAmount) >= 0 + }) + + if totalAmount.Cmp(targetAmount) < 0 { + return nil, uint128.Zero + } + + return utxos, totalAmount.Sub(targetAmount) +} + func (bvk *BaseUTXOViewKeeper) IterateAllUTXOs(ctx sdk.Context, cb func(utxo *types.UTXO) (stop bool)) { store := ctx.KVStore(bvk.storeKey) @@ -171,6 +200,30 @@ func (bvk *BaseUTXOViewKeeper) IterateUTXOsByAddr(ctx sdk.Context, addr string, } } +func (bvk *BaseUTXOViewKeeper) IterateRunesUTXOs(ctx sdk.Context, addr string, id string, cb func(addr string, id string, amount uint128.Uint128, utxo *types.UTXO) (stop bool)) { + store := ctx.KVStore(bvk.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, append(append(types.BtcOwnerRunesUtxoKeyPrefix, []byte(addr)...), []byte(id)...)) + defer iterator.Close() + + prefixLen := 1 + len(addr) + len(id) + + for ; iterator.Valid(); iterator.Next() { + key := iterator.Key() + value := iterator.Value() + + hash := key[prefixLen : prefixLen+64] + vout := key[prefixLen+64:] + + amount := types.RuneAmountFromString(string(value)) + + utxo := bvk.GetUTXO(ctx, string(hash), new(big.Int).SetBytes(vout).Uint64()) + if cb(addr, id, amount, utxo) { + break + } + } +} + type BaseUTXOKeeper struct { BaseUTXOViewKeeper @@ -196,7 +249,13 @@ func (bk *BaseUTXOKeeper) SetUTXO(ctx sdk.Context, utxo *types.UTXO) { func (bk *BaseUTXOKeeper) SetOwnerUTXO(ctx sdk.Context, utxo *types.UTXO) { store := ctx.KVStore(bk.storeKey) - store.Set(types.BtcOwnerUtxoKey(utxo.Address, utxo.Txid, utxo.Vout), []byte{1}) + store.Set(types.BtcOwnerUtxoKey(utxo.Address, utxo.Txid, utxo.Vout), []byte{}) +} + +func (bk *BaseUTXOKeeper) SetOwnerRunesUTXO(ctx sdk.Context, utxo *types.UTXO, id string, amount string) { + store := ctx.KVStore(bk.storeKey) + + store.Set(types.BtcOwnerRunesUtxoKey(utxo.Address, id, utxo.Txid, utxo.Vout), []byte(amount)) } func (bk *BaseUTXOKeeper) LockUTXO(ctx sdk.Context, hash string, vout uint64) error { @@ -275,6 +334,10 @@ func (bk *BaseUTXOKeeper) SpendUTXOs(ctx sdk.Context, utxos []*types.UTXO) error func (bk *BaseUTXOKeeper) saveUTXO(ctx sdk.Context, utxo *types.UTXO) { bk.SetUTXO(ctx, utxo) bk.SetOwnerUTXO(ctx, utxo) + + for _, r := range utxo.Runes { + bk.SetOwnerRunesUTXO(ctx, utxo, r.Id, r.Amount) + } } // removeUTXO deletes the given utxo which is assumed to exist. @@ -284,4 +347,8 @@ func (bk *BaseUTXOKeeper) removeUTXO(ctx sdk.Context, hash string, vout uint64) store.Delete(types.BtcUtxoKey(hash, vout)) store.Delete(types.BtcOwnerUtxoKey(utxo.Address, hash, vout)) + + for _, r := range utxo.Runes { + store.Delete(types.BtcOwnerRunesUtxoKey(utxo.Address, r.Id, hash, vout)) + } } diff --git a/x/btcbridge/types/asset.go b/x/btcbridge/types/asset.go new file mode 100644 index 00000000..8d32de63 --- /dev/null +++ b/x/btcbridge/types/asset.go @@ -0,0 +1,19 @@ +package types + +import ( + "fmt" + "strings" +) + +// AssetTypeFromDenom returns the asset type according to the denom +func AssetTypeFromDenom(denom string, p Params) AssetType { + if denom == p.BtcVoucherDenom { + return AssetType_ASSET_TYPE_BTC + } + + if strings.HasPrefix(denom, fmt.Sprintf("%s/", RunesProtocolName)) { + return AssetType_ASSET_TYPE_RUNE + } + + return AssetType_ASSET_TYPE_UNSPECIFIED +} diff --git a/x/btcbridge/types/bitcoin.pb.go b/x/btcbridge/types/bitcoin.pb.go index 5bc6120f..02ee4bfc 100644 --- a/x/btcbridge/types/bitcoin.pb.go +++ b/x/btcbridge/types/bitcoin.pb.go @@ -273,6 +273,8 @@ type UTXO struct { PubKeyScript []byte `protobuf:"bytes,6,opt,name=pub_key_script,json=pubKeyScript,proto3" json:"pub_key_script,omitempty"` IsCoinbase bool `protobuf:"varint,7,opt,name=is_coinbase,json=isCoinbase,proto3" json:"is_coinbase,omitempty"` IsLocked bool `protobuf:"varint,8,opt,name=is_locked,json=isLocked,proto3" json:"is_locked,omitempty"` + // rune balances associated with the UTXO + Runes []*RuneBalance `protobuf:"bytes,9,rep,name=runes,proto3" json:"runes,omitempty"` } func (m *UTXO) Reset() { *m = UTXO{} } @@ -364,56 +366,243 @@ func (m *UTXO) GetIsLocked() bool { return false } +func (m *UTXO) GetRunes() []*RuneBalance { + if m != nil { + return m.Runes + } + return nil +} + +// Rune Balance +type RuneBalance struct { + // serialized rune id + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // rune amount + Amount string `protobuf:"bytes,2,opt,name=amount,proto3" json:"amount,omitempty"` +} + +func (m *RuneBalance) Reset() { *m = RuneBalance{} } +func (m *RuneBalance) String() string { return proto.CompactTextString(m) } +func (*RuneBalance) ProtoMessage() {} +func (*RuneBalance) Descriptor() ([]byte, []int) { + return fileDescriptor_b004a69efe3c7d84, []int{3} +} +func (m *RuneBalance) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *RuneBalance) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_RuneBalance.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *RuneBalance) XXX_Merge(src proto.Message) { + xxx_messageInfo_RuneBalance.Merge(m, src) +} +func (m *RuneBalance) XXX_Size() int { + return m.Size() +} +func (m *RuneBalance) XXX_DiscardUnknown() { + xxx_messageInfo_RuneBalance.DiscardUnknown(m) +} + +var xxx_messageInfo_RuneBalance proto.InternalMessageInfo + +func (m *RuneBalance) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +func (m *RuneBalance) GetAmount() string { + if m != nil { + return m.Amount + } + return "" +} + +// Rune ID +type RuneId struct { + // block height + Block uint64 `protobuf:"varint,1,opt,name=block,proto3" json:"block,omitempty"` + // tx index + Tx uint32 `protobuf:"varint,2,opt,name=tx,proto3" json:"tx,omitempty"` +} + +func (m *RuneId) Reset() { *m = RuneId{} } +func (m *RuneId) String() string { return proto.CompactTextString(m) } +func (*RuneId) ProtoMessage() {} +func (*RuneId) Descriptor() ([]byte, []int) { + return fileDescriptor_b004a69efe3c7d84, []int{4} +} +func (m *RuneId) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *RuneId) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_RuneId.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *RuneId) XXX_Merge(src proto.Message) { + xxx_messageInfo_RuneId.Merge(m, src) +} +func (m *RuneId) XXX_Size() int { + return m.Size() +} +func (m *RuneId) XXX_DiscardUnknown() { + xxx_messageInfo_RuneId.DiscardUnknown(m) +} + +var xxx_messageInfo_RuneId proto.InternalMessageInfo + +func (m *RuneId) GetBlock() uint64 { + if m != nil { + return m.Block + } + return 0 +} + +func (m *RuneId) GetTx() uint32 { + if m != nil { + return m.Tx + } + return 0 +} + +// Rune Edict +type Edict struct { + Id *RuneId `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Amount string `protobuf:"bytes,2,opt,name=amount,proto3" json:"amount,omitempty"` + Output uint32 `protobuf:"varint,3,opt,name=output,proto3" json:"output,omitempty"` +} + +func (m *Edict) Reset() { *m = Edict{} } +func (m *Edict) String() string { return proto.CompactTextString(m) } +func (*Edict) ProtoMessage() {} +func (*Edict) Descriptor() ([]byte, []int) { + return fileDescriptor_b004a69efe3c7d84, []int{5} +} +func (m *Edict) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Edict) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Edict.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Edict) XXX_Merge(src proto.Message) { + xxx_messageInfo_Edict.Merge(m, src) +} +func (m *Edict) XXX_Size() int { + return m.Size() +} +func (m *Edict) XXX_DiscardUnknown() { + xxx_messageInfo_Edict.DiscardUnknown(m) +} + +var xxx_messageInfo_Edict proto.InternalMessageInfo + +func (m *Edict) GetId() *RuneId { + if m != nil { + return m.Id + } + return nil +} + +func (m *Edict) GetAmount() string { + if m != nil { + return m.Amount + } + return "" +} + +func (m *Edict) GetOutput() uint32 { + if m != nil { + return m.Output + } + return 0 +} + func init() { proto.RegisterEnum("side.btcbridge.SigningStatus", SigningStatus_name, SigningStatus_value) proto.RegisterType((*BlockHeader)(nil), "side.btcbridge.BlockHeader") proto.RegisterType((*BitcoinSigningRequest)(nil), "side.btcbridge.BitcoinSigningRequest") proto.RegisterType((*UTXO)(nil), "side.btcbridge.UTXO") + proto.RegisterType((*RuneBalance)(nil), "side.btcbridge.RuneBalance") + proto.RegisterType((*RuneId)(nil), "side.btcbridge.RuneId") + proto.RegisterType((*Edict)(nil), "side.btcbridge.Edict") } func init() { proto.RegisterFile("side/btcbridge/bitcoin.proto", fileDescriptor_b004a69efe3c7d84) } var fileDescriptor_b004a69efe3c7d84 = []byte{ - // 613 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x53, 0xcd, 0x6e, 0xd3, 0x40, - 0x10, 0x8e, 0x9b, 0x9f, 0x26, 0xd3, 0x1f, 0x85, 0xa5, 0x2d, 0x26, 0x2d, 0xa6, 0x2a, 0x1c, 0x2a, - 0x0e, 0x8e, 0x04, 0xe2, 0x01, 0xf2, 0xe3, 0xb6, 0xe1, 0x27, 0x45, 0xeb, 0x54, 0x42, 0x5c, 0x2c, - 0xff, 0xac, 0x9c, 0x55, 0x13, 0xaf, 0xf1, 0xae, 0xa3, 0xf6, 0x2d, 0x78, 0x25, 0x6e, 0x1c, 0xcb, - 0x8d, 0x03, 0x07, 0xd4, 0xbe, 0x02, 0x0f, 0x80, 0x76, 0x9c, 0x54, 0x4d, 0xc4, 0xed, 0xfb, 0xbe, - 0x99, 0x9d, 0xf9, 0x66, 0xc6, 0x86, 0x03, 0xc9, 0x23, 0xd6, 0x0e, 0x54, 0x18, 0x64, 0x3c, 0x8a, - 0x59, 0x3b, 0xe0, 0x2a, 0x14, 0x3c, 0xb1, 0xd3, 0x4c, 0x28, 0x41, 0xb6, 0x75, 0xd4, 0xbe, 0x8f, - 0xb6, 0x76, 0x62, 0x11, 0x0b, 0x0c, 0xb5, 0x35, 0x2a, 0xb2, 0x8e, 0xfe, 0x1a, 0xb0, 0xd1, 0x9d, - 0x88, 0xf0, 0xf2, 0x8c, 0xf9, 0x11, 0xcb, 0x88, 0x09, 0xeb, 0x33, 0x96, 0x49, 0x2e, 0x12, 0xd3, - 0x38, 0x34, 0x8e, 0x2b, 0x74, 0x41, 0x09, 0x81, 0xca, 0xd8, 0x97, 0x63, 0x73, 0xed, 0xd0, 0x38, - 0x6e, 0x50, 0xc4, 0x64, 0x0f, 0x6a, 0x63, 0xc6, 0xe3, 0xb1, 0x32, 0xcb, 0x98, 0x3c, 0x67, 0xc4, - 0x86, 0xc7, 0x69, 0xc6, 0x66, 0x5c, 0xe4, 0xd2, 0x0b, 0x74, 0x75, 0x0f, 0x9f, 0x56, 0xf0, 0xe9, - 0xa3, 0x45, 0xa8, 0xe8, 0xab, 0xeb, 0x3c, 0x87, 0x8d, 0x29, 0xcb, 0x2e, 0x27, 0xcc, 0xcb, 0x84, - 0x50, 0x66, 0x15, 0xf3, 0xa0, 0x90, 0xa8, 0x10, 0x8a, 0xec, 0x40, 0x35, 0x11, 0x49, 0xc8, 0xcc, - 0x1a, 0xf6, 0x29, 0x88, 0xb6, 0x14, 0x70, 0x25, 0xcd, 0xf5, 0xc2, 0x92, 0xc6, 0x5a, 0x53, 0x7c, - 0xca, 0xcc, 0x3a, 0x26, 0x22, 0x26, 0x4d, 0x28, 0x27, 0xea, 0xca, 0x6c, 0xa0, 0xa4, 0xe1, 0xd1, - 0x4f, 0x03, 0x76, 0xbb, 0xc5, 0xba, 0x5c, 0x1e, 0x27, 0x3c, 0x89, 0x29, 0xfb, 0x9a, 0x33, 0xa9, - 0xf4, 0x02, 0xfc, 0x28, 0xca, 0x98, 0x94, 0xb8, 0x80, 0x06, 0x5d, 0x50, 0xac, 0x7c, 0xc5, 0xa3, - 0xc5, 0x02, 0x34, 0xd6, 0x5a, 0x2a, 0x83, 0x62, 0xfc, 0x06, 0x45, 0x4c, 0xde, 0x42, 0x4d, 0x2a, - 0x5f, 0xe5, 0x12, 0xe7, 0xdd, 0x7e, 0xfd, 0xcc, 0x5e, 0xbe, 0x84, 0x3d, 0xef, 0xe8, 0x62, 0x12, - 0x9d, 0x27, 0x93, 0x16, 0xd4, 0xa5, 0xf6, 0xa0, 0xa7, 0xac, 0xa2, 0xd3, 0x7b, 0x4e, 0x5e, 0xc0, - 0xd6, 0xcc, 0xcf, 0x27, 0xca, 0x5b, 0x58, 0xab, 0x61, 0xbf, 0x4d, 0x14, 0x3b, 0x85, 0x76, 0xf4, - 0xdb, 0x80, 0xca, 0xc5, 0xe8, 0xf3, 0xf9, 0xbd, 0x51, 0x63, 0xd9, 0xe8, 0x4c, 0xe4, 0x0a, 0xcd, - 0x57, 0x28, 0xe2, 0x87, 0xa3, 0x96, 0x97, 0x47, 0xdd, 0x83, 0x9a, 0x3f, 0x15, 0x79, 0xa2, 0x70, - 0x84, 0x0a, 0x9d, 0xb3, 0x07, 0xf7, 0xae, 0x2e, 0xdd, 0xfb, 0x25, 0x6c, 0xa7, 0x79, 0xe0, 0x5d, - 0xb2, 0x6b, 0x4f, 0x86, 0x19, 0x4f, 0x15, 0x1a, 0xdc, 0xa4, 0x9b, 0x69, 0x1e, 0xbc, 0x67, 0xd7, - 0x2e, 0x6a, 0xfa, 0xca, 0x5c, 0x7a, 0x7a, 0xe7, 0x81, 0x2f, 0x19, 0x5e, 0xad, 0x4e, 0x81, 0xcb, - 0xde, 0x5c, 0x21, 0xfb, 0xd0, 0xe0, 0xd2, 0xd3, 0x5f, 0x05, 0x8b, 0xf0, 0x80, 0x75, 0x5a, 0xe7, - 0xf2, 0x03, 0xf2, 0x57, 0xdf, 0x0d, 0xd8, 0x5a, 0xda, 0x1c, 0xb1, 0xa0, 0xe5, 0x0e, 0x4e, 0x87, - 0x83, 0xe1, 0xa9, 0xe7, 0x8e, 0x3a, 0xa3, 0x0b, 0xd7, 0xbb, 0x18, 0xba, 0x9f, 0x9c, 0xde, 0xe0, - 0x64, 0xe0, 0xf4, 0x9b, 0x25, 0xd2, 0x82, 0xbd, 0x95, 0x78, 0x8f, 0x3a, 0x9d, 0x91, 0xd3, 0x6f, - 0x1a, 0xe4, 0x29, 0xec, 0xae, 0xc4, 0x34, 0x75, 0xfa, 0xcd, 0xb5, 0xff, 0x94, 0xed, 0xd2, 0xf3, - 0x4e, 0xbf, 0xd7, 0x71, 0xf5, 0xd3, 0x32, 0x39, 0x00, 0x73, 0xb5, 0xec, 0xf9, 0xf0, 0x64, 0x40, - 0x3f, 0x3a, 0xfd, 0x66, 0x85, 0xec, 0xc3, 0x93, 0x95, 0x28, 0x75, 0xde, 0x39, 0x3d, 0xfd, 0xb4, - 0xda, 0x3d, 0xfb, 0x71, 0x6b, 0x19, 0x37, 0xb7, 0x96, 0xf1, 0xe7, 0xd6, 0x32, 0xbe, 0xdd, 0x59, - 0xa5, 0x9b, 0x3b, 0xab, 0xf4, 0xeb, 0xce, 0x2a, 0x7d, 0xb1, 0x63, 0xae, 0xc6, 0x79, 0x60, 0x87, - 0x62, 0xda, 0xd6, 0x9f, 0x0b, 0xfe, 0x9d, 0xa1, 0x98, 0x20, 0x69, 0x5f, 0x3d, 0xf8, 0xcb, 0xd5, - 0x75, 0xca, 0x64, 0x50, 0xc3, 0x84, 0x37, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0x8b, 0x6e, 0xf2, - 0x5a, 0x04, 0x04, 0x00, 0x00, + // 715 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x54, 0xdd, 0x6e, 0xda, 0x48, + 0x14, 0xc6, 0xfc, 0x05, 0x86, 0x80, 0xd8, 0xd9, 0x84, 0xf5, 0x92, 0x2c, 0x1b, 0xb1, 0xab, 0x2a, + 0xea, 0x85, 0x51, 0x53, 0xe5, 0x01, 0xf8, 0x71, 0x12, 0xfa, 0x43, 0xaa, 0x31, 0x91, 0xaa, 0xde, + 0x58, 0xfe, 0x19, 0xc1, 0x28, 0xe0, 0x71, 0x3d, 0x63, 0x44, 0x9e, 0xa2, 0x7d, 0xa5, 0xde, 0xf5, + 0x32, 0xbd, 0xeb, 0x65, 0x95, 0xbc, 0x42, 0x1f, 0xa0, 0x9a, 0x63, 0x83, 0x00, 0x45, 0xbd, 0x3b, + 0xdf, 0x77, 0xfe, 0xbe, 0x73, 0xce, 0xd8, 0xe8, 0x58, 0x30, 0x9f, 0x76, 0x5c, 0xe9, 0xb9, 0x11, + 0xf3, 0x27, 0xb4, 0xe3, 0x32, 0xe9, 0x71, 0x16, 0x18, 0x61, 0xc4, 0x25, 0xc7, 0x35, 0xe5, 0x35, + 0xd6, 0xde, 0xe6, 0xc1, 0x84, 0x4f, 0x38, 0xb8, 0x3a, 0xca, 0x4a, 0xa2, 0xda, 0x3f, 0x35, 0x54, + 0xe9, 0xcd, 0xb8, 0x77, 0x7b, 0x45, 0x1d, 0x9f, 0x46, 0x58, 0x47, 0x7b, 0x0b, 0x1a, 0x09, 0xc6, + 0x03, 0x5d, 0x3b, 0xd1, 0x4e, 0xf3, 0x64, 0x05, 0x31, 0x46, 0xf9, 0xa9, 0x23, 0xa6, 0x7a, 0xf6, + 0x44, 0x3b, 0x2d, 0x13, 0xb0, 0x71, 0x03, 0x15, 0xa7, 0x94, 0x4d, 0xa6, 0x52, 0xcf, 0x41, 0x70, + 0x8a, 0xb0, 0x81, 0xfe, 0x0c, 0x23, 0xba, 0x60, 0x3c, 0x16, 0xb6, 0xab, 0xaa, 0xdb, 0x90, 0x9a, + 0x87, 0xd4, 0x3f, 0x56, 0xae, 0xa4, 0xaf, 0xaa, 0xf3, 0x2f, 0xaa, 0xcc, 0x69, 0x74, 0x3b, 0xa3, + 0x76, 0xc4, 0xb9, 0xd4, 0x0b, 0x10, 0x87, 0x12, 0x8a, 0x70, 0x2e, 0xf1, 0x01, 0x2a, 0x04, 0x3c, + 0xf0, 0xa8, 0x5e, 0x84, 0x3e, 0x09, 0x50, 0x92, 0x5c, 0x26, 0x85, 0xbe, 0x97, 0x48, 0x52, 0xb6, + 0xe2, 0x24, 0x9b, 0x53, 0xbd, 0x04, 0x81, 0x60, 0xe3, 0x3a, 0xca, 0x05, 0x72, 0xa9, 0x97, 0x81, + 0x52, 0x66, 0xfb, 0x9b, 0x86, 0x0e, 0x7b, 0xc9, 0xba, 0x2c, 0x36, 0x09, 0x58, 0x30, 0x21, 0xf4, + 0x63, 0x4c, 0x85, 0x54, 0x0b, 0x70, 0x7c, 0x3f, 0xa2, 0x42, 0xc0, 0x02, 0xca, 0x64, 0x05, 0xa1, + 0xf2, 0x92, 0xf9, 0xab, 0x05, 0x28, 0x5b, 0x71, 0xa1, 0x70, 0x93, 0xf1, 0xcb, 0x04, 0x6c, 0x7c, + 0x8e, 0x8a, 0x42, 0x3a, 0x32, 0x16, 0x30, 0x6f, 0xed, 0xec, 0x1f, 0x63, 0xfb, 0x12, 0x46, 0xda, + 0xd1, 0x82, 0x20, 0x92, 0x06, 0xe3, 0x26, 0x2a, 0x09, 0xa5, 0x41, 0x4d, 0x59, 0x00, 0xa5, 0x6b, + 0x8c, 0xff, 0x43, 0xd5, 0x85, 0x13, 0xcf, 0xa4, 0xbd, 0x92, 0x56, 0x84, 0x7e, 0xfb, 0x40, 0x76, + 0x13, 0xae, 0xfd, 0x29, 0x8b, 0xf2, 0x37, 0xe3, 0xf7, 0xd7, 0x6b, 0xa1, 0xda, 0xb6, 0xd0, 0x05, + 0x8f, 0x25, 0x88, 0xcf, 0x13, 0xb0, 0x37, 0x47, 0xcd, 0x6d, 0x8f, 0xda, 0x40, 0x45, 0x67, 0xce, + 0xe3, 0x40, 0xc2, 0x08, 0x79, 0x92, 0xa2, 0x8d, 0x7b, 0x17, 0xb6, 0xee, 0xfd, 0x3f, 0xaa, 0x85, + 0xb1, 0x6b, 0xdf, 0xd2, 0x3b, 0x5b, 0x78, 0x11, 0x0b, 0x25, 0x08, 0xdc, 0x27, 0xfb, 0x61, 0xec, + 0xbe, 0xa6, 0x77, 0x16, 0x70, 0xea, 0xca, 0x4c, 0xd8, 0x6a, 0xe7, 0xae, 0x23, 0x28, 0x5c, 0xad, + 0x44, 0x10, 0x13, 0xfd, 0x94, 0xc1, 0x47, 0xa8, 0xcc, 0x84, 0xad, 0x5e, 0x05, 0xf5, 0xe1, 0x80, + 0x25, 0x52, 0x62, 0xe2, 0x0d, 0x60, 0xfc, 0x02, 0x15, 0xa2, 0x38, 0xa0, 0x42, 0x2f, 0x9f, 0xe4, + 0x4e, 0x2b, 0x67, 0x47, 0xbb, 0x5b, 0x25, 0x71, 0x40, 0x7b, 0xce, 0xcc, 0x09, 0x3c, 0x4a, 0x92, + 0xc8, 0xf6, 0x39, 0xaa, 0x6c, 0xb0, 0xb8, 0x86, 0xb2, 0xeb, 0xad, 0x64, 0x99, 0xbf, 0x31, 0x65, + 0x72, 0xd2, 0x14, 0xb5, 0x0d, 0x54, 0x54, 0x69, 0x43, 0x5f, 0x3d, 0x3b, 0x78, 0xbe, 0xe9, 0xb7, + 0x90, 0x00, 0x55, 0x47, 0x2e, 0x21, 0xa7, 0x4a, 0xb2, 0x72, 0xd9, 0xb6, 0x51, 0xc1, 0xf4, 0x99, + 0x27, 0xf1, 0xb3, 0x75, 0x83, 0xca, 0x59, 0xe3, 0x29, 0x7d, 0x43, 0xff, 0x77, 0x8d, 0x15, 0xcf, + 0x63, 0x19, 0xc6, 0xc9, 0x7b, 0xaa, 0x92, 0x14, 0x3d, 0xff, 0xa2, 0xa1, 0xea, 0xd6, 0xa3, 0xc1, + 0x2d, 0xd4, 0xb4, 0x86, 0x97, 0xa3, 0xe1, 0xe8, 0xd2, 0xb6, 0xc6, 0xdd, 0xf1, 0x8d, 0x65, 0xdf, + 0x8c, 0xac, 0x77, 0x66, 0x7f, 0x78, 0x31, 0x34, 0x07, 0xf5, 0x0c, 0x6e, 0xa2, 0xc6, 0x8e, 0xbf, + 0x4f, 0xcc, 0xee, 0xd8, 0x1c, 0xd4, 0x35, 0xfc, 0x37, 0x3a, 0xdc, 0xf1, 0x29, 0x68, 0x0e, 0xea, + 0xd9, 0x27, 0xca, 0xf6, 0xc8, 0x75, 0x77, 0xd0, 0xef, 0x5a, 0x2a, 0x35, 0x87, 0x8f, 0x91, 0xbe, + 0x5b, 0xf6, 0x7a, 0x74, 0x31, 0x24, 0x6f, 0xcd, 0x41, 0x3d, 0x8f, 0x8f, 0xd0, 0x5f, 0x3b, 0x5e, + 0x62, 0xbe, 0x32, 0xfb, 0x2a, 0xb5, 0xd0, 0xbb, 0xfa, 0xfa, 0xd0, 0xd2, 0xee, 0x1f, 0x5a, 0xda, + 0x8f, 0x87, 0x96, 0xf6, 0xf9, 0xb1, 0x95, 0xb9, 0x7f, 0x6c, 0x65, 0xbe, 0x3f, 0xb6, 0x32, 0x1f, + 0x8c, 0x09, 0x93, 0xd3, 0xd8, 0x35, 0x3c, 0x3e, 0xef, 0xa8, 0x9d, 0xc1, 0x8f, 0xc9, 0xe3, 0x33, + 0x00, 0x9d, 0xe5, 0xc6, 0x0f, 0x4e, 0xde, 0x85, 0x54, 0xb8, 0x45, 0x08, 0x78, 0xf9, 0x2b, 0x00, + 0x00, 0xff, 0xff, 0x50, 0x8f, 0x56, 0x8e, 0xff, 0x04, 0x00, 0x00, } func (m *BlockHeader) Marshal() (dAtA []byte, err error) { @@ -573,6 +762,20 @@ func (m *UTXO) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.Runes) > 0 { + for iNdEx := len(m.Runes) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Runes[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintBitcoin(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x4a + } + } if m.IsLocked { i-- if m.IsLocked { @@ -632,6 +835,123 @@ func (m *UTXO) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *RuneBalance) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RuneBalance) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *RuneBalance) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Amount) > 0 { + i -= len(m.Amount) + copy(dAtA[i:], m.Amount) + i = encodeVarintBitcoin(dAtA, i, uint64(len(m.Amount))) + i-- + dAtA[i] = 0x12 + } + if len(m.Id) > 0 { + i -= len(m.Id) + copy(dAtA[i:], m.Id) + i = encodeVarintBitcoin(dAtA, i, uint64(len(m.Id))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *RuneId) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RuneId) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *RuneId) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Tx != 0 { + i = encodeVarintBitcoin(dAtA, i, uint64(m.Tx)) + i-- + dAtA[i] = 0x10 + } + if m.Block != 0 { + i = encodeVarintBitcoin(dAtA, i, uint64(m.Block)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *Edict) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Edict) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Edict) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Output != 0 { + i = encodeVarintBitcoin(dAtA, i, uint64(m.Output)) + i-- + dAtA[i] = 0x18 + } + if len(m.Amount) > 0 { + i -= len(m.Amount) + copy(dAtA[i:], m.Amount) + i = encodeVarintBitcoin(dAtA, i, uint64(len(m.Amount))) + i-- + dAtA[i] = 0x12 + } + if m.Id != nil { + { + size, err := m.Id.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintBitcoin(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintBitcoin(dAtA []byte, offset int, v uint64) int { offset -= sovBitcoin(v) base := offset @@ -747,6 +1067,64 @@ func (m *UTXO) Size() (n int) { if m.IsLocked { n += 2 } + if len(m.Runes) > 0 { + for _, e := range m.Runes { + l = e.Size() + n += 1 + l + sovBitcoin(uint64(l)) + } + } + return n +} + +func (m *RuneBalance) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Id) + if l > 0 { + n += 1 + l + sovBitcoin(uint64(l)) + } + l = len(m.Amount) + if l > 0 { + n += 1 + l + sovBitcoin(uint64(l)) + } + return n +} + +func (m *RuneId) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Block != 0 { + n += 1 + sovBitcoin(uint64(m.Block)) + } + if m.Tx != 0 { + n += 1 + sovBitcoin(uint64(m.Tx)) + } + return n +} + +func (m *Edict) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Id != nil { + l = m.Id.Size() + n += 1 + l + sovBitcoin(uint64(l)) + } + l = len(m.Amount) + if l > 0 { + n += 1 + l + sovBitcoin(uint64(l)) + } + if m.Output != 0 { + n += 1 + sovBitcoin(uint64(m.Output)) + } return n } @@ -1469,6 +1847,379 @@ func (m *UTXO) Unmarshal(dAtA []byte) error { } } m.IsLocked = bool(v != 0) + case 9: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Runes", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthBitcoin + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthBitcoin + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Runes = append(m.Runes, &RuneBalance{}) + if err := m.Runes[len(m.Runes)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipBitcoin(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthBitcoin + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RuneBalance) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RuneBalance: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RuneBalance: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthBitcoin + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthBitcoin + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Id = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Amount", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthBitcoin + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthBitcoin + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Amount = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipBitcoin(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthBitcoin + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RuneId) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RuneId: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RuneId: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Block", wireType) + } + m.Block = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Block |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Tx", wireType) + } + m.Tx = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Tx |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipBitcoin(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthBitcoin + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Edict) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Edict: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Edict: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthBitcoin + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthBitcoin + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Id == nil { + m.Id = &RuneId{} + } + if err := m.Id.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Amount", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthBitcoin + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthBitcoin + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Amount = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Output", wireType) + } + m.Output = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBitcoin + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Output |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipBitcoin(dAtA[iNdEx:]) diff --git a/x/btcbridge/types/bitcoin_transaction.go b/x/btcbridge/types/bitcoin_transaction.go index 3c9d3e1a..d39114c3 100644 --- a/x/btcbridge/types/bitcoin_transaction.go +++ b/x/btcbridge/types/bitcoin_transaction.go @@ -1,6 +1,8 @@ package types import ( + "lukechampine.com/uint128" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -23,9 +25,10 @@ const ( ) // BuildPsbt builds a bitcoin psbt from the given params. -// Assume that the utxo script type is native segwit. +// Assume that the utxo script type is witness. func BuildPsbt(utxos []*UTXO, recipient string, amount int64, feeRate int64, change string) (*psbt.Packet, []*UTXO, *UTXO, error) { chaincfg := sdk.GetConfig().GetBtcChainCfg() + recipientAddr, err := btcutil.DecodeAddress(recipient, chaincfg) if err != nil { return nil, nil, nil, err @@ -44,7 +47,7 @@ func BuildPsbt(utxos []*UTXO, recipient string, amount int64, feeRate int64, cha txOuts := make([]*wire.TxOut, 0) txOuts = append(txOuts, wire.NewTxOut(amount, recipientPkScript)) - unsignedTx, selectedUTXOs, changeUTXO, err := BuildUnsignedTransaction(utxos, txOuts, feeRate, changeAddr) + unsignedTx, selectedUTXOs, changeUTXO, err := BuildUnsignedTransaction([]*UTXO{}, txOuts, utxos, feeRate, changeAddr) if err != nil { return nil, nil, nil, err } @@ -62,13 +65,107 @@ func BuildPsbt(utxos []*UTXO, recipient string, amount int64, feeRate int64, cha return p, selectedUTXOs, changeUTXO, nil } +// BuildRunesPsbt builds a bitcoin psbt for runes edict from the given params. +// Assume that the utxo script type is witness. +func BuildRunesPsbt(utxos []*UTXO, paymentUTXOs []*UTXO, recipient string, runeId string, amount uint128.Uint128, feeRate int64, runesChangeAmount uint128.Uint128, runesChange string, change string) (*psbt.Packet, []*UTXO, *UTXO, *UTXO, error) { + chaincfg := sdk.GetConfig().GetBtcChainCfg() + + recipientAddr, err := btcutil.DecodeAddress(recipient, chaincfg) + if err != nil { + return nil, nil, nil, nil, err + } + + recipientPkScript, err := txscript.PayToAddrScript(recipientAddr) + if err != nil { + return nil, nil, nil, nil, err + } + + changeAddr, err := btcutil.DecodeAddress(change, chaincfg) + if err != nil { + return nil, nil, nil, nil, err + } + + runesChangeAddr, err := btcutil.DecodeAddress(runesChange, chaincfg) + if err != nil { + return nil, nil, nil, nil, err + } + + runesChangePkScript, err := txscript.PayToAddrScript(runesChangeAddr) + if err != nil { + return nil, nil, nil, nil, err + } + + txOuts := make([]*wire.TxOut, 0) + + // fill the runes protocol script with empty output script first + txOuts = append(txOuts, wire.NewTxOut(0, []byte{})) + + var runesChangeUTXO *UTXO + edictOutputIndex := uint32(1) + + if runesChangeAmount.Cmp64(0) > 0 { + // we can guarantee that every runes UTXO only includes a single rune by the deposit policy + runesChangeUTXO = GetRunesChangeUTXO(runeId, runesChangeAmount, runesChange, runesChangePkScript, 1) + + // allocate the remaining runes to the first non-OP_RETURN output by default + txOuts = append(txOuts, wire.NewTxOut(RunesOutValue, runesChangePkScript)) + + // advance the edict output index + edictOutputIndex++ + } + + // edict output + txOuts = append(txOuts, wire.NewTxOut(RunesOutValue, recipientPkScript)) + + runesScript, err := BuildEdictScript(runeId, amount, edictOutputIndex) + if err != nil { + return nil, nil, nil, nil, err + } + + // populate the runes protocol script + txOuts[0].PkScript = runesScript + + unsignedTx, selectedUTXOs, changeUTXO, err := BuildUnsignedTransaction(utxos, txOuts, paymentUTXOs, feeRate, changeAddr) + if err != nil { + return nil, nil, nil, nil, err + } + + if runesChangeUTXO != nil { + runesChangeUTXO.Txid = unsignedTx.TxHash().String() + } + + p, err := psbt.NewFromUnsignedTx(unsignedTx) + if err != nil { + return nil, nil, nil, nil, err + } + + for i, utxo := range utxos { + p.Inputs[i].SighashType = txscript.SigHashAll + p.Inputs[i].WitnessUtxo = wire.NewTxOut(int64(utxo.Amount), utxo.PubKeyScript) + } + + for i, utxo := range selectedUTXOs { + p.Inputs[i+len(utxos)].SighashType = txscript.SigHashAll + p.Inputs[i+len(utxos)].WitnessUtxo = wire.NewTxOut(int64(utxo.Amount), utxo.PubKeyScript) + } + + return p, selectedUTXOs, changeUTXO, runesChangeUTXO, nil +} + // BuildUnsignedTransaction builds an unsigned tx from the given params. -func BuildUnsignedTransaction(utxos []*UTXO, txOuts []*wire.TxOut, feeRate int64, change btcutil.Address) (*wire.MsgTx, []*UTXO, *UTXO, error) { +func BuildUnsignedTransaction(utxos []*UTXO, txOuts []*wire.TxOut, paymentUTXOs []*UTXO, feeRate int64, change btcutil.Address) (*wire.MsgTx, []*UTXO, *UTXO, error) { tx := wire.NewMsgTx(TxVersion) + inAmount := int64(0) outAmount := int64(0) + + for _, utxo := range utxos { + AddUTXOToTx(tx, utxo) + inAmount += int64(utxo.Amount) + } + for _, txOut := range txOuts { - if mempool.IsDust(txOut, MinRelayFee) { + if IsDustOut(txOut) { return nil, nil, nil, ErrDustOutput } @@ -83,53 +180,38 @@ func BuildUnsignedTransaction(utxos []*UTXO, txOuts []*wire.TxOut, feeRate int64 changeOut := wire.NewTxOut(0, changePkScript) - selectedUTXOs, err := AddUTXOsToTx(tx, utxos, outAmount, changeOut, feeRate) + selectedUTXOs, err := AddPaymentUTXOsToTx(tx, utxos, inAmount-outAmount, paymentUTXOs, changeOut, feeRate) if err != nil { return nil, nil, nil, err } var changeUTXO *UTXO if len(tx.TxOut) > len(txOuts) { - changeOut := tx.TxOut[len(tx.TxOut)-1] - changeUTXO = &UTXO{ - Txid: tx.TxHash().String(), - Vout: uint64(len(tx.TxOut) - 1), - Address: change.EncodeAddress(), - Amount: uint64(changeOut.Value), - PubKeyScript: changeOut.PkScript, - } + changeUTXO = GetChangeUTXO(tx, change.EncodeAddress()) } return tx, selectedUTXOs, changeUTXO, nil } -// AddUTXOsToTx adds the given utxos to the tx. -func AddUTXOsToTx(tx *wire.MsgTx, utxos []*UTXO, outAmount int64, changeOut *wire.TxOut, feeRate int64) ([]*UTXO, error) { +// AddPaymentUTXOsToTx adds the given payment utxos to the tx. +func AddPaymentUTXOsToTx(tx *wire.MsgTx, utxos []*UTXO, inOutDiff int64, paymentUtxos []*UTXO, changeOut *wire.TxOut, feeRate int64) ([]*UTXO, error) { selectedUTXOs := make([]*UTXO, 0) - inputAmount := int64(0) - - for _, utxo := range utxos { - txIn := new(wire.TxIn) + paymentValue := int64(0) - hash, err := chainhash.NewHashFromStr(utxo.Txid) - if err != nil { - return nil, err - } - - txIn.PreviousOutPoint = *wire.NewOutPoint(hash, uint32(utxo.Vout)) - - tx.AddTxIn(txIn) + for _, utxo := range paymentUtxos { + AddUTXOToTx(tx, utxo) tx.AddTxOut(changeOut) + utxos = append(utxos, utxo) selectedUTXOs = append(selectedUTXOs, utxo) - inputAmount += int64(utxo.Amount) + paymentValue += int64(utxo.Amount) fee := GetTxVirtualSize(tx, utxos) * feeRate - changeValue := inputAmount - outAmount - fee + changeValue := paymentValue + inOutDiff - fee if changeValue > 0 { tx.TxOut[len(tx.TxOut)-1].Value = changeValue - if mempool.IsDust(tx.TxOut[len(tx.TxOut)-1], btcutil.Amount(MinRelayFee)) { + if IsDustOut(tx.TxOut[len(tx.TxOut)-1]) { tx.TxOut = tx.TxOut[0 : len(tx.TxOut)-1] } @@ -144,7 +226,7 @@ func AddUTXOsToTx(tx *wire.MsgTx, utxos []*UTXO, outAmount int64, changeOut *wir if changeValue < 0 { feeWithoutChange := GetTxVirtualSize(tx, selectedUTXOs) * feeRate - if inputAmount-outAmount-feeWithoutChange >= 0 { + if paymentValue+inOutDiff-feeWithoutChange >= 0 { return selectedUTXOs, nil } } @@ -153,6 +235,51 @@ func AddUTXOsToTx(tx *wire.MsgTx, utxos []*UTXO, outAmount int64, changeOut *wir return nil, ErrInsufficientUTXOs } +// AddUTXOToTx adds the given utxo to the specified tx +// Make sure the utxo is valid +func AddUTXOToTx(tx *wire.MsgTx, utxo *UTXO) { + txIn := new(wire.TxIn) + + hash, err := chainhash.NewHashFromStr(utxo.Txid) + if err != nil { + panic(err) + } + + txIn.PreviousOutPoint = *wire.NewOutPoint(hash, uint32(utxo.Vout)) + + tx.AddTxIn(txIn) +} + +// GetChangeUTXO returns the change output from the given tx +// Make sure that the tx is valid and the change output is the last output +func GetChangeUTXO(tx *wire.MsgTx, change string) *UTXO { + changeOut := tx.TxOut[len(tx.TxOut)-1] + + return &UTXO{ + Txid: tx.TxHash().String(), + Vout: uint64(len(tx.TxOut) - 1), + Address: change, + Amount: uint64(changeOut.Value), + PubKeyScript: changeOut.PkScript, + } +} + +// GetRunesChangeUTXO gets the runes change utxo. +func GetRunesChangeUTXO(runeId string, changeAmount uint128.Uint128, change string, changePkScript []byte, outIndex uint32) *UTXO { + return &UTXO{ + Vout: uint64(outIndex), + Address: change, + Amount: RunesOutValue, + PubKeyScript: changePkScript, + Runes: []*RuneBalance{ + { + Id: runeId, + Amount: changeAmount.String(), + }, + }, + } +} + // GetTxVirtualSize gets the virtual size of the given tx. // Assume that the utxo script type is p2tr, p2wpkh, p2sh-p2wpkh or p2pkh. func GetTxVirtualSize(tx *wire.MsgTx, utxos []*UTXO) int64 { @@ -186,8 +313,13 @@ func GetTxVirtualSize(tx *wire.MsgTx, utxos []*UTXO) int64 { return mempool.GetTxVirtualSize(btcutil.NewTx(newTx)) } -// CheckOutput checks the given output -func CheckOutput(address string, amount int64) error { +// IsDustOut returns true if the given output is dust, false otherwise +func IsDustOut(out *wire.TxOut) bool { + return !IsOpReturnOutput(out) && mempool.IsDust(out, MinRelayFee) +} + +// CheckOutputAmount checks if the given output amount is dust +func CheckOutputAmount(address string, amount int64) error { addr, err := btcutil.DecodeAddress(address, sdk.GetConfig().GetBtcChainCfg()) if err != nil { return err @@ -198,9 +330,14 @@ func CheckOutput(address string, amount int64) error { return err } - if mempool.IsDust(&wire.TxOut{Value: amount, PkScript: pkScript}, MinRelayFee) { + if IsDustOut(&wire.TxOut{Value: amount, PkScript: pkScript}) { return ErrDustOutput } return nil } + +// IsOpReturnOutput returns true if the script of the given out starts with OP_RETURN +func IsOpReturnOutput(out *wire.TxOut) bool { + return len(out.PkScript) > 0 && out.PkScript[0] == txscript.OP_RETURN +} diff --git a/x/btcbridge/types/deposit_policy.go b/x/btcbridge/types/deposit_policy.go index ad2d1e32..85dc382a 100644 --- a/x/btcbridge/types/deposit_policy.go +++ b/x/btcbridge/types/deposit_policy.go @@ -8,14 +8,29 @@ import ( ) const ( - // maximum allowed number of the non-vault outputs for the deposit transaction + // maximum allowed number of the non-vault outputs for the btc deposit transaction MaxNonVaultOutNum = 1 + + // maximum allowed number of the non-vault outputs for the runes deposit transaction + RunesMaxNonVaultOutNum = 3 + + // allowed number of edicts in the runes payload for the runes deposit transaction + RunesEdictNum = 1 ) -// ExtractRecipientAddr extracts the recipient address for minting voucher token. +// ExtractRecipientAddr extracts the recipient address for minting voucher token by the type of the asset to be deposited +func ExtractRecipientAddr(tx *wire.MsgTx, prevTx *wire.MsgTx, vaults []*Vault, isRunes bool, chainCfg *chaincfg.Params) (btcutil.Address, error) { + if isRunes { + return ExtractRunesRecipientAddr(tx, prevTx, vaults, chainCfg) + } + + return ExtractCommonRecipientAddr(tx, prevTx, vaults, chainCfg) +} + +// ExtractCommonRecipientAddr extracts the recipient address for minting voucher token in the common case. // First, extract the recipient from the tx out which is a non-vault address; -// Then fallback to the first input -func ExtractRecipientAddr(tx *wire.MsgTx, prevTx *wire.MsgTx, vaults []*Vault, chainCfg *chaincfg.Params) (btcutil.Address, error) { +// Then fall back to the first input +func ExtractCommonRecipientAddr(tx *wire.MsgTx, prevTx *wire.MsgTx, vaults []*Vault, chainCfg *chaincfg.Params) (btcutil.Address, error) { var recipient btcutil.Address nonVaultOutCount := 0 @@ -48,7 +63,57 @@ func ExtractRecipientAddr(tx *wire.MsgTx, prevTx *wire.MsgTx, vaults []*Vault, c return recipient, nil } - // fallback to extract from the first input + // fall back to extract from the first input + pkScript, err := txscript.ParsePkScript(prevTx.TxOut[tx.TxIn[0].PreviousOutPoint.Index].PkScript) + if err != nil { + return nil, err + } + + return pkScript.Address(chainCfg) +} + +// ExtractRunesRecipientAddr extracts the recipient address for minting runes voucher token. +// First, extract the recipient from the tx out which is a non-vault and non-OP_RETURN output; +// Then fall back to the first input +func ExtractRunesRecipientAddr(tx *wire.MsgTx, prevTx *wire.MsgTx, vaults []*Vault, chainCfg *chaincfg.Params) (btcutil.Address, error) { + var recipient btcutil.Address + + nonVaultOutCount := 0 + + // extract from the tx out which is a non-vault and non-OP_RETURN output + for _, out := range tx.TxOut { + if IsOpReturnOutput(out) { + nonVaultOutCount++ + continue + } + + pkScript, err := txscript.ParsePkScript(out.PkScript) + if err != nil { + return nil, err + } + + addr, err := pkScript.Address(chainCfg) + if err != nil { + return nil, err + } + + vault := SelectVaultByBitcoinAddress(vaults, addr.EncodeAddress()) + if vault == nil { + recipient = addr + nonVaultOutCount++ + } + } + + // exceed allowed non vault out number + if nonVaultOutCount > RunesMaxNonVaultOutNum { + return nil, ErrInvalidDepositTransaction + } + + if recipient != nil { + return recipient, nil + } + + // fall back to extract from the first input pkScript, err := txscript.ParsePkScript(prevTx.TxOut[tx.TxIn[0].PreviousOutPoint.Index].PkScript) if err != nil { return nil, err @@ -56,3 +121,31 @@ func ExtractRecipientAddr(tx *wire.MsgTx, prevTx *wire.MsgTx, vaults []*Vault, c return pkScript.Address(chainCfg) } + +// CheckRunesDepositTransaction checks if the given tx is valid runes deposit tx +func CheckRunesDepositTransaction(tx *wire.MsgTx, vaults []*Vault) (*Edict, error) { + edicts, err := ParseRunes(tx) + if err != nil { + return nil, ErrInvalidDepositTransaction + } + + if len(edicts) == 0 { + return nil, nil + } + + if len(edicts) != RunesEdictNum { + return nil, ErrInvalidDepositTransaction + } + + // even split is not supported + if edicts[0].Output == uint32(len(tx.TxOut)) { + return nil, ErrInvalidDepositTransaction + } + + vault := SelectVaultByPkScript(vaults, tx.TxOut[edicts[0].Output].PkScript) + if vault == nil || vault.AssetType != AssetType_ASSET_TYPE_RUNE { + return nil, ErrInvalidDepositTransaction + } + + return edicts[0], nil +} diff --git a/x/btcbridge/types/errors.go b/x/btcbridge/types/errors.go index a4b67f8f..58c38bba 100644 --- a/x/btcbridge/types/errors.go +++ b/x/btcbridge/types/errors.go @@ -35,7 +35,11 @@ var ( ErrInvalidAmount = errorsmod.Register(ModuleName, 6100, "invalid amount") ErrInvalidFeeRate = errorsmod.Register(ModuleName, 6101, "invalid fee rate") - ErrDustOutput = errorsmod.Register(ModuleName, 6102, "dust output value") - ErrInsufficientUTXOs = errorsmod.Register(ModuleName, 6103, "insufficient utxos") - ErrFailToSerializePsbt = errorsmod.Register(ModuleName, 6104, "failed to serialize psbt") + ErrAssetNotSupported = errorsmod.Register(ModuleName, 6102, "asset not supported") + ErrDustOutput = errorsmod.Register(ModuleName, 6103, "too small output amount") + ErrInsufficientUTXOs = errorsmod.Register(ModuleName, 6104, "insufficient utxos") + ErrFailToSerializePsbt = errorsmod.Register(ModuleName, 6105, "failed to serialize psbt") + + ErrInvalidRunes = errorsmod.Register(ModuleName, 7100, "invalid runes") + ErrInvalidRuneId = errorsmod.Register(ModuleName, 7101, "invalid rune id") ) diff --git a/x/btcbridge/types/keys.go b/x/btcbridge/types/keys.go index 6bf8529a..27314190 100644 --- a/x/btcbridge/types/keys.go +++ b/x/btcbridge/types/keys.go @@ -32,11 +32,11 @@ var ( BtcBestBlockHeaderKey = []byte{0x13} // key for the best block height BtcSigningRequestPrefix = []byte{0x14} // prefix for each key to a signing request - BtcUtxoKeyPrefix = []byte{0x15} // prefix for each key to a utxo - BtcOwnerUtxoKeyPrefix = []byte{0x16} // prefix for each key to an owned utxo - - BtcMintedTxHashKeyPrefix = []byte{0x17} // prefix for each key to a minted tx hash + BtcUtxoKeyPrefix = []byte{0x15} // prefix for each key to a utxo + BtcOwnerUtxoKeyPrefix = []byte{0x16} // prefix for each key to an owned utxo + BtcOwnerRunesUtxoKeyPrefix = []byte{0x17} // prefix for each key to an owned runes utxo + BtcMintedTxHashKeyPrefix = []byte{0x18} // prefix for each key to a minted tx hash ) func Int64ToBytes(number uint64) []byte { @@ -56,6 +56,14 @@ func BtcOwnerUtxoKey(owner string, hash string, vout uint64) []byte { return key } +func BtcOwnerRunesUtxoKey(owner string, id string, hash string, vout uint64) []byte { + key := append(append(BtcOwnerRunesUtxoKeyPrefix, []byte(owner)...), []byte(id)...) + key = append(key, []byte(hash)...) + key = append(key, Int64ToBytes(vout)...) + + return key +} + func BtcBlockHeaderHashKey(hash string) []byte { return append(BtcBlockHeaderHashPrefix, []byte(hash)...) } diff --git a/x/btcbridge/types/message_withdraw_bitcoin.go b/x/btcbridge/types/message_withdraw_bitcoin.go index 68898296..7af3b1db 100644 --- a/x/btcbridge/types/message_withdraw_bitcoin.go +++ b/x/btcbridge/types/message_withdraw_bitcoin.go @@ -48,16 +48,11 @@ func (msg *MsgWithdrawBitcoinRequest) ValidateBasic() error { return sdkerrors.Wrapf(err, "invalid Sender address (%s)", err) } - coin, err := sdk.ParseCoinNormalized(msg.Amount) + _, err = sdk.ParseCoinNormalized(msg.Amount) if err != nil { return sdkerrors.Wrapf(ErrInvalidAmount, "invalid amount %s", msg.Amount) } - err = CheckOutput(msg.Sender, coin.Amount.Int64()) - if err != nil { - return err - } - feeRate, err := strconv.ParseInt(msg.FeeRate, 10, 64) if err != nil { return err diff --git a/x/btcbridge/types/params.go b/x/btcbridge/types/params.go index c6d49a6f..9e7460fc 100644 --- a/x/btcbridge/types/params.go +++ b/x/btcbridge/types/params.go @@ -1,6 +1,13 @@ package types -import sdk "github.com/cosmos/cosmos-sdk/types" +import ( + "bytes" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/txscript" + + sdk "github.com/cosmos/cosmos-sdk/types" +) // NewParams creates a new Params instance func NewParams(relayers []string) Params { @@ -69,3 +76,37 @@ func SelectVaultByPubKey(vaults []*Vault, pubKey string) *Vault { return nil } + +// SelectVaultByAssetType returns the vault by the asset type +func SelectVaultByAssetType(vaults []*Vault, assetType AssetType) *Vault { + for _, v := range vaults { + if v.AssetType == assetType { + return v + } + } + + return nil +} + +// SelectVaultByPkScript returns the vault by the pk script +func SelectVaultByPkScript(vaults []*Vault, pkScript []byte) *Vault { + chainCfg := sdk.GetConfig().GetBtcChainCfg() + + for _, v := range vaults { + addr, err := btcutil.DecodeAddress(v.Address, chainCfg) + if err != nil { + continue + } + + addrScript, err := txscript.PayToAddrScript(addr) + if err != nil { + continue + } + + if bytes.Equal(addrScript, pkScript) { + return v + } + } + + return nil +} diff --git a/x/btcbridge/types/runes.go b/x/btcbridge/types/runes.go new file mode 100644 index 00000000..9af3c685 --- /dev/null +++ b/x/btcbridge/types/runes.go @@ -0,0 +1,209 @@ +package types + +import ( + "fmt" + "strconv" + "strings" + + "lukechampine.com/uint128" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +const ( + // runes protocol name + RunesProtocolName = "runes" + + // runes magic number + MagicNumber = txscript.OP_13 + + // tag indicating that the following are edicts + TagBody = 0 + + // the number of components of each edict + EdictLen = 4 + + // sats in the runes output by default + RunesOutValue = 546 +) + +// ParseRunes parses the potential runes protocol from the given tx; +// If no OP_RETURN found, no error returned +// Only support edicts for now +func ParseRunes(tx *wire.MsgTx) ([]*Edict, error) { + for _, out := range tx.TxOut { + tokenizer := txscript.MakeScriptTokenizer(0, out.PkScript) + if !tokenizer.Next() || tokenizer.Err() != nil || tokenizer.Opcode() != txscript.OP_RETURN { + continue + } + + if !tokenizer.Next() || tokenizer.Err() != nil || tokenizer.Opcode() != MagicNumber { + continue + } + + var payload []byte + + for tokenizer.Next() { + if txscript.IsSmallInt(tokenizer.Opcode()) || tokenizer.Opcode() <= txscript.OP_PUSHDATA4 { + payload = append(payload, tokenizer.Data()...) + } else { + return nil, ErrInvalidRunes + } + } + + if tokenizer.Err() != nil { + return nil, ErrInvalidRunes + } + + return ParseEdicts(tx, payload) + } + + return nil, nil +} + +// ParseEdicts parses the given payload to a set of edicts +func ParseEdicts(tx *wire.MsgTx, payload []byte) ([]*Edict, error) { + integers, err := DecodeVec(payload) + if err != nil { + return nil, err + } + + if len(integers) < EdictLen+1 || len(integers[1:])%EdictLen != 0 || !integers[0].Equals(uint128.From64(TagBody)) { + return nil, ErrInvalidRunes + } + + integers = integers[1:] + + edicts := make([]*Edict, 0) + + for i := 0; i < len(integers); i = i + 4 { + output := uint32(integers[i+3].Big().Uint64()) + if output > uint32(len(tx.TxOut)) { + return nil, ErrInvalidRunes + } + + edict := Edict{ + Id: &RuneId{ + Block: integers[i].Big().Uint64(), + Tx: uint32(integers[i+1].Big().Uint64()), + }, + Amount: integers[i+2].String(), + Output: output, + } + + edicts = append(edicts, &edict) + } + + return edicts, nil +} + +// ParseEdict parses the given payload to edict +func ParseEdict(payload []byte) (*Edict, error) { + integers, err := DecodeVec(payload) + if err != nil { + return nil, err + } + + if len(integers) != EdictLen+1 && !integers[0].Equals(uint128.From64(TagBody)) { + return nil, ErrInvalidRunes + } + + return &Edict{ + Id: &RuneId{ + Block: integers[1].Big().Uint64(), + Tx: uint32(integers[2].Big().Uint64()), + }, + Amount: integers[3].String(), + Output: uint32(integers[4].Big().Uint64()), + }, nil +} + +// BuildEdictScript builds the edict script +func BuildEdictScript(runeId string, amount uint128.Uint128, output uint32) ([]byte, error) { + var id RuneId + id.MustUnmarshal([]byte(runeId)) + + edict := Edict{ + Id: &id, + Amount: amount.String(), + Output: output, + } + + payload := []byte{TagBody} + payload = append(payload, edict.MustMarshalLEB128()...) + + scriptBuilder := txscript.NewScriptBuilder() + scriptBuilder.AddOp(txscript.OP_RETURN).AddOp(MagicNumber).AddData(payload) + + return scriptBuilder.Script() +} + +func (id *RuneId) ToString() string { + return fmt.Sprintf("%d:%d", id.Block, id.Tx) +} + +func (id *RuneId) FromString(idStr string) error { + parts := strings.Split(idStr, ":") + if len(parts) != 2 { + return ErrInvalidRuneId + } + + block, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return err + } + + tx, err := strconv.ParseUint(parts[1], 10, 32) + if err != nil { + return err + } + + id.Block = block + id.Tx = uint32(tx) + + return nil +} + +func (id *RuneId) MustUnmarshal(bz []byte) { + err := id.FromString(string(bz)) + if err != nil { + panic(err) + } +} + +// Denom returns the corresponding denom for the runes voucher token +func (id *RuneId) Denom() string { + return fmt.Sprintf("%s/%s", RunesProtocolName, id.ToString()) +} + +// FromDenom converts the denom to the rune id +func (id *RuneId) FromDenom(denom string) { + idStr := strings.TrimPrefix(denom, fmt.Sprintf("%s/", RunesProtocolName)) + + id.MustUnmarshal([]byte(idStr)) +} + +func (e *Edict) MustMarshalLEB128() []byte { + amount := RuneAmountFromString(e.Amount) + + payload := make([]byte, 0) + + payload = append(payload, EncodeUint64(e.Id.Block)...) + payload = append(payload, EncodeUint32(e.Id.Tx)...) + payload = append(payload, EncodeUint128(&amount)...) + payload = append(payload, EncodeUint32(e.Output)...) + + return payload +} + +// RuneAmountFromString converts the given string to the rune amount +// Panic if any error occurred +func RuneAmountFromString(str string) uint128.Uint128 { + amount, err := uint128.FromString(str) + if err != nil { + panic(err) + } + + return amount +} diff --git a/x/btcbridge/types/runes_test.go b/x/btcbridge/types/runes_test.go new file mode 100644 index 00000000..6711dca7 --- /dev/null +++ b/x/btcbridge/types/runes_test.go @@ -0,0 +1,84 @@ +package types_test + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/btcsuite/btcd/wire" + + "github.com/sideprotocol/side/x/btcbridge/types" +) + +func TestParseRunes(t *testing.T) { + testCases := []struct { + name string + pkScriptHex string + edicts []*types.Edict + expectPass bool + }{ + { + name: "valid runes edict", + pkScriptHex: "6a5d0b00c0a2330380cab5ee0101", + edicts: []*types.Edict{ + { + Id: &types.RuneId{Block: 840000, Tx: 3}, + Amount: "500000000", + Output: 1, + }, + }, + expectPass: true, + }, + { + name: "output index is out of range", + pkScriptHex: "6a5d0b00c0a2330380cab5ee0102", + expectPass: false, + }, + { + name: "no OP_RETURN", + pkScriptHex: "615d0b00c0a2330380cab5ee0102", + expectPass: true, + edicts: nil, + }, + { + name: "no runes magic number", + pkScriptHex: "6a5c0b00c0a2330380cab5ee0102", + expectPass: true, + edicts: nil, + }, + { + name: "non data push op", + pkScriptHex: "6a5d4f00c0a2330380cab5ee0102", + expectPass: false, + }, + { + name: "no tag body for edicts", + pkScriptHex: "6a5d0b01c0a2330380cab5ee0102", + expectPass: false, + }, + { + name: "invalid edict", + pkScriptHex: "6a5d0b00c0a2330380cab5ee01", + expectPass: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + pkScript, err := hex.DecodeString(tc.pkScriptHex) + require.NoError(t, err) + + tx := wire.NewMsgTx(types.TxVersion) + tx.AddTxOut(wire.NewTxOut(0, pkScript)) + + edicts, err := types.ParseRunes(tx) + if tc.expectPass { + require.NoError(t, err) + require.EqualValues(t, tc.edicts, edicts) + } else { + require.NotNil(t, err) + } + }) + } +} diff --git a/x/btcbridge/types/varint.go b/x/btcbridge/types/varint.go new file mode 100644 index 00000000..2ac94712 --- /dev/null +++ b/x/btcbridge/types/varint.go @@ -0,0 +1,96 @@ +package types + +import ( + "errors" + "math/big" + + "lukechampine.com/uint128" +) + +func EncodeUint32(n uint32) []byte { + var result []byte + + for n >= 128 { + result = append(result, byte(n&0x7F|0x80)) + n >>= 7 + } + + result = append(result, byte(n)) + return result +} + +func EncodeUint64(n uint64) []byte { + var result []byte + + for n >= 128 { + result = append(result, byte(n&0x7F|0x80)) + n >>= 7 + } + + result = append(result, byte(n)) + return result +} + +func EncodeUint128(n *uint128.Uint128) []byte { + return EncodeBigInt(n.Big()) +} + +func EncodeBigInt(n *big.Int) []byte { + var result []byte + + for n.Cmp(big.NewInt(128)) >= 0 { + temp := new(big.Int).Set(n) + last := temp.And(n, new(big.Int).SetUint64(0b0111_1111)) + result = append(result, last.Or(last, new(big.Int).SetUint64(0b1000_0000)).Bytes()[0]) + n.Rsh(n, 7) + } + + if len(n.Bytes()) == 0 { + result = append(result, 0) + } else { + result = append(result, n.Bytes()...) + } + + return result +} + +func Decode(bz []byte) (uint128.Uint128, int, error) { + n := big.NewInt(0) + + for i, b := range bz { + if i > 18 { + return uint128.Zero, 0, errors.New("varint overflow") + } + + value := uint64(b) & 0b0111_1111 + if i == 18 && value&0b0111_1100 != 0 { + return uint128.Zero, 0, errors.New("varint too large") + } + + temp := new(big.Int).SetUint64(value) + n.Or(n, temp.Lsh(temp, uint(7*i))) + + if b&0b1000_0000 == 0 { + return uint128.FromBig(n), i + 1, nil + } + } + + return uint128.Zero, 0, errors.New("varint too short") +} + +func DecodeVec(payload []byte) ([]uint128.Uint128, error) { + vec := make([]uint128.Uint128, 0) + i := 0 + + for i < len(payload) { + value, length, err := Decode(payload[i:]) + if err != nil { + return nil, err + } + + vec = append(vec, value) + i += length + } + + return vec, nil +}