Skip to content

Commit c51f12f

Browse files
committed
Implement Electrum connection initialization
The function initializes connection to a remote Electrum server. TCP and SSL types of connections are supported. Most of the servers run electrum protocol 1.4 version, hence we use it for verification. If a server runs another version of the protocol a warning will be issued. A connection to an Electrum server may be dropped if no requests are submitted, hence we implement a keep alive loop that will ping the server to keep the connection alive.
1 parent 3a9548c commit c51f12f

2 files changed

Lines changed: 142 additions & 6 deletions

File tree

pkg/bitcoin/electrum/config.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package electrum
2+
3+
import "time"
4+
5+
const (
6+
// DefaultKeepAliveInterval is a default interval used for Electrum server
7+
// connection keep alive requests.
8+
DefaultKeepAliveInterval = 5 * time.Minute
9+
10+
// DefaultRequestRetryTimeout is a default timeout used for Electrum request
11+
// retries.
12+
DefaultRequestRetryTimeout = 60 * time.Second
13+
)
14+
15+
// Config holds configurable properties.
16+
type Config struct {
17+
// URL to the Electrum server in format: `hostname:port`.
18+
URL string
19+
// Electrum server protocol connection (`TCP` or `SSL`).
20+
Protocol Protocol
21+
// Interval for connection keep alive requests.
22+
// An Electrum server may disconnect clients that have not sent any requests
23+
// for roughly 10 minutes.
24+
KeepAliveInterval time.Duration
25+
// Timeout for Electrum requests retries.
26+
RequestRetryTimeout time.Duration
27+
}

pkg/bitcoin/electrum/electrum.go

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,147 @@
11
package electrum
22

33
import (
4+
"context"
5+
"crypto/tls"
6+
"fmt"
7+
"strings"
8+
"time"
9+
10+
"github.com/checksum0/go-electrum/electrum"
11+
"github.com/ipfs/go-log"
12+
"golang.org/x/exp/slices"
13+
14+
"github.com/keep-network/keep-common/pkg/wrappers"
415
"github.com/keep-network/keep-core/pkg/bitcoin"
516
)
617

7-
type Client struct{}
18+
var (
19+
supportedProtocolVersions = []string{"1.4"}
20+
logger = log.Logger("keep-electrum")
21+
)
22+
23+
// Connection is a handle for interactions with Electrum server.
24+
type Connection struct {
25+
ctx context.Context
26+
client *electrum.Client
27+
requestRetryTimeout time.Duration
28+
}
29+
30+
// Connect initializes handle with provided Config.
31+
func Connect(ctx context.Context, config Config) (bitcoin.Chain, error) {
32+
if config.KeepAliveInterval == 0 {
33+
config.KeepAliveInterval = DefaultKeepAliveInterval
34+
}
35+
if config.RequestRetryTimeout == 0 {
36+
config.RequestRetryTimeout = DefaultRequestRetryTimeout
37+
}
38+
39+
var client *electrum.Client
40+
var err error
41+
switch config.Protocol {
42+
case TCP:
43+
client, err = electrum.NewClientTCP(ctx, config.URL)
44+
case SSL:
45+
// TODO: Implement certificate verification to be able to disable the `InsecureSkipVerify: true` workaround.
46+
tlsConfig := &tls.Config{InsecureSkipVerify: true}
47+
client, err = electrum.NewClientSSL(ctx, config.URL, tlsConfig)
48+
default:
49+
return nil, fmt.Errorf("unsupported protocol: [%s]", config.Protocol)
50+
}
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to initialize electrum client: [%w]", err)
53+
}
54+
55+
// Get the server's details.
56+
err = wrappers.DoWithDefaultRetry(
57+
config.RequestRetryTimeout,
58+
func(ctx context.Context,
59+
) error {
60+
serverVersion, protocolVersion, err := client.ServerVersion(ctx)
61+
if err != nil {
62+
return fmt.Errorf("ServerVersion failed: [%w]", err)
63+
}
64+
logger.Infof(
65+
"connected to electrum server [version: [%s], protocol: [%s]]",
66+
serverVersion,
67+
protocolVersion,
68+
)
69+
70+
// Log a warning if connected to a server running an unsupported protocol version.
71+
if !slices.Contains(supportedProtocolVersions, protocolVersion) {
72+
logger.Warnf(
73+
"electrum server [%s] runs an unsupported protocol version: [%s]; expected one of: [%s]",
74+
config.URL,
75+
protocolVersion,
76+
strings.Join(supportedProtocolVersions, ","),
77+
)
78+
}
79+
80+
return nil
81+
})
82+
if err != nil {
83+
return nil, fmt.Errorf("failed to get server version: [%w]", err)
84+
}
85+
86+
// Keep the connection alive and check the connection health.
87+
go func() {
88+
ticker := time.NewTicker(config.KeepAliveInterval)
89+
90+
for {
91+
select {
92+
case <-ticker.C:
93+
err := wrappers.DoWithDefaultRetry(config.RequestRetryTimeout, client.Ping)
94+
if err != nil {
95+
logger.Errorf(
96+
"failed to ping the electrum server; "+
97+
"please verify health of the electrum server: [%v]",
98+
err,
99+
)
100+
}
101+
case <-ctx.Done():
102+
ticker.Stop()
103+
client.Shutdown()
104+
return
105+
}
106+
}
107+
}()
108+
109+
// TODO: Add reconnects on lost connection.
110+
111+
return &Connection{
112+
ctx: ctx,
113+
client: client,
114+
requestRetryTimeout: config.RequestRetryTimeout,
115+
}, nil
116+
}
8117

9-
func (c *Client) GetTransaction(
118+
func (c *Connection) GetTransaction(
10119
transactionHash bitcoin.Hash,
11120
) (*bitcoin.Transaction, error) {
12121
// TODO: Implementation.
13122
panic("not implemented")
14123
}
15124

16-
func (c *Client) GetTransactionConfirmations(
125+
func (c *Connection) GetTransactionConfirmations(
17126
transactionHash bitcoin.Hash,
18127
) (uint, error) {
19128
// TODO: Implementation.
20129
panic("not implemented")
21130
}
22131

23-
func (c *Client) BroadcastTransaction(
132+
func (c *Connection) BroadcastTransaction(
24133
transaction *bitcoin.Transaction,
25134
) error {
26135
// TODO: Implementation.
27136
panic("not implemented")
28137
}
29138

30-
func (c *Client) GetLatestBlockHeight() (uint, error) {
139+
func (c *Connection) GetLatestBlockHeight() (uint, error) {
31140
// TODO: Implementation.
32141
panic("not implemented")
33142
}
34143

35-
func (c *Client) GetBlockHeader(
144+
func (c *Connection) GetBlockHeader(
36145
blockHeight uint,
37146
) (*bitcoin.BlockHeader, error) {
38147
// TODO: Implementation.

0 commit comments

Comments
 (0)