diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 9f9e693ba4..c6a184cd32 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -18,6 +18,7 @@ package main import ( "bufio" + "crypto/tls" "errors" "fmt" "math/big" @@ -34,9 +35,13 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/permission/core" "github.com/ethereum/go-ethereum/private" "github.com/ethereum/go-ethereum/private/engine" + "github.com/ethereum/go-ethereum/qlight" "github.com/naoina/toml" "gopkg.in/urfave/cli.v1" ) @@ -135,6 +140,8 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { // Apply flags. utils.SetNodeConfig(ctx, &cfg.Node) + utils.SetQLightConfig(ctx, &cfg.Node, &cfg.Eth) + stack, err := node.New(&cfg.Node) if err != nil { utils.Fatalf("Failed to create the protocol stack: %v", err) @@ -144,10 +151,74 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name) } applyMetricConfig(ctx, &cfg) + if cfg.Eth.QuorumLightServer { + p2p.SetQLightTLSConfig(readQLightServerTLSConfig(ctx)) + // permissioning for the qlight P2P server + stack.QServer().SetNewTransportFunc(p2p.NewQlightServerTransport) + if ctx.GlobalIsSet(utils.QuorumLightServerP2PPermissioningFlag.Name) { + prefix := "qlight" + if ctx.GlobalIsSet(utils.QuorumLightServerP2PPermissioningPrefixFlag.Name) { + prefix = ctx.GlobalString(utils.QuorumLightServerP2PPermissioningPrefixFlag.Name) + } + fbp := core.NewFileBasedPermissoningWithPrefix(prefix) + stack.QServer().SetIsNodePermissioned(fbp.IsNodePermissionedEnode) + } + } + if cfg.Eth.QuorumLightClient.Enabled() { + p2p.SetQLightTLSConfig(readQLightClientTLSConfig(ctx)) + stack.Server().SetNewTransportFunc(p2p.NewQlightClientTransport) + } return stack, cfg } +func readQLightClientTLSConfig(ctx *cli.Context) *tls.Config { + if !ctx.GlobalIsSet(utils.QuorumLightTLSFlag.Name) { + return nil + } + if !ctx.GlobalIsSet(utils.QuorumLightTLSCACertsFlag.Name) { + utils.Fatalf("QLight tls flag is set but no client certificate authorities has been provided") + } + tlsConfig, err := qlight.NewTLSConfig(&qlight.TLSConfig{ + CACertFileName: ctx.GlobalString(utils.QuorumLightTLSCACertsFlag.Name), + CertFileName: ctx.GlobalString(utils.QuorumLightTLSCertFlag.Name), + KeyFileName: ctx.GlobalString(utils.QuorumLightTLSKeyFlag.Name), + ServerName: enode.MustParse(ctx.GlobalString(utils.QuorumLightClientServerNodeFlag.Name)).IP().String(), + CipherSuites: ctx.GlobalString(utils.QuorumLightTLSCipherSuitesFlag.Name), + }) + + if err != nil { + utils.Fatalf("Unable to load the specified tls configuration: %v", err) + } + return tlsConfig +} + +func readQLightServerTLSConfig(ctx *cli.Context) *tls.Config { + if !ctx.GlobalIsSet(utils.QuorumLightTLSFlag.Name) { + return nil + } + if !ctx.GlobalIsSet(utils.QuorumLightTLSCertFlag.Name) { + utils.Fatalf("QLight TLS is enabled but no server certificate has been provided") + } + if !ctx.GlobalIsSet(utils.QuorumLightTLSKeyFlag.Name) { + utils.Fatalf("QLight TLS is enabled but no server key has been provided") + } + + tlsConfig, err := qlight.NewTLSConfig(&qlight.TLSConfig{ + CertFileName: ctx.GlobalString(utils.QuorumLightTLSCertFlag.Name), + KeyFileName: ctx.GlobalString(utils.QuorumLightTLSKeyFlag.Name), + ClientCACertFileName: ctx.GlobalString(utils.QuorumLightTLSCACertsFlag.Name), + ClientAuth: ctx.GlobalInt(utils.QuorumLightTLSClientAuthFlag.Name), + CipherSuites: ctx.GlobalString(utils.QuorumLightTLSCipherSuitesFlag.Name), + }) + + if err != nil { + utils.Fatalf("QLight TLS - unable to read server tls configuration: %v", err) + } + + return tlsConfig +} + // makeFullNode loads geth configuration and creates the Ethereum backend. func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { stack, cfg := makeConfigNode(ctx) @@ -174,7 +245,7 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { utils.RegisterPermissionService(stack, ctx.Bool(utils.RaftDNSEnabledFlag.Name), backend.ChainConfig().ChainID) } - if ctx.GlobalBool(utils.RaftModeFlag.Name) { + if ctx.GlobalBool(utils.RaftModeFlag.Name) && !cfg.Eth.QuorumLightClient.Enabled() { utils.RegisterRaftService(stack, ctx, &cfg.Node, ethService) } @@ -296,7 +367,7 @@ func quorumInitialisePrivacy(ctx *cli.Context) error { return err } - err = private.InitialiseConnection(cfg) + err = private.InitialiseConnection(cfg, ctx.GlobalIsSet(utils.QuorumLightClientFlag.Name)) if err != nil { return err } diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 667b600e84..138cc6d3ee 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -190,6 +190,30 @@ var ( utils.QuorumPTMTlsClientCertFlag, utils.QuorumPTMTlsClientKeyFlag, utils.QuorumPTMTlsInsecureSkipVerify, + utils.QuorumLightServerFlag, + utils.QuorumLightServerP2PListenPortFlag, + utils.QuorumLightServerP2PMaxPeersFlag, + utils.QuorumLightServerP2PNetrestrictFlag, + utils.QuorumLightServerP2PPermissioningFlag, + utils.QuorumLightServerP2PPermissioningPrefixFlag, + utils.QuorumLightClientFlag, + utils.QuorumLightClientPSIFlag, + utils.QuorumLightClientTokenEnabledFlag, + utils.QuorumLightClientTokenValueFlag, + utils.QuorumLightClientTokenManagementFlag, + utils.QuorumLightClientRPCTLSFlag, + utils.QuorumLightClientRPCTLSInsecureSkipVerifyFlag, + utils.QuorumLightClientRPCTLSCACertFlag, + utils.QuorumLightClientRPCTLSCertFlag, + utils.QuorumLightClientRPCTLSKeyFlag, + utils.QuorumLightClientServerNodeFlag, + utils.QuorumLightClientServerNodeRPCFlag, + utils.QuorumLightTLSFlag, + utils.QuorumLightTLSCertFlag, + utils.QuorumLightTLSKeyFlag, + utils.QuorumLightTLSCACertsFlag, + utils.QuorumLightTLSClientAuthFlag, + utils.QuorumLightTLSCipherSuitesFlag, // End-Quorum } diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 282559cf2b..40ccf60cec 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -257,6 +257,35 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.QuorumEnablePrivacyMarker, }, }, + { + Name: "QUORUM LIGHT CLIENT/SERVER", + Flags: []cli.Flag{ + utils.QuorumLightServerFlag, + utils.QuorumLightServerP2PListenPortFlag, + utils.QuorumLightServerP2PMaxPeersFlag, + utils.QuorumLightServerP2PNetrestrictFlag, + utils.QuorumLightServerP2PPermissioningFlag, + utils.QuorumLightServerP2PPermissioningPrefixFlag, + utils.QuorumLightClientFlag, + utils.QuorumLightClientPSIFlag, + utils.QuorumLightClientTokenEnabledFlag, + utils.QuorumLightClientTokenValueFlag, + utils.QuorumLightClientTokenManagementFlag, + utils.QuorumLightClientRPCTLSFlag, + utils.QuorumLightClientRPCTLSInsecureSkipVerifyFlag, + utils.QuorumLightClientRPCTLSCACertFlag, + utils.QuorumLightClientRPCTLSCertFlag, + utils.QuorumLightClientRPCTLSKeyFlag, + utils.QuorumLightClientServerNodeFlag, + utils.QuorumLightClientServerNodeRPCFlag, + utils.QuorumLightTLSFlag, + utils.QuorumLightTLSCertFlag, + utils.QuorumLightTLSKeyFlag, + utils.QuorumLightTLSCACertsFlag, + utils.QuorumLightTLSClientAuthFlag, + utils.QuorumLightTLSCipherSuitesFlag, + }, + }, { Name: "QUORUM PRIVATE TRANSACTION MANAGER", Flags: []cli.Flag{ diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 7c992c7568..c2448a25a8 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -953,6 +953,105 @@ var ( Name: "ptm.tls.insecureskipverify", Usage: "Disable verification of server's TLS certificate on connection to private transaction manager", } + QuorumLightServerFlag = cli.BoolFlag{ + Name: "qlight.server", + Usage: "If enabled, the quorum light P2P protocol is started in addition to the other P2P protocols", + } + QuorumLightServerP2PListenPortFlag = cli.IntFlag{ + Name: "qlight.server.p2p.port", + Usage: "QLight Network listening port", + Value: 30305, + } + QuorumLightServerP2PMaxPeersFlag = cli.IntFlag{ + Name: "qlight.server.p2p.maxpeers", + Usage: "Maximum number of qlight peers", + Value: 10, + } + QuorumLightServerP2PNetrestrictFlag = cli.StringFlag{ + Name: "qlight.server.p2p.netrestrict", + Usage: "Restricts network communication to the given IP networks (CIDR masks)", + } + QuorumLightServerP2PPermissioningFlag = cli.BoolFlag{ + Name: "qlight.server.p2p.permissioning", + Usage: "If enabled, the qlight peers are checked against a permissioned list and a disallowed list.", + } + QuorumLightServerP2PPermissioningPrefixFlag = cli.StringFlag{ + Name: "qlight.server.p2p.permissioning.prefix", + Usage: "The prefix for the permissioned-nodes.json and disallowed-nodes.json files.", + } + QuorumLightClientFlag = cli.BoolFlag{ + Name: "qlight.client", + Usage: "If enabled, the quorum light client P2P protocol is started (only)", + } + QuorumLightClientPSIFlag = cli.StringFlag{ + Name: "qlight.client.psi", + Usage: "The PSI this client will use to connect to a server node.", + } + QuorumLightClientTokenEnabledFlag = cli.BoolFlag{ + Name: "qlight.client.token.enabled", + Usage: "Whether the client uses a token when connecting to the qlight server.", + } + QuorumLightClientTokenValueFlag = cli.StringFlag{ + Name: "qlight.client.token.value", + Usage: "The token this client will use to connect to a server node.", + } + QuorumLightClientTokenManagementFlag = cli.StringFlag{ + Name: "qlight.client.token.management", + Usage: "The mechanism used to refresh the token. Possible values: none (developer mode)/external (new token must be injected via the qlight RPC API)/client-security-plugin (the client security plugin must be deployed/configured).", + } + QuorumLightClientRPCTLSFlag = cli.BoolFlag{ + Name: "qlight.client.rpc.tls", + Usage: "If enabled, the quorum light client RPC connection will be configured to use TLS", + } + QuorumLightClientRPCTLSInsecureSkipVerifyFlag = cli.BoolFlag{ + Name: "qlight.client.rpc.tls.insecureskipverify", + Usage: "If enabled, the quorum light client RPC connection skips TLS verification", + } + QuorumLightClientRPCTLSCACertFlag = cli.StringFlag{ + Name: "qlight.client.rpc.tls.cacert", + Usage: "The quorum light client RPC client certificate authority.", + } + QuorumLightClientRPCTLSCertFlag = cli.StringFlag{ + Name: "qlight.client.rpc.tls.cert", + Usage: "The quorum light client RPC client certificate.", + } + QuorumLightClientRPCTLSKeyFlag = cli.StringFlag{ + Name: "qlight.client.rpc.tls.key", + Usage: "The quorum light client RPC client certificate private key.", + } + QuorumLightClientServerNodeFlag = cli.StringFlag{ + Name: "qlight.client.serverNode", + Usage: "The node ID of the target server node", + } + QuorumLightClientServerNodeRPCFlag = cli.StringFlag{ + Name: "qlight.client.serverNodeRPC", + Usage: "The RPC URL of the target server node", + } + QuorumLightTLSFlag = cli.BoolFlag{ + Name: "qlight.tls", + Usage: "If enabled, the quorum light client P2P protocol will use tls", + } + QuorumLightTLSCertFlag = cli.StringFlag{ + Name: "qlight.tls.cert", + Usage: "The certificate file to use for the qlight P2P connection", + } + QuorumLightTLSKeyFlag = cli.StringFlag{ + Name: "qlight.tls.key", + Usage: "The key file to use for the qlight P2P connection", + } + QuorumLightTLSCACertsFlag = cli.StringFlag{ + Name: "qlight.tls.cacerts", + Usage: "The certificate authorities file to use for validating P2P connection", + } + QuorumLightTLSClientAuthFlag = cli.IntFlag{ + Name: "qlight.tls.clientauth", + Usage: "The way the client is authenticated. Possible values: 0=NoClientCert(default) 1=RequestClientCert 2=RequireAnyClientCert 3=VerifyClientCertIfGiven 4=RequireAndVerifyClientCert", + Value: 0, + } + QuorumLightTLSCipherSuitesFlag = cli.StringFlag{ + Name: "qlight.tls.ciphersuites", + Usage: "The cipher suites to use for the qlight P2P connection", + } ) // MakeDataDir retrieves the currently requested data directory, terminating @@ -1393,9 +1492,41 @@ func SetP2PConfig(ctx *cli.Context, cfg *p2p.Config) { } } +func SetQP2PConfig(ctx *cli.Context, cfg *p2p.Config) { + setNodeKey(ctx, cfg) + //setNAT(ctx, cfg) + cfg.NAT = nil + if ctx.GlobalIsSet(QuorumLightServerP2PListenPortFlag.Name) { + cfg.ListenAddr = fmt.Sprintf(":%d", ctx.GlobalInt(QuorumLightServerP2PListenPortFlag.Name)) + } + + cfg.EnableNodePermission = ctx.GlobalIsSet(QuorumLightServerP2PPermissioningFlag.Name) + + cfg.MaxPeers = 10 + if ctx.GlobalIsSet(QuorumLightServerP2PMaxPeersFlag.Name) { + cfg.MaxPeers = ctx.GlobalInt(QuorumLightServerP2PMaxPeersFlag.Name) + } + + if netrestrict := ctx.GlobalString(QuorumLightServerP2PNetrestrictFlag.Name); netrestrict != "" { + list, err := netutil.ParseNetlist(netrestrict) + if err != nil { + Fatalf("Option %q: %v", QuorumLightServerP2PNetrestrictFlag.Name, err) + } + cfg.NetRestrict = list + } + + cfg.MaxPendingPeers = 0 + cfg.NoDiscovery = true + cfg.DiscoveryV5 = false + cfg.NoDial = true +} + // SetNodeConfig applies node-related command line flags to the config. func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { SetP2PConfig(ctx, &cfg.P2P) + if cfg.QP2P != nil { + SetQP2PConfig(ctx, cfg.QP2P) + } setIPC(ctx, cfg) setHTTP(ctx, cfg) setGraphQL(ctx, cfg) @@ -1745,6 +1876,106 @@ func CheckExclusive(ctx *cli.Context, args ...interface{}) { } } +func SetQLightConfig(ctx *cli.Context, nodeCfg *node.Config, ethCfg *ethconfig.Config) { + if ctx.GlobalIsSet(QuorumLightServerFlag.Name) { + ethCfg.QuorumLightServer = ctx.GlobalBool(QuorumLightServerFlag.Name) + } + + if ethCfg.QuorumLightServer { + if nodeCfg.QP2P == nil { + nodeCfg.QP2P = &p2p.Config{ + ListenAddr: ":30305", + MaxPeers: 10, + NAT: nil, + NoDial: true, + NoDiscovery: true, + } + SetQP2PConfig(ctx, nodeCfg.QP2P) + } + } else { + nodeCfg.QP2P = nil + } + + ethCfg.QuorumLightClient = ðconfig.QuorumLightClient{} + if ctx.GlobalIsSet(QuorumLightClientFlag.Name) { + ethCfg.QuorumLightClient.Use = ctx.GlobalBool(QuorumLightClientFlag.Name) + } + + if len(ethCfg.QuorumLightClient.PSI) == 0 { + ethCfg.QuorumLightClient.PSI = "private" + } + if ctx.GlobalIsSet(QuorumLightClientPSIFlag.Name) { + ethCfg.QuorumLightClient.PSI = ctx.GlobalString(QuorumLightClientPSIFlag.Name) + } + + if ctx.GlobalIsSet(QuorumLightClientTokenEnabledFlag.Name) { + ethCfg.QuorumLightClient.TokenEnabled = ctx.GlobalBool(QuorumLightClientTokenEnabledFlag.Name) + } + + if ctx.GlobalIsSet(QuorumLightClientTokenValueFlag.Name) { + ethCfg.QuorumLightClient.TokenValue = ctx.GlobalString(QuorumLightClientTokenValueFlag.Name) + } + + if len(ethCfg.QuorumLightClient.TokenManagement) == 0 { + ethCfg.QuorumLightClient.TokenManagement = "client-security-plugin" + } + if ctx.GlobalIsSet(QuorumLightClientTokenManagementFlag.Name) { + ethCfg.QuorumLightClient.TokenManagement = ctx.GlobalString(QuorumLightClientTokenManagementFlag.Name) + } + if !isValidTokenManagement(ethCfg.QuorumLightClient.TokenManagement) { + Fatalf("Invalid value specified '%s' for flag '%s'.", ethCfg.QuorumLightClient.TokenManagement, QuorumLightClientTokenManagementFlag.Name) + } + + if ctx.GlobalIsSet(QuorumLightClientRPCTLSFlag.Name) { + ethCfg.QuorumLightClient.RPCTLS = ctx.GlobalBool(QuorumLightClientRPCTLSFlag.Name) + } + + if ctx.GlobalIsSet(QuorumLightClientRPCTLSCACertFlag.Name) { + ethCfg.QuorumLightClient.RPCTLSCACert = ctx.GlobalString(QuorumLightClientRPCTLSCACertFlag.Name) + } + + if ctx.GlobalIsSet(QuorumLightClientRPCTLSInsecureSkipVerifyFlag.Name) { + ethCfg.QuorumLightClient.RPCTLSInsecureSkipVerify = ctx.GlobalBool(QuorumLightClientRPCTLSInsecureSkipVerifyFlag.Name) + } + + if ctx.GlobalIsSet(QuorumLightClientRPCTLSCertFlag.Name) && ctx.GlobalIsSet(QuorumLightClientRPCTLSKeyFlag.Name) { + ethCfg.QuorumLightClient.RPCTLSCert = ctx.GlobalString(QuorumLightClientRPCTLSCertFlag.Name) + ethCfg.QuorumLightClient.RPCTLSKey = ctx.GlobalString(QuorumLightClientRPCTLSKeyFlag.Name) + } else if ctx.GlobalIsSet(QuorumLightClientRPCTLSCertFlag.Name) { + Fatalf("'%s' specified without specifying '%s'", QuorumLightClientRPCTLSCertFlag.Name, QuorumLightClientRPCTLSKeyFlag.Name) + } else if ctx.GlobalIsSet(QuorumLightClientRPCTLSKeyFlag.Name) { + Fatalf("'%s' specified without specifying '%s'", QuorumLightClientRPCTLSKeyFlag.Name, QuorumLightClientRPCTLSCertFlag.Name) + } + + if ctx.GlobalIsSet(QuorumLightClientServerNodeRPCFlag.Name) { + ethCfg.QuorumLightClient.ServerNodeRPC = ctx.GlobalString(QuorumLightClientServerNodeRPCFlag.Name) + } + + if ctx.GlobalIsSet(QuorumLightClientServerNodeFlag.Name) { + ethCfg.QuorumLightClient.ServerNode = ctx.GlobalString(QuorumLightClientServerNodeFlag.Name) + // This is already done in geth/config - before the node.New invocation (at which point the StaticNodes is already copied) + //stack.Config().P2P.StaticNodes = []*enode.Node{enode.MustParse(ethCfg.QuorumLightClientServerNode)} + } + + if ethCfg.QuorumLightClient.Enabled() { + if ctx.GlobalBool(MiningEnabledFlag.Name) { + Fatalf("QLight clients do not support mining") + } + if len(ethCfg.QuorumLightClient.ServerNode) == 0 { + Fatalf("Please specify the '%s' when running a qlight client.", QuorumLightClientServerNodeFlag.Name) + } + if len(ethCfg.QuorumLightClient.ServerNodeRPC) == 0 { + Fatalf("Please specify the '%s' when running a qlight client.", QuorumLightClientServerNodeRPCFlag.Name) + } + + nodeCfg.P2P.StaticNodes = []*enode.Node{enode.MustParse(ethCfg.QuorumLightClient.ServerNode)} + log.Info("The node is configured to run as a qlight client. 'maxpeers' is overridden to `1` and the P2P listener is disabled.") + nodeCfg.P2P.MaxPeers = 1 + // force the qlight client node to disable the local P2P listener + nodeCfg.P2P.ListenAddr = "" + } +} + // SetEthConfig applies eth-related command line flags to the config. func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { // Avoid conflicting network flags @@ -1779,6 +2010,13 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.GlobalIsSet(SyncModeFlag.Name) { cfg.SyncMode = *GlobalTextMarshaler(ctx, SyncModeFlag.Name).(*downloader.SyncMode) } + + // Quorum + if cfg.QuorumLightClient.Enabled() && cfg.SyncMode != downloader.FullSync { + Fatalf("Only the 'full' syncmode is supported for the qlight client.") + } + // End Quorum + if ctx.GlobalIsSet(NetworkIdFlag.Name) { cfg.NetworkId = ctx.GlobalUint64(NetworkIdFlag.Name) } @@ -2357,3 +2595,14 @@ func MigrateFlags(action func(ctx *cli.Context) error) func(*cli.Context) error return action(ctx) } } + +func isValidTokenManagement(value string) bool { + switch value { + case + "none", + "external", + "client-security-plugin": + return true + } + return false +} diff --git a/common/types.go b/common/types.go index a193f893cc..0afaefa565 100644 --- a/common/types.go +++ b/common/types.go @@ -24,12 +24,14 @@ import ( "encoding/json" "errors" "fmt" + "io" "math/big" "math/rand" "reflect" "strings" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rlp" "golang.org/x/crypto/sha3" ) @@ -57,6 +59,44 @@ type EncryptedPayloadHash [EncryptedPayloadHashLength]byte // Using map to enable fast lookup type EncryptedPayloadHashes map[EncryptedPayloadHash]struct{} +func (h *EncryptedPayloadHash) MarshalJSON() (j []byte, err error) { + return json.Marshal(h.ToBase64()) +} + +func (h *EncryptedPayloadHash) UnmarshalJSON(j []byte) (err error) { + var ephStr string + err = json.Unmarshal(j, &ephStr) + if err != nil { + return err + } + eph, err := Base64ToEncryptedPayloadHash(ephStr) + if err != nil { + return err + } + h.SetBytes(eph.Bytes()) + return nil +} + +func (h *EncryptedPayloadHashes) MarshalJSON() (j []byte, err error) { + return json.Marshal(h.ToBase64s()) +} + +func (h *EncryptedPayloadHashes) UnmarshalJSON(j []byte) (err error) { + var ephStrArray []string + err = json.Unmarshal(j, &ephStrArray) + if err != nil { + return err + } + for _, str := range ephStrArray { + eph, err := Base64ToEncryptedPayloadHash(str) + if err != nil { + return err + } + h.Add(eph) + } + return nil +} + // BytesToEncryptedPayloadHash sets b to EncryptedPayloadHash. // If b is larger than len(h), b will be cropped from the left. func BytesToEncryptedPayloadHash(b []byte) EncryptedPayloadHash { @@ -300,6 +340,27 @@ func (ephs EncryptedPayloadHashes) Add(eph EncryptedPayloadHash) { ephs[eph] = struct{}{} } +func (ephs EncryptedPayloadHashes) EncodeRLP(writer io.Writer) error { + encryptedPayloadHashesArray := make([]EncryptedPayloadHash, len(ephs)) + idx := 0 + for key := range ephs { + encryptedPayloadHashesArray[idx] = key + idx++ + } + return rlp.Encode(writer, encryptedPayloadHashesArray) +} + +func (ephs EncryptedPayloadHashes) DecodeRLP(stream *rlp.Stream) error { + var encryptedPayloadHashesRLP []EncryptedPayloadHash + if err := stream.Decode(&encryptedPayloadHashesRLP); err != nil { + return err + } + for _, val := range encryptedPayloadHashesRLP { + ephs.Add(val) + } + return nil +} + func Base64sToEncryptedPayloadHashes(b64s []string) (EncryptedPayloadHashes, error) { ephs := make(EncryptedPayloadHashes) for _, b64 := range b64s { diff --git a/core/blockchain.go b/core/blockchain.go index a4a2c78128..0497cf7da2 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -46,6 +46,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/qlight" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" lru "github.com/hashicorp/golang-lru" @@ -220,7 +221,8 @@ type BlockChain struct { setPrivateState func([]*types.Log, *state.StateDB, types.PrivateStateIdentifier) // Function to check extension and set private state // privateStateManager manages private state(s) for this blockchain - privateStateManager mps.PrivateStateManager + privateStateManager mps.PrivateStateManager + privateStateRootHashValidator qlight.PrivateStateRootHashValidator // End Quorum } @@ -776,6 +778,11 @@ func (bc *BlockChain) StatePSI(psi types.PrivateStateIdentifier) (*state.StateDB // Quorum // +// SetPrivateStateRootHashValidator allows injecting a private state root hash validator +func (bc *BlockChain) SetPrivateStateRootHashValidator(psrhv qlight.PrivateStateRootHashValidator) { + bc.privateStateRootHashValidator = psrhv +} + // StatePSI returns a new mutable public state and a mutable private state for the given PSI, // based on a particular point in time. func (bc *BlockChain) StateAtPSI(root common.Hash, psi types.PrivateStateIdentifier) (*state.StateDB, *state.StateDB, error) { @@ -1718,6 +1725,12 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. if err != nil { return NonStatTy, err } + if bc.privateStateRootHashValidator != nil { + err = bc.privateStateRootHashValidator.ValidatePrivateStateRoot(block.Hash(), block.Root()) + if err != nil { + return NonStatTy, err + } + } // /Quorum triedb := bc.stateCache.TrieDB() diff --git a/core/mps/default_psr.go b/core/mps/default_psr.go index c1b65d4819..07c81df076 100644 --- a/core/mps/default_psr.go +++ b/core/mps/default_psr.go @@ -56,6 +56,10 @@ func (dpsr *DefaultPrivateStateRepository) IsMPS() bool { return false } +func (dpsr *DefaultPrivateStateRepository) PrivateStateRoot(psi types.PrivateStateIdentifier) (common.Hash, error) { + return dpsr.root, nil +} + func (dpsr *DefaultPrivateStateRepository) StatePSI(psi types.PrivateStateIdentifier) (*state.StateDB, error) { if psi != types.DefaultPrivateStateIdentifier { return nil, fmt.Errorf("only the 'private' psi is supported by the default private state manager") diff --git a/core/mps/interface.go b/core/mps/interface.go index 5e8c4f6efa..8d9b321464 100644 --- a/core/mps/interface.go +++ b/core/mps/interface.go @@ -35,6 +35,7 @@ type PrivateStateMetadataResolver interface { // PrivateStateRepository abstracts how we handle private state(s) including // retrieving from and peristing private states to the underlying database type PrivateStateRepository interface { + PrivateStateRoot(psi types.PrivateStateIdentifier) (common.Hash, error) StatePSI(psi types.PrivateStateIdentifier) (*state.StateDB, error) CommitAndWrite(isEIP158 bool, block *types.Block) error Commit(isEIP158 bool, block *types.Block) error diff --git a/core/mps/mock_interface.go b/core/mps/mock_interface.go index 71f1c0465a..ed1116919e 100644 --- a/core/mps/mock_interface.go +++ b/core/mps/mock_interface.go @@ -352,6 +352,21 @@ func (mr *MockPrivateStateRepositoryMockRecorder) MergeReceipts(pub, priv interf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MergeReceipts", reflect.TypeOf((*MockPrivateStateRepository)(nil).MergeReceipts), pub, priv) } +// PrivateStateRoot mocks base method. +func (m *MockPrivateStateRepository) PrivateStateRoot(psi types.PrivateStateIdentifier) (common.Hash, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrivateStateRoot", psi) + ret0, _ := ret[0].(common.Hash) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PrivateStateRoot indicates an expected call of PrivateStateRoot. +func (mr *MockPrivateStateRepositoryMockRecorder) PrivateStateRoot(psi interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrivateStateRoot", reflect.TypeOf((*MockPrivateStateRepository)(nil).PrivateStateRoot), psi) +} + // Reset mocks base method. func (m *MockPrivateStateRepository) Reset() error { m.ctrl.T.Helper() diff --git a/core/mps/multiple_psr.go b/core/mps/multiple_psr.go index 5761619f30..b0d785c8bd 100644 --- a/core/mps/multiple_psr.go +++ b/core/mps/multiple_psr.go @@ -97,6 +97,14 @@ func (mpsr *MultiplePrivateStateRepository) IsMPS() bool { return true } +func (mpsr *MultiplePrivateStateRepository) PrivateStateRoot(psi types.PrivateStateIdentifier) (common.Hash, error) { + privateStateRoot, err := mpsr.trie.TryGet([]byte(psi)) + if err != nil { + return common.Hash{}, err + } + return common.BytesToHash(privateStateRoot), nil +} + func (mpsr *MultiplePrivateStateRepository) StatePSI(psi types.PrivateStateIdentifier) (*state.StateDB, error) { mpsr.mux.Lock() ms, found := mpsr.managedStates[psi] @@ -173,6 +181,7 @@ func (mpsr *MultiplePrivateStateRepository) CommitAndWrite(isEIP158 bool, block if err != nil { return err } + // update the managed state root in the trie of state roots if err := mpsr.trie.TryUpdate([]byte(psi), privateRoot.Bytes()); err != nil { return err diff --git a/core/mps/types.go b/core/mps/types.go index 21c8c3b829..60fb427de3 100644 --- a/core/mps/types.go +++ b/core/mps/types.go @@ -58,6 +58,16 @@ func (psm *PrivateStateMetadata) NotIncludeAny(addresses ...string) bool { return true } +func (psm *PrivateStateMetadata) FilterAddresses(addresses ...string) []string { + result := make([]string, 0) + for _, addr := range addresses { + if _, found := psm.addressIndex[addr]; found { + result = append(result, addr) + } + } + return result +} + func (psm *PrivateStateMetadata) String() string { return fmt.Sprintf("ID=%s,Name=%s,Desc=%s,Type=%d,Addresses=%v", psm.ID, psm.Name, psm.Description, psm.Type, psm.Addresses) } diff --git a/eth/api_backend.go b/eth/api_backend.go index 20d1d9746d..06ae2d136c 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -58,6 +58,16 @@ type EthAPIBackend struct { // timeout value for call evmCallTimeOut time.Duration + // Quorum + proxyClient *rpc.Client +} + +func (b *EthAPIBackend) ProxyEnabled() bool { + return b.eth.config.QuorumLightClient.Enabled() +} + +func (b *EthAPIBackend) ProxyClient() *rpc.Client { + return b.proxyClient } // ChainConfig returns the active chain configuration. diff --git a/eth/backend.go b/eth/backend.go index bba50ea2e3..a9e5f4a707 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "math/big" + "net/http" "os" "runtime" "sync" @@ -44,6 +45,7 @@ import ( "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/eth/protocols/eth" + qlightproto "github.com/ethereum/go-ethereum/eth/protocols/qlight" "github.com/ethereum/go-ethereum/eth/protocols/snap" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" @@ -54,6 +56,9 @@ import ( "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/plugin/security" + "github.com/ethereum/go-ethereum/private" + "github.com/ethereum/go-ethereum/qlight" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" ) @@ -99,6 +104,8 @@ type Ethereum struct { // Quorum - consensus as eth-service (e.g. raft) consensusServicePendingLogsFeed *event.Feed + qlightServerHandler *handler + qlightP2pServer *p2p.Server } // New creates a new Ethereum object (including the @@ -176,6 +183,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { bloomRequests: make(chan chan *bloombits.Retrieval), bloomIndexer: core.NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms), p2pServer: stack.Server(), + qlightP2pServer: stack.QServer(), consensusServicePendingLogsFeed: new(event.Feed), } @@ -272,26 +280,136 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if checkpoint == nil { checkpoint = params.TrustedCheckpoints[genesisHash] } - if eth.handler, err = newHandler(&handlerConfig{ - Database: chainDb, - Chain: eth.blockchain, - TxPool: eth.txPool, - Network: config.NetworkId, - Sync: config.SyncMode, - BloomCache: uint64(cacheLimit), - EventMux: eth.eventMux, - Checkpoint: checkpoint, - AuthorizationList: config.AuthorizationList, - RaftMode: config.RaftMode, - Engine: eth.engine, - }); err != nil { - return nil, err + + if eth.config.QuorumLightClient.Enabled() { + clientCache, err := qlight.NewClientCache(chainDb) + if eth.config.QuorumLightClient.TokenEnabled { + qlight.SetCurrentToken(eth.config.QuorumLightClient.TokenValue) + switch eth.config.QuorumLightClient.TokenManagement { + case "client-security-plugin": + // TODO QLight - when hte client-security-plugin is implemented this may be a good place to initialize it + return nil, fmt.Errorf("The client-scurity-plugin token management is not implemented") + case "none": + log.Warn("Starting qlight client with auth token enabled but without a token management strategy. This is for development purposes only.") + case "external": + log.Info("Starting qlight client with auth token enabled and `external` token management strategy.") + default: + return nil, fmt.Errorf("Invalid value %s for `qlight.client.token.management`", eth.config.QuorumLightClient.TokenManagement) + } + } + if err != nil { + return nil, err + } + if eth.handler, err = newQLightClientHandler(&handlerConfig{ + Database: chainDb, + Chain: eth.blockchain, + TxPool: eth.txPool, + Network: config.NetworkId, + Sync: config.SyncMode, + BloomCache: uint64(cacheLimit), + EventMux: eth.eventMux, + Checkpoint: checkpoint, + AuthorizationList: config.AuthorizationList, + RaftMode: config.RaftMode, + Engine: eth.engine, + psi: config.QuorumLightClient.PSI, + privateClientCache: clientCache, + }); err != nil { + return nil, err + } + eth.blockchain.SetPrivateStateRootHashValidator(clientCache) + } else { + if eth.handler, err = newHandler(&handlerConfig{ + Database: chainDb, + Chain: eth.blockchain, + TxPool: eth.txPool, + Network: config.NetworkId, + Sync: config.SyncMode, + BloomCache: uint64(cacheLimit), + EventMux: eth.eventMux, + Checkpoint: checkpoint, + AuthorizationList: config.AuthorizationList, + RaftMode: config.RaftMode, + Engine: eth.engine, + }); err != nil { + return nil, err + } + if eth.config.QuorumLightServer { + authManProvider := func() security.AuthenticationManager { + _, authManager, _ := stack.GetSecuritySupports() + return authManager + } + if eth.qlightServerHandler, err = newQLightServerHandler(&handlerConfig{ + Database: chainDb, + Chain: eth.blockchain, + TxPool: eth.txPool, + Network: config.NetworkId, + Sync: config.SyncMode, + BloomCache: uint64(cacheLimit), + EventMux: eth.eventMux, + Checkpoint: checkpoint, + AuthorizationList: config.AuthorizationList, + RaftMode: config.RaftMode, + Engine: eth.engine, + authProvider: qlight.NewAuthProvider(eth.blockchain.PrivateStateManager(), authManProvider), + privateBlockDataResolver: qlight.NewPrivateBlockDataResolver(eth.blockchain.PrivateStateManager(), private.P), + }); err != nil { + return nil, err + } + } } eth.miner = miner.New(eth, &config.Miner, chainConfig, eth.EventMux(), eth.engine, eth.isLocalBlock) eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData, eth.blockchain.Config().IsQuorum)) hexNodeId := fmt.Sprintf("%x", crypto.FromECDSAPub(&stack.GetNodeKey().PublicKey)[1:]) // Quorum - eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil, hexNodeId, config.EVMCallTimeOut} + if eth.config.QuorumLightClient.Enabled() { + var ( + proxyClient *rpc.Client + err error + ) + // setup rpc client TLS context + if eth.config.QuorumLightClient.RPCTLS { + tlsConfig, err := qlight.NewTLSConfig(&qlight.TLSConfig{ + InsecureSkipVerify: eth.config.QuorumLightClient.RPCTLSInsecureSkipVerify, + CACertFileName: eth.config.QuorumLightClient.RPCTLSCACert, + CertFileName: eth.config.QuorumLightClient.RPCTLSCert, + KeyFileName: eth.config.QuorumLightClient.RPCTLSKey, + }) + if err != nil { + return nil, err + } + customHttpClient := &http.Client{ + Transport: http.DefaultTransport, + } + customHttpClient.Transport.(*http.Transport).TLSClientConfig = tlsConfig + proxyClient, err = rpc.DialHTTPWithClient(eth.config.QuorumLightClient.ServerNodeRPC, customHttpClient) + if err != nil { + return nil, err + } + } else { + proxyClient, err = rpc.Dial(eth.config.QuorumLightClient.ServerNodeRPC) + if err != nil { + return nil, err + } + } + + if eth.config.QuorumLightClient.TokenEnabled { + proxyClient = proxyClient.WithHTTPCredentials(qlight.TokenCredentialsProvider) + } + + if len(eth.config.QuorumLightClient.PSI) > 0 { + proxyClient = proxyClient.WithPSI(types.PrivateStateIdentifier(eth.config.QuorumLightClient.PSI)) + } + // TODO qlight - need to find a better way to inject the rpc client into the tx manager + rpcClientSetter, ok := private.P.(private.HasRPCClient) + if ok { + rpcClientSetter.SetRPCClient(proxyClient) + } + eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil, hexNodeId, config.EVMCallTimeOut, proxyClient} + } else { + eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil, hexNodeId, config.EVMCallTimeOut, nil} + } + if eth.APIBackend.allowUnprotectedTxs { log.Info("Unprotected transactions allowed") } @@ -314,7 +432,14 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { // Register the backend on the node stack.RegisterAPIs(eth.APIs()) + if eth.config.QuorumLightClient.Enabled() && eth.config.QuorumLightClient.TokenEnabled && + eth.config.QuorumLightClient.TokenManagement == "external" { + stack.RegisterAPIs(eth.QLightClientAPIs()) + } stack.RegisterProtocols(eth.Protocols()) + if eth.config.QuorumLightServer { + stack.RegisterQProtocols(eth.QProtocols()) + } stack.RegisterLifecycle(eth) // Check for unclean shutdown if uncleanShutdowns, discards, err := rawdb.PushUncleanShutdownMarker(chainDb); err != nil { @@ -349,6 +474,17 @@ func makeExtraData(extra []byte, isQuorum bool) []byte { return extra } +func (s *Ethereum) QLightClientAPIs() []rpc.API { + return []rpc.API{ + { + Namespace: "qlight", + Version: "1.0", + Service: qlight.NewPrivateQLightAPI(s.handler.peers, s.APIBackend.proxyClient), + Public: false, + }, + } +} + // APIs return the collection of RPC services the ethereum package offers. // NOTE, some of these services probably need to be moved to somewhere else. func (s *Ethereum) APIs() []rpc.API { @@ -590,6 +726,11 @@ func (s *Ethereum) BloomIndexer() *core.ChainIndexer { return s.bloomIndexer } // Protocols returns all the currently configured // network protocols to start. func (s *Ethereum) Protocols() []p2p.Protocol { + if s.config.QuorumLightClient.Enabled() { + protos := qlightproto.MakeProtocolsClient((*qlightClientHandler)(s.handler), s.networkID, s.ethDialCandidates) + return protos + } + protos := eth.MakeProtocols((*ethHandler)(s.handler), s.networkID, s.ethDialCandidates) if s.config.SnapshotCache > 0 { protos = append(protos, snap.MakeProtocols((*snapHandler)(s.handler), s.snapDialCandidates)...) @@ -606,6 +747,11 @@ func (s *Ethereum) Protocols() []p2p.Protocol { return protos } +func (s *Ethereum) QProtocols() []p2p.Protocol { + protos := qlightproto.MakeProtocolsServer((*qlightServerHandler)(s.qlightServerHandler), s.networkID, s.ethDialCandidates) + return protos +} + // Start implements node.Lifecycle, starting all internal goroutines needed by the // Ethereum protocol implementation. func (s *Ethereum) Start() error { @@ -623,7 +769,15 @@ func (s *Ethereum) Start() error { maxPeers -= s.config.LightPeers } // Start the networking layer and the light server if requested - s.handler.Start(maxPeers) + if s.config.QuorumLightClient.Enabled() { + s.handler.StartQLightClient() + } else { + s.handler.Start(maxPeers) + if s.qlightServerHandler != nil { + s.qlightServerHandler.StartQLightServer(s.qlightP2pServer.MaxPeers) + } + } + return nil } @@ -631,7 +785,14 @@ func (s *Ethereum) Start() error { // Ethereum protocol. func (s *Ethereum) Stop() error { // Stop all the peer-related stuff first. - s.handler.Stop() + if s.config.QuorumLightClient.Enabled() { + s.handler.StopQLightClient() + } else { + if s.qlightServerHandler != nil { + s.qlightServerHandler.StopQLightServer() + } + s.handler.Stop() + } // Then stop everything else. s.bloomIndexer.Close() diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 71fe7d905a..85741fcd01 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -218,6 +218,29 @@ type Config struct { // Quorum core.QuorumChainConfig `toml:"-"` + + // QuorumLight + QuorumLightServer bool `toml:",omitempty"` + QuorumLightClient *QuorumLightClient `toml:",omitempty"` +} + +type QuorumLightClient struct { + Use bool `toml:",omitempty"` + PSI string `toml:",omitempty"` + TokenEnabled bool `toml:",omitempty"` + TokenValue string `toml:",omitempty"` + TokenManagement string `toml:",omitempty"` + RPCTLS bool `toml:",omitempty"` + RPCTLSInsecureSkipVerify bool `toml:",omitempty"` + RPCTLSCACert string `toml:",omitempty"` + RPCTLSCert string `toml:",omitempty"` + RPCTLSKey string `toml:",omitempty"` + ServerNode string `toml:",omitempty"` + ServerNodeRPC string `toml:",omitempty"` +} + +func (q *QuorumLightClient) Enabled() bool { + return q != nil && q.Use } // CreateConsensusEngine creates a consensus engine for the given chain configuration. diff --git a/eth/handler.go b/eth/handler.go index 4831c33033..fdd1a53d19 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -43,6 +43,7 @@ import ( "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/qlight" "github.com/ethereum/go-ethereum/trie" ) @@ -97,6 +98,14 @@ type handlerConfig struct { // Quorum Engine consensus.Engine RaftMode bool + + // Quorum QLight + // client + psi string + privateClientCache qlight.PrivateClientCache + // server + authProvider qlight.AuthProvider + privateBlockDataResolver qlight.PrivateBlockDataResolver } type handler struct { @@ -142,6 +151,14 @@ type handler struct { // Test fields or hooks broadcastTxAnnouncesOnly bool // Testing field, disable transaction propagation + + // Quorum QLight + // client + psi string + privateClientCache qlight.PrivateClientCache + // server + authProvider qlight.AuthProvider + privateBlockDataResolver qlight.PrivateBlockDataResolver } // newHandler returns a handler for all Ethereum chain management protocol. diff --git a/eth/handler_qlight_client.go b/eth/handler_qlight_client.go new file mode 100644 index 0000000000..cca8afa668 --- /dev/null +++ b/eth/handler_qlight_client.go @@ -0,0 +1,408 @@ +package eth + +import ( + "errors" + "fmt" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/forkid" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/downloader" + "github.com/ethereum/go-ethereum/eth/fetcher" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + qlightproto "github.com/ethereum/go-ethereum/eth/protocols/qlight" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/qlight" + "github.com/ethereum/go-ethereum/trie" +) + +type qlightClientHandler ethHandler + +func (h *qlightClientHandler) Chain() *core.BlockChain { return h.chain } +func (h *qlightClientHandler) StateBloom() *trie.SyncBloom { return h.stateBloom } +func (h *qlightClientHandler) TxPool() eth.TxPool { return h.txpool } + +func (h *qlightClientHandler) RunPeer(peer *eth.Peer, handler eth.Handler) error { + return nil +} +func (h *qlightClientHandler) Handle(peer *eth.Peer, packet eth.Packet) error { + return (*ethHandler)(h).Handle(peer, packet) +} + +func (h *qlightClientHandler) RunQPeer(peer *qlightproto.Peer, hand qlightproto.Handler) error { + return (*handler)(h).runQLightClientPeer(peer, hand) +} + +// PeerInfo retrieves all known `eth` information about a peer. +func (h *qlightClientHandler) PeerInfo(id enode.ID) interface{} { + if p := h.peers.peer(id.String()); p != nil { + return p.info() + } + return nil +} + +// AcceptTxs retrieves whether transaction processing is enabled on the node +// or if inbound transactions should simply be dropped. +func (h *qlightClientHandler) AcceptTxs() bool { + return atomic.LoadUint32(&h.acceptTxs) == 1 +} + +// newHandler returns a handler for all Ethereum chain management protocol. +func newQLightClientHandler(config *handlerConfig) (*handler, error) { + // Create the protocol manager with the base fields + if config.EventMux == nil { + config.EventMux = new(event.TypeMux) // Nicety initialization for tests + } + h := &handler{ + networkID: config.Network, + forkFilter: forkid.NewFilter(config.Chain), + eventMux: config.EventMux, + database: config.Database, + txpool: config.TxPool, + chain: config.Chain, + peers: newPeerSet(), + authorizationList: config.AuthorizationList, + txsyncCh: make(chan *txsync), + quitSync: make(chan struct{}), + raftMode: config.RaftMode, + engine: config.Engine, + psi: config.psi, + privateClientCache: config.privateClientCache, + } + + if config.Sync == downloader.FullSync { + // The database seems empty as the current block is the genesis. Yet the fast + // block is ahead, so fast sync was enabled for this node at a certain point. + // The scenarios where this can happen is + // * if the user manually (or via a bad block) rolled back a fast sync node + // below the sync point. + // * the last fast sync is not finished while user specifies a full sync this + // time. But we don't have any recent state for full sync. + // In these cases however it's safe to reenable fast sync. + fullBlock, fastBlock := h.chain.CurrentBlock(), h.chain.CurrentFastBlock() + if fullBlock.NumberU64() == 0 && fastBlock.NumberU64() > 0 { + h.fastSync = uint32(1) + log.Warn("Switch sync mode from full sync to fast sync") + } + } else { + if h.chain.CurrentBlock().NumberU64() > 0 { + // Print warning log if database is not empty to run fast sync. + log.Warn("Switch sync mode from fast sync to full sync") + } else { + // If fast sync was requested and our database is empty, grant it + h.fastSync = uint32(1) + if config.Sync == downloader.SnapSync { + h.snapSync = uint32(1) + } + } + } + // If we have trusted checkpoints, enforce them on the chain + if config.Checkpoint != nil { + h.checkpointNumber = (config.Checkpoint.SectionIndex+1)*params.CHTFrequency - 1 + h.checkpointHash = config.Checkpoint.SectionHead + } + // Construct the downloader (long sync) and its backing state bloom if fast + // sync is requested. The downloader is responsible for deallocating the state + // bloom when it's done. + if atomic.LoadUint32(&h.fastSync) == 1 { + h.stateBloom = trie.NewSyncBloom(config.BloomCache, config.Database) + } + h.downloader = downloader.New(h.checkpointNumber, config.Database, h.stateBloom, h.eventMux, h.chain, nil, h.removePeer) + + // Construct the fetcher (short sync) + validator := func(header *types.Header) error { + return h.chain.Engine().VerifyHeader(h.chain, header, true) + } + heighter := func() uint64 { + return h.chain.CurrentBlock().NumberU64() + } + inserter := func(blocks types.Blocks) (int, error) { + // If sync hasn't reached the checkpoint yet, deny importing weird blocks. + // + // Ideally we would also compare the head block's timestamp and similarly reject + // the propagated block if the head is too old. Unfortunately there is a corner + // case when starting new networks, where the genesis might be ancient (0 unix) + // which would prevent full nodes from accepting it. + if h.chain.CurrentBlock().NumberU64() < h.checkpointNumber { + log.Warn("Unsynced yet, discarded propagated block", "number", blocks[0].Number(), "hash", blocks[0].Hash()) + return 0, nil + } + // If fast sync is running, deny importing weird blocks. This is a problematic + // clause when starting up a new network, because fast-syncing miners might not + // accept each others' blocks until a restart. Unfortunately we haven't figured + // out a way yet where nodes can decide unilaterally whether the network is new + // or not. This should be fixed if we figure out a solution. + if atomic.LoadUint32(&h.fastSync) == 1 { + log.Warn("Fast syncing, discarded propagated block", "number", blocks[0].Number(), "hash", blocks[0].Hash()) + return 0, nil + } + n, err := h.chain.InsertChain(blocks) + if err == nil { + atomic.StoreUint32(&h.acceptTxs, 1) // Mark initial sync done on any fetcher import + } + return n, err + } + h.blockFetcher = fetcher.NewBlockFetcher(false, nil, h.chain.GetBlockByHash, validator, h.BroadcastBlockQLightClient, heighter, nil, inserter, h.removePeer) + + fetchTx := func(peer string, hashes []common.Hash) error { + p := h.peers.peer(peer) + if p == nil { + return errors.New("unknown peer") + } + return p.RequestTxs(hashes) + } + h.txFetcher = fetcher.NewTxFetcher(h.txpool.Has, h.txpool.AddRemotes, fetchTx) + h.chainSync = newChainSyncer(h) + return h, nil +} + +// runEthPeer registers an eth peer into the joint eth/snap peerset, adds it to +// various subsistems and starts handling messages. +func (h *handler) runQLightClientPeer(peer *qlightproto.Peer, handler qlightproto.Handler) error { + // If the peer has a `snap` extension, wait for it to connect so we can have + // a uniform initialization/teardown mechanism + snap, err := h.peers.waitSnapExtension(peer.EthPeer) + if err != nil { + peer.Log().Error("Snapshot extension barrier failed", "err", err) + return err + } + // TODO(karalabe): Not sure why this is needed + if !h.chainSync.handlePeerEvent(peer.EthPeer) { + return p2p.DiscQuitting + } + h.peerWG.Add(1) + defer h.peerWG.Done() + + // Execute the Ethereum handshake + var ( + genesis = h.chain.Genesis() + head = h.chain.CurrentHeader() + hash = head.Hash() + number = head.Number.Uint64() + td = h.chain.GetTd(hash, number) + ) + forkID := forkid.NewID(h.chain.Config(), h.chain.Genesis().Hash(), h.chain.CurrentHeader().Number.Uint64()) + if err := peer.EthPeer.Handshake(h.networkID, td, hash, genesis.Hash(), forkID, h.forkFilter); err != nil { + peer.Log().Debug("Ethereum handshake failed", "err", err) + + // Quorum + // When the Handshake() returns an error, the Run method corresponding to `eth` protocol returns with the error, causing the peer to drop, signal subprotocol as well to exit the `Run` method + peer.EthPeerDisconnected <- struct{}{} + // End Quorum + return err + } + + log.Info("QLight attempting handshake") + if err := peer.QLightHandshake(false, h.psi, qlight.GetCurrentToken()); err != nil { + peer.Log().Debug("QLight handshake failed", "err", err) + log.Info("QLight handshake failed", "err", err) + + // Quorum + // When the Handshake() returns an error, the Run method corresponding to `eth` protocol returns with the error, causing the peer to drop, signal subprotocol as well to exit the `Run` method + peer.EthPeerDisconnected <- struct{}{} + // End Quorum + return err + } + + peer.Log().Debug("QLight handshake result for peer", "peer", peer.ID(), "server", peer.QLightServer(), "psi", peer.QLightPSI(), "token", peer.QLightToken()) + log.Info("QLight handshake result for peer", "peer", peer.ID(), "server", peer.QLightServer(), "psi", peer.QLightPSI(), "token", peer.QLightToken()) + // if we're not connected to a qlight server - disconnect the peer + if !peer.QLightServer() { + peer.Log().Debug("QLight connected to a non server peer. Disconnecting.") + + // Quorum + // When the Handshake() returns an error, the Run method corresponding to `eth` protocol returns with the error, causing the peer to drop, signal subprotocol as well to exit the `Run` method + peer.EthPeerDisconnected <- struct{}{} + // End Quorum + return fmt.Errorf("connected to a non server peer") + } + + reject := false // reserved peer slots + if atomic.LoadUint32(&h.snapSync) == 1 { + if snap == nil { + // If we are running snap-sync, we want to reserve roughly half the peer + // slots for peers supporting the snap protocol. + // The logic here is; we only allow up to 5 more non-snap peers than snap-peers. + if all, snp := h.peers.len(), h.peers.snapLen(); all-snp > snp+5 { + reject = true + } + } + } + // Ignore maxPeers if this is a trusted peer + if !peer.Peer.Info().Network.Trusted { + if reject || h.peers.len() >= h.maxPeers { + return p2p.DiscTooManyPeers + } + } + peer.Log().Debug("Ethereum peer connected", "name", peer.Name()) + + // Register the peer locally + if err := h.peers.registerQPeer(peer); err != nil { + peer.Log().Error("Ethereum peer registration failed", "err", err) + + // Quorum + // When the Register() returns an error, the Run method corresponding to `eth` protocol returns with the error, causing the peer to drop, signal subprotocol as well to exit the `Run` method + peer.EthPeerDisconnected <- struct{}{} + // End Quorum + + return err + } + defer h.removePeer(peer.ID()) + + p := h.peers.peer(peer.ID()) + if p == nil { + return errors.New("peer dropped during handling") + } + // Register the peer in the downloader. If the downloader considers it banned, we disconnect + if err := h.downloader.RegisterPeer(peer.ID(), peer.Version(), peer.EthPeer); err != nil { + peer.Log().Error("Failed to register peer in eth syncer", "err", err) + return err + } + if snap != nil { + if err := h.downloader.SnapSyncer.Register(snap); err != nil { + peer.Log().Error("Failed to register peer in snap syncer", "err", err) + return err + } + } + h.chainSync.handlePeerEvent(peer.EthPeer) + + // Propagate existing transactions. new transactions appearing + // after this will be sent via broadcasts. + h.syncTransactions(peer.EthPeer) + + // If we have a trusted CHT, reject all peers below that (avoid fast sync eclipse) + if h.checkpointHash != (common.Hash{}) { + // Request the peer's checkpoint header for chain height/weight validation + if err := peer.EthPeer.RequestHeadersByNumber(h.checkpointNumber, 1, 0, false); err != nil { + return err + } + // Start a timer to disconnect if the peer doesn't reply in time + p.syncDrop = time.AfterFunc(syncChallengeTimeout, func() { + peer.Log().Warn("Checkpoint challenge timed out, dropping", "addr", peer.RemoteAddr(), "type", peer.Name()) + h.removePeer(peer.ID()) + }) + // Make sure it's cleaned up if the peer dies off + defer func() { + if p.syncDrop != nil { + p.syncDrop.Stop() + p.syncDrop = nil + } + }() + } + // If we have any explicit authorized block hashes, request them + for number := range h.authorizationList { + if err := peer.EthPeer.RequestHeadersByNumber(number, 1, 0, false); err != nil { + return err + } + } + + // Quorum notify other subprotocols that the eth peer is ready, and has been added to the peerset. + p.EthPeerRegistered <- struct{}{} + // Quorum + + // Handle incoming messages until the connection is torn down + return handler(peer) +} + +func (h *handler) StartQLightClient() { + h.maxPeers = 1 + // Quorum + if h.raftMode { + // We set this immediately in raft mode to make sure the miner never drops + // incoming txes. Raft mode doesn't use the fetcher or downloader, and so + // this would never be set otherwise. + atomic.StoreUint32(&h.acceptTxs, 1) + } + // End Quorum + + // start sync handlers + h.wg.Add(1) + go h.chainSync.loop() +} + +func (h *handler) StopQLightClient() { + // Quit chainSync and txsync64. + // After this is done, no new peers will be accepted. + close(h.quitSync) + h.wg.Wait() + + // Disconnect existing sessions. + // This also closes the gate for any new registrations on the peer set. + // sessions which are already established but not added to h.peers yet + // will exit when they try to register. + h.peers.close() + h.peerWG.Wait() + log.Info("QLight client protocol stopped") +} + +// BroadcastBlock will either propagate a block to a subset of its peers, or +// will only announce its availability (depending what's requested). +func (h *handler) BroadcastBlockQLightClient(block *types.Block, propagate bool) { +} + +// Handle is invoked from a peer's message handler when it receives a new remote +// message that the handler couldn't consume and serve itself. +func (h *qlightClientHandler) QHandle(peer *qlightproto.Peer, packet eth.Packet) error { + // Consume any broadcasts and announces, forwarding the rest to the downloader + switch packet := packet.(type) { + case *eth.BlockHeadersPacket: + return (*ethHandler)(h).Handle(peer.EthPeer, packet) + + case *eth.BlockBodiesPacket: + txset, uncleset := packet.Unpack() + h.handleBodiesQLight(txset) + return (*ethHandler)(h).handleBodies(peer.EthPeer, txset, uncleset) + + case *eth.NewBlockHashesPacket: + return (*ethHandler)(h).Handle(peer.EthPeer, packet) + + case *eth.NewBlockPacket: + h.updateCacheWithNonPartyTxData(packet.Block.Transactions()) + return (*ethHandler)(h).handleBlockBroadcast(peer.EthPeer, packet.Block, packet.TD) + case *qlightproto.BlockPrivateDataPacket: + return h.handleBlockPrivateData(packet) + case *eth.NewPooledTransactionHashesPacket: + return (*ethHandler)(h).Handle(peer.EthPeer, packet) + case *eth.TransactionsPacket: + h.updateCacheWithNonPartyTxData(*packet) + return (*ethHandler)(h).Handle(peer.EthPeer, packet) + case *eth.PooledTransactionsPacket: + return (*ethHandler)(h).Handle(peer.EthPeer, packet) + + default: + return fmt.Errorf("unexpected eth packet type: %T", packet) + } +} + +// handleBodies is invoked from a peer's message handler when it transmits a batch +// of block bodies for the local node to process. +func (h *qlightClientHandler) handleBodiesQLight(txs [][]*types.Transaction) { + for _, txArray := range txs { + h.updateCacheWithNonPartyTxData(txArray) + } +} + +func (h *qlightClientHandler) updateCacheWithNonPartyTxData(transactions []*types.Transaction) { + for _, tx := range transactions { + if tx.IsPrivate() || tx.IsPrivacyMarker() { + txHash := common.BytesToEncryptedPayloadHash(tx.Data()) + h.privateClientCache.CheckAndAddEmptyEntry(txHash) + } + } +} + +func (h *qlightClientHandler) handleBlockPrivateData(blockPrivateData *qlightproto.BlockPrivateDataPacket) error { + for _, b := range *blockPrivateData { + if err := h.privateClientCache.AddPrivateBlock(b); err != nil { + return fmt.Errorf("Unable to handle private block data: %v", err) + } + } + return nil +} diff --git a/eth/handler_qlight_server.go b/eth/handler_qlight_server.go new file mode 100644 index 0000000000..efd2de8028 --- /dev/null +++ b/eth/handler_qlight_server.go @@ -0,0 +1,356 @@ +package eth + +import ( + "errors" + "fmt" + "math/big" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/forkid" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + qlightproto "github.com/ethereum/go-ethereum/eth/protocols/qlight" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/qlight" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" +) + +type qlightServerHandler ethHandler + +func (h *qlightServerHandler) Chain() *core.BlockChain { return h.chain } +func (h *qlightServerHandler) StateBloom() *trie.SyncBloom { return h.stateBloom } +func (h *qlightServerHandler) TxPool() eth.TxPool { return h.txpool } + +func (h *qlightServerHandler) RunPeer(peer *eth.Peer, handler eth.Handler) error { + return nil +} +func (h *qlightServerHandler) Handle(peer *eth.Peer, packet eth.Packet) error { + return (*ethHandler)(h).Handle(peer, packet) +} + +func (h *qlightServerHandler) RunQPeer(peer *qlightproto.Peer, hand qlightproto.Handler) error { + return (*handler)(h).runQLightServerPeer(peer, hand) +} + +// PeerInfo retrieves all known `eth` information about a peer. +func (h *qlightServerHandler) PeerInfo(id enode.ID) interface{} { + if p := h.peers.peer(id.String()); p != nil { + return p.info() + } + return nil +} + +// AcceptTxs retrieves whether transaction processing is enabled on the node +// or if inbound transactions should simply be dropped. +func (h *qlightServerHandler) AcceptTxs() bool { + return atomic.LoadUint32(&h.acceptTxs) == 1 +} + +// newHandler returns a handler for all Ethereum chain management protocol. +func newQLightServerHandler(config *handlerConfig) (*handler, error) { + // Create the protocol manager with the base fields + h := &handler{ + networkID: config.Network, + forkFilter: forkid.NewFilter(config.Chain), + eventMux: config.EventMux, + database: config.Database, + txpool: config.TxPool, + chain: config.Chain, + peers: newPeerSet(), + authorizationList: config.AuthorizationList, + txsyncCh: make(chan *txsync), + quitSync: make(chan struct{}), + raftMode: config.RaftMode, + engine: config.Engine, + authProvider: config.authProvider, + privateBlockDataResolver: config.privateBlockDataResolver, + } + + return h, nil +} + +// runEthPeer registers an eth peer into the joint eth/snap peerset, adds it to +// various subsistems and starts handling messages. +func (h *handler) runQLightServerPeer(peer *qlightproto.Peer, handler qlightproto.Handler) error { + h.peerWG.Add(1) + defer h.peerWG.Done() + + // Execute the Ethereum handshake + var ( + genesis = h.chain.Genesis() + head = h.chain.CurrentHeader() + hash = head.Hash() + number = head.Number.Uint64() + td = h.chain.GetTd(hash, number) + ) + forkID := forkid.NewID(h.chain.Config(), h.chain.Genesis().Hash(), h.chain.CurrentHeader().Number.Uint64()) + if err := peer.EthPeer.Handshake(h.networkID, td, hash, genesis.Hash(), forkID, h.forkFilter); err != nil { + peer.Log().Debug("Ethereum handshake failed", "err", err) + + // Quorum + // When the Handshake() returns an error, the Run method corresponding to `eth` protocol returns with the error, causing the peer to drop, signal subprotocol as well to exit the `Run` method + peer.EthPeerDisconnected <- struct{}{} + // End Quorum + return err + } + + log.Info("QLight attempting handshake") + if err := peer.QLightHandshake(true, "", ""); err != nil { + peer.Log().Debug("QLight handshake failed", "err", err) + log.Info("QLight handshake failed", "err", err) + + // Quorum + // When the Handshake() returns an error, the Run method corresponding to `eth` protocol returns with the error, causing the peer to drop, signal subprotocol as well to exit the `Run` method + peer.EthPeerDisconnected <- struct{}{} + // End Quorum + return err + } + + peer.Log().Debug("QLight handshake result for peer", "peer", peer.ID(), "server", peer.QLightServer(), "psi", peer.QLightPSI(), "token", peer.QLightToken()) + log.Info("QLight handshake result for peer", "peer", peer.ID(), "server", peer.QLightServer(), "psi", peer.QLightPSI(), "token", peer.QLightToken()) + // if we're not connected to a qlight server - disconnect the peer + if peer.QLightServer() { + peer.Log().Debug("QLight server connected to a server peer. Disconnecting.") + + // Quorum + // When the Handshake() returns an error, the Run method corresponding to `eth` protocol returns with the error, causing the peer to drop, signal subprotocol as well to exit the `Run` method + peer.EthPeerDisconnected <- struct{}{} + // End Quorum + return fmt.Errorf("connected to a server peer") + } + + // Ignore maxPeers if this is a trusted peer + if !peer.Peer.Info().Network.Trusted { + if h.peers.len() >= h.maxPeers { + return p2p.DiscTooManyPeers + } + } + peer.Log().Debug("Ethereum peer connected", "name", peer.Name()) + + err := h.authProvider.Authorize(peer.QLightToken(), peer.QLightPSI()) + if err != nil { + peer.Log().Error("Auth error", "err", err) + return p2p.DiscAuthError + } + + // Register the peer locally + if err := h.peers.registerQPeer(peer); err != nil { + peer.Log().Error("Ethereum peer registration failed", "err", err) + + // Quorum + // When the Register() returns an error, the Run method corresponding to `eth` protocol returns with the error, causing the peer to drop, signal subprotocol as well to exit the `Run` method + peer.EthPeerDisconnected <- struct{}{} + // End Quorum + + return err + } + defer h.removeQLightServerPeer(peer.ID()) + + // start periodic auth checks + peer.QLightPeriodicAuthFunc = func() error { return h.authProvider.Authorize(peer.QLightToken(), peer.QLightPSI()) } + go peer.PeriodicAuthCheck() + + p := h.peers.peer(peer.ID()) + if p == nil { + return errors.New("peer dropped during handling") + } + + // Propagate existing transactions. new transactions appearing + // after this will be sent via broadcasts. + h.syncTransactions(peer.EthPeer) + + // Quorum notify other subprotocols that the eth peer is ready, and has been added to the peerset. + p.EthPeerRegistered <- struct{}{} + // Quorum + + // Handle incoming messages until the connection is torn down + return handler(peer) +} + +func (h *handler) StartQLightServer(maxPeers int) { + h.maxPeers = maxPeers + h.wg.Add(1) + h.txsCh = make(chan core.NewTxsEvent, txChanSize) + h.txsSub = h.txpool.SubscribeNewTxsEvent(h.txsCh) + go h.txBroadcastLoop() + + // broadcast mined blocks + h.wg.Add(1) + go h.newBlockBroadcastLoop() + + h.authProvider.Initialize() +} + +func (h *handler) StopQLightServer() { + h.txsSub.Unsubscribe() + close(h.quitSync) + h.wg.Wait() + + // Disconnect existing sessions. + // This also closes the gate for any new registrations on the peer set. + // sessions which are already established but not added to h.peers yet + // will exit when they try to register. + h.peers.close() + h.peerWG.Wait() + log.Info("QLight server protocol stopped") +} + +func (h *handler) newBlockBroadcastLoop() { + defer h.wg.Done() + + headCh := make(chan core.ChainHeadEvent, 10) + headSub := h.chain.SubscribeChainHeadEvent(headCh) + defer headSub.Unsubscribe() + + for { + select { + case ev := <-headCh: + log.Debug("Announcing block to peers", "number", ev.Block.Number(), "hash", ev.Block.Hash(), "td", ev.Block.Difficulty()) + h.BroadcastBlockQLServer(ev.Block) + + case <-h.quitSync: + return + } + } +} + +func (h *handler) BroadcastBlockQLServer(block *types.Block) { + hash := block.Hash() + peers := h.peers.qlightPeersWithoutBlock(hash) + + // Calculate the TD of the block (it's not imported yet, so block.Td is not valid) + var td *big.Int + if parent := h.chain.GetBlock(block.ParentHash(), block.NumberU64()-1); parent != nil { + td = new(big.Int).Add(block.Difficulty(), h.chain.GetTd(block.ParentHash(), block.NumberU64()-1)) + } else { + log.Error("Propagating dangling block", "number", block.Number(), "hash", hash) + return + } + // Send the block to a subset of our peers + for _, peer := range peers { + log.Info("Preparing new block private data") + blockPrivateData, err := h.privateBlockDataResolver.PrepareBlockPrivateData(block, peer.qlight.QLightPSI()) + if err != nil { + log.Error("Unable to prepare private data for block", "number", block.Number(), "hash", hash, "err", err, "psi", peer.qlight.QLightPSI()) + return + } + log.Info("Private transactions data", "is nil", blockPrivateData == nil) + peer.qlight.AsyncSendNewBlock(block, td, blockPrivateData) + } + log.Trace("Propagated block", "hash", hash, "recipients", len(peers), "duration", common.PrettyDuration(time.Since(block.ReceivedAt))) +} + +// removePeer unregisters a peer from the downloader and fetchers, removes it from +// the set of tracked peers and closes the network connection to it. +func (h *handler) removeQLightServerPeer(id string) { + // Create a custom logger to avoid printing the entire id + var logger log.Logger + if len(id) < 16 { + // Tests use short IDs, don't choke on them + logger = log.New("peer", id) + } else { + logger = log.New("peer", id[:8]) + } + // Abort if the peer does not exist + peer := h.peers.peer(id) + if peer == nil { + logger.Error("Ethereum peer removal failed", "err", errPeerNotRegistered) + return + } + // Remove the `eth` peer if it exists + logger.Debug("Removing QLight server peer", "snap", peer.snapExt != nil) + + if err := h.peers.unregisterPeer(id); err != nil { + logger.Error("Ethereum peer removal failed", "err", err) + } + // Hard disconnect at the networking layer + peer.Peer.Disconnect(p2p.DiscUselessPeer) +} + +func (ps *peerSet) qlightPeersWithoutBlock(hash common.Hash) []*ethPeer { + ps.lock.RLock() + defer ps.lock.RUnlock() + + list := make([]*ethPeer, 0, len(ps.peers)) + for _, p := range ps.peers { + if !p.qlight.KnownBlock(hash) { + list = append(list, p) + } + } + return list +} + +// Handle is invoked from a peer's message handler when it receives a new remote +// message that the handler couldn't consume and serve itself. +func (h *qlightServerHandler) QHandle(peer *qlightproto.Peer, packet eth.Packet) error { + // Consume any broadcasts and announces, forwarding the rest to the downloader + switch packet := packet.(type) { + case *eth.NewPooledTransactionHashesPacket: + return (*ethHandler)(h).Handle(peer.EthPeer, packet) + case *eth.TransactionsPacket: + return (*ethHandler)(h).Handle(peer.EthPeer, packet) + case *eth.PooledTransactionsPacket: + return (*ethHandler)(h).Handle(peer.EthPeer, packet) + case *eth.GetBlockBodiesPacket: + return h.handleGetBlockBodies(packet, peer) + default: + return fmt.Errorf("unexpected eth packet type: %T", packet) + } +} + +func (h *qlightServerHandler) handleGetBlockBodies(query *eth.GetBlockBodiesPacket, peer *qlightproto.Peer) error { + blockPublicData, blockPrivateData, err := h.answerGetBlockBodiesQuery(query, peer) + if err != nil { + return err + } + if len(blockPrivateData) > 0 { + err := peer.SendBlockPrivateData(blockPrivateData) + if err != nil { + log.Info("Error occurred while sending private data msg", "err", err) + return err + } + } + return peer.EthPeer.SendBlockBodiesRLP(blockPublicData) +} + +const ( + // softResponseLimit is the target maximum size of replies to data retrievals. + softResponseLimit = 2 * 1024 * 1024 + + maxBodiesServe = 1024 +) + +func (h *qlightServerHandler) answerGetBlockBodiesQuery(query *eth.GetBlockBodiesPacket, peer *qlightproto.Peer) ([]rlp.RawValue, []qlight.BlockPrivateData, error) { + // Gather blocks until the fetch or network limits is reached + var ( + bytes int + bodies []rlp.RawValue + blockPrivateDatas []qlight.BlockPrivateData + ) + for lookups, hash := range *query { + if bytes >= softResponseLimit || len(bodies) >= maxBodiesServe || + lookups >= 2*maxBodiesServe { + break + } + block := h.chain.GetBlockByHash(hash) + if block != nil { + if bpd, err := h.privateBlockDataResolver.PrepareBlockPrivateData(block, peer.QLightPSI()); err != nil { + return nil, nil, fmt.Errorf("Unable to produce block private transaction data %v: %v", hash, err) + } else if bpd != nil { + blockPrivateDatas = append(blockPrivateDatas, *bpd) + } + // TODO qlight - add soft limits for block private data as well + } + if data := h.chain.GetBodyRLP(hash); len(data) != 0 { + bodies = append(bodies, data) + bytes += len(data) + } + } + return bodies, blockPrivateDatas, nil +} diff --git a/eth/peer.go b/eth/peer.go index 1cea9c640e..a6ee053c10 100644 --- a/eth/peer.go +++ b/eth/peer.go @@ -22,6 +22,7 @@ import ( "time" "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/eth/protocols/qlight" "github.com/ethereum/go-ethereum/eth/protocols/snap" ) @@ -37,6 +38,7 @@ type ethPeerInfo struct { type ethPeer struct { *eth.Peer snapExt *snapPeer // Satellite `snap` connection + qlight *qlight.Peer syncDrop *time.Timer // Connection dropper if `eth` sync progress isn't validated in time snapWait chan struct{} // Notification channel for snap connections diff --git a/eth/peerset.go b/eth/peerset.go index 1e864a8e46..392587c878 100644 --- a/eth/peerset.go +++ b/eth/peerset.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/eth/protocols/qlight" "github.com/ethereum/go-ethereum/eth/protocols/snap" "github.com/ethereum/go-ethereum/p2p" ) @@ -156,6 +157,28 @@ func (ps *peerSet) registerPeer(peer *eth.Peer, ext *snap.Peer) error { return nil } +// registerPeer injects a new `eth` peer into the working set, or returns an error +// if the peer is already known. +func (ps *peerSet) registerQPeer(peer *qlight.Peer) error { + // Start tracking the new peer + ps.lock.Lock() + defer ps.lock.Unlock() + + if ps.closed { + return errPeerSetClosed + } + id := peer.ID() + if _, ok := ps.peers[id]; ok { + return errPeerAlreadyRegistered + } + eth := ðPeer{ + Peer: peer.EthPeer, + qlight: peer, + } + ps.peers[id] = eth + return nil +} + // unregisterPeer removes a remote peer from the active set, disabling any further // actions to/from that particular entity. func (ps *peerSet) unregisterPeer(id string) error { @@ -257,3 +280,19 @@ func (ps *peerSet) close() { } ps.closed = true } + +// Quorum +func (ps *peerSet) UpdateTokenForRunningQPeers(token string) error { + ps.lock.Lock() + defer ps.lock.Unlock() + + for _, p := range ps.peers { + if p.qlight != nil { + err := p.qlight.SendNewAuthToken(token) + if err != nil { + return err + } + } + } + return nil +} diff --git a/eth/protocols/eth/qlight_deps.go b/eth/protocols/eth/qlight_deps.go new file mode 100644 index 0000000000..2f1e573b39 --- /dev/null +++ b/eth/protocols/eth/qlight_deps.go @@ -0,0 +1,68 @@ +package eth + +import ( + mapset "github.com/deckarep/golang-set" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/p2p" +) + +func CurrentENREntry(chain *core.BlockChain) *enrEntry { + return currentENREntry(chain) +} + +func NodeInfoFunc(chain *core.BlockChain, network uint64) *NodeInfo { + return nodeInfo(chain, network) +} + +var ETH_65_FULL_SYNC = map[uint64]msgHandler{ + // old 64 messages + GetBlockHeadersMsg: handleGetBlockHeaders, + BlockHeadersMsg: handleBlockHeaders, + GetBlockBodiesMsg: handleGetBlockBodies, + BlockBodiesMsg: handleBlockBodies, + NewBlockHashesMsg: handleNewBlockhashes, + NewBlockMsg: handleNewBlock, + TransactionsMsg: handleTransactions, + // New eth65 messages + NewPooledTransactionHashesMsg: handleNewPooledTransactionHashes, + GetPooledTransactionsMsg: handleGetPooledTransactions, + PooledTransactionsMsg: handlePooledTransactions, +} + +func NewPeerWithTxBroadcast(version uint, p *p2p.Peer, rw p2p.MsgReadWriter, txpool TxPool) *Peer { + peer := NewPeerNoBroadcast(version, p, rw, txpool) + // Start up all the broadcasters + go peer.broadcastTransactions() + if version >= ETH65 { + go peer.announceTransactions() + } + return peer +} + +func NewPeerNoBroadcast(version uint, p *p2p.Peer, rw p2p.MsgReadWriter, txpool TxPool) *Peer { + peer := &Peer{ + id: p.ID().String(), + Peer: p, + rw: rw, + version: version, + knownTxs: mapset.NewSet(), + knownBlocks: mapset.NewSet(), + queuedBlocks: make(chan *blockPropagation, maxQueuedBlocks), + queuedBlockAnns: make(chan *types.Block, maxQueuedBlockAnns), + txBroadcast: make(chan []common.Hash), + txAnnounce: make(chan []common.Hash), + txpool: txpool, + term: make(chan struct{}), + } + return peer +} + +func (p *Peer) MarkBlock(hash common.Hash) { + p.markBlock(hash) +} + +func (p *Peer) MarkTransaction(hash common.Hash) { + p.markTransaction(hash) +} diff --git a/eth/protocols/qlight/auth.go b/eth/protocols/qlight/auth.go new file mode 100644 index 0000000000..2051a7d6ab --- /dev/null +++ b/eth/protocols/qlight/auth.go @@ -0,0 +1,26 @@ +package qlight + +import ( + "time" + + "github.com/ethereum/go-ethereum/p2p" +) + +func (p *Peer) PeriodicAuthCheck() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + p.Log().Debug("Performing periodic auth check") + err := p.QLightPeriodicAuthFunc() + if err != nil { + p.Log().Error("Disconnecting peer due to periodic auth check", "err", err) + p.Disconnect(p2p.DiscAuthError) + } + case <-p.term: + return + } + } +} diff --git a/eth/protocols/qlight/broadcast.go b/eth/protocols/qlight/broadcast.go new file mode 100644 index 0000000000..93d88b9032 --- /dev/null +++ b/eth/protocols/qlight/broadcast.go @@ -0,0 +1,43 @@ +package qlight + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/qlight" +) + +// blockPropagation is a block propagation event, waiting for its turn in the +// broadcast queue. +type blockPropagation struct { + block *types.Block + td *big.Int + blockPrivateData *qlight.BlockPrivateData +} + +// broadcastBlocks is a write loop that multiplexes blocks and block accouncements +// to the remote peer. The goal is to have an async writer that does not lock up +// node internals and at the same time rate limits queued data. +func (p *Peer) broadcastBlocksQLightServer() { + for { + select { + case prop := <-p.queuedBlocks: + if prop.blockPrivateData != nil { + if prop.blockPrivateData.PSI.String() == p.qlightPSI { + if err := p.SendBlockPrivateData([]qlight.BlockPrivateData{*prop.blockPrivateData}); err != nil { + p.Log().Error("Error occurred while sending private data msg", "err", err) + return + } + } else { + p.Log().Error("PSI mismatch for block private data", "bpdPSI", prop.blockPrivateData.PSI, "peerPSI", p.qlightPSI) + } + } + if err := p.SendNewBlock(prop.block, prop.td); err != nil { + return + } + p.Log().Trace("Propagated block", "number", prop.block.Number(), "hash", prop.block.Hash(), "td", prop.td) + case <-p.term: + return + } + } +} diff --git a/eth/protocols/qlight/handler.go b/eth/protocols/qlight/handler.go new file mode 100644 index 0000000000..8b4a567631 --- /dev/null +++ b/eth/protocols/qlight/handler.go @@ -0,0 +1,24 @@ +package qlight + +import ( + "github.com/ethereum/go-ethereum/eth/protocols/eth" +) + +// Handler is a callback to invoke from an outside runner after the boilerplate +// exchanges have passed. +type Handler func(peer *Peer) error + +// Backend defines the data retrieval methods to serve remote requests and the +// callback methods to invoke on remote deliveries. +type Backend interface { + eth.Backend + // RunPeer is invoked when a peer joins on the `eth` protocol. The handler + // should do any peer maintenance work, handshakes and validations. If all + // is passed, control should be given back to the `handler` to process the + // inbound messages going forward. + RunQPeer(peer *Peer, handler Handler) error + // Handle is a callback to be invoked when a data packet is received from + // the remote peer. Only packets not consumed by the protocol handler will + // be forwarded to the backend. + QHandle(peer *Peer, packet eth.Packet) error +} diff --git a/eth/protocols/qlight/handler_client.go b/eth/protocols/qlight/handler_client.go new file mode 100644 index 0000000000..919cfef4ce --- /dev/null +++ b/eth/protocols/qlight/handler_client.go @@ -0,0 +1,99 @@ +package qlight + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" +) + +// MakeProtocols constructs the P2P protocol definitions for `eth`. +func MakeProtocolsClient(backend Backend, network uint64, dnsdisc enode.Iterator) []p2p.Protocol { + protocols := make([]p2p.Protocol, 1) + version := uint(QLIGHT65) + protocols[0] = p2p.Protocol{ + Name: ProtocolName, + Version: QLIGHT65, + Length: QLightProtocolLength, + Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error { + ethPeer := eth.NewPeerNoBroadcast(version, p, rw, backend.TxPool()) + peer := NewPeer(version, p, rw, ethPeer) + defer ethPeer.Close() + defer peer.Close() + + return backend.RunQPeer(peer, func(peer *Peer) error { + return HandleClient(backend, peer) + }) + }, + NodeInfo: func() interface{} { + return eth.NodeInfoFunc(backend.Chain(), network) + }, + PeerInfo: func(id enode.ID) interface{} { + return backend.PeerInfo(id) + }, + Attributes: []enr.Entry{eth.CurrentENREntry(backend.Chain())}, + DialCandidates: dnsdisc, + } + return protocols +} + +// Handle is invoked whenever an `eth` connection is made that successfully passes +// the protocol handshake. This method will keep processing messages until the +// connection is torn down. +func HandleClient(backend Backend, peer *Peer) error { + for { + if err := handleMessageClient(backend, peer); err != nil { + return err + } + } +} + +// handleMessage is invoked whenever an inbound message is received from a remote +// peer. The remote connection is torn down upon returning any error. +func handleMessageClient(backend Backend, peer *Peer) error { + // Read the next message from the remote peer, and ensure it's fully consumed + msg, err := peer.rw.ReadMsg() + if err != nil { + return err + } + if msg.Size > maxMessageSize { + return fmt.Errorf("%w: %v > %v", errMsgTooLarge, msg.Size, maxMessageSize) + } + defer msg.Discard() + + peer.Log().Info("QLight client message received", "msg", msg.Code) + + var handlers = eth.ETH_65_FULL_SYNC + + switch msg.Code { + case eth.BlockHeadersMsg: + if handler := handlers[msg.Code]; handler != nil { + return handler(backend, msg, peer.EthPeer) + } + case eth.TransactionsMsg: + return qlightClientHandleTransactions(backend, msg, peer) + case eth.BlockBodiesMsg: + res := new(eth.BlockBodiesPacket) + if err := msg.Decode(res); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + return backend.QHandle(peer, res) + case eth.NewBlockHashesMsg: + if handler := handlers[msg.Code]; handler != nil { + return handler(backend, msg, peer.EthPeer) + } + case QLightNewBlockPrivateDataMsg: + peer.Log().Info("QLight Received block private data message", "msg", msg.Code) + return qlightClientHandleNewBlockPrivateData(backend, msg, peer) + case eth.NewBlockMsg: + return qlightClientHandleNewBlock(backend, msg, peer) + case eth.NewPooledTransactionHashesMsg: + if handler := handlers[msg.Code]; handler != nil { + return handler(backend, msg, peer.EthPeer) + } + } + peer.Log().Info("QLight Unable to find handler for received message", "msg", msg.Code) + return fmt.Errorf("%w: %v", errInvalidMsgCode, msg.Code) +} diff --git a/eth/protocols/qlight/handler_server.go b/eth/protocols/qlight/handler_server.go new file mode 100644 index 0000000000..1cbf51ef83 --- /dev/null +++ b/eth/protocols/qlight/handler_server.go @@ -0,0 +1,96 @@ +package qlight + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" +) + +// MakeProtocolsServer constructs the P2P protocol definitions for `qlight` server. +func MakeProtocolsServer(backend Backend, network uint64, dnsdisc enode.Iterator) []p2p.Protocol { + protocols := make([]p2p.Protocol, 1) + version := uint(QLIGHT65) + protocols[0] = p2p.Protocol{ + Name: ProtocolName, + Version: QLIGHT65, + Length: QLightProtocolLength, + Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error { + ethPeer := eth.NewPeerWithTxBroadcast(version, p, rw, backend.TxPool()) + peer := NewPeerWithBlockBroadcast(version, p, rw, ethPeer) + defer ethPeer.Close() + defer peer.Close() + + return backend.RunQPeer(peer, func(peer *Peer) error { + return HandleServer(backend, peer) + }) + }, + NodeInfo: func() interface{} { + return eth.NodeInfoFunc(backend.Chain(), network) + }, + PeerInfo: func(id enode.ID) interface{} { + return backend.PeerInfo(id) + }, + Attributes: []enr.Entry{eth.CurrentENREntry(backend.Chain())}, + DialCandidates: dnsdisc, + } + return protocols +} + +func HandleServer(backend Backend, peer *Peer) error { + for { + if err := handleMessageServer(backend, peer); err != nil { + return err + } + } +} + +func handleMessageServer(backend Backend, peer *Peer) error { + // Read the next message from the remote peer, and ensure it's fully consumed + msg, err := peer.rw.ReadMsg() + if err != nil { + return err + } + if msg.Size > maxMessageSize { + return fmt.Errorf("%w: %v > %v", errMsgTooLarge, msg.Size, maxMessageSize) + } + defer msg.Discard() + + peer.Log().Info("QLight client message received", "msg", msg.Code) + + var handlers = eth.ETH_65_FULL_SYNC + + switch msg.Code { + case eth.BlockHeadersMsg: + peer.Log().Info("QLight Block Headers message received. Ignoring.") + return nil + case eth.TransactionsMsg: + peer.Log().Info("QLight Transactions message received. Ignoring.") + return nil + case QLightTokenUpdateMsg: + peer.Log().Info("QLight Token update received.") + res := new(qLightTokenUpdateData) + if err := msg.Decode(res); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + peer.qlightToken = res.Token + return nil + case eth.GetBlockHeadersMsg: + if handler := handlers[msg.Code]; handler != nil { + return handler(backend, msg, peer.EthPeer) + } + case eth.GetBlockBodiesMsg: + res := new(eth.GetBlockBodiesPacket) + if err := msg.Decode(res); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + return backend.QHandle(peer, res) + case eth.NewBlockHashesMsg: + peer.Log().Info("QLight New Block Hashes message received. Ignoring.") + return nil + } + peer.Log().Info("QLight Unable to find handler for received message", "msg", msg.Code) + return fmt.Errorf("%w: %v", errInvalidMsgCode, msg.Code) +} diff --git a/eth/protocols/qlight/handlers.go b/eth/protocols/qlight/handlers.go new file mode 100644 index 0000000000..4ef8acacde --- /dev/null +++ b/eth/protocols/qlight/handlers.go @@ -0,0 +1,69 @@ +package qlight + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/trie" +) + +func qlightClientHandleNewBlock(backend Backend, msg eth.Decoder, peer *Peer) error { + ann := new(eth.NewBlockPacket) + if err := msg.Decode(ann); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + if hash := types.CalcUncleHash(ann.Block.Uncles()); hash != ann.Block.UncleHash() { + log.Warn("Propagated block has invalid uncles", "have", hash, "exp", ann.Block.UncleHash()) + return nil // TODO(karalabe): return error eventually, but wait a few releases + } + if hash := types.DeriveSha(ann.Block.Transactions(), trie.NewStackTrie(nil)); hash != ann.Block.TxHash() { + log.Warn("Propagated block has invalid body", "have", hash, "exp", ann.Block.TxHash()) + return nil // TODO(karalabe): return error eventually, but wait a few releases + } + if err := ann.Block.SanityCheck(); err != nil { + return err + } + //TD at mainnet block #7753254 is 76 bits. If it becomes 100 million times + // larger, it will still fit within 100 bits + if tdlen := ann.TD.BitLen(); tdlen > 100 { + return fmt.Errorf("too large block TD: bitlen %d", tdlen) + } + + ann.Block.ReceivedAt = msg.Time() + ann.Block.ReceivedFrom = peer + + // Mark the peer as owning the block + peer.EthPeer.MarkBlock(ann.Block.Hash()) + + return backend.QHandle(peer, ann) +} + +func qlightClientHandleNewBlockPrivateData(backend Backend, msg eth.Decoder, peer *Peer) error { + res := new(BlockPrivateDataPacket) + if err := msg.Decode(res); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + return backend.QHandle(peer, res) +} + +func qlightClientHandleTransactions(backend Backend, msg eth.Decoder, peer *Peer) error { + // Transactions arrived, make sure we have a valid and fresh chain to handle them + if !backend.AcceptTxs() { + return nil + } + // Transactions can be processed, parse all of them and deliver to the pool + var txs eth.TransactionsPacket + if err := msg.Decode(&txs); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + for i, tx := range txs { + // Validate and mark the remote transaction + if tx == nil { + return fmt.Errorf("%w: transaction %d is nil", errDecode, i) + } + peer.EthPeer.MarkTransaction(tx.Hash()) + } + return backend.QHandle(peer, &txs) +} diff --git a/eth/protocols/qlight/handshake.go b/eth/protocols/qlight/handshake.go new file mode 100644 index 0000000000..036289c032 --- /dev/null +++ b/eth/protocols/qlight/handshake.go @@ -0,0 +1,69 @@ +package qlight + +import ( + "fmt" + "time" + + "github.com/ethereum/go-ethereum/p2p" +) + +const ( + // handshakeTimeout is the maximum allowed time for the `eth` handshake to + // complete before dropping the connection.= as malicious. + handshakeTimeout = 5 * time.Second +) + +func (p *Peer) QLightHandshake(server bool, psi string, token string) error { + // Send out own handshake in a new thread + errc := make(chan error, 2) + + var ( + status qLightStatusData // safe to read after two values have been received from errc + ) + go func() { + errc <- p2p.Send(p.rw, QLightStatusMsg, &qLightStatusData{ + ProtocolVersion: uint32(p.version), + Server: server, + PSI: psi, + Token: token, + }) + }() + go func() { + errc <- p.readQLightStatus(&status) + }() + timeout := time.NewTimer(handshakeTimeout) + defer timeout.Stop() + for i := 0; i < 2; i++ { + select { + case err := <-errc: + if err != nil { + return err + } + case <-timeout.C: + return p2p.DiscReadTimeout + } + } + p.qlightServer, p.qlightPSI, p.qlightToken = status.Server, status.PSI, status.Token + return nil +} + +func (p *Peer) readQLightStatus(qligtStatus *qLightStatusData) error { + msg, err := p.rw.ReadMsg() + if err != nil { + return err + } + if msg.Code != QLightStatusMsg { + return fmt.Errorf("%w: second msg has code %x (!= %x)", errNoStatusMsg, msg.Code, QLightStatusMsg) + } + if msg.Size > maxMessageSize { + return fmt.Errorf("Message too long: %v > %v", msg.Size, maxMessageSize) + } + // Decode the handshake and make sure everything matches + if err := msg.Decode(&qligtStatus); err != nil { + return fmt.Errorf("%w: msg %v: %v", errDecode, msg, err) + } + if !qligtStatus.Server && len(qligtStatus.PSI) == 0 { + return fmt.Errorf("client connected without specifying PSI") + } + return nil +} diff --git a/eth/protocols/qlight/peer.go b/eth/protocols/qlight/peer.go new file mode 100644 index 0000000000..47684e4cc0 --- /dev/null +++ b/eth/protocols/qlight/peer.go @@ -0,0 +1,148 @@ +package qlight + +import ( + "math/big" + "sync" + + mapset "github.com/deckarep/golang-set" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/qlight" +) + +const ( + // maxKnownBlocks is the maximum block hashes to keep in the known list + // before starting to randomly evict them. + maxKnownBlocks = 1024 + + // maxQueuedBlocks is the maximum number of block propagations to queue up before + // dropping broadcasts. There's not much point in queueing stale blocks, so a few + // that might cover uncles should be enough. + maxQueuedBlocks = 4 +) + +// Peer is a collection of relevant information we have about a `snap` peer. +type Peer struct { + id string // Unique ID for the peer, cached + + *p2p.Peer // The embedded P2P package peer + rw p2p.MsgReadWriter + version uint // Protocol version negotiated + + logger log.Logger // Contextual logger with the peer id injected + + EthPeer *eth.Peer + + knownBlocks mapset.Set // Set of block hashes known to be known by this peer + queuedBlocks chan *blockPropagation // Queue of blocks to broadcast to the peer + queuedBlockAnns chan *types.Block // Queue of blocks to announce to the peer + + term chan struct{} // Termination channel to stop the broadcasters + lock sync.RWMutex // Mutex protecting the internal fields + + qlightServer bool + qlightPSI string + qlightToken string + + QLightPeriodicAuthFunc func() error +} + +// newPeer create a wrapper for a network connection and negotiated protocol +// version. +func NewPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter, ethPeer *eth.Peer) *Peer { + id := p.ID().String() + return &Peer{ + id: id, + Peer: p, + rw: rw, + version: version, + logger: log.New("peer", id[:8]), + EthPeer: ethPeer, + term: make(chan struct{}), + knownBlocks: mapset.NewSet(), + queuedBlocks: make(chan *blockPropagation, maxQueuedBlocks), + } +} + +func NewPeerWithBlockBroadcast(version uint, p *p2p.Peer, rw p2p.MsgReadWriter, ethPeer *eth.Peer) *Peer { + peer := NewPeer(version, p, rw, ethPeer) + go peer.broadcastBlocksQLightServer() + return peer +} + +// ID retrieves the peer's unique identifier. +func (p *Peer) ID() string { + return p.id +} + +// Version retrieves the peer's negoatiated `snap` protocol version. +func (p *Peer) Version() uint { + return p.version +} + +// Log overrides the P2P logget with the higher level one containing only the id. +func (p *Peer) Log() log.Logger { + return p.logger +} + +func (p *Peer) QLightServer() bool { + return p.qlightServer +} + +func (p *Peer) QLightPSI() string { + return p.qlightPSI +} + +func (p *Peer) QLightToken() string { + return p.qlightToken +} + +func (p *Peer) SendNewAuthToken(token string) error { + return p2p.Send(p.rw, QLightTokenUpdateMsg, &qLightTokenUpdateData{ + Token: token, + }) +} + +func (p *Peer) SendNewBlock(block *types.Block, td *big.Int) error { + // Mark all the block hash as known, but ensure we don't overflow our limits + for p.knownBlocks.Cardinality() >= maxKnownBlocks { + p.knownBlocks.Pop() + } + p.knownBlocks.Add(block.Hash()) + return p2p.Send(p.rw, eth.NewBlockMsg, ð.NewBlockPacket{ + Block: block, + TD: td, + }) +} + +func (p *Peer) SendBlockPrivateData(data []qlight.BlockPrivateData) error { + // Mark all the block hash as known, but ensure we don't overflow our limits + return p2p.Send(p.rw, QLightNewBlockPrivateDataMsg, data) +} + +// AsyncSendNewBlock queues an entire block for propagation to a remote peer. If +// the peer's broadcast queue is full, the event is silently dropped. +func (p *Peer) AsyncSendNewBlock(block *types.Block, td *big.Int, blockPrivateData *qlight.BlockPrivateData) { + select { + case p.queuedBlocks <- &blockPropagation{block: block, td: td, blockPrivateData: blockPrivateData}: + // Mark all the block hash as known, but ensure we don't overflow our limits + for p.knownBlocks.Cardinality() >= maxKnownBlocks { + p.knownBlocks.Pop() + } + p.knownBlocks.Add(block.Hash()) + default: + p.Log().Debug("Dropping block propagation", "number", block.NumberU64(), "hash", block.Hash()) + } +} + +// KnownBlock returns whether peer is known to already have a block. +func (p *Peer) KnownBlock(hash common.Hash) bool { + return p.knownBlocks.Contains(hash) +} + +func (p *Peer) Close() { + close(p.term) +} diff --git a/eth/protocols/qlight/protocol.go b/eth/protocols/qlight/protocol.go new file mode 100644 index 0000000000..0686189bd4 --- /dev/null +++ b/eth/protocols/qlight/protocol.go @@ -0,0 +1,44 @@ +package qlight + +import ( + "errors" + + "github.com/ethereum/go-ethereum/qlight" +) + +const ( + QLightStatusMsg = 0x11 + QLightTokenUpdateMsg = 0x12 + QLightNewBlockPrivateDataMsg = 0x13 +) + +const QLightProtocolLength = 20 + +const QLIGHT65 = 65 +const ProtocolName = "qlight" + +// maxMessageSize is the maximum cap on the size of a protocol message. +const maxMessageSize = 10 * 1024 * 1024 + +var ( + errNoStatusMsg = errors.New("no status message") + errMsgTooLarge = errors.New("message too long") + errDecode = errors.New("invalid message") + errInvalidMsgCode = errors.New("invalid message code") +) + +type qLightStatusData struct { + ProtocolVersion uint32 + Server bool + PSI string + Token string +} + +type qLightTokenUpdateData struct { + Token string +} + +type BlockPrivateDataPacket []qlight.BlockPrivateData + +func (*BlockPrivateDataPacket) Name() string { return "BlockPrivateData" } +func (*BlockPrivateDataPacket) Kind() byte { return QLightNewBlockPrivateDataMsg } diff --git a/extension/backend.go b/extension/backend.go index 7e75c2f95f..49fa6d0643 100644 --- a/extension/backend.go +++ b/extension/backend.go @@ -37,6 +37,8 @@ type PrivacyService struct { node *node.Node config *params.ChainConfig + + isQlightClient bool } var ( @@ -86,6 +88,14 @@ func New(stack *node.Node, ptm private.PrivateTransactionManager, manager *accou apiBackendHelper: apiBackendHelper, node: stack, config: config, + isQlightClient: false, + } + + apiSupport, ok := service.apiBackendHelper.(ethapi.ProxyAPISupport) + if ok { + if apiSupport.ProxyEnabled() { + service.isQlightClient = true + } } var err error @@ -152,6 +162,11 @@ func (service *PrivacyService) watchForNewContracts(psi types.PrivateStateIdenti isSender, _ := service.ptm.IsSender(enclaveKey) + if service.isQlightClient { + log.Debug("Extension: this is a light node and it does not handle self vote events", "address", newContractExtension.ContractExtended.Hex()) + return + } + if isSender { fetchedParties, err := service.ptm.GetParticipants(enclaveKey) if err != nil || len(fetchedParties) == 0 { @@ -207,6 +222,12 @@ func (service *PrivacyService) watchForCompletionEvents(psi types.PrivateStateId cb := func(l types.Log) { log.Debug("Extension: Received a completion event", "address", l.Address.Hex(), "blockNumber", l.BlockNumber) + + if service.isQlightClient { + log.Debug("Extension: this is a light node and it does not handle completion events", "address", l.Address.Hex()) + return + } + service.mu.Lock() defer func() { service.mu.Unlock() @@ -339,7 +360,7 @@ func (service *PrivacyService) apis() []rpc.API { { Namespace: "quorumExtension", Version: "1.0", - Service: NewPrivateExtensionAPI(service), + Service: NewPrivateExtensionProxyAPI(service), Public: true, }, } diff --git a/extension/proxy_api.go b/extension/proxy_api.go new file mode 100644 index 0000000000..c10efa4b97 --- /dev/null +++ b/extension/proxy_api.go @@ -0,0 +1,69 @@ +package extension + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" +) + +type PrivateExtensionProxyAPI struct { + PrivateExtensionAPI + proxyClient *rpc.Client +} + +func NewPrivateExtensionProxyAPI(privacyService *PrivacyService) interface{} { + apiSupport, ok := privacyService.apiBackendHelper.(ethapi.ProxyAPISupport) + if ok { + if apiSupport.ProxyEnabled() { + return &PrivateExtensionProxyAPI{ + PrivateExtensionAPI{privacyService}, + apiSupport.ProxyClient(), + } + } + } + return NewPrivateExtensionAPI(privacyService) +} + +// ActiveExtensionContracts returns the list of all currently outstanding extension contracts +func (api *PrivateExtensionProxyAPI) ActiveExtensionContracts(ctx context.Context) []ExtensionContract { + api.privacyService.mu.Lock() + defer api.privacyService.mu.Unlock() + + psi, err := api.privacyService.apiBackendHelper.PSMR().ResolveForUserContext(ctx) + if err != nil { + return nil + } + + extracted := make([]ExtensionContract, 0) + for _, contract := range api.privacyService.psiContracts[psi.ID] { + extracted = append(extracted, *contract) + } + + return extracted +} + +// ApproveContractExtension submits the vote to the specified extension management contract. The vote indicates whether to extend +// a given contract to a new participant or not +func (api *PrivateExtensionProxyAPI) ApproveExtension(ctx context.Context, addressToVoteOn common.Address, vote bool, txa ethapi.SendTxArgs) (string, error) { + log.Info("QLight - proxy enabled") + var result string + err := api.proxyClient.CallContext(ctx, &result, "quorumExtension_approveExtension", addressToVoteOn, vote, txa) + return result, err +} + +func (api *PrivateExtensionProxyAPI) ExtendContract(ctx context.Context, toExtend common.Address, newRecipientPtmPublicKey string, recipientAddr common.Address, txa ethapi.SendTxArgs) (string, error) { + log.Info("QLight - proxy enabled") + var result string + err := api.proxyClient.CallContext(ctx, &result, "quorumExtension_extendContract", toExtend, newRecipientPtmPublicKey, recipientAddr, txa) + return result, err +} + +func (api *PrivateExtensionProxyAPI) CancelExtension(ctx context.Context, extensionContract common.Address, txa ethapi.SendTxArgs) (string, error) { + log.Info("QLight - proxy enabled") + var result string + err := api.proxyClient.CallContext(ctx, &result, "quorumExtension_cancelExtension", extensionContract, txa) + return result, err +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 000b2a3fc8..d6070ee296 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -19,6 +19,7 @@ package ethapi import ( "bytes" "context" + "encoding/base64" "encoding/hex" "encoding/json" "errors" @@ -2757,6 +2758,88 @@ func (s *PublicBlockChainAPI) GetQuorumPayload(ctx context.Context, digestHex st return fmt.Sprintf("0x%x", data), nil } +func (s *PublicBlockChainAPI) GetQuorumPayloadExtra(ctx context.Context, digestHex string) (*engine.QuorumPayloadExtra, error) { + if !private.IsQuorumPrivacyEnabled() { + return nil, fmt.Errorf("PrivateTransactionManager is not enabled") + } + psm, err := s.b.PSMR().ResolveForUserContext(ctx) + if err != nil { + return nil, err + } + if len(digestHex) < 3 { + return nil, fmt.Errorf("Invalid digest hex") + } + if digestHex[:2] == "0x" { + digestHex = digestHex[2:] + } + b, err := hex.DecodeString(digestHex) + if err != nil { + return nil, err + } + + if len(b) != common.EncryptedPayloadHashLength { + return nil, fmt.Errorf("Expected a Quorum digest of length 64, but got %d", len(b)) + } + _, managedParties, data, extraMetaData, err := private.P.Receive(common.BytesToEncryptedPayloadHash(b)) + if err != nil { + return nil, err + } + if s.b.PSMR().NotIncludeAny(psm, managedParties...) { + return nil, nil + } + isSender := false + if len(psm.Addresses) == 0 { + isSender, _ = private.P.IsSender(common.BytesToEncryptedPayloadHash(b)) + } else { + isSender = !psm.NotIncludeAny(extraMetaData.Sender) + } + return &engine.QuorumPayloadExtra{ + Payload: fmt.Sprintf("0x%x", data), + ExtraMetaData: extraMetaData, + IsSender: isSender, + }, nil +} + +// DecryptQuorumPayload returns the decrypted version of the input transaction +func (s *PublicBlockChainAPI) DecryptQuorumPayload(ctx context.Context, payloadHex string) (*engine.QuorumPayloadExtra, error) { + if !private.IsQuorumPrivacyEnabled() { + return nil, fmt.Errorf("PrivateTransactionManager is not enabled") + } + psm, err := s.b.PSMR().ResolveForUserContext(ctx) + if err != nil { + return nil, err + } + if len(payloadHex) < 3 { + return nil, fmt.Errorf("Invalid payload hex") + } + if payloadHex[:2] == "0x" { + payloadHex = payloadHex[2:] + } + b, err := hex.DecodeString(payloadHex) + if err != nil { + return nil, err + } + + var payload common.DecryptRequest + if err := json.Unmarshal(b, &payload); err != nil { + return nil, err + } + // if we are MPS and the sender is not part of the resolved PSM - return empty + if len(psm.Addresses) != 0 && psm.NotIncludeAny(base64.StdEncoding.EncodeToString(payload.SenderKey)) { + return nil, nil + } + data, extraMetaData, err := private.P.DecryptPayload(payload) + if err != nil { + return nil, err + } + + return &engine.QuorumPayloadExtra{ + Payload: fmt.Sprintf("0x%x", data), + ExtraMetaData: extraMetaData, + IsSender: true, + }, nil +} + // Quorum // for raw private transaction, privateTxArgs.privateFrom will be updated with value from Tessera when payload is retrieved func checkAndHandlePrivateTransaction(ctx context.Context, b Backend, tx *types.Transaction, privateTxArgs *PrivateTxArgs, from common.Address, txnType TransactionType) (isPrivate bool, replaceDataWithHash bool, hash common.EncryptedPayloadHash, err error) { diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 1ed9438546..83fa55e016 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -117,7 +117,7 @@ func GetAPIs(apiBackend Backend) []rpc.API { }, { Namespace: "eth", Version: "1.0", - Service: NewPublicTransactionPoolAPI(apiBackend, nonceLock), + Service: NewPublicTransactionPoolProxyAPI(apiBackend, nonceLock), Public: true, }, { Namespace: "txpool", @@ -141,7 +141,7 @@ func GetAPIs(apiBackend Backend) []rpc.API { }, { Namespace: "personal", Version: "1.0", - Service: NewPrivateAccountAPI(apiBackend, nonceLock), + Service: NewPrivateAccountProxyAPI(apiBackend, nonceLock), Public: false, }, } diff --git a/internal/ethapi/proxy_api.go b/internal/ethapi/proxy_api.go new file mode 100644 index 0000000000..c5dcd47893 --- /dev/null +++ b/internal/ethapi/proxy_api.go @@ -0,0 +1,159 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ethapi + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" +) + +type ProxyAPISupport interface { + ProxyEnabled() bool + ProxyClient() *rpc.Client +} + +// PublicTransactionPoolAPI exposes methods for the RPC interface +type PublicTransactionPoolProxyAPI struct { + PublicTransactionPoolAPI + proxyClient *rpc.Client +} + +// NewPublicTransactionPoolAPI creates a new RPC service with methods specific for the transaction pool. +func NewPublicTransactionPoolProxyAPI(b Backend, nonceLock *AddrLocker) interface{} { + apiSupport, ok := b.(ProxyAPISupport) + if ok && apiSupport.ProxyEnabled() { + signer := types.LatestSigner(b.ChainConfig()) + return &PublicTransactionPoolProxyAPI{ + PublicTransactionPoolAPI{b, nonceLock, signer}, + apiSupport.ProxyClient(), + } + } + return NewPublicTransactionPoolAPI(b, nonceLock) +} + +func (s *PublicTransactionPoolProxyAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { + log.Info("QLight - proxy enabled") + var result common.Hash + err := s.proxyClient.CallContext(ctx, &result, "eth_sendTransaction", args) + return result, err +} + +func (s *PublicTransactionPoolProxyAPI) SendRawTransaction(ctx context.Context, encodedTx hexutil.Bytes) (common.Hash, error) { + log.Info("QLight - proxy enabled") + var result common.Hash + err := s.proxyClient.CallContext(ctx, &result, "eth_sendRawTransaction", encodedTx) + return result, err +} + +func (s *PublicTransactionPoolProxyAPI) SendRawPrivateTransaction(ctx context.Context, encodedTx hexutil.Bytes, args SendRawTxArgs) (common.Hash, error) { + log.Info("QLight - proxy enabled") + var result common.Hash + err := s.proxyClient.CallContext(ctx, &result, "eth_sendRawPrivateTransaction", encodedTx, args) + return result, err +} + +func (s *PublicTransactionPoolProxyAPI) FillTransaction(ctx context.Context, args SendTxArgs) (*SignTransactionResult, error) { + log.Info("QLight - proxy enabled") + var result SignTransactionResult + err := s.proxyClient.CallContext(ctx, &result, "eth_fillTransaction", args) + return &result, err +} + +func (s *PublicTransactionPoolProxyAPI) DistributePrivateTransaction(ctx context.Context, encodedTx hexutil.Bytes, args SendRawTxArgs) (string, error) { + log.Info("QLight - proxy enabled") + var result string + err := s.proxyClient.CallContext(ctx, &result, "eth_distributePrivateTransaction", encodedTx, args) + return result, err +} + +func (s *PublicTransactionPoolProxyAPI) Resend(ctx context.Context, sendArgs SendTxArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) { + log.Info("QLight - proxy enabled") + var result common.Hash + err := s.proxyClient.CallContext(ctx, &result, "eth_resend", sendArgs, gasPrice, gasLimit) + return result, err +} + +func (s *PublicTransactionPoolProxyAPI) SendTransactionAsync(ctx context.Context, args AsyncSendTxArgs) (common.Hash, error) { + log.Info("QLight - proxy enabled") + var result common.Hash + err := s.proxyClient.CallContext(ctx, &result, "eth_sendTransactionAsync", args) + return result, err +} + +func (s *PublicTransactionPoolProxyAPI) Sign(addr common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { + log.Info("QLight - proxy enabled") + var result hexutil.Bytes + err := s.proxyClient.Call(&result, "eth_sign", addr, data) + return result, err +} + +func (s *PublicTransactionPoolProxyAPI) SignTransaction(ctx context.Context, args SendTxArgs) (*SignTransactionResult, error) { + log.Info("QLight - proxy enabled") + var result SignTransactionResult + err := s.proxyClient.CallContext(ctx, &result, "eth_signTransaction", args) + return &result, err +} + +type PrivateAccountProxyAPI struct { + PrivateAccountAPI + proxyClient *rpc.Client +} + +func NewPrivateAccountProxyAPI(b Backend, nonceLock *AddrLocker) interface{} { + apiSupport, ok := b.(ProxyAPISupport) + if ok && apiSupport.ProxyEnabled() { + return &PrivateAccountProxyAPI{ + PrivateAccountAPI{ + am: b.AccountManager(), + nonceLock: nonceLock, + b: b, + }, + apiSupport.ProxyClient(), + } + } + return NewPrivateAccountAPI(b, nonceLock) +} + +func (s *PrivateAccountProxyAPI) SendTransaction(ctx context.Context, args SendTxArgs, passwd string) (common.Hash, error) { + log.Info("QLight - proxy enabled") + var result common.Hash + err := s.proxyClient.CallContext(ctx, &result, "personal_sendTransaction", args, passwd) + return result, err +} + +func (s *PrivateAccountProxyAPI) SignTransaction(ctx context.Context, args SendTxArgs, passwd string) (*SignTransactionResult, error) { + log.Info("QLight - proxy enabled") + var result SignTransactionResult + err := s.proxyClient.CallContext(ctx, &result, "personal_signTransaction", args, passwd) + return &result, err +} + +func (s *PrivateAccountProxyAPI) Sign(ctx context.Context, data hexutil.Bytes, addr common.Address, passwd string) (hexutil.Bytes, error) { + log.Info("QLight - proxy enabled") + var result hexutil.Bytes + err := s.proxyClient.CallContext(ctx, &result, "personal_sign", data, addr, passwd) + return result, err +} + +func (s *PrivateAccountProxyAPI) SignAndSendTransaction(ctx context.Context, args SendTxArgs, passwd string) (common.Hash, error) { + return s.SendTransaction(ctx, args, passwd) +} diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 6791a976ba..b615ddd48a 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -39,6 +39,7 @@ var Modules = map[string]string{ "quorumPermission": QUORUM_NODE_JS, "quorumExtension": Extension_JS, "plugin_account": Account_Plugin_Js, + "qlight": QLight_JS, } const ChequebookJs = ` @@ -223,10 +224,18 @@ web3._extend({ name: 'nodeInfo', getter: 'admin_nodeInfo' }), + new web3._extend.Property({ + name: 'qnodeInfo', + getter: 'admin_qnodeInfo' + }), new web3._extend.Property({ name: 'peers', getter: 'admin_peers' }), + new web3._extend.Property({ + name: 'qpeers', + getter: 'admin_qpeers' + }), new web3._extend.Property({ name: 'datadir', getter: 'admin_datadir' @@ -633,6 +642,18 @@ web3._extend({ params: 1, inputFormatter: [null] }), + new web3._extend.Method({ + name: 'getQuorumPayloadExtra', + call: 'eth_getQuorumPayloadExtra', + params: 1, + inputFormatter: [null] + }), + new web3._extend.Method({ + name: 'decryptQuorumPayload', + call: 'eth_decryptQuorumPayload', + params: 1, + inputFormatter: [null] + }), new web3._extend.Method({ name: 'getPSI', call: 'eth_getPSI', @@ -1258,6 +1279,33 @@ web3._extend({ }); ` +const QLight_JS = ` +web3._extend({ + property: 'qlight', + methods: + [ + new web3._extend.Method({ + name: 'getCurrentToken', + call: 'qlight_getCurrentToken', + params: 0 + }), + new web3._extend.Method({ + name: 'setCurrentToken', + call: 'qlight_setCurrentToken', + params: 1, + inputFormatter: [null] + }), + ], + properties: + [ + new web3._extend.Property({ + name: 'token', + getter: 'qlight_getCurrentToken' + }) + ] +}); +` + const Account_Plugin_Js = ` web3._extend({ property: 'plugin_account', diff --git a/node/api.go b/node/api.go index b3ca8c3c66..f3886f1edb 100644 --- a/node/api.go +++ b/node/api.go @@ -312,6 +312,14 @@ func (api *publicAdminAPI) Peers() ([]*p2p.PeerInfo, error) { return server.PeersInfo(), nil } +func (api *publicAdminAPI) Qpeers() ([]*p2p.PeerInfo, error) { + server := api.node.qserver + if server == nil { + return nil, nil + } + return server.PeersInfo(), nil +} + // NodeInfo retrieves all the information we know about the host node at the // protocol granularity. func (api *publicAdminAPI) NodeInfo() (*QuorumNodeInfo, error) { @@ -325,6 +333,16 @@ func (api *publicAdminAPI) NodeInfo() (*QuorumNodeInfo, error) { }, nil } +func (api *publicAdminAPI) QnodeInfo() (*QuorumNodeInfo, error) { + server := api.node.QServer() + if server == nil { + return nil, nil + } + return &QuorumNodeInfo{ + NodeInfo: server.NodeInfo(), + }, nil +} + // Datadir retrieves the current data directory the node is using. func (api *publicAdminAPI) Datadir() string { return api.node.DataDir() diff --git a/node/config.go b/node/config.go index b0d21faae4..7f5a7cc98d 100644 --- a/node/config.go +++ b/node/config.go @@ -80,6 +80,9 @@ type Config struct { // Configuration of peer-to-peer networking. P2P p2p.Config + // Quorum + QP2P *p2p.Config `toml:",omitempty"` + // KeyStoreDir is the file system folder that contains private keys. The directory can // be specified as a relative path, in which case it is resolved relative to the // current directory. @@ -240,6 +243,13 @@ func (c *Config) NodeDB() string { return c.ResolvePath(datadirNodeDatabase) } +func (c *Config) QNodeDB() string { + if c.DataDir == "" { + return "" // ephemeral + } + return c.ResolvePath("qnodes") +} + // DefaultIPCEndpoint returns the IPC path used by default. func DefaultIPCEndpoint(clientIdentifier string) string { if clientIdentifier == "" { diff --git a/node/node.go b/node/node.go index 2b91fbdb5b..6994072994 100644 --- a/node/node.go +++ b/node/node.go @@ -50,6 +50,7 @@ type Node struct { dirLock fileutil.Releaser // prevents concurrent use of instance directory stop chan struct{} // Channel to wait for termination notifications server *p2p.Server // Currently running P2P networking layer + qserver *p2p.Server // Currently running P2P networking layer for QLight startStopLock sync.Mutex // Start/Stop are protected by an additional lock state int // Tracks state of node lifecycle @@ -113,6 +114,9 @@ func New(conf *Config) (*Node, error) { databases: make(map[*closeTrackingDB]struct{}), pluginManager: plugin.NewEmptyPluginManager(), } + if conf.QP2P != nil { + node.qserver = &p2p.Server{Config: *conf.QP2P} + } // Register built-in APIs. node.rpcAPIs = append(node.rpcAPIs, node.apis()...) @@ -143,6 +147,13 @@ func New(conf *Config) (*Node, error) { if node.server.Config.NodeDatabase == "" { node.server.Config.NodeDatabase = node.config.NodeDB() } + if node.qserver != nil { + node.qserver.Config.PrivateKey = node.config.NodeKey() + node.qserver.Config.Name = "qgeth" + node.qserver.Config.Logger = node.log + node.qserver.Config.NodeDatabase = node.config.QNodeDB() + node.qserver.Config.DataDir = node.config.DataDir + } // Check HTTP/WS prefixes are valid. if err := validatePrefix("HTTP", conf.HTTPPathPrefix); err != nil { @@ -286,6 +297,11 @@ func (n *Node) openEndpoints() error { if err := n.server.Start(); err != nil { return convertFileLockError(err) } + if n.qserver != nil { + if err := n.qserver.Start(); err != nil { + return convertFileLockError(err) + } + } // start RPC endpoints err := n.startRPC() if err != nil { @@ -325,6 +341,9 @@ func (n *Node) stopServices(running []Lifecycle) error { // Stop p2p networking. n.server.Stop() + if n.qserver != nil { + n.qserver.Stop() + } if len(failure.Services) > 0 { return failure @@ -483,6 +502,16 @@ func (n *Node) RegisterProtocols(protocols []p2p.Protocol) { n.server.Protocols = append(n.server.Protocols, protocols...) } +func (n *Node) RegisterQProtocols(protocols []p2p.Protocol) { + n.lock.Lock() + defer n.lock.Unlock() + + if n.state != initializingState { + panic("can't register protocols on running/stopped node") + } + n.qserver.Protocols = append(n.qserver.Protocols, protocols...) +} + // RegisterAPIs registers the APIs a service provides on the node. func (n *Node) RegisterAPIs(apis []rpc.API) { n.lock.Lock() @@ -550,6 +579,13 @@ func (n *Node) Server() *p2p.Server { return n.server } +func (n *Node) QServer() *p2p.Server { + n.lock.Lock() + defer n.lock.Unlock() + + return n.qserver +} + // DataDir retrieves the current datadir used by the protocol stack. // Deprecated: No files should be stored in this directory, use InstanceDir instead. func (n *Node) DataDir() string { diff --git a/p2p/peer_error.go b/p2p/peer_error.go index f44c5b4c50..b6c86113b6 100644 --- a/p2p/peer_error.go +++ b/p2p/peer_error.go @@ -82,7 +82,8 @@ const ( DiscUnexpectedIdentity DiscSelf DiscReadTimeout - DiscSubprotocolError = 0x10 + DiscSubprotocolError = 0x10 + DiscAuthError DiscReason = 0x20 ) var discReasonToString = [...]string{ @@ -99,6 +100,7 @@ var discReasonToString = [...]string{ DiscSelf: "connected to self", DiscReadTimeout: "read timeout", DiscSubprotocolError: "subprotocol error", + DiscAuthError: "invalid auth error", } func (d DiscReason) String() string { diff --git a/p2p/qlight_transport.go b/p2p/qlight_transport.go new file mode 100644 index 0000000000..db7a428d9c --- /dev/null +++ b/p2p/qlight_transport.go @@ -0,0 +1,60 @@ +package p2p + +import ( + "crypto/ecdsa" + "crypto/tls" + "net" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p/rlpx" +) + +var qlightTLSConfig *tls.Config + +func SetQLightTLSConfig(config *tls.Config) { + qlightTLSConfig = config +} + +type tlsErrorTransport struct { + err error +} + +func (tr *tlsErrorTransport) doEncHandshake(prv *ecdsa.PrivateKey) (*ecdsa.PublicKey, error) { + return nil, tr.err +} +func (tr *tlsErrorTransport) doProtoHandshake(our *protoHandshake) (*protoHandshake, error) { + return nil, tr.err +} +func (tr *tlsErrorTransport) ReadMsg() (Msg, error) { return Msg{}, tr.err } +func (tr *tlsErrorTransport) WriteMsg(Msg) error { return tr.err } +func (tr *tlsErrorTransport) close(err error) {} + +func NewQlightClientTransport(conn net.Conn, dialDest *ecdsa.PublicKey) transport { + log.Info("Setting up qlight client transport") + if qlightTLSConfig != nil { + tlsConn := tls.Client(conn, qlightTLSConfig) + err := tlsConn.Handshake() + if err != nil { + log.Error("Failure setting up qlight client transport", "err", err) + return &tlsErrorTransport{err} + } + log.Info("Qlight client tls transport established successfully") + return &rlpxTransport{conn: rlpx.NewConn(tlsConn, dialDest)} + } + return &rlpxTransport{conn: rlpx.NewConn(conn, dialDest)} +} + +func NewQlightServerTransport(conn net.Conn, dialDest *ecdsa.PublicKey) transport { + log.Info("Setting up qlight server transport") + if qlightTLSConfig != nil { + tlsConn := tls.Server(conn, qlightTLSConfig) + err := tlsConn.Handshake() + if err != nil { + log.Error("Failure setting up qlight server transport", "err", err) + return &tlsErrorTransport{err} + } + log.Info("Qlight server tls transport established successfully") + return &rlpxTransport{conn: rlpx.NewConn(tlsConn, dialDest)} + } + return &rlpxTransport{conn: rlpx.NewConn(conn, dialDest)} +} diff --git a/p2p/server.go b/p2p/server.go index f7bbbd97e2..6fb7e72856 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -1193,3 +1193,7 @@ func (srv *Server) SetIsNodePermissioned(f func(*enode.Node, string, string, str srv.isNodePermissionedFunc = f } } + +func (srv *Server) SetNewTransportFunc(f func(net.Conn, *ecdsa.PublicKey) transport) { + srv.newTransport = f +} diff --git a/params/quorum.go b/params/quorum.go index 0eddcd52b4..9d20a5d706 100644 --- a/params/quorum.go +++ b/params/quorum.go @@ -2,7 +2,7 @@ package params const ( PERMISSIONED_CONFIG = "permissioned-nodes.json" - BLACKLIST_CONFIG = "disallowed-nodes.json" + DISALLOWED_CONFIG = "disallowed-nodes.json" PERMISSION_MODEL_CONFIG = "permission-config.json" DEFAULT_ORGCACHE_SIZE = 2000 DEFAULT_ROLECACHE_SIZE = 2500 diff --git a/permission/core/permissions.go b/permission/core/permissions.go index 9eaf4af08e..131dcba255 100644 --- a/permission/core/permissions.go +++ b/permission/core/permissions.go @@ -13,10 +13,39 @@ import ( "github.com/ethereum/go-ethereum/params" ) -// check if a given node is permissioned to connect to the change +type FileBasedPermissioning struct { + PermissionFile string + DisallowedFile string +} + +var defaultFileBasedPermissioning = FileBasedPermissioning{ + PermissionFile: params.PERMISSIONED_CONFIG, + DisallowedFile: params.DISALLOWED_CONFIG, +} + +func NewFileBasedPermissoningWithPrefix(prefix string) FileBasedPermissioning { + return FileBasedPermissioning{ + PermissionFile: prefix + "-" + params.PERMISSIONED_CONFIG, + DisallowedFile: prefix + "-" + params.DISALLOWED_CONFIG, + } +} + func IsNodePermissioned(nodename string, currentNode string, datadir string, direction string) bool { + return defaultFileBasedPermissioning.IsNodePermissioned(nodename, currentNode, datadir, direction) +} + +func isNodeDisallowed(nodeName, dataDir string) bool { + return defaultFileBasedPermissioning.isNodeDisallowed(nodeName, dataDir) +} + +func (fbp *FileBasedPermissioning) IsNodePermissionedEnode(node *enode.Node, nodename string, currentNode string, datadir string, direction string) bool { + return fbp.IsNodePermissioned(nodename, currentNode, datadir, direction) +} + +// check if a given node is permissioned to connect to the change +func (fbp *FileBasedPermissioning) IsNodePermissioned(nodename string, currentNode string, datadir string, direction string) bool { var permissionedList []string - nodes := ParsePermissionedNodes(datadir) + nodes := fbp.ParsePermissionedNodes(datadir) for _, v := range nodes { permissionedList = append(permissionedList, v.ID().String()) } @@ -25,8 +54,8 @@ func IsNodePermissioned(nodename string, currentNode string, datadir string, dir for _, v := range permissionedList { if v == nodename { log.Debug("IsNodePermissioned", "connection", direction, "nodename", nodename[:params.NODE_NAME_LENGTH], "ALLOWED-BY", currentNode[:params.NODE_NAME_LENGTH]) - // check if the node is blacklisted - return !isNodeBlackListed(nodename, datadir) + // check if the node is disallowed + return !fbp.isNodeDisallowed(nodename, datadir) } } log.Debug("IsNodePermissioned", "connection", direction, "nodename", nodename[:params.NODE_NAME_LENGTH], "DENIED-BY", currentNode[:params.NODE_NAME_LENGTH]) @@ -36,13 +65,13 @@ func IsNodePermissioned(nodename string, currentNode string, datadir string, dir //this is a shameless copy from the config.go. It is a duplication of the code //for the timebeing to allow reload of the permissioned nodes while the server is running -func ParsePermissionedNodes(DataDir string) []*enode.Node { +func (fbp *FileBasedPermissioning) ParsePermissionedNodes(DataDir string) []*enode.Node { - log.Debug("parsePermissionedNodes", "DataDir", DataDir, "file", params.PERMISSIONED_CONFIG) + log.Debug("parsePermissionedNodes", "DataDir", DataDir, "file", fbp.PermissionFile) - path := filepath.Join(DataDir, params.PERMISSIONED_CONFIG) + path := filepath.Join(DataDir, fbp.PermissionFile) if _, err := os.Stat(path); err != nil { - log.Error("Read Error for permissioned-nodes.json file. This is because 'permissioned' flag is specified but no permissioned-nodes.json file is present.", "err", err) + log.Error("Read Error for permissioned-nodes file. This is because 'permissioned' flag is specified but no permissioned-nodes file is present.", "fileName", fbp.PermissionFile, "err", err) return nil } // Load the nodes from the config file @@ -74,19 +103,19 @@ func ParsePermissionedNodes(DataDir string) []*enode.Node { return nodes } -// This function checks if the node is black-listed -func isNodeBlackListed(nodeName, dataDir string) bool { - log.Debug("isNodeBlackListed", "DataDir", dataDir, "file", params.BLACKLIST_CONFIG) +// This function checks if the node is disallowed +func (fbp *FileBasedPermissioning) isNodeDisallowed(nodeName, dataDir string) bool { + log.Debug("isNodeDisallowed", "DataDir", dataDir, "file", fbp.DisallowedFile) - path := filepath.Join(dataDir, params.BLACKLIST_CONFIG) + path := filepath.Join(dataDir, fbp.DisallowedFile) if _, err := os.Stat(path); err != nil { - log.Debug("Read Error for disallowed-nodes.json file. disallowed-nodes.json file is not present.", "err", err) + log.Debug("Read Error for disallowed-nodes file. disallowed-nodes file is not present.", "fileName", fbp.DisallowedFile, "err", err) return false } // Load the nodes from the config file blob, err := ioutil.ReadFile(path) if err != nil { - log.Debug("isNodeBlackListed: Failed to access nodes", "err", err) + log.Debug("isNodeDisallowed: Failed to access nodes", "err", err) return true } diff --git a/permission/core/permissions_test.go b/permission/core/permissions_test.go index dd04d76833..be766e54b3 100644 --- a/permission/core/permissions_test.go +++ b/permission/core/permissions_test.go @@ -28,7 +28,7 @@ func TestIsNodePermissioned(t *testing.T) { defer os.RemoveAll(d) writeNodeToFile(d, params.PERMISSIONED_CONFIG, node1) writeNodeToFile(d, params.PERMISSIONED_CONFIG, node3) - writeNodeToFile(d, params.BLACKLIST_CONFIG, node3) + writeNodeToFile(d, params.DISALLOWED_CONFIG, node3) n1, _ := enode.ParseV4(node1) n2, _ := enode.ParseV4(node2) n3, _ := enode.ParseV4(node3) @@ -73,7 +73,7 @@ func Test_isNodeBlackListed(t *testing.T) { d, _ := ioutil.TempDir("", "qdata") defer os.RemoveAll(d) - writeNodeToFile(d, params.BLACKLIST_CONFIG, node1) + writeNodeToFile(d, params.DISALLOWED_CONFIG, node1) n1, _ := enode.ParseV4(node1) n2, _ := enode.ParseV4(node2) @@ -96,8 +96,8 @@ func Test_isNodeBlackListed(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := isNodeBlackListed(tt.args.nodeName, tt.args.dataDir); got != tt.want { - t.Errorf("isNodeBlackListed() = %v, want %v", got, tt.want) + if got := isNodeDisallowed(tt.args.nodeName, tt.args.dataDir); got != tt.want { + t.Errorf("isNodeDisallowed() = %v, want %v", got, tt.want) } }) } diff --git a/permission/core/types/backend.go b/permission/core/types/backend.go index 470e77e0d5..b364cac7d6 100644 --- a/permission/core/types/backend.go +++ b/permission/core/types/backend.go @@ -197,11 +197,11 @@ func UpdateFile(fileName, enodeId string, operation NodeOperation, createFile bo return err } -//this function populates the black listed Node information into the disallowed-nodes.json file +//this function populates the disallowed Node information into the disallowed-nodes.json file func UpdateDisallowedNodes(dataDir, url string, operation NodeOperation) error { fileExists := true - path := filepath.Join(dataDir, params.BLACKLIST_CONFIG) + path := filepath.Join(dataDir, params.DISALLOWED_CONFIG) // Check if the file is existing. If the file is not existing create the file if _, err := os.Stat(path); err != nil { if _, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644); err != nil { diff --git a/private/engine/common.go b/private/engine/common.go index a31192a467..3ba8d104b5 100644 --- a/private/engine/common.go +++ b/private/engine/common.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" ) const ( @@ -48,6 +49,12 @@ type ExtraMetadata struct { MandatoryRecipients []string } +type QuorumPayloadExtra struct { + Payload string + ExtraMetaData *ExtraMetadata + IsSender bool +} + type Client struct { HttpClient *http.Client BaseURL string @@ -126,3 +133,15 @@ func NewFeatureSet(features ...PrivateTransactionManagerFeature) *FeatureSet { func (p *FeatureSet) HasFeature(feature PrivateTransactionManagerFeature) bool { return uint64(feature)&p.features != 0 } + +type ExtraMetaDataRLP ExtraMetadata + +func (emd *ExtraMetadata) DecodeRLP(stream *rlp.Stream) error { + // initialize the ACHashes map before passing it into the rlp decoder + emd.ACHashes = make(map[common.EncryptedPayloadHash]struct{}) + emdRLP := (*ExtraMetaDataRLP)(emd) + if err := stream.Decode(emdRLP); err != nil { + return err + } + return nil +} diff --git a/private/engine/qlightptm/caching_proxy.go b/private/engine/qlightptm/caching_proxy.go new file mode 100644 index 0000000000..435e9325c4 --- /dev/null +++ b/private/engine/qlightptm/caching_proxy.go @@ -0,0 +1,231 @@ +package qlightptm + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/private/cache" + "github.com/ethereum/go-ethereum/private/engine" + "github.com/ethereum/go-ethereum/rpc" + gocache "github.com/patrickmn/go-cache" +) + +type RPCClientCaller interface { + Call(result interface{}, method string, args ...interface{}) error +} + +type CachingProxyTxManager struct { + features *engine.FeatureSet + cache *gocache.Cache + rpcClient RPCClientCaller +} + +type CPItem struct { + cache.PrivateCacheItem + IsSender bool + IsEmpty bool +} + +func Is(ptm interface{}) bool { + _, ok := ptm.(*CachingProxyTxManager) + return ok +} + +func New() *CachingProxyTxManager { + return &CachingProxyTxManager{ + features: engine.NewFeatureSet(engine.PrivacyEnhancements), + cache: gocache.New(cache.DefaultExpiration, cache.CleanupInterval), + } +} + +func (t *CachingProxyTxManager) SetRPCClient(client *rpc.Client) { + t.rpcClient = client +} + +func (t *CachingProxyTxManager) SetRPCClientCaller(client RPCClientCaller) { + t.rpcClient = client +} + +func (t *CachingProxyTxManager) Send(data []byte, from string, to []string, extra *engine.ExtraMetadata) (string, []string, common.EncryptedPayloadHash, error) { + panic("implement me") +} + +func (t *CachingProxyTxManager) EncryptPayload(data []byte, from string, to []string, extra *engine.ExtraMetadata) ([]byte, error) { + panic("implement me") +} + +func (t *CachingProxyTxManager) StoreRaw(data []byte, from string) (common.EncryptedPayloadHash, error) { + panic("implement me") +} + +func (t *CachingProxyTxManager) SendSignedTx(data common.EncryptedPayloadHash, to []string, extra *engine.ExtraMetadata) (string, []string, []byte, error) { + panic("implement me") +} + +func (t *CachingProxyTxManager) Receive(hash common.EncryptedPayloadHash) (string, []string, []byte, *engine.ExtraMetadata, error) { + return t.receive(hash, false) +} + +// retrieve raw will not return information about medata. +// Related to SendSignedTx +func (t *CachingProxyTxManager) ReceiveRaw(hash common.EncryptedPayloadHash) ([]byte, string, *engine.ExtraMetadata, error) { + sender, _, data, extra, err := t.receive(hash, true) + return data, sender, extra, err +} + +// retrieve raw will not return information about medata +func (t *CachingProxyTxManager) receive(hash common.EncryptedPayloadHash, isRaw bool) (string, []string, []byte, *engine.ExtraMetadata, error) { + if common.EmptyEncryptedPayloadHash(hash) { + return "", nil, nil, nil, nil + } + cacheKey := hash.Hex() + if isRaw { + // indicate the cache item is incomplete, this will be fulfilled in SendSignedTx + cacheKey = fmt.Sprintf("%s-incomplete", cacheKey) + } + log.Info("qlight: retrieving private data from ptm cache") + if item, found := t.cache.Get(cacheKey); found { + cacheItem, ok := item.(CPItem) + if !ok { + return "", nil, nil, nil, fmt.Errorf("unknown cache item. expected type PrivateCacheItem") + } + if cacheItem.IsEmpty { + return "", nil, nil, nil, nil + } + return cacheItem.Extra.Sender, cacheItem.Extra.ManagedParties, cacheItem.Payload, &cacheItem.Extra, nil + } + + log.Info("qlight: no private data in ptm cache, retrieving from qlight server node") + var result engine.QuorumPayloadExtra + err := t.rpcClient.Call(&result, "eth_getQuorumPayloadExtra", hash.Hex()) + if err != nil { + return "", nil, nil, nil, err + } + if len(result.Payload) > 3 { + payloadBytes, err := hex.DecodeString(result.Payload[2:]) + if err != nil { + return "", nil, nil, nil, err + } + + toCache := &CachablePrivateTransactionData{ + Hash: hash, + QuorumPrivateTxData: result, + } + if err := t.Cache(toCache); err != nil { + log.Warn("unable to cache ptm data", "err", err) + } + + return result.ExtraMetaData.Sender, result.ExtraMetaData.ManagedParties, payloadBytes, result.ExtraMetaData, nil + } + return "", nil, nil, nil, nil +} + +func (t *CachingProxyTxManager) CheckAndAddEmptyToCache(hash common.EncryptedPayloadHash) { + if common.EmptyEncryptedPayloadHash(hash) { + return + } + cacheKey := hash.Hex() + + if _, found := t.cache.Get(cacheKey); found { + return + } + + t.cache.Set(cacheKey, CPItem{ + IsEmpty: true, + }, gocache.DefaultExpiration) +} + +type CachablePrivateTransactionData struct { + Hash common.EncryptedPayloadHash + QuorumPrivateTxData engine.QuorumPayloadExtra +} + +func (t *CachingProxyTxManager) Cache(privateTxData *CachablePrivateTransactionData) error { + if common.EmptyEncryptedPayloadHash(privateTxData.Hash) { + return nil + } + cacheKey := privateTxData.Hash.Hex() + + payload, err := hexutil.Decode(privateTxData.QuorumPrivateTxData.Payload) + if err != nil { + return err + } + + t.cache.Set(cacheKey, CPItem{ + PrivateCacheItem: cache.PrivateCacheItem{ + Payload: payload, + Extra: *privateTxData.QuorumPrivateTxData.ExtraMetaData, + }, + IsSender: privateTxData.QuorumPrivateTxData.IsSender, + }, gocache.DefaultExpiration) + + return nil +} + +// retrieve raw will not return information about medata +func (t *CachingProxyTxManager) DecryptPayload(payload common.DecryptRequest) ([]byte, *engine.ExtraMetadata, error) { + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, nil, err + } + payloadHex := fmt.Sprintf("0x%x", payloadBytes) + + var result engine.QuorumPayloadExtra + err = t.rpcClient.Call(&result, "eth_decryptQuorumPayload", payloadHex) + if err != nil { + return nil, nil, err + } + + responsePayloadHex := result.Payload + if len(responsePayloadHex) < 3 { + return nil, nil, fmt.Errorf("Invalid payload hex") + } + if responsePayloadHex[:2] == "0x" { + responsePayloadHex = responsePayloadHex[2:] + } + responsePayload, err := hex.DecodeString(responsePayloadHex) + if err != nil { + return nil, nil, err + } + return responsePayload, result.ExtraMetaData, nil +} + +func (t *CachingProxyTxManager) IsSender(data common.EncryptedPayloadHash) (bool, error) { + _, _, _, _, err := t.receive(data, false) + if err != nil { + return false, err + } + cacheKey := data.Hex() + if item, found := t.cache.Get(cacheKey); found { + cacheItem, ok := item.(CPItem) + if !ok { + return false, fmt.Errorf("unknown cache item. expected type PrivateCacheItem") + } + return cacheItem.IsSender, nil + } + return false, nil +} + +func (t *CachingProxyTxManager) GetParticipants(txHash common.EncryptedPayloadHash) ([]string, error) { + panic("implement me") +} + +func (t *CachingProxyTxManager) GetMandatory(txHash common.EncryptedPayloadHash) ([]string, error) { + panic("implement me") +} + +func (t *CachingProxyTxManager) Groups() ([]engine.PrivacyGroup, error) { + panic("implement me") +} + +func (t *CachingProxyTxManager) Name() string { + return "CachingP2PProxy" +} + +func (t *CachingProxyTxManager) HasFeature(f engine.PrivateTransactionManagerFeature) bool { + return t.features.HasFeature(f) +} diff --git a/private/engine/qlightptm/caching_proxy_test.go b/private/engine/qlightptm/caching_proxy_test.go new file mode 100644 index 0000000000..1ef0482cd6 --- /dev/null +++ b/private/engine/qlightptm/caching_proxy_test.go @@ -0,0 +1,232 @@ +package qlightptm + +import ( + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/private/engine" + "github.com/ethereum/go-ethereum/rpc" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestCachingProxy_ReceiveWithDataAvailableInCache(t *testing.T) { + assert := assert.New(t) + + cpTM := New() + cpTM.Cache(&CachablePrivateTransactionData{ + Hash: common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1")), + QuorumPrivateTxData: engine.QuorumPayloadExtra{ + Payload: fmt.Sprintf("0x%x", []byte("payload")), + ExtraMetaData: &engine.ExtraMetadata{ + ACHashes: nil, + ACMerkleRoot: common.Hash{}, + PrivacyFlag: 0, + ManagedParties: nil, + Sender: "sender1", + MandatoryRecipients: nil, + }, + IsSender: false, + }, + }) + + cpTM.Cache(&CachablePrivateTransactionData{ + Hash: common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash2")), + QuorumPrivateTxData: engine.QuorumPayloadExtra{ + Payload: fmt.Sprintf("0x%x", []byte("payload")), + ExtraMetaData: &engine.ExtraMetadata{ + ACHashes: nil, + ACMerkleRoot: common.Hash{}, + PrivacyFlag: 0, + ManagedParties: nil, + Sender: "sender2", + MandatoryRecipients: nil, + }, + IsSender: true, + }, + }) + + sender, _, payload, extraMetaData, err := cpTM.Receive(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1"))) + + assert.Nil(err) + assert.Equal("sender1", sender) + assert.Equal([]byte("payload"), payload) + assert.NotNil(extraMetaData) + + isSender, err := cpTM.IsSender(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1"))) + + assert.Nil(err) + assert.False(isSender) + + isSender, err = cpTM.IsSender(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash2"))) + + assert.Nil(err) + assert.True(isSender) +} + +func TestCachingProxy_ReceiveWithDataNotAvailableInCache(t *testing.T) { + assert := assert.New(t) + + cpTM := New() + cpTM.CheckAndAddEmptyToCache(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1"))) + + _, _, payload, extraMetaData, err := cpTM.Receive(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1"))) + + assert.Nil(err) + assert.Nil(payload) + assert.Nil(extraMetaData) + + isSender, err := cpTM.IsSender(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1"))) + + assert.Nil(err) + assert.False(isSender) +} + +func TestCachingProxy_ReceiveWithDataMissingFromCacheAvailableRemotely(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cpTM := New() + mockRPCClient := NewMockRPCClientCaller(ctrl) + mockRPCClient.EXPECT().Call(gomock.Any(), gomock.Eq("eth_getQuorumPayloadExtra"), + gomock.Eq(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1")).Hex())).DoAndReturn( + func(result interface{}, method string, args ...interface{}) error { + res, _ := result.(*engine.QuorumPayloadExtra) + res.IsSender = false + res.ExtraMetaData = &engine.ExtraMetadata{ + ACHashes: nil, + ACMerkleRoot: common.Hash{}, + PrivacyFlag: 0, + ManagedParties: nil, + Sender: "sender1", + MandatoryRecipients: nil, + } + res.Payload = fmt.Sprintf("0x%x", []byte("payload")) + return nil + }) + + cpTM.SetRPCClientCaller(mockRPCClient) + + sender, _, payload, extraMetaData, err := cpTM.Receive(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1"))) + + assert.Nil(err) + assert.Equal("sender1", sender) + assert.Equal([]byte("payload"), payload) + assert.NotNil(extraMetaData) +} + +func TestCachingProxy_ReceiveWithDataMissingFromCacheUnavailableRemotely(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cpTM := New() + mockRPCClient := NewMockRPCClientCaller(ctrl) + mockRPCClient.EXPECT().Call(gomock.Any(), gomock.Eq("eth_getQuorumPayloadExtra"), + gomock.Eq(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1")).Hex())).Return(nil) + + cpTM.SetRPCClientCaller(mockRPCClient) + + sender, _, payload, extraMetaData, err := cpTM.Receive(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1"))) + + assert.Nil(err) + assert.Equal("", sender) + assert.Nil(payload) + assert.Nil(extraMetaData) +} + +func TestCachingProxy_ReceiveWithDataMissingFromCacheAndRPCError(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cpTM := New() + mockRPCClient := NewMockRPCClientCaller(ctrl) + mockRPCClient.EXPECT().Call(gomock.Any(), gomock.Eq("eth_getQuorumPayloadExtra"), + gomock.Eq(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1")).Hex())).Return(fmt.Errorf("RPC Error")) + + cpTM.SetRPCClientCaller(mockRPCClient) + + _, _, _, _, err := cpTM.Receive(common.BytesToEncryptedPayloadHash([]byte("encryptedpayloadhash1"))) + + assert.EqualError(err, "RPC Error") +} + +func TestCachingProxy_DecryptPayloadSuccess(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cpTM := New() + mockRPCClient := NewMockRPCClientCaller(ctrl) + mockRPCClient.EXPECT().Call(gomock.Any(), gomock.Eq("eth_decryptQuorumPayload"), + gomock.Any()).DoAndReturn( + func(result interface{}, method string, args ...interface{}) error { + res, _ := result.(*engine.QuorumPayloadExtra) + res.IsSender = false + res.ExtraMetaData = &engine.ExtraMetadata{ + ACHashes: nil, + ACMerkleRoot: common.Hash{}, + PrivacyFlag: 0, + ManagedParties: nil, + Sender: "sender1", + MandatoryRecipients: nil, + } + res.Payload = fmt.Sprintf("0x%x", []byte("payload")) + return nil + }) + + cpTM.SetRPCClientCaller(mockRPCClient) + + payload, extraMetaData, err := cpTM.DecryptPayload(common.DecryptRequest{ + SenderKey: []byte("sender1"), + CipherText: []byte("ciphertext"), + CipherTextNonce: []byte("nonce"), + RecipientBoxes: nil, + RecipientNonce: []byte("nonce"), + RecipientKeys: nil, + }) + + assert.Nil(err) + assert.Equal("sender1", extraMetaData.Sender) + assert.Equal([]byte("payload"), payload) + assert.NotNil(extraMetaData) +} + +func TestCachingProxy_DecryptPayloadErrorInCall(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cpTM := New() + mockRPCClient := NewMockRPCClientCaller(ctrl) + mockRPCClient.EXPECT().Call(gomock.Any(), gomock.Eq("eth_decryptQuorumPayload"), + gomock.Any()).Return(fmt.Errorf("RPC Error")) + + cpTM.SetRPCClientCaller(mockRPCClient) + + _, _, err := cpTM.DecryptPayload(common.DecryptRequest{ + SenderKey: []byte("sender1"), + CipherText: []byte("ciphertext"), + CipherTextNonce: []byte("nonce"), + RecipientBoxes: nil, + RecipientNonce: []byte("nonce"), + RecipientKeys: nil, + }) + + assert.EqualError(err, "RPC Error") +} + +type HasRPCClient interface { + SetRPCClient(client *rpc.Client) +} + +func TestCachingProxy_HasRPCClient(t *testing.T) { + assert := assert.New(t) + var cpTM interface{} = New() + + _, ok := cpTM.(HasRPCClient) + assert.True(ok) +} diff --git a/private/engine/qlightptm/mock_caching_proxy.go b/private/engine/qlightptm/mock_caching_proxy.go new file mode 100644 index 0000000000..edb39d7342 --- /dev/null +++ b/private/engine/qlightptm/mock_caching_proxy.go @@ -0,0 +1,53 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: caching_proxy.go + +// Package qlightptm is a generated GoMock package. +package qlightptm + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockRPCClientCaller is a mock of RPCClientCaller interface. +type MockRPCClientCaller struct { + ctrl *gomock.Controller + recorder *MockRPCClientCallerMockRecorder +} + +// MockRPCClientCallerMockRecorder is the mock recorder for MockRPCClientCaller. +type MockRPCClientCallerMockRecorder struct { + mock *MockRPCClientCaller +} + +// NewMockRPCClientCaller creates a new mock instance. +func NewMockRPCClientCaller(ctrl *gomock.Controller) *MockRPCClientCaller { + mock := &MockRPCClientCaller{ctrl: ctrl} + mock.recorder = &MockRPCClientCallerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRPCClientCaller) EXPECT() *MockRPCClientCallerMockRecorder { + return m.recorder +} + +// Call mocks base method. +func (m *MockRPCClientCaller) Call(result interface{}, method string, args ...interface{}) error { + m.ctrl.T.Helper() + varargs := []interface{}{result, method} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Call", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Call indicates an expected call of Call. +func (mr *MockRPCClientCallerMockRecorder) Call(result, method interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{result, method}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockRPCClientCaller)(nil).Call), varargs...) +} diff --git a/private/private.go b/private/private.go index 54f6b988be..da51a1f2bb 100644 --- a/private/private.go +++ b/private/private.go @@ -14,7 +14,9 @@ import ( "github.com/ethereum/go-ethereum/private/engine" "github.com/ethereum/go-ethereum/private/engine/constellation" "github.com/ethereum/go-ethereum/private/engine/notinuse" + "github.com/ethereum/go-ethereum/private/engine/qlightptm" "github.com/ethereum/go-ethereum/private/engine/tessera" + "github.com/ethereum/go-ethereum/rpc" ) var ( @@ -24,6 +26,10 @@ var ( isPrivacyEnabled = false ) +type HasRPCClient interface { + SetRPCClient(client *rpc.Client) +} + type Identifiable interface { Name() string HasFeature(f engine.PrivateTransactionManagerFeature) bool @@ -64,8 +70,12 @@ func FromEnvironmentOrNil(name string) (http2.Config, error) { return cfg, nil } -func InitialiseConnection(cfg http2.Config) error { +func InitialiseConnection(cfg http2.Config, isLightClient bool) error { var err error + if isLightClient { + P, err = NewQLightTxManager() + return err + } P, err = NewPrivateTxManager(cfg) return err } @@ -74,6 +84,11 @@ func IsQuorumPrivacyEnabled() bool { return isPrivacyEnabled } +func NewQLightTxManager() (PrivateTransactionManager, error) { + isPrivacyEnabled = true + return qlightptm.New(), nil +} + func NewPrivateTxManager(cfg http2.Config) (PrivateTransactionManager, error) { if cfg.ConnectionType == http2.NoConnection { @@ -129,9 +144,13 @@ func selectPrivateTxManager(client *engine.Client) (PrivateTransactionManager, e // Retrieve the private transaction that is associated with a privacy marker transaction func FetchPrivateTransaction(data []byte) (*types.Transaction, []string, *engine.ExtraMetadata, error) { + return FetchPrivateTransactionWithPTM(data, P) +} + +func FetchPrivateTransactionWithPTM(data []byte, ptm PrivateTransactionManager) (*types.Transaction, []string, *engine.ExtraMetadata, error) { txHash := common.BytesToEncryptedPayloadHash(data) - _, managedParties, txData, metadata, err := P.Receive(txHash) + _, managedParties, txData, metadata, err := ptm.Receive(txHash) if err != nil { return nil, nil, nil, err } diff --git a/qlight/api.go b/qlight/api.go new file mode 100644 index 0000000000..a1721081eb --- /dev/null +++ b/qlight/api.go @@ -0,0 +1,33 @@ +package qlight + +import "github.com/ethereum/go-ethereum/rpc" + +type RunningPeerAuthUpdater interface { + UpdateTokenForRunningQPeers(token string) error +} + +type PrivateQLightAPI struct { + peerUpdater RunningPeerAuthUpdater + rpcClient *rpc.Client +} + +// NewPublicEthereumAPI creates a new Ethereum protocol API for full nodes. +func NewPrivateQLightAPI(peerUpdater RunningPeerAuthUpdater, rpcClient *rpc.Client) *PrivateQLightAPI { + return &PrivateQLightAPI{peerUpdater: peerUpdater, rpcClient: rpcClient} +} + +func (p *PrivateQLightAPI) SetCurrentToken(token string) { + SetCurrentToken(token) + p.peerUpdater.UpdateTokenForRunningQPeers(token) + if p.rpcClient != nil { + if len(token) > 0 { + p.rpcClient.WithHTTPCredentials(TokenCredentialsProvider) + } else { + p.rpcClient.WithHTTPCredentials(nil) + } + } +} + +func (p *PrivateQLightAPI) GetCurrentToken() string { + return GetCurrentToken() +} diff --git a/qlight/client_cache.go b/qlight/client_cache.go new file mode 100644 index 0000000000..2b813e35a4 --- /dev/null +++ b/qlight/client_cache.go @@ -0,0 +1,78 @@ +package qlight + +import ( + "bytes" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/private" + "github.com/ethereum/go-ethereum/private/cache" + "github.com/ethereum/go-ethereum/private/engine/qlightptm" + gocache "github.com/patrickmn/go-cache" +) + +type clientCache struct { + txCache CacheWithEmpty + privateBlockCache *gocache.Cache + db ethdb.Database +} + +func NewClientCache(db ethdb.Database) (PrivateClientCache, error) { + cachingTXManager, ok := private.P.(*qlightptm.CachingProxyTxManager) + if !ok { + return nil, fmt.Errorf("unable to initialize txCache") + } + return NewClientCacheWithEmpty(db, cachingTXManager, gocache.New(cache.DefaultExpiration, cache.CleanupInterval)) +} + +func NewClientCacheWithEmpty(db ethdb.Database, cacheWithEmpty CacheWithEmpty, gocache *gocache.Cache) (PrivateClientCache, error) { + return &clientCache{ + txCache: cacheWithEmpty, + privateBlockCache: gocache, + db: db, + }, nil +} + +func (c *clientCache) AddPrivateBlock(blockPrivateData BlockPrivateData) error { + for _, pvtTx := range blockPrivateData.PrivateTransactions { + if err := c.txCache.Cache(pvtTx.ToCachable()); err != nil { + return err + } + } + if !common.EmptyHash(blockPrivateData.PrivateStateRoot) { + return c.privateBlockCache.Add(blockPrivateData.BlockHash.ToBase64(), blockPrivateData.PrivateStateRoot.ToBase64(), gocache.DefaultExpiration) + } + return nil +} + +func (c *clientCache) CheckAndAddEmptyEntry(hash common.EncryptedPayloadHash) { + c.txCache.CheckAndAddEmptyToCache(hash) +} + +func (c *clientCache) ValidatePrivateStateRoot(blockHash common.Hash, publicStateRoot common.Hash) error { + dbPrivateStateRoot := rawdb.GetPrivateStateRoot(c.db, publicStateRoot) + + cachePrivateStateRootStr, found := c.privateBlockCache.Get(blockHash.ToBase64()) + if !found { + // this means that we don't have private data for this block or that the server does not have the corresponding + // private state root (which can happen when caching is enabled on the server side) + return nil + } + cachePrivateStateRootB64, ok := cachePrivateStateRootStr.(string) + if !ok { + return fmt.Errorf("Invalid private block cache item") + } + cachePrivateStateRoot, err := common.Base64ToHash(cachePrivateStateRootB64) + if err != nil { + return fmt.Errorf("Invalid encoding for private state root: %s", cachePrivateStateRootB64) + } + if !bytes.Equal(cachePrivateStateRoot.Bytes(), dbPrivateStateRoot.Bytes()) { + log.Error("QLight - Private state root hash check failure for block", "hash", blockHash) + return fmt.Errorf("Private root hash missmatch for block %s", blockHash) + } + log.Info("QLight - Private state root hash check successful for block", "hash", blockHash) + return nil +} diff --git a/qlight/config.go b/qlight/config.go new file mode 100644 index 0000000000..9e51cad4bc --- /dev/null +++ b/qlight/config.go @@ -0,0 +1,101 @@ +package qlight + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" + "strings" + + "github.com/ethereum/go-ethereum/plugin/security" +) + +type TLSConfig struct { + CACertFileName string + ClientCACertFileName string + ClientAuth int + CertFileName string + KeyFileName string + InsecureSkipVerify bool + CipherSuites string + ServerName string +} + +func NewTLSConfig(config *TLSConfig) (*tls.Config, error) { + if config.InsecureSkipVerify { + return &tls.Config{ + InsecureSkipVerify: true, + }, nil + } + var ( + CA_Pool *x509.CertPool + err error + ) + if len(config.CACertFileName) > 0 { + CA_Pool, err = x509.SystemCertPool() + if err != nil { + CA_Pool = x509.NewCertPool() + } + cert, err := os.ReadFile(config.CACertFileName) + if err != nil { + return nil, err + } + CA_Pool.AppendCertsFromPEM(cert) + } + + var ( + ClientCA_Pool *x509.CertPool + ClientAuth tls.ClientAuthType + ) + if len(config.ClientCACertFileName) > 0 { + ClientCA_Pool, err = x509.SystemCertPool() + if err != nil { + ClientCA_Pool = x509.NewCertPool() + } + cert, err := os.ReadFile(config.ClientCACertFileName) + if err != nil { + return nil, err + } + ClientCA_Pool.AppendCertsFromPEM(cert) + if config.ClientAuth < 0 || config.ClientAuth > 4 { + return nil, fmt.Errorf("Invalid ClientAuth value: %d", config.ClientAuth) + } + ClientAuth = tls.ClientAuthType(config.ClientAuth) + } + + var certificates []tls.Certificate + + if len(config.CertFileName) > 0 && len(config.KeyFileName) > 0 { + cert, err := tls.LoadX509KeyPair(config.CertFileName, config.KeyFileName) + if err != nil { + return nil, err + } + certificates = []tls.Certificate{cert} + } + + var CipherSuites []uint16 + if len(config.CipherSuites) > 0 { + cipherSuitesStrings := strings.FieldsFunc(config.CipherSuites, func(r rune) bool { + return r == ',' + }) + if len(cipherSuitesStrings) > 0 { + cipherSuiteList := make(security.CipherSuiteList, len(cipherSuitesStrings)) + for i, s := range cipherSuitesStrings { + cipherSuiteList[i] = security.CipherSuite(strings.TrimSpace(s)) + } + CipherSuites, err = cipherSuiteList.ToUint16Array() + if err != nil { + return nil, err + } + } + } + + return &tls.Config{ + RootCAs: CA_Pool, + Certificates: certificates, + ServerName: config.ServerName, + ClientCAs: ClientCA_Pool, + ClientAuth: ClientAuth, + CipherSuites: CipherSuites, + }, nil +} diff --git a/qlight/server.go b/qlight/server.go new file mode 100644 index 0000000000..03f0ef9b49 --- /dev/null +++ b/qlight/server.go @@ -0,0 +1,187 @@ +package qlight + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/mps" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/multitenancy" + "github.com/ethereum/go-ethereum/plugin/security" + "github.com/ethereum/go-ethereum/private" + "github.com/ethereum/go-ethereum/rpc" +) + +type privateBlockDataResolverImpl struct { + privateStateManager mps.PrivateStateManager + ptm private.PrivateTransactionManager +} + +func NewPrivateBlockDataResolver(privateStateManager mps.PrivateStateManager, ptm private.PrivateTransactionManager) PrivateBlockDataResolver { + return &privateBlockDataResolverImpl{privateStateManager: privateStateManager, ptm: ptm} +} + +func (p *privateBlockDataResolverImpl) PrepareBlockPrivateData(block *types.Block, psi string) (*BlockPrivateData, error) { + PSI := types.PrivateStateIdentifier(psi) + var pvtTxs []PrivateTransactionData + psm, err := p.privateStateManager.ResolveForUserContext(rpc.WithPrivateStateIdentifier(context.Background(), PSI)) + if err != nil { + return nil, err + } + for _, tx := range block.Transactions() { + if tx.IsPrivacyMarker() { + ptd, err := p.fetchPrivateData(tx.Data(), psm) + if err != nil { + return nil, err + } + if ptd != nil { + pvtTxs = append(pvtTxs, *ptd) + } + + innerTx, _, _, _ := private.FetchPrivateTransactionWithPTM(tx.Data(), p.ptm) + if innerTx != nil { + tx = innerTx + } + } + + if tx.IsPrivate() { + ptd, err := p.fetchPrivateData(tx.Data(), psm) + if err != nil { + return nil, err + } + if ptd != nil { + pvtTxs = append(pvtTxs, *ptd) + } + } + } + if len(pvtTxs) == 0 { + return nil, nil + } + + var privateStateRoot = common.Hash{} + + stateRepo, err := p.privateStateManager.StateRepository(block.Root()) + if err != nil { + log.Debug("Unable to retrieve private state repo while preparing the private block data", "block.No", block.Number(), "psi", psi, "err", err) + } else { + privateStateRoot, err = stateRepo.PrivateStateRoot(PSI) + if err != nil { + log.Debug("Unable to retrieve private state root while preparing the private block data", "block.No", block.Number(), "psi", psi, "err", err) + } + } + + return &BlockPrivateData{ + BlockHash: block.Hash(), + PSI: PSI, + PrivateStateRoot: privateStateRoot, + PrivateTransactions: pvtTxs, + }, nil +} + +func (p *privateBlockDataResolverImpl) fetchPrivateData(encryptedPayloadHash []byte, psm *mps.PrivateStateMetadata) (*PrivateTransactionData, error) { + txHash := common.BytesToEncryptedPayloadHash(encryptedPayloadHash) + _, _, privateTx, extra, err := p.ptm.Receive(txHash) + if err != nil { + return nil, err + } + // we're not party to this transaction + if privateTx == nil { + return nil, nil + } + if p.privateStateManager.NotIncludeAny(psm, extra.ManagedParties...) { + return nil, nil + } + + extra.ManagedParties = psm.FilterAddresses(extra.ManagedParties...) + + ptd := PrivateTransactionData{ + Hash: &txHash, + Payload: privateTx, + Extra: extra, + IsSender: false, + } + if len(psm.Addresses) == 0 { + // this is not an MPS node so we have to ask tessera + ptd.IsSender, err = p.ptm.IsSender(txHash) + if err != nil { + return nil, err + } + } else { + // this is an MPS node so we can speed up the IsSender logic by checking the addresses in the private state metadata + ptd.IsSender = !psm.NotIncludeAny(extra.Sender) + } + + return &ptd, nil +} + +type authProviderImpl struct { + privateStateManager mps.PrivateStateManager + authManagerProvider AuthManagerProvider + authManager security.AuthenticationManager + enabled bool +} + +func NewAuthProvider(privateStateManager mps.PrivateStateManager, authManagerProvider AuthManagerProvider) AuthProvider { + return &authProviderImpl{ + privateStateManager: privateStateManager, + authManagerProvider: authManagerProvider, + enabled: false, + } +} + +func (a *authProviderImpl) Initialize() error { + if a.authManagerProvider != nil { + a.authManager = a.authManagerProvider() + if a.authManager == nil { + return nil + } + authEnabled, err := a.authManager.IsEnabled(context.Background()) + if err != nil { + return err + } + a.enabled = authEnabled + } + return nil +} + +func (a *authProviderImpl) Authorize(token string, psi string) error { + if !a.enabled { + return nil + } + + authToken, err := a.authManager.Authenticate(context.Background(), token) + if err != nil { + return err + } + PSI := types.PrivateStateIdentifier(psi) + // check that we have access to the relevant PSI + psiAuth, err := multitenancy.IsPSIAuthorized(authToken, PSI) + if err != nil { + return err + } + if !psiAuth { + return fmt.Errorf("PSI not authorized") + } + // check that we have access to qlight://p2p , rpc://eth_* + qlightP2P := false + rpcETH := false + for _, ga := range authToken.GetAuthorities() { + if ga.GetRaw() == "p2p://qlight" { + qlightP2P = true + } + if ga.GetRaw() == "rpc://eth_*" { + rpcETH = true + } + } + if !qlightP2P || !rpcETH { + return fmt.Errorf("The P2P token does not have the necessary authorization p2p=%v rpcETH=%v", qlightP2P, rpcETH) + } + // try to resolve the PSI + _, err = a.privateStateManager.ResolveForUserContext(rpc.WithPrivateStateIdentifier(context.Background(), PSI)) + if err != nil { + return fmt.Errorf("QLight auth error: %w", err) + } + return nil +} diff --git a/qlight/test/client_cache_test.go b/qlight/test/client_cache_test.go new file mode 100644 index 0000000000..e41f8a9f8f --- /dev/null +++ b/qlight/test/client_cache_test.go @@ -0,0 +1,143 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/private/cache" + "github.com/ethereum/go-ethereum/private/engine" + "github.com/ethereum/go-ethereum/private/engine/qlightptm" + "github.com/ethereum/go-ethereum/qlight" + "github.com/golang/mock/gomock" + gocache "github.com/patrickmn/go-cache" + "github.com/stretchr/testify/assert" +) + +func TestClientCache_AddPrivateBlock(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memDB := rawdb.NewMemoryDatabase() + cacheWithEmpty := NewMockCacheWithEmpty(ctrl) + gocache := gocache.New(cache.DefaultExpiration, cache.CleanupInterval) + + clientCache, _ := qlight.NewClientCacheWithEmpty(memDB, cacheWithEmpty, gocache) + + txHash1 := common.BytesToEncryptedPayloadHash([]byte("TXHash1")) + ptd1 := qlight.PrivateTransactionData{ + Hash: &txHash1, + Payload: []byte("payload"), + Extra: &engine.ExtraMetadata{ + ACHashes: nil, + ACMerkleRoot: common.Hash{}, + PrivacyFlag: 0, + ManagedParties: nil, + Sender: "", + MandatoryRecipients: nil, + }, + IsSender: false, + } + blockPrivateData := qlight.BlockPrivateData{ + BlockHash: common.StringToHash("BlockHash"), + PSI: "", + PrivateStateRoot: common.StringToHash("PrivateStateRoot"), + PrivateTransactions: []qlight.PrivateTransactionData{ptd1}, + } + + var capturedCacheItem *qlightptm.CachablePrivateTransactionData + cacheWithEmpty.EXPECT().Cache(gomock.Any()).DoAndReturn(func(privateTxData *qlightptm.CachablePrivateTransactionData) error { + capturedCacheItem = privateTxData + return nil + }) + + clientCache.AddPrivateBlock(blockPrivateData) + + assert.Equal(fmt.Sprintf("0x%x", ptd1.Payload), capturedCacheItem.QuorumPrivateTxData.Payload) + assert.Equal(ptd1.Hash, &capturedCacheItem.Hash) + + psr, _ := gocache.Get(blockPrivateData.BlockHash.ToBase64()) + assert.Equal(blockPrivateData.PrivateStateRoot.ToBase64(), psr) +} + +func TestClientCache_ValidatePrivateStateRootSuccess(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memDB := rawdb.NewMemoryDatabase() + cacheWithEmpty := NewMockCacheWithEmpty(ctrl) + gocache := gocache.New(cache.DefaultExpiration, cache.CleanupInterval) + + clientCache, _ := qlight.NewClientCacheWithEmpty(memDB, cacheWithEmpty, gocache) + + publicStateRoot := common.StringToHash("PublicStateRoot") + blockPrivateData := qlight.BlockPrivateData{ + BlockHash: common.StringToHash("BlockHash"), + PSI: "", + PrivateStateRoot: common.StringToHash("PrivateStateRoot"), + PrivateTransactions: []qlight.PrivateTransactionData{}, + } + + clientCache.AddPrivateBlock(blockPrivateData) + rawdb.WritePrivateStateRoot(memDB, publicStateRoot, blockPrivateData.PrivateStateRoot) + + err := clientCache.ValidatePrivateStateRoot(blockPrivateData.BlockHash, publicStateRoot) + + assert.Nil(err) +} + +func TestClientCache_ValidatePrivateStateRootMismatch(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memDB := rawdb.NewMemoryDatabase() + cacheWithEmpty := NewMockCacheWithEmpty(ctrl) + gocache := gocache.New(cache.DefaultExpiration, cache.CleanupInterval) + + clientCache, _ := qlight.NewClientCacheWithEmpty(memDB, cacheWithEmpty, gocache) + + publicStateRoot := common.StringToHash("PublicStateRoot") + blockPrivateData := qlight.BlockPrivateData{ + BlockHash: common.StringToHash("BlockHash"), + PSI: "", + PrivateStateRoot: common.StringToHash("PrivateStateRoot"), + PrivateTransactions: []qlight.PrivateTransactionData{}, + } + + clientCache.AddPrivateBlock(blockPrivateData) + rawdb.WritePrivateStateRoot(memDB, publicStateRoot, common.StringToHash("Mismatch")) + + err := clientCache.ValidatePrivateStateRoot(blockPrivateData.BlockHash, publicStateRoot) + + assert.Error(err) +} + +func TestClientCache_ValidatePrivateStateRootNoDataInClientCache(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memDB := rawdb.NewMemoryDatabase() + cacheWithEmpty := NewMockCacheWithEmpty(ctrl) + gocache := gocache.New(cache.DefaultExpiration, cache.CleanupInterval) + + clientCache, _ := qlight.NewClientCacheWithEmpty(memDB, cacheWithEmpty, gocache) + + publicStateRoot := common.StringToHash("PublicStateRoot") + blockPrivateData := qlight.BlockPrivateData{ + BlockHash: common.StringToHash("BlockHash"), + PSI: "", + PrivateStateRoot: common.StringToHash("PrivateStateRoot"), + PrivateTransactions: []qlight.PrivateTransactionData{}, + } + + rawdb.WritePrivateStateRoot(memDB, publicStateRoot, blockPrivateData.PrivateStateRoot) + + err := clientCache.ValidatePrivateStateRoot(blockPrivateData.BlockHash, publicStateRoot) + + assert.Nil(err) +} diff --git a/qlight/test/mock_types.go b/qlight/test/mock_types.go new file mode 100644 index 0000000000..9e9f5cf1f4 --- /dev/null +++ b/qlight/test/mock_types.go @@ -0,0 +1,253 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: types.go + +// Package qlighttest is a generated GoMock package. +package test + +import ( + reflect "reflect" + + common "github.com/ethereum/go-ethereum/common" + types "github.com/ethereum/go-ethereum/core/types" + qlightptm "github.com/ethereum/go-ethereum/private/engine/qlightptm" + qlight "github.com/ethereum/go-ethereum/qlight" + gomock "github.com/golang/mock/gomock" +) + +// MockPrivateStateRootHashValidator is a mock of PrivateStateRootHashValidator interface. +type MockPrivateStateRootHashValidator struct { + ctrl *gomock.Controller + recorder *MockPrivateStateRootHashValidatorMockRecorder +} + +// MockPrivateStateRootHashValidatorMockRecorder is the mock recorder for MockPrivateStateRootHashValidator. +type MockPrivateStateRootHashValidatorMockRecorder struct { + mock *MockPrivateStateRootHashValidator +} + +// NewMockPrivateStateRootHashValidator creates a new mock instance. +func NewMockPrivateStateRootHashValidator(ctrl *gomock.Controller) *MockPrivateStateRootHashValidator { + mock := &MockPrivateStateRootHashValidator{ctrl: ctrl} + mock.recorder = &MockPrivateStateRootHashValidatorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPrivateStateRootHashValidator) EXPECT() *MockPrivateStateRootHashValidatorMockRecorder { + return m.recorder +} + +// ValidatePrivateStateRoot mocks base method. +func (m *MockPrivateStateRootHashValidator) ValidatePrivateStateRoot(blockHash, blockPublicStateRoot common.Hash) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidatePrivateStateRoot", blockHash, blockPublicStateRoot) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidatePrivateStateRoot indicates an expected call of ValidatePrivateStateRoot. +func (mr *MockPrivateStateRootHashValidatorMockRecorder) ValidatePrivateStateRoot(blockHash, blockPublicStateRoot interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatePrivateStateRoot", reflect.TypeOf((*MockPrivateStateRootHashValidator)(nil).ValidatePrivateStateRoot), blockHash, blockPublicStateRoot) +} + +// MockPrivateClientCache is a mock of PrivateClientCache interface. +type MockPrivateClientCache struct { + ctrl *gomock.Controller + recorder *MockPrivateClientCacheMockRecorder +} + +// MockPrivateClientCacheMockRecorder is the mock recorder for MockPrivateClientCache. +type MockPrivateClientCacheMockRecorder struct { + mock *MockPrivateClientCache +} + +// NewMockPrivateClientCache creates a new mock instance. +func NewMockPrivateClientCache(ctrl *gomock.Controller) *MockPrivateClientCache { + mock := &MockPrivateClientCache{ctrl: ctrl} + mock.recorder = &MockPrivateClientCacheMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPrivateClientCache) EXPECT() *MockPrivateClientCacheMockRecorder { + return m.recorder +} + +// AddPrivateBlock mocks base method. +func (m *MockPrivateClientCache) AddPrivateBlock(blockPrivateData qlight.BlockPrivateData) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPrivateBlock", blockPrivateData) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddPrivateBlock indicates an expected call of AddPrivateBlock. +func (mr *MockPrivateClientCacheMockRecorder) AddPrivateBlock(blockPrivateData interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPrivateBlock", reflect.TypeOf((*MockPrivateClientCache)(nil).AddPrivateBlock), blockPrivateData) +} + +// CheckAndAddEmptyEntry mocks base method. +func (m *MockPrivateClientCache) CheckAndAddEmptyEntry(hash common.EncryptedPayloadHash) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "CheckAndAddEmptyEntry", hash) +} + +// CheckAndAddEmptyEntry indicates an expected call of CheckAndAddEmptyEntry. +func (mr *MockPrivateClientCacheMockRecorder) CheckAndAddEmptyEntry(hash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckAndAddEmptyEntry", reflect.TypeOf((*MockPrivateClientCache)(nil).CheckAndAddEmptyEntry), hash) +} + +// ValidatePrivateStateRoot mocks base method. +func (m *MockPrivateClientCache) ValidatePrivateStateRoot(blockHash, blockPublicStateRoot common.Hash) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidatePrivateStateRoot", blockHash, blockPublicStateRoot) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidatePrivateStateRoot indicates an expected call of ValidatePrivateStateRoot. +func (mr *MockPrivateClientCacheMockRecorder) ValidatePrivateStateRoot(blockHash, blockPublicStateRoot interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatePrivateStateRoot", reflect.TypeOf((*MockPrivateClientCache)(nil).ValidatePrivateStateRoot), blockHash, blockPublicStateRoot) +} + +// MockPrivateBlockDataResolver is a mock of PrivateBlockDataResolver interface. +type MockPrivateBlockDataResolver struct { + ctrl *gomock.Controller + recorder *MockPrivateBlockDataResolverMockRecorder +} + +// MockPrivateBlockDataResolverMockRecorder is the mock recorder for MockPrivateBlockDataResolver. +type MockPrivateBlockDataResolverMockRecorder struct { + mock *MockPrivateBlockDataResolver +} + +// NewMockPrivateBlockDataResolver creates a new mock instance. +func NewMockPrivateBlockDataResolver(ctrl *gomock.Controller) *MockPrivateBlockDataResolver { + mock := &MockPrivateBlockDataResolver{ctrl: ctrl} + mock.recorder = &MockPrivateBlockDataResolverMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPrivateBlockDataResolver) EXPECT() *MockPrivateBlockDataResolverMockRecorder { + return m.recorder +} + +// PrepareBlockPrivateData mocks base method. +func (m *MockPrivateBlockDataResolver) PrepareBlockPrivateData(block *types.Block, psi string) (*qlight.BlockPrivateData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrepareBlockPrivateData", block, psi) + ret0, _ := ret[0].(*qlight.BlockPrivateData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PrepareBlockPrivateData indicates an expected call of PrepareBlockPrivateData. +func (mr *MockPrivateBlockDataResolverMockRecorder) PrepareBlockPrivateData(block, psi interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareBlockPrivateData", reflect.TypeOf((*MockPrivateBlockDataResolver)(nil).PrepareBlockPrivateData), block, psi) +} + +// MockAuthProvider is a mock of AuthProvider interface. +type MockAuthProvider struct { + ctrl *gomock.Controller + recorder *MockAuthProviderMockRecorder +} + +// MockAuthProviderMockRecorder is the mock recorder for MockAuthProvider. +type MockAuthProviderMockRecorder struct { + mock *MockAuthProvider +} + +// NewMockAuthProvider creates a new mock instance. +func NewMockAuthProvider(ctrl *gomock.Controller) *MockAuthProvider { + mock := &MockAuthProvider{ctrl: ctrl} + mock.recorder = &MockAuthProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAuthProvider) EXPECT() *MockAuthProviderMockRecorder { + return m.recorder +} + +// Authorize mocks base method. +func (m *MockAuthProvider) Authorize(token, psi string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Authorize", token, psi) + ret0, _ := ret[0].(error) + return ret0 +} + +// Authorize indicates an expected call of Authorize. +func (mr *MockAuthProviderMockRecorder) Authorize(token, psi interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorize", reflect.TypeOf((*MockAuthProvider)(nil).Authorize), token, psi) +} + +// Initialize mocks base method. +func (m *MockAuthProvider) Initialize() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Initialize") + ret0, _ := ret[0].(error) + return ret0 +} + +// Initialize indicates an expected call of Initialize. +func (mr *MockAuthProviderMockRecorder) Initialize() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Initialize", reflect.TypeOf((*MockAuthProvider)(nil).Initialize)) +} + +// MockCacheWithEmpty is a mock of CacheWithEmpty interface. +type MockCacheWithEmpty struct { + ctrl *gomock.Controller + recorder *MockCacheWithEmptyMockRecorder +} + +// MockCacheWithEmptyMockRecorder is the mock recorder for MockCacheWithEmpty. +type MockCacheWithEmptyMockRecorder struct { + mock *MockCacheWithEmpty +} + +// NewMockCacheWithEmpty creates a new mock instance. +func NewMockCacheWithEmpty(ctrl *gomock.Controller) *MockCacheWithEmpty { + mock := &MockCacheWithEmpty{ctrl: ctrl} + mock.recorder = &MockCacheWithEmptyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCacheWithEmpty) EXPECT() *MockCacheWithEmptyMockRecorder { + return m.recorder +} + +// Cache mocks base method. +func (m *MockCacheWithEmpty) Cache(privateTxData *qlightptm.CachablePrivateTransactionData) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Cache", privateTxData) + ret0, _ := ret[0].(error) + return ret0 +} + +// Cache indicates an expected call of Cache. +func (mr *MockCacheWithEmptyMockRecorder) Cache(privateTxData interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cache", reflect.TypeOf((*MockCacheWithEmpty)(nil).Cache), privateTxData) +} + +// CheckAndAddEmptyToCache mocks base method. +func (m *MockCacheWithEmpty) CheckAndAddEmptyToCache(hash common.EncryptedPayloadHash) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "CheckAndAddEmptyToCache", hash) +} + +// CheckAndAddEmptyToCache indicates an expected call of CheckAndAddEmptyToCache. +func (mr *MockCacheWithEmptyMockRecorder) CheckAndAddEmptyToCache(hash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckAndAddEmptyToCache", reflect.TypeOf((*MockCacheWithEmpty)(nil).CheckAndAddEmptyToCache), hash) +} diff --git a/qlight/test/server_test.go b/qlight/test/server_test.go new file mode 100644 index 0000000000..f433a320c7 --- /dev/null +++ b/qlight/test/server_test.go @@ -0,0 +1,534 @@ +package test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/mps" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/plugin/security" + "github.com/ethereum/go-ethereum/private" + "github.com/ethereum/go-ethereum/private/engine" + "github.com/ethereum/go-ethereum/qlight" + "github.com/golang/mock/gomock" + "github.com/jpmorganchase/quorum-security-plugin-sdk-go/proto" + "github.com/stretchr/testify/assert" +) + +func TestPrivateBlockDataResolverImpl_PrepareBlockPrivateData_EmptyBlock(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockpsm := mps.NewMockPrivateStateManager(ctrl) + mockptm := private.NewMockPrivateTransactionManager(ctrl) + + saved := private.P + defer func() { + private.P = saved + }() + private.P = mockptm + + mockptm.EXPECT().HasFeature(engine.MultiplePrivateStates).Return(true) + mockptm.EXPECT().Groups().Return(PrivacyGroups, nil).AnyTimes() + + mockpsm.EXPECT().ResolveForUserContext(gomock.Any()).Return(PSI1PSM, nil).AnyTimes() + + pbdr := qlight.NewPrivateBlockDataResolver(mockpsm, mockptm) + blocks, _, _ := buildTestChainWithZeroTxPerBlock(1, params.QuorumMPSTestChainConfig) + + blockPrivateData, err := pbdr.PrepareBlockPrivateData(blocks[0], PSI1PSM.ID.String()) + + assert.Nil(err) + assert.Nil(blockPrivateData) +} + +func TestPrivateBlockDataResolverImpl_PrepareBlockPrivateData_PartyTransaction(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockpsm := mps.NewMockPrivateStateManager(ctrl) + mockptm := private.NewMockPrivateTransactionManager(ctrl) + mockstaterepo := mps.NewMockPrivateStateRepository(ctrl) + + saved := private.P + defer func() { + private.P = saved + }() + private.P = mockptm + + mockptm.EXPECT().Receive(gomock.Not(common.EncryptedPayloadHash{})).Return("AAA", []string{"AAA", "CCC"}, common.FromHex(testCode), &engine.ExtraMetadata{ + ACHashes: nil, + ACMerkleRoot: common.Hash{}, + PrivacyFlag: 0, + ManagedParties: []string{"AAA", "CCC"}, + Sender: "AAA", + MandatoryRecipients: nil, + }, nil).AnyTimes() + mockptm.EXPECT().HasFeature(engine.MultiplePrivateStates).Return(true) + mockptm.EXPECT().Groups().Return(PrivacyGroups, nil).AnyTimes() + + mockpsm.EXPECT().ResolveForUserContext(gomock.Any()).Return(PSI1PSM, nil).AnyTimes() + mockpsm.EXPECT().NotIncludeAny(gomock.Any(), gomock.Any()).Return(false).AnyTimes() + mockpsm.EXPECT().StateRepository(gomock.Any()).Return(mockstaterepo, nil).AnyTimes() + mockpsm.EXPECT().PSIs().Return([]types.PrivateStateIdentifier{PSI1PSM.ID, PSI2PSM.ID, types.DefaultPrivateStateIdentifier, types.ToPrivateStateIdentifier("other")}).AnyTimes() + + mockstaterepo.EXPECT().PrivateStateRoot(gomock.Any()).Return(common.StringToHash("PrivateStateRoot"), nil) + + pbdr := qlight.NewPrivateBlockDataResolver(mockpsm, mockptm) + blocks, _, _ := buildTestChainWithOneTxPerBlock(1, params.QuorumMPSTestChainConfig) + + blockPrivateData, err := pbdr.PrepareBlockPrivateData(blocks[0], PSI1PSM.ID.String()) + + assert.Nil(err) + assert.NotNil(blockPrivateData) + assert.Equal(common.StringToHash("PrivateStateRoot"), blockPrivateData.PrivateStateRoot) + assert.Equal(blocks[0].Hash(), blockPrivateData.BlockHash) + assert.Len(blockPrivateData.PrivateTransactions, 1) + privateTransactionData := blockPrivateData.PrivateTransactions[0] + assert.True(privateTransactionData.IsSender) + assert.Equal(common.FromHex(testCode), privateTransactionData.Payload) + assert.ElementsMatch(privateTransactionData.Extra.ManagedParties, []string{"AAA"}) +} + +func TestPrivateBlockDataResolverImpl_PrepareBlockPrivateData_NonPartyTransaction(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockpsm := mps.NewMockPrivateStateManager(ctrl) + mockptm := private.NewMockPrivateTransactionManager(ctrl) + + saved := private.P + defer func() { + private.P = saved + }() + private.P = mockptm + + mockptm.EXPECT().Receive(gomock.Not(common.EncryptedPayloadHash{})).Return("", nil, nil, nil, nil).AnyTimes() + mockptm.EXPECT().HasFeature(engine.MultiplePrivateStates).Return(true) + mockptm.EXPECT().Groups().Return(PrivacyGroups, nil).AnyTimes() + + mockpsm.EXPECT().ResolveForUserContext(gomock.Any()).Return(PSI1PSM, nil).AnyTimes() + + pbdr := qlight.NewPrivateBlockDataResolver(mockpsm, mockptm) + blocks, _, _ := buildTestChainWithOneTxPerBlock(1, params.QuorumMPSTestChainConfig) + + blockPrivateData, err := pbdr.PrepareBlockPrivateData(blocks[0], PSI1PSM.ID.String()) + + assert.Nil(err) + assert.Nil(blockPrivateData) +} + +func TestPrivateBlockDataResolverImpl_PrepareBlockPrivateData_PMTTransaction(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockpsm := mps.NewMockPrivateStateManager(ctrl) + mockptm := private.NewMockPrivateTransactionManager(ctrl) + mockstaterepo := mps.NewMockPrivateStateRepository(ctrl) + + saved := private.P + defer func() { + private.P = saved + }() + private.P = mockptm + + tx, err := types.SignTx(types.NewContractCreation(0, big.NewInt(0), testGas, nil, common.BytesToEncryptedPayloadHash([]byte("pmt private tx")).Bytes()), types.QuorumPrivateTxSigner{}, testKey) + assert.Nil(err) + txData := new(bytes.Buffer) + err = json.NewEncoder(txData).Encode(tx) + assert.Nil(err) + + mockptm.EXPECT().Receive(common.BytesToEncryptedPayloadHash([]byte("pmt inner tx"))).Return("AAA", []string{"AAA", "CCC"}, txData.Bytes(), &engine.ExtraMetadata{ + ACHashes: nil, + ACMerkleRoot: common.Hash{}, + PrivacyFlag: 0, + ManagedParties: []string{"AAA", "CCC"}, + Sender: "AAA", + MandatoryRecipients: nil, + }, nil).AnyTimes() + mockptm.EXPECT().Receive(common.BytesToEncryptedPayloadHash([]byte("pmt private tx"))).Return("AAA", []string{"AAA", "CCC"}, common.FromHex(testCode), &engine.ExtraMetadata{ + ACHashes: nil, + ACMerkleRoot: common.Hash{}, + PrivacyFlag: 0, + ManagedParties: []string{"AAA", "CCC"}, + Sender: "AAA", + MandatoryRecipients: nil, + }, nil).AnyTimes() + mockptm.EXPECT().HasFeature(engine.MultiplePrivateStates).Return(true) + mockptm.EXPECT().Groups().Return(PrivacyGroups, nil).AnyTimes() + + mockpsm.EXPECT().ResolveForUserContext(gomock.Any()).Return(PSI1PSM, nil).AnyTimes() + mockpsm.EXPECT().NotIncludeAny(gomock.Any(), gomock.Any()).Return(false).AnyTimes() + mockpsm.EXPECT().StateRepository(gomock.Any()).Return(mockstaterepo, nil).AnyTimes() + mockpsm.EXPECT().PSIs().Return([]types.PrivateStateIdentifier{PSI1PSM.ID, PSI2PSM.ID, types.DefaultPrivateStateIdentifier, types.ToPrivateStateIdentifier("other")}).AnyTimes() + + mockstaterepo.EXPECT().PrivateStateRoot(gomock.Any()).Return(common.StringToHash("PrivateStateRoot"), nil) + + pbdr := qlight.NewPrivateBlockDataResolver(mockpsm, mockptm) + blocks, _, _ := buildTestChainWithOnePMTTxPerBlock(1, params.QuorumMPSTestChainConfig) + + blockPrivateData, err := pbdr.PrepareBlockPrivateData(blocks[0], PSI1PSM.ID.String()) + + assert.Nil(err) + assert.NotNil(blockPrivateData) + assert.Equal(common.StringToHash("PrivateStateRoot"), blockPrivateData.PrivateStateRoot) + assert.Equal(blocks[0].Hash(), blockPrivateData.BlockHash) + assert.Len(blockPrivateData.PrivateTransactions, 2) + + pmtTransactionData := blockPrivateData.PrivateTransactions[0] + assert.True(pmtTransactionData.IsSender) + assert.Equal(txData.Bytes(), pmtTransactionData.Payload) + assert.ElementsMatch(pmtTransactionData.Extra.ManagedParties, []string{"AAA"}) + + privateTransactionData := blockPrivateData.PrivateTransactions[1] + assert.True(privateTransactionData.IsSender) + assert.Equal(common.FromHex(testCode), privateTransactionData.Payload) + assert.ElementsMatch(privateTransactionData.Extra.ManagedParties, []string{"AAA"}) +} + +func TestAuthProviderImpl_Authorize_AuthManagerNil(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockpsm := mps.NewMockPrivateStateManager(ctrl) + mockpsm.EXPECT().ResolveForUserContext(gomock.Any()).Return(PSI1PSM, nil).AnyTimes() + authProvider := qlight.NewAuthProvider(mockpsm, func() security.AuthenticationManager { return nil }) + + err := authProvider.Initialize() + assert.Nil(err) + + err = authProvider.Authorize("token", "psi1") + assert.Nil(err) +} + +func TestAuthProviderImpl_Authorize_AuthManagerDisabled(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockpsm := mps.NewMockPrivateStateManager(ctrl) + mockpsm.EXPECT().ResolveForUserContext(gomock.Any()).Return(PSI1PSM, nil).AnyTimes() + authProvider := qlight.NewAuthProvider(mockpsm, func() security.AuthenticationManager { + return &testAuthManager{ + enabled: false, + authError: nil, + authToken: nil, + } + }) + + err := authProvider.Initialize() + assert.Nil(err) + + err = authProvider.Authorize("token", "psi1") + assert.Nil(err) +} + +func TestAuthProviderImpl_Authorize_AuthManagerEnabledAuthError(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockpsm := mps.NewMockPrivateStateManager(ctrl) + mockpsm.EXPECT().ResolveForUserContext(gomock.Any()).Return(PSI1PSM, nil).AnyTimes() + authProvider := qlight.NewAuthProvider(mockpsm, func() security.AuthenticationManager { + return &testAuthManager{ + enabled: true, + authError: fmt.Errorf("auth error"), + authToken: nil, + } + }) + + err := authProvider.Initialize() + assert.Nil(err) + + err = authProvider.Authorize("token", "psi1") + assert.EqualError(err, "auth error") +} + +func TestAuthProviderImpl_Authorize_AuthManagerEnabledNotEntitledToPSI(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockpsm := mps.NewMockPrivateStateManager(ctrl) + mockpsm.EXPECT().ResolveForUserContext(gomock.Any()).Return(PSI1PSM, nil).AnyTimes() + authProvider := qlight.NewAuthProvider(mockpsm, func() security.AuthenticationManager { + return &testAuthManager{ + enabled: true, + authError: nil, + authToken: &proto.PreAuthenticatedAuthenticationToken{ + RawToken: nil, + ExpiredAt: nil, + Authorities: []*proto.GrantedAuthority{&proto.GrantedAuthority{ + Service: "psi", + Method: "psi2", + Raw: "psi://psi2", + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }}, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + } + }) + + err := authProvider.Initialize() + assert.Nil(err) + + err = authProvider.Authorize("token", "psi1") + assert.EqualError(err, "PSI not authorized") +} + +func TestAuthProviderImpl_Authorize_AuthManagerEnabledMissingEntitlement(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockpsm := mps.NewMockPrivateStateManager(ctrl) + mockpsm.EXPECT().ResolveForUserContext(gomock.Any()).Return(PSI1PSM, nil).AnyTimes() + authProvider := qlight.NewAuthProvider(mockpsm, func() security.AuthenticationManager { + return &testAuthManager{ + enabled: true, + authError: nil, + authToken: &proto.PreAuthenticatedAuthenticationToken{ + RawToken: nil, + ExpiredAt: nil, + Authorities: []*proto.GrantedAuthority{&proto.GrantedAuthority{ + Service: "psi", + Method: "psi1", + Raw: "psi://psi1", + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, &proto.GrantedAuthority{ + Service: "p2p", + Method: "qlight", + Raw: "p2p://qlight", + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + }, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + } + }) + + err := authProvider.Initialize() + assert.Nil(err) + + err = authProvider.Authorize("token", "psi1") + assert.EqualError(err, "The P2P token does not have the necessary authorization p2p=true rpcETH=false") +} + +func TestAuthProviderImpl_Authorize_AuthManagerEnabledSuccess(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockpsm := mps.NewMockPrivateStateManager(ctrl) + mockpsm.EXPECT().ResolveForUserContext(gomock.Any()).Return(PSI1PSM, nil).AnyTimes() + authProvider := qlight.NewAuthProvider(mockpsm, func() security.AuthenticationManager { + return &testAuthManager{ + enabled: true, + authError: nil, + authToken: &proto.PreAuthenticatedAuthenticationToken{ + RawToken: nil, + ExpiredAt: nil, + Authorities: []*proto.GrantedAuthority{&proto.GrantedAuthority{ + Service: "psi", + Method: "psi1", + Raw: "psi://psi1", + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, &proto.GrantedAuthority{ + Service: "p2p", + Method: "qlight", + Raw: "p2p://qlight", + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, &proto.GrantedAuthority{ + Service: "rpc", + Method: "eth_*", + Raw: "rpc://eth_*", + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + }, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + } + }) + + err := authProvider.Initialize() + assert.Nil(err) + + err = authProvider.Authorize("token", "psi1") + assert.Nil(err) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +////// Helpers ///////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const ( + // testCode is the testing contract binary code which will initialises some + // variables in constructor + testCode = "0x60806040527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0060005534801561003457600080fd5b5060fc806100436000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c80630c4dae8814603757806398a213cf146053575b600080fd5b603d607e565b6040518082815260200191505060405180910390f35b607c60048036036020811015606757600080fd5b81019080803590602001909291905050506084565b005b60005481565b806000819055507fe9e44f9f7da8c559de847a3232b57364adc0354f15a2cd8dc636d54396f9587a6000546040518082815260200191505060405180910390a15056fea265627a7a723058208ae31d9424f2d0bc2a3da1a5dd659db2d71ec322a17db8f87e19e209e3a1ff4a64736f6c634300050a0032" + + // testGas is the gas required for contract deployment. + testGas = 144109 +) + +var ( + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + testAddress = crypto.PubkeyToAddress(testKey.PublicKey) +) + +func buildTestChainWithZeroTxPerBlock(n int, config *params.ChainConfig) ([]*types.Block, map[common.Hash]*types.Block, *core.BlockChain) { + testdb := rawdb.NewMemoryDatabase() + genesis := core.GenesisBlockForTesting(testdb, testAddress, big.NewInt(1000000000)) + blocks, _ := core.GenerateChain(config, genesis, ethash.NewFaker(), testdb, n, func(i int, block *core.BlockGen) { + block.SetCoinbase(common.Address{0}) + }) + + hashes := make([]common.Hash, n+1) + hashes[len(hashes)-1] = genesis.Hash() + blockm := make(map[common.Hash]*types.Block, n+1) + blockm[genesis.Hash()] = genesis + for i, b := range blocks { + hashes[len(hashes)-i-2] = b.Hash() + blockm[b.Hash()] = b + } + + blockchain, _ := core.NewBlockChain(testdb, nil, config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + return blocks, blockm, blockchain +} + +func buildTestChainWithOneTxPerBlock(n int, config *params.ChainConfig) ([]*types.Block, map[common.Hash]*types.Block, *core.BlockChain) { + testdb := rawdb.NewMemoryDatabase() + genesis := core.GenesisBlockForTesting(testdb, testAddress, big.NewInt(1000000000)) + blocks, _ := core.GenerateChain(config, genesis, ethash.NewFaker(), testdb, n, func(i int, block *core.BlockGen) { + block.SetCoinbase(common.Address{0}) + + signer := types.QuorumPrivateTxSigner{} + tx, err := types.SignTx(types.NewContractCreation(block.TxNonce(testAddress), big.NewInt(0), testGas, nil, common.FromHex(testCode)), signer, testKey) + if err != nil { + panic(err) + } + block.AddTx(tx) + }) + + hashes := make([]common.Hash, n+1) + hashes[len(hashes)-1] = genesis.Hash() + blockm := make(map[common.Hash]*types.Block, n+1) + blockm[genesis.Hash()] = genesis + for i, b := range blocks { + hashes[len(hashes)-i-2] = b.Hash() + blockm[b.Hash()] = b + } + + blockchain, _ := core.NewBlockChain(testdb, nil, config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + return blocks, blockm, blockchain +} + +func buildTestChainWithOnePMTTxPerBlock(n int, config *params.ChainConfig) ([]*types.Block, map[common.Hash]*types.Block, *core.BlockChain) { + testdb := rawdb.NewMemoryDatabase() + genesis := core.GenesisBlockForTesting(testdb, testAddress, big.NewInt(1000000000)) + blocks, _ := core.GenerateChain(config, genesis, ethash.NewFaker(), testdb, n, func(i int, block *core.BlockGen) { + block.SetCoinbase(common.Address{0}) + + signer := types.LatestSigner(config) + tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testAddress), common.QuorumPrivacyPrecompileContractAddress(), big.NewInt(0), testGas, nil, common.BytesToEncryptedPayloadHash([]byte("pmt inner tx")).Bytes()), signer, testKey) + if err != nil { + panic(err) + } + block.AddTx(tx) + }) + + hashes := make([]common.Hash, n+1) + hashes[len(hashes)-1] = genesis.Hash() + blockm := make(map[common.Hash]*types.Block, n+1) + blockm[genesis.Hash()] = genesis + for i, b := range blocks { + hashes[len(hashes)-i-2] = b.Hash() + blockm[b.Hash()] = b + } + + blockchain, _ := core.NewBlockChain(testdb, nil, config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + return blocks, blockm, blockchain +} + +var PSI1PSM = mps.NewPrivateStateMetadata("psi1", "psi1", "private state 1", mps.Resident, PG1.Members) + +var PSI2PSM = mps.NewPrivateStateMetadata("psi2", "psi2", "private state 2", mps.Resident, PG2.Members) + +var PG1 = engine.PrivacyGroup{ + Type: "RESIDENT", + Name: "RG1", + PrivacyGroupId: "RG1", + Description: "Resident Group 1", + From: "", + Members: []string{"AAA", "BBB"}, +} + +var PG2 = engine.PrivacyGroup{ + Type: "RESIDENT", + Name: "RG2", + PrivacyGroupId: "RG2", + Description: "Resident Group 2", + From: "", + Members: []string{"CCC", "DDD"}, +} + +var PrivacyGroups = []engine.PrivacyGroup{ + PG1, + PG2, + { + Type: "LEGACY", + Name: "LEGACY1", + PrivacyGroupId: "LEGACY1", + Description: "Legacy Group 1", + From: "", + Members: []string{"LEG1", "LEG2"}, + }, +} + +type testAuthManager struct { + enabled bool + authError error + authToken *proto.PreAuthenticatedAuthenticationToken +} + +func (am *testAuthManager) Authenticate(ctx context.Context, token string) (*proto.PreAuthenticatedAuthenticationToken, error) { + return am.authToken, am.authError +} + +func (am *testAuthManager) IsEnabled(ctx context.Context) (bool, error) { + return am.enabled, nil +} diff --git a/qlight/test/types_test.go b/qlight/test/types_test.go new file mode 100644 index 0000000000..080f5da39f --- /dev/null +++ b/qlight/test/types_test.go @@ -0,0 +1,56 @@ +package test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/private/engine" + "github.com/ethereum/go-ethereum/qlight" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" +) + +func TestBlockPrivateData_RLPEncodeDecode(t *testing.T) { + txHash := common.BytesToEncryptedPayloadHash([]byte("EPH1")) + data := qlight.BlockPrivateData{ + BlockHash: common.BytesToHash([]byte("BlockHash")), + PSI: "PS1", + PrivateStateRoot: common.BytesToHash([]byte("PSR")), + PrivateTransactions: []qlight.PrivateTransactionData{ + { + Hash: &txHash, + Payload: []byte("data"), + Extra: &engine.ExtraMetadata{ + ACHashes: common.EncryptedPayloadHashes{common.BytesToEncryptedPayloadHash([]byte("ACEPH1")): struct{}{}}, + ACMerkleRoot: common.BytesToHash([]byte("root")), + PrivacyFlag: engine.PrivacyFlagPartyProtection, + ManagedParties: []string{"party1", "party2"}, + Sender: "party3", + MandatoryRecipients: []string{"party1"}, + }, + IsSender: false, + }}, + } + + bytes, err := rlp.EncodeToBytes(data) + assert.NoError(t, err) + var decodedData qlight.BlockPrivateData + err = rlp.DecodeBytes(bytes, &decodedData) + assert.NoError(t, err) + assert.Equal(t, data.PSI, decodedData.PSI) + assert.Equal(t, data.BlockHash, decodedData.BlockHash) + assert.Equal(t, data.PrivateStateRoot, decodedData.PrivateStateRoot) + assert.Len(t, decodedData.PrivateTransactions, 1) + privateTx := decodedData.PrivateTransactions[0] + assert.Equal(t, &txHash, privateTx.Hash) + assert.Equal(t, data.PrivateTransactions[0].Payload, privateTx.Payload) + assert.Equal(t, data.PrivateTransactions[0].IsSender, privateTx.IsSender) + assert.Equal(t, data.PrivateTransactions[0].Hash, privateTx.Hash) + assert.Equal(t, data.PrivateTransactions[0].Extra.Sender, privateTx.Extra.Sender) + assert.Equal(t, data.PrivateTransactions[0].Extra.ACMerkleRoot, privateTx.Extra.ACMerkleRoot) + assert.Equal(t, data.PrivateTransactions[0].Extra.PrivacyFlag, privateTx.Extra.PrivacyFlag) + assert.Equal(t, data.PrivateTransactions[0].Extra.MandatoryRecipients, privateTx.Extra.MandatoryRecipients) + assert.Len(t, decodedData.PrivateTransactions[0].Extra.ACHashes, 1) + _, found := decodedData.PrivateTransactions[0].Extra.ACHashes[common.BytesToEncryptedPayloadHash([]byte("ACEPH1"))] + assert.True(t, found) +} diff --git a/qlight/token_holder.go b/qlight/token_holder.go new file mode 100644 index 0000000000..4b3af0828c --- /dev/null +++ b/qlight/token_holder.go @@ -0,0 +1,11 @@ +package qlight + +var token string + +func GetCurrentToken() string { + return token +} + +func SetCurrentToken(newToken string) { + token = newToken +} diff --git a/qlight/types.go b/qlight/types.go new file mode 100644 index 0000000000..a793219833 --- /dev/null +++ b/qlight/types.go @@ -0,0 +1,83 @@ +package qlight + +import ( + "context" + "encoding/base64" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/plugin/security" + "github.com/ethereum/go-ethereum/private/engine" + "github.com/ethereum/go-ethereum/private/engine/qlightptm" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" +) + +type PrivateStateRootHashValidator interface { + ValidatePrivateStateRoot(blockHash common.Hash, blockPublicStateRoot common.Hash) error +} + +type PrivateClientCache interface { + PrivateStateRootHashValidator + AddPrivateBlock(blockPrivateData BlockPrivateData) error + CheckAndAddEmptyEntry(hash common.EncryptedPayloadHash) +} + +type PrivateBlockDataResolver interface { + PrepareBlockPrivateData(block *types.Block, psi string) (*BlockPrivateData, error) +} + +type AuthManagerProvider func() security.AuthenticationManager + +type AuthProvider interface { + Initialize() error + Authorize(token string, psi string) error +} + +type CacheWithEmpty interface { + Cache(privateTxData *qlightptm.CachablePrivateTransactionData) error + CheckAndAddEmptyToCache(hash common.EncryptedPayloadHash) +} + +type BlockPrivateData struct { + BlockHash common.Hash + PSI types.PrivateStateIdentifier + PrivateStateRoot common.Hash + PrivateTransactions []PrivateTransactionData +} + +type QLightCacheKey struct { + BlockHash common.Hash + PSI types.PrivateStateIdentifier +} + +func (k *QLightCacheKey) String() string { + bytes, err := rlp.EncodeToBytes(k) + if err != nil { + return err.Error() + } + return base64.StdEncoding.EncodeToString(bytes) +} + +type PrivateTransactionData struct { + Hash *common.EncryptedPayloadHash + Payload []byte + Extra *engine.ExtraMetadata + IsSender bool +} + +func (d *PrivateTransactionData) ToCachable() *qlightptm.CachablePrivateTransactionData { + return &qlightptm.CachablePrivateTransactionData{ + Hash: *d.Hash, + QuorumPrivateTxData: engine.QuorumPayloadExtra{ + Payload: fmt.Sprintf("0x%x", d.Payload), + ExtraMetaData: d.Extra, + IsSender: d.IsSender, + }, + } +} + +var TokenCredentialsProvider rpc.HttpCredentialsProviderFunc = func(ctx context.Context) (string, error) { + return GetCurrentToken(), nil +}