diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index acae5b6784b..acf3512af22 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -548,9 +548,10 @@ func (c *ChainArbitrator) Start() error { c.activeChannels[chanPoint] = channelArb // Republish any closing transactions for this channel. - err = c.publishClosingTxs(channel) + err = c.republishClosingTxs(channel) if err != nil { - return err + log.Errorf("Failed to republish closing txs for "+ + "channel %v", chanPoint) } } @@ -825,10 +826,10 @@ func (c *ChainArbitrator) dispatchBlocks( } } -// publishClosingTxs will load any stored cooperative or unilater closing +// republishClosingTxs will load any stored cooperative or unilateral closing // transactions and republish them. This helps ensure propagation of the // transactions in the event that prior publications failed. -func (c *ChainArbitrator) publishClosingTxs( +func (c *ChainArbitrator) republishClosingTxs( channel *channeldb.OpenChannel) error { // If the channel has had its unilateral close broadcasted already, diff --git a/docs/release-notes/release-notes-0.18.0.md b/docs/release-notes/release-notes-0.18.0.md index 785d49eb6ed..13e52baa769 100644 --- a/docs/release-notes/release-notes-0.18.0.md +++ b/docs/release-notes/release-notes-0.18.0.md @@ -115,6 +115,12 @@ without the user needing to publicly give away a lot of privacy-sensitive data. +* When publishing transactions in `lnd`, all the transactions will now go + through [mempool acceptance + check](https://github.com/lightningnetwork/lnd/pull/8345) before being + broadcast. This means when a transaction has failed the `testmempoolaccept` + check by bitcoind or btcd, the broadcast won't be attempted. + ## RPC Additions * [Deprecated](https://github.com/lightningnetwork/lnd/pull/7175) diff --git a/go.mod b/go.mod index 0054d179e90..1b0a9de8936 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/btcsuite/btcd/btcutil/psbt v1.1.8 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f - github.com/btcsuite/btcwallet v0.16.10-0.20231129183218-5df09dd43358 + github.com/btcsuite/btcwallet v0.16.10-0.20240122184004-f1648e92097f github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 github.com/btcsuite/btcwallet/walletdb v1.4.0 diff --git a/go.sum b/go.sum index 2e818f776f4..ea87b0c02a1 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtyd github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcwallet v0.16.10-0.20231129183218-5df09dd43358 h1:lZUSo6TISHUJQxpn/AniW5gqaN1iRNS87SDWvV3AHfg= -github.com/btcsuite/btcwallet v0.16.10-0.20231129183218-5df09dd43358/go.mod h1:WSKhOJWUmUOHKCKEzdt+jWAHFAE/t4RqVbCwL2pEdiU= +github.com/btcsuite/btcwallet v0.16.10-0.20240122184004-f1648e92097f h1:wtA3/5W+SAIQaWEuvnjMJqWEPjf2mWupXBB6KHbFKYA= +github.com/btcsuite/btcwallet v0.16.10-0.20240122184004-f1648e92097f/go.mod h1:LzcW/LYkQLgDufv6Ouw4cOIW0YsY+A60MTtc61/OZTU= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 h1:etuLgGEojecsDOYTII8rYiGHjGyV5xTqsXi+ZQ715UU= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2/go.mod h1:Zpk/LOb2sKqwP2lmHjaZT9AdaKsHPSbNLm2Uql5IQ/0= github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 h1:BtEN5Empw62/RVnZ0VcJaVtVlBijnLlJY+dwjAye2Bg= diff --git a/lnutils/errors.go b/lnutils/errors.go new file mode 100644 index 00000000000..f6a9c57c99a --- /dev/null +++ b/lnutils/errors.go @@ -0,0 +1,21 @@ +package lnutils + +import "errors" + +// ErrorAs behaves the same as `errors.As` except there's no need to declare +// the target error as a variable first. +// Instead of writing: +// +// var targetErr *TargetErr +// errors.As(err, &targetErr) +// +// We can write: +// +// lnutils.ErrorAs[*TargetErr](err) +// +// To save us from declaring the target error variable. +func ErrorAs[Target error](err error) bool { + var targetErr Target + + return errors.As(err, &targetErr) +} diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index a5fd7534b2e..93f909072a7 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -23,10 +23,12 @@ import ( "github.com/btcsuite/btcwallet/wallet/txrules" "github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) @@ -1199,33 +1201,102 @@ func (b *BtcWallet) ListUnspentWitness(minConfs, maxConfs int32, // already published to the network (either in the mempool or chain) no error // will be returned. func (b *BtcWallet) PublishTransaction(tx *wire.MsgTx, label string) error { - if err := b.wallet.PublishTransaction(tx, label); err != nil { + // handleErr is a helper closure that handles the error from + // PublishTransaction and TestMempoolAccept. + handleErr := func(err error) error { // If we failed to publish the transaction, check whether we // got an error of known type. - switch err.(type) { + switch { // If the wallet reports a double spend, convert it to our // internal ErrDoubleSpend and return. - case *base.ErrDoubleSpend: + case lnutils.ErrorAs[*base.ErrDoubleSpend](err): return lnwallet.ErrDoubleSpend // If the wallet reports a replacement error, return // ErrDoubleSpend, as we currently are never attempting to // replace transactions. - case *base.ErrReplacement: + case lnutils.ErrorAs[*base.ErrReplacement](err): return lnwallet.ErrDoubleSpend - // If the wallet reports that fee requirements for accepting the - // tx into mempool are not met, convert it to our internal + // If the wallet reports that fee requirements for accepting + // the tx into mempool are not met, convert it to our internal // ErrMempoolFee and return. - case *base.ErrMempoolFee: + case lnutils.ErrorAs[*base.ErrMempoolFee](err): return fmt.Errorf("%w: %v", lnwallet.ErrMempoolFee, err.Error()) - - default: - return err } + + return err } - return nil + + // For neutrino backend there's no mempool, so we return early by + // publishing the transaction. + if b.chain.BackEnd() == "neutrino" { + err := b.wallet.PublishTransaction(tx, label) + + return handleErr(err) + } + + // For non-neutrino nodes, we will first check whether the transaction + // can be accepted by the mempool. + // Use a max feerate of 0 means the default value will be used when + // testing mempool acceptance. The default max feerate is 0.10 BTC/kvb, + // or 10,000 sat/vb. + results, err := b.chain.TestMempoolAccept([]*wire.MsgTx{tx}, 0) + if err != nil { + return err + } + + // Sanity check that the expected single result is returned. + if len(results) != 1 { + return fmt.Errorf("expected 1 result from TestMempoolAccept, "+ + "instead got %v", len(results)) + } + + result := results[0] + log.Debugf("TestMempoolAccept result: %s", spew.Sdump(result)) + + // Once mempool check passed, we can publish the transaction. + if result.Allowed { + err = b.wallet.PublishTransaction(tx, label) + + return handleErr(err) + } + + // If the check failed, there's no need to publish it. We'll handle the + // error and return. + log.Warnf("Transaction %v not accepted by mempool: %v", + tx.TxHash(), result.RejectReason) + + // We need to use the string to create an error type and map it to a + // btcwallet error. + err = base.MapBroadcastBackendError(errors.New(result.RejectReason)) + + //nolint:lll + // These two errors are ignored inside `PublishTransaction`: + // https://github.com/btcsuite/btcwallet/blob/master/wallet/wallet.go#L3763 + // To keep our current behavior, we need to ignore the same errors + // returned from TestMempoolAccept. + // + // TODO(yy): since `LightningWallet.PublishTransaction` always publish + // the same tx twice, we'd always get ErrInMempool. We should instead + // create a new rebroadcaster that monitors the mempool, and only + // rebroadcast when the tx is evicted. This way we don't need to + // broadcast twice, and can instead return these errors here. + switch { + // NOTE: In addition to ignoring these errors, we need to call + // `PublishTransaction` again because we need to mark the label in the + // wallet. We can remove this exception once we have the above TODO + // fixed. + case lnutils.ErrorAs[*base.ErrInMempool](err), + lnutils.ErrorAs[*base.ErrAlreadyConfirmed](err): + + err := b.wallet.PublishTransaction(tx, label) + + return handleErr(err) + } + + return handleErr(err) } // LabelTransaction adds a label to a transaction. If the tx already diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index b33349119c0..aa3a6625ef0 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" @@ -1471,15 +1472,20 @@ func testTransactionSubscriptions(miner *rpctest.Harness, // We'll also ensure that the client is able to send our new // notifications when we _create_ transactions ourselves that spend our // own outputs. - b := txscript.NewScriptBuilder() - b.AddOp(txscript.OP_RETURN) - outputScript, err := b.Script() - require.NoError(t, err, "unable to make output script") + addr, err := alice.NewAddress( + lnwallet.WitnessPubKey, false, + lnwallet.DefaultAccountName, + ) + require.NoError(t, err) + + outputScript, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + burnOutput := wire.NewTxOut(outputAmt, outputScript) tx, err := alice.SendOutputs( []*wire.TxOut{burnOutput}, 2500, 1, labels.External, ) - require.NoError(t, err, "unable to create burn tx") + require.NoError(t, err, "unable to create tx") txid := tx.TxHash() err = waitForMempoolTx(miner, &txid) require.NoError(t, err, "tx not relayed to miner") @@ -1721,187 +1727,206 @@ func testPublishTransaction(r *rpctest.Harness, keyDesc, err := alice.DeriveNextKey(keychain.KeyFamilyMultiSig) require.NoError(t, err, "unable to obtain public key") - // We will first check that publishing a transaction already in the - // mempool does NOT return an error. Create the tx. - tx1 := newTx(t, r, keyDesc.PubKey, alice, false) + t.Run("no_error_on_duplicate_tx", func(t *testing.T) { + // We will first check that publishing a transaction already in + // the mempool does NOT return an error. Create the tx. + tx1 := newTx(t, r, keyDesc.PubKey, alice, false) - // Publish the transaction. - err = alice.PublishTransaction(tx1, labels.External) - require.NoError(t, err, "unable to publish") + // Publish the transaction. + err = alice.PublishTransaction(tx1, labels.External) + require.NoError(t, err) - txid1 := tx1.TxHash() - err = waitForMempoolTx(r, &txid1) - require.NoError(t, err, "tx not relayed to miner") + txid1 := tx1.TxHash() + err = waitForMempoolTx(r, &txid1) + require.NoError(t, err, "tx not relayed to miner") - // Publish the exact same transaction again. This should not return an - // error, even though the transaction is already in the mempool. - err = alice.PublishTransaction(tx1, labels.External) - require.NoError(t, err, "unable to publish") + // Publish the exact same transaction again. This should not + // return an error, even though the transaction is already in + // the mempool. + err = alice.PublishTransaction(tx1, labels.External) + require.NoError(t, err) - // Mine the transaction. - if _, err := r.Client.Generate(1); err != nil { - t.Fatalf("unable to generate block: %v", err) - } + // Mine the transaction. + _, err := r.Client.Generate(1) + require.NoError(t, err) + }) - // We'll now test that we don't get an error if we try to publish a - // transaction that is already mined. - // - // Create a new transaction. We must do this to properly test the - // reject messages from our peers. They might only send us a reject - // message for a given tx once, so we create a new to make sure it is - // not just immediately rejected. - tx2 := newTx(t, r, keyDesc.PubKey, alice, false) - - // Publish this tx. - err = alice.PublishTransaction(tx2, labels.External) - require.NoError(t, err, "unable to publish") - - // Mine the transaction. - if err := mineAndAssert(r, tx2); err != nil { - t.Fatalf("unable to mine tx: %v", err) - } + t.Run("no_error_on_minged_tx", func(t *testing.T) { + // We'll now test that we don't get an error if we try to + // publish a transaction that is already mined. + // + // Create a new transaction. We must do this to properly test + // the reject messages from our peers. They might only send us + // a reject message for a given tx once, so we create a new to + // make sure it is not just immediately rejected. + tx2 := newTx(t, r, keyDesc.PubKey, alice, false) + + // Publish this tx. + err = alice.PublishTransaction(tx2, labels.External) + require.NoError(t, err) - // Publish the transaction again. It is already mined, and we don't - // expect this to return an error. - err = alice.PublishTransaction(tx2, labels.External) - require.NoError(t, err, "unable to publish") + // Mine the transaction. + err := mineAndAssert(r, tx2) + require.NoError(t, err) - // We'll do the next mempool check on both RBF and non-RBF enabled - // transactions. + // Publish the transaction again. It is already mined, and we + // don't expect this to return an error. + err = alice.PublishTransaction(tx2, labels.External) + require.NoError(t, err) + }) + + // We'll do the next mempool check on both RBF and non-RBF + // enabled transactions. var ( - txFee = btcutil.Amount(0.005 * btcutil.SatoshiPerBitcoin) + txFee = btcutil.Amount( + 0.005 * btcutil.SatoshiPerBitcoin, + ) tx3, tx3Spend *wire.MsgTx ) + t.Run("rbf_tests", func(t *testing.T) { + for _, rbf := range []bool{false, true} { + // Now we'll try to double spend an output with a + // different transaction. Create a new tx and publish + // it. This is the output we'll try to double spend. + tx3 = newTx(t, r, keyDesc.PubKey, alice, false) + err := alice.PublishTransaction(tx3, labels.External) + require.NoError(t, err) + + // Mine the transaction. + err = mineAndAssert(r, tx3) + require.NoError(t, err) + + // Now we create a transaction that spends the output + // from the tx just mined. + tx4, err := txFromOutput( + tx3, alice.Cfg.Signer, keyDesc.PubKey, + keyDesc.PubKey, txFee, rbf, + ) + require.NoError(t, err) - for _, rbf := range []bool{false, true} { - // Now we'll try to double spend an output with a different - // transaction. Create a new tx and publish it. This is the - // output we'll try to double spend. - tx3 = newTx(t, r, keyDesc.PubKey, alice, false) - err := alice.PublishTransaction(tx3, labels.External) - if err != nil { - t.Fatalf("unable to publish: %v", err) - } - - // Mine the transaction. - if err := mineAndAssert(r, tx3); err != nil { - t.Fatalf("unable to mine tx: %v", err) - } - - // Now we create a transaction that spends the output from the - // tx just mined. - tx4, err := txFromOutput( - tx3, alice.Cfg.Signer, keyDesc.PubKey, - keyDesc.PubKey, txFee, rbf, - ) - if err != nil { - t.Fatal(err) - } + // This should be accepted into the mempool. + err = alice.PublishTransaction(tx4, labels.External) + require.NoError(t, err) - // This should be accepted into the mempool. - err = alice.PublishTransaction(tx4, labels.External) - if err != nil { - t.Fatalf("unable to publish: %v", err) - } + // Keep track of the last successfully published tx to + // spend tx3. + tx3Spend = tx4 - // Keep track of the last successfully published tx to spend - // tx3. - tx3Spend = tx4 + txid4 := tx4.TxHash() + err = waitForMempoolTx(r, &txid4) + require.NoError(t, err, "tx not relayed to miner") - txid4 := tx4.TxHash() - err = waitForMempoolTx(r, &txid4) - if err != nil { - t.Fatalf("tx not relayed to miner: %v", err) - } - - // Create a new key we'll pay to, to ensure we create a unique - // transaction. - keyDesc2, err := alice.DeriveNextKey( - keychain.KeyFamilyMultiSig, - ) - if err != nil { - t.Fatalf("unable to obtain public key: %v", err) - } + // Create a new key we'll pay to, to ensure we create a + // unique transaction. + keyDesc2, err := alice.DeriveNextKey( + keychain.KeyFamilyMultiSig, + ) + require.NoError(t, err, "unable to obtain public key") + + // Create a new transaction that spends the output from + // tx3, and that pays to a different address. We expect + // this to be rejected because it is a double spend. + tx5, err := txFromOutput( + tx3, alice.Cfg.Signer, keyDesc.PubKey, + keyDesc2.PubKey, txFee, rbf, + ) + require.NoError(t, err) - // Create a new transaction that spends the output from tx3, - // and that pays to a different address. We expect this to be - // rejected because it is a double spend. - tx5, err := txFromOutput( - tx3, alice.Cfg.Signer, keyDesc.PubKey, - keyDesc2.PubKey, txFee, rbf, - ) - if err != nil { - t.Fatal(err) - } + err = alice.PublishTransaction(tx5, labels.External) + require.ErrorIs(t, err, lnwallet.ErrDoubleSpend) - err = alice.PublishTransaction(tx5, labels.External) - if err != lnwallet.ErrDoubleSpend { - t.Fatalf("expected ErrDoubleSpend, got: %v", err) - } + // Create another transaction that spends the same + // output, but has a higher fee. We expect also this tx + // to be rejected for non-RBF enabled transactions, + // while it should succeed otherwise. + pubKey3, err := alice.DeriveNextKey( + keychain.KeyFamilyMultiSig, + ) + require.NoError(t, err, "unable to obtain public key") - // Create another transaction that spends the same output, but - // has a higher fee. We expect also this tx to be rejected for - // non-RBF enabled transactions, while it should succeed - // otherwise. - pubKey3, err := alice.DeriveNextKey(keychain.KeyFamilyMultiSig) - if err != nil { - t.Fatalf("unable to obtain public key: %v", err) - } - tx6, err := txFromOutput( - tx3, alice.Cfg.Signer, keyDesc.PubKey, - pubKey3.PubKey, 2*txFee, rbf, - ) - if err != nil { - t.Fatal(err) - } + tx6, err := txFromOutput( + tx3, alice.Cfg.Signer, keyDesc.PubKey, + pubKey3.PubKey, 2*txFee, rbf, + ) + require.NoError(t, err) + + // Expect rejection in non-RBF case. + expErr := lnwallet.ErrDoubleSpend + if rbf { + // Expect success in rbf case. + expErr = nil + tx3Spend = tx6 + } + err = alice.PublishTransaction(tx6, labels.External) + require.ErrorIs(t, err, expErr) - // Expect rejection in non-RBF case. - expErr := lnwallet.ErrDoubleSpend - if rbf { - // Expect success in rbf case. - expErr = nil - tx3Spend = tx6 - } - err = alice.PublishTransaction(tx6, labels.External) - if err != expErr { - t.Fatalf("expected ErrDoubleSpend, got: %v", err) + // Mine the tx spending tx3. + err = mineAndAssert(r, tx3Spend) + require.NoError(t, err) } + }) - // Mine the tx spending tx3. - if err := mineAndAssert(r, tx3Spend); err != nil { - t.Fatalf("unable to mine tx: %v", err) - } - } + t.Run("tx_double_spend", func(t *testing.T) { + // At last we try to spend an output already spent by a + // confirmed transaction. + // + // TODO(halseth): we currently skip this test for neutrino, as + // the backing btcd node will consider the tx being an orphan, + // and will accept it. Should look into if this is the behavior + // also for bitcoind, and update test accordingly. + if alice.BackEnd() != "neutrino" { + // Create another tx spending tx3. + pubKey4, err := alice.DeriveNextKey( + keychain.KeyFamilyMultiSig, + ) + require.NoError(t, err, "unable to obtain public key") - // At last we try to spend an output already spent by a confirmed - // transaction. - // TODO(halseth): we currently skip this test for neutrino, as the - // backing btcd node will consider the tx being an orphan, and will - // accept it. Should look into if this is the behavior also for - // bitcoind, and update test accordingly. - if alice.BackEnd() != "neutrino" { - // Create another tx spending tx3. - pubKey4, err := alice.DeriveNextKey( - keychain.KeyFamilyMultiSig, - ) - if err != nil { - t.Fatalf("unable to obtain public key: %v", err) - } - tx7, err := txFromOutput( - tx3, alice.Cfg.Signer, keyDesc.PubKey, - pubKey4.PubKey, txFee, false, - ) + tx7, err := txFromOutput( + tx3, alice.Cfg.Signer, keyDesc.PubKey, + pubKey4.PubKey, txFee, false, + ) + require.NoError(t, err) - if err != nil { - t.Fatal(err) + // Expect rejection. + err = alice.PublishTransaction(tx7, labels.External) + require.ErrorIs(t, err, lnwallet.ErrDoubleSpend) } + }) - // Expect rejection. - err = alice.PublishTransaction(tx7, labels.External) - if err != lnwallet.ErrDoubleSpend { - t.Fatalf("expected ErrDoubleSpend, got: %v", err) - } - } + t.Run("test_tx_size_limit", func(t *testing.T) { + // In this test, we'll try to create a massive transaction that + // can't be mined as dictacted by widely deployed transaction + // policy. + // + // To do this, we'll take out of the prior transactions, and + // add a bunch of outputs, putting it over the max weight + // limit. + testTx := tx3.Copy() + for i := 0; i < blockchain.MaxOutputsPerBlock; i++ { + testTx.AddTxOut(&wire.TxOut{ + Value: tx3.TxOut[0].Value, + PkScript: tx3.TxOut[0].PkScript, + }) + } + + // Now broadcast the transaction, we should get an error that + // the weight is too large. + // + // TODO(roasbeef): we can't use Unwrap() here as TxRuleError + // doesn't define it + err := alice.PublishTransaction(testTx, labels.External) + + errStr := "bad-txns-oversize" + + // For neutrino backend, the error string in not mapped. + // + // TODO(yy): unify error matching in neutrino too. + if alice.BackEnd() == "neutrino" { + errStr = "serialized transaction is too big" + } + + require.Contains(t, err.Error(), errStr) + }) } func testSignOutputUsingTweaks(r *rpctest.Harness, @@ -3244,9 +3269,15 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, case "bitcoind": // Start a bitcoind instance. tempBitcoindDir := t.TempDir() - zmqBlockHost := "ipc:///" + tempBitcoindDir + "/blocks.socket" - zmqTxHost := "ipc:///" + tempBitcoindDir + "/tx.socket" + rpcPort := getFreePort() + zmqBlockPort := getFreePort() + zmqTxPort := getFreePort() + zmqBlockHost := fmt.Sprintf("tcp://127.0.0.1:%d", + zmqBlockPort) + zmqTxHost := fmt.Sprintf("tcp://127.0.0.1:%d", + zmqTxPort) + bitcoind := exec.Command( "bitcoind", "-datadir="+tempBitcoindDir,