Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 20 additions & 38 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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))
}
Expand Down
159 changes: 64 additions & 95 deletions backend/coins/btc/electrum/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are a lot more such references in client.go to replace

@x1ddos x1ddos Dec 11, 2020

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, the initial plan was to make a minimal change but then it escalated a bit :)
ok, I removed all other references except this one.

// 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
}
Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -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

@benma benma Dec 11, 2020

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why nice to have? if not needed, we could drop the response?

should we check the server version and drop the connection if it is newer than 1.4?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we as a client should drop the connection. This is clear from the protocol spec:

The server should use the highest protocol version both support ...
If this is below the value ...
then there is no protocol version in common and the server must close the connection. Otherwise it should send a response appropriate for that protocol version.

In other words, if the server accepts our client version, it means our client is compatible with whatever protocol the server is running. Hence, "nice to have" - we don't make use of it except for logging during the connection.

Maybe in the future, if say there's a protocol v1.5 which has some new method we want to use, but we also want to support v1.4 protocol anyway, then it'll be a different story. For now, the pragmatic in me says that's really just nice-to-have, for logging basically.

}
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().
Expand All @@ -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,
Expand All @@ -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),
Expand All @@ -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,
Expand Down Expand Up @@ -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),
Expand All @@ -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"`
Comment thread
x1ddos marked this conversation as resolved.
}

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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
Loading