feat: add bootnode binary (#397)

This commit is contained in:
noot
2023-04-21 18:52:07 +02:00
committed by GitHub
parent dda4cacaea
commit dbcc97f9c1
21 changed files with 527 additions and 187 deletions

86
bootnode/bootnode.go Normal file
View File

@@ -0,0 +1,86 @@
// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
// Package bootnode is responsible for assembling, running and cleanly shutting
// down a swap bootnode.
package bootnode
import (
"context"
"errors"
"fmt"
"math/big"
"net/http"
"github.com/athanorlabs/atomic-swap/net"
"github.com/athanorlabs/atomic-swap/rpc"
"github.com/hashicorp/go-multierror"
logging "github.com/ipfs/go-log"
)
var log = logging.Logger("bootnode")
// Config provides the configuration for a bootnode.
type Config struct {
DataDir string
Bootnodes []string
HostListenIP string
Libp2pPort uint16
Libp2pKeyFile string
RPCPort uint16
EthereumChainID *big.Int
}
// RunBootnode assembles and runs a bootnode instance, blocking until the node is
// shut down. Typically, shutdown happens because a signal handler cancels the
// passed in context, or when the shutdown RPC method is called.
func RunBootnode(ctx context.Context, cfg *Config) error {
host, err := net.NewHost(&net.Config{
Ctx: ctx,
DataDir: cfg.DataDir,
Port: cfg.Libp2pPort,
KeyFile: cfg.Libp2pKeyFile,
Bootnodes: cfg.Bootnodes,
ProtocolID: fmt.Sprintf("%s/%d", net.ProtocolID, cfg.EthereumChainID.Int64()),
ListenIP: cfg.HostListenIP,
IsRelayer: false,
IsBootnodeOnly: true,
})
if err != nil {
return err
}
defer func() {
if hostErr := host.Stop(); hostErr != nil {
err = multierror.Append(err, fmt.Errorf("error shutting down peer-to-peer services: %w", hostErr))
}
}()
if err = host.Start(); err != nil {
return err
}
rpcServer, err := rpc.NewServer(&rpc.Config{
Ctx: ctx,
Address: fmt.Sprintf("127.0.0.1:%d", cfg.RPCPort),
Net: host,
Namespaces: map[string]struct{}{
rpc.DaemonNamespace: {},
rpc.NetNamespace: {},
},
IsBootnodeOnly: true,
})
log.Infof("starting bootnode with data-dir %s", cfg.DataDir)
err = rpcServer.Start()
if errors.Is(err, http.ErrServerClosed) {
// Remove the error for a clean program exit, as ErrServerClosed only
// happens when the server is told to shut down
err = nil
}
// err can get set in defer blocks, so return err or use an empty
// return statement below (not nil)
return err
}

61
cliutil/log.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
package cliutil
import (
"fmt"
logging "github.com/ipfs/go-log"
"github.com/urfave/cli/v2"
)
const (
// FlagLogLevel is the log level flag.
FlagLogLevel = "log-level"
)
// SetLogLevelsFromContext sets the log levels for all packages from the CLI context.
func SetLogLevelsFromContext(c *cli.Context) error {
const (
levelError = "error"
levelWarn = "warn"
levelInfo = "info"
levelDebug = "debug"
)
level := c.String(FlagLogLevel)
switch level {
case levelError, levelWarn, levelInfo, levelDebug:
default:
return fmt.Errorf("invalid log level %q", level)
}
SetLogLevels(level)
return nil
}
// SetLogLevels sets the log levels for all packages.
func SetLogLevels(level string) {
// alphabetically ordered
_ = logging.SetLogLevel("bootnode", level)
_ = logging.SetLogLevel("cmd", level)
_ = logging.SetLogLevel("coins", level)
_ = logging.SetLogLevel("common", level)
_ = logging.SetLogLevel("contracts", level)
_ = logging.SetLogLevel("cmd", level)
_ = logging.SetLogLevel("extethclient", level)
_ = logging.SetLogLevel("ethereum/watcher", level)
_ = logging.SetLogLevel("ethereum/block", level)
_ = logging.SetLogLevel("monero", level)
_ = logging.SetLogLevel("net", level)
_ = logging.SetLogLevel("offers", level)
_ = logging.SetLogLevel("p2pnet", level) // external
_ = logging.SetLogLevel("pricefeed", level)
_ = logging.SetLogLevel("protocol", level)
_ = logging.SetLogLevel("relayer", level) // external and internal
_ = logging.SetLogLevel("rpc", level)
_ = logging.SetLogLevel("txsender", level)
_ = logging.SetLogLevel("xmrmaker", level)
_ = logging.SetLogLevel("xmrtaker", level)
}

View File

@@ -1,7 +1,7 @@
// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
package main
package cliutil
import (
"context"
@@ -9,9 +9,12 @@ import (
"os/signal"
"syscall"
"time"
logging "github.com/ipfs/go-log"
)
func signalHandler(ctx context.Context, cancel context.CancelFunc) {
// SignalHandler handles OS signals and shuts down the program if necessary.
func SignalHandler(ctx context.Context, cancel context.CancelFunc, log *logging.ZapEventLogger) {
sigc := make(chan os.Signal, 1)
signal.Ignore(syscall.SIGHUP)
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)

View File

@@ -1,15 +1,17 @@
// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
package main
package cliutil
import (
"context"
"testing"
logging "github.com/ipfs/go-log"
)
func TestDaemon_signalHandler(_ *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go signalHandler(ctx, cancel)
go SignalHandler(ctx, cancel, logging.Logger("test"))
}

173
cmd/bootnode/main.go Normal file
View File

@@ -0,0 +1,173 @@
// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
// Package main provides the entrypoint of the bootnode executable,
// a node that is only used to bootstrap the p2p network and does not run
// any swap services.
package main
import (
"context"
"fmt"
"os"
"path"
"github.com/athanorlabs/atomic-swap/bootnode"
"github.com/athanorlabs/atomic-swap/cliutil"
"github.com/athanorlabs/atomic-swap/common"
logging "github.com/ipfs/go-log"
"github.com/urfave/cli/v2"
)
const (
defaultLibp2pPort = 9909
defaultRPCPort = common.DefaultSwapdPort
flagDataDir = "data-dir"
flagLibp2pKey = "libp2p-key"
flagLibp2pPort = "libp2p-port"
flagBootnodes = "bootnodes"
flagRPCPort = "rpc-port"
flagEnv = "env"
)
var log = logging.Logger("cmd")
func cliApp() *cli.App {
return &cli.App{
Name: "bootnode",
Usage: "A bootnode for the atomic swap p2p network.",
Version: cliutil.GetVersion(),
Action: runBootnode,
EnableBashCompletion: true,
Suggest: true,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagDataDir,
Usage: "Path to store swap artifacts",
Value: "{HOME}/.atomicswap/{ENV}/bootnode", // For --help only, actual default replaces variables
},
&cli.StringFlag{
Name: flagLibp2pKey,
Usage: "libp2p private key",
Value: fmt.Sprintf("{DATA_DIR}/%s", common.DefaultLibp2pKeyFileName),
},
&cli.UintFlag{
Name: flagLibp2pPort,
Usage: "libp2p port to listen on",
Value: defaultLibp2pPort,
},
&cli.StringSliceFlag{
Name: flagBootnodes,
Aliases: []string{"bn"},
Usage: "libp2p bootnode, comma separated if passing multiple to a single flag",
EnvVars: []string{"SWAPD_BOOTNODES"},
},
&cli.UintFlag{
Name: flagRPCPort,
Usage: "Port for the bootnode RPC server to run on",
Value: defaultRPCPort,
},
&cli.StringFlag{
Name: flagEnv,
Usage: "Environment to use: one of mainnet, stagenet, or dev",
Value: "dev",
},
&cli.StringFlag{
Name: cliutil.FlagLogLevel,
Usage: "Set log level: one of [error|warn|info|debug]",
Value: "info",
},
},
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go cliutil.SignalHandler(ctx, cancel, log)
err := cliApp().RunContext(ctx, os.Args)
if err != nil {
log.Fatal(err)
}
}
func runBootnode(c *cli.Context) error {
// Fail if any non-flag arguments were passed
if c.Args().Present() {
return fmt.Errorf("unknown command %q", c.Args().First())
}
if err := cliutil.SetLogLevelsFromContext(c); err != nil {
return err
}
config, err := getEnvConfig(c)
if err != nil {
return err
}
libp2pKeyFile := config.LibP2PKeyFile()
if c.IsSet(flagLibp2pKey) {
libp2pKeyFile = c.String(flagLibp2pKey)
if libp2pKeyFile == "" {
return errFlagValueEmpty(flagLibp2pKey)
}
}
if libp2pKeyFile == "" {
libp2pKeyFile = path.Join(config.DataDir, common.DefaultLibp2pKeyFileName)
}
libp2pPort := uint16(c.Uint(flagLibp2pPort))
hostListenIP := "0.0.0.0"
if config.Env == common.Development {
hostListenIP = "127.0.0.1"
}
rpcPort := uint16(c.Uint(flagRPCPort))
return bootnode.RunBootnode(c.Context, &bootnode.Config{
DataDir: config.DataDir,
Bootnodes: config.Bootnodes,
HostListenIP: hostListenIP,
Libp2pPort: libp2pPort,
Libp2pKeyFile: libp2pKeyFile,
RPCPort: rpcPort,
EthereumChainID: config.EthereumChainID,
})
}
func getEnvConfig(c *cli.Context) (*common.Config, error) {
env, err := common.NewEnv(c.String(flagEnv))
if err != nil {
return nil, err
}
conf := common.ConfigDefaultsForEnv(env)
// cfg.DataDir already has a default set, so only override if the user explicitly set the flag
if c.IsSet(flagDataDir) {
conf.DataDir = c.String(flagDataDir) // override the value derived from `flagEnv`
if conf.DataDir == "" {
return nil, errFlagValueEmpty(flagDataDir)
}
}
conf.DataDir = path.Join(conf.DataDir, "bootnode")
if err = common.MakeDir(conf.DataDir); err != nil {
return nil, err
}
if c.IsSet(flagBootnodes) {
conf.Bootnodes = cliutil.ExpandBootnodes(c.StringSlice(flagBootnodes))
}
return conf, nil
}
func errFlagValueEmpty(flag string) error {
return fmt.Errorf("flag %q requires a non-empty value", flag)
}

View File

@@ -77,7 +77,7 @@ const (
flagForwarderAddress = "forwarder-address"
flagNoTransferBack = "no-transfer-back"
flagLogLevel = "log-level"
flagLogLevel = cliutil.FlagLogLevel
flagProfile = "profile"
)
@@ -217,7 +217,7 @@ func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go signalHandler(ctx, cancel)
go cliutil.SignalHandler(ctx, cancel, log)
err := cliApp().RunContext(ctx, os.Args)
if err != nil {
@@ -225,55 +225,13 @@ func main() {
}
}
func setLogLevelsFromContext(c *cli.Context) error {
const (
levelError = "error"
levelWarn = "warn"
levelInfo = "info"
levelDebug = "debug"
)
level := c.String(flagLogLevel)
switch level {
case levelError, levelWarn, levelInfo, levelDebug:
default:
return fmt.Errorf("invalid log level %q", level)
}
setLogLevels(level)
return nil
}
func setLogLevels(level string) {
// alphabetically ordered
_ = logging.SetLogLevel("cmd", level)
_ = logging.SetLogLevel("coins", level)
_ = logging.SetLogLevel("common", level)
_ = logging.SetLogLevel("contracts", level)
_ = logging.SetLogLevel("cmd", level)
_ = logging.SetLogLevel("extethclient", level)
_ = logging.SetLogLevel("ethereum/watcher", level)
_ = logging.SetLogLevel("ethereum/block", level)
_ = logging.SetLogLevel("monero", level)
_ = logging.SetLogLevel("net", level)
_ = logging.SetLogLevel("offers", level)
_ = logging.SetLogLevel("p2pnet", level) // external
_ = logging.SetLogLevel("pricefeed", level)
_ = logging.SetLogLevel("protocol", level)
_ = logging.SetLogLevel("relayer", level) // external and internal
_ = logging.SetLogLevel("rpc", level)
_ = logging.SetLogLevel("txsender", level)
_ = logging.SetLogLevel("xmrmaker", level)
_ = logging.SetLogLevel("xmrtaker", level)
}
func runDaemon(c *cli.Context) error {
// Fail if any non-flag arguments were passed
if c.Args().Present() {
return fmt.Errorf("unknown command %q", c.Args().First())
}
if err := setLogLevelsFromContext(c); err != nil {
if err := cliutil.SetLogLevelsFromContext(c); err != nil {
return err
}

View File

@@ -86,41 +86,8 @@ func main() {
}
}
func setLogLevels(c *cli.Context) error {
const (
levelError = "error"
levelWarn = "warn"
levelInfo = "info"
levelDebug = "debug"
)
_ = logging.SetLogLevel("cmd", levelInfo)
level := c.String(flagLogLevel)
if level == "" {
level = levelInfo
}
switch level {
case levelError, levelWarn, levelInfo, levelDebug:
default:
return fmt.Errorf("invalid log level")
}
_ = logging.SetLogLevel("xmrtaker", level)
_ = logging.SetLogLevel("xmrmaker", level)
_ = logging.SetLogLevel("common", level)
_ = logging.SetLogLevel("net", level)
_ = logging.SetLogLevel("rpc", level)
_ = logging.SetLogLevel("rpcclient", level)
_ = logging.SetLogLevel("wsclient", level)
_ = logging.SetLogLevel("monero", level)
_ = logging.SetLogLevel("contracts", level)
return nil
}
func runTester(c *cli.Context) error {
err := setLogLevels(c)
err := cliutil.SetLogLevelsFromContext(c)
if err != nil {
return err
}

View File

@@ -4,6 +4,7 @@
package common
import (
"math/big"
"os"
"path"
"time"
@@ -34,6 +35,7 @@ type MoneroNode struct {
// Config contains constants that are defaults for various environments
type Config struct {
Env Environment
EthereumChainID *big.Int
DataDir string
MoneroNodes []*MoneroNode
SwapCreatorAddr ethcommon.Address
@@ -44,8 +46,9 @@ type Config struct {
// MainnetConfig is the mainnet ethereum and monero configuration
func MainnetConfig() *Config {
return &Config{
Env: Mainnet,
DataDir: path.Join(baseDir, "mainnet"),
Env: Mainnet,
EthereumChainID: big.NewInt(MainnetChainID),
DataDir: path.Join(baseDir, "mainnet"),
MoneroNodes: []*MoneroNode{
{
Host: "node.sethforprivacy.com",
@@ -74,8 +77,9 @@ func MainnetConfig() *Config {
// StagenetConfig is the monero stagenet and ethereum Sepolia configuration
func StagenetConfig() *Config {
return &Config{
Env: Stagenet,
DataDir: path.Join(baseDir, "stagenet"),
Env: Stagenet,
EthereumChainID: big.NewInt(SepoliaChainID),
DataDir: path.Join(baseDir, "stagenet"),
MoneroNodes: []*MoneroNode{
{
Host: "node.sethforprivacy.com",
@@ -108,8 +112,9 @@ func StagenetConfig() *Config {
// DevelopmentConfig is the monero and ethereum development environment configuration
func DevelopmentConfig() *Config {
return &Config{
Env: Development,
DataDir: path.Join(baseDir, "dev"),
Env: Development,
EthereumChainID: big.NewInt(1337),
DataDir: path.Join(baseDir, "dev"),
MoneroNodes: []*MoneroNode{
{
Host: "127.0.0.1",

View File

@@ -155,6 +155,7 @@ func RunSwapDaemon(ctx context.Context, conf *SwapdConfig) (err error) {
XMRMaker: xmrMaker,
ProtocolBackend: swapBackend,
RecoveryDB: sdb.RecoveryDB(),
Namespaces: rpc.AllNamespaces(),
})
log.Infof("starting swapd with data-dir %s", conf.EnvConf.DataDir)

View File

@@ -17,10 +17,10 @@ import (
ethcommon "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
logging "github.com/ipfs/go-log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/cliutil"
"github.com/athanorlabs/atomic-swap/coins"
"github.com/athanorlabs/atomic-swap/common/types"
"github.com/athanorlabs/atomic-swap/ethereum/block"
@@ -39,27 +39,7 @@ const (
)
func init() {
// alphabetically ordered
level := "debug"
_ = logging.SetLogLevel("cmd", level)
_ = logging.SetLogLevel("coins", level)
_ = logging.SetLogLevel("common", level)
_ = logging.SetLogLevel("contracts", level)
_ = logging.SetLogLevel("cmd", level)
_ = logging.SetLogLevel("extethclient", level)
_ = logging.SetLogLevel("ethereum/watcher", level)
_ = logging.SetLogLevel("ethereum/block", level)
_ = logging.SetLogLevel("monero", level)
_ = logging.SetLogLevel("net", level)
_ = logging.SetLogLevel("offers", level)
_ = logging.SetLogLevel("p2pnet", level) // external
_ = logging.SetLogLevel("pricefeed", level)
_ = logging.SetLogLevel("protocol", level)
_ = logging.SetLogLevel("relayer", level) // external and internal
_ = logging.SetLogLevel("rpc", level)
_ = logging.SetLogLevel("txsender", level)
_ = logging.SetLogLevel("xmrmaker", level)
_ = logging.SetLogLevel("xmrtaker", level)
cliutil.SetLogLevels("debug")
}
func privKeyToAddr(privKey *ecdsa.PrivateKey) ethcommon.Address {

29
docs/bootnode.md Normal file
View File

@@ -0,0 +1,29 @@
# Bootnode
The swap daemon uses a p2p network to discover offer-makers to do a swap with, and also the
run the actual swap protocol. A node must know addresses of nodes already in the network to
join the network. These nodes are often publicly posted and referred to as bootnodes.
Bootnodes act as an entry-point into the p2p network.
This repo comes with a `bootnode` program that runs only the p2p components of a swap node,
and thus can be used as a lightweight bootnode.
## Requirements
- see [build instructions](./build.md) for installation requirements.
## Build and run
To build and run the bootnode binary:
```bash
make build-all
./bin/bootnode --env ENVIRONMENT
```
`ENVIRONMENT` is one of `mainnet`, `stagenet`, or `dev`.
To get the p2p addresses of the bootnode:
```bash
./bin/swapcli addresses
```
You can then distribute these addresses for other swap nodes to connect to.

View File

@@ -64,18 +64,14 @@ Stores information on a swap when it reaches the stage where ethereum is locked.
Only written when `--deploy` is passed to swapd. This file stores the address
that the contract was deployed to along with other data.
## Relayer default file locations
## Bootnode default file locations
### {DATA_DIR}/relayer
### {DATA_DIR}/bootnode
By default, all relayer-related files will be placed in the `relayer` directory within the data dir.
By default, all bootnode-related files will be placed in the `bootnode` directory within the data dir.
### {DATA_DIR}/relayer/eth.key
### {DATA_DIR}/bootnode/net.key
The location of the Ethereum private key used by the relayer to submit transactions. Fees received by the relayer will also go into this account. Alternate locations can be configured with `--ethereum-privkey`. If the file does not exist, the relayer will error on startup.
### {DATA_DIR}/relayer/net.key
The private key to the relayer's libp2p identity. If the file does not exist, a new
The private key to the bootnode's libp2p identity. If the file does not exist, a new
random key will be generated and placed in this location. Alternate locations can be
configured with `--libp2p-key`. It does not necessarily need to be a different key than that used by swapd.

View File

@@ -8,7 +8,7 @@ support most 64-bit Linux distributions, macOS, and WSL on Windows both with X86
and ARM processors.
#### Installed Dependencies for Building/Testing
- go 1.19+ (see [build instructions](./build.md) to download Go.)
- go 1.20+ (see [build instructions](./build.md) to download Go.)
- node/npm (to install ganache, see suggestions after list)
- ganache (can be installed with `npm install --location=global ganache`)
- jq, curl, bzip2, realpath

View File

@@ -8,6 +8,7 @@ import (
)
var (
errBootnodeCannotRelay = errors.New("bootnode cannot be a relayer")
errNilHandler = errors.New("handler is nil")
errNoOngoingSwap = errors.New("no swap currently happening")
errSwapAlreadyInProgress = errors.New("already have ongoing swap")

View File

@@ -62,6 +62,9 @@ type Host struct {
h P2pHost
isRelayer bool
// set to true if the node is a bootnode-only node
isBootnode bool
makerHandler MakerHandler
relayHandler RelayHandler
@@ -72,25 +75,31 @@ type Host struct {
// Config holds the initialization parameters for the NewHost constructor.
type Config struct {
Ctx context.Context
DataDir string
Port uint16
KeyFile string
Bootnodes []string
ProtocolID string
ListenIP string
IsRelayer bool
Ctx context.Context
DataDir string
Port uint16
KeyFile string
Bootnodes []string
ProtocolID string
ListenIP string
IsRelayer bool
IsBootnodeOnly bool
}
// NewHost returns a new Host.
// The host implemented in this package is swap-specific; ie. it supports swap-specific
// messages (initiate and query).
func NewHost(cfg *Config) (*Host, error) {
if cfg.IsBootnodeOnly && cfg.IsRelayer {
return nil, errBootnodeCannotRelay
}
h := &Host{
ctx: cfg.Ctx,
h: nil, // set below
isRelayer: cfg.IsRelayer,
swaps: make(map[types.Hash]*swap),
ctx: cfg.Ctx,
h: nil, // set below
isRelayer: cfg.IsRelayer,
isBootnode: cfg.IsBootnodeOnly,
swaps: make(map[types.Hash]*swap),
}
var err error
@@ -108,17 +117,18 @@ func NewHost(cfg *Config) (*Host, error) {
return nil, err
}
log.Debugf("using base protocol %s", cfg.ProtocolID)
return h, nil
}
func (h *Host) advertisedNamespaces() []string {
provides := []string{""}
if len(h.makerHandler.GetOffers()) > 0 {
if !h.isBootnode && len(h.makerHandler.GetOffers()) > 0 {
provides = append(provides, string(coins.ProvidesXMR))
}
if h.isRelayer {
if !h.isBootnode && h.isRelayer {
provides = append(provides, RelayerProvidesStr)
}
@@ -138,7 +148,7 @@ func (h *Host) SetHandlers(makerHandler MakerHandler, relayHandler RelayHandler)
// Start starts the bootstrap and discovery process.
func (h *Host) Start() error {
if h.makerHandler == nil || h.relayHandler == nil {
if (h.makerHandler == nil || h.relayHandler == nil) && !h.isBootnode {
return errNilHandler
}

View File

@@ -9,9 +9,11 @@ import (
var (
// net_ errors
errNoOfferWithID = errors.New("peer does not have offer with given ID")
errNoOfferWithID = errors.New("peer does not have offer with given ID")
errUnsupportedForBootnode = errors.New("unsupported for bootnode")
// ws errors
errUnimplemented = errors.New("unimplemented")
errInvalidMethod = errors.New("invalid method")
errUnimplemented = errors.New("unimplemented")
errInvalidMethod = errors.New("invalid method")
errNamespaceNotEnabled = errors.New("namespace not enabled")
)

View File

@@ -34,19 +34,21 @@ type Net interface {
// NetService is the RPC service prefixed by net_.
type NetService struct {
net Net
xmrtaker XMRTaker
xmrmaker XMRMaker
sm SwapManager
net Net
xmrtaker XMRTaker
xmrmaker XMRMaker
sm SwapManager
isBootnode bool
}
// NewNetService ...
func NewNetService(net Net, xmrtaker XMRTaker, xmrmaker XMRMaker, sm SwapManager) *NetService {
func NewNetService(net Net, xmrtaker XMRTaker, xmrmaker XMRMaker, sm SwapManager, isBootnode bool) *NetService {
return &NetService{
net: net,
xmrtaker: xmrtaker,
xmrmaker: xmrmaker,
sm: sm,
net: net,
xmrtaker: xmrtaker,
xmrmaker: xmrmaker,
sm: sm,
isBootnode: isBootnode,
}
}
@@ -72,6 +74,10 @@ func (s *NetService) Peers(_ *http.Request, _ *interface{}, resp *rpctypes.Peers
// QueryAll discovers peers who provide a certain coin and queries all of them for their current offers.
func (s *NetService) QueryAll(_ *http.Request, req *rpctypes.QueryAllRequest, resp *rpctypes.QueryAllResponse) error {
if s.isBootnode {
return errUnsupportedForBootnode
}
peerIDs, err := s.discover(req)
if err != nil {
return err
@@ -126,8 +132,14 @@ func (s *NetService) Discover(_ *http.Request, req *rpctypes.DiscoverRequest, re
}
// QueryPeer queries a peer for the coins they provide, their maximum amounts, and desired exchange rate.
func (s *NetService) QueryPeer(_ *http.Request, req *rpctypes.QueryPeerRequest,
resp *rpctypes.QueryPeerResponse) error {
func (s *NetService) QueryPeer(
_ *http.Request,
req *rpctypes.QueryPeerRequest,
resp *rpctypes.QueryPeerResponse,
) error {
if s.isBootnode {
return errUnsupportedForBootnode
}
msg, err := s.net.Query(req.PeerID)
if err != nil {
@@ -144,6 +156,10 @@ func (s *NetService) TakeOffer(
req *rpctypes.TakeOfferRequest,
_ *interface{},
) error {
if s.isBootnode {
return errUnsupportedForBootnode
}
_, err := s.takeOffer(req.PeerID, req.OfferID, req.ProvidesAmount)
if err != nil {
return err
@@ -208,6 +224,9 @@ func (s *NetService) TakeOfferSync(
req *rpctypes.TakeOfferRequest,
resp *TakeOfferSyncResponse,
) error {
if s.isBootnode {
return errUnsupportedForBootnode
}
if _, err := s.takeOffer(req.PeerID, req.OfferID, req.ProvidesAmount); err != nil {
return err
@@ -240,6 +259,10 @@ func (s *NetService) MakeOffer(
req *rpctypes.MakeOfferRequest,
resp *rpctypes.MakeOfferResponse,
) error {
if s.isBootnode {
return errUnsupportedForBootnode
}
offerResp, _, err := s.makeOffer(req)
if err != nil {
return err

View File

@@ -14,7 +14,7 @@ import (
)
func TestNet_Discover(t *testing.T) {
ns := NewNetService(new(mockNet), new(mockXMRTaker), nil, new(mockSwapManager))
ns := NewNetService(new(mockNet), new(mockXMRTaker), nil, new(mockSwapManager), false)
req := &rpctypes.DiscoverRequest{
Provides: "",
@@ -28,7 +28,7 @@ func TestNet_Discover(t *testing.T) {
}
func TestNet_Query(t *testing.T) {
ns := NewNetService(new(mockNet), new(mockXMRTaker), nil, new(mockSwapManager))
ns := NewNetService(new(mockNet), new(mockXMRTaker), nil, new(mockSwapManager), false)
req := &rpctypes.QueryPeerRequest{
PeerID: "12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5",
@@ -42,7 +42,7 @@ func TestNet_Query(t *testing.T) {
}
func TestNet_TakeOffer(t *testing.T) {
ns := NewNetService(new(mockNet), new(mockXMRTaker), nil, new(mockSwapManager))
ns := NewNetService(new(mockNet), new(mockXMRTaker), nil, new(mockSwapManager), false)
req := &rpctypes.TakeOfferRequest{
PeerID: "12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5",
@@ -55,7 +55,7 @@ func TestNet_TakeOffer(t *testing.T) {
}
func TestNet_TakeOfferSync(t *testing.T) {
ns := NewNetService(new(mockNet), new(mockXMRTaker), nil, new(mockSwapManager))
ns := NewNetService(new(mockNet), new(mockXMRTaker), nil, new(mockSwapManager), false)
req := &rpctypes.TakeOfferRequest{
PeerID: "12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5",

View File

@@ -32,6 +32,14 @@ import (
"github.com/athanorlabs/atomic-swap/protocol/txsender"
)
const (
DaemonNamespace = "daemon" //nolint:revive
DatabaseNamespace = "database" //nolint:revive
NetNamespace = "net" //nolint:revive
PersonalName = "personal" //nolint:revive
SwapNamespace = "swap" //nolint:revive
)
var log = logging.Logger("rpc")
// Server represents the JSON-RPC server
@@ -50,6 +58,19 @@ type Config struct {
XMRMaker XMRMaker
ProtocolBackend ProtocolBackend
RecoveryDB RecoveryDB
Namespaces map[string]struct{}
IsBootnodeOnly bool
}
// AllNamespaces returns a map with all RPC namespaces set for usage in the config.
func AllNamespaces() map[string]struct{} {
return map[string]struct{}{
DaemonNamespace: {},
DatabaseNamespace: {},
NetNamespace: {},
PersonalName: {},
SwapNamespace: {},
}
}
// NewServer ...
@@ -57,39 +78,51 @@ func NewServer(cfg *Config) (*Server, error) {
rpcServer := rpc.NewServer()
rpcServer.RegisterCodec(NewCodec(), "application/json")
ns := NewNetService(cfg.Net, cfg.XMRTaker, cfg.XMRMaker, cfg.ProtocolBackend.SwapManager())
if err := rpcServer.RegisterService(ns, "net"); err != nil {
serverCtx, serverCancel := context.WithCancel(cfg.Ctx)
err := rpcServer.RegisterService(NewDaemonService(serverCancel, cfg.ProtocolBackend), "daemon")
if err != nil {
return nil, err
}
serverCtx, serverCancel := context.WithCancel(cfg.Ctx)
var swapManager swap.Manager
if cfg.ProtocolBackend != nil {
swapManager = cfg.ProtocolBackend.SwapManager()
}
err := rpcServer.RegisterService(NewPersonalService(serverCtx, cfg.XMRMaker, cfg.ProtocolBackend), "personal")
var netService *NetService
for ns := range cfg.Namespaces {
switch ns {
case DaemonNamespace:
continue
case DatabaseNamespace:
err = rpcServer.RegisterService(NewDatabaseService(cfg.RecoveryDB), DatabaseNamespace)
case NetNamespace:
netService = NewNetService(cfg.Net, cfg.XMRTaker, cfg.XMRMaker, swapManager, cfg.IsBootnodeOnly)
err = rpcServer.RegisterService(netService, NetNamespace)
case PersonalName:
err = rpcServer.RegisterService(NewPersonalService(serverCtx, cfg.XMRMaker, cfg.ProtocolBackend), PersonalName)
case SwapNamespace:
err = rpcServer.RegisterService(
NewSwapService(
serverCtx,
swapManager,
cfg.XMRTaker,
cfg.XMRMaker,
cfg.Net,
cfg.ProtocolBackend,
),
SwapNamespace,
)
default:
err = fmt.Errorf("unknown namespace %s", ns)
}
}
if err != nil {
serverCancel()
return nil, err
}
swapService := NewSwapService(
serverCtx,
cfg.ProtocolBackend.SwapManager(),
cfg.XMRTaker,
cfg.XMRMaker,
cfg.Net,
cfg.ProtocolBackend,
)
if err = rpcServer.RegisterService(swapService, "swap"); err != nil {
serverCancel()
return nil, err
}
databaseService := NewDatabaseService(cfg.RecoveryDB)
if err = rpcServer.RegisterService(databaseService, "database"); err != nil {
serverCancel()
return nil, err
}
wsServer := newWsServer(serverCtx, cfg.ProtocolBackend.SwapManager(), ns, cfg.ProtocolBackend, cfg.XMRTaker)
wsServer := newWsServer(serverCtx, swapManager, netService, cfg.ProtocolBackend, cfg.XMRTaker)
lc := net.ListenConfig{}
ln, err := lc.Listen(serverCtx, "tcp", cfg.Address)
@@ -114,18 +147,11 @@ func NewServer(cfg *Config) (*Server, error) {
},
}
s := &Server{
return &Server{
ctx: serverCtx,
listener: ln,
httpServer: server,
}
if err = rpcServer.RegisterService(NewDaemonService(serverCancel, cfg.ProtocolBackend), "daemon"); err != nil {
serverCancel()
return nil, err
}
return s, nil
}, nil
}
// HttpURL returns the URL used for HTTP requests

View File

@@ -92,6 +92,10 @@ func (s *wsServer) handleRequest(conn *websocket.Conn, req *rpctypes.Request) er
case rpctypes.SubscribeNewPeer:
return errUnimplemented
case rpctypes.NetDiscover:
if s.ns == nil {
return errNamespaceNotEnabled
}
params := new(rpctypes.DiscoverRequest)
if err := vjson.UnmarshalStruct(req.Params, params); err != nil {
return fmt.Errorf("failed to unmarshal parameters: %w", err)
@@ -105,6 +109,10 @@ func (s *wsServer) handleRequest(conn *websocket.Conn, req *rpctypes.Request) er
return writeResponse(conn, resp)
case rpctypes.NetQueryPeer:
if s.ns == nil {
return errNamespaceNotEnabled
}
params := new(rpctypes.QueryPeerRequest)
if err := vjson.UnmarshalStruct(req.Params, params); err != nil {
return fmt.Errorf("failed to unmarshal parameters: %w", err)
@@ -125,6 +133,10 @@ func (s *wsServer) handleRequest(conn *websocket.Conn, req *rpctypes.Request) er
return s.subscribeSwapStatus(s.ctx, conn, params.OfferID)
case rpctypes.SubscribeTakeOffer:
if s.ns == nil {
return errNamespaceNotEnabled
}
params := new(rpctypes.TakeOfferRequest)
if err := vjson.UnmarshalStruct(req.Params, params); err != nil {
return fmt.Errorf("failed to unmarshal parameters: %w", err)
@@ -137,6 +149,10 @@ func (s *wsServer) handleRequest(conn *websocket.Conn, req *rpctypes.Request) er
return s.subscribeTakeOffer(s.ctx, conn, ch)
case rpctypes.SubscribeMakeOffer:
if s.ns == nil {
return errNamespaceNotEnabled
}
params := new(rpctypes.MakeOfferRequest)
if err := vjson.UnmarshalStruct(req.Params, params); err != nil {
return fmt.Errorf("failed to unmarshal parameters: %w", err)

View File

@@ -35,6 +35,7 @@ func newServer(t *testing.T) *Server {
ProtocolBackend: newMockProtocolBackend(),
XMRTaker: new(mockXMRTaker),
XMRMaker: new(mockXMRMaker),
Namespaces: AllNamespaces(),
}
s, err := NewServer(cfg)