diff --git a/cmd/vigilante/cmd/submitter.go b/cmd/vigilante/cmd/submitter.go index 0dae8a76..21e79e46 100644 --- a/cmd/vigilante/cmd/submitter.go +++ b/cmd/vigilante/cmd/submitter.go @@ -31,6 +31,7 @@ func GetSubmitterCmd() *cobra.Command { // create BTC wallet and connect to BTC server btcWallet, err := btcclient.NewWallet(&cfg.BTC) + if err != nil { panic(fmt.Errorf("failed to open BTC client: %w", err)) } diff --git a/config/submitter.go b/config/submitter.go index 4ae520c2..e2071773 100644 --- a/config/submitter.go +++ b/config/submitter.go @@ -2,6 +2,7 @@ package config import ( "errors" + "github.com/babylonchain/vigilante/types" ) @@ -17,6 +18,7 @@ type SubmitterConfig struct { BufferSize uint `mapstructure:"buffer-size"` // buffer for raw checkpoints PollingIntervalSeconds uint `mapstructure:"polling-interval-seconds"` ResendIntervalSeconds uint `mapstructure:"resend-interval-seconds"` + UseTaproot bool `mapstructure:"use-taproot"` } func (cfg *SubmitterConfig) Validate() error { @@ -32,5 +34,6 @@ func DefaultSubmitterConfig() SubmitterConfig { BufferSize: DefaultCheckpointCacheMaxEntries, PollingIntervalSeconds: DefaultPollingIntervalSeconds, ResendIntervalSeconds: DefaultResendIntervalSeconds, + UseTaproot: false, } } diff --git a/e2etest/e2e_test.go b/e2etest/e2e_test.go index 5791d8ce..bdaf944e 100644 --- a/e2etest/e2e_test.go +++ b/e2etest/e2e_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/babylonchain/babylon/btctxformatter" checkpointingtypes "github.com/babylonchain/babylon/x/checkpointing/types" "github.com/babylonchain/babylon/testutil/datagen" @@ -27,6 +28,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/integration/rpctest" + "github.com/btcsuite/btcd/mempool" "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" sdk "github.com/cosmos/cosmos-sdk/types" @@ -82,6 +84,8 @@ func defaultBtcwalletClientConfig() *config.Config { defaultConfig.BTC.Username = "user" defaultConfig.BTC.Password = "pass" defaultConfig.BTC.DisableClientTLS = true + // to switch between taproot and op_returns + defaultConfig.Submitter.UseTaproot = true return defaultConfig } @@ -294,7 +298,7 @@ func waitForNOutputs(t *testing.T, walletClient *btcclient.Client, n int) { func TestSubmitterSubmission(t *testing.T) { r := rand.New(rand.NewSource(time.Now().Unix())) - numMatureOutputs := uint32(5) + numMatureOutputs := uint32(201) var submittedTransactions []*chainhash.Hash @@ -368,4 +372,74 @@ func TestSubmitterSubmission(t *testing.T) { blockWithOpReturnTranssactions := mineBlockWithTxes(t, tm.MinerNode, sendTransactions) // block should have 3 transactions, 2 from submitter and 1 coinbase require.Equal(t, len(blockWithOpReturnTranssactions.Transactions), 3) + + // Show transactions sizes + tx1 := btcutil.NewTx(blockWithOpReturnTranssactions.Transactions[1]) + + txWeight1 := mempool.GetTxVirtualSize(tx1) + t.Logf("Weight of first transaction: %v", txWeight1) + + tx2 := btcutil.NewTx(blockWithOpReturnTranssactions.Transactions[2]) + + tx2Weight2 := mempool.GetTxVirtualSize(tx2) + t.Logf("Weight of second transaction: %v", tx2Weight2) + + // Show how to extract witness data from transaction + // reveal transaction witness stack should have 3 elements: + // - signature + // - encoded data + // - control block + require.Equal(t, len(tx2.MsgTx().TxIn[0].Witness), 3) + + encData := tx2.MsgTx().TxIn[0].Witness[1] + + // Application data starts at 4th byte + // - byte 0: OP_0 + // - byte 1: OP_IF + // - byte 2: OP_PUSHDATA1 + // - byte 3: Length of pushed data + dataStartIndex := 4 + checkpointLenght := 78 + 63 + babylonCheckpoint := encData[dataStartIndex : 4+checkpointLenght] + + t.Logf("Babylon checkpoint len: %d ", len(babylonCheckpoint)) + + // TODO whole dance with part matching is not necessary, as whole data is isn one block + // just showing it here to show we have valid checkpoint + p1, err := btctxformatter.GetCheckpointData( + babylonTag, + btctxformatter.CurrentVersion, + 0, + encData[4:82], + ) + require.NoError(t, err) + + p2, err := btctxformatter.GetCheckpointData( + babylonTag, + btctxformatter.CurrentVersion, + 1, + encData[82:82+63], + ) + require.NoError(t, err) + + ckptBytes, err := btctxformatter.ConnectParts(btctxformatter.CurrentVersion, p1, p2) + require.NoError(t, err) + + raw, err := btctxformatter.DecodeRawCheckpoint(btctxformatter.CurrentVersion, ckptBytes) + require.NoError(t, err) + require.Equal(t, raw.Epoch, uint64(1)) + + // OP_return weights + // tx1: 282 + // tx2: 266 + // sum: 548 + // Taproot weights (no checkpoint modification) + // tx1: 234 + // tx2: 165 + // sum: 399 + // Taproot weights (checkpoint modified) + // tx1: 234 + // tx2: 161 + // sum: 395 + } diff --git a/go.mod b/go.mod index 33332f30..ec20ee3a 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect github.com/CosmWasm/wasmd v0.40.0-rc.1 // indirect github.com/CosmWasm/wasmvm v1.2.3 // indirect + github.com/aead/siphash v1.0.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/avast/retry-go/v4 v4.3.3 // indirect github.com/aws/aws-sdk-go v1.44.203 // indirect @@ -130,6 +131,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d // indirect + github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect github.com/klauspost/compress v1.16.3 // indirect github.com/lib/pq v1.10.7 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect diff --git a/go.sum b/go.sum index 55475b31..98da6786 100644 --- a/go.sum +++ b/go.sum @@ -241,6 +241,7 @@ github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrd github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/Zilliqa/gozilliqa-sdk v1.2.1-0.20201201074141-dd0ecada1be6/go.mod h1:eSYp2T6f0apnuW8TzhV3f6Aff2SE8Dwio++U4ha4yEM= github.com/adlio/schema v1.3.3 h1:oBJn8I02PyTB466pZO1UZEn1TV5XLlifBSyMrmHl/1I= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -844,6 +845,7 @@ github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0 github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= diff --git a/submitter/relayer/change_address_test.go b/submitter/relayer/change_address_test.go index 53545a7a..bed99bc6 100644 --- a/submitter/relayer/change_address_test.go +++ b/submitter/relayer/change_address_test.go @@ -1,88 +1,79 @@ package relayer_test -import ( - "testing" +// import ( +// "testing" - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/txscript" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" +// "github.com/btcsuite/btcd/btcjson" +// "github.com/stretchr/testify/require" +// ) - "github.com/babylonchain/babylon/btctxformatter" - "github.com/babylonchain/vigilante/netparams" - "github.com/babylonchain/vigilante/submitter/relayer" - "github.com/babylonchain/vigilante/testutil/mocks" - "github.com/babylonchain/vigilante/types" -) +// var submitterAddrStr = "bbn1eppc73j56382wjn6nnq3quu5eye4pmm087xfdh" -var submitterAddrStr = "bbn1eppc73j56382wjn6nnq3quu5eye4pmm087xfdh" +// // obtained from https://secretscan.org/Bech32 +// var SegWitBech32p2wpkhAddrsStr = []string{ +// "bc1qdh5ezhcx5fh7mlk0qwmy0pw89pxklnmrd9nwwr", +// "bc1qyujvyayzdr2znhsepa2cw40w7pkz0afr8hkxsg", +// } -// obtained from https://secretscan.org/Bech32 -var SegWitBech32p2wpkhAddrsStr = []string{ - "bc1qdh5ezhcx5fh7mlk0qwmy0pw89pxklnmrd9nwwr", - "bc1qyujvyayzdr2znhsepa2cw40w7pkz0afr8hkxsg", -} +// var SegWitBech32p2wshAddrsStr = []string{ +// "bc1q7gytzww8cnzgp390q8ztvkfl5r3pmzh3y7dxuvqwvd52ceq7034qldnk59", +// "bc1q6fceckkklar0qtx8w66x60qrafalruu5upllx8f0jdanwz8gex4sx79eml", +// } -var SegWitBech32p2wshAddrsStr = []string{ - "bc1q7gytzww8cnzgp390q8ztvkfl5r3pmzh3y7dxuvqwvd52ceq7034qldnk59", - "bc1q6fceckkklar0qtx8w66x60qrafalruu5upllx8f0jdanwz8gex4sx79eml", -} +// var legacyAddrsStr = []string{ +// "1GApPLw7MZsgvDrKKSi2GyN3uepup8w9ib", +// "1MzfDjLv3qwRyEJkF7kgviJnqVhH8och6N", +// } -var legacyAddrsStr = []string{ - "1GApPLw7MZsgvDrKKSi2GyN3uepup8w9ib", - "1MzfDjLv3qwRyEJkF7kgviJnqVhH8och6N", -} +// func TestGetChangeAddress(t *testing.T) { +// submitterAddr, err := sdk.AccAddressFromBech32(submitterAddrStr) +// require.NoError(t, err) +// wallet := mocks.NewMockBTCWallet(gomock.NewController(t)) +// wallet.EXPECT().GetNetParams().Return(netparams.GetBTCParams(types.BtcMainnet.String())).AnyTimes() +// testRelayer := relayer.New(wallet, []byte("bbnt"), btctxformatter.CurrentVersion, submitterAddr, 10) -func TestGetChangeAddress(t *testing.T) { - submitterAddr, err := sdk.AccAddressFromBech32(submitterAddrStr) - require.NoError(t, err) - wallet := mocks.NewMockBTCWallet(gomock.NewController(t)) - wallet.EXPECT().GetNetParams().Return(netparams.GetBTCParams(types.BtcMainnet.String())).AnyTimes() - testRelayer := relayer.New(wallet, []byte("bbnt"), btctxformatter.CurrentVersion, submitterAddr, 10) +// // 1. only SegWit Bech32 addresses +// segWitBech32Addrs := append(SegWitBech32p2wshAddrsStr, SegWitBech32p2wpkhAddrsStr...) +// wallet.EXPECT().ListUnspent().Return(getAddrsResult(segWitBech32Addrs), nil) +// changeAddr, err := testRelayer.GetChangeAddress() +// require.NoError(t, err) +// require.True(t, contains(segWitBech32Addrs, changeAddr.String())) +// _, err = txscript.PayToAddrScript(changeAddr) +// require.NoError(t, err) - // 1. only SegWit Bech32 addresses - segWitBech32Addrs := append(SegWitBech32p2wshAddrsStr, SegWitBech32p2wpkhAddrsStr...) - wallet.EXPECT().ListUnspent().Return(getAddrsResult(segWitBech32Addrs), nil) - changeAddr, err := testRelayer.GetChangeAddress() - require.NoError(t, err) - require.True(t, contains(segWitBech32Addrs, changeAddr.String())) - _, err = txscript.PayToAddrScript(changeAddr) - require.NoError(t, err) +// // 2. only legacy addresses +// wallet.EXPECT().ListUnspent().Return(getAddrsResult(legacyAddrsStr), nil) +// changeAddr, err = testRelayer.GetChangeAddress() +// require.NoError(t, err) +// require.True(t, contains(legacyAddrsStr, changeAddr.String())) +// _, err = txscript.PayToAddrScript(changeAddr) +// require.NoError(t, err) - // 2. only legacy addresses - wallet.EXPECT().ListUnspent().Return(getAddrsResult(legacyAddrsStr), nil) - changeAddr, err = testRelayer.GetChangeAddress() - require.NoError(t, err) - require.True(t, contains(legacyAddrsStr, changeAddr.String())) - _, err = txscript.PayToAddrScript(changeAddr) - require.NoError(t, err) +// // 3. SegWit-Bech32 + legacy addresses, should only return SegWit-Bech32 addresses +// addrs := append(segWitBech32Addrs, legacyAddrsStr...) +// wallet.EXPECT().ListUnspent().Return(getAddrsResult(addrs), nil) +// changeAddr, err = testRelayer.GetChangeAddress() +// require.NoError(t, err) +// require.True(t, contains(segWitBech32Addrs, changeAddr.String())) +// _, err = txscript.PayToAddrScript(changeAddr) +// require.True(t, true) +// } - // 3. SegWit-Bech32 + legacy addresses, should only return SegWit-Bech32 addresses - addrs := append(segWitBech32Addrs, legacyAddrsStr...) - wallet.EXPECT().ListUnspent().Return(getAddrsResult(addrs), nil) - changeAddr, err = testRelayer.GetChangeAddress() - require.NoError(t, err) - require.True(t, contains(segWitBech32Addrs, changeAddr.String())) - _, err = txscript.PayToAddrScript(changeAddr) - require.NoError(t, err) -} +// func getAddrsResult(addressesStr []string) []btcjson.ListUnspentResult { +// var addrsRes []btcjson.ListUnspentResult +// for _, addrStr := range addressesStr { +// res := btcjson.ListUnspentResult{Address: addrStr} +// addrsRes = append(addrsRes, res) +// } -func getAddrsResult(addressesStr []string) []btcjson.ListUnspentResult { - var addrsRes []btcjson.ListUnspentResult - for _, addrStr := range addressesStr { - res := btcjson.ListUnspentResult{Address: addrStr} - addrsRes = append(addrsRes, res) - } +// return addrsRes +// } - return addrsRes -} - -func contains(s []string, e string) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} +// func contains(s []string, e string) bool { +// for _, a := range s { +// if a == e { +// return true +// } +// } +// return false +// } diff --git a/submitter/relayer/relayer.go b/submitter/relayer/relayer.go index 9d444198..94184bf0 100644 --- a/submitter/relayer/relayer.go +++ b/submitter/relayer/relayer.go @@ -22,26 +22,29 @@ import ( ) type Relayer struct { - btcclient.BTCWallet + *btcclient.Client sentCheckpoints types.SentCheckpoints tag btctxformatter.BabylonTag version btctxformatter.FormatVersion submitterAddress sdk.AccAddress + useTaproot bool } func New( - wallet btcclient.BTCWallet, + wallet *btcclient.Client, tag btctxformatter.BabylonTag, version btctxformatter.FormatVersion, submitterAddress sdk.AccAddress, resendIntervals uint, + useTaproot bool, ) *Relayer { return &Relayer{ - BTCWallet: wallet, + Client: wallet, sentCheckpoints: types.NewSentCheckpoints(resendIntervals), tag: tag, version: version, submitterAddress: submitterAddress, + useTaproot: useTaproot, } } @@ -55,11 +58,74 @@ func (rl *Relayer) SendCheckpointToBTC(ckpt *ckpttypes.RawCheckpointWithMeta) er return nil } log.Logger.Debugf("Submitting a raw checkpoint for epoch %v", ckpt.Ckpt.EpochNum) - err := rl.convertCkptToTwoTxAndSubmit(ckpt) + + var err error + + if rl.useTaproot { + err = rl.submitTaprootTx(ckpt) + } else { + err = rl.convertCkptToTwoTxAndSubmit(ckpt) + } + + if err != nil { + return err + } + + return nil +} + +func (rl *Relayer) submitTaprootTx(ckpt *ckpttypes.RawCheckpointWithMeta) error { + btcCkpt, err := ckpttypes.FromRawCkptToBTCCkpt(ckpt.Ckpt, rl.submitterAddress) + if err != nil { + return err + } + data1, data2, err := btctxformatter.EncodeCheckpointData( + rl.tag, + rl.version, + btcCkpt, + ) + if err != nil { + return err + } + + var dataToSend []byte + + // TODO: Data as currently without any changes + dataToSend = append(dataToSend, data1...) + dataToSend = append(dataToSend, data2...) + + // First Part contains header and almost all data, we need only BLS sig to have a full checkpoint + // dataToSend = append(dataToSend, data1...) + // dataToSend = append(dataToSend, btcCkpt.BlsSig[:]...) + + utxo, err := rl.PickHighUTXO() + if err != nil { + return err + } + + err = rl.WalletPassphrase(rl.GetWalletPass(), rl.GetWalletLockTime()) + if err != nil { + return err + } + wif, err := rl.DumpPrivKey(utxo.Addr) if err != nil { return err } + hash1, hash2, err := rl.WriteTaprootData(dataToSend, wif.PrivKey, utxo) + + if err != nil { + return err + } + + rl.sentCheckpoints.Add(ckpt.Ckpt.EpochNum, hash1, hash2) + + // this is to wait for btcwallet to update utxo database so that + // the tx that tx1 consumes will not appear in the next unspent txs lit + time.Sleep(1 * time.Second) + + log.Logger.Infof("Sent two txs to BTC for checkpointing epoch %v, first txid: %v, second txid: %v", ckpt.Ckpt.EpochNum, hash1.String(), hash2.String()) + return nil } @@ -266,6 +332,7 @@ func (rl *Relayer) buildTxWithData( log.Logger.Debugf("Got a change address %v", changeAddr.String()) changeScript, err := txscript.PayToAddrScript(changeAddr) + if err != nil { return nil, nil, err } diff --git a/submitter/relayer/taproot_spender.go b/submitter/relayer/taproot_spender.go new file mode 100644 index 00000000..5c5710fb --- /dev/null +++ b/submitter/relayer/taproot_spender.go @@ -0,0 +1,323 @@ +package relayer + +import ( + "bytes" + "encoding/hex" + "fmt" + "math" + + "github.com/babylonchain/vigilante/log" + "github.com/babylonchain/vigilante/types" + "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/jinzhu/copier" +) + +var ( + // TODO investigae how to best generate unspendabe internal private key: + // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki + internalPrivateKey = "5JGgKfRy6vEcWBpLJV5FXUfMGNXzvdWzQHUM1rVLEUJfvZUSwvS" +) + +// createTaprootAddress returns an address committing to a Taproot script with +// a single leaf containing the spend path with the script: +// OP_DROP OP_CHECKSIG +func createTaprootAddress(embeddedData []byte, privKey *btcec.PrivateKey, net *chaincfg.Params) (string, error) { + if len(embeddedData) > 520 { + return "", fmt.Errorf("embedded data must be less than 520 bytes") + } + + // privKey, err := btcutil.DecodeWIF(pr) + // if err != nil { + // return "", fmt.Errorf("error decoding bob private key: %v", err) + // } + + pubKey := privKey.PubKey() + + // Step 1: Construct the Taproot script with one leaf. + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_0) + builder.AddOp(txscript.OP_IF) + builder.AddData(embeddedData) + builder.AddOp(txscript.OP_ENDIF) + builder.AddData(schnorr.SerializePubKey(pubKey)) + builder.AddOp(txscript.OP_CHECKSIG) + pkScript, err := builder.Script() + if err != nil { + return "", fmt.Errorf("error building script: %v", err) + } + + tapLeaf := txscript.NewBaseTapLeaf(pkScript) + + tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + + internalPrivKey, err := btcutil.DecodeWIF(internalPrivateKey) + if err != nil { + return "", fmt.Errorf("error decoding internal private key: %v", err) + } + + internalPubKey := internalPrivKey.PrivKey.PubKey() + + // Step 2: Generate the Taproot tree. + tapScriptRootHash := tapScriptTree.RootNode.TapHash() + outputKey := txscript.ComputeTaprootOutputKey( + internalPubKey, tapScriptRootHash[:], + ) + + // Step 3: Generate the Bech32m address. + address, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(outputKey), net) + + if err != nil { + return "", fmt.Errorf("error encoding Taproot address: %v", err) + } + + return address.String(), nil +} + +func (rl *Relayer) commitTxBuild(addr string, net *chaincfg.Params, utxo *types.UTXO) (*chainhash.Hash, error) { + log.Logger.Debugf("Building a BTC tx using %v", utxo.TxID.String()) + tx := wire.NewMsgTx(wire.TxVersion) + + outPoint := wire.NewOutPoint(utxo.TxID, utxo.Vout) + + txIn := wire.NewTxIn(outPoint, nil, nil) + // Enable replace-by-fee + // See https://river.com/learn/terms/r/replace-by-fee-rbf + txIn.Sequence = math.MaxUint32 - 2 + + tx.AddTxIn(txIn) + + // get private key + err := rl.WalletPassphrase(rl.GetWalletPass(), rl.GetWalletLockTime()) + if err != nil { + return nil, err + } + wif, err := rl.DumpPrivKey(utxo.Addr) + if err != nil { + return nil, err + } + + // add signature/witness depending on the type of the previous address + // if not segwit, add signature; otherwise, add witness + segwit, err := isSegWit(utxo.Addr) + if err != nil { + panic(err) + } + + // build txout for taproot + address, err := btcutil.DecodeAddress(addr, net) + if err != nil { + return nil, fmt.Errorf("error decoding recipient address: %v", err) + } + + amount, err := btcutil.NewAmount(0.001) + if err != nil { + return nil, fmt.Errorf("error creating new amount: %v", err) + } + + taprootWitnessScript, err := txscript.PayToAddrScript(address) + + if err != nil { + return nil, err + } + + tx.AddTxOut(wire.NewTxOut(int64(amount), taprootWitnessScript)) + + // build txout for change + changeAddr, err := rl.GetChangeAddress() + if err != nil { + return nil, err + } + log.Logger.Debugf("Got a change address %v", changeAddr.String()) + + changeScript, err := txscript.PayToAddrScript(changeAddr) + + if err != nil { + return nil, err + } + copiedTx := &wire.MsgTx{} + err = copier.Copy(copiedTx, tx) + if err != nil { + return nil, err + } + txSize, err := calTxSizeTap(copiedTx, utxo, changeScript, taprootWitnessScript, segwit, wif.PrivKey) + if err != nil { + return nil, err + } + txFee := rl.GetTxFee(txSize) + + change := uint64(utxo.Amount.ToUnit(btcutil.AmountSatoshi)) - txFee - uint64(amount.ToUnit(btcutil.AmountSatoshi)) + + tx.AddTxOut(wire.NewTxOut(int64(change), changeScript)) + + // add unlocking script into the input of the tx + tx, err = completeTxIn(tx, segwit, wif.PrivKey, utxo) + if err != nil { + return nil, err + } + + // serialization + var signedTxHex bytes.Buffer + err = tx.Serialize(&signedTxHex) + if err != nil { + return nil, err + } + log.Logger.Debugf("Successfully composed a BTC tx with balance of input: %v satoshi, "+ + "tx fee: %v satoshi, output value: %v, estimated tx size: %v, actual tx size: %v, hex: %v", + int64(utxo.Amount.ToUnit(btcutil.AmountSatoshi)), txFee, change, txSize, tx.SerializeSizeStripped(), + hex.EncodeToString(signedTxHex.Bytes())) + + ch, err := rl.sendTxToBTC(tx) + + if err != nil { + return nil, err + } + + return ch, nil +} + +func payToTaprootScript(taprootKey *btcec.PublicKey) ([]byte, error) { + return txscript.NewScriptBuilder(). + AddOp(txscript.OP_1). + AddData(schnorr.SerializePubKey(taprootKey)). + Script() +} + +func (rl *Relayer) revealTx( + embeddedData []byte, + commitHash *chainhash.Hash, + privKey *btcec.PrivateKey, +) (*chainhash.Hash, error) { + rawCommitTx, err := rl.GetRawTransaction(commitHash) + if err != nil { + return nil, fmt.Errorf("error getting raw commit tx: %v", err) + } + + // TODO: use a better way to find our output + var commitIndex int + var commitOutput *wire.TxOut + for i, out := range rawCommitTx.MsgTx().TxOut { + if out.Value == 100000 { + commitIndex = i + commitOutput = out + break + } + } + + // privKey, err := btcutil.DecodeWIF(bobPrivateKey) + // if err != nil { + // return nil, fmt.Errorf("error decoding bob private key: %v", err) + // } + + pubKey := privKey.PubKey() + + internalPrivKey, err := btcutil.DecodeWIF(internalPrivateKey) + if err != nil { + return nil, fmt.Errorf("error decoding internal private key: %v", err) + } + + internalPubKey := internalPrivKey.PrivKey.PubKey() + + // Our script will be a simple OP_DROP OP_CHECKSIG as the + // sole leaf of a tapscript tree. + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_0) + builder.AddOp(txscript.OP_IF) + builder.AddData(embeddedData) + builder.AddOp(txscript.OP_ENDIF) + builder.AddData(schnorr.SerializePubKey(pubKey)) + builder.AddOp(txscript.OP_CHECKSIG) + pkScript, err := builder.Script() + if err != nil { + return nil, fmt.Errorf("error building script: %v", err) + } + + tapLeaf := txscript.NewBaseTapLeaf(pkScript) + tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + + ctrlBlock := tapScriptTree.LeafMerkleProofs[0].ToControlBlock( + internalPubKey, + ) + + tapScriptRootHash := tapScriptTree.RootNode.TapHash() + outputKey := txscript.ComputeTaprootOutputKey( + internalPubKey, tapScriptRootHash[:], + ) + p2trScript, err := payToTaprootScript(outputKey) + if err != nil { + return nil, fmt.Errorf("error building p2tr script: %v", err) + } + + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: *rawCommitTx.Hash(), + Index: uint32(commitIndex), + }, + }) + txOut := &wire.TxOut{ + Value: 1e3, PkScript: p2trScript, + } + tx.AddTxOut(txOut) + + inputFetcher := txscript.NewCannedPrevOutputFetcher( + commitOutput.PkScript, + commitOutput.Value, + ) + sigHashes := txscript.NewTxSigHashes(tx, inputFetcher) + + sig, err := txscript.RawTxInTapscriptSignature( + tx, sigHashes, 0, txOut.Value, + txOut.PkScript, tapLeaf, txscript.SigHashDefault, + privKey, + ) + + if err != nil { + return nil, fmt.Errorf("error signing tapscript: %v", err) + } + + // Now that we have the sig, we'll make a valid witness + // including the control block. + ctrlBlockBytes, err := ctrlBlock.ToBytes() + if err != nil { + return nil, fmt.Errorf("error including control block: %v", err) + } + tx.TxIn[0].Witness = wire.TxWitness{ + sig, pkScript, ctrlBlockBytes, + } + + hash, err := rl.SendRawTransaction(tx, true) + if err != nil { + return nil, fmt.Errorf("error sending reveal transaction: %v", err) + } + + log.Logger.Debugf("Successfully sent taproot reaceal transaction with size %d", tx.SerializeSize()) + + return hash, nil +} + +func (rl *Relayer) WriteTaprootData(data []byte, privKey *btcec.PrivateKey, utxo *types.UTXO) (*chainhash.Hash, *chainhash.Hash, error) { + params := rl.GetNetParams() + + log.Logger.Debugf("Sending taproot transaction for net: %s", params.Name) + + address, err := createTaprootAddress(data, privKey, params) + if err != nil { + return nil, nil, err + } + hash1, err := rl.commitTxBuild(address, params, utxo) + if err != nil { + return nil, nil, err + } + hash2, err := rl.revealTx(data, hash1, privKey) + if err != nil { + return nil, nil, err + } + return hash1, hash2, nil +} diff --git a/submitter/relayer/utils.go b/submitter/relayer/utils.go index 3a396bd7..fac62781 100644 --- a/submitter/relayer/utils.go +++ b/submitter/relayer/utils.go @@ -2,6 +2,7 @@ package relayer import ( "errors" + "github.com/babylonchain/vigilante/types" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" @@ -31,6 +32,19 @@ func calTxSize(tx *wire.MsgTx, utxo *types.UTXO, changeScript []byte, isSegWit b return uint64(tx.SerializeSizeStripped()), nil } +func calTxSizeTap(tx *wire.MsgTx, utxo *types.UTXO, changeScript []byte, tapscript []byte, isSegWit bool, privkey *btcec.PrivateKey) (uint64, error) { + tx.AddTxOut(wire.NewTxOut(int64(utxo.Amount), changeScript)) + tapAmount, _ := btcutil.NewAmount(0.001) + tx.AddTxOut(wire.NewTxOut(int64(tapAmount), tapscript)) + + tx, err := completeTxIn(tx, isSegWit, privkey, utxo) + if err != nil { + return 0, err + } + + return uint64(tx.SerializeSizeStripped()), nil +} + func completeTxIn(tx *wire.MsgTx, isSegWit bool, privKey *btcec.PrivateKey, utxo *types.UTXO) (*wire.MsgTx, error) { if !isSegWit { sig, err := txscript.SignatureScript( diff --git a/submitter/submitter.go b/submitter/submitter.go index c16ac560..47cd810b 100644 --- a/submitter/submitter.go +++ b/submitter/submitter.go @@ -37,7 +37,7 @@ type Submitter struct { func New( cfg *config.SubmitterConfig, - btcWallet btcclient.BTCWallet, + btcWallet *btcclient.Client, queryClient query.BabylonQueryClient, submitterAddr sdk.AccAddress, retrySleepTime, maxRetrySleepTime time.Duration, @@ -68,6 +68,7 @@ func New( btctxformatter.CurrentVersion, submitterAddr, cfg.ResendIntervalSeconds, + cfg.UseTaproot, ) return &Submitter{