Skip to content

Commit 07b0834

Browse files
authored
Framework that allows adding additional Quorum features as plugins (#923)
1 parent 4b125de commit 07b0834

File tree

552 files changed

+167785
-19740
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

552 files changed

+167785
-19740
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,4 @@ endif
195195
ifeq (, $(shell which jq))
196196
@echo "Please install jq from https://stedolan.github.io/jq/download"
197197
endif
198-
# QUORUM - END
198+
# QUORUM - END

cmd/geth/config.go

+6
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ func enableWhisper(ctx *cli.Context) bool {
157157
func makeFullNode(ctx *cli.Context) *node.Node {
158158
stack, cfg := makeConfigNode(ctx)
159159

160+
// this must be done first to make sure plugin manager is fully up.
161+
// any fatal at this point is safe
162+
if cfg.Node.Plugins != nil {
163+
utils.RegisterPluginService(stack, &cfg.Node, ctx.Bool(utils.PluginSkipVerifyFlag.Name), ctx.Bool(utils.PluginLocalVerifyFlag.Name), ctx.String(utils.PluginPublicKeyFlag.Name))
164+
}
165+
160166
ethChan := utils.RegisterEthService(stack, &cfg.Eth)
161167

162168
if cfg.Node.IsPermissionEnabled() {

cmd/geth/main.go

+4
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ var (
145145
utils.EmitCheckpointsFlag,
146146
utils.IstanbulRequestTimeoutFlag,
147147
utils.IstanbulBlockPeriodFlag,
148+
utils.PluginSettingsFlag,
149+
utils.PluginSkipVerifyFlag,
150+
utils.PluginLocalVerifyFlag,
151+
utils.PluginPublicKeyFlag,
148152
// End-Quorum
149153
}
150154

cmd/geth/usage.go

+4
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ var AppHelpFlagGroups = []flagGroup{
141141
Name: "QUORUM",
142142
Flags: []cli.Flag{
143143
utils.EnableNodePermissionFlag,
144+
utils.PluginSettingsFlag,
145+
utils.PluginSkipVerifyFlag,
146+
utils.PluginLocalVerifyFlag,
147+
utils.PluginPublicKeyFlag,
144148
},
145149
},
146150
{

cmd/utils/flags.go

+78-1
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@ package utils
1919

2020
import (
2121
"crypto/ecdsa"
22+
"encoding/json"
2223
"fmt"
24+
"io"
2325
"io/ioutil"
2426
"math/big"
27+
"net/url"
2528
"os"
2629
"path/filepath"
2730
"strconv"
2831
"strings"
2932

3033
"github.com/ethereum/go-ethereum/permission"
34+
"github.com/ethereum/go-ethereum/plugin"
3135

3236
"time"
3337

@@ -614,7 +618,23 @@ var (
614618
Name: "permissioned",
615619
Usage: "If enabled, the node will allow only a defined list of nodes to connect",
616620
}
617-
621+
// Plugins settings
622+
PluginSettingsFlag = cli.StringFlag{
623+
Name: "plugins",
624+
Usage: "The URI of configuration which describes plugins being used. E.g.: file:///opt/geth/plugins.json",
625+
}
626+
PluginLocalVerifyFlag = cli.BoolFlag{
627+
Name: "plugins.localverify",
628+
Usage: "If enabled, verify plugin integrity from local file system. This requires plugin signature file and PGP public key file to be available",
629+
}
630+
PluginPublicKeyFlag = cli.StringFlag{
631+
Name: "plugins.publickey",
632+
Usage: fmt.Sprintf("The URI of PGP public key for local plugin verification. E.g.: file:///opt/geth/pubkey.pgp.asc. This flag is only valid if --%s is set (default = file:///<pluginBaseDir>/%s)", PluginLocalVerifyFlag.Name, plugin.DefaultPublicKeyFile),
633+
}
634+
PluginSkipVerifyFlag = cli.BoolFlag{
635+
Name: "plugins.skipverify",
636+
Usage: "If enabled, plugin integrity is NOT verified",
637+
}
618638
// Istanbul settings
619639
IstanbulRequestTimeoutFlag = cli.Uint64Flag{
620640
Name: "istanbul.requesttimeout",
@@ -1053,6 +1073,51 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
10531073
if ctx.GlobalIsSet(NoUSBFlag.Name) {
10541074
cfg.NoUSB = ctx.GlobalBool(NoUSBFlag.Name)
10551075
}
1076+
if err := setPlugins(ctx, cfg); err != nil {
1077+
Fatalf(err.Error())
1078+
}
1079+
}
1080+
1081+
// Quorum
1082+
//
1083+
// Read plugin settings from --plugins flag. Overwrite settings defined in --config if any
1084+
func setPlugins(ctx *cli.Context, cfg *node.Config) error {
1085+
if ctx.GlobalIsSet(PluginSettingsFlag.Name) {
1086+
// validate flag combination
1087+
if ctx.GlobalBool(PluginSkipVerifyFlag.Name) && ctx.GlobalBool(PluginLocalVerifyFlag.Name) {
1088+
return fmt.Errorf("only --%s or --%s must be set", PluginSkipVerifyFlag.Name, PluginLocalVerifyFlag.Name)
1089+
}
1090+
if !ctx.GlobalBool(PluginLocalVerifyFlag.Name) && ctx.GlobalIsSet(PluginPublicKeyFlag.Name) {
1091+
return fmt.Errorf("--%s is required for setting --%s", PluginLocalVerifyFlag.Name, PluginPublicKeyFlag.Name)
1092+
}
1093+
pluginSettingsURL, err := url.Parse(ctx.GlobalString(PluginSettingsFlag.Name))
1094+
if err != nil {
1095+
return fmt.Errorf("plugins: Invalid URL for --%s due to %s", PluginSettingsFlag.Name, err)
1096+
}
1097+
var pluginSettings plugin.Settings
1098+
r, err := urlReader(pluginSettingsURL)
1099+
if err != nil {
1100+
return fmt.Errorf("plugins: unable to create reader due to %s", err)
1101+
}
1102+
defer func() {
1103+
_ = r.Close()
1104+
}()
1105+
if err := json.NewDecoder(r).Decode(&pluginSettings); err != nil {
1106+
return fmt.Errorf("plugins: unable to parse settings due to %s", err)
1107+
}
1108+
pluginSettings.SetDefaults()
1109+
cfg.Plugins = &pluginSettings
1110+
}
1111+
return nil
1112+
}
1113+
1114+
func urlReader(u *url.URL) (io.ReadCloser, error) {
1115+
s := u.Scheme
1116+
switch s {
1117+
case "file":
1118+
return os.Open(filepath.Join(u.Host, u.Path))
1119+
}
1120+
return nil, fmt.Errorf("unsupported scheme %s", s)
10561121
}
10571122

10581123
func setGPO(ctx *cli.Context, cfg *gasprice.Config) {
@@ -1395,6 +1460,18 @@ func RegisterEthStatsService(stack *node.Node, url string) {
13951460

13961461
// Quorum
13971462
//
1463+
// Register plugin manager as a service in geth
1464+
func RegisterPluginService(stack *node.Node, cfg *node.Config, skipVerify bool, localVerify bool, publicKey string) {
1465+
if err := cfg.ResolvePluginBaseDir(); err != nil {
1466+
Fatalf("plugins: unable to resolve plugin base dir due to %s", err)
1467+
}
1468+
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
1469+
return plugin.NewPluginManager(cfg.UserIdent, cfg.Plugins, skipVerify, localVerify, publicKey)
1470+
}); err != nil {
1471+
Fatalf("plugins: Failed to register the Plugins service: %v", err)
1472+
}
1473+
}
1474+
13981475
// Configure smart-contract-based permissioning service
13991476
func RegisterPermissionService(ctx *cli.Context, stack *node.Node) {
14001477
if err := stack.Register(func(sctx *node.ServiceContext) (node.Service, error) {

cmd/utils/flags_test.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package utils
2+
3+
import (
4+
"flag"
5+
"io/ioutil"
6+
"os"
7+
"path"
8+
"testing"
9+
10+
"github.com/ethereum/go-ethereum/node"
11+
"github.com/stretchr/testify/assert"
12+
"gopkg.in/urfave/cli.v1"
13+
)
14+
15+
func TestSetPlugins_whenPluginsNotEnabled(t *testing.T) {
16+
arbitraryNodeConfig := &node.Config{}
17+
arbitraryCLIContext := cli.NewContext(nil, &flag.FlagSet{}, nil)
18+
19+
assert.NoError(t, setPlugins(arbitraryCLIContext, arbitraryNodeConfig))
20+
21+
assert.Nil(t, arbitraryNodeConfig.Plugins)
22+
}
23+
24+
func TestSetPlugins_whenInvalidFlagsCombination(t *testing.T) {
25+
arbitraryNodeConfig := &node.Config{}
26+
fs := &flag.FlagSet{}
27+
fs.String(PluginSettingsFlag.Name, "", "")
28+
fs.Bool(PluginSkipVerifyFlag.Name, true, "")
29+
fs.Bool(PluginLocalVerifyFlag.Name, true, "")
30+
fs.String(PluginPublicKeyFlag.Name, "", "")
31+
arbitraryCLIContext := cli.NewContext(nil, fs, nil)
32+
assert.NoError(t, arbitraryCLIContext.GlobalSet(PluginSettingsFlag.Name, "arbitrary value"))
33+
34+
verifyErrorMessage(t, arbitraryCLIContext, arbitraryNodeConfig, "only --plugins.skipverify or --plugins.localverify must be set")
35+
36+
assert.NoError(t, arbitraryCLIContext.GlobalSet(PluginSkipVerifyFlag.Name, "false"))
37+
assert.NoError(t, arbitraryCLIContext.GlobalSet(PluginLocalVerifyFlag.Name, "false"))
38+
assert.NoError(t, arbitraryCLIContext.GlobalSet(PluginPublicKeyFlag.Name, "arbitry value"))
39+
40+
verifyErrorMessage(t, arbitraryCLIContext, arbitraryNodeConfig, "--plugins.localverify is required for setting --plugins.publickey")
41+
}
42+
43+
func TestSetPlugins_whenInvalidPluginSettingsURL(t *testing.T) {
44+
arbitraryNodeConfig := &node.Config{}
45+
fs := &flag.FlagSet{}
46+
fs.String(PluginSettingsFlag.Name, "", "")
47+
arbitraryCLIContext := cli.NewContext(nil, fs, nil)
48+
assert.NoError(t, arbitraryCLIContext.GlobalSet(PluginSettingsFlag.Name, "arbitrary value"))
49+
50+
verifyErrorMessage(t, arbitraryCLIContext, arbitraryNodeConfig, "plugins: unable to create reader due to unsupported scheme ")
51+
}
52+
53+
func TestSetPlugins_whenTypical(t *testing.T) {
54+
tmpDir, err := ioutil.TempDir("", "q-")
55+
if err != nil {
56+
t.Fatal(err)
57+
}
58+
defer func() {
59+
_ = os.RemoveAll(tmpDir)
60+
}()
61+
arbitraryJSONFile := path.Join(tmpDir, "arbitary.json")
62+
if err := ioutil.WriteFile(arbitraryJSONFile, []byte("{}"), 0644); err != nil {
63+
t.Fatal(err)
64+
}
65+
arbitraryNodeConfig := &node.Config{}
66+
fs := &flag.FlagSet{}
67+
fs.String(PluginSettingsFlag.Name, "", "")
68+
arbitraryCLIContext := cli.NewContext(nil, fs, nil)
69+
assert.NoError(t, arbitraryCLIContext.GlobalSet(PluginSettingsFlag.Name, "file://"+arbitraryJSONFile))
70+
71+
assert.NoError(t, setPlugins(arbitraryCLIContext, arbitraryNodeConfig))
72+
73+
assert.NotNil(t, arbitraryNodeConfig.Plugins)
74+
}
75+
76+
func verifyErrorMessage(t *testing.T, ctx *cli.Context, cfg *node.Config, expectedMsg string) {
77+
err := setPlugins(ctx, cfg)
78+
assert.EqualError(t, err, expectedMsg)
79+
}
+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
title: Internals - Pluggable Architecture - Quorum
2+
3+
## Background
4+
5+
### Go Plugin
6+
`geth` is written in the Go programming language. [Go 1.8 introduced](https://golang.org/doc/go1.8#plugin) a new plugin architecture
7+
which allows for the creation of plugins (via `plugin` build mode) and to use these plugins at runtime (via `plugin` package).
8+
In order to utilize this architecture, there are strict requirements in developing plugins.
9+
10+
By using the network RPC interface, the plugin is independently built and distributed without having to rebuild `geth`.
11+
Especially with gRPC interfaces, plugins can be written in different languages (see our [examples](../PluginDevelopment/#examples)).
12+
This makes it easy for you to build a prototype feature or even a proprietary plugin for your organization's internal use.
13+
14+
We use HashiCorp's [`go-plugin`](https://github.com/hashicorp/go-plugin) library as it fits our asks
15+
and it has been proven in many plugin-based production systems.
16+
17+
### Why we decided to use plugins
18+
19+
There are number of benefits:
20+
21+
- Dynamically-linked binaries (which you get when using plugins) are much smaller than statically compiled binaries.
22+
- We value the ability to isolate failures. E.g.: Quorum client would continue mining/validating even if security plugin has crashed.
23+
- Easily enables support for open source plugins written in languages other than Go.
24+
25+
## Design
26+
27+
```plantuml
28+
skinparam componentStyle uml2
29+
skinparam shadowing false
30+
skinparam backgroundColor transparent
31+
skinparam rectangle {
32+
roundCorner<<component>> 25
33+
}
34+
35+
file "JSON File" as json
36+
file "TOML File" as toml
37+
note left of toml : Standard Ethereum Config
38+
note right of json : Quorum Plugin Settings
39+
40+
node "geth" <<process>> {
41+
rectangle "CLI Flags" as flags
42+
frame "plugin.Settings" as settings {
43+
storage "Plugin1\nDefinition" as pd1
44+
storage "Plugin2\nDefinition" as pd2
45+
storage "Plugin Central\nConnectivity" as pcc
46+
}
47+
48+
json <-down- flags : "via\n""--plugins"""
49+
toml <-down- flags : "via\n""--config"""
50+
flags -down-> settings : populate
51+
52+
interface """node.Service""" as service
53+
rectangle """plugin.PluginManager""" <<geth service>> as pm
54+
note right of pm
55+
registered and managed
56+
as standard ""geth""
57+
service life cycle
58+
end note
59+
60+
pm -up- service
61+
pm -up- settings
62+
63+
card "arbitrary" <<component>> as arbitrary
64+
interface "internal1" as i1
65+
interface "internal2" as i2
66+
interface "internal3" as i3
67+
68+
package "Plugin Interface 1" {
69+
rectangle "Plugin1" <<template>> as p1
70+
rectangle "Gateway1" <<adapter>> as p1gw1
71+
rectangle "Gateway2" <<adapter>> as p1gw2
72+
73+
interface "grpc service interface1A" as grpcI1A
74+
interface "grpc service interface1B" as grpcI1B
75+
76+
rectangle "GRPC Stub Client1" <<grpc client>> as grpcC1
77+
}
78+
79+
package "Plugin Interface 2" {
80+
rectangle "Plugin2" <<template>> as p2
81+
rectangle "Gateway" <<adapter>> as p2gw
82+
83+
interface "grpc service interface2" as grpcI2
84+
85+
rectangle "GRPC Stub Client2" <<grpc client>> as grpcC2
86+
}
87+
88+
pm -- p1
89+
pm -- p2
90+
91+
arbitrary --( i1
92+
arbitrary --( i2
93+
arbitrary --( i3
94+
95+
p1gw1 -- i1
96+
p1gw2 -- i2
97+
p2gw -- i3
98+
99+
p1 -- p1gw1
100+
p1 -- p1gw2
101+
p2 -- p2gw
102+
103+
grpcC1 --( grpcI1A
104+
grpcC1 --( grpcI1B
105+
grpcC2 --( grpcI2
106+
107+
p1gw1 --> grpcC1 : use
108+
p1gw2 --> grpcC1 : use
109+
p2gw --> grpcC2 : use
110+
}
111+
112+
node "Plugin1" <<process>> {
113+
rectangle "Implementation" <<grpc server>> as impl1
114+
}
115+
116+
node "Plugin2" <<process>> {
117+
rectangle "Implementation" <<grpc server>> as impl2
118+
}
119+
120+
impl1 -up- grpcI1A
121+
impl1 -up- grpcI1B
122+
impl2 -up- grpcI2
123+
124+
```
125+
126+
### Discovery
127+
128+
The Quorum client reads the plugin [settings](../Settings) file to determine which plugins are going to be loaded and searches for installed plugins
129+
(`<name>-<version>.zip` files) in the plugin `baseDir` (defaults to `<datadir>/plugins`). If the required plugin doesnt exist in the path, Quorum will attempt to use the configured `plugin central` to download the plugin.
130+
131+
### PluginManager
132+
133+
The `PluginManager` manages the plugins being used inside `geth`. It reads the [configuration](../Settings) and builds a registry of plugins.
134+
`PluginManager` implements the standard `Service` interface in `geth`, hence being embedded into the `geth` service life cycle, i.e.: expose service APIs, start and stop.
135+
The `PluginManager` service is registered as early as possible in the node lifecycle. This is to ensure the node fails fast if an issue is encountered when registering the `PluginManager`, so as not to impact other services.
136+
137+
### Plugin Reloading
138+
139+
The `PluginManager` exposes an API (`admin_reloadPlugin`) that allows reloading a plugin. This attempts to restart the current plugin process.
140+
141+
Any changes to the plugin config after initial node start will be applied when reloading the plugin.
142+
This is demonstrated in the [HelloWorld plugin example](http://localhost:8000/PluggableArchitecture/Overview/#example-helloworld-plugin).

0 commit comments

Comments
 (0)