diff --git a/backend/backend.go b/backend/backend.go index 59851344c4..593beff7f1 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -60,6 +60,10 @@ import ( "github.com/sirupsen/logrus" ) +func init() { + electrum.SetClientSoftwareVersion(Version) +} + type backendEvent struct { Type string `json:"type"` Data string `json:"data"` @@ -545,56 +549,34 @@ func (backend *Backend) defaultProdServers(code coinpkg.Code) []*config.ServerIn } func defaultDevServers(code coinpkg.Code) []*config.ServerInfo { + // O=Shift Crypto, CN=ShiftCrypto DEV R1 + // Serial: f67ab2bc7470c90ce027ce778a274384 const devShiftCA = `-----BEGIN CERTIFICATE----- -MIIGGjCCBAKgAwIBAgIJAO1AEqR+xvjRMA0GCSqGSIb3DQEBDQUAMIGZMQswCQYD -VQQGEwJDSDEPMA0GA1UECAwGWnVyaWNoMR0wGwYDVQQKDBRTaGlmdCBDcnlwdG9z -ZWN1cml0eTEzMDEGA1UECwwqU2hpZnQgQ3J5cHRvc2VjdXJpdHkgQ2VydGlmaWNh -dGUgQXV0aG9yaXR5MSUwIwYDVQQDDBxTaGlmdCBDcnlwdG9zZWN1cml0eSBSb290 -IENBMB4XDTE4MDMwNzE3MzUxMloXDTM4MDMwMjE3MzUxMlowgZkxCzAJBgNVBAYT -AkNIMQ8wDQYDVQQIDAZadXJpY2gxHTAbBgNVBAoMFFNoaWZ0IENyeXB0b3NlY3Vy -aXR5MTMwMQYDVQQLDCpTaGlmdCBDcnlwdG9zZWN1cml0eSBDZXJ0aWZpY2F0ZSBB -dXRob3JpdHkxJTAjBgNVBAMMHFNoaWZ0IENyeXB0b3NlY3VyaXR5IFJvb3QgQ0Ew -ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDlz32VZk/D3rfm7Qwx6WkE -Fp9cdQV2FNYTeTjWVErVeTev02ctHHXV1fR3Svk8iIJWaALSJy7phdEDwC/3gDIQ -Ylm15kpntCibOWiQPZZxGq7Udts20fooccdZqtG/PKFRCPWZ2MOgHAOWDKGk6Kb+ -siqkr55hkxwtiHuwkCcTh/Q2orEIuteSRbbYwgURZwd6dDIQq4ty7reC3j32xphh -edbnVBoDE6DSdebSS5SJL/gb6LxUdio98XdJPwkaD8292uEODxx0DKw/Ou2e1f5Q -Iv1WBl+LBaSrZ3sJSFUqoSvCQwBQmMAPoPJ1O13jCnFz1xoNygxUfz2eiKRL5E2l -VTmTh7zIez4oniOh5MOmDnKMVgTUGP1II2UU5r6PAq2tDpw4lVwyezhyLaBegwMc -pg/LinbABxUJrP8c8G2tve0yuTAhsir7r+Koo+nAE7FwcuIkD0UTyQcoag2IMS8O -dKZdYMGXjfUPJRBWg60LfXJeqMyU1oHpDrsRoa5iaYPt7ZApxc41kyynqfuuuIRD -du8327gd1nJ6ExMxGHY7dYelE4GNkOg3R0+5czykm/RxnGyDuDcO/RcYBJTChN1L -HYq+dTt0dYPAzBtiXnfuvjDyOsDK5f65pbrDgoOr6AQ4lvDJabcXFsWPrulM9Dyu -p0Y4+fuwXOCd8cr1Zm34MQIDAQABo2MwYTAdBgNVHQ4EFgQU486X86LMbNNSDw7J -NcT2U30NrikwHwYDVR0jBBgwFoAU486X86LMbNNSDw7JNcT2U30NrikwDwYDVR0T -AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQENBQADggIBAN0N -IPVBv8aaKDHDK9Nsu5fwiGp8GgkAN0B1+D34CbxTuzCDurToVMHCPEdo9tk/AzE4 -Aa1p/kMW9X3XP8IyCFFj+BpEVkBRr9fXTVuh3XRHbyN6tXFbkKWQ/6QeUcnefq2k -DCpqEGjJQWsujZ4tJKkJl2HLIBZL6FAa/kaDLFHd3LeV1immC66CiN3ieHejCJL1 -zZXiWi8pNxvEanTLPBaBjCw/AAl/owg/ySu2hGZzL0wsFboPrUbo4J+KvL1pvwql -PCT8AylJKCu+cn/N9zZDtUsgZJQBIq7btoakC3mCSnfVTlcbxfHVef0DbfohFqoV -ZpdmIuy0/njw7o+2uL/ArPJscPOhNl60ocDbdFIyYvc85oxyts8yMvKDdWV9Bm// -kl7lv4QUAvjqjb7ZgUhYibVk3Eu6n1MGZOP40l1/mm922/Wcd2n/HZVk/LsJs4tt -B6DLMDpf5nzeI1Yz/QtDGvNyb4aiJoRV5tQb9KkFfIeSzBS/ORZto4tVHKS37lxV -d1r8kFyCgpL9KASdahfyLBWCC7awlcOQP1QJA5QoO9u5Feq3lU0VnJF0YCZh8GOy -py3n1TR6S59eT495BiKDjWnhdVchEa8zMGIW/wFW7EX/LyW2zX3hQsdfnmMWUPVr -O3nOxjgSfRAfKWQ2Ny1APKcn6I83P5PFLhtO5I12 +MIIBmDCCAT2gAwIBAgIRAPZ6srx0cMkM4CfOd4onQ4QwCgYIKoZIzj0EAwIwNDEV +MBMGA1UEChMMU2hpZnQgQ3J5cHRvMRswGQYDVQQDExJTaGlmdENyeXB0byBERVYg +UjEwHhcNMjAxMjAyMTUzODM1WhcNMzAxMjAzMDMzODM1WjA0MRUwEwYDVQQKEwxT +aGlmdCBDcnlwdG8xGzAZBgNVBAMTElNoaWZ0Q3J5cHRvIERFViBSMTBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABEC9ECgMAR04mfmcT499pjQFayUom3Tuczg+CPUU +BKAHMosD2hj0E5Oia5/+zYLrxwu5p/IWhuqS76tqVhFdIAmjMDAuMA4GA1UdDwEB +/wQEAwIChDAPBgNVHRMBAf8EBTADAQH/MAsGA1UdEQQEMAKCADAKBggqhkjOPQQD +AgNJADBGAiEAgEXhjnZffIZlnhnjlav3AJaqRcl/2jn+CnwTg7nFDxwCIQDE4UDd +mCMuGBNHsbrs6rI1hbI4Qq6GYazLaDRqdCufTA== -----END CERTIFICATE-----` switch code { case coinpkg.CodeBTC: - return []*config.ServerInfo{{Server: "dev.shiftcrypto.ch:50002", TLS: true, PEMCert: devShiftCA}} + return []*config.ServerInfo{{Server: "btc1.shiftcrypto.dev:50001", TLS: true, PEMCert: devShiftCA}} case coinpkg.CodeTBTC: return []*config.ServerInfo{ - {Server: "s1.dev.shiftcrypto.ch:51003", TLS: true, PEMCert: devShiftCA}, - {Server: "s2.dev.shiftcrypto.ch:51003", TLS: true, PEMCert: devShiftCA}, + {Server: "tbtc1.shiftcrypto.dev:51001", TLS: true, PEMCert: devShiftCA}, + {Server: "tbtc2.shiftcrypto.dev:51002", TLS: true, PEMCert: devShiftCA}, } case coinpkg.CodeRBTC: return []*config.ServerInfo{{Server: "127.0.0.1:52001", TLS: false, PEMCert: ""}} case coinpkg.CodeLTC: - return []*config.ServerInfo{{Server: "dev.shiftcrypto.ch:50004", TLS: true, PEMCert: devShiftCA}} + return []*config.ServerInfo{{Server: "ltc1.shiftcrypto.dev:50011", TLS: true, PEMCert: devShiftCA}} case coinpkg.CodeTLTC: - return []*config.ServerInfo{{Server: "dev.shiftcrypto.ch:51004", TLS: true, PEMCert: devShiftCA}} + return []*config.ServerInfo{{Server: "tltc1.shiftcrypto.dev:51011", TLS: true, PEMCert: devShiftCA}} default: panic(errp.Newf("The given code %s is unknown.", code)) } diff --git a/backend/coins/btc/electrum/client/client.go b/backend/coins/btc/electrum/client/client.go index b47b9ffafc..f8fb0b2dbc 100644 --- a/backend/coins/btc/electrum/client/client.go +++ b/backend/coins/btc/electrum/client/client.go @@ -13,7 +13,7 @@ // limitations under the License. // Package client implements an Electrum JSON RPC client. -// See https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst +// See ElectrumClient for more details. package client import ( @@ -35,21 +35,25 @@ import ( "github.com/sirupsen/logrus" ) -const ( - clientVersion = "0.0.1" - clientProtocolVersion = "1.2" -) +// SoftwareVersion reports to an electrum protocol compatible server +// its name and a version so that server owners can identify what kind of +// clients are connected. +// It is set at the app startup in the backend and never changes during the runtime. +var SoftwareVersion = "BitBoxApp/uninitialized" + +// supportedProtocolVersion reports to the servers the minimal supported electrum +// protocol version during the initial connection phase. +const supportedProtocolVersion = "1.4" -// ElectrumClient is a high level API access to an ElectrumX server. -// See https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst. +// ElectrumClient is a high level API access to an Electrum protocol compatible server. +// See https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html +// selecting the supportedProtocolVersion currently in use. type ElectrumClient struct { rpc *jsonrpc.RPCClient scriptHashNotificationCallbacks map[string][]func(string) scriptHashNotificationCallbacksLock sync.RWMutex - serverVersion *ServerVersion - close bool log *logrus.Entry } @@ -91,11 +95,10 @@ func NewElectrumClient(rpcClient *jsonrpc.RPCClient, log *logrus.Entry) *Electru rpcClient.OnConnect(func() error { // Sends the version and must be the first message, to establish which methods the server // accepts. - version, err := electrumClient.ServerVersion() + version, err := electrumClient.negotiateProtocol() if err != nil { return err } - electrumClient.serverVersion = version log.WithField("server-version", version).Debug("electrumx server version") return nil }) @@ -128,58 +131,45 @@ func (client *ElectrumClient) RegisterOnConnectionStatusChangedEvent(onConnectio }) } -// ServerVersion is returned by ServerVersion(). -type ServerVersion struct { - Version string - ProtocolVersion *semver.SemVer +// serverVersion is returned by serverVersion(). +type serverVersion struct { + software string + protocol *semver.SemVer } -func (version *ServerVersion) String() string { - return fmt.Sprintf("%s;%s", version.Version, version.ProtocolVersion) +func (v serverVersion) String() string { + return fmt.Sprintf("%s;%s", v.software, v.protocol) } -// UnmarshalJSON implements the json.Unmarshaler interface. -func (version *ServerVersion) UnmarshalJSON(b []byte) error { - slice := []string{} - if err := json.Unmarshal(b, &slice); err != nil { - return errp.WithContext(errp.Wrap(err, "Failed to unmarshal JSON"), errp.Context{"raw": string(b)}) +// negotiateProtocol performs client/server protocol negotiation using the +// server.version RPC call. +// ElectrumX will reply with a success only once. Subsequent calls return +// a "server.version already sent" error. Electrs doesn't enforce the "only once" +// condition. +func (client *ElectrumClient) negotiateProtocol() (serverVersion, error) { + var resp [2]string // [software version, protocol version] + err := client.rpc.MethodSync(&resp, "server.version", SoftwareVersion, supportedProtocolVersion) + if err != nil { + return serverVersion{}, err } - if len(slice) != 2 { - return errp.WithContext(errp.New("Unexpected reply"), errp.Context{"raw": string(b)}) + semv, err := semver.NewSemVerFromString(resp[1]) + if err != nil { + semv, err = semver.NewSemVerFromString(resp[1] + ".0") } - version.Version = slice[0] - protocolVersion := slice[1] - // We expect the protocolVersion to be either major.minor or major.minor.patch. - protocolSemVer, err := semver.NewSemVerFromString(protocolVersion) if err != nil { - protocolSemVer, err = semver.NewSemVerFromString(protocolVersion + ".0") - if err != nil { - return err - } + semv = &semver.SemVer{} // don't really care; nice to have } - version.ProtocolVersion = protocolSemVer - return nil + return serverVersion{software: resp[0], protocol: semv}, nil } -// ServerVersion does the server.version() RPC call. -// https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#serverversion -func (client *ElectrumClient) ServerVersion() (*ServerVersion, error) { - response := &ServerVersion{} - err := client.rpc.MethodSync(response, "server.version", clientVersion, clientProtocolVersion) - return response, err -} - -// ServerFeatures is returned by ServerFeatures(). -type ServerFeatures struct { - GenesisHash string `json:"genesis_hash"` -} - -// ServerFeatures does the server.features() RPC call. -// https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#serverfeatures -func (client *ElectrumClient) ServerFeatures() (*ServerFeatures, error) { - response := &ServerFeatures{} - err := client.rpc.MethodSync(response, "server.features") - return response, err +// CheckConnection reports whether the server returns a succesfull response +// to a server.ping RPC method. +// It is different when compared to a raw socket connection check. +// A client seemingly connected at a transport level is not indicative of +// a functioning application layer connection. This reports the latter. +func (client *ElectrumClient) CheckConnection() error { + var empty struct{} + return client.rpc.MethodSync(&empty, "server.ping") } // Balance is returned by ScriptHashGetBalance(). @@ -188,8 +178,7 @@ type Balance struct { Unconfirmed int64 `json:"unconfirmed"` } -// ScriptHashGetBalance does the blockchain.scripthash.get_balance() RPC call. -// https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#blockchainscripthashget_balance +// ScriptHashGetBalance does the blockchain.scripthash.get_balance RPC call. func (client *ElectrumClient) ScriptHashGetBalance( scriptHashHex string, success func(*Balance) error, @@ -210,8 +199,7 @@ func (client *ElectrumClient) ScriptHashGetBalance( scriptHashHex) } -// ScriptHashGetHistory does the blockchain.scripthash.get_history() RPC call. -// https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#blockchainscripthashget_history +// ScriptHashGetHistory does the blockchain.scripthash.get_history RPC call. func (client *ElectrumClient) ScriptHashGetHistory( scriptHashHex blockchain.ScriptHashHex, success func(blockchain.TxHistory), @@ -234,8 +222,7 @@ func (client *ElectrumClient) ScriptHashGetHistory( string(scriptHashHex)) } -// ScriptHashSubscribe does the blockchain.scripthash.subscribe() RPC call. -// https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#blockchainscripthashsubscribe +// ScriptHashSubscribe does the blockchain.scripthash.subscribe RPC call. func (client *ElectrumClient) ScriptHashSubscribe( setupAndTeardown func() func(error), scriptHashHex blockchain.ScriptHashHex, @@ -281,8 +268,7 @@ func parseTX(rawTXHex string) (*wire.MsgTx, error) { return tx, nil } -// TransactionGet downloads a transaction. -// See https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#blockchaintransactionget +// TransactionGet downloads a transaction using the blockchain.transaction.get RPC method. func (client *ElectrumClient) TransactionGet( txHash chainhash.Hash, success func(*wire.MsgTx), @@ -308,26 +294,14 @@ func (client *ElectrumClient) TransactionGet( txHash.String()) } -type electrumHeader struct { - // Provided by v1.4 - BlockHeight int `json:"block_height"` - // Provided by v1.2 - Height int `json:"height"` -} - -func (h *electrumHeader) height(serverVersion *semver.SemVer) int { - if serverVersion.AtLeast(semver.NewSemVer(1, 4, 0)) { - return h.Height - } - return h.BlockHeight -} - -// HeadersSubscribe does the blockchain.headers.subscribe() RPC call. -// https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#blockchainheaderssubscribe +// HeadersSubscribe does the blockchain.headers.subscribe RPC call. func (client *ElectrumClient) HeadersSubscribe( setupAndTeardown func() func(error), success func(*blockchain.Header), ) { + type header struct { + Height int + } client.rpc.SubscribeNotifications("blockchain.headers.subscribe", func(responseBytes []byte) { response := []json.RawMessage{} if err := json.Unmarshal(responseBytes, &response); err != nil { @@ -338,20 +312,20 @@ func (client *ElectrumClient) HeadersSubscribe( client.log.Error("could not handle header notification") return } - header := &electrumHeader{} - if err := json.Unmarshal(response[0], header); err != nil { + var h header + if err := json.Unmarshal(response[0], &h); err != nil { client.log.WithError(err).Error("could not handle header notification") return } - success(&blockchain.Header{BlockHeight: header.height(client.serverVersion.ProtocolVersion)}) + success(&blockchain.Header{BlockHeight: h.Height}) }) client.rpc.Method( func(responseBytes []byte) error { - header := &electrumHeader{} - if err := json.Unmarshal(responseBytes, header); err != nil { + var h header + if err := json.Unmarshal(responseBytes, &h); err != nil { return errp.WithStack(err) } - success(&blockchain.Header{BlockHeight: header.height(client.serverVersion.ProtocolVersion)}) + success(&blockchain.Header{BlockHeight: h.Height}) return nil }, setupAndTeardown, @@ -393,8 +367,7 @@ type UTXO struct { Height int `json:"height"` } -// ScriptHashListUnspent does the blockchain.address.listunspent() RPC call. -// https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#blockchainscripthashlistunspent +// ScriptHashListUnspent does the blockchain.address.listunspent RPC call. func (client *ElectrumClient) ScriptHashListUnspent(scriptHashHex string) ([]*UTXO, error) { response := []*UTXO{} if err := client.rpc.MethodSync(&response, "blockchain.scripthash.listunspent", scriptHashHex); err != nil { @@ -403,8 +376,7 @@ func (client *ElectrumClient) ScriptHashListUnspent(scriptHashHex string) ([]*UT return response, nil } -// TransactionBroadcast does the blockchain.transaction.broadcast() RPC call. -// https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#blockchaintransactionbroadcast +// TransactionBroadcast does the blockchain.transaction.broadcast RPC call. func (client *ElectrumClient) TransactionBroadcast(transaction *wire.MsgTx) error { rawTx := &bytes.Buffer{} _ = transaction.BtcEncode(rawTx, 0, wire.WitnessEncoding) @@ -422,8 +394,7 @@ func (client *ElectrumClient) TransactionBroadcast(transaction *wire.MsgTx) erro return nil } -// RelayFee does the blockchain.relayfee() RPC call. -// https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#blockchainrelayfee +// RelayFee does the blockchain.relayfee RPC call. func (client *ElectrumClient) RelayFee( success func(btcutil.Amount), cleanup func(error), @@ -443,9 +414,9 @@ func (client *ElectrumClient) RelayFee( } // EstimateFee estimates the fee rate (unit/kB) needed to be confirmed within the given number of -// blocks. If the fee rate could not be estimated by the blockchain node, `nil` is passed to the +// blocks using the blockchain.estimatefee RPC method. +// If the fee rate could not be estimated by the blockchain node, nil is passed to the // success callback. -// https://github.com/kyuupichan/electrumx/blob/159db3f8e70b2b2cbb8e8cd01d1e9df3fe83828f/docs/PROTOCOL.rst#blockchainestimatefee func (client *ElectrumClient) EstimateFee( number int, success func(*btcutil.Amount), @@ -491,8 +462,7 @@ func parseHeaders(reader io.Reader) ([]*wire.BlockHeader, error) { return headers, nil } -// Headers does the blockchain.block.headers() RPC call. See -// https://github.com/kyuupichan/electrumx/blob/1.3/docs/protocol-methods.rst#blockchainblockheaders +// Headers does the blockchain.block.headers RPC call. func (client *ElectrumClient) Headers( startHeight int, count int, success func(headers []*wire.BlockHeader, max int), @@ -527,8 +497,7 @@ func (client *ElectrumClient) Headers( startHeight, count) } -// GetMerkle does the blockchain.transaction.get_merkle() RPC call. See -// https://github.com/kyuupichan/electrumx/blob/1.3/docs/protocol-methods.rst#blockchaintransactionget_merkle +// GetMerkle does the blockchain.transaction.get_merkle RPC call. func (client *ElectrumClient) GetMerkle( txHash chainhash.Hash, height int, success func(merkle []blockchain.TXHash, pos int), diff --git a/backend/coins/btc/electrum/electrum.go b/backend/coins/btc/electrum/electrum.go index 867f43fabc..8f2fe92746 100644 --- a/backend/coins/btc/electrum/electrum.go +++ b/backend/coins/btc/electrum/electrum.go @@ -19,6 +19,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/pem" + "fmt" "io" "net" "time" @@ -29,10 +30,19 @@ import ( "github.com/digitalbitbox/bitbox-wallet-app/util/errp" "github.com/digitalbitbox/bitbox-wallet-app/util/jsonrpc" "github.com/digitalbitbox/bitbox-wallet-app/util/socksproxy" + "github.com/digitalbitbox/bitbox02-api-go/util/semver" "github.com/sirupsen/logrus" "golang.org/x/net/proxy" ) +// SetClientSoftwareVersion updates an electrumx client software version string +// sent to the servers during the protocol version negotiation. +// This is purely informational and has no impact on supported protocol versions. +// SetClientSoftwareVersion is unsafe for concurrent use. +func SetClientSoftwareVersion(v *semver.SemVer) { + client.SoftwareVersion = fmt.Sprintf("BitBoxApp/%s", v) +} + // establishConnection connects to a backend and returns an rpc client // or an error if the connection could not be established. func establishConnection( @@ -216,7 +226,7 @@ func CheckElectrumServer(serverInfo *config.ServerInfo, log *logrus.Entry, diale // We receive the first one that comes back. defer electrumClient.Close() go func() { - _, err := electrumClient.ServerVersion() + err := electrumClient.CheckConnection() select { case errChan <- err: default: diff --git a/backend/config/config.go b/backend/config/config.go index 34c2aae0b3..e4224d7ef2 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -134,41 +134,19 @@ type AppConfig struct { Frontend interface{} `json:"frontend"` } +// O=Shift Crypto, CN=ShiftCrypto R1 +// Serial: d35011c382968211cd4e29fefc144891. const shiftRootCA = ` -----BEGIN CERTIFICATE----- -MIIGGjCCBAKgAwIBAgIJAKRWPF0NRtHyMA0GCSqGSIb3DQEBDQUAMIGZMQswCQYD -VQQGEwJDSDEPMA0GA1UECAwGWnVyaWNoMR0wGwYDVQQKDBRTaGlmdCBDcnlwdG9z -ZWN1cml0eTEzMDEGA1UECwwqU2hpZnQgQ3J5cHRvc2VjdXJpdHkgQ2VydGlmaWNh -dGUgQXV0aG9yaXR5MSUwIwYDVQQDDBxTaGlmdCBDcnlwdG9zZWN1cml0eSBSb290 -IENBMB4XDTE4MDYwODE0NTA0MloXDTM4MDYwMzE0NTA0MlowgZkxCzAJBgNVBAYT -AkNIMQ8wDQYDVQQIDAZadXJpY2gxHTAbBgNVBAoMFFNoaWZ0IENyeXB0b3NlY3Vy -aXR5MTMwMQYDVQQLDCpTaGlmdCBDcnlwdG9zZWN1cml0eSBDZXJ0aWZpY2F0ZSBB -dXRob3JpdHkxJTAjBgNVBAMMHFNoaWZ0IENyeXB0b3NlY3VyaXR5IFJvb3QgQ0Ew -ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0H598q5C+yDJI9F8QYkYK -6/48kFNQ0rbAKcKkgR0+H8CGuFVOGQdcv7tObCMe0Dyr8ioNkq7AP+Nt1e1TVgKQ -ANmJqz2rKvA4sIIgdBjUs0DXPuCaDzGGbJHIXnGMuGANX6xnqvdOj7kIA6r6s7Hh -eWQEB8tGiRdHWJitpkc1xEfW1DhnMQPnSihSJM5qltXVPKxzqqElv0iGI/La3S8W -nJV7kTGTsLouX1CcwLjp6avlVy56utOYRXkgfuY88XxmOjlAECeoYCWFBGaSWK+h -2sBLbRC9G0YWmNCqB+GjMj8myj06crLn7mZgBODEyUrFYMjAPrpmAScmw38y2rwN -AK6ii75P+sHc3BPi05Vap2GoTAY0db62NiN3dsNxHB5DbehA4Zfaqzcakjv4CSRo -zkg2JSlofOZWd3aomxIKfFLl+aVFjukXEKaz8P+2xe5/2/M35kKIIJCuHz1Ybor1 -Ze9YmLAnLnbTCA7VcKkUs25lskL/zRC4sdLzgJ2V2UdHWPAo/ttwBXtw6piw4v4N -DfCuKDMiomxwNiGvb3GZWMhOHT30NLZ0nuRAGjeg7jFBqSh2SPeDu+hnImAAh2WX -7ul3/kschLF+3otC/x7jmAMWXzb1oVWRqj2Gyjner1p82gWxj5k7Hs/2mG3TV6sv -pyVqMqonbirxuO+kbzYLxQIDAQABo2MwYTAdBgNVHQ4EFgQU301oVCni/CbDXJ9V -Fez6Vgu0arUwHwYDVR0jBBgwFoAU301oVCni/CbDXJ9VFez6Vgu0arUwDwYDVR0T -AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQENBQADggIBAD1d -/KJ3w1Je3oOx0afcXOf2IOoMvKSFbBg9u+rpXBh60cacjPtwbIMIyF3ynYYGzx7D -x8mr6wagJ+uqKn9E7JGp2h0lKhT9cgxzqIk3r4D/jhvh1zijInCEbPPphwbemzIG -JxxmpDOeURHVxCcSpIJlGfRURdfdXwleWiz9zCkNUvmgTDrfBjEk6ywSSKD4uJuT -jBcav1P4OkeFokAPO1Uc9NCXox5NUAsDosdZUbxbH8vf61Xbr6fnxmy731s9D7cc -djXPb3pbXtRL4A0hNnOWcuPM30hn4ZkIm08TGT+IMOFYBk+pe2IXSFzUcDYEL/ws -wuHqctRlw/t4extJFYvzASOkBr4zFceR9jCSWR8kOkWY81evx/bxG+eQBJMkzrdw -LOChedVDVIuoTZxfqNzU4Y2TgMGMRWsrEVvBvTMIY3qBue1yeT9M4jzAPhms3/It -Ps7ZeqmF+HrRtFz5ctHQa0QOdZodsKJO0WwjjzjYTDMzZO+bnVFFUy9cG+Gr6mt1 -XMKJKkvXQuYTfbRrox4HzIjyfi54xYHnUI35uUUUzEO19Qtm4Ds+sz7/vyz3cYFI -d8IgKoqstjsxtaRq1IS6WIj0bQ/nEqoTNg0I3bndrmCq5LbCoq0z2yXYr5Vl5Gvf -ffbrVM+I91v3R03Svv2Nte2xdbx1RmoI/y3tMyZL +MIIBgjCCASigAwIBAgIRANNQEcOCloIRzU4p/vwUSJEwCgYIKoZIzj0EAwIwMDEV +MBMGA1UEChMMU2hpZnQgQ3J5cHRvMRcwFQYDVQQDEw5TaGlmdENyeXB0byBSMTAe +Fw0yMDEyMDgyMzU5MzZaFw0zMDEyMDYyMzU5MzZaMDAxFTATBgNVBAoTDFNoaWZ0 +IENyeXB0bzEXMBUGA1UEAxMOU2hpZnRDcnlwdG8gUjEwWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAARr5MqlIwZF1Vm4Ng5Smlb4ZuQ1wIwCVxl9zcX5kgMmaJFlTPpk +8jj3KE4+DXkxixuHarJgdamGP/SYawG69HyMoyMwITAOBgNVHQ8BAf8EBAMCAoQw +DwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiEApDfR07EgYNtXuXM1 +YFnRE7QtJtiu97fYZk3Qu8W3/TECIAtCHAzvA2AX/eX4fNTjUho/9y1qF9GAsnNN +hbWFoWMI -----END CERTIFICATE----- ` @@ -192,12 +170,12 @@ func NewDefaultAppConfig() AppConfig { BTC: btcCoinConfig{ ElectrumServers: []*ServerInfo{ { - Server: "btc.shiftcrypto.ch:443", + Server: "btc1.shiftcrypto.io:50001", TLS: true, PEMCert: shiftRootCA, }, { - Server: "merkle.shiftcrypto.ch:443", + Server: "btc2.shiftcrypto.io:50002", TLS: true, PEMCert: shiftRootCA, }, @@ -206,12 +184,12 @@ func NewDefaultAppConfig() AppConfig { TBTC: btcCoinConfig{ ElectrumServers: []*ServerInfo{ { - Server: "btc.shiftcrypto.ch:51002", + Server: "tbtc1.shiftcrypto.io:51001", TLS: true, PEMCert: shiftRootCA, }, { - Server: "merkle.shiftcrypto.ch:51002", + Server: "tbtc2.shiftcrypto.io:51002", TLS: true, PEMCert: shiftRootCA, }, @@ -229,12 +207,12 @@ func NewDefaultAppConfig() AppConfig { LTC: btcCoinConfig{ ElectrumServers: []*ServerInfo{ { - Server: "ltc.shiftcrypto.ch:443", + Server: "ltc1.shiftcrypto.io:50011", TLS: true, PEMCert: shiftRootCA, }, { - Server: "ltc.shamir.shiftcrypto.ch:443", + Server: "ltc2.shiftcrypto.io:50012", TLS: true, PEMCert: shiftRootCA, }, @@ -243,12 +221,12 @@ func NewDefaultAppConfig() AppConfig { TLTC: btcCoinConfig{ ElectrumServers: []*ServerInfo{ { - Server: "ltc.shiftcrypto.ch:51004", + Server: "tltc1.shiftcrypto.io:51011", TLS: true, PEMCert: shiftRootCA, }, { - Server: "ltc.shamir.shiftcrypto.ch:51004", + Server: "tltc2.shiftcrypto.io:51012", TLS: true, PEMCert: shiftRootCA, }, @@ -295,6 +273,7 @@ func NewConfig(appConfigFilename string, accountsConfigFilename string) (*Config appconf := config.appConfig migrateFiatList(&appconf) migrateFiatCode(&appconf) + migrateElectrumX(&appconf) if err := config.SetAppConfig(appconf); err != nil { return nil, errp.WithStack(err) } @@ -418,3 +397,31 @@ func migrateFiatCode(appconf *AppConfig) { delete(frontconf, "fiatCode") } } + +// migrateElectrumX replaces all instances of old ElectrumX servers to the new +// ones which run protocol version of at least 1.4. +func migrateElectrumX(appconf *AppConfig) { + migrateBTCCoinConfig(&appconf.Backend.BTC) + migrateBTCCoinConfig(&appconf.Backend.TBTC) + migrateBTCCoinConfig(&appconf.Backend.LTC) + migrateBTCCoinConfig(&appconf.Backend.TLTC) +} + +func migrateBTCCoinConfig(conf *btcCoinConfig) { + newServers := map[string]string{ // old -> new + "btc.shiftcrypto.ch:443": "btc1.shiftcrypto.io:50001", + "merkle.shiftcrypto.ch:443": "btc2.shiftcrypto.io:50002", + "btc.shiftcrypto.ch:51002": "tbtc1.shiftcrypto.io:51001", + "merkle.shiftcrypto.ch:51002": "tbtc2.shiftcrypto.io:51002", + "ltc.shiftcrypto.ch:443": "ltc1.shiftcrypto.io:50011", + "ltc.shamir.shiftcrypto.ch:443": "ltc2.shiftcrypto.io:50012", + "ltc.shiftcrypto.ch:51004": "tltc1.shiftcrypto.io:51011", + "ltc.shamir.shiftcrypto.ch:51004": "tltc2.shiftcrypto.io:51012", + } + for _, item := range conf.ElectrumServers { + if host, ok := newServers[item.Server]; ok { + item.Server = host + item.PEMCert = shiftRootCA + } + } +} diff --git a/util/jsonrpc/jsonrpc.go b/util/jsonrpc/jsonrpc.go index d0f2a4aa7f..b74e483b3b 100644 --- a/util/jsonrpc/jsonrpc.go +++ b/util/jsonrpc/jsonrpc.go @@ -177,11 +177,18 @@ func (client *RPCClient) requeueSubscriptions() { func (client *RPCClient) resendPendingRequests() { defer client.pendingRequestsLock.RLock()() client.log.Debugf("Queueing %v pending requests to resend.", len(client.pendingRequests)) + // An alternative is to grab the lock in the goroutine below + // but there's a high risk it'll keep the mutex locked for too long, + // preventing new requests to be dispatched. + pending := make([][]byte, 0, len(client.pendingRequests)) + for _, req := range client.pendingRequests { + pending = append(pending, req.jsonText) + } // This needs to be executed in a go-routine so that it doesn't block if the connection fails // and a failover is initiated. go func() { - for _, request := range client.pendingRequests { - err := client.send(request.jsonText) + for _, jsonText := range pending { + err := client.send(jsonText) if err != nil { wait := time.Minute / 4 client.log.Debugf("Resending failed. Waiting for %v", wait) @@ -331,6 +338,14 @@ func (client *RPCClient) conn() (*connection, error) { func (client *RPCClient) cleanupFinishedRequest(responseError error, conn *connection, responseID int) { defer client.pendingRequestsLock.Lock()() finishedRequest := client.pendingRequests[responseID] + // Without the nil check it sometimes crashes with SIGSEGV on failover: + // panic: runtime error: invalid memory address or nil pointer dereference. + // It is unclear when exactly it happens but this package will be rewritten, + // so a simple nil check will do for now. + if finishedRequest == nil { + client.log.Infof("No pending req with response ID=%d; won't clean up", responseID) + return + } finishedRequest.responseCallbacks.cleanup(responseError) if responseError != nil && client.isSubscriptionRequest(finishedRequest.method) { func() { @@ -519,10 +534,13 @@ func (client *RPCClient) prepare( params ...interface{}, ) []byte { // Ideally, we should have a worker thread that processes a "to be send" list. - cleanup := func(error) {} + var cleanup func(error) if setupAndTeardown != nil { cleanup = setupAndTeardown() } + if cleanup == nil { + cleanup = func(error) {} // noop + } msgID, jsonText := client.transform(method, params...) @@ -561,14 +579,17 @@ func (client *RPCClient) Method( // json-deserialized into response. func (client *RPCClient) MethodSync(response interface{}, method string, params ...interface{}) error { responseChan := make(chan []byte) - errChan := make(chan error) + // errChan needs a buffer of at least 1 to accommodate MethodSync version check + // during a new connection in backend/coins/btc/electrum/client pkg without + // deadlocks. + errChan := make(chan error, 1) client.Method( func(responseBytes []byte) error { responseChan <- responseBytes return nil }, - func() func(error) { return func(error) {} }, + func() func(error) { return func(err error) { errChan <- err } }, method, params...) select { case err := <-errChan: