diff --git a/.circleci/config.yml b/.circleci/config.yml index fc7fdaa..c5c5c7c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,7 +18,6 @@ jobs: command: "go env" - go/load-cache: key: go-mod-v6-{{ checksum "go.sum" }} - - add_ssh_keys - go/mod-download - go/save-cache: key: go-mod-v6-{{ checksum "go.sum" }} @@ -46,7 +45,6 @@ jobs: resource_class: large steps: - checkout - - add_ssh_keys - aws-ecr/build-image: push-image: false dockerfile: Dockerfile @@ -54,7 +52,6 @@ jobs: build-path: ./ tag: "$CIRCLE_SHA1,$CIRCLE_TAG" repo: "$CIRCLE_PROJECT_REPONAME" - extra-build-args: "--secret id=sshKey,src=/home/circleci/.ssh/$DEPLOY_KEY_NAME" - run: name: Save Docker image to export it to workspace command: | @@ -93,6 +90,10 @@ workflows: filters: tags: only: /.*/ + branches: + only: + - main + - dev - push_docker: requires: - build_docker diff --git a/Dockerfile b/Dockerfile index 15572b7..197bac2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,16 +11,11 @@ RUN apk add --no-cache --update openssh git make build-base linux-headers libc-d pkgconfig zeromq-dev musl-dev alpine-sdk libsodium-dev \ libzmq-static libsodium-static gcc -# Load private repos SSH deploy key and configure ssh -RUN mkdir -p /root/.ssh && ssh-keyscan github.com >> /root/.ssh/known_hosts -RUN git config --global url."git@github.com:".insteadOf "https://github.com/" -ENV GOPRIVATE=github.com/babylonchain/* - # Build WORKDIR /go/src/github.com/babylonchain/cli-tools # Cache dependencies COPY go.mod go.sum /go/src/github.com/babylonchain/cli-tools/ -RUN --mount=type=secret,id=sshKey,target=/root/.ssh/id_rsa go mod download +RUN go mod download # Copy the rest of the files COPY ./ /go/src/github.com/babylonchain/cli-tools/ diff --git a/Makefile b/Makefile index 43809bb..6b21cf6 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ $(BUILDDIR)/: mkdir -p $(BUILDDIR)/ build-docker: - $(DOCKER) build --secret id=sshKey,src=${BBN_PRIV_DEPLOY_KEY} --tag babylonchain/cli-tools -f Dockerfile \ + $(DOCKER) build --tag babylonchain/cli-tools -f Dockerfile \ $(shell git rev-parse --show-toplevel) .PHONY: build build-docker install tests diff --git a/cmd/createStakingTxCmd.go b/cmd/createStakingTxCmd.go index c9c56c7..1e83bd1 100644 --- a/cmd/createStakingTxCmd.go +++ b/cmd/createStakingTxCmd.go @@ -72,7 +72,7 @@ func serializeBTCTx(tx *wire.MsgTx) ([]byte, error) { return txBuf.Bytes(), nil } -func serializeBTCTxToHex(tx *wire.MsgTx) (string, error) { +func SerializeBTCTxToHex(tx *wire.MsgTx) (string, error) { bytes, err := serializeBTCTx(tx) if err != nil { @@ -302,7 +302,7 @@ var createStakingTxCmd = &cobra.Command{ return err } - serializedTx, err := serializeBTCTxToHex(tx) + serializedTx, err := SerializeBTCTxToHex(tx) if err != nil { return err } diff --git a/cmd/createUnbondingTxCmd.go b/cmd/createUnbondingTxCmd.go index 4deb5b4..d237b7f 100644 --- a/cmd/createUnbondingTxCmd.go +++ b/cmd/createUnbondingTxCmd.go @@ -225,7 +225,7 @@ var createUnbondingTxCmd = &cobra.Command{ unbondingTxHash := unbondingTx.TxHash() - unbondingTxHex, err := serializeBTCTxToHex(unbondingTx) + unbondingTxHex, err := SerializeBTCTxToHex(unbondingTx) if err != nil { return err diff --git a/cmd/createWithdrawTxCmg.go b/cmd/createWithdrawTxCmg.go index 503b431..481d3cb 100644 --- a/cmd/createWithdrawTxCmg.go +++ b/cmd/createWithdrawTxCmg.go @@ -254,7 +254,7 @@ var createWithdrawCmd = &cobra.Command{ } // at this point we created unsigned withdraw tx lets create response - serializedWithdrawTx, err := serializeBTCTxToHex(info.spendStakeTx) + serializedWithdrawTx, err := SerializeBTCTxToHex(info.spendStakeTx) if err != nil { return err @@ -348,7 +348,7 @@ var createWithdrawCmd = &cobra.Command{ // serialize tx with witness - serializedWithdrawTx, err = serializeBTCTxToHex(info.spendStakeTx) + serializedWithdrawTx, err = SerializeBTCTxToHex(info.spendStakeTx) if err != nil { return err diff --git a/cmd/timestampFileCmd.go b/cmd/timestampFileCmd.go new file mode 100644 index 0000000..46eb26d --- /dev/null +++ b/cmd/timestampFileCmd.go @@ -0,0 +1,181 @@ +package cmd + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/spf13/cobra" +) + +const ( + FlagFeeInTx = "fee-in-tx" +) + +type TimestampFileOutput struct { + TimestampTx string `json:"timestamp_tx_hex"` + FileHash string `json:"file_hash"` +} + +func init() { + _ = btcTimestampFileCmd.Flags().Int64(FlagFeeInTx, 2000, "the amount of satoshi to pay as fee for the tx") + _ = btcTimestampFileCmd.Flags().String(FlagNetwork, "signet", "network one of (mainnet, testnet3, regtest, simnet, signet)") + + rootCmd.AddCommand(btcTimestampFileCmd) +} + +var btcTimestampFileCmd = &cobra.Command{ + Use: "create-timestamp-transaction [funded-tx-addr-hex] [file-path] [address]", + Example: `cli-tools create-timestamp-transaction [funded-tx-addr-hex] ./path/to/file/to/timestamp 836e9fc730ff37de48f2ff3a76b3c2380fbabaf66d9e50754d86b2a2e2952156`, + Short: "Creates a timestamp btc transaction by hashing the file input.", + Long: `Creates a timestamp BTC transaction with 2 outputs and one input. + One output is the nullDataScript of the file hash, as the file hash + being the sha256 of the input file path. This output is the timestamp of the file. + The other output is the pay to addr script which contains the pay to witness pubkey + with the value as ({funded-tx-output-value} - {FlagFeeInTx}). This output is needed + to continue to have spendable funds to the p2wpkh address.`, + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + fundedTxHex, inputFilePath, addressStr := args[0], args[1], args[2] + flags := cmd.Flags() + feeInTx, err := flags.GetInt64(FlagFeeInTx) + if err != nil { + return fmt.Errorf("failed to parse flag %s: %w", FlagFeeInTx, err) + } + + networkParamStr, err := flags.GetString(FlagNetwork) + if err != nil { + return fmt.Errorf("failed to parse flag %s: %w", FlagNetwork, err) + } + + btcParams, err := getBtcNetworkParams(networkParamStr) + if err != nil { + return fmt.Errorf("unable parse BTC network %s: %w", networkParamStr, err) + } + + timestampOutput, err := CreateTimestampTx(fundedTxHex, inputFilePath, addressStr, feeInTx, btcParams) + if err != nil { + return fmt.Errorf("failed to create timestamping tx: %w", err) + } + + PrintRespJSON(timestampOutput) + return nil + }, +} + +func outputIndexForPkScript(pkScript []byte, tx *wire.MsgTx) (int, error) { + for i, txOut := range tx.TxOut { + if bytes.Equal(txOut.PkScript, pkScript) { + return i, nil + } + } + return -1, fmt.Errorf("unable to find output index for pk script") +} + +// CreateTimestampTx outputs the hash of file and BTC transaction that timestamp that +// hash in one of the outputs. The funded tx needs to have one output with value +// for the changeAddress p2wpkh. The changeAddress needs to be a EncodeAddress +// which is the encoding of the payment address associated with the Address value, +// to be used to generate a pay to address script pay-to-witness-pubkey-hash (P2WKH) format. +func CreateTimestampTx( + fundedTxHex, filePath, changeAddress string, + fee int64, + networkParams *chaincfg.Params, +) (*TimestampFileOutput, error) { + txOutFileHash, fileHash, err := txOutTimestampFile(filePath) + if err != nil { + return nil, fmt.Errorf("unable to create tx out with filepath %s: %w", filePath, err) + } + + fundingTx, _, err := newBTCTxFromHex(fundedTxHex) + if err != nil { + return nil, fmt.Errorf("unable parse BTC Tx %s: %w", fundedTxHex, err) + } + + address, err := btcutil.DecodeAddress(changeAddress, networkParams) + if err != nil { + return nil, fmt.Errorf("invalid address %s: %w", changeAddress, err) + } + + addressPkScript, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, fmt.Errorf("unable to create pk script from address %s: %w", changeAddress, err) + } + + if !txscript.IsPayToWitnessPubKeyHash(addressPkScript) { + return nil, fmt.Errorf("address %s is not a pay-to-witness-pubkey-hash", changeAddress) + } + + fundingOutputIdx, err := outputIndexForPkScript(addressPkScript, fundingTx) + if err != nil { + return nil, fmt.Errorf("unable to find output index for pk script: %w", err) + } + fundingTxHash := fundingTx.TxHash() + fundingInput := wire.NewTxIn( + wire.NewOutPoint(&fundingTxHash, uint32(fundingOutputIdx)), + nil, + nil, + ) + + valueIn := fundingTx.TxOut[fundingOutputIdx].Value + if valueIn < fee { + return nil, fmt.Errorf("the value of input in %d is bigger than the fee %d", valueIn, fee) + } + + changeOutput := wire.NewTxOut( + valueIn-fee, + addressPkScript, + ) + + timestampTx := wire.NewMsgTx(2) + timestampTx.AddTxIn(fundingInput) + timestampTx.AddTxOut(changeOutput) + timestampTx.AddTxOut(txOutFileHash) + + txHex, err := SerializeBTCTxToHex(timestampTx) + if err != nil { + return nil, fmt.Errorf("failed to serialize timestamping tx: %w", err) + } + + return &TimestampFileOutput{ + TimestampTx: txHex, + FileHash: hex.EncodeToString(fileHash), + }, nil +} + +func txOutTimestampFile(filePath string) (txOut *wire.TxOut, fileHash []byte, err error) { + fileHash, err = hashFromFile(filePath) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate hash from file %s: %w", filePath, err) + } + + dataScript, err := txscript.NullDataScript(fileHash) + if err != nil { + return nil, nil, fmt.Errorf("failed to create op return with hash from file %s: %w", fileHash, err) + } + + return wire.NewTxOut(0, dataScript), fileHash, nil +} + +func hashFromFile(filePath string) ([]byte, error) { + h := sha256.New() + + f, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("failed to open the file %s: %w", filePath, err) + } + defer f.Close() + + if _, err := io.Copy(h, f); err != nil { + return nil, err + } + + return h.Sum(nil), nil +} diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..90d56b4 --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,140 @@ +# Explain CLI commands + +## BTC Timestamp File + +The command `create-timestamp-transaction` is used to timestamp a file into bitcoin. +In this case, timestamping refers to generating a SHA256 hash of the file and +creating an output containing a `NullDataScript` with the hash in it and a zero +value. This transaction needs a funded encoded address that converts pubkeys to +P2WPKH addresses. + +Following are instructions on how the timestamp can be generated and submitted +to Bitcoin. To execute the below commands, you need a running +[bitcoind](https://github.com/bitcoin/bitcoin) daemon that has access to a +funded wallet. + +1. Create a new Wallet + +```shell +$ bitcoin-cli createwallet timestamp-w + +{ + "name": "timestamp-w" +} +``` + +2. Generate a new address + +```shell +$ bitcoin-cli -rpcwallet=timestamp-w getnewaddress + +bcrt1qagw463xxngygwsdjrm5f4etvr2hkq0yq0up8ly +``` + +3. Send funds to this address + +```shell +$ bitcoin-cli -rpcwallet=wallet-with-funds sendtoaddress bcrt1qagw463xxngygwsdjrm5f4etvr2hkq0yq0up8ly 15 + +6ce4a2b2797da995d2f49ffd707d1bb34f1717205e6f7fa3d6f3f1aa8b2c9eca +``` + +4. Get the tx data of the `sendtoaddress` transaction + +```shell +$ bitcoin-cli -rpcwallet=timestamp-w getrawtransaction 6ce4a2b2797da995d2f49ffd707d1bb34f1717205e6f7fa3d6f3f1aa8b2c9eca + +020000000001014db94bb61981724775d3a9a27dc62facbb422e5d76887205019dab39e031f7400000000000fdffffff02002f685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c80389c9bd000000000160014dd619252c3983779e6938366ece5d4e2015172b3024730440220180472b948c4fc4e2c608462375b41ff545ef2d0fe97d3ae8ac1e6ad25a6de1f0220036e23c181cb4b65264a58524215e97b27501d1039cd3a2f7d009f860aa8ed46012102a932128cbf3ddf90e12a4c94a6acce81ea3d1ea1dc325101f90b013ccecc60fe95000000 +``` + +5. Create the timestamp transaction using the `create-timestamp-transaction` utility. +The first parameter corresponds to the transaction data, the second is the file +that will be timestamped, and the last parameter is the address previously +created that will fund the transaction. + +```shell +$ ./build/cli-tools create-timestamp-transaction 020000000001014db94bb61981724775d3a9a27dc62facbb422e5d76887205019dab39e031f7400000000000fdffffff02002f685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c80389c9bd000000000160014dd619252c3983779e6938366ece5d4e2015172b3024730440220180472b948c4fc4e2c608462375b41ff545ef2d0fe97d3ae8ac1e6ad25a6de1f0220036e23c181cb4b65264a58524215e97b27501d1039cd3a2f7d009f860aa8ed46012102a932128cbf3ddf90e12a4c94a6acce81ea3d1ea1dc325101f90b013ccecc60fe95000000 ./README.md bcrt1qagw463xxngygwsdjrm5f4etvr2hkq0yq0up8ly + +{ + "timestamp_tx_hex": "0200000001ca9e2c8baaf1f3d6a37f6f5e2017174fb31b7d70fd9ff4d295a97d79b2a2e46c0000000000ffffffff023027685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c800000000000000000226a203531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f00000000", + "file_hash": "3531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f" +} +``` + +6. Sign the transaction by passing the `timestamp_tx_hex` property of the +`create-timestamp-transaction` command. + +```shell +$ bitcoin-cli -rpcwallet=timestamp-w signrawtransactionwithwallet 0200000001ca9e2c8baaf1f3d6a37f6f5e2017174fb31b7d70fd9ff4d295a97d79b2a2e46c0000000000ffffffff023027685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c800000000000000000226a203531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f00000000 + +{ + "hex": "02000000000101ca9e2c8baaf1f3d6a37f6f5e2017174fb31b7d70fd9ff4d295a97d79b2a2e46c0000000000ffffffff023027685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c800000000000000000226a203531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f0247304402206f272dcc7b94474dd6df3f0d4eafd5e96ef7e568ec391ca9e06b466cdb694677022039546f7fc09a955d2b71ba02b927a839b91865c72d04d811e2a0f2f0838030ef0121029928fe0c0b89122500dee8a6b29cdac54925770f0d484778ff2be878854e1c4a00000000", + "complete": true +} +``` + +7. Broadcast the transaction to Bitcoin + +```shell +$ bitcoin-cli -rpcwallet=timestamp-w sendrawtransaction 02000000000101ca9e2c8baaf1f3d6a37f6f5e2017174fb31b7d70fd9ff4d295a97d79b2a2e46c0000000000ffffffff023027685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c800000000000000000226a203531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f0247304402206f272dcc7b94474dd6df3f0d4eafd5e96ef7e568ec391ca9e06b466cdb694677022039546f7fc09a955d2b71ba02b927a839b91865c72d04d811e2a0f2f0838030ef0121029928fe0c0b89122500dee8a6b29cdac54925770f0d484778ff2be878854e1c4a00000000 + +25b65b31c6f4d2f46ebeb5fa4c9a6d1fa4de03813404718f9797269b79023122 +``` + +8. To verify whether the transactions has been included, you can query Bitcoin +and check the transaction's number of confirmations. + +```shell +$ bitcoin-cli -rpcwallet=timestamp-w gettransaction 25b65b31c6f4d2f46ebeb5fa4c9a6d1fa4de03813404718f9797269b79023122 + +{ + "amount": 0.00000000, + "fee": -0.00002000, + "confirmations": 4, + "blockhash": "46582163473053a7e5f8743a79675d9f9cc5cbf4890f28f1e7e816316b28ef69", + "blockheight": 201, + "blockindex": 2, + "blocktime": 1715821032, + "txid": "25b65b31c6f4d2f46ebeb5fa4c9a6d1fa4de03813404718f9797269b79023122", + "wtxid": "3b2ce841ef57b43bc5b59d772ccc6fcaf38ca2bce94dbb787a5791a4ee0765df", + "walletconflicts": [ + ], + "time": 1715820976, + "timereceived": 1715820976, + "bip125-replaceable": "no", + "details": [ + { + "address": "bcrt1qagw463xxngygwsdjrm5f4etvr2hkq0yq0up8ly", + "category": "send", + "amount": -14.99998000, + "label": "", + "vout": 0, + "fee": -0.00002000, + "abandoned": false + }, + { + "category": "send", + "amount": 0.00000000, + "vout": 1, + "fee": -0.00002000, + "abandoned": false + }, + { + "address": "bcrt1qagw463xxngygwsdjrm5f4etvr2hkq0yq0up8ly", + "parent_descs": [ + "wpkh(tpubD6NzVbkrYhZ4X4XfxiyNepqE9Uy48atSoXCcd68V1hNokNUFiVUF6AptTPPDcU2mnLumM3m5Jq3eLjaoNd2vQdDtNjhGyfU2HJVSBEPYdtD/84h/1h/0h/0/*)#fawa8eh6" + ], + "category": "receive", + "amount": 14.99998000, + "label": "", + "vout": 0, + "abandoned": false + } + ], + "hex": "02000000000101ca9e2c8baaf1f3d6a37f6f5e2017174fb31b7d70fd9ff4d295a97d79b2a2e46c0000000000ffffffff023027685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c800000000000000000226a203531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f0247304402206f272dcc7b94474dd6df3f0d4eafd5e96ef7e568ec391ca9e06b466cdb694677022039546f7fc09a955d2b71ba02b927a839b91865c72d04d811e2a0f2f0838030ef0121029928fe0c0b89122500dee8a6b29cdac54925770f0d484778ff2be878854e1c4a00000000", + "lastprocessedblock": { + "hash": "27bc75d8210c69b72f0304aecb8c72c109691b8f94dd7d022ae5fc9026fb4008", + "height": 204 + } +} +``` diff --git a/itest/bitcoind_node_setup.go b/itest/bitcoind_node_setup.go index 0fe5ea2..a2f9e4b 100644 --- a/itest/bitcoind_node_setup.go +++ b/itest/bitcoind_node_setup.go @@ -10,6 +10,10 @@ import ( "time" "github.com/babylonchain/cli-tools/itest/containers" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/stretchr/testify/require" ) @@ -92,7 +96,7 @@ func (h *BitcoindTestHandler) CreateWallet(walletName string, passphrase string) } func (h *BitcoindTestHandler) GenerateBlocks(count int) *GenerateBlockResponse { - buff, _, err := h.m.ExecBitcoindCliCmd(h.t, []string{"-generate", fmt.Sprintf("%d", count)}) + buff, _, err := h.m.ExecBitcoindCliCmd(h.t, []string{fmt.Sprintf("-rpcwallet=%s", "test-wallet"), "-generate", fmt.Sprintf("%d", count)}) require.NoError(h.t, err) var response GenerateBlockResponse @@ -101,3 +105,113 @@ func (h *BitcoindTestHandler) GenerateBlocks(count int) *GenerateBlockResponse { return &response } + +func (h *BitcoindTestHandler) GetNewAddress(rpcWallet string) btcutil.Address { + buff, _, err := h.m.ExecBitcoindCliCmd(h.t, []string{fmt.Sprintf("-rpcwallet=%s", rpcWallet), "getnewaddress"}) + require.NoError(h.t, err) + + trimAddr := strings.TrimSpace(buff.String()) + + newAddr, err := btcutil.DecodeAddress(trimAddr, &chaincfg.RegressionNetParams) + require.NoError(h.t, err) + + return newAddr +} + +func (h *BitcoindTestHandler) ListUnspent(rpcWallet string) string { + buff, _, err := h.m.ExecBitcoindCliCmd(h.t, []string{fmt.Sprintf("-rpcwallet=%s", rpcWallet), "listunspent"}) + require.NoError(h.t, err) + + unspentTrim := strings.TrimSpace(buff.String()) + return unspentTrim +} + +func (h *BitcoindTestHandler) GetAddressInfo(rpcWallet, address string) btcjson.GetAddressInfoResult { + cmd := []string{fmt.Sprintf("-rpcwallet=%s", rpcWallet), "getaddressinfo", address} + buff, _, err := h.m.ExecBitcoindCliCmd(h.t, cmd) + require.NoError(h.t, err) + + var result btcjson.GetAddressInfoResult + err = json.Unmarshal(buff.Bytes(), &result) + require.NoError(h.t, err) + + return result +} + +func (h *BitcoindTestHandler) GetTransaction(txID string) btcjson.GetTransactionResult { + cmd := []string{"gettransaction", txID} + buff, _, err := h.m.ExecBitcoindCliCmd(h.t, cmd) + require.NoError(h.t, err) + + var result btcjson.GetTransactionResult + err = json.Unmarshal(buff.Bytes(), &result) + require.NoError(h.t, err) + + return result +} + +func (h *BitcoindTestHandler) FundRawTx(rpcWallet, rawTxHex string) btcjson.FundRawTransactionResult { + opt := btcjson.FundRawTransactionOpts{ + FeeRate: btcjson.Float64(0.005), + } + optBz, err := json.Marshal(opt) + require.NoError(h.t, err) + + cmd := []string{fmt.Sprintf("-rpcwallet=%s", rpcWallet), "fundrawtransaction", rawTxHex, string(optBz)} + buff, _, err := h.m.ExecBitcoindCliCmd(h.t, cmd) + require.NoError(h.t, err) + + var result btcjson.FundRawTransactionResult + err = result.UnmarshalJSON(buff.Bytes()) + require.NoError(h.t, err) + + return result +} + +func (h *BitcoindTestHandler) SignRawTxWithWallet(rpcWallet, fundedRawTxHex string) btcjson.SignRawTransactionWithWalletResult { + cmd := []string{fmt.Sprintf("-rpcwallet=%s", rpcWallet), "signrawtransactionwithwallet", fundedRawTxHex} + buff, _, err := h.m.ExecBitcoindCliCmd(h.t, cmd) + require.NoError(h.t, err) + + var result btcjson.SignRawTransactionWithWalletResult + err = json.Unmarshal(buff.Bytes(), &result) + require.NoError(h.t, err) + + return result +} + +func (h *BitcoindTestHandler) WalletPassphrase(rpcWallet, passphrase, timeoutSec string) string { + cmd := []string{fmt.Sprintf("-rpcwallet=%s", rpcWallet), "walletpassphrase", passphrase, timeoutSec} + buff, _, err := h.m.ExecBitcoindCliCmd(h.t, cmd) + require.NoError(h.t, err) + return buff.String() +} + +func (h *BitcoindTestHandler) SendRawTx(rpcWallet, fundedSignedTxHex string) string { + cmd := []string{fmt.Sprintf("-rpcwallet=%s", rpcWallet), "sendrawtransaction", fundedSignedTxHex} + buff, _, err := h.m.ExecBitcoindCliCmd(h.t, cmd) + require.NoError(h.t, err) + return buff.String() +} + +func (h *BitcoindTestHandler) SendToAddress(rpcWallet, address, amount string) *chainhash.Hash { + buff, errBuf, err := h.m.ExecBitcoindCliCmd(h.t, []string{ + fmt.Sprintf("-rpcwallet=%s", rpcWallet), "-named", + "sendtoaddress", + fmt.Sprintf("address=%s", address), + fmt.Sprintf("amount=%s", amount), + "fee_rate=5", + }) + require.NoError(h.t, err, "errBuf", errBuf) + + trimTxHash := strings.TrimSpace(buff.String()) + txHash, err := chainhash.NewHashFromStr(trimTxHash) + require.NoError(h.t, err) + + return txHash +} + +func (h *BitcoindTestHandler) LoadWallet(walletName string) { + _, _, err := h.m.ExecBitcoindCliCmd(h.t, []string{"loadwallet", walletName}) + require.NoError(h.t, err) +} diff --git a/itest/containers/containers.go b/itest/containers/containers.go index ba0cded..a220ca5 100644 --- a/itest/containers/containers.go +++ b/itest/containers/containers.go @@ -63,7 +63,7 @@ func (m *Manager) ExecCmd(t *testing.T, containerName string, command []string) errBuf bytes.Buffer ) - timeout := 20 * time.Second + timeout := 5 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 0d4a713..74f38f0 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -9,6 +9,8 @@ import ( "errors" "fmt" "math" + "os" + "path/filepath" "testing" "time" @@ -17,16 +19,19 @@ import ( signercfg "github.com/babylonchain/covenant-signer/config" "github.com/babylonchain/covenant-signer/signerapp" "github.com/babylonchain/covenant-signer/signerservice" + "github.com/babylonchain/covenant-signer/utils" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" + "github.com/babylonchain/cli-tools/cmd" "github.com/babylonchain/cli-tools/internal/btcclient" "github.com/babylonchain/cli-tools/internal/config" "github.com/babylonchain/cli-tools/internal/db" @@ -37,7 +42,8 @@ import ( ) const ( - passphrase = "pass" + passphrase = "pass" + FundWalletName = "test-wallet" ) var ( @@ -104,7 +110,9 @@ func PurgeAllCollections(ctx context.Context, client *mongo.Client, databaseName func StartManager( t *testing.T, - numMatureOutputsInWallet uint32) *TestManager { + numMatureOutputsInWallet uint32, + runMongodb bool, +) *TestManager { logger := logger.DefaultLogger() m, err := containers.NewManager() require.NoError(t, err) @@ -115,18 +123,22 @@ func StartManager( h := NewBitcoindHandler(t, m) h.Start() - _, err = m.RunMongoDbResource() - require.NoError(t, err) + appConfig := config.DefaultConfig() + + if runMongodb { + _, err = m.RunMongoDbResource() + require.NoError(t, err) + + appConfig.Db.Address = fmt.Sprintf("mongodb://%s", m.MongoHost()) + } // Give some time to launch mongo and bitcoind time.Sleep(2 * time.Second) - _ = h.CreateWallet("test-wallet", passphrase) + _ = h.CreateWallet(FundWalletName, passphrase) // only outputs which are 100 deep are mature _ = h.GenerateBlocks(int(numMatureOutputsInWallet) + 100) - appConfig := config.DefaultConfig() - appConfig.Btc.Host = "127.0.0.1:18443" appConfig.Btc.User = "user" appConfig.Btc.Pass = "pass" @@ -136,7 +148,6 @@ func StartManager( signerCfg, signerGlobalParams, signingServer := startSigningServer(t, magicBytes) appConfig.Signer = *signerCfg - appConfig.Db.Address = fmt.Sprintf("mongodb://%s", m.MongoHost()) var gp = services.ParsedGlobalParams{} @@ -171,11 +182,6 @@ func StartManager( fpKey, err := btcec.NewPrivateKey() require.NoError(t, err) - testDbConnection, err := db.New(context.TODO(), appConfig.Db.DbName, appConfig.Db.Address) - require.NoError(t, err) - - storeController := services.NewPersistentUnbondingStorage(testDbConnection) - pipeLine, err := services.NewUnbondingPipelineFromConfig( logger, appConfig, @@ -183,7 +189,7 @@ func StartManager( ) require.NoError(t, err) - return &TestManager{ + tm := &TestManager{ t: t, bitcoindHandler: h, walletPass: passphrase, @@ -197,10 +203,20 @@ func StartManager( magicBytes: []byte{0x0, 0x1, 0x2, 0x3}, pipeLineConfig: appConfig, pipeLine: pipeLine, - testStoreController: storeController, + testStoreController: nil, signingServer: signingServer, parameters: &gp, } + + if runMongodb { + testDbConnection, err := db.New(context.TODO(), appConfig.Db.DbName, appConfig.Db.Address) + require.NoError(t, err) + + storeController := services.NewPersistentUnbondingStorage(testDbConnection) + tm.testStoreController = storeController + } + + return tm } func startSigningServer( @@ -446,8 +462,98 @@ func (tm *TestManager) createNUnbondingTransactions(n int, d *stakingData) ([]*u return unbondingTxs, sendStakingTransactions } +func TestBtcTimestamp(t *testing.T) { + tm := StartManager(t, 10, false) + btcd := tm.bitcoindHandler + + wName := "btc-file-timestamping" + resp := btcd.CreateWallet(wName, passphrase) + require.Equal(t, wName, resp.Name) + + // generate new address + newAddr := btcd.GetNewAddress(wName) + require.NotEmpty(t, newAddr) + + // fund the new addr + fundingTxHash := btcd.SendToAddress(FundWalletName, newAddr.String(), "25") + btcd.GenerateBlocks(5) + + newAddrPkScript, err := txscript.PayToAddrScript(newAddr) + require.NoError(t, err) + + tx, conf, err := tm.btcClient.TxDetails(fundingTxHash, newAddrPkScript) + require.NoError(t, err) + require.NotNil(t, tx) + require.Equal(t, btcclient.TxInChain, conf) + + fundinTxSerialized, err := cmd.SerializeBTCTxToHex(tx.Tx) + require.NoError(t, err) + + btcd.WalletPassphrase(wName, passphrase, "70") + + // timestamp the go.mod + currentPath, err := os.Getwd() + require.NoError(t, err) + modFilePath := filepath.Join(currentPath, "../go.mod") + + timestampFileOutput, err := cmd.CreateTimestampTx( + fundinTxSerialized, + modFilePath, + newAddr.EncodeAddress(), + 3000, + netParams, + ) + require.NoError(t, err) + require.NotNil(t, timestampFileOutput) + + signedTimestampTx := btcd.SignRawTxWithWallet(wName, timestampFileOutput.TimestampTx) + + stx, _, err := utils.NewBTCTxFromHex(signedTimestampTx.Hex) + require.NoError(t, err) + require.NotNil(t, signedTimestampTx) + + stxHash := stx.TxHash() + + btcd.SendRawTx(wName, signedTimestampTx.Hex) + btcd.GenerateBlocks(5) + + stxConfirmation, stxState, err := tm.btcClient.TxDetails(&stxHash, newAddrPkScript) + require.NoError(t, err) + require.Equal(t, btcclient.TxInChain, stxState) + require.NotNil(t, stxConfirmation) + + // check timestamp from funded timestamp tx + fundedFromTxTimestamp, err := cmd.SerializeBTCTxToHex(stx) + require.NoError(t, err) + + timestampOutputFromFundedTimestamp, err := cmd.CreateTimestampTx( + fundedFromTxTimestamp, + modFilePath, + newAddr.EncodeAddress(), + 3000, + netParams, + ) + require.NoError(t, err) + require.NotNil(t, timestampOutputFromFundedTimestamp) + + signTxFromTimest := btcd.SignRawTxWithWallet(wName, timestampOutputFromFundedTimestamp.TimestampTx) + + btcd.SendRawTx(wName, signTxFromTimest.Hex) + btcd.GenerateBlocks(5) + + timestampTxFromTimestampTx, _, err := utils.NewBTCTxFromHex(timestampOutputFromFundedTimestamp.TimestampTx) + require.NoError(t, err) + + timestampTxHash := timestampTxFromTimestampTx.TxHash() + + stxConfirmation, stxState, err = tm.btcClient.TxDetails(×tampTxHash, newAddrPkScript) + require.NoError(t, err) + require.Equal(t, btcclient.TxInChain, stxState) + require.NotNil(t, stxConfirmation) +} + func TestSendingFreshTransactions(t *testing.T) { - m := StartManager(t, 10) + m := StartManager(t, 10, true) d := defaultStakingData() numUnbondingTxs := 10 @@ -520,7 +626,7 @@ func (tm *TestManager) updateSchnorSigInDb(newSig *schnorr.Signature, txHash *ch } func TestHandlingCriticalError(t *testing.T) { - m := StartManager(t, 10) + m := StartManager(t, 10, true) d := defaultStakingData() unb, stk := m.createNUnbondingTransactions(1, d)