From d5f1c8a55b9fa2f29a8d5b2afca03dfbcdf5d67e Mon Sep 17 00:00:00 2001 From: Conor Date: Wed, 11 Apr 2018 23:06:33 +0100 Subject: [PATCH 01/85] Added more text to BerkeleyDB flag. --- config/config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 0d25f08..a6cb8e4 100644 --- a/config/config.go +++ b/config/config.go @@ -47,7 +47,8 @@ func InitFlags() { flag.String(PublicKeys, "", "Public keys hosted by this node") flag.String(PrivateKeys, "", "Private keys hosted by this node") flag.String(Storage, "crux.db", "Database storage file name") - flag.Bool(BerkeleyDb, false, "Use Berkeley DB for storage") + flag.Bool(BerkeleyDb, false, + "Use Berkeley DB for working with an existing Constellation data store [experimental]") flag.Int(Verbosity, 1, "Verbosity level of logs") flag.String(AlwaysSendTo, "", "List of public keys for nodes to send all transactions too") From acb40f6e9499dbb849819c83b34d4a7ead8e428f Mon Sep 17 00:00:00 2001 From: Conor Date: Mon, 16 Apr 2018 21:45:35 +0100 Subject: [PATCH 02/85] 1. Created README. 2. Created Travis build file. --- .travis.yml | 14 +++++++++ REAME.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 .travis.yml create mode 100644 REAME.md diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9108e7d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: go + +go: + - "1.10.x" + - tip + +before_install: + - go get -t -v ./... + +script: + - go test -race -coverprofile=coverage.txt -covermode=atomic + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/REAME.md b/REAME.md new file mode 100644 index 0000000..c2f231b --- /dev/null +++ b/REAME.md @@ -0,0 +1,82 @@ +# Crux + +Data privacy for Quorum. + +Crux is a secure enclave for Quorum written in Golang. + +It is a replacement for [Constellation](https://github.com/jpmorganchase/constellation/), the +secure enclave component of [Quorum](https://github.com/jpmorganchase/quorum/), written in Haskell. + +## Getting started + +The best way to start is to run the +[7 Nodes Quorum example](https://github.com/blk-io/quorum-examples) that uses Crux. This is a fork +of the JP Morgan version. + +If you you'd prefer to run just a client, you can build using the below instructions and run as per +the below. + +```bash +git clone https://github.com/blk-io/crux.git +cd crux +make setup && make +./bin/crux + +Usage of ./bin/crux: + crux.config Optional config file + --alwayssendto string List of public keys for nodes to send all transactions too + --berkeleydb Use Berkeley DB for storage + --generate-keys string Generate a new keypair + --othernodes string "Boot nodes" to connect to to discover the network + --port int The local port to listen on (default -1) + --privatekeys string Private keys hosted by this node + --publickeys string Public keys hosted by this node + --socket string IPC socket to create for access to the Private API + --storage string Database storage file name (default "crux.db") + --url string The URL to advertise to other nodes (reachable by them) + --verbosity int Verbosity level of logs (default 1) + --workdir string The folder to put stuff in (default: .) (default ".") +``` + +## Generating keys + +Each Crux instance requires at least one key-pair to be associated with it. The key-pair is used +to ensure transaction privacy. Crux uses the [NaCl cryptography library](https://nacl.cr.yp.to/). + +You use the `--generate-keys` argument to generate a new key-pair with Crux: + +```bash +crux --generate-keys myKey +``` + +This will produce two files, named `myKey` and `myKey.pub` reflecting the private and public keys +respectively. + +## Core configuration + +At a minimum, Crux requires the following configuration parameters. This tells the Crux instance +what port it is running on and what ip address it should advertise to other peers. + +Details of at least one key-pair must be provided for the Crux node to store requests on behalf of. + +```bash +crux --url=http://127.0.0.1:9001/ --port=9001 --workdir=crux --publickeys=tm.pub --privatekeys=tm.key --othernodes=https://127.0.0.1:9001/ +``` + +## How does it work? + +At present, Crux performs its cryptographic operations in a manner identical to Constellation. You +can read the specifics [here](https://github.com/jpmorganchase/constellation/#how-it-works). + +## Why Crux? + +*Crux is a constellation located in the southern sky in a bright portion of the Milky Way. It is +among the most easily distinguished constellations, even though it is the smallest of all 88 +modern constellations. (Source: [Wikipedia](https://en.wikipedia.org/wiki/Crux))* + +*The critical or transitional moment or issue, a turning point.* + +## Thanks + +[@patrickmn](https://github.com/patrickmn) the original author of Constellation. Crux would not +exist were it not for his work. From 24a0f822e90f05041b7088d87bc9c238463719f5 Mon Sep 17 00:00:00 2001 From: Conor Date: Mon, 16 Apr 2018 21:47:37 +0100 Subject: [PATCH 03/85] Spelt README correctly this time around. --- REAME.md => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename REAME.md => README.md (100%) diff --git a/REAME.md b/README.md similarity index 100% rename from REAME.md rename to README.md From 3ace46be6c711ec3f3d36726b5f9dc4dc8019561 Mon Sep 17 00:00:00 2001 From: Conor Date: Mon, 16 Apr 2018 21:54:46 +0100 Subject: [PATCH 04/85] Small updates to README. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c2f231b..ccf5101 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ secure enclave component of [Quorum](https://github.com/jpmorganchase/quorum/), ## Getting started -The best way to start is to run the -[7 Nodes Quorum example](https://github.com/blk-io/quorum-examples) that uses Crux. This is a fork -of the JP Morgan version. +The best way to start is to run the Crux +[7 Nodes Quorum example](https://github.com/blk-io/quorum-examples). This is a fork of the JP +Morgan version. If you you'd prefer to run just a client, you can build using the below instructions and run as per the below. From ee137b9d6377d4a8b82d3059aef4a3b03ca59b08 Mon Sep 17 00:00:00 2001 From: Conor Date: Wed, 18 Apr 2018 21:56:17 +0100 Subject: [PATCH 05/85] 1. Added sequence diagrams to README. 2. Added default ipc config. --- README.md | 10 ++ config/config.go | 2 +- docs/new-tx.mermaid | 10 ++ docs/new-tx.svg | 350 +++++++++++++++++++++++++++++++++++++++++++ docs/read-tx.mermaid | 9 ++ docs/read-tx.svg | 350 +++++++++++++++++++++++++++++++++++++++++++ test.txt | 13 -- 7 files changed, 730 insertions(+), 14 deletions(-) create mode 100644 docs/new-tx.mermaid create mode 100644 docs/new-tx.svg create mode 100644 docs/read-tx.mermaid create mode 100644 docs/read-tx.svg delete mode 100644 test.txt diff --git a/README.md b/README.md index ccf5101..5625fdc 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,16 @@ crux --url=http://127.0.0.1:9001/ --port=9001 --workdir=crux --publickeys=tm.pub At present, Crux performs its cryptographic operations in a manner identical to Constellation. You can read the specifics [here](https://github.com/jpmorganchase/constellation/#how-it-works). +The two main workflows for handling private transactions are as follows. + +Submission of a new transaction: + +![New Transaction Sequence](./docs/new-tx.svg) + +Retrieval of an existing transaction: + +![Read Transaction Sequence](./docs/read-tx.svg) + ## Why Crux? *Crux is a constellation located in the southern sky in a bright portion of the Milky Way. It is diff --git a/config/config.go b/config/config.go index a6cb8e4..a85bd17 100644 --- a/config/config.go +++ b/config/config.go @@ -42,7 +42,7 @@ func InitFlags() { flag.String(Url, "", "The URL to advertise to other nodes (reachable by them)") flag.Int(Port, -1, "The local port to listen on") flag.String(WorkDir, ".", "The folder to put stuff in (default: .)") - flag.String(Socket, "", "IPC socket to create for access to the Private API") + flag.String(Socket, "crux.ipc", "IPC socket to create for access to the Private API") flag.String(OtherNodes, "", "\"Boot nodes\" to connect to to discover the network") flag.String(PublicKeys, "", "Public keys hosted by this node") flag.String(PrivateKeys, "", "Private keys hosted by this node") diff --git a/docs/new-tx.mermaid b/docs/new-tx.mermaid new file mode 100644 index 0000000..761de33 --- /dev/null +++ b/docs/new-tx.mermaid @@ -0,0 +1,10 @@ +sequenceDiagram + participant Client + participant Quorum + participant Crux + participant Remote Crux + +Client->>Quorum: send(transaction, privateFor) +Quorum->>Crux: send(transaction, privateFor) +Crux->>Remote Crux: push(encryptedPayload) +Crux-->>Quorum: sendResponse(transactionHash) diff --git a/docs/new-tx.svg b/docs/new-tx.svg new file mode 100644 index 0000000..8719b28 --- /dev/null +++ b/docs/new-tx.svg @@ -0,0 +1,350 @@ +ClientQuorumCruxRemote Cruxsend(transaction, privateFor)send(transaction, privateFor)push(encryptedPayload)sendResponse(transactionHash)ClientQuorumCruxRemote Crux \ No newline at end of file diff --git a/docs/read-tx.mermaid b/docs/read-tx.mermaid new file mode 100644 index 0000000..5e28ada --- /dev/null +++ b/docs/read-tx.mermaid @@ -0,0 +1,9 @@ +sequenceDiagram + participant Client + participant Quorum + participant Crux + +Client->>Quorum: getQuorumPayload(transactionHash, to) +Quorum->>Crux: receive(transactionHash, to) +Crux-->>Quorum: receiveResponse(transaction) +Quorum->>Client: transaction diff --git a/docs/read-tx.svg b/docs/read-tx.svg new file mode 100644 index 0000000..9bf9aa8 --- /dev/null +++ b/docs/read-tx.svg @@ -0,0 +1,350 @@ +ClientQuorumCruxgetQuorumPayload(transactionHash, to)receive(transactionHash, to)receiveResponse(transaction)transactionClientQuorumCrux \ No newline at end of file diff --git a/test.txt b/test.txt deleted file mode 100644 index 5b8d126..0000000 --- a/test.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -X GET http://localhost:8000/upcheck - - -curl -d '{"payload":"bWVzc2FnZSBwYXlsb2FkMQ==","from":"BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo=","to":["QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc="]}' -H "Content-Type: application/json" -X POST http://localhost:8000/send - -curl -d '{"key":"1ifl2vtckF+8SiqIwFfPkmbkngbanOZ4Pu886ehqRGMagWwJ0BviEymdekCLS/S66sGz7mWEKjwEMkSY49TMdQ==","to":"QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc="}' -H "Content-Type: application/json" -X POST http://localhost:8000/receive - -leveldbutil dump ~/code/go/blk-io/tmp/crux.db/000001.log - - -// constellation-node --url=https://127.0.0.7:9007/ --port=9007 --workdir=qdata/c7 --socket=tm.ipc --publickeys=tm.pub --privatekeys=tm.key --othernodes=https://127.0.0.1:9001/ >> qdata/logs/constellation7.log 2>&1 & - -// "/Users/Conor/code/go/blk-io/tmp/crux.db" From c28cb89f0eeffc11e0a54a29529750a64a2225b4 Mon Sep 17 00:00:00 2001 From: Conor Date: Wed, 18 Apr 2018 21:59:15 +0100 Subject: [PATCH 06/85] Changed label to header. --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5625fdc..34071cc 100644 --- a/README.md +++ b/README.md @@ -68,13 +68,14 @@ crux --url=http://127.0.0.1:9001/ --port=9001 --workdir=crux --publickeys=tm.pub At present, Crux performs its cryptographic operations in a manner identical to Constellation. You can read the specifics [here](https://github.com/jpmorganchase/constellation/#how-it-works). -The two main workflows for handling private transactions are as follows. +The two main workflows for handling private transactions are the submission and retrieval +demonstrated below. -Submission of a new transaction: +### New transaction submission ![New Transaction Sequence](./docs/new-tx.svg) -Retrieval of an existing transaction: +### Existing transaction retrieval ![Read Transaction Sequence](./docs/read-tx.svg) From 9acbace9f6e37de5b6b24f5d764eb15eeab72851 Mon Sep 17 00:00:00 2001 From: Puneetha17 Date: Thu, 3 May 2018 06:19:49 +0100 Subject: [PATCH 07/85] Fix Key Genereation using Crux Signed-off-by: Puneetha17 --- enclave/enclave.go | 2 +- enclave/enclave_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/enclave/enclave.go b/enclave/enclave.go index dc0031c..e2a66c4 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -476,7 +476,7 @@ func DoKeyGeneration(keyFile string) error { return fmt.Errorf("unable to encode private key: %v, error: %v", jsonKey, err) } - err = ioutil.WriteFile(keyFile, encoded, 0600) + err = ioutil.WriteFile(keyFile + ".key", encoded, 0600) if err != nil { return fmt.Errorf("unable to write private key: %s, error: %v", keyFile, err) } diff --git a/enclave/enclave_test.go b/enclave/enclave_test.go index 46efee6..d3f2226 100644 --- a/enclave/enclave_test.go +++ b/enclave/enclave_test.go @@ -445,7 +445,7 @@ func TestDoKeyGeneration(t *testing.T) { t.Fatal(err) } - _, err = loadPrivKeys([]string{keyFiles}) + _, err = loadPrivKeys([]string{keyFiles + ".key"}) if err != nil { t.Fatal(err) } From e8a915d6be2625a6d9c1aff3171244d59fb8990c Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 3 May 2018 12:01:19 +0100 Subject: [PATCH 08/85] Added documentation to fix #2. --- api/client.go | 18 ++++++++++ api/internal.go | 24 +++++++++---- config/config.go | 5 +++ enclave/enclave.go | 85 ++++++++++++++++++++++++++++++-------------- server/server.go | 4 +++ storage/datastore.go | 1 + utils/http.go | 1 + utils/math.go | 2 -- 8 files changed, 105 insertions(+), 35 deletions(-) diff --git a/api/client.go b/api/client.go index b71ba46..ec61744 100644 --- a/api/client.go +++ b/api/client.go @@ -1,38 +1,56 @@ package api +// SendRequest sends a new transaction to the enclave for storage and propagation to the provided +// recipients. type SendRequest struct { + // Payload is the transaction payload data we wish to store. Payload string `json:"payload"` + // From is the sender node identification. From string `json:"from"` + // To is a list of the recipient nodes that should be privy to this transaction payload. To []string `json:"to"` } +// SendResponse is the response to the SendRequest type SendResponse struct { + // Key is the key that can be used to retrieve the submitted transaction. Key string `json:"key"` } +// ReceiveRequest type ReceiveRequest struct { Key string `json:"key"` To string `json:"to"` } +// ReceiveResponse returns the raw payload associated with the ReceiveRequest. type ReceiveResponse struct { Payload string `json:"payload"` } +// DeleteRequest deletes the entry matching the given key from the enclave. type DeleteRequest struct { Key string `json:"key"` } +// ResendRequest is used to resend previous transactions. +// There are two types of supported request. +// 1. All transactions associated with a node, in which case the Key field should be omitted. +// 2. A specific transaction with the given key value. type ResendRequest struct { + // Type is the resend request type. It should be either "all" or "individual" depending on if + // you want to request an individual transaction, or all transactions associated with a node. Type string `json:"type"` PublicKey string `json:"publicKey"` Key string `json:"key,omitempty"` } + type PrivateKeyBytes struct { Bytes string `json:"bytes"` } +// PrivateKey is a container for a private key. type PrivateKey struct { Data PrivateKeyBytes `json:"data"` Type string `json:"type"` diff --git a/api/internal.go b/api/internal.go index 995dd6f..db83eac 100644 --- a/api/internal.go +++ b/api/internal.go @@ -14,6 +14,8 @@ import ( "fmt" ) +// EncryptedPayload is the struct used for storing all data associated with an encrypted +// transaction. type EncryptedPayload struct { Sender nacl.Key CipherText []byte @@ -22,19 +24,21 @@ type EncryptedPayload struct { RecipientNonce nacl.Nonce } +// PartyInfo is a struct that stores details of all enclave nodes (or parties) on the network. type PartyInfo struct { - url string - // public key -> URL - recipients map[[nacl.KeySize]byte]string - parties map[string]bool // URLs + url string // URL identifying this node + recipients map[[nacl.KeySize]byte]string // public key -> URL + parties map[string]bool // Node (or party) URLs client utils.HttpClient } +// GetRecipient retrieves the URL associated with the provided recipient. func (s *PartyInfo) GetRecipient(key nacl.Key) (string, bool) { value, ok := s.recipients[*key] return value, ok } +// InitPartyInfo initializes a new PartyInfo store. func InitPartyInfo(rawUrl string, otherNodes []string, client utils.HttpClient) PartyInfo { parties := make(map[string]bool) for _, node := range otherNodes { @@ -49,6 +53,7 @@ func InitPartyInfo(rawUrl string, otherNodes []string, client utils.HttpClient) } } +// CreatePartyInfo creates a new PartyInfo struct. func CreatePartyInfo( url string, otherNodes []string, @@ -70,12 +75,15 @@ func CreatePartyInfo( } } +// RegisterPublicKeys associates the provided public keys with this node. func (s *PartyInfo) RegisterPublicKeys(pubKeys []nacl.Key) { for _, pubKey := range pubKeys { s.recipients[*pubKey] = s.url } } +// GetPartyInfo requests PartyInfo data from all remote nodes this node is aware of. The data +// provided in each response is applied to this node. func (s *PartyInfo) GetPartyInfo() { encodedPartyInfo := EncodePartyInfo(*s) @@ -152,9 +160,10 @@ func (s *PartyInfo) PollPartyInfo() { }() } -// This can happen from the /partyinfo server endpoint being hit, or -// by a response from us hitting another nodes /partyinfo endpoint -// TODO: Control access via a channel for updates +// UpdatePartyInfo updates the PartyInfo datastore with the provided encoded data. +// This can happen from the /partyinfo server endpoint being hit, or by a response from us hitting +// another nodes /partyinfo endpoint. +// TODO: Control access via a channel for updates. func (s *PartyInfo) UpdatePartyInfo(encoded []byte) { log.Debugf("Updating party info payload: %s", hex.EncodeToString(encoded)) pi, err := DecodePartyInfo(encoded) @@ -180,6 +189,7 @@ func (s *PartyInfo) UpdatePartyInfo(encoded []byte) { } } +// Push is responsible for propagating the encoded payload to the given remote node. func Push(encoded []byte, url string, client utils.HttpClient) (string, error) { endPoint, err := utils.BuildUrl(url, "/push") diff --git a/config/config.go b/config/config.go index a85bd17..ffd417b 100644 --- a/config/config.go +++ b/config/config.go @@ -1,3 +1,4 @@ +// Package config provides the configuration settings to be used by the application at runtime package config import ( @@ -37,6 +38,7 @@ const ( TlsServerKey = "tlsserverkey" ) +// InitFlags initializes all supported command line flags. func InitFlags() { flag.String(GenerateKeys, "", "Generate a new keypair") flag.String(Url, "", "The URL to advertise to other nodes (reachable by them)") @@ -59,17 +61,20 @@ func InitFlags() { pflag.CommandLine.AddGoFlagSet(flag.CommandLine) } +// Usage prints usage instructions to the console. func Usage() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %-25s%s\n", "crux.config", "Optional config file") pflag.PrintDefaults() } +// ParseCommandLine parses all provided command line arguments. func ParseCommandLine() { pflag.Parse() viper.BindPFlags(pflag.CommandLine) } +// LoadConfig loads all configuration settings in the provided configPath location. func LoadConfig(configPath string) error { viper.SetConfigType("hcl") viper.SetConfigFile(configPath) diff --git a/enclave/enclave.go b/enclave/enclave.go index e2a66c4..9454715 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -1,3 +1,4 @@ +// Package enclave provides enclaves for the secure storage and propagation of transactions. package enclave import ( @@ -20,28 +21,32 @@ import ( "github.com/blk-io/crux/utils" ) +// SecureEnclave is the secure transaction enclave. type SecureEnclave struct { - Db storage.DataStore - PubKeys []nacl.Key - PrivKeys []nacl.Key - selfPubKey nacl.Key - PartyInfo api.PartyInfo - keyCache map[nacl.Key]map[nacl.Key]nacl.Key // maps sender -> recipient -> shared key - client utils.HttpClient + Db storage.DataStore // The underlying key-value datastore for encrypted transactions + PubKeys []nacl.Key // Public keys associated with this enclave + PrivKeys []nacl.Key // Private keys associated with this enclave + selfPubKey nacl.Key // An ephemeral key used for transactions only intended for this enclave + PartyInfo api.PartyInfo // Details of all other nodes (or parties) on the network + keyCache map[nacl.Key]map[nacl.Key]nacl.Key // Maps sender -> recipient -> shared key + client utils.HttpClient // The underlying HTTP client used to propagate requests } +// Init creates a new instance of the SecureEnclave. func Init( db storage.DataStore, pubKeyFiles, privKeyFiles []string, pi api.PartyInfo, client utils.HttpClient) *SecureEnclave { + // Key format: // BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= pubKeys, err := loadPubKeys(pubKeyFiles) if err != nil { log.Fatalf("Unable to load public key files: %s, error: %v", pubKeyFiles, err) } + // Key format: // {"data":{"bytes":"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA="},"type":"unlocked"} privKeys, err := loadPrivKeys(privKeyFiles) if err != nil { @@ -56,6 +61,26 @@ func Init( client: client, } + // We use shared keys for encrypting data. The keys between a specific sender and recipient are + // computed once for each unique pair. + // + // Encrypt scenarios: + // The sender value must always be a public key that we have the corresponding private key for + // privateFor: [] => encrypt with sharedKey [self-private, selfPub-public] + // store in cache as (self-public, selfPub-public) + // privateFor: [recipient1, ...] => encrypt with sharedKey1 [self-private, recipient1-public], ... + // store in cache as (self-public, recipient1-public) + // Decrypt scenarios: + // epl, [] => The payload was pushed to us (we are recipient1), decrypt with sharedKey + // [recipient1-private, sender-public] + // lookup in cache as (recipient1-public, sender-public) + // epl, [recipient1, ...,] => The payload originated with us (we are self), decrypt with + // sharedKey [self-private, recipient1-public] + // lookup in cache as (self-public, recipient1-public) + // + // Note that sharedKey(privA, pubB) produces the same key as sharedKey(pubA, privB), which is + // why when sending to ones self we encrypt with sharedKey [self-private, selfPub-public], then + // retrieve with sharedKey [self-private, selfPub-public] enc.keyCache = make(map[nacl.Key]map[nacl.Key]nacl.Key) enc.selfPubKey = nacl.NewKey() @@ -71,27 +96,13 @@ func Init( enc.resolveSharedKey(enc.PrivKeys[0], pubKey, enc.selfPubKey) } - // We use shared keys for encrypting data. The keys between a specific sender and recipient are - // computed once for each unique pair. - // - // Encrypt scenarios: - // The sender value must always be a public key that we have the corresponding private key for - // privateFor: [] => encrypt with sharedKey [self-private, selfPub-public] - // store in cache as (self-public, selfPub-public) - // privateFor: [recipient1, ...] => encrypt with sharedKey1 [self-private, recipient1-public], ... - // store in cache as (self-public, recipient1-public) - // Decrypt scenarios: - // epl, [] => The payload was pushed to us (we are recipient1), decrypt with sharedKey [recipient1-private, sender-public] - // lookup in cache as (recipient1-public, sender-public) - // epl, [recipient1, ...,] => The payload originated with us (we are self), decrypt with sharedKey [self-private, recipient1-public] - // lookup in cache as (self-public, recipient1-public) - // - // Note that sharedKey(privA, pubB) produces the same key as sharedKey(pubA, privB), which is why - // when sending to ones self we encrypt with sharedKey [self-private, selfPub-public], then - // retrieve with sharedKey [self-private, selfPub-public] return &enc } +// Store a payload submitted via an Ethereum node. +// This function encrypts the payload, and distributes the encrypted payload to the other +// specified recipients in the network. +// The hash of the encrypted payload is returned to the sender. func (s *SecureEnclave) Store( message *[]byte, sender []byte, recipients [][]byte) ([]byte, error) { @@ -228,7 +239,8 @@ func (s *SecureEnclave) publishPayload(epl api.EncryptedPayload, recipient []byt } } -func (s *SecureEnclave) resolveSharedKey(senderPrivKey, senderPubKey, recipientPubKey nacl.Key) nacl.Key { +func (s *SecureEnclave) resolveSharedKey( + senderPrivKey, senderPubKey, recipientPubKey nacl.Key) nacl.Key { keyCache, ok := s.keyCache[senderPubKey] if !ok { @@ -255,6 +267,10 @@ func (s *SecureEnclave) resolvePrivateKey(publicKey nacl.Key) (nacl.Key, error) hex.EncodeToString((*publicKey)[:])) } +// Store a binary encoded payload within this SecureEnclave. +// This will be a payload that has been propagated to this node as it is a party on the +// transaction. I.e. it is not the original recipient of the transaction, but one of the recipients +// it is intended for. func (s *SecureEnclave) StorePayload(encoded []byte) ([]byte, error) { epl, _ := api.DecodePayloadWithRecipients(encoded) return s.storePayload(epl, encoded) @@ -278,12 +294,17 @@ func sealPayload( sharedKey) } +// RetrieveDefault is used to retrieve the provided payload. It attempts to use a default key +// value of the first public key associated with this SecureEnclave instance. +// If the payload cannot be found, or decrypted successfully an error is returned. func (s *SecureEnclave) RetrieveDefault(digestHash *[]byte) ([]byte, error) { // to address is either default or specified on communication key := (*s.PubKeys[0])[:] return s.Retrieve(digestHash, &key) } +// Retrieve is used to retrieve the provided payload. +// If the payload cannot be found, or decrypted successfully an error is returned. func (s *SecureEnclave) Retrieve(digestHash *[]byte, to *[]byte) ([]byte, error) { encoded, err := s.Db.Read(digestHash) @@ -336,6 +357,8 @@ func (s *SecureEnclave) Retrieve(digestHash *[]byte, to *[]byte) ([]byte, error) return payload, nil } +// RetrieveFor retrieves a payload with the given digestHash for a specific recipient who was one +// of the original recipients specified on the payload. func (s *SecureEnclave) RetrieveFor(digestHash *[]byte, reqRecipient *[]byte) (*[]byte, error) { encoded, err := s.Db.Read(digestHash) if err != nil { @@ -360,6 +383,9 @@ func (s *SecureEnclave) RetrieveFor(digestHash *[]byte, reqRecipient *[]byte) (* return nil, fmt.Errorf("invalid recipient %q requested for payload", reqRecipient) } +// RetrieveAllFor retrieves all payloads that the specified recipient was an original recipient +// for. +// Each payload found is published to the specified recipient. func (s *SecureEnclave) RetrieveAllFor(reqRecipient *[]byte) error { return s.Db.ReadAll(func(key, value *[]byte) { epl, recipients := api.DecodePayloadWithRecipients(*value) @@ -381,14 +407,18 @@ func (s *SecureEnclave) RetrieveAllFor(reqRecipient *[]byte) error { }) } +// Delete deletes the payload associated with the given digestHash from the SecureEnclave's store. func (s *SecureEnclave) Delete(digestHash *[]byte) error { return s.Db.Delete(digestHash) } +// UpdatePartyInfo applies the provided binary encoded party details to the SecureEnclave's +// own party details store. func (s *SecureEnclave) UpdatePartyInfo(encoded []byte) { s.PartyInfo.UpdatePartyInfo(encoded) } +// GetEncodedPartyInfo provides this SecureEnclaves PartyInfo details in a binary encoded format. func (s *SecureEnclave) GetEncodedPartyInfo() []byte { return api.EncodePartyInfo(s.PartyInfo) } @@ -444,6 +474,9 @@ func loadKeys( return keys, nil } +// DoKeyGeneration is used to generate new public and private key-pairs, writing them to the +// provided file locations. +// Public keys have the "pub" suffix, whereas private keys have the "key" suffix. func DoKeyGeneration(keyFile string) error { pubKey, privKey, err := box.GenerateKey(rand.Reader) if err != nil { diff --git a/server/server.go b/server/server.go index 4d45268..86ab086 100644 --- a/server/server.go +++ b/server/server.go @@ -1,3 +1,4 @@ +// Package server contains the core server components. package server import ( @@ -15,6 +16,7 @@ import ( "net/http/httputil" ) +// Enclave is the interface used by the transaction enclaves. type Enclave interface { Store(message *[]byte, sender []byte, recipients [][]byte) ([]byte, error) StorePayload(encoded []byte) ([]byte, error) @@ -27,6 +29,7 @@ type Enclave interface { GetEncodedPartyInfo() []byte } +// TransactionManager is responsible for handling all transaction requests. type TransactionManager struct { Enclave Enclave } @@ -65,6 +68,7 @@ func requestLogger(handler http.Handler) http.Handler { }) } +// Init initializes a new TransactionManager instance. func Init(enc Enclave, port int, ipcPath string) (TransactionManager, error) { tm := TransactionManager{Enclave : enc} diff --git a/storage/datastore.go b/storage/datastore.go index c46b0a1..b78f27b 100644 --- a/storage/datastore.go +++ b/storage/datastore.go @@ -1,5 +1,6 @@ package storage +// DataStore is an interface that facilitates operations with an underlying persistent data store. type DataStore interface { Write(key *[]byte, value *[]byte) error Read(key *[]byte) (*[]byte, error) diff --git a/utils/http.go b/utils/http.go index 990b1fc..efb5f64 100644 --- a/utils/http.go +++ b/utils/http.go @@ -2,6 +2,7 @@ package utils import "net/http" +// HttpClient is an interface for sending synchronous HTTP requests. type HttpClient interface { Do(req *http.Request) (*http.Response, error) } diff --git a/utils/math.go b/utils/math.go index 2c57916..d0eeccb 100644 --- a/utils/math.go +++ b/utils/math.go @@ -1,7 +1,5 @@ package utils - - func NextPowerOf2(v int) int { v-- v |= v >> 1 From ec87ae24be1ca03f99f8af0d2350845e5bd5b0e6 Mon Sep 17 00:00:00 2001 From: Conor Date: Wed, 9 May 2018 13:27:52 +0100 Subject: [PATCH 09/85] Added pthread dependency to hopefully resolve BerkeleyDB build issues. --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4e4319a..4f57285 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,7 +10,7 @@ variables: # repository in /go/src/gitlab.com/namespace/project # Thus, making a symbolic link corrects this. before_script: - - apt-get update -qq && apt-get install -y -qq libdb-dev # This is hopefully temporary until we completely remove BerkeleyDB. + - apt-get update -qq && apt-get install -y -qq libdb-dev pthread # This is hopefully temporary until we completely remove BerkeleyDB. - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) - ln -svf $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME From 6b85b76872e036a00a77d069e64e4ff9a43c2d4d Mon Sep 17 00:00:00 2001 From: Conor Date: Wed, 9 May 2018 13:31:30 +0100 Subject: [PATCH 10/85] Added correct name for pthread dependency. --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4f57285..56abd07 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,7 +10,7 @@ variables: # repository in /go/src/gitlab.com/namespace/project # Thus, making a symbolic link corrects this. before_script: - - apt-get update -qq && apt-get install -y -qq libdb-dev pthread # This is hopefully temporary until we completely remove BerkeleyDB. + - apt-get update -qq && apt-get install -y -qq libdb-dev libpthread-stubs0-dev # This is hopefully temporary until we completely remove BerkeleyDB. - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) - ln -svf $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME From e03572acf79ea9c47b9ec04ca6474a85b721f68f Mon Sep 17 00:00:00 2001 From: Conor Date: Wed, 9 May 2018 14:20:49 +0100 Subject: [PATCH 11/85] Removed some of the compiler flags. --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 56abd07..4a388f0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,7 +30,7 @@ format: compile: stage: build script: - - go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_DIR/crux + - go build -race -o $CI_PROJECT_DIR/crux artifacts: paths: - crux From 382722bb991cf12c7810369d449397ed72fa642d Mon Sep 17 00:00:00 2001 From: Puneetha Date: Mon, 21 May 2018 16:58:04 +0100 Subject: [PATCH 12/85] Add more testcases. Signed-off-by: Puneetha --- config/config.go | 1 + config/config_test.go | 42 ++++++++++++++++++--- server/data/key | 1 + server/data/key.pub | 1 + server/server_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++- storage/berkleydb.go | 3 +- utils/base64.go | 9 ----- utils/file_test.go | 17 +++++++++ 8 files changed, 144 insertions(+), 17 deletions(-) create mode 100644 server/data/key create mode 100644 server/data/key.pub delete mode 100644 utils/base64.go create mode 100644 utils/file_test.go diff --git a/config/config.go b/config/config.go index ffd417b..5b23c12 100644 --- a/config/config.go +++ b/config/config.go @@ -59,6 +59,7 @@ func InitFlags() { // TLS is not currently supported pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + viper.BindPFlags(pflag.CommandLine) } // Usage prints usage instructions to the console. diff --git a/config/config_test.go b/config/config_test.go index bf9d1e6..faabe76 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -7,6 +7,27 @@ import ( const configFile = "config_testdata.conf" +func TestInitFlags(t *testing.T) { + InitFlags() + conf := AllSettings() + expected := map[string]interface{}{ + Port: -1, + Verbosity: 1, + BerkeleyDb: false, + GenerateKeys: "", + AlwaysSendTo: "", + Storage: "crux.db", + WorkDir: ".", + Url: "", + PublicKeys: "", + OtherNodes: "", + PrivateKeys: "", + Socket: "crux.ipc", + } + + verifyConfig(t, conf, expected) +} + func TestLoadConfig(t *testing.T) { err := LoadConfig(configFile) @@ -24,11 +45,11 @@ func TestLoadConfig(t *testing.T) { WorkDir: "data", Url: "http://127.0.0.1:9001/", TlsServerTrust: "tofu", - PublicKeys: [1]string{"foo.pub"}, - OtherNodes: [1]string{"http://127.0.0.1:9000/"}, + PublicKeys: []interface{}{"foo.pub"}, + OtherNodes: []interface{}{"http://127.0.0.1:9000/"}, TlsKnownServers:"tls-known-servers", TlsClientCert: "tls-client-cert.pem", - PrivateKeys: [1]string{"foo.key"}, + PrivateKeys: []interface{}{"foo.key"}, TlsServerCert: "tls-server-cert.pem", Tls: "strict", TlsKnownClients: "tls-known-clients", @@ -46,7 +67,7 @@ func TestLoadConfig(t *testing.T) { func verifyConfig(t *testing.T, conf map[string]interface{}, expected map[string]interface{}) { for expK, expV := range expected { //if conf[key] != value { - if actV, ok := conf[expK]; !ok { + if actV, ok := conf[expK]; ok { var eq bool switch actV.(type) { // we cannot use == for equality with []interface{} case []interface{}: @@ -54,10 +75,21 @@ func verifyConfig(t *testing.T, conf map[string]interface{}, expected map[string default: eq = actV == expV } - if !eq { t.Errorf("Key: %s with value %v could not be found", expK, expV) } } } } + +func TestGetBoolConfig(t *testing.T) { + if GetBool(Verbosity) != true { + t.Errorf("Verbosity is %t", GetBool(Verbosity)) + } +} + +func TestGetIntConfig(t *testing.T) { + if GetInt(Port) != 9001 { + t.Errorf("Port num 9001 is expected but we got %d", GetInt(Port)) + } +} \ No newline at end of file diff --git a/server/data/key b/server/data/key new file mode 100644 index 0000000..fdb841b --- /dev/null +++ b/server/data/key @@ -0,0 +1 @@ +{"data":{"bytes":"W1n0C+NfjcU/cUBXsP5FQ/frU+qpvKQ7Pi/Mu5Hf/Ic="},"type":"unlocked"} \ No newline at end of file diff --git a/server/data/key.pub b/server/data/key.pub new file mode 100644 index 0000000..e8d6224 --- /dev/null +++ b/server/data/key.pub @@ -0,0 +1 @@ +zSifTnkv5r4K67Dq304eVcM4FpxGfHLe1yTCBm0/7wg= \ No newline at end of file diff --git a/server/server_test.go b/server/server_test.go index 6ab41ae..56e8f06 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -10,6 +10,9 @@ import ( "bytes" "reflect" "github.com/kevinburke/nacl" + "github.com/blk-io/crux/enclave" + "github.com/blk-io/crux/storage" + "path" ) const sender = "BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo=" @@ -153,6 +156,16 @@ func TestReceivedRaw(t *testing.T) { runRawHandlerTest(t, headers, payload, payload, receiveRaw, tm.receiveRaw) } +func TestNilKeyReceivedRaw(t *testing.T) { + tm := TransactionManager{Enclave: &MockEnclave{}} + + headers := make(http.Header) + headers[hKey] = []string{""} + headers[hTo] = []string{receiver} + + runFailingRawHandlerTest(t, headers, payload, payload, receiveRaw, tm.receiveRaw) +} + func TestPush(t *testing.T) { epl := api.EncryptedPayload{ @@ -173,6 +186,7 @@ func TestPush(t *testing.T) { rr := httptest.NewRecorder() tm := TransactionManager{Enclave: &MockEnclave{}} + //tm := testInit(t) handler := http.HandlerFunc(tm.push) handler.ServeHTTP(rr, req) @@ -239,6 +253,32 @@ func runJsonHandlerTest( t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) } } +func runFailingRawHandlerTest( + t *testing.T, + headers http.Header, + payload, expected []byte, + url string, + handlerFunc http.HandlerFunc) { + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + t.Fatal(err) + } + + for k, v := range headers { + req.Header.Set(k, v[0]) + } + + rr := httptest.NewRecorder() + + handler := http.HandlerFunc(handlerFunc) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status == http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } +} func runRawHandlerTest( t *testing.T, @@ -372,4 +412,49 @@ func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { t.Errorf("handler returned unexpected body: got %v wanted %v\n", rr.Body.Bytes(), payload) } -} \ No newline at end of file +} + +func TestInit(t *testing.T) { + db, err := storage.InitLevelDb("data/dir:storage") + if err != nil { + t.Errorf("Error starting server: %v\n", err) + } + pubKeyFiles := []string{"key.pub"} + privKeyFiles := []string{"key"} + + for i, keyFile := range privKeyFiles { + privKeyFiles[i] = path.Join("data", keyFile) + } + + for i, keyFile := range pubKeyFiles { + pubKeyFiles[i] = path.Join("data", keyFile) + } + + key := []nacl.Key{nacl.NewKey()} + expUrl := "http://localhost:9000" + + pi := api.CreatePartyInfo( + "http://localhost:9000", + []string{"http://localhost:9001"}, + key, + http.DefaultClient) + + pi.GetPartyInfo() + + enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient) + tm, err := Init(enc, 9001, "data/crux.db") + if err != nil { + t.Errorf("Error starting server: %v\n", err) + } + runSimpleGetRequest(t, upCheck, upCheckResponse, tm.upcheck) + + expKey := []nacl.Key{nacl.NewKey()} + + pi.RegisterPublicKeys(expKey) + pi.PollPartyInfo() + + url, ok := pi.GetRecipient(expKey[0]) + if ok && url != expUrl{ + t.Errorf("Url is %s whereas %s is expected", url, expUrl) + } +} diff --git a/storage/berkleydb.go b/storage/berkleydb.go index 5babb3d..b2cb610 100644 --- a/storage/berkleydb.go +++ b/storage/berkleydb.go @@ -3,7 +3,6 @@ package storage import ( "encoding/base64" "github.com/jsimonetti/berkeleydb" - "github.com/blk-io/crux/utils" ) type berkleyDb struct { @@ -48,7 +47,7 @@ func (db *berkleyDb) Read(key *[]byte) (*[]byte, error) { } var decoded []byte - decoded, err = utils.DecodeBase64(value) + decoded, err = base64.StdEncoding.DecodeString(value) return &decoded, err } diff --git a/utils/base64.go b/utils/base64.go deleted file mode 100644 index 3c011ff..0000000 --- a/utils/base64.go +++ /dev/null @@ -1,9 +0,0 @@ -package utils - -import "encoding/base64" - -func DecodeBase64(value string) ([]byte, error) { - dest := make([]byte, base64.StdEncoding.DecodedLen(len(value))) - n, err := base64.StdEncoding.Decode(dest, []byte(value)) - return dest[:n], err -} \ No newline at end of file diff --git a/utils/file_test.go b/utils/file_test.go new file mode 100644 index 0000000..33ddeb8 --- /dev/null +++ b/utils/file_test.go @@ -0,0 +1,17 @@ +package utils + +import ( + "testing" +) + +func TestCreateIpcSocket(t *testing.T) { + listener, err := CreateIpcSocket("data/crux.db") + + if err != nil{ + t.Error(err) + } + + if listener == nil { + t.Errorf("Listener not initialised") + } +} From aedc910899df022f34909d7c69501dae87ab69ec Mon Sep 17 00:00:00 2001 From: Puneetha Date: Wed, 23 May 2018 17:27:10 +0100 Subject: [PATCH 13/85] Incorporated the review comments Signed-off-by: Puneetha --- api/internal_test.go | 21 ++++++++++++++++++++- server/data/key | 1 - server/data/key.pub | 1 - server/server_test.go | 32 ++++++++++++++------------------ utils/file_test.go | 8 +++++++- 5 files changed, 41 insertions(+), 22 deletions(-) delete mode 100644 server/data/key delete mode 100644 server/data/key.pub diff --git a/api/internal_test.go b/api/internal_test.go index 20f1166..5e3145d 100644 --- a/api/internal_test.go +++ b/api/internal_test.go @@ -2,8 +2,27 @@ package api import ( "testing" + "net/http" + "github.com/kevinburke/nacl" ) -func TestPush(t *testing.T) { +func TestRegisterPublicKeys(t *testing.T) { + key := []nacl.Key{nacl.NewKey()} + + pi := CreatePartyInfo( + "http://localhost:9000", + []string{"http://localhost:9001"}, + key, + http.DefaultClient) + + expKey := []nacl.Key{nacl.NewKey()} + expUrl := "http://localhost:9000" + + pi.RegisterPublicKeys(expKey) + + url, ok := pi.GetRecipient(expKey[0]) + if !ok || url != expUrl{ + t.Errorf("Url is %s whereas %s is expected", url, expUrl) + } } diff --git a/server/data/key b/server/data/key deleted file mode 100644 index fdb841b..0000000 --- a/server/data/key +++ /dev/null @@ -1 +0,0 @@ -{"data":{"bytes":"W1n0C+NfjcU/cUBXsP5FQ/frU+qpvKQ7Pi/Mu5Hf/Ic="},"type":"unlocked"} \ No newline at end of file diff --git a/server/data/key.pub b/server/data/key.pub deleted file mode 100644 index e8d6224..0000000 --- a/server/data/key.pub +++ /dev/null @@ -1 +0,0 @@ -zSifTnkv5r4K67Dq304eVcM4FpxGfHLe1yTCBm0/7wg= \ No newline at end of file diff --git a/server/server_test.go b/server/server_test.go index 56e8f06..2f27c36 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -13,6 +13,7 @@ import ( "github.com/blk-io/crux/enclave" "github.com/blk-io/crux/storage" "path" + "io/ioutil" ) const sender = "BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo=" @@ -186,7 +187,6 @@ func TestPush(t *testing.T) { rr := httptest.NewRecorder() tm := TransactionManager{Enclave: &MockEnclave{}} - //tm := testInit(t) handler := http.HandlerFunc(tm.push) handler.ServeHTTP(rr, req) @@ -415,7 +415,11 @@ func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { } func TestInit(t *testing.T) { - db, err := storage.InitLevelDb("data/dir:storage") + dbPath, err := ioutil.TempDir("", "TestInit") + if err != nil { + t.Error(err) + } + db, err := storage.InitLevelDb(dbPath) if err != nil { t.Errorf("Error starting server: %v\n", err) } @@ -423,15 +427,14 @@ func TestInit(t *testing.T) { privKeyFiles := []string{"key"} for i, keyFile := range privKeyFiles { - privKeyFiles[i] = path.Join("data", keyFile) + privKeyFiles[i] = path.Join("../enclave/testdata", keyFile) } for i, keyFile := range pubKeyFiles { - pubKeyFiles[i] = path.Join("data", keyFile) + pubKeyFiles[i] = path.Join("../enclave/testdata", keyFile) } key := []nacl.Key{nacl.NewKey()} - expUrl := "http://localhost:9000" pi := api.CreatePartyInfo( "http://localhost:9000", @@ -439,22 +442,15 @@ func TestInit(t *testing.T) { key, http.DefaultClient) - pi.GetPartyInfo() - enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient) - tm, err := Init(enc, 9001, "data/crux.db") + + ipcPath, err := ioutil.TempDir("", "TestInitIpc") + if err != nil { + t.Error(err) + } + tm, err := Init(enc, 9001, ipcPath) if err != nil { t.Errorf("Error starting server: %v\n", err) } runSimpleGetRequest(t, upCheck, upCheckResponse, tm.upcheck) - - expKey := []nacl.Key{nacl.NewKey()} - - pi.RegisterPublicKeys(expKey) - pi.PollPartyInfo() - - url, ok := pi.GetRecipient(expKey[0]) - if ok && url != expUrl{ - t.Errorf("Url is %s whereas %s is expected", url, expUrl) - } } diff --git a/utils/file_test.go b/utils/file_test.go index 33ddeb8..d2f39f5 100644 --- a/utils/file_test.go +++ b/utils/file_test.go @@ -2,10 +2,16 @@ package utils import ( "testing" + "io/ioutil" ) func TestCreateIpcSocket(t *testing.T) { - listener, err := CreateIpcSocket("data/crux.db") + dbPath, err := ioutil.TempDir("", "TestCreateIpcSocket") + if err != nil { + t.Error(err) + } + + listener, err := CreateIpcSocket(dbPath) if err != nil{ t.Error(err) From d9bde54703c002659aedb4e590ecf47d6809c0b3 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Thu, 24 May 2018 13:51:33 +0100 Subject: [PATCH 14/85] Added comment Signed-off-by: Puneetha --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 5b23c12..9d3d096 100644 --- a/config/config.go +++ b/config/config.go @@ -59,7 +59,7 @@ func InitFlags() { // TLS is not currently supported pflag.CommandLine.AddGoFlagSet(flag.CommandLine) - viper.BindPFlags(pflag.CommandLine) + viper.BindPFlags(pflag.CommandLine) // Binding the flags to test the initial configuration } // Usage prints usage instructions to the console. From 8e9f95ff5939483a18a307ac774d76172c9a66e2 Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 24 May 2018 16:02:36 +0100 Subject: [PATCH 15/85] Added logical architecture diagram. --- README.md | 6 ++++++ docs/quorum-architecture.png | Bin 0 -> 146071 bytes docs/quorum-architecture.xml | 1 + 3 files changed, 7 insertions(+) create mode 100644 docs/quorum-architecture.png create mode 100644 docs/quorum-architecture.xml diff --git a/README.md b/README.md index 34071cc..bbc718c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Crux +Quorum Slack + Data privacy for Quorum. Crux is a secure enclave for Quorum written in Golang. @@ -79,6 +81,10 @@ demonstrated below. ![Read Transaction Sequence](./docs/read-tx.svg) +## Logical architecture + +![Logical architecture](https://github.com/blk-io/crux/docs/quorum-architecture.png) + ## Why Crux? *Crux is a constellation located in the southern sky in a bright portion of the Milky Way. It is diff --git a/docs/quorum-architecture.png b/docs/quorum-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..c6c2fe47c521fa418aca6ae034e038adf222e453 GIT binary patch literal 146071 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ȁP$~kp*4*1%`c)s3E&elI4bEHi^*IO)Wa&) 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 P_*0TolYK4o!2f>$%M76* literal 0 HcmV?d00001 diff --git a/docs/quorum-architecture.xml b/docs/quorum-architecture.xml new file mode 100644 index 0000000..1b5c87e --- /dev/null +++ b/docs/quorum-architecture.xml @@ -0,0 +1 @@ +7Ztbb+MoFMc/TaTuPPne9DFJm7bSdJSdVFvtI7FJgorBwuS2n37BxqlvaUjjOu2uOw+DD2Ab/j/D4UB69ijc3jMQLZ9oAHHPMoJtz77tWZbZ927Ef9KyUxbDvE4tC4YCZXszTNE/MCuorCsUwLhQkFOKOYqKRp8SAn1esAHG6KZYbE5x8akRWMCKYeoDXLW+oIAvldVzrbeMB4gWy+zRZtbkGfBfF4yuiHpgz7LnyV+aHYLsZqql8RIEdJMz2Xc9e8Qo5Wkq3I4glr2b9Vtab3wgd//iDBKuU8F20xprgFcwe+Xkxfgu642kOVBWMHr2cLNEHE4j4MvcjQBA2JY8xOLKFMl9g2TZBQZxrNIBiJfJTWQpn4bIVxkYzCAe7vttRDFlIotQAuX9OKOvMDOK7vSSv31Opo8tLHOEca7k3JX/pJ0SPgYhwpLHB4jXkCMfqAxFn9UX1wCjBREXGM5F7w2rnan6dw0Zh9ucSXXuPaQh5GwniqhcV4GffQmO0n2Tw8p1lXGZQ8rzlBEolhf7e7/JKRJK0QM4OBV176F47lkKn6iXkfxp6ZV+KJp6uVW9pCyiIB4oM6dRVkm1teb7OFnSfr8oqedWJL22ahR1rQYUtWs+Vw/Lxs9EYiETgyjCohc4okSUHCICkrd/JByyuRTVMq4Gw8c/sorikfu6rZLh+X04m+uQEQDYn/uaZJhejgwfymY38y3bRlF5y6gqb9Yp329A+OqXXBH+BWAMeSfribI63gVlrZt+S7I+odivF9UDoZSKzOIovU7KBGidFbkSDlJMhejWSDofiCCySNMojCgTrIzhVibyg0GufsfNO9x41gW58Y5zM8TUf/WXAJED9FRoSWrEKSCcARIDX84iqaUMmxGLLhU8fQV0Am/muXo+4Xxu+eehk74bZQFk5fc65Pvm3d0GyDOdInm21SJ518fJm3LAoS50A98XEnOFnRiwBHliSSeTATxAHqdMrMD+d+R9gg/TKjr94+jc/fWkC84jEX2+8pWfC7fQX8l0R8X5LlCrVNwcp2IkJiFI4lWsy8aEvnQcnO/StMqBaR4HYbojh3zhCgPjlby9nFTmIOZpKiYgipdUXWHZgK/ASd+a2XoxrcCF/cC5NCd2KQbiOG1yYh3n5BfkG8pedVHJbHEESGYM4DqyohSU3z8n2zwn+XIdPmc7IW5NCO3z8NEIod0CDmYg1vZhf8I1xLfDbig53/Vol4Vq+GXEVttWBbsbeMMawUyj1kn4DgFyIddNQdO6ELmd7Zg1HiI3qyvUP1eUrcJWZf3vbXsYx/c9bO+z9j1MjbXjw/PzRBQZTB67GPipn6xt2wV1Ww1mmhpLwFOn5B/dnHxGfDE7iHGJSTm777thIuJjsNaGwWcwiUhKXx4Sn+0iufURwCTVEdLAeNHqCtCqRgp+1C3hhpC9Yri7PbhVVlrKycM7QmDxcjimUvxVJDfHpKnEgugmXhS8KKnSOa+dMh2Zu0MUBPIhtXwVCWz6pI0WUDcN8WOVvAnvusqP49R5E03woxFBeH7b/xIlnwABC/HxaI43U0iCdLRh0IdorYaeiNEILJKRyODbbtxpYNxpNUL5zumbgoAllLLVx2F2ygSUbvB7Mur82g/xcnPJPXqdwx0n72z8Hoyf09FkMhw/d2PIB8YQ45J7oDonNyYMrdNZ4qSd9O+2d/7dwOlf8tiF1ub5VrSVAJwAEEai25NzFd2ccZLMVmnOuNaV2WlCZq3dcAa/lsTf7Ut2nQtKbGvEN/4WVcTaTjYzksuOryT2dzvUUN6sbldsjaVm595/PFB5Qffe1loNXk3oVPnrdNB5XeefSXJ0Nbab0FjDW2/grIlcBXLx5OxHO2Is6I6bfFoQoNVYta1xXvpEgn4lizsDkTntBpSPxJ7d9na3xOXbj3mTvNxvpu27fwE= \ No newline at end of file From 5e0efcbc76ed79110b3393b381b3687eed1ea097 Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 24 May 2018 16:03:50 +0100 Subject: [PATCH 16/85] Fixed logical architecture diagram URL. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bbc718c..8fbb0d4 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ demonstrated below. ## Logical architecture -![Logical architecture](https://github.com/blk-io/crux/docs/quorum-architecture.png) +![Logical architecture](https://github.com/blk-io/crux/blob/master/docs/quorum-architecture.png) ## Why Crux? From e337e92e622d0c64cdfe745561b946d928fe59c9 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Thu, 24 May 2018 16:49:28 +0100 Subject: [PATCH 17/85] Added Codecoverage support Signed-off-by: Puneetha --- .gitlab-ci.yml | 14 ++++++++++++++ Makefile | 3 +-- README.md | 2 ++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4a388f0..0d72a6d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,3 +34,17 @@ compile: artifacts: paths: - crux + +code_coverage: + stage: test + script: + - rm -f $GOPATH/cover/*.out $GOPATH/cover/all.merged + - mkdir -p $GOPATH/cover + - for MOD in $(go list ./... | grep -v /vendor/); do \ + - go test -coverpkg=`echo $(go list ./... | grep -v /vendor/)|tr " " ","` \ + - -coverprofile "$GOPATH/cover/unit-`echo $MOD|tr "/" "_"|tr "." "_"`.out" \ + - $MOD 2>&1 | grep -v "no packages being tested depend on"; \ + - done + - ./bin/gocovmerge $GOPATH/cover/*.out > $GOPATH/cover/all.merged + - go tool cover -html $GOPATH/cover/all.merged + diff --git a/Makefile b/Makefile index b523249..3b89b73 100644 --- a/Makefile +++ b/Makefile @@ -109,9 +109,8 @@ Q := $(if $V,,@) .PHONY: bin/gocovmerge bin/goimports bin/gocovmerge: .GOPATH/.ok - @test -d ./vendor/github.com/wadey/gocovmerge || \ + @test -f ./bin/gocovmerge || \ { echo "Vendored gocovmerge not found, try running 'make setup'..."; exit 1; } - $Q go install $(IMPORT_PATH)/vendor/github.com/wadey/gocovmerge bin/goimports: .GOPATH/.ok @test -d ./vendor/golang.org/x/tools/cmd/goimports || \ { echo "Vendored goimports not found, try running 'make setup'..."; exit 1; } diff --git a/README.md b/README.md index 8fbb0d4..245a867 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Quorum Slack +[![Build Status](https://gitlab.com/blk-io/crux//badges/master/build.svg)](https://gitlab.com/blk-io/crux//commits/master) [![Coverage Report](https://gitlab.com/blk-io/crux/badges/master/coverage.svg)](http://blk-io.gitlab.io/crux) + Data privacy for Quorum. Crux is a secure enclave for Quorum written in Golang. From 27673717b49a006db7822c26a687d1e90f633ecc Mon Sep 17 00:00:00 2001 From: Puneetha Date: Fri, 25 May 2018 11:05:01 +0100 Subject: [PATCH 18/85] Fix codecoverage command in .gitlab-ci.yml Signed-off-by: Puneetha --- .gitlab-ci.yml | 13 ++++--------- README.md | 2 -- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0d72a6d..1c16736 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,7 @@ before_script: - ln -svf $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME - go get ./... + - make setup stages: - build @@ -25,12 +26,12 @@ format: script: - go fmt $(go list ./... | grep -v /vendor/) - go vet $(go list ./... | grep -v /vendor/) - - go test -race $(go list ./... | grep -v /vendor/) + - make test compile: stage: build script: - - go build -race -o $CI_PROJECT_DIR/crux + - make build artifacts: paths: - crux @@ -40,11 +41,5 @@ code_coverage: script: - rm -f $GOPATH/cover/*.out $GOPATH/cover/all.merged - mkdir -p $GOPATH/cover - - for MOD in $(go list ./... | grep -v /vendor/); do \ - - go test -coverpkg=`echo $(go list ./... | grep -v /vendor/)|tr " " ","` \ - - -coverprofile "$GOPATH/cover/unit-`echo $MOD|tr "/" "_"|tr "." "_"`.out" \ - - $MOD 2>&1 | grep -v "no packages being tested depend on"; \ - - done - - ./bin/gocovmerge $GOPATH/cover/*.out > $GOPATH/cover/all.merged - - go tool cover -html $GOPATH/cover/all.merged + - make cover diff --git a/README.md b/README.md index 245a867..8fbb0d4 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ Quorum Slack -[![Build Status](https://gitlab.com/blk-io/crux//badges/master/build.svg)](https://gitlab.com/blk-io/crux//commits/master) [![Coverage Report](https://gitlab.com/blk-io/crux/badges/master/coverage.svg)](http://blk-io.gitlab.io/crux) - Data privacy for Quorum. Crux is a secure enclave for Quorum written in Golang. From bf5bfd2cd75dd95787f52db51551d46bcdc42288 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Tue, 29 May 2018 10:45:14 +0100 Subject: [PATCH 19/85] Updated .travis.yml file to use travis CI Signed-off-by: Puneetha --- .travis.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9108e7d..30f3a5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,21 @@ go: - "1.10.x" - tip +env: + REPO_NAME: github.com/blk-io/crux + before_install: - - go get -t -v ./... + - sudo apt-get update -qq && apt-get install -y -qq libdb-dev libpthread-stubs0-dev # This is hopefully temporary until we completely remove BerkeleyDB. + - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) + - ln -svf $TRAVIS_BUILD_DIR $GOPATH/src/$REPO_NAME + - cd $GOPATH/src/$REPO_NAME + - go get ./... + - make setup script: - - go test -race -coverprofile=coverage.txt -covermode=atomic + - make build + - make test + - make cover after_success: - bash <(curl -s https://codecov.io/bash) From d141d8da74585891b3ef9e6c0bd45f0158aaa7f5 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Tue, 29 May 2018 10:52:16 +0100 Subject: [PATCH 20/85] Fixed permission issue in travis.yml Signed-off-by: Puneetha --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 30f3a5f..b874486 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ env: REPO_NAME: github.com/blk-io/crux before_install: - - sudo apt-get update -qq && apt-get install -y -qq libdb-dev libpthread-stubs0-dev # This is hopefully temporary until we completely remove BerkeleyDB. + - apt-get update -qq && apt-get install -y -qq libdb-dev libpthread-stubs0-dev # This is hopefully temporary until we completely remove BerkeleyDB. - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) - ln -svf $TRAVIS_BUILD_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME From 3b2c339396b63a56991e8d0b751708cf504fe52f Mon Sep 17 00:00:00 2001 From: Puneetha Date: Tue, 29 May 2018 11:05:04 +0100 Subject: [PATCH 21/85] Tried to fix travis CI build Signed-off-by: Puneetha --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b874486..8088f4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ env: REPO_NAME: github.com/blk-io/crux before_install: - - apt-get update -qq && apt-get install -y -qq libdb-dev libpthread-stubs0-dev # This is hopefully temporary until we completely remove BerkeleyDB. + - sudo apt-get update -qq && sudo apt-get install -y -qq libdb-dev libpthread-stubs0-dev # This is hopefully temporary until we completely remove BerkeleyDB. - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) - ln -svf $TRAVIS_BUILD_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME From 6c06520c9638188e1da3461d0a5f76c48b12d0cd Mon Sep 17 00:00:00 2001 From: Puneetha Date: Tue, 29 May 2018 11:44:23 +0100 Subject: [PATCH 22/85] Fixed build issues in enclave.go Signed-off-by: Puneetha --- enclave/enclave.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enclave/enclave.go b/enclave/enclave.go index 9454715..34fc359 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -380,7 +380,7 @@ func (s *SecureEnclave) RetrieveFor(digestHash *[]byte, reqRecipient *[]byte) (* return &encoded, nil } } - return nil, fmt.Errorf("invalid recipient %q requested for payload", reqRecipient) + return nil, fmt.Errorf("invalid recipient %x requested for payload", reqRecipient) } // RetrieveAllFor retrieves all payloads that the specified recipient was an original recipient From c74901e56d3934eb8a9eebac70c5bc8dc454aed8 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Tue, 29 May 2018 12:05:42 +0100 Subject: [PATCH 23/85] Added badge to display build status. Signed-off-by: Puneetha --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8fbb0d4..7d1bc98 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Crux Quorum Slack +Build Status Data privacy for Quorum. From 72c9a56029ff4697a033250dff71c9418f931f1c Mon Sep 17 00:00:00 2001 From: Puneetha17 Date: Tue, 29 May 2018 15:42:10 +0100 Subject: [PATCH 24/85] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d1bc98..05448f0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Crux Quorum Slack -Build Status +Build Status Data privacy for Quorum. From 9412cee2e013d72f637fff5aa85c6dfc3f43210c Mon Sep 17 00:00:00 2001 From: Puneetha Date: Fri, 1 Jun 2018 11:29:02 +0100 Subject: [PATCH 25/85] Add gRPC + protobuf support for basic functionality i.e, version & upcheck Signed-off-by: Puneetha --- .travis.yml | 4 ++ Gopkg.lock | 91 +++++++++++++++++++++++++++++++++++++++- Gopkg.toml | 12 ++++++ Makefile | 18 +++++++- config/config.go | 2 + crux.go | 6 ++- server/proto_server.go | 52 +++++++++++++++++++++++ server/server.go | 67 ++++++++++++++++------------- server/server.proto | 30 +++++++++++++ server/server_handler.go | 16 +++++++ server/server_test.go | 2 +- 11 files changed, 266 insertions(+), 34 deletions(-) create mode 100644 server/proto_server.go create mode 100644 server/server.proto create mode 100644 server/server_handler.go diff --git a/.travis.yml b/.travis.yml index 8088f4b..3f2251b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,10 @@ before_install: - ln -svf $TRAVIS_BUILD_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME - go get ./... + - curl -OL https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip + - unzip protoc-3.5.1-linux-x86_64.zip -d protoc3 + - sudo mv protoc3/bin/* /usr/local/bin/ + - sudo mv protoc3/include/* /usr/local/include/ - make setup script: diff --git a/Gopkg.lock b/Gopkg.lock index 7f0a838..f29e819 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -7,12 +7,37 @@ revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" version = "v1.4.7" +[[projects]] + name = "github.com/golang/protobuf" + packages = [ + "jsonpb", + "proto", + "protoc-gen-go/descriptor", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/struct", + "ptypes/timestamp" + ] + revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" + version = "v1.1.0" + [[projects]] branch = "master" name = "github.com/golang/snappy" packages = ["."] revision = "553a641470496b2327abcac10b36396bd98e45c9" +[[projects]] + name = "github.com/grpc-ecosystem/grpc-gateway" + packages = [ + "runtime", + "runtime/internal", + "utilities" + ] + revision = "92583770e3f01b09a0d3e9bdf64321d8bebd48f2" + version = "v1.4.1" + [[projects]] branch = "master" name = "github.com/hashicorp/hcl" @@ -137,6 +162,20 @@ ] revision = "beb2a9779c3b677077c41673505f150149fce895" +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = [ + "context", + "http/httpguts", + "http2", + "http2/hpack", + "idna", + "internal/timeseries", + "trace" + ] + revision = "89e543239a64caf31d3a6865872ea120b41446df" + [[projects]] branch = "master" name = "golang.org/x/sys" @@ -149,16 +188,64 @@ [[projects]] name = "golang.org/x/text" packages = [ + "collate", + "collate/build", + "internal/colltab", "internal/gen", + "internal/tag", "internal/triegen", "internal/ucd", + "language", + "secure/bidirule", "transform", + "unicode/bidi", "unicode/cldr", - "unicode/norm" + "unicode/norm", + "unicode/rangetable" ] revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" +[[projects]] + branch = "master" + name = "google.golang.org/genproto" + packages = [ + "googleapis/api/annotations", + "googleapis/rpc/status" + ] + revision = "694d95ba50e67b2e363f3483057db5d4910c18f9" + +[[projects]] + name = "google.golang.org/grpc" + packages = [ + ".", + "balancer", + "balancer/base", + "balancer/roundrobin", + "channelz", + "codes", + "connectivity", + "credentials", + "encoding", + "encoding/proto", + "grpclb/grpc_lb_v1/messages", + "grpclog", + "internal", + "keepalive", + "metadata", + "naming", + "peer", + "resolver", + "resolver/dns", + "resolver/passthrough", + "stats", + "status", + "tap", + "transport" + ] + revision = "41344da2231b913fa3d983840a57a6b1b7b631a1" + version = "v1.12.0" + [[projects]] name = "gopkg.in/yaml.v2" packages = ["."] @@ -168,6 +255,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "41d3681f00e64a0f64e2519e43b5843746a675772e7112a45abbb21ba87fd6bf" + inputs-digest = "7abc6d1648579cee39f30b03a91bf627a43a06b3a080d90f9c56cae2435eec8c" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 7768a2f..da1e970 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -56,3 +56,15 @@ [prune] go-tests = true unused-packages = true + + [[prune.project]] + name = "github.com/grpc-ecosystem/grpc-gateway" + unused-packages = false + + [[prune.project]] + name = "google.golang.org/grpc" + unused-packages = false + +[[constraint]] + name = "github.com/grpc-ecosystem/grpc-gateway" + version = "1.4.0" diff --git a/Makefile b/Makefile index 3b89b73..0c13262 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,23 @@ IGNORED_PACKAGES := /vendor/ .PHONY: all all: build +protofiles: server/server.pb.gp server/server.pb.gw.go + +server/server.pb.gp: server/server.proto + $Q protoc -I server/ \ + -I $(GOPATH)/../vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + --go_out=plugins=grpc:server \ + server/server.proto + +server/server.pb.gw.go: server/server.proto + $Q protoc -I server/ \ + -I $(GOPATH)/../vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + --grpc-gateway_out=logtostderr=true:server \ + server/server.proto + + .PHONY: build -build: .GOPATH/.ok +build: protofiles .GOPATH/.ok $Q go install $(if $V,-v) $(VERSION_FLAGS) $(IMPORT_PATH) ### Code not in the repository root? Another binary? Add to the path like this. @@ -77,6 +92,7 @@ setup: clean .GOPATH/.ok go get -u github.com/golang/dep/cmd/dep go get -u golang.org/x/tools/cmd/goimports go get -u github.com/wadey/gocovmerge + go get -u github.com/golang/protobuf/protoc-gen-go @test -f Gopkg.toml || \ (cd $(CURDIR)/.GOPATH/src/$(IMPORT_PATH) && ./bin/dep init) (cd $(CURDIR)/.GOPATH/src/$(IMPORT_PATH) && ./bin/dep ensure) diff --git a/config/config.go b/config/config.go index 9d3d096..58611f8 100644 --- a/config/config.go +++ b/config/config.go @@ -24,6 +24,7 @@ const ( GenerateKeys = "generate-keys" BerkeleyDb = "berkeleydb" + UseGRPC = "grpc" Tls = "tls" TlsServerChain = "tlsserverchain" @@ -54,6 +55,7 @@ func InitFlags() { flag.Int(Verbosity, 1, "Verbosity level of logs") flag.String(AlwaysSendTo, "", "List of public keys for nodes to send all transactions too") + flag.Bool(UseGRPC, false, "Use gRPC server") // storage not currently supported as we use LevelDB // TLS is not currently supported diff --git a/crux.go b/crux.go index b39a496..d5738b0 100644 --- a/crux.go +++ b/crux.go @@ -117,11 +117,15 @@ func main() { log.Fatalln("Port must be specified") } - _, err = server.Init(enc, port, ipcPath) + grpc := config.GetBool(config.UseGRPC) + + _, err = server.Init(enc, port, ipcPath, grpc) if err != nil { log.Fatalf("Error starting server: %v\n", err) } + + pi.PollPartyInfo() select {} diff --git a/server/proto_server.go b/server/proto_server.go new file mode 100644 index 0000000..67f48bd --- /dev/null +++ b/server/proto_server.go @@ -0,0 +1,52 @@ +package server + +import ( + "net" + "fmt" + "google.golang.org/grpc" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + log "github.com/sirupsen/logrus" + "golang.org/x/net/context" + "net/http" +) + +func startRPCServer(port int) error { + lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", "localhost", port - 1)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := Server{} + grpcServer := grpc.NewServer() + RegisterClientServer(grpcServer, &s) + go func() { + log.Fatal(grpcServer.Serve(lis)) + }() + + + go func() error { + err := startRESTServer(port) + if err != nil { + log.Fatalf("failed to start gRPC REST server: %s", err) + } + return err + }() + + return err +} + +func startRESTServer(port int) error { + grpcAddress := fmt.Sprintf("%s:%d", "localhost", port - 1) + address := fmt.Sprintf("%s:%d", "localhost", port) + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + mux := runtime.NewServeMux() + opts := []grpc.DialOption{grpc.WithInsecure()} + err := RegisterClientHandlerFromEndpoint(ctx, mux, grpcAddress, opts) + if err != nil { + return fmt.Errorf("could not register service Ping: %s", err) + } + log.Printf("starting HTTP/1.1 REST server on %s", address) + http.ListenAndServe(address, mux) + return nil +} diff --git a/server/server.go b/server/server.go index 86ab086..e302c42 100644 --- a/server/server.go +++ b/server/server.go @@ -69,37 +69,46 @@ func requestLogger(handler http.Handler) http.Handler { } // Init initializes a new TransactionManager instance. -func Init(enc Enclave, port int, ipcPath string) (TransactionManager, error) { +func Init(enc Enclave, port int, ipcPath string, grpc bool) (TransactionManager, error) { tm := TransactionManager{Enclave : enc} + var err error + + if grpc == true { + err = startRPCServer(port) + } else { + httpServer := http.NewServeMux() + httpServer.HandleFunc(upCheck, tm.upcheck) + httpServer.HandleFunc(version, tm.version) + httpServer.HandleFunc(push, tm.push) + httpServer.HandleFunc(resend, tm.resend) + httpServer.HandleFunc(partyInfo, tm.partyInfo) + + serverUrl := "localhost:" + strconv.Itoa(port) + go func () { + log.Fatal(http.ListenAndServe(serverUrl, requestLogger(httpServer))) + }() + log.Infof("HTTP server is running at: %s", serverUrl) + + // Restricted to IPC + ipcServer := http.NewServeMux() + ipcServer.HandleFunc(upCheck, tm.upcheck) + ipcServer.HandleFunc(version, tm.version) + ipcServer.HandleFunc(send, tm.send) + ipcServer.HandleFunc(sendRaw, tm.sendRaw) + ipcServer.HandleFunc(receive, tm.receive) + ipcServer.HandleFunc(receiveRaw, tm.receiveRaw) + ipcServer.HandleFunc(delete, tm.delete) + + ipc, err := utils.CreateIpcSocket(ipcPath) + if err != nil { + log.Fatalf("Failed to start IPC Server at %s", ipcPath) + } + go func() { + log.Fatal(http.Serve(ipc, requestLogger(ipcServer))) + }() + log.Infof("IPC server is running at: %s", ipcPath) + } - httpServer := http.NewServeMux() - httpServer.HandleFunc(upCheck, tm.upcheck) - httpServer.HandleFunc(version, tm.version) - httpServer.HandleFunc(push, tm.push) - httpServer.HandleFunc(resend, tm.resend) - httpServer.HandleFunc(partyInfo, tm.partyInfo) - - serverUrl := "localhost:" + strconv.Itoa(port) - go func() { - log.Fatal(http.ListenAndServe(serverUrl, requestLogger(httpServer))) - }() - log.Infof("HTTP server is running at: %s", serverUrl) - - // Restricted to IPC - ipcServer := http.NewServeMux() - ipcServer.HandleFunc(upCheck, tm.upcheck) - ipcServer.HandleFunc(version, tm.version) - ipcServer.HandleFunc(send, tm.send) - ipcServer.HandleFunc(sendRaw, tm.sendRaw) - ipcServer.HandleFunc(receive, tm.receive) - ipcServer.HandleFunc(receiveRaw, tm.receiveRaw) - ipcServer.HandleFunc(delete, tm.delete) - - ipc, err := utils.CreateIpcSocket(ipcPath) - go func() { - log.Fatal(http.Serve(ipc, requestLogger(ipcServer))) - }() - log.Infof("IPC server is running at: %s", ipcPath) return tm, err } diff --git a/server/server.proto b/server/server.proto new file mode 100644 index 0000000..975bcb3 --- /dev/null +++ b/server/server.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; +package server; + +import "google/api/annotations.proto"; + +message apiVersion{ + string version = 1; +} +message upCheckResp{ + string upcheck = 1; +} +message Tm{ + apiVersion vers = 1; +} + +service client { + rpc version(Tm) returns (apiVersion) { + option (google.api.http) = { + post: "/version" + body: "*" + }; + } + + rpc upcheck(Tm) returns (upCheckResp) { + option (google.api.http) = { + post: "/upcheck" + body: "*" + }; + } +} \ No newline at end of file diff --git a/server/server_handler.go b/server/server_handler.go new file mode 100644 index 0000000..08c68e3 --- /dev/null +++ b/server/server_handler.go @@ -0,0 +1,16 @@ +package server + +import ( + "golang.org/x/net/context" +) + +type Server struct { +} + +func (s *Server) Version(ctx context.Context, in *Tm) (*ApiVersion, error) { + return &ApiVersion{Version:apiVersion}, nil +} + +func (s *Server) Upcheck(ctx context.Context, in *Tm) (*UpCheckResp, error) { + return &UpCheckResp{Upcheck:upCheckResponse}, nil +} \ No newline at end of file diff --git a/server/server_test.go b/server/server_test.go index 2f27c36..43dda2b 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -448,7 +448,7 @@ func TestInit(t *testing.T) { if err != nil { t.Error(err) } - tm, err := Init(enc, 9001, ipcPath) + tm, err := Init(enc, 9001, ipcPath, false) if err != nil { t.Errorf("Error starting server: %v\n", err) } From fb45bbf8254d5a798cb2c0c89d7b8a256ee4d3d5 Mon Sep 17 00:00:00 2001 From: Puneetha17 Date: Fri, 1 Jun 2018 11:32:55 +0100 Subject: [PATCH 26/85] Simple formatting crux.go --- crux.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crux.go b/crux.go index d5738b0..47dac3d 100644 --- a/crux.go +++ b/crux.go @@ -124,8 +124,6 @@ func main() { log.Fatalf("Error starting server: %v\n", err) } - - pi.PollPartyInfo() select {} @@ -134,4 +132,4 @@ func main() { func exit() { config.Usage() os.Exit(1) -} \ No newline at end of file +} From 3a6449acd0324bec659bf02f99464b5f96caff68 Mon Sep 17 00:00:00 2001 From: Puneetha17 Date: Fri, 1 Jun 2018 11:39:43 +0100 Subject: [PATCH 27/85] Update .travis.yml to avoid reduntant go get --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3f2251b..3b03216 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ before_install: - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) - ln -svf $TRAVIS_BUILD_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME - - go get ./... - curl -OL https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip - unzip protoc-3.5.1-linux-x86_64.zip -d protoc3 - sudo mv protoc3/bin/* /usr/local/bin/ From 91160ec6f5c2fa1ec2239b7c80b0789189ade42a Mon Sep 17 00:00:00 2001 From: Puneetha Date: Fri, 1 Jun 2018 17:03:12 +0100 Subject: [PATCH 28/85] Fix Travis build Signed-off-by: Puneetha --- .travis.yml | 4 ++++ Gopkg.toml | 9 --------- Makefile | 1 + 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3b03216..74a3f36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,10 @@ before_install: - unzip protoc-3.5.1-linux-x86_64.zip -d protoc3 - sudo mv protoc3/bin/* /usr/local/bin/ - sudo mv protoc3/include/* /usr/local/include/ + - go get -u github.com/golang/protobuf/protoc-gen-go + - go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway + - go get -u github.com/golang/dep/cmd/dep + - dep ensure - make setup script: diff --git a/Gopkg.toml b/Gopkg.toml index da1e970..a84569f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -55,15 +55,6 @@ [prune] go-tests = true - unused-packages = true - - [[prune.project]] - name = "github.com/grpc-ecosystem/grpc-gateway" - unused-packages = false - - [[prune.project]] - name = "google.golang.org/grpc" - unused-packages = false [[constraint]] name = "github.com/grpc-ecosystem/grpc-gateway" diff --git a/Makefile b/Makefile index 0c13262..1f9fa75 100644 --- a/Makefile +++ b/Makefile @@ -93,6 +93,7 @@ setup: clean .GOPATH/.ok go get -u golang.org/x/tools/cmd/goimports go get -u github.com/wadey/gocovmerge go get -u github.com/golang/protobuf/protoc-gen-go + go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway @test -f Gopkg.toml || \ (cd $(CURDIR)/.GOPATH/src/$(IMPORT_PATH) && ./bin/dep init) (cd $(CURDIR)/.GOPATH/src/$(IMPORT_PATH) && ./bin/dep ensure) From e99ae80dc94e671069190c785c83b4b5c60a930d Mon Sep 17 00:00:00 2001 From: Puneetha Date: Mon, 4 Jun 2018 11:53:47 +0100 Subject: [PATCH 29/85] Code refactoring Signed-off-by: Puneetha --- .travis.yml | 8 ---- Gopkg.lock | 8 +--- Makefile | 17 +++++--- server/{server.proto => grpc.proto} | 10 +---- server/messages.proto | 14 ++++++ server/server.go | 68 ++++++++++++++++------------- 6 files changed, 66 insertions(+), 59 deletions(-) rename server/{server.proto => grpc.proto} (73%) create mode 100644 server/messages.proto diff --git a/.travis.yml b/.travis.yml index 74a3f36..d50a022 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,14 +12,6 @@ before_install: - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) - ln -svf $TRAVIS_BUILD_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME - - curl -OL https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip - - unzip protoc-3.5.1-linux-x86_64.zip -d protoc3 - - sudo mv protoc3/bin/* /usr/local/bin/ - - sudo mv protoc3/include/* /usr/local/include/ - - go get -u github.com/golang/protobuf/protoc-gen-go - - go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway - - go get -u github.com/golang/dep/cmd/dep - - dep ensure - make setup script: diff --git a/Gopkg.lock b/Gopkg.lock index f29e819..7a047c8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -12,7 +12,6 @@ packages = [ "jsonpb", "proto", - "protoc-gen-go/descriptor", "ptypes", "ptypes/any", "ptypes/duration", @@ -209,10 +208,7 @@ [[projects]] branch = "master" name = "google.golang.org/genproto" - packages = [ - "googleapis/api/annotations", - "googleapis/rpc/status" - ] + packages = ["googleapis/rpc/status"] revision = "694d95ba50e67b2e363f3483057db5d4910c18f9" [[projects]] @@ -255,6 +251,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "7abc6d1648579cee39f30b03a91bf627a43a06b3a080d90f9c56cae2435eec8c" + inputs-digest = "a7dbc72760828be466ae1a6d718b76f8274462afaa818082b1d605065021ed40" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Makefile b/Makefile index 1f9fa75..48567c9 100644 --- a/Makefile +++ b/Makefile @@ -11,19 +11,19 @@ IGNORED_PACKAGES := /vendor/ .PHONY: all all: build -protofiles: server/server.pb.gp server/server.pb.gw.go +protofiles: proto grpc -server/server.pb.gp: server/server.proto +proto: $Q protoc -I server/ \ -I $(GOPATH)/../vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ --go_out=plugins=grpc:server \ - server/server.proto + server/*.proto -server/server.pb.gw.go: server/server.proto +grpc: $Q protoc -I server/ \ -I $(GOPATH)/../vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ --grpc-gateway_out=logtostderr=true:server \ - server/server.proto + server/grpc.proto .PHONY: build @@ -41,6 +41,7 @@ build: protofiles .GOPATH/.ok clean: $Q rm -rf bin .GOPATH + $Q rm -f server/*\.pb* test: .GOPATH/.ok $Q go test $(if $V,-v) -i -race $(allpackages) # install -race libs to speed up next run @@ -89,6 +90,12 @@ setup: clean .GOPATH/.ok echo "/.GOPATH" >> .gitignore; \ echo "/bin" >> .gitignore; \ fi + $Q curl -OL https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip + $Q unzip protoc-3.5.1-linux-x86_64.zip -d protoc3 + $Q mv protoc3/bin/* .GOPATH/bin/ + $Q mv protoc3/include/* .GOPATH/include/ + $Q rm -r protoc3 + $Q rm protoc-3.5.1-linux-x86_64.zip go get -u github.com/golang/dep/cmd/dep go get -u golang.org/x/tools/cmd/goimports go get -u github.com/wadey/gocovmerge diff --git a/server/server.proto b/server/grpc.proto similarity index 73% rename from server/server.proto rename to server/grpc.proto index 975bcb3..d2df041 100644 --- a/server/server.proto +++ b/server/grpc.proto @@ -2,16 +2,8 @@ syntax = "proto3"; package server; import "google/api/annotations.proto"; +import "messages.proto"; -message apiVersion{ - string version = 1; -} -message upCheckResp{ - string upcheck = 1; -} -message Tm{ - apiVersion vers = 1; -} service client { rpc version(Tm) returns (apiVersion) { diff --git a/server/messages.proto b/server/messages.proto new file mode 100644 index 0000000..e37c493 --- /dev/null +++ b/server/messages.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; +package server; + +import "google/api/annotations.proto"; + +message apiVersion{ + string version = 1; +} +message upCheckResp{ + string upcheck = 1; +} +message Tm{ + apiVersion vers = 1; +} \ No newline at end of file diff --git a/server/server.go b/server/server.go index e302c42..b72a26c 100644 --- a/server/server.go +++ b/server/server.go @@ -76,42 +76,48 @@ func Init(enc Enclave, port int, ipcPath string, grpc bool) (TransactionManager, if grpc == true { err = startRPCServer(port) } else { - httpServer := http.NewServeMux() - httpServer.HandleFunc(upCheck, tm.upcheck) - httpServer.HandleFunc(version, tm.version) - httpServer.HandleFunc(push, tm.push) - httpServer.HandleFunc(resend, tm.resend) - httpServer.HandleFunc(partyInfo, tm.partyInfo) - - serverUrl := "localhost:" + strconv.Itoa(port) - go func () { - log.Fatal(http.ListenAndServe(serverUrl, requestLogger(httpServer))) - }() - log.Infof("HTTP server is running at: %s", serverUrl) - - // Restricted to IPC - ipcServer := http.NewServeMux() - ipcServer.HandleFunc(upCheck, tm.upcheck) - ipcServer.HandleFunc(version, tm.version) - ipcServer.HandleFunc(send, tm.send) - ipcServer.HandleFunc(sendRaw, tm.sendRaw) - ipcServer.HandleFunc(receive, tm.receive) - ipcServer.HandleFunc(receiveRaw, tm.receiveRaw) - ipcServer.HandleFunc(delete, tm.delete) - - ipc, err := utils.CreateIpcSocket(ipcPath) - if err != nil { - log.Fatalf("Failed to start IPC Server at %s", ipcPath) - } - go func() { - log.Fatal(http.Serve(ipc, requestLogger(ipcServer))) - }() - log.Infof("IPC server is running at: %s", ipcPath) + err = tm.startHTTPserver(port, ipcPath) } return tm, err } +func (tm *TransactionManager) startHTTPserver(port int, ipcPath string) error { + httpServer := http.NewServeMux() + httpServer.HandleFunc(upCheck, tm.upcheck) + httpServer.HandleFunc(version, tm.version) + httpServer.HandleFunc(push, tm.push) + httpServer.HandleFunc(resend, tm.resend) + httpServer.HandleFunc(partyInfo, tm.partyInfo) + + serverUrl := "localhost:" + strconv.Itoa(port) + go func () { + log.Fatal(http.ListenAndServe(serverUrl, requestLogger(httpServer))) + }() + log.Infof("HTTP server is running at: %s", serverUrl) + + // Restricted to IPC + ipcServer := http.NewServeMux() + ipcServer.HandleFunc(upCheck, tm.upcheck) + ipcServer.HandleFunc(version, tm.version) + ipcServer.HandleFunc(send, tm.send) + ipcServer.HandleFunc(sendRaw, tm.sendRaw) + ipcServer.HandleFunc(receive, tm.receive) + ipcServer.HandleFunc(receiveRaw, tm.receiveRaw) + ipcServer.HandleFunc(delete, tm.delete) + + ipc, err := utils.CreateIpcSocket(ipcPath) + if err != nil { + log.Fatalf("Failed to start IPC Server at %s", ipcPath) + } + go func() { + log.Fatal(http.Serve(ipc, requestLogger(ipcServer))) + }() + log.Infof("IPC server is running at: %s", ipcPath) + + return err +} + func (s *TransactionManager) upcheck(w http.ResponseWriter, req *http.Request) { fmt.Fprint(w, upCheckResponse) } From 0da1eef6b8fef1cb78609ebbd30ef543a7c6089a Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Mon, 4 Jun 2018 12:23:24 +0100 Subject: [PATCH 30/85] Fix travis build failure --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index d50a022..cffedfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,14 @@ before_install: - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) - ln -svf $TRAVIS_BUILD_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME + - curl -OL https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip + - unzip protoc-3.5.1-linux-x86_64.zip -d protoc3 + - sudo mv protoc3/bin/* /usr/local/bin/ + - sudo mv protoc3/include/* /usr/local/include/ + - rm -r protoc3 + - rm protoc-3.5.1-linux-x86_64.zip + - go get -u github.com/golang/protobuf/protoc-gen-go + - go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway - make setup script: From 00b0cbb74c7e3eb7aa0c0207c4bfa768e050375e Mon Sep 17 00:00:00 2001 From: Puneetha Date: Tue, 5 Jun 2018 11:44:02 +0100 Subject: [PATCH 31/85] Updating protobufs according to the protobuf style guide Signed-off-by: Puneetha --- server/grpc.proto | 4 ++-- server/messages.proto | 8 ++++---- server/proto_server.go | 1 - server/server.go | 2 +- server/server_handler.go | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/server/grpc.proto b/server/grpc.proto index d2df041..5a0c57f 100644 --- a/server/grpc.proto +++ b/server/grpc.proto @@ -6,14 +6,14 @@ import "messages.proto"; service client { - rpc version(Tm) returns (apiVersion) { + rpc version(Tm) returns (ApiVersion) { option (google.api.http) = { post: "/version" body: "*" }; } - rpc upcheck(Tm) returns (upCheckResp) { + rpc upcheck(Tm) returns (UpCheckResponse) { option (google.api.http) = { post: "/upcheck" body: "*" diff --git a/server/messages.proto b/server/messages.proto index e37c493..85349af 100644 --- a/server/messages.proto +++ b/server/messages.proto @@ -3,12 +3,12 @@ package server; import "google/api/annotations.proto"; -message apiVersion{ +message ApiVersion{ string version = 1; } -message upCheckResp{ - string upcheck = 1; +message UpCheckResponse{ + string up_check = 1; } message Tm{ - apiVersion vers = 1; + ApiVersion vers = 1; } \ No newline at end of file diff --git a/server/proto_server.go b/server/proto_server.go index 67f48bd..eaa0141 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -22,7 +22,6 @@ func startRPCServer(port int) error { log.Fatal(grpcServer.Serve(lis)) }() - go func() error { err := startRESTServer(port) if err != nil { diff --git a/server/server.go b/server/server.go index b72a26c..976ed5c 100644 --- a/server/server.go +++ b/server/server.go @@ -91,7 +91,7 @@ func (tm *TransactionManager) startHTTPserver(port int, ipcPath string) error { httpServer.HandleFunc(partyInfo, tm.partyInfo) serverUrl := "localhost:" + strconv.Itoa(port) - go func () { + go func() { log.Fatal(http.ListenAndServe(serverUrl, requestLogger(httpServer))) }() log.Infof("HTTP server is running at: %s", serverUrl) diff --git a/server/server_handler.go b/server/server_handler.go index 08c68e3..956a45f 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -11,6 +11,6 @@ func (s *Server) Version(ctx context.Context, in *Tm) (*ApiVersion, error) { return &ApiVersion{Version:apiVersion}, nil } -func (s *Server) Upcheck(ctx context.Context, in *Tm) (*UpCheckResp, error) { - return &UpCheckResp{Upcheck:upCheckResponse}, nil +func (s *Server) Upcheck(ctx context.Context, in *Tm) (*UpCheckResponse, error) { + return &UpCheckResponse{UpCheck:upCheckResponse}, nil } \ No newline at end of file From 177f062d029b9a51b061a7c134504d2664fb239e Mon Sep 17 00:00:00 2001 From: Puneetha Date: Thu, 7 Jun 2018 16:02:04 +0100 Subject: [PATCH 32/85] Add send functionality using gRPC. Signed-off-by: Puneetha --- server/grpc.proto | 9 ++++--- server/messages.proto | 10 ++++++++ server/proto_server.go | 26 +++++++++++++------ server/server.go | 3 ++- server/server_handler.go | 55 +++++++++++++++++++++++++++++++++++++++- server/server_test.go | 53 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+), 13 deletions(-) diff --git a/server/grpc.proto b/server/grpc.proto index 5a0c57f..fb40507 100644 --- a/server/grpc.proto +++ b/server/grpc.proto @@ -5,18 +5,21 @@ import "google/api/annotations.proto"; import "messages.proto"; -service client { - rpc version(Tm) returns (ApiVersion) { +service Client { + rpc Version(Tm) returns (ApiVersion) { option (google.api.http) = { post: "/version" body: "*" }; } - rpc upcheck(Tm) returns (UpCheckResponse) { + rpc Upcheck(Tm) returns (UpCheckResponse) { option (google.api.http) = { post: "/upcheck" body: "*" }; } + + rpc Send(SendRequest) returns (SendResponse) { + } } \ No newline at end of file diff --git a/server/messages.proto b/server/messages.proto index 85349af..b04bced 100644 --- a/server/messages.proto +++ b/server/messages.proto @@ -11,4 +11,14 @@ message UpCheckResponse{ } message Tm{ ApiVersion vers = 1; + SendRequest req = 2; + SendResponse resp = 3; +} +message SendRequest{ + string Payload = 1; + string From = 2; + repeated string To = 3; +} +message SendResponse{ + string Key = 1; } \ No newline at end of file diff --git a/server/proto_server.go b/server/proto_server.go index eaa0141..534368e 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -1,21 +1,22 @@ package server import ( - "net" "fmt" "google.golang.org/grpc" "github.com/grpc-ecosystem/grpc-gateway/runtime" log "github.com/sirupsen/logrus" "golang.org/x/net/context" "net/http" + "github.com/blk-io/crux/utils" + "net" ) -func startRPCServer(port int) error { - lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", "localhost", port - 1)) +func (tm *TransactionManager) startRPCServer(port int, ipcPath string) error { + lis, err := utils.CreateIpcSocket(ipcPath) if err != nil { log.Fatalf("failed to listen: %v", err) } - s := Server{} + s := Server{Enclave : tm.Enclave} grpcServer := grpc.NewServer() RegisterClientServer(grpcServer, &s) go func() { @@ -23,7 +24,7 @@ func startRPCServer(port int) error { }() go func() error { - err := startRESTServer(port) + err := tm.startRESTServer(port) if err != nil { log.Fatalf("failed to start gRPC REST server: %s", err) } @@ -33,15 +34,24 @@ func startRPCServer(port int) error { return err } -func startRESTServer(port int) error { - grpcAddress := fmt.Sprintf("%s:%d", "localhost", port - 1) +func (tm *TransactionManager) startRESTServer(port int) error { + grpcAddress := fmt.Sprintf("%s:%d", "localhost", port-1) + lis, err := net.Listen("tcp", grpcAddress) + + s := Server{Enclave : tm.Enclave} + grpcServer := grpc.NewServer() + RegisterClientServer(grpcServer, &s) + go func() { + log.Fatal(grpcServer.Serve(lis)) + }() + address := fmt.Sprintf("%s:%d", "localhost", port) ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithInsecure()} - err := RegisterClientHandlerFromEndpoint(ctx, mux, grpcAddress, opts) + err = RegisterClientHandlerFromEndpoint(ctx, mux, grpcAddress, opts) if err != nil { return fmt.Errorf("could not register service Ping: %s", err) } diff --git a/server/server.go b/server/server.go index 976ed5c..7bd5c3d 100644 --- a/server/server.go +++ b/server/server.go @@ -74,7 +74,8 @@ func Init(enc Enclave, port int, ipcPath string, grpc bool) (TransactionManager, var err error if grpc == true { - err = startRPCServer(port) + err = tm.startRPCServer(port, ipcPath) + } else { err = tm.startHTTPserver(port, ipcPath) } diff --git a/server/server_handler.go b/server/server_handler.go index 956a45f..a8a85e5 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -2,9 +2,14 @@ package server import ( "golang.org/x/net/context" + log "github.com/sirupsen/logrus" + "encoding/base64" + "encoding/hex" + "fmt" ) type Server struct { + Enclave Enclave } func (s *Server) Version(ctx context.Context, in *Tm) (*ApiVersion, error) { @@ -13,4 +18,52 @@ func (s *Server) Version(ctx context.Context, in *Tm) (*ApiVersion, error) { func (s *Server) Upcheck(ctx context.Context, in *Tm) (*UpCheckResponse, error) { return &UpCheckResponse{UpCheck:upCheckResponse}, nil -} \ No newline at end of file +} +func (s *Server) Send(ctx context.Context, in *SendRequest) (*SendResponse, error) { + payload, err := base64.StdEncoding.DecodeString(in.Payload) + if err != nil { + decodeErrorGRPC("payload", in.GetPayload(), err) + } + + key, err := s.processSend(in.GetFrom(), in.GetTo(), &payload) + var sendResp SendResponse + if err != nil { + log.Error(err) + } else { + encodedKey := base64.StdEncoding.EncodeToString(key) + sendResp = SendResponse{Key: encodedKey} + } + return &sendResp, err +} + +func (s *Server) processSend(b64from string, b64recipients []string, payload *[]byte) ([]byte, error) { + log.WithFields(log.Fields{ + "b64From" : b64from, + "b64Recipients": b64recipients, + "payload": hex.EncodeToString(*payload),}).Debugf( + "Processing send request") + + sender, err := base64.StdEncoding.DecodeString(b64from) + if err != nil { + decodeErrorGRPC("sender", b64from, err) + return nil, err + } + + recipients := make([][]byte, len(b64recipients)) + for i, value := range b64recipients { + recipient, err := base64.StdEncoding.DecodeString(value) + if err != nil { + decodeErrorGRPC("recipients", value, err) + return nil, err + } else { + recipients[i] = recipient + } + } + + return s.Enclave.Store(payload, sender, recipients) +} + +func decodeErrorGRPC(name string, value string, err error) { + log.Error(fmt.Sprintf("Invalid request: unable to decode %s: %s, error: %s\n", + name, value, err)) +} diff --git a/server/server_test.go b/server/server_test.go index 43dda2b..7684097 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -12,8 +12,11 @@ import ( "github.com/kevinburke/nacl" "github.com/blk-io/crux/enclave" "github.com/blk-io/crux/storage" + "golang.org/x/net/context" + "google.golang.org/grpc" "path" "io/ioutil" + "fmt" ) const sender = "BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo=" @@ -116,6 +119,45 @@ func TestSend(t *testing.T) { } } +func TestGRPCSend(t *testing.T) { + sendReqs := []SendRequest{ + { + Payload: encodedPayload, + From: sender, + To: []string{receiver}, + }, + { + Payload: encodedPayload, + To: []string{}, + }, + { + Payload: encodedPayload, + }, + } + expected := SendResponse{Key: encodedPayload} + + ipcPath := InitgRPCServer(t, true) + + var conn *grpc.ClientConn + conn, err := grpc.Dial(fmt.Sprintf("passthrough:///unix://%s", ipcPath), grpc.WithInsecure()) + if err != nil { + t.Fatalf("Connection to gRPC server failed with error %s", err) + } + defer conn.Close() + c := NewClientClient(conn) + + for _, sendReq := range sendReqs { + resp, err:= c.Send(context.Background(), &sendReq) + if err != nil { + t.Fatalf("gRPC send failed with %s", err) + } + response := SendResponse{Key:resp.Key} + if !reflect.DeepEqual(response, expected) { + t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) + } + } +} + func TestSendRaw(t *testing.T) { tm := TransactionManager{Enclave: &MockEnclave{}} @@ -414,6 +456,17 @@ func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { } } +func InitgRPCServer(t *testing.T, grpc bool) (string) { + ipcPath, err := ioutil.TempDir("", "TestInitIpc") + tm, err := Init(&MockEnclave{}, 9005, ipcPath, grpc) + + if err != nil { + t.Errorf("Error starting server: %v\n", err) + } + runSimpleGetRequest(t, upCheck, upCheckResponse, tm.upcheck) + return ipcPath +} + func TestInit(t *testing.T) { dbPath, err := ioutil.TempDir("", "TestInit") if err != nil { From add07b560b3309951e4c9d8f49f8621d1be3eab2 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Fri, 8 Jun 2018 17:01:19 +0100 Subject: [PATCH 33/85] Add receive functionality using gRPC Signed-off-by: Puneetha --- server/grpc.proto | 7 +++++-- server/messages.proto | 22 +++++++++++--------- server/proto_server.go | 6 +++--- server/server.go | 6 +++--- server/server_handler.go | 40 ++++++++++++++++++++++++++---------- server/server_test.go | 44 +++++++++++++++++++++++++++++++++------- 6 files changed, 89 insertions(+), 36 deletions(-) diff --git a/server/grpc.proto b/server/grpc.proto index fb40507..a11d7fb 100644 --- a/server/grpc.proto +++ b/server/grpc.proto @@ -6,14 +6,14 @@ import "messages.proto"; service Client { - rpc Version(Tm) returns (ApiVersion) { + rpc Version(ApiVersion) returns (ApiVersion) { option (google.api.http) = { post: "/version" body: "*" }; } - rpc Upcheck(Tm) returns (UpCheckResponse) { + rpc Upcheck(UpCheckResponse) returns (UpCheckResponse) { option (google.api.http) = { post: "/upcheck" body: "*" @@ -22,4 +22,7 @@ service Client { rpc Send(SendRequest) returns (SendResponse) { } + + rpc Receive(ReceiveRequest) returns (ReceiveResponse){ + } } \ No newline at end of file diff --git a/server/messages.proto b/server/messages.proto index b04bced..a282309 100644 --- a/server/messages.proto +++ b/server/messages.proto @@ -7,18 +7,20 @@ message ApiVersion{ string version = 1; } message UpCheckResponse{ - string up_check = 1; -} -message Tm{ - ApiVersion vers = 1; - SendRequest req = 2; - SendResponse resp = 3; + string message = 1; } message SendRequest{ - string Payload = 1; - string From = 2; - repeated string To = 3; + bytes payload = 1; + string from = 2; + repeated string to = 3; } message SendResponse{ - string Key = 1; + bytes key = 1; +} +message ReceiveRequest{ + bytes key = 1; + string to = 2; +} +message ReceiveResponse{ + bytes payload = 1; } \ No newline at end of file diff --git a/server/proto_server.go b/server/proto_server.go index 534368e..f007d45 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -11,7 +11,7 @@ import ( "net" ) -func (tm *TransactionManager) startRPCServer(port int, ipcPath string) error { +func (tm *TransactionManager) startRpcServer(port int, ipcPath string) error { lis, err := utils.CreateIpcSocket(ipcPath) if err != nil { log.Fatalf("failed to listen: %v", err) @@ -24,7 +24,7 @@ func (tm *TransactionManager) startRPCServer(port int, ipcPath string) error { }() go func() error { - err := tm.startRESTServer(port) + err := tm.startRestServer(port) if err != nil { log.Fatalf("failed to start gRPC REST server: %s", err) } @@ -34,7 +34,7 @@ func (tm *TransactionManager) startRPCServer(port int, ipcPath string) error { return err } -func (tm *TransactionManager) startRESTServer(port int) error { +func (tm *TransactionManager) startRestServer(port int) error { grpcAddress := fmt.Sprintf("%s:%d", "localhost", port-1) lis, err := net.Listen("tcp", grpcAddress) diff --git a/server/server.go b/server/server.go index 7bd5c3d..a804bba 100644 --- a/server/server.go +++ b/server/server.go @@ -74,16 +74,16 @@ func Init(enc Enclave, port int, ipcPath string, grpc bool) (TransactionManager, var err error if grpc == true { - err = tm.startRPCServer(port, ipcPath) + err = tm.startRpcServer(port, ipcPath) } else { - err = tm.startHTTPserver(port, ipcPath) + err = tm.startHttpserver(port, ipcPath) } return tm, err } -func (tm *TransactionManager) startHTTPserver(port int, ipcPath string) error { +func (tm *TransactionManager) startHttpserver(port int, ipcPath string) error { httpServer := http.NewServeMux() httpServer.HandleFunc(upCheck, tm.upcheck) httpServer.HandleFunc(version, tm.version) diff --git a/server/server_handler.go b/server/server_handler.go index a8a85e5..1f7c554 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -12,26 +12,20 @@ type Server struct { Enclave Enclave } -func (s *Server) Version(ctx context.Context, in *Tm) (*ApiVersion, error) { +func (s *Server) Version(ctx context.Context, in *ApiVersion) (*ApiVersion, error) { return &ApiVersion{Version:apiVersion}, nil } -func (s *Server) Upcheck(ctx context.Context, in *Tm) (*UpCheckResponse, error) { - return &UpCheckResponse{UpCheck:upCheckResponse}, nil +func (s *Server) Upcheck(ctx context.Context, in *UpCheckResponse) (*UpCheckResponse, error) { + return &UpCheckResponse{Message:upCheckResponse}, nil } func (s *Server) Send(ctx context.Context, in *SendRequest) (*SendResponse, error) { - payload, err := base64.StdEncoding.DecodeString(in.Payload) - if err != nil { - decodeErrorGRPC("payload", in.GetPayload(), err) - } - - key, err := s.processSend(in.GetFrom(), in.GetTo(), &payload) + key, err := s.processSend(in.GetFrom(), in.GetTo(), &in.Payload) var sendResp SendResponse if err != nil { log.Error(err) } else { - encodedKey := base64.StdEncoding.EncodeToString(key) - sendResp = SendResponse{Key: encodedKey} + sendResp = SendResponse{Key: key} } return &sendResp, err } @@ -63,6 +57,30 @@ func (s *Server) processSend(b64from string, b64recipients []string, payload *[] return s.Enclave.Store(payload, sender, recipients) } +func (s *Server) Receive(ctx context.Context, in *ReceiveRequest) (*ReceiveResponse, error) { + payload, err := s.processReceive(in.Key, in.To) + var receiveResp ReceiveResponse + if err != nil { + log.Error(err) + } else { + receiveResp = ReceiveResponse{Payload: payload} + } + return &receiveResp, err +} + +func (s *Server) processReceive(b64Key []byte, b64To string) ([]byte, error) { + if b64To != "" { + to, err := base64.StdEncoding.DecodeString(b64To) + if err != nil { + return nil, fmt.Errorf("unable to decode to: %s", b64Key) + } + + return s.Enclave.Retrieve(&b64Key, &to) + } else { + return s.Enclave.RetrieveDefault(&b64Key) + } +} + func decodeErrorGRPC(name string, value string, err error) { log.Error(fmt.Sprintf("Invalid request: unable to decode %s: %s, error: %s\n", name, value, err)) diff --git a/server/server_test.go b/server/server_test.go index 7684097..3eced41 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -122,21 +122,21 @@ func TestSend(t *testing.T) { func TestGRPCSend(t *testing.T) { sendReqs := []SendRequest{ { - Payload: encodedPayload, + Payload: payload, From: sender, To: []string{receiver}, }, { - Payload: encodedPayload, + Payload: payload, To: []string{}, }, { - Payload: encodedPayload, + Payload: payload, }, } - expected := SendResponse{Key: encodedPayload} + expected := SendResponse{Key: payload} - ipcPath := InitgRPCServer(t, true) + ipcPath := InitgRPCServer(t, true, 9005) var conn *grpc.ClientConn conn, err := grpc.Dial(fmt.Sprintf("passthrough:///unix://%s", ipcPath), grpc.WithInsecure()) @@ -189,6 +189,36 @@ func TestReceive(t *testing.T) { } } +func TestGRPCReceive(t *testing.T) { + receiveReqs := []ReceiveRequest{ + { + Key: payload, + To: receiver, + }, + } + expected := ReceiveResponse{Payload: payload} + + ipcPath := InitgRPCServer(t, true, 9010) + var conn *grpc.ClientConn + conn, err := grpc.Dial(fmt.Sprintf("passthrough:///unix://%s", ipcPath), grpc.WithInsecure()) + if err != nil { + t.Fatalf("Connection to gRPC server failed with error %s", err) + } + defer conn.Close() + c := NewClientClient(conn) + + for _, receiveReq := range receiveReqs { + resp, err:= c.Receive(context.Background(), &receiveReq) + if err != nil { + t.Fatalf("gRPC receive failed with %s", err) + } + response := ReceiveResponse{Payload:resp.Payload} + if !reflect.DeepEqual(response, expected) { + t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) + } + } +} + func TestReceivedRaw(t *testing.T) { tm := TransactionManager{Enclave: &MockEnclave{}} @@ -456,9 +486,9 @@ func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { } } -func InitgRPCServer(t *testing.T, grpc bool) (string) { +func InitgRPCServer(t *testing.T, grpc bool, port int) (string) { ipcPath, err := ioutil.TempDir("", "TestInitIpc") - tm, err := Init(&MockEnclave{}, 9005, ipcPath, grpc) + tm, err := Init(&MockEnclave{}, port, ipcPath, grpc) if err != nil { t.Errorf("Error starting server: %v\n", err) From 2987c7d0dff692ac9d23cdf4f99aca8831e939b7 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Mon, 11 Jun 2018 17:11:29 +0100 Subject: [PATCH 34/85] Add the functionality to connect to othernodes using gRPC. Signed-off-by: Puneetha --- api/client.go | 3 +++ api/internal.go | 17 +++++++++++------ enclave/enclave.go | 6 +++++- server/grpc.proto | 7 +++++++ server/messages.proto | 3 +++ server/server.go | 4 +++- server/server_handler.go | 12 ++++++++++++ server/server_test.go | 7 +++++-- 8 files changed, 49 insertions(+), 10 deletions(-) diff --git a/api/client.go b/api/client.go index ec61744..26857bb 100644 --- a/api/client.go +++ b/api/client.go @@ -45,6 +45,9 @@ type ResendRequest struct { Key string `json:"key,omitempty"` } +type UpdatePartyInfo struct { + Payload []byte `json:"payload"` +} type PrivateKeyBytes struct { Bytes string `json:"bytes"` diff --git a/api/internal.go b/api/internal.go index db83eac..0b85de8 100644 --- a/api/internal.go +++ b/api/internal.go @@ -10,6 +10,7 @@ import ( "github.com/kevinburke/nacl" "github.com/blk-io/crux/utils" "encoding/hex" + "encoding/json" "net/http/httputil" "fmt" ) @@ -105,9 +106,13 @@ func (s *PartyInfo) GetPartyInfo() { "Invalid endpoint provided") } + encoded, err := json.Marshal(UpdatePartyInfo{encodedPartyInfo}) + if err != nil { + log.Errorf("Marshalling failed %v", err) + continue + } var req *http.Request - req, err = http.NewRequest("POST", endPoint, bytes.NewBuffer(encodedPartyInfo[:])) - + req, err = http.NewRequest("POST", endPoint, bytes.NewBuffer(encoded)) if err != nil { log.WithField("url", rawUrl).Errorf( "Error creating /partyinfo request, %v", err) @@ -122,22 +127,22 @@ func (s *PartyInfo) GetPartyInfo() { "Error sending /partyinfo request, %v", err) continue } - if resp.StatusCode != http.StatusOK { log.WithField("url", rawUrl).Errorf( "Error sending /partyinfo request, non-200 status code: %v", resp) continue } - var encoded []byte - encoded, err = ioutil.ReadAll(resp.Body) + var partyInfoReq UpdatePartyInfo + err = json.NewDecoder(resp.Body).Decode(&partyInfoReq) + resp.Body.Close() if err != nil { log.WithField("url", rawUrl).Errorf( "Unable to read partyInfo response from host, %v", err) break } - s.UpdatePartyInfo(encoded) + s.UpdatePartyInfo(partyInfoReq.Payload) } } diff --git a/enclave/enclave.go b/enclave/enclave.go index 34fc359..50683f5 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -420,7 +420,11 @@ func (s *SecureEnclave) UpdatePartyInfo(encoded []byte) { // GetEncodedPartyInfo provides this SecureEnclaves PartyInfo details in a binary encoded format. func (s *SecureEnclave) GetEncodedPartyInfo() []byte { - return api.EncodePartyInfo(s.PartyInfo) + encoded, err := json.Marshal(api.UpdatePartyInfo{Payload:api.EncodePartyInfo(s.PartyInfo)}) + if err != nil{ + log.Errorf("Marshalling failed %v", err) + } + return encoded } func loadPubKeys(pubKeyFiles []string) ([]nacl.Key, error) { diff --git a/server/grpc.proto b/server/grpc.proto index a11d7fb..9443764 100644 --- a/server/grpc.proto +++ b/server/grpc.proto @@ -25,4 +25,11 @@ service Client { rpc Receive(ReceiveRequest) returns (ReceiveResponse){ } + + rpc UpdatePartyInfo(PartyInfo) returns (PartyInfo) { + option (google.api.http) = { + post: "/partyinfo" + body: "*" + }; + } } \ No newline at end of file diff --git a/server/messages.proto b/server/messages.proto index a282309..45b7b69 100644 --- a/server/messages.proto +++ b/server/messages.proto @@ -23,4 +23,7 @@ message ReceiveRequest{ } message ReceiveResponse{ bytes payload = 1; +} +message PartyInfo{ + bytes payload = 1; } \ No newline at end of file diff --git a/server/server.go b/server/server.go index a804bba..9b5ad66 100644 --- a/server/server.go +++ b/server/server.go @@ -364,7 +364,9 @@ func (s *TransactionManager) resend(w http.ResponseWriter, req *http.Request) { } func (s *TransactionManager) partyInfo(w http.ResponseWriter, req *http.Request) { - payload, err := ioutil.ReadAll(req.Body) + var partyInfoReq api.UpdatePartyInfo + err := json.NewDecoder(req.Body).Decode(&partyInfoReq) + payload := partyInfoReq.Payload req.Body.Close() if err != nil { internalServerError(w, fmt.Sprintf("Unable to read request body, error: %s\n", err)) diff --git a/server/server_handler.go b/server/server_handler.go index 1f7c554..46fdfdc 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "encoding/json" ) type Server struct { @@ -81,6 +82,17 @@ func (s *Server) processReceive(b64Key []byte, b64To string) ([]byte, error) { } } +func (s *Server) UpdatePartyInfo(ctx context.Context, in *PartyInfo) (*PartyInfo, error) { + s.Enclave.UpdatePartyInfo(in.Payload) + encoded := s.Enclave.GetEncodedPartyInfo() + var decodedPartyInfo PartyInfo + err := json.Unmarshal(encoded, &decodedPartyInfo) + if err != nil{ + log.Errorf("Unmarshalling failed with %v", err) + } + return &PartyInfo{Payload: decodedPartyInfo.Payload}, nil +} + func decodeErrorGRPC(name string, value string, err error) { log.Error(fmt.Sprintf("Invalid request: unable to decode %s: %s, error: %s\n", name, value, err)) diff --git a/server/server_test.go b/server/server_test.go index 3eced41..26435c6 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -462,8 +462,11 @@ func TestPartyInfo(t *testing.T) { } func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { - encoded := api.EncodePartyInfo(pi) - + encodedPartyInfo := api.EncodePartyInfo(pi) + encoded, err := json.Marshal(api.UpdatePartyInfo{Payload:encodedPartyInfo}) + if err != nil { + t.Errorf("Marshalling failed %v", err) + } req, err := http.NewRequest("POST", push, bytes.NewBuffer(encoded)) if err != nil { t.Fatal(err) From 24d4e5c7e9c371f0372a790101e8154fd5a9cba2 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Tue, 12 Jun 2018 09:48:55 +0100 Subject: [PATCH 35/85] Format grpc.proto Signed-off-by: Puneetha --- enclave/enclave.go | 2 +- server/grpc.proto | 24 +++++++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/enclave/enclave.go b/enclave/enclave.go index 50683f5..8103231 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -420,7 +420,7 @@ func (s *SecureEnclave) UpdatePartyInfo(encoded []byte) { // GetEncodedPartyInfo provides this SecureEnclaves PartyInfo details in a binary encoded format. func (s *SecureEnclave) GetEncodedPartyInfo() []byte { - encoded, err := json.Marshal(api.UpdatePartyInfo{Payload:api.EncodePartyInfo(s.PartyInfo)}) + encoded, err := json.Marshal(api.UpdatePartyInfo{Payload: api.EncodePartyInfo(s.PartyInfo)}) if err != nil{ log.Errorf("Marshalling failed %v", err) } diff --git a/server/grpc.proto b/server/grpc.proto index 9443764..6ebf8b9 100644 --- a/server/grpc.proto +++ b/server/grpc.proto @@ -8,28 +8,26 @@ import "messages.proto"; service Client { rpc Version(ApiVersion) returns (ApiVersion) { option (google.api.http) = { - post: "/version" - body: "*" - }; + post: "/version" + body: "*" + }; } rpc Upcheck(UpCheckResponse) returns (UpCheckResponse) { option (google.api.http) = { - post: "/upcheck" - body: "*" - }; + post: "/upcheck" + body: "*" + }; } - rpc Send(SendRequest) returns (SendResponse) { - } + rpc Send(SendRequest) returns (SendResponse); - rpc Receive(ReceiveRequest) returns (ReceiveResponse){ - } + rpc Receive(ReceiveRequest) returns (ReceiveResponse); rpc UpdatePartyInfo(PartyInfo) returns (PartyInfo) { option (google.api.http) = { - post: "/partyinfo" - body: "*" - }; + post: "/partyinfo" + body: "*" + }; } } \ No newline at end of file From eb1bfc166a394b8449f564b9ae233aa9a215a30b Mon Sep 17 00:00:00 2001 From: Puneetha Date: Wed, 13 Jun 2018 16:35:18 +0100 Subject: [PATCH 36/85] Ensure backward compatibility Signed-off-by: Puneetha --- api/internal.go | 88 +++++++++++++++++++++++++++++++++++++++- crux.go | 2 +- enclave/enclave.go | 14 +++++-- server/server.go | 16 +++++++- server/server_handler.go | 2 +- server/server_test.go | 2 +- 6 files changed, 114 insertions(+), 10 deletions(-) diff --git a/api/internal.go b/api/internal.go index 0b85de8..2a5dd06 100644 --- a/api/internal.go +++ b/api/internal.go @@ -94,6 +94,65 @@ func (s *PartyInfo) GetPartyInfo() { urls[k] = v } + for rawUrl := range urls { + if rawUrl == s.url { + continue + } + + endPoint, err := utils.BuildUrl(rawUrl, "/partyinfo") + + if err != nil { + log.WithFields(log.Fields{"rawUrl": rawUrl, "endPoint": "/partyinfo"}).Errorf( + "Invalid endpoint provided") + } + + var req *http.Request + req, err = http.NewRequest("POST", endPoint, bytes.NewBuffer(encodedPartyInfo[:])) + + if err != nil { + log.WithField("url", rawUrl).Errorf( + "Error creating /partyinfo request, %v", err) + break + } + req.Header.Set("Content-Type", "application/octet-stream") + + logRequest(req) + resp, err := s.client.Do(req) + if err != nil { + log.WithField("url", rawUrl).Errorf( + "Error sending /partyinfo request, %v", err) + continue + } + + if resp.StatusCode != http.StatusOK { + log.WithField("url", rawUrl).Errorf( + "Error sending /partyinfo request, non-200 status code: %v", resp) + continue + } + + var encoded []byte + encoded, err = ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + log.WithField("url", rawUrl).Errorf( + "Unable to read partyInfo response from host, %v", err) + break + } + s.UpdatePartyInfo(encoded) + } +} + +// GetPartyInfoGrpc requests PartyInfo data from all remote nodes this node is aware of. The data +// provided in each response is applied to this node. +func (s *PartyInfo) GetPartyInfoGrpc() { + encodedPartyInfo := EncodePartyInfo(*s) + + // First copy our endpoints as we update this map in place + urls := make(map[string]bool) + for k, v := range s.parties { + urls[k] = v + } + for rawUrl := range urls { if rawUrl == s.url { continue @@ -146,7 +205,15 @@ func (s *PartyInfo) GetPartyInfo() { } } -func (s *PartyInfo) PollPartyInfo() { +func (s *PartyInfo) PollPartyInfo(grpc bool) { + if grpc{ + s.PollPartyInfoGrpc() + } else { + s.PollPartyInfoHttp() + } +} + +func (s *PartyInfo) PollPartyInfoHttp() { time.Sleep(time.Duration(rand.Intn(16)) * time.Second) s.GetPartyInfo() @@ -165,6 +232,25 @@ func (s *PartyInfo) PollPartyInfo() { }() } +func (s *PartyInfo) PollPartyInfoGrpc() { + time.Sleep(time.Duration(rand.Intn(16)) * time.Second) + s.GetPartyInfoGrpc() + + ticker := time.NewTicker(2 * time.Minute) + quit := make(chan struct{}) + go func() { + for { + select { + case <- ticker.C: + s.GetPartyInfoGrpc() + case <- quit: + ticker.Stop() + return + } + } + }() +} + // UpdatePartyInfo updates the PartyInfo datastore with the provided encoded data. // This can happen from the /partyinfo server endpoint being hit, or by a response from us hitting // another nodes /partyinfo endpoint. diff --git a/crux.go b/crux.go index 47dac3d..eef0e28 100644 --- a/crux.go +++ b/crux.go @@ -124,7 +124,7 @@ func main() { log.Fatalf("Error starting server: %v\n", err) } - pi.PollPartyInfo() + pi.PollPartyInfo(grpc) select {} } diff --git a/enclave/enclave.go b/enclave/enclave.go index 8103231..e9b23ee 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -419,10 +419,16 @@ func (s *SecureEnclave) UpdatePartyInfo(encoded []byte) { } // GetEncodedPartyInfo provides this SecureEnclaves PartyInfo details in a binary encoded format. -func (s *SecureEnclave) GetEncodedPartyInfo() []byte { - encoded, err := json.Marshal(api.UpdatePartyInfo{Payload: api.EncodePartyInfo(s.PartyInfo)}) - if err != nil{ - log.Errorf("Marshalling failed %v", err) +func (s *SecureEnclave) GetEncodedPartyInfo(grpc bool) []byte { + var encoded []byte + if grpc { + encoded, err := json.Marshal(api.UpdatePartyInfo{Payload: api.EncodePartyInfo(s.PartyInfo)}) + if err != nil{ + log.Errorf("Marshalling failed %v", err) + } + return encoded + } else { + encoded = api.EncodePartyInfo(s.PartyInfo) } return encoded } diff --git a/server/server.go b/server/server.go index 9b5ad66..979b5e0 100644 --- a/server/server.go +++ b/server/server.go @@ -26,7 +26,7 @@ type Enclave interface { RetrieveAllFor(reqRecipient *[]byte) error Delete(digestHash *[]byte) error UpdatePartyInfo(encoded []byte) - GetEncodedPartyInfo() []byte + GetEncodedPartyInfo(grpc bool) []byte } // TransactionManager is responsible for handling all transaction requests. @@ -364,6 +364,18 @@ func (s *TransactionManager) resend(w http.ResponseWriter, req *http.Request) { } func (s *TransactionManager) partyInfo(w http.ResponseWriter, req *http.Request) { + payload, err := ioutil.ReadAll(req.Body) + req.Body.Close() + if err != nil { + internalServerError(w, fmt.Sprintf("Unable to read request body, error: %s\n", err)) + return + } else { + s.Enclave.UpdatePartyInfo(payload) + w.Write(s.Enclave.GetEncodedPartyInfo(false)) + } +} + +func (s *TransactionManager) partyInfoGrpc(w http.ResponseWriter, req *http.Request) { var partyInfoReq api.UpdatePartyInfo err := json.NewDecoder(req.Body).Decode(&partyInfoReq) payload := partyInfoReq.Payload @@ -373,7 +385,7 @@ func (s *TransactionManager) partyInfo(w http.ResponseWriter, req *http.Request) return } else { s.Enclave.UpdatePartyInfo(payload) - w.Write(s.Enclave.GetEncodedPartyInfo()) + w.Write(s.Enclave.GetEncodedPartyInfo(true)) } } diff --git a/server/server_handler.go b/server/server_handler.go index 46fdfdc..815759a 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -84,7 +84,7 @@ func (s *Server) processReceive(b64Key []byte, b64To string) ([]byte, error) { func (s *Server) UpdatePartyInfo(ctx context.Context, in *PartyInfo) (*PartyInfo, error) { s.Enclave.UpdatePartyInfo(in.Payload) - encoded := s.Enclave.GetEncodedPartyInfo() + encoded := s.Enclave.GetEncodedPartyInfo(true) var decodedPartyInfo PartyInfo err := json.Unmarshal(encoded, &decodedPartyInfo) if err != nil{ diff --git a/server/server_test.go b/server/server_test.go index 26435c6..a46c6fa 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -57,7 +57,7 @@ func (s* MockEnclave) Delete(digestHash *[]byte) error { func (s* MockEnclave) UpdatePartyInfo(encoded []byte) {} -func (s* MockEnclave) GetEncodedPartyInfo() []byte { +func (s* MockEnclave) GetEncodedPartyInfo(grpc bool) []byte { return payload } From 30816e705592fe3fd098ded5e209bd587b3c20c8 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Tue, 19 Jun 2018 15:12:11 +0100 Subject: [PATCH 37/85] Add TLS support to HTTP. Signed-off-by: Puneetha --- cert/server.crt | 19 +++++++++++++++++++ cert/server.csr | 17 +++++++++++++++++ cert/server.key | 27 +++++++++++++++++++++++++++ config/config.go | 3 ++- crux.go | 12 +++++++++++- server/server.go | 24 +++++++++++++++++++----- server/server_test.go | 5 +++-- 7 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 cert/server.crt create mode 100644 cert/server.csr create mode 100644 cert/server.key diff --git a/cert/server.crt b/cert/server.crt new file mode 100644 index 0000000..283b8a1 --- /dev/null +++ b/cert/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJDCCAgwCCQDmw4WXCHzRUTANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJV +SzEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xDzANBgNVBAoMBmJs +ay1pbzESMBAGA1UEAwwJMTI3LjAuMC4xMB4XDTE4MDYxNDE0Mzg1NFoXDTI4MDYx +MTE0Mzg1NFowVDELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UE +BwwGTG9uZG9uMQ8wDQYDVQQKDAZibGstaW8xEjAQBgNVBAMMCTEyNy4wLjAuMTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALSS/JHKnE9f9pU7jw3TZint +6aaj7jSEzV5BArjBs0GzDk/R+55AvE+428iFTywiAdeUBO3xxxEzCZ8OOLkffJ95 +Vza0giWcR3/+F9na+1kCOuc2H/fiIvB8DMzGQxjukVLdIaCjl/F5Uyn7gKYKb7Tr +cryf/HkaaT3eA4TahC1dOhhq4q/WmkaWariM9QlFyJDfvYiL1XkmOuiiC6vD+yvd +71dBtugDpIcNgmAZ+rVn4ZoC3Tc16OzeqrvxquF1YO0QakGqiGskZA6NTm+VL/Gc +ubnFweKXkrvisTcqUleoGQM9TibbVQf/A0DamAVrauRs6rxva5JqW7Gh4o5dBUcC +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYHeXraxdqSm5PTYrWqVks+5ctTv/tO4w +cEtleZHy86qnmtu8gqHH+tAKVjn/JNGvYNX/M0oMPjxn0LffGCH6MhC9072RDA10 +XC+yUQx2tm5Q8RRVjb/S+6GczAqXVE6Qc59zgTaFtE8xc/J/wW6XPhJ99Jly2oba +y/6UMvPfCLtf7pA99V4lXat0oRYtVbFkyxIGVl0Gx2COJx65UUtUs+Grf41CQCYr +VXSUTl6wTNj6llAMDkgyFobmfwWzr0DWJJSDycHuC5oxdIe6nhLRq1EgU5DHwyGD +Mz9tLPJHGpJPX8ml4U/XuiYHNDt5hJYZ0veJ6fFM3iSBMCfBIh4Npg== +-----END CERTIFICATE----- diff --git a/cert/server.csr b/cert/server.csr new file mode 100644 index 0000000..85379c9 --- /dev/null +++ b/cert/server.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICsTCCAZkCAQAwVDELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0G +A1UEBwwGTG9uZG9uMQ8wDQYDVQQKDAZibGstaW8xEjAQBgNVBAMMCTEyNy4wLjAu +MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALSS/JHKnE9f9pU7jw3T +Zint6aaj7jSEzV5BArjBs0GzDk/R+55AvE+428iFTywiAdeUBO3xxxEzCZ8OOLkf +fJ95Vza0giWcR3/+F9na+1kCOuc2H/fiIvB8DMzGQxjukVLdIaCjl/F5Uyn7gKYK +b7Trcryf/HkaaT3eA4TahC1dOhhq4q/WmkaWariM9QlFyJDfvYiL1XkmOuiiC6vD ++yvd71dBtugDpIcNgmAZ+rVn4ZoC3Tc16OzeqrvxquF1YO0QakGqiGskZA6NTm+V +L/GcubnFweKXkrvisTcqUleoGQM9TibbVQf/A0DamAVrauRs6rxva5JqW7Gh4o5d +BUcCAwEAAaAYMBYGCSqGSIb3DQEJBzEJDAdibGtjcnV4MA0GCSqGSIb3DQEBCwUA +A4IBAQBYRPFuhVoNhYBVoO4qaNet7W4JAZ2YkpTdE/SdECyBAt4lX4OPV+lFYrUD +AjPf98IjwVwNijz6DboqEx+1rUKzLEV/Jv8k0O8noJDR3fRkFU3cyIDcW/N9Wwcc +p3qHwRuSvv+QmDoikJeCR8iN61jeo6WG2KKB4GUETK35SMDS31ExkWHNa/WIFzrV +MTB/Vy2yH0xgxhDRa8sEbsGrvf/XnhKKGNbffmTp46dBItBkDKeiHh2FGI6F8CBv +bkhB0/7fC8Hi4aoBMSdT88L8Y7+mVMNfybmCuSr1foK+gMSS7UweLn2oPEMohFZW +4mkS8TD1bUkavlCrhVuExCSLVXLu +-----END CERTIFICATE REQUEST----- diff --git a/cert/server.key b/cert/server.key new file mode 100644 index 0000000..9592691 --- /dev/null +++ b/cert/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAtJL8kcqcT1/2lTuPDdNmKe3ppqPuNITNXkECuMGzQbMOT9H7 +nkC8T7jbyIVPLCIB15QE7fHHETMJnw44uR98n3lXNrSCJZxHf/4X2dr7WQI65zYf +9+Ii8HwMzMZDGO6RUt0hoKOX8XlTKfuApgpvtOtyvJ/8eRppPd4DhNqELV06GGri +r9aaRpZquIz1CUXIkN+9iIvVeSY66KILq8P7K93vV0G26AOkhw2CYBn6tWfhmgLd +NzXo7N6qu/Gq4XVg7RBqQaqIayRkDo1Ob5Uv8Zy5ucXB4peSu+KxNypSV6gZAz1O +JttVB/8DQNqYBWtq5GzqvG9rkmpbsaHijl0FRwIDAQABAoIBAFx/iteXxRg33RU2 +zCHV71h9IZoWEThf6t9kR+OifZOjCCrFMrIvEQd5d0QxXYwK44ytqxTELCfYUA5k +95OE2I7MVmuUbbKcalfbLhaPwP8oUEoOBLQy55juwpPG36oO0uxyj/48ruGoN3yi +85SadfxkO4L6JRdX+x8Q8haE5P5jDUZt/lol1neQR8moEpKwy+KhAEPBe3c+MlXl +wHWYk/j8GHnfgcDCLITpu7dHGUDye7l0dbM4EjbU9R3vcsTfT18eekUqm6MER0Gd +wA4bcrAhlNQjNyLocmSMgzD3NjeXzbbA4NXO32X6OmH+I+sGPEJHcZU0Kew0Depe +yhkmXKkCgYEA5pRIhouqF3RzbGf+yMMdo9bQc7U4iUhTcZ9Vm/z/Nxs0d1pCHy5L +n3VAgOSsIb8AP/DmhpRGyBAGodKnkpXrvMG03y7Pf2KjOxh/A2thTDeB4dcelEvu +Q9Uj1s/BctyVzelrL0DZjBhwxBuT56lqq56E8Yb/jn7yZZ8g7A7vetMCgYEAyHtk +qKfh3d4oF5/mro9KmWsmjB/jfSwXJzu9UsdmLFlRhsRZBXLYxwLxtAFzcM+y+ac3 +18dowvBD2p3tvEwKVF0ncxn0vDNDF8n+sYlVsBrLuFccfw1bgl3jmmAhuXX6NZO8 +HcO/S6PjrnSKYd4WXRjHqiv5ufMZFQ1OKjz1mz0CgYEAmOi7E+ao3LcQGFL65p5m +GJHLWQBTxs6c75uvhSuJAD1dVM0ZTl5ALjXumcuLzzE/9CdIaPUJ34CpNUVidVZQ +p7N5xAvh9OMvxm/fQyBBvO6OhntHPyb/kiJViw3phsd73Lqvpv2Fh19p4NM9CYMT +R05vcVCKRzAuhW+6wHDDJZsCgYEAxptXEK2f2Efot960jGFvqaS4v0AoASzYkwlf +eM4Irg6d8UA6YGdx0VVdVNHLJwrbZu79J0powhV7YuvpRAygfwr5tdEU3gx6fuqg +4ggHVzp0bt39YPA+o1uXyqtJPY1eng0I4wO0Up69Q2o4XNPCm9+cjTybXFczleNk +d/uD5JECgYBcwAPEFNU2+NcU+mYWFHIvNK4hVLD/Bfr6EmkJcOLYE+BZPdaWaPGv +ZE9ZgLpf6bB9cmQGUifWdSHDXwQH7kpYKS5pJdGsPbuFys5HU4xCbCEErsRnPKlE +K/hcgeDuKNJ6/nV1ypUsOXpO6ukhLnTvj1R9MQVipprTaQbxRBxx8g== +-----END RSA PRIVATE KEY----- diff --git a/config/config.go b/config/config.go index 58611f8..9371481 100644 --- a/config/config.go +++ b/config/config.go @@ -56,9 +56,10 @@ func InitFlags() { flag.Int(Verbosity, 1, "Verbosity level of logs") flag.String(AlwaysSendTo, "", "List of public keys for nodes to send all transactions too") flag.Bool(UseGRPC, false, "Use gRPC server") + flag.String(TlsServerCert, "", "The server certificate to be used") + flag.String(TlsServerKey, "", "The server private key") // storage not currently supported as we use LevelDB - // TLS is not currently supported pflag.CommandLine.AddGoFlagSet(flag.CommandLine) viper.BindPFlags(pflag.CommandLine) // Binding the flags to test the initial configuration diff --git a/crux.go b/crux.go index eef0e28..cefa17f 100644 --- a/crux.go +++ b/crux.go @@ -119,7 +119,17 @@ func main() { grpc := config.GetBool(config.UseGRPC) - _, err = server.Init(enc, port, ipcPath, grpc) + servCert := config.GetString(config.TlsServerCert) + servKey := config.GetString(config.TlsServerKey) + + if (len(servCert) != len(servKey)) || (len(servCert) <= 0) { + log.Fatalf("Please provide server certificate and key for TLS %s %s %d ", servKey, servCert, len(servCert)) + } + + tlsCertFile := path.Join(workDir, servCert) + tlsKeyFile := path.Join(workDir, servKey) + + _, err = server.Init(enc, port, ipcPath, grpc, tlsCertFile, tlsKeyFile) if err != nil { log.Fatalf("Error starting server: %v\n", err) } diff --git a/server/server.go b/server/server.go index 979b5e0..fb90b8f 100644 --- a/server/server.go +++ b/server/server.go @@ -14,6 +14,7 @@ import ( "encoding/hex" "net/textproto" "net/http/httputil" + "os" ) // Enclave is the interface used by the transaction enclaves. @@ -69,7 +70,7 @@ func requestLogger(handler http.Handler) http.Handler { } // Init initializes a new TransactionManager instance. -func Init(enc Enclave, port int, ipcPath string, grpc bool) (TransactionManager, error) { +func Init(enc Enclave, port int, ipcPath string, grpc bool, certFile, keyFile string) (TransactionManager, error) { tm := TransactionManager{Enclave : enc} var err error @@ -77,13 +78,13 @@ func Init(enc Enclave, port int, ipcPath string, grpc bool) (TransactionManager, err = tm.startRpcServer(port, ipcPath) } else { - err = tm.startHttpserver(port, ipcPath) + err = tm.startHttpserver(port, ipcPath, certFile, keyFile) } return tm, err } -func (tm *TransactionManager) startHttpserver(port int, ipcPath string) error { +func (tm *TransactionManager) startHttpserver(port int, ipcPath string, certFile, keyFile string) error { httpServer := http.NewServeMux() httpServer.HandleFunc(upCheck, tm.upcheck) httpServer.HandleFunc(version, tm.version) @@ -92,10 +93,14 @@ func (tm *TransactionManager) startHttpserver(port int, ipcPath string) error { httpServer.HandleFunc(partyInfo, tm.partyInfo) serverUrl := "localhost:" + strconv.Itoa(port) + err := CheckCertFiles(certFile, keyFile) + if err != nil { + log.Fatal(err) + } go func() { - log.Fatal(http.ListenAndServe(serverUrl, requestLogger(httpServer))) + log.Fatal(http.ListenAndServeTLS(serverUrl, certFile, keyFile, requestLogger(httpServer))) }() - log.Infof("HTTP server is running at: %s", serverUrl) + log.Infof("HTTPS server is running at: %s", serverUrl) // Restricted to IPC ipcServer := http.NewServeMux() @@ -119,6 +124,15 @@ func (tm *TransactionManager) startHttpserver(port int, ipcPath string) error { return err } +func CheckCertFiles(certFile, keyFile string) error { + if _, err := os.Stat(certFile); os.IsNotExist(err) { + return err + } else if _, err := os.Stat(keyFile); os.IsNotExist(err) { + return err + } + return nil +} + func (s *TransactionManager) upcheck(w http.ResponseWriter, req *http.Request) { fmt.Fprint(w, upCheckResponse) } diff --git a/server/server_test.go b/server/server_test.go index a46c6fa..2004d60 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -491,7 +491,7 @@ func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { func InitgRPCServer(t *testing.T, grpc bool, port int) (string) { ipcPath, err := ioutil.TempDir("", "TestInitIpc") - tm, err := Init(&MockEnclave{}, port, ipcPath, grpc) + tm, err := Init(&MockEnclave{}, port, ipcPath, grpc, "", "") if err != nil { t.Errorf("Error starting server: %v\n", err) @@ -534,7 +534,8 @@ func TestInit(t *testing.T) { if err != nil { t.Error(err) } - tm, err := Init(enc, 9001, ipcPath, false) + certFile, keyFile := "../cert/server.crt", "../cert/server.key" + tm, err := Init(enc, 9001, ipcPath, false, certFile, keyFile) if err != nil { t.Errorf("Error starting server: %v\n", err) } From 8ce1503a5b193f105db8eb2c2cfe1c2888c3a3f6 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Thu, 21 Jun 2018 15:26:14 +0100 Subject: [PATCH 38/85] Added a complete PartyInfo protobuf. Signed-off-by: Puneetha --- api/internal.go | 158 +++++++++++++++------------------------ enclave/enclave.go | 24 +++--- server/grpc.proto | 4 +- server/messages.proto | 7 +- server/server.go | 22 ++---- server/server_handler.go | 25 +++++-- server/server_test.go | 12 ++- 7 files changed, 120 insertions(+), 132 deletions(-) diff --git a/api/internal.go b/api/internal.go index 2a5dd06..293e402 100644 --- a/api/internal.go +++ b/api/internal.go @@ -39,6 +39,10 @@ func (s *PartyInfo) GetRecipient(key nacl.Key) (string, bool) { return value, ok } +func (s *PartyInfo) GetAllValues() (string, map[[nacl.KeySize]byte]string, map[string]bool){ + return s.url, s.recipients, s.parties +} + // InitPartyInfo initializes a new PartyInfo store. func InitPartyInfo(rawUrl string, otherNodes []string, client utils.HttpClient) PartyInfo { parties := make(map[string]bool) @@ -85,7 +89,7 @@ func (s *PartyInfo) RegisterPublicKeys(pubKeys []nacl.Key) { // GetPartyInfo requests PartyInfo data from all remote nodes this node is aware of. The data // provided in each response is applied to this node. -func (s *PartyInfo) GetPartyInfo() { +func (s *PartyInfo) GetPartyInfo(grpc bool) { encodedPartyInfo := EncodePartyInfo(*s) // First copy our endpoints as we update this map in place @@ -107,7 +111,8 @@ func (s *PartyInfo) GetPartyInfo() { } var req *http.Request - req, err = http.NewRequest("POST", endPoint, bytes.NewBuffer(encodedPartyInfo[:])) + encoded := getEncoded(grpc, encodedPartyInfo) + req, err = http.NewRequest("POST", endPoint, bytes.NewBuffer(encoded)) if err != nil { log.WithField("url", rawUrl).Errorf( @@ -130,92 +135,51 @@ func (s *PartyInfo) GetPartyInfo() { continue } - var encoded []byte - encoded, err = ioutil.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - log.WithField("url", rawUrl).Errorf( - "Unable to read partyInfo response from host, %v", err) - break + if grpc { + var partyInfoReq UpdatePartyInfo + err = json.NewDecoder(resp.Body).Decode(&partyInfoReq) + resp.Body.Close() + if err != nil { + log.WithField("url", rawUrl).Errorf( + "Unable to read partyInfo response from host, %v", err) + break + } + pi, err := DecodePartyInfo(partyInfoReq.Payload) + if err != nil { + log.WithField("url", rawUrl).Errorf( + "Unable to decode partyInfo response from host, %v", err) + break + } + s.UpdatePartyInfoGrpc(pi.url, pi.recipients, pi.parties) + } else { + var encoded []byte + encoded, err = ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + log.WithField("url", rawUrl).Errorf( + "Unable to read partyInfo response from host, %v", err) + break + } + s.UpdatePartyInfo(encoded) } - s.UpdatePartyInfo(encoded) } } -// GetPartyInfoGrpc requests PartyInfo data from all remote nodes this node is aware of. The data -// provided in each response is applied to this node. -func (s *PartyInfo) GetPartyInfoGrpc() { - encodedPartyInfo := EncodePartyInfo(*s) - - // First copy our endpoints as we update this map in place - urls := make(map[string]bool) - for k, v := range s.parties { - urls[k] = v - } - - for rawUrl := range urls { - if rawUrl == s.url { - continue - } - - endPoint, err := utils.BuildUrl(rawUrl, "/partyinfo") - - if err != nil { - log.WithFields(log.Fields{"rawUrl": rawUrl, "endPoint": "/partyinfo"}).Errorf( - "Invalid endpoint provided") - } - - encoded, err := json.Marshal(UpdatePartyInfo{encodedPartyInfo}) - if err != nil { +func getEncoded(grpc bool, encodedPartyInfo []byte) []byte{ + if grpc{ + e, err := json.Marshal(UpdatePartyInfo{encodedPartyInfo}) + if err != nil{ log.Errorf("Marshalling failed %v", err) - continue - } - var req *http.Request - req, err = http.NewRequest("POST", endPoint, bytes.NewBuffer(encoded)) - if err != nil { - log.WithField("url", rawUrl).Errorf( - "Error creating /partyinfo request, %v", err) - break + return nil } - req.Header.Set("Content-Type", "application/octet-stream") - - logRequest(req) - resp, err := s.client.Do(req) - if err != nil { - log.WithField("url", rawUrl).Errorf( - "Error sending /partyinfo request, %v", err) - continue - } - if resp.StatusCode != http.StatusOK { - log.WithField("url", rawUrl).Errorf( - "Error sending /partyinfo request, non-200 status code: %v", resp) - continue - } - - var partyInfoReq UpdatePartyInfo - err = json.NewDecoder(resp.Body).Decode(&partyInfoReq) - - resp.Body.Close() - if err != nil { - log.WithField("url", rawUrl).Errorf( - "Unable to read partyInfo response from host, %v", err) - break - } - s.UpdatePartyInfo(partyInfoReq.Payload) + return e } + return encodedPartyInfo[:] } func (s *PartyInfo) PollPartyInfo(grpc bool) { - if grpc{ - s.PollPartyInfoGrpc() - } else { - s.PollPartyInfoHttp() - } -} - -func (s *PartyInfo) PollPartyInfoHttp() { time.Sleep(time.Duration(rand.Intn(16)) * time.Second) - s.GetPartyInfo() + s.GetPartyInfo(grpc) ticker := time.NewTicker(2 * time.Minute) quit := make(chan struct{}) @@ -223,26 +187,7 @@ func (s *PartyInfo) PollPartyInfoHttp() { for { select { case <- ticker.C: - s.GetPartyInfo() - case <- quit: - ticker.Stop() - return - } - } - }() -} - -func (s *PartyInfo) PollPartyInfoGrpc() { - time.Sleep(time.Duration(rand.Intn(16)) * time.Second) - s.GetPartyInfoGrpc() - - ticker := time.NewTicker(2 * time.Minute) - quit := make(chan struct{}) - go func() { - for { - select { - case <- ticker.C: - s.GetPartyInfoGrpc() + s.GetPartyInfo(grpc) case <- quit: ticker.Stop() return @@ -280,6 +225,23 @@ func (s *PartyInfo) UpdatePartyInfo(encoded []byte) { } } +func (s *PartyInfo) UpdatePartyInfoGrpc(url string, recipients map[[nacl.KeySize]byte]string, parties map[string]bool) { + for publicKey, url := range recipients { + // we should ignore messages about ourselves + // in order to stop people masquerading as you, there + // should be a digital signature associated with each + // url -> node broadcast + if url != s.url { + s.recipients[publicKey] = url + } + } + + for url := range parties { + // we don't want to broadcast party info to ourselves + s.parties[url] = true + } +} + // Push is responsible for propagating the encoded payload to the given remote node. func Push(encoded []byte, url string, client utils.HttpClient) (string, error) { @@ -324,4 +286,4 @@ func logRequest(r *http.Request) { log.Debugf("%q", dump) } -} \ No newline at end of file +} diff --git a/enclave/enclave.go b/enclave/enclave.go index e9b23ee..724b3ec 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -418,21 +418,27 @@ func (s *SecureEnclave) UpdatePartyInfo(encoded []byte) { s.PartyInfo.UpdatePartyInfo(encoded) } +func (s *SecureEnclave) UpdatePartyInfoGrpc(url string, recipients map[[nacl.KeySize]byte]string, parties map[string]bool) { + s.PartyInfo.UpdatePartyInfoGrpc(url, recipients, parties) +} + // GetEncodedPartyInfo provides this SecureEnclaves PartyInfo details in a binary encoded format. -func (s *SecureEnclave) GetEncodedPartyInfo(grpc bool) []byte { - var encoded []byte - if grpc { - encoded, err := json.Marshal(api.UpdatePartyInfo{Payload: api.EncodePartyInfo(s.PartyInfo)}) - if err != nil{ +func (s *SecureEnclave) GetEncodedPartyInfo() []byte { + return api.EncodePartyInfo(s.PartyInfo) +} + +func (s *SecureEnclave) GetEncodedPartyInfoGrpc() []byte { + encoded, err := json.Marshal(api.UpdatePartyInfo{Payload: api.EncodePartyInfo(s.PartyInfo)}) + if err != nil{ log.Errorf("Marshalling failed %v", err) - } - return encoded - } else { - encoded = api.EncodePartyInfo(s.PartyInfo) } return encoded } +func (s *SecureEnclave) GetPartyInfo() (string, map[[nacl.KeySize]byte]string, map[string]bool){ + return s.PartyInfo.GetAllValues() +} + func loadPubKeys(pubKeyFiles []string) ([]nacl.Key, error) { return loadKeys( pubKeyFiles, diff --git a/server/grpc.proto b/server/grpc.proto index 6ebf8b9..5dc58c2 100644 --- a/server/grpc.proto +++ b/server/grpc.proto @@ -24,10 +24,10 @@ service Client { rpc Receive(ReceiveRequest) returns (ReceiveResponse); - rpc UpdatePartyInfo(PartyInfo) returns (PartyInfo) { + rpc UpdatePartyInfo(PartyInfo) returns (PartyInfoResponse) { option (google.api.http) = { post: "/partyinfo" body: "*" }; } -} \ No newline at end of file +} diff --git a/server/messages.proto b/server/messages.proto index 45b7b69..0080e64 100644 --- a/server/messages.proto +++ b/server/messages.proto @@ -25,5 +25,10 @@ message ReceiveResponse{ bytes payload = 1; } message PartyInfo{ + string url = 1; + map receipients = 2; + map parties = 3; +} +message PartyInfoResponse{ bytes payload = 1; -} \ No newline at end of file +} diff --git a/server/server.go b/server/server.go index 979b5e0..d47d98d 100644 --- a/server/server.go +++ b/server/server.go @@ -14,6 +14,7 @@ import ( "encoding/hex" "net/textproto" "net/http/httputil" + "github.com/kevinburke/nacl" ) // Enclave is the interface used by the transaction enclaves. @@ -26,7 +27,10 @@ type Enclave interface { RetrieveAllFor(reqRecipient *[]byte) error Delete(digestHash *[]byte) error UpdatePartyInfo(encoded []byte) - GetEncodedPartyInfo(grpc bool) []byte + UpdatePartyInfoGrpc(url string, recipients map[[nacl.KeySize]byte]string, parties map[string]bool) + GetEncodedPartyInfo() []byte + GetEncodedPartyInfoGrpc() []byte + GetPartyInfo() (url string, recipients map[[nacl.KeySize]byte]string, parties map[string]bool) } // TransactionManager is responsible for handling all transaction requests. @@ -371,21 +375,7 @@ func (s *TransactionManager) partyInfo(w http.ResponseWriter, req *http.Request) return } else { s.Enclave.UpdatePartyInfo(payload) - w.Write(s.Enclave.GetEncodedPartyInfo(false)) - } -} - -func (s *TransactionManager) partyInfoGrpc(w http.ResponseWriter, req *http.Request) { - var partyInfoReq api.UpdatePartyInfo - err := json.NewDecoder(req.Body).Decode(&partyInfoReq) - payload := partyInfoReq.Payload - req.Body.Close() - if err != nil { - internalServerError(w, fmt.Sprintf("Unable to read request body, error: %s\n", err)) - return - } else { - s.Enclave.UpdatePartyInfo(payload) - w.Write(s.Enclave.GetEncodedPartyInfo(true)) + w.Write(s.Enclave.GetEncodedPartyInfo()) } } diff --git a/server/server_handler.go b/server/server_handler.go index 815759a..3eea72c 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "github.com/kevinburke/nacl" "encoding/json" ) @@ -82,15 +83,29 @@ func (s *Server) processReceive(b64Key []byte, b64To string) ([]byte, error) { } } -func (s *Server) UpdatePartyInfo(ctx context.Context, in *PartyInfo) (*PartyInfo, error) { - s.Enclave.UpdatePartyInfo(in.Payload) - encoded := s.Enclave.GetEncodedPartyInfo(true) - var decodedPartyInfo PartyInfo +func (s *Server) UpdatePartyInfo(ctx context.Context, in *PartyInfo) (*PartyInfoResponse, error) { + recipients := make(map[[nacl.KeySize]byte]string) + for url, key := range in.Receipients{ + var as [32]byte + // couldn't find a better way to reduce a slice to array of fixed length + for idx, i := range key{ + if idx < 32{ + as[idx] = i + } else { + break + } + } + recipients[as] = url + } + + s.Enclave.UpdatePartyInfoGrpc(in.Url, recipients, in.Parties) + encoded := s.Enclave.GetEncodedPartyInfoGrpc() + var decodedPartyInfo PartyInfoResponse err := json.Unmarshal(encoded, &decodedPartyInfo) if err != nil{ log.Errorf("Unmarshalling failed with %v", err) } - return &PartyInfo{Payload: decodedPartyInfo.Payload}, nil + return &PartyInfoResponse{Payload: decodedPartyInfo.Payload}, nil } func decodeErrorGRPC(name string, value string, err error) { diff --git a/server/server_test.go b/server/server_test.go index a46c6fa..edba0b6 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -57,10 +57,20 @@ func (s* MockEnclave) Delete(digestHash *[]byte) error { func (s* MockEnclave) UpdatePartyInfo(encoded []byte) {} -func (s* MockEnclave) GetEncodedPartyInfo(grpc bool) []byte { +func (s* MockEnclave) UpdatePartyInfoGrpc(string, map[[nacl.KeySize]byte]string, map[string]bool) {} + +func (s* MockEnclave) GetEncodedPartyInfo() []byte { + return payload +} + +func (s* MockEnclave) GetEncodedPartyInfoGrpc() []byte{ return payload } +func (s *MockEnclave) GetPartyInfo() (string, map[[nacl.KeySize]byte]string, map[string]bool){ + return "", nil, nil +} + func TestUpcheck(t *testing.T) { tm := TransactionManager{} runSimpleGetRequest(t, upCheck, upCheckResponse, tm.upcheck) From b29021f9008e856fc312b7f68d175e07562bbbf7 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Fri, 22 Jun 2018 12:41:41 +0100 Subject: [PATCH 39/85] Refractor code. Signed-off-by: Puneetha --- api/internal.go | 61 ++++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/api/internal.go b/api/internal.go index 293e402..23de456 100644 --- a/api/internal.go +++ b/api/internal.go @@ -136,33 +136,46 @@ func (s *PartyInfo) GetPartyInfo(grpc bool) { } if grpc { - var partyInfoReq UpdatePartyInfo - err = json.NewDecoder(resp.Body).Decode(&partyInfoReq) - resp.Body.Close() - if err != nil { - log.WithField("url", rawUrl).Errorf( - "Unable to read partyInfo response from host, %v", err) - break - } - pi, err := DecodePartyInfo(partyInfoReq.Payload) - if err != nil { - log.WithField("url", rawUrl).Errorf( - "Unable to decode partyInfo response from host, %v", err) - break - } - s.UpdatePartyInfoGrpc(pi.url, pi.recipients, pi.parties) + err = s.updatePartyInfoGrpc(resp, rawUrl) } else { - var encoded []byte - encoded, err = ioutil.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - log.WithField("url", rawUrl).Errorf( - "Unable to read partyInfo response from host, %v", err) - break - } - s.UpdatePartyInfo(encoded) + err = s.updatePartyInfo(resp, rawUrl) } + if err != nil { + break + } + } +} + +func (s *PartyInfo) updatePartyInfoGrpc(resp *http.Response, rawUrl string) error { + var partyInfoReq UpdatePartyInfo + err := json.NewDecoder(resp.Body).Decode(&partyInfoReq) + resp.Body.Close() + if err != nil { + log.WithField("url", rawUrl).Errorf( + "Unable to read partyInfo response from host, %v", err) + return err + } + pi, err := DecodePartyInfo(partyInfoReq.Payload) + if err != nil { + log.WithField("url", rawUrl).Errorf( + "Unable to decode partyInfo response from host, %v", err) + return err + } + s.UpdatePartyInfoGrpc(pi.url, pi.recipients, pi.parties) + return nil +} + +func (s *PartyInfo) updatePartyInfo(resp *http.Response, rawUrl string) error { + var encoded []byte + encoded, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + log.WithField("url", rawUrl).Errorf( + "Unable to read partyInfo response from host, %v", err) + return err } + s.UpdatePartyInfo(encoded) + return nil } func getEncoded(grpc bool, encodedPartyInfo []byte) []byte{ From 68c607a196ead86b9f3e57c56f949f7da28342e0 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Fri, 22 Jun 2018 13:15:49 +0100 Subject: [PATCH 40/85] Fix indenting in enclave.go --- enclave/enclave.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enclave/enclave.go b/enclave/enclave.go index 724b3ec..816eda8 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -430,7 +430,7 @@ func (s *SecureEnclave) GetEncodedPartyInfo() []byte { func (s *SecureEnclave) GetEncodedPartyInfoGrpc() []byte { encoded, err := json.Marshal(api.UpdatePartyInfo{Payload: api.EncodePartyInfo(s.PartyInfo)}) if err != nil{ - log.Errorf("Marshalling failed %v", err) + log.Errorf("Marshalling failed %v", err) } return encoded } From 20de4718d54e261650c031a9cfaa6e3507216288 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Fri, 22 Jun 2018 16:56:22 +0100 Subject: [PATCH 41/85] Add push API using gRPC. Signed-off-by: Puneetha --- enclave/enclave.go | 4 ++++ server/grpc.proto | 7 ++++++ server/messages.proto | 11 +++++++++ server/server.go | 1 + server/server_handler.go | 49 ++++++++++++++++++++++++++++++++++++++++ server/server_test.go | 5 ++-- 6 files changed, 75 insertions(+), 2 deletions(-) diff --git a/enclave/enclave.go b/enclave/enclave.go index 816eda8..1b3687f 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -276,6 +276,10 @@ func (s *SecureEnclave) StorePayload(encoded []byte) ([]byte, error) { return s.storePayload(epl, encoded) } +func (s *SecureEnclave) StorePayloadGrpc(epl api.EncryptedPayload, encoded []byte) ([]byte, error) { + return s.storePayload(epl, encoded) +} + func (s *SecureEnclave) storePayload(epl api.EncryptedPayload, encoded []byte) ([]byte, error) { digestHash := utils.Sha3Hash(epl.CipherText) err := s.Db.Write(&digestHash, &encoded) diff --git a/server/grpc.proto b/server/grpc.proto index 5dc58c2..274a087 100644 --- a/server/grpc.proto +++ b/server/grpc.proto @@ -30,4 +30,11 @@ service Client { body: "*" }; } + + rpc Push(PushPayload) returns (PartyInfoResponse) { + option (google.api.http) = { + post: "/push" + body: "*" + }; + } } diff --git a/server/messages.proto b/server/messages.proto index 0080e64..8d4fed5 100644 --- a/server/messages.proto +++ b/server/messages.proto @@ -32,3 +32,14 @@ message PartyInfo{ message PartyInfoResponse{ bytes payload = 1; } +message EncryptedPayload{ + bytes sender = 1; + bytes cipherText = 2; + bytes nonce = 3; + repeated bytes reciepientBoxes = 4; + bytes reciepientNonce = 5; +} +message PushPayload{ + EncryptedPayload ep = 1; + bytes encoded = 2; +} diff --git a/server/server.go b/server/server.go index d47d98d..64cd263 100644 --- a/server/server.go +++ b/server/server.go @@ -20,6 +20,7 @@ import ( // Enclave is the interface used by the transaction enclaves. type Enclave interface { Store(message *[]byte, sender []byte, recipients [][]byte) ([]byte, error) + StorePayloadGrpc(epl api.EncryptedPayload, encoded []byte) ([]byte, error) StorePayload(encoded []byte) ([]byte, error) Retrieve(digestHash *[]byte, to *[]byte) ([]byte, error) RetrieveDefault(digestHash *[]byte) ([]byte, error) diff --git a/server/server_handler.go b/server/server_handler.go index 3eea72c..b81b0a8 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/kevinburke/nacl" "encoding/json" + "github.com/blk-io/crux/api" ) type Server struct { @@ -108,6 +109,54 @@ func (s *Server) UpdatePartyInfo(ctx context.Context, in *PartyInfo) (*PartyInfo return &PartyInfoResponse{Payload: decodedPartyInfo.Payload}, nil } + +func (s *Server) Push(ctx context.Context, in *PushPayload) (*PartyInfoResponse, error){ + sender := sliceToarrNaclKey(in.Ep.Sender) + nounce := sliceToarrNaclNonce(in.Ep.Nonce) + recipientNounce := sliceToarrNaclNonce(in.Ep.ReciepientNonce) + encyptedPayload := api.EncryptedPayload{ + Sender:sender, + CipherText:in.Ep.CipherText, + Nonce:nounce, + RecipientBoxes:in.Ep.ReciepientBoxes, + RecipientNonce:recipientNounce, + } + + digestHash, err := s.Enclave.StorePayloadGrpc(encyptedPayload, in.Encoded) + if err != nil { + log.Fatalf("Unable to store payload, error: %s\n", err) + } + + return &PartyInfoResponse{Payload: digestHash}, nil +} + + +func sliceToarrNaclKey(key []byte) nacl.Key { + var as nacl.Key + // couldn't find a better way to reduce a slice to array of fixed length + for idx, i := range key{ + if idx < 32{ + as[idx] = i + } else { + break + } + } + return as +} + +func sliceToarrNaclNonce(key []byte) nacl.Nonce { + var as nacl.Nonce + // couldn't find a better way to reduce a slice to array of fixed length + for idx, i := range key{ + if idx < 24{ + as[idx] = i + } else { + break + } + } + return as +} + func decodeErrorGRPC(name string, value string, err error) { log.Error(fmt.Sprintf("Invalid request: unable to decode %s: %s, error: %s\n", name, value, err)) diff --git a/server/server_test.go b/server/server_test.go index edba0b6..6c6e300 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -34,6 +34,9 @@ func (s* MockEnclave) Store(message *[]byte, sender []byte, recipients [][]byte) func (s* MockEnclave) StorePayload(encoded []byte) ([]byte, error) { return encoded, nil } +func (s* MockEnclave) StorePayloadGrpc(epl api.EncryptedPayload, encoded []byte) ([]byte, error) { + return encoded, nil +} func (s* MockEnclave) Retrieve(digestHash *[]byte, to *[]byte) ([]byte, error) { return *digestHash, nil @@ -261,12 +264,10 @@ func TestPush(t *testing.T) { var recipients [][]byte encoded := api.EncodePayloadWithRecipients(epl, recipients) - req, err := http.NewRequest("POST", push, bytes.NewBuffer(encoded)) if err != nil { t.Fatal(err) } - rr := httptest.NewRecorder() tm := TransactionManager{Enclave: &MockEnclave{}} From b07507c1a21a39da860b8ec94930fa27dc6c881c Mon Sep 17 00:00:00 2001 From: Puneetha Date: Fri, 22 Jun 2018 17:06:41 +0100 Subject: [PATCH 42/85] Add support to use TLS with gRPC. Signed-off-by: Puneetha --- crux.go | 22 ++++---- {cert => enclave/testdata/cert}/server.crt | 0 {cert => enclave/testdata/cert}/server.csr | 0 {cert => enclave/testdata/cert}/server.key | 0 server/proto_server.go | 61 +++++++++++++++++++++- server/server.go | 29 ++++++---- server/server_test.go | 6 +-- 7 files changed, 92 insertions(+), 26 deletions(-) rename {cert => enclave/testdata/cert}/server.crt (100%) rename {cert => enclave/testdata/cert}/server.csr (100%) rename {cert => enclave/testdata/cert}/server.key (100%) diff --git a/crux.go b/crux.go index cefa17f..e9e5c40 100644 --- a/crux.go +++ b/crux.go @@ -118,18 +118,20 @@ func main() { } grpc := config.GetBool(config.UseGRPC) + tls := config.GetBool(config.Tls) + var tlsCertFile, tlsKeyFile string + if tls { + servCert := config.GetString(config.TlsServerCert) + servKey := config.GetString(config.TlsServerKey) + + if (len(servCert) != len(servKey)) || (len(servCert) <= 0) { + log.Fatalf("Please provide server certificate and key for TLS %s %s %d ", servKey, servCert, len(servCert)) + } - servCert := config.GetString(config.TlsServerCert) - servKey := config.GetString(config.TlsServerKey) - - if (len(servCert) != len(servKey)) || (len(servCert) <= 0) { - log.Fatalf("Please provide server certificate and key for TLS %s %s %d ", servKey, servCert, len(servCert)) + tlsCertFile = path.Join(workDir, servCert) + tlsKeyFile = path.Join(workDir, servKey) } - - tlsCertFile := path.Join(workDir, servCert) - tlsKeyFile := path.Join(workDir, servKey) - - _, err = server.Init(enc, port, ipcPath, grpc, tlsCertFile, tlsKeyFile) + _, err = server.Init(enc, port, ipcPath, grpc, tls, tlsCertFile, tlsKeyFile) if err != nil { log.Fatalf("Error starting server: %v\n", err) } diff --git a/cert/server.crt b/enclave/testdata/cert/server.crt similarity index 100% rename from cert/server.crt rename to enclave/testdata/cert/server.crt diff --git a/cert/server.csr b/enclave/testdata/cert/server.csr similarity index 100% rename from cert/server.csr rename to enclave/testdata/cert/server.csr diff --git a/cert/server.key b/enclave/testdata/cert/server.key similarity index 100% rename from cert/server.key rename to enclave/testdata/cert/server.key diff --git a/server/proto_server.go b/server/proto_server.go index f007d45..c771912 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -9,9 +9,10 @@ import ( "net/http" "github.com/blk-io/crux/utils" "net" + "google.golang.org/grpc/credentials" ) -func (tm *TransactionManager) startRpcServer(port int, ipcPath string) error { +func (tm *TransactionManager) startRpcServer(port int, ipcPath string, tls bool, certFile, keyFile string) error { lis, err := utils.CreateIpcSocket(ipcPath) if err != nil { log.Fatalf("failed to listen: %v", err) @@ -24,7 +25,12 @@ func (tm *TransactionManager) startRpcServer(port int, ipcPath string) error { }() go func() error { - err := tm.startRestServer(port) + var err error + if tls { + err = tm.startRestServerTLS(port, certFile, keyFile, certFile) + } else { + err = tm.startRestServer(port) + } if err != nil { log.Fatalf("failed to start gRPC REST server: %s", err) } @@ -59,3 +65,54 @@ func (tm *TransactionManager) startRestServer(port int) error { http.ListenAndServe(address, mux) return nil } + +func (tm *TransactionManager) startRestServerTLS(port int, certFile, keyFile, ca string) error { + freePort, err := GetFreePort() + if err != nil { + log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) + } + grpcAddress := fmt.Sprintf("%s:%d", "localhost", freePort) + lis, err := net.Listen("tcp", grpcAddress) + if err != nil { + log.Fatalf("failed to start gRPC REST server: %s", err) + } + s := Server{Enclave : tm.Enclave} + creds, err := credentials.NewServerTLSFromFile(certFile, keyFile) + opts := []grpc.ServerOption{grpc.Creds(creds)} + if err != nil { + log.Fatalf("failed to load credentials : %v", err) + } + grpcServer := grpc.NewServer(opts...) + RegisterClientServer(grpcServer, &s) + go func() { + log.Fatal(grpcServer.Serve(lis)) + }() + + address := fmt.Sprintf("%s:%d", "localhost", port) + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + mux := runtime.NewServeMux() + err = RegisterClientHandlerFromEndpoint(ctx, mux, grpcAddress, []grpc.DialOption{grpc.WithTransportCredentials(creds)}) + if err != nil { + log.Fatalf("could not register service Ping: %s", err) + return err + } + http.ListenAndServeTLS(address, certFile, keyFile, mux) + log.Printf("started HTTPS REST server on %s", address) + return nil +} + +func GetFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} \ No newline at end of file diff --git a/server/server.go b/server/server.go index fb90b8f..3b72781 100644 --- a/server/server.go +++ b/server/server.go @@ -70,21 +70,21 @@ func requestLogger(handler http.Handler) http.Handler { } // Init initializes a new TransactionManager instance. -func Init(enc Enclave, port int, ipcPath string, grpc bool, certFile, keyFile string) (TransactionManager, error) { +func Init(enc Enclave, port int, ipcPath string, grpc bool, tls bool, certFile, keyFile string) (TransactionManager, error) { tm := TransactionManager{Enclave : enc} var err error if grpc == true { - err = tm.startRpcServer(port, ipcPath) + err = tm.startRpcServer(port, ipcPath, tls, certFile, keyFile) } else { - err = tm.startHttpserver(port, ipcPath, certFile, keyFile) + err = tm.startHttpserver(port, ipcPath, tls, certFile, keyFile) } return tm, err } -func (tm *TransactionManager) startHttpserver(port int, ipcPath string, certFile, keyFile string) error { +func (tm *TransactionManager) startHttpserver(port int, ipcPath string, tls bool, certFile, keyFile string) error { httpServer := http.NewServeMux() httpServer.HandleFunc(upCheck, tm.upcheck) httpServer.HandleFunc(version, tm.version) @@ -93,14 +93,21 @@ func (tm *TransactionManager) startHttpserver(port int, ipcPath string, certFile httpServer.HandleFunc(partyInfo, tm.partyInfo) serverUrl := "localhost:" + strconv.Itoa(port) - err := CheckCertFiles(certFile, keyFile) - if err != nil { - log.Fatal(err) + if tls{ + err := CheckCertFiles(certFile, keyFile) + if err != nil { + log.Fatal(err) + } + go func() { + log.Fatal(http.ListenAndServeTLS(serverUrl, certFile, keyFile, requestLogger(httpServer))) + }() + log.Infof("HTTPS server is running at: %s", serverUrl) + } else { + go func() { + log.Fatal(http.ListenAndServe(serverUrl, requestLogger(httpServer))) + }() + log.Infof("HTTP server is running at: %s", serverUrl) } - go func() { - log.Fatal(http.ListenAndServeTLS(serverUrl, certFile, keyFile, requestLogger(httpServer))) - }() - log.Infof("HTTPS server is running at: %s", serverUrl) // Restricted to IPC ipcServer := http.NewServeMux() diff --git a/server/server_test.go b/server/server_test.go index 2004d60..53f8975 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -491,7 +491,7 @@ func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { func InitgRPCServer(t *testing.T, grpc bool, port int) (string) { ipcPath, err := ioutil.TempDir("", "TestInitIpc") - tm, err := Init(&MockEnclave{}, port, ipcPath, grpc, "", "") + tm, err := Init(&MockEnclave{}, port, ipcPath, grpc, false, "", "") if err != nil { t.Errorf("Error starting server: %v\n", err) @@ -534,8 +534,8 @@ func TestInit(t *testing.T) { if err != nil { t.Error(err) } - certFile, keyFile := "../cert/server.crt", "../cert/server.key" - tm, err := Init(enc, 9001, ipcPath, false, certFile, keyFile) + certFile, keyFile := "../enclave/testdata/cert/server.crt", "../enclave/testdata/cert/server.key" + tm, err := Init(enc, 9001, ipcPath, false, true, certFile, keyFile) if err != nil { t.Errorf("Error starting server: %v\n", err) } From b70fc4972f40525fb5277f48ca83422f62ce357c Mon Sep 17 00:00:00 2001 From: Puneetha Date: Wed, 27 Jun 2018 11:02:31 +0100 Subject: [PATCH 43/85] Code refractoring. Signed-off-by: Puneetha --- enclave/enclave.go | 2 +- server/server_handler.go | 49 ++++++++-------------------------------- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/enclave/enclave.go b/enclave/enclave.go index 1b3687f..06b5721 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -433,7 +433,7 @@ func (s *SecureEnclave) GetEncodedPartyInfo() []byte { func (s *SecureEnclave) GetEncodedPartyInfoGrpc() []byte { encoded, err := json.Marshal(api.UpdatePartyInfo{Payload: api.EncodePartyInfo(s.PartyInfo)}) - if err != nil{ + if err != nil { log.Errorf("Marshalling failed %v", err) } return encoded diff --git a/server/server_handler.go b/server/server_handler.go index b81b0a8..3541bee 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -88,14 +88,7 @@ func (s *Server) UpdatePartyInfo(ctx context.Context, in *PartyInfo) (*PartyInfo recipients := make(map[[nacl.KeySize]byte]string) for url, key := range in.Receipients{ var as [32]byte - // couldn't find a better way to reduce a slice to array of fixed length - for idx, i := range key{ - if idx < 32{ - as[idx] = i - } else { - break - } - } + copy(as[:], key) recipients[as] = url } @@ -111,15 +104,18 @@ func (s *Server) UpdatePartyInfo(ctx context.Context, in *PartyInfo) (*PartyInfo func (s *Server) Push(ctx context.Context, in *PushPayload) (*PartyInfoResponse, error){ - sender := sliceToarrNaclKey(in.Ep.Sender) - nounce := sliceToarrNaclNonce(in.Ep.Nonce) - recipientNounce := sliceToarrNaclNonce(in.Ep.ReciepientNonce) + var sender *[nacl.KeySize]byte + var nonce, recipientNonce *[nacl.NonceSize]byte + copy(sender[:], in.Ep.Sender) + copy(nonce[:], in.Ep.Nonce) + copy(recipientNonce[:], in.Ep.ReciepientNonce) + encyptedPayload := api.EncryptedPayload{ Sender:sender, CipherText:in.Ep.CipherText, - Nonce:nounce, + Nonce:nonce, RecipientBoxes:in.Ep.ReciepientBoxes, - RecipientNonce:recipientNounce, + RecipientNonce:recipientNonce, } digestHash, err := s.Enclave.StorePayloadGrpc(encyptedPayload, in.Encoded) @@ -130,33 +126,6 @@ func (s *Server) Push(ctx context.Context, in *PushPayload) (*PartyInfoResponse, return &PartyInfoResponse{Payload: digestHash}, nil } - -func sliceToarrNaclKey(key []byte) nacl.Key { - var as nacl.Key - // couldn't find a better way to reduce a slice to array of fixed length - for idx, i := range key{ - if idx < 32{ - as[idx] = i - } else { - break - } - } - return as -} - -func sliceToarrNaclNonce(key []byte) nacl.Nonce { - var as nacl.Nonce - // couldn't find a better way to reduce a slice to array of fixed length - for idx, i := range key{ - if idx < 24{ - as[idx] = i - } else { - break - } - } - return as -} - func decodeErrorGRPC(name string, value string, err error) { log.Error(fmt.Sprintf("Invalid request: unable to decode %s: %s, error: %s\n", name, value, err)) From bc7c53ff663cae6dc329a531002cd7877df93176 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Mon, 2 Jul 2018 13:17:34 +0100 Subject: [PATCH 44/85] Add tls flag --- config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.go b/config/config.go index 9371481..46bf37b 100644 --- a/config/config.go +++ b/config/config.go @@ -56,6 +56,7 @@ func InitFlags() { flag.Int(Verbosity, 1, "Verbosity level of logs") flag.String(AlwaysSendTo, "", "List of public keys for nodes to send all transactions too") flag.Bool(UseGRPC, false, "Use gRPC server") + flag.Bool(Tls, false, "Use TLS to secure HTTP communications") flag.String(TlsServerCert, "", "The server certificate to be used") flag.String(TlsServerKey, "", "The server private key") From e9dc107c14062d58f4388eed9e0acff13b443cc2 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Tue, 3 Jul 2018 10:37:01 +0100 Subject: [PATCH 45/85] Add CHANGELOG.md and update README.md Signed-off-by: Puneetha --- CHANGELOG.md | 5 +++++ README.md | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2c47bb0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +## 0.0.1 - 2018-07-05 +### Added + - Crux, a secure enclave for Quorum written in Golang + - Protobuf and gRPC support. + - TLS can be enabled. diff --git a/README.md b/README.md index 05448f0..002f18b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ Usage of ./bin/crux: --url string The URL to advertise to other nodes (reachable by them) --verbosity int Verbosity level of logs (default 1) --workdir string The folder to put stuff in (default: .) (default ".") + --grpc Use protobuf + gRPC for communication between nodes + --tls Use TLS to secure HTTP communications + --tlsservercert TLS server certificate + --tlsserverkey TLS server key ``` ## Generating keys From 0682a04143c23d46a3334c8ae95d08a2d42000e1 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Tue, 3 Jul 2018 15:18:53 +0100 Subject: [PATCH 46/85] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c47bb0..8f06881 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.0.1 - 2018-07-05 +## 1.0.0 - 2018-07-05 ### Added - Crux, a secure enclave for Quorum written in Golang - Protobuf and gRPC support. From 9740f4a66174411588a7bec6a76194a6aae4f2c6 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Mon, 9 Jul 2018 14:27:00 +0100 Subject: [PATCH 47/85] Update Crux to sync with Quorumv2.0.2 Signed-off-by: Puneetha --- server/server.go | 10 +++++----- server/server_test.go | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/server.go b/server/server.go index d232135..fa5463e 100644 --- a/server/server.go +++ b/server/server.go @@ -210,12 +210,12 @@ func (s *TransactionManager) sendRaw(w http.ResponseWriter, req *http.Request) { return } - // Uncomment the below for Quorum v2.0.2 onwards + // Uncomment the below for Quorum v2.0.1 or below // see https://github.com/jpmorganchase/quorum/commit/ee498061b5a74bf1f3290139a53840345fa038cb#diff-63fbbd6b2c0487b8cd4445e881822cdd - // encodedKey := base64.StdEncoding.EncodeToString(key) - // fmt.Fprint(w, encodedKey) - // Then delete this line - w.Write(key) + //w.Write(key) + // Then delete the below lines + encodedKey := base64.StdEncoding.EncodeToString(key) + fmt.Fprint(w, encodedKey) } func (s *TransactionManager) processSend( diff --git a/server/server_test.go b/server/server_test.go index 385c603..8cc2a51 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -178,9 +178,9 @@ func TestSendRaw(t *testing.T) { headers[hFrom] = []string{sender} headers[hTo] = []string{receiver} - // Uncomment the below for Quorum v2.0.2 onwards - //runRawHandlerTest(t, headers, payload, []byte(encodedPayload), sendRaw, tm.sendRaw) - runRawHandlerTest(t, headers, payload, payload, sendRaw, tm.sendRaw) + runRawHandlerTest(t, headers, payload, []byte(encodedPayload), sendRaw, tm.sendRaw) + // Uncomment the below for Quorum v2.0.1 or below + //runRawHandlerTest(t, headers, payload, payload, sendRaw, tm.sendRaw) } func TestReceive(t *testing.T) { From 535504f1e72c0ad1281d9e13e9e7efb7e3540f65 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Mon, 16 Jul 2018 12:30:59 +0100 Subject: [PATCH 48/85] Fix partyinfo API Signed-off-by: Puneetha --- Gopkg.lock | 86 ++++++++++++++++++++++++++++++++++------ api/client.go | 7 +++- api/internal.go | 12 ++++-- enclave/enclave.go | 2 +- server/messages.proto | 2 +- server/server_handler.go | 3 +- server/server_test.go | 2 +- 7 files changed, 92 insertions(+), 22 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 7a047c8..9d19580 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,12 +2,15 @@ [[projects]] + digest = "1:1834c1bcecde8bc847628b7c316b8258315749995f0c3a398b1370db8c98a1b3" name = "github.com/fsnotify/fsnotify" packages = ["."] + pruneopts = "T" revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" version = "v1.4.7" [[projects]] + digest = "1:c97cc0085879c38dbe7cfc84cca26a8d8d6f2a93f671acdd066208985a85d4b3" name = "github.com/golang/protobuf" packages = [ "jsonpb", @@ -16,29 +19,35 @@ "ptypes/any", "ptypes/duration", "ptypes/struct", - "ptypes/timestamp" + "ptypes/timestamp", ] + pruneopts = "T" revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" version = "v1.1.0" [[projects]] branch = "master" + digest = "1:e99dbc9bee79a1c2406af764186ac826ec502f73de63ee69999866b7d1b412f2" name = "github.com/golang/snappy" packages = ["."] + pruneopts = "T" revision = "553a641470496b2327abcac10b36396bd98e45c9" [[projects]] + digest = "1:1709a0c115bdec713416bed33eee5df8c5d614b8886a102151377919c1f67a9b" name = "github.com/grpc-ecosystem/grpc-gateway" packages = [ "runtime", "runtime/internal", - "utilities" + "utilities", ] + pruneopts = "T" revision = "92583770e3f01b09a0d3e9bdf64321d8bebd48f2" version = "v1.4.1" [[projects]] branch = "master" + digest = "1:8f7d764c7172f13e906e664bdd6a1304a91ae0b3fbffbfd36675fd9b0738bc48" name = "github.com/hashicorp/hcl" packages = [ ".", @@ -50,17 +59,21 @@ "hcl/token", "json/parser", "json/scanner", - "json/token" + "json/token", ] + pruneopts = "T" revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" [[projects]] branch = "master" + digest = "1:e29757afd23af4c0c109594ccf0daaf8dd8e325f56ad38febbc4d13adfde0614" name = "github.com/jsimonetti/berkeleydb" packages = ["."] + pruneopts = "T" revision = "5cde5eaaf78c6510c5f64f5347244806a06ba87b" [[projects]] + digest = "1:08f48150e800c09ee719ab61baa8ed8161b6896d1d9bc40cd92fe1fe62b7a5c2" name = "github.com/kevinburke/nacl" packages = [ ".", @@ -68,70 +81,90 @@ "onetimeauth", "randombytes", "scalarmult", - "secretbox" + "secretbox", ] + pruneopts = "T" revision = "247b7cfd826641547b7ee8b89c785901c4e94b0b" version = "0.5" [[projects]] + digest = "1:23efb83c2ae6eb987f4187ea73315db2aea24d849f950a4d9ca7fd6cef91e4f1" name = "github.com/magiconair/properties" packages = ["."] + pruneopts = "T" revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" version = "v1.7.6" [[projects]] branch = "master" + digest = "1:2514da1e59c0a936d8c1e0fbf5592267a3c5893eb4555ce767bb54d149e9cf6e" name = "github.com/mitchellh/mapstructure" packages = ["."] + pruneopts = "T" revision = "00c29f56e2386353d58c599509e8dc3801b0d716" [[projects]] + digest = "1:b1e91eaa4f0bb9ef59cc342f1f3df6160957cd882c5c18b8bd6dc2d0857fdc87" name = "github.com/pelletier/go-toml" packages = ["."] + pruneopts = "T" revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" version = "v1.1.0" [[projects]] + digest = "1:f2d6485978c29de1bf447577aed1e53ff4d7cb0000361bbbbbde23a87fae298a" name = "github.com/sirupsen/logrus" packages = ["."] + pruneopts = "T" revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" version = "v1.0.5" [[projects]] + digest = "1:6d5669c75d8044093981de5b0955e582e24978c4a2a04dfef3a7f5fbe3c6ee9e" name = "github.com/spf13/afero" packages = [ ".", - "mem" + "mem", ] + pruneopts = "T" revision = "63644898a8da0bc22138abf860edaf5277b6102e" version = "v1.1.0" [[projects]] + digest = "1:516e71bed754268937f57d4ecb190e01958452336fa73dbac880894164e91c1f" name = "github.com/spf13/cast" packages = ["."] + pruneopts = "T" revision = "8965335b8c7107321228e3e3702cab9832751bac" version = "v1.2.0" [[projects]] branch = "master" + digest = "1:080e5f630945ad754f4b920e60b4d3095ba0237ebf88dc462eb28002932e3805" name = "github.com/spf13/jwalterweatherman" packages = ["."] + pruneopts = "T" revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" [[projects]] + digest = "1:9798f8595f3bf57586a622e8e78b7b8d159ceb375b9485c7fbcd2fd686ac4c46" name = "github.com/spf13/pflag" packages = ["."] + pruneopts = "T" revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" version = "v1.0.0" [[projects]] + digest = "1:a49ffa1d09595ac8729e1e772cba6d60ea26ed10f0c4be4820b7230c6cfc12ed" name = "github.com/spf13/viper" packages = ["."] + pruneopts = "T" revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736" version = "v1.0.2" [[projects]] branch = "master" + digest = "1:a2c06f269d9dc602ad583db7b6dd19d19e94c5a509cbe1d94d9e0f794ba9e305" name = "github.com/syndtr/goleveldb" packages = [ "leveldb", @@ -145,24 +178,28 @@ "leveldb/opt", "leveldb/storage", "leveldb/table", - "leveldb/util" + "leveldb/util", ] + pruneopts = "T" revision = "714f901b98fdb3aa954b4193d8cbd64a28d80cad" [[projects]] branch = "master" + digest = "1:68e7337ca5407b8da2b49b21142d47bce8517a31152c488efafd9c6ad6dce48e" name = "golang.org/x/crypto" packages = [ "curve25519", "poly1305", "salsa20/salsa", "sha3", - "ssh/terminal" + "ssh/terminal", ] + pruneopts = "T" revision = "beb2a9779c3b677077c41673505f150149fce895" [[projects]] branch = "master" + digest = "1:f49302030f7a716cb6c5973a77d6377f0ee4bbf5a250a48f8c9072b2ded10792" name = "golang.org/x/net" packages = [ "context", @@ -171,20 +208,24 @@ "http2/hpack", "idna", "internal/timeseries", - "trace" + "trace", ] + pruneopts = "T" revision = "89e543239a64caf31d3a6865872ea120b41446df" [[projects]] branch = "master" + digest = "1:6cb6853c6c658ef6eb0ddec84764f0b9a1e6728e6d6ec1a61c8757d4634f75ae" name = "golang.org/x/sys" packages = [ "unix", - "windows" + "windows", ] + pruneopts = "T" revision = "3b87a42e500a6dc65dae1a55d0b641295971163e" [[projects]] + digest = "1:24db346d9931fe01f1e9a02aba78ba22c1ecd55bf0f79dd10ba5169719cf002d" name = "golang.org/x/text" packages = [ "collate", @@ -200,18 +241,22 @@ "unicode/bidi", "unicode/cldr", "unicode/norm", - "unicode/rangetable" + "unicode/rangetable", ] + pruneopts = "T" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] branch = "master" + digest = "1:5ecad12ed407b601c1cdf780e84b095d57a59343589a12a385d3828ecb2cc0b2" name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] + pruneopts = "T" revision = "694d95ba50e67b2e363f3483057db5d4910c18f9" [[projects]] + digest = "1:025336210cc711d68bf3d61cab51ccf7b0b33ea2d08def98065c2014ee8cc6c9" name = "google.golang.org/grpc" packages = [ ".", @@ -237,20 +282,37 @@ "stats", "status", "tap", - "transport" + "transport", ] + pruneopts = "T" revision = "41344da2231b913fa3d983840a57a6b1b7b631a1" version = "v1.12.0" [[projects]] + digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "T" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" version = "v2.2.1" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a7dbc72760828be466ae1a6d718b76f8274462afaa818082b1d605065021ed40" + input-imports = [ + "github.com/grpc-ecosystem/grpc-gateway/runtime", + "github.com/jsimonetti/berkeleydb", + "github.com/kevinburke/nacl", + "github.com/kevinburke/nacl/box", + "github.com/kevinburke/nacl/secretbox", + "github.com/sirupsen/logrus", + "github.com/spf13/pflag", + "github.com/spf13/viper", + "github.com/syndtr/goleveldb/leveldb", + "golang.org/x/crypto/sha3", + "golang.org/x/net/context", + "google.golang.org/grpc", + "google.golang.org/grpc/credentials", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/api/client.go b/api/client.go index 26857bb..c3ee76f 100644 --- a/api/client.go +++ b/api/client.go @@ -46,9 +46,14 @@ type ResendRequest struct { } type UpdatePartyInfo struct { - Payload []byte `json:"payload"` + Url string `json:"url"` + Recipients map[string][]byte `json:"recipients"` + Parties map[string]bool `json:"parties"` } +type PartyInfoResponse struct { + Payload []byte `json:"payload"` +} type PrivateKeyBytes struct { Bytes string `json:"bytes"` } diff --git a/api/internal.go b/api/internal.go index 23de456..8d54706 100644 --- a/api/internal.go +++ b/api/internal.go @@ -111,7 +111,7 @@ func (s *PartyInfo) GetPartyInfo(grpc bool) { } var req *http.Request - encoded := getEncoded(grpc, encodedPartyInfo) + encoded := s.getEncoded(grpc, encodedPartyInfo) req, err = http.NewRequest("POST", endPoint, bytes.NewBuffer(encoded)) if err != nil { @@ -147,7 +147,7 @@ func (s *PartyInfo) GetPartyInfo(grpc bool) { } func (s *PartyInfo) updatePartyInfoGrpc(resp *http.Response, rawUrl string) error { - var partyInfoReq UpdatePartyInfo + var partyInfoReq PartyInfoResponse err := json.NewDecoder(resp.Body).Decode(&partyInfoReq) resp.Body.Close() if err != nil { @@ -178,9 +178,13 @@ func (s *PartyInfo) updatePartyInfo(resp *http.Response, rawUrl string) error { return nil } -func getEncoded(grpc bool, encodedPartyInfo []byte) []byte{ +func (s *PartyInfo) getEncoded(grpc bool, encodedPartyInfo []byte) []byte{ if grpc{ - e, err := json.Marshal(UpdatePartyInfo{encodedPartyInfo}) + recipients := make(map[string][]byte) + for key, url := range s.recipients{ + recipients[url] = key[:] + } + e, err := json.Marshal(UpdatePartyInfo{s.url, recipients, s.parties}) if err != nil{ log.Errorf("Marshalling failed %v", err) return nil diff --git a/enclave/enclave.go b/enclave/enclave.go index 06b5721..dd89eaf 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -432,7 +432,7 @@ func (s *SecureEnclave) GetEncodedPartyInfo() []byte { } func (s *SecureEnclave) GetEncodedPartyInfoGrpc() []byte { - encoded, err := json.Marshal(api.UpdatePartyInfo{Payload: api.EncodePartyInfo(s.PartyInfo)}) + encoded, err := json.Marshal(api.PartyInfoResponse{Payload: api.EncodePartyInfo(s.PartyInfo)}) if err != nil { log.Errorf("Marshalling failed %v", err) } diff --git a/server/messages.proto b/server/messages.proto index 8d4fed5..849f5f2 100644 --- a/server/messages.proto +++ b/server/messages.proto @@ -26,7 +26,7 @@ message ReceiveResponse{ } message PartyInfo{ string url = 1; - map receipients = 2; + map recipients = 2; map parties = 3; } message PartyInfoResponse{ diff --git a/server/server_handler.go b/server/server_handler.go index 3541bee..390285a 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -86,12 +86,11 @@ func (s *Server) processReceive(b64Key []byte, b64To string) ([]byte, error) { func (s *Server) UpdatePartyInfo(ctx context.Context, in *PartyInfo) (*PartyInfoResponse, error) { recipients := make(map[[nacl.KeySize]byte]string) - for url, key := range in.Receipients{ + for url, key := range in.Recipients{ var as [32]byte copy(as[:], key) recipients[as] = url } - s.Enclave.UpdatePartyInfoGrpc(in.Url, recipients, in.Parties) encoded := s.Enclave.GetEncodedPartyInfoGrpc() var decodedPartyInfo PartyInfoResponse diff --git a/server/server_test.go b/server/server_test.go index 8cc2a51..ea4bc7b 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -474,7 +474,7 @@ func TestPartyInfo(t *testing.T) { func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { encodedPartyInfo := api.EncodePartyInfo(pi) - encoded, err := json.Marshal(api.UpdatePartyInfo{Payload:encodedPartyInfo}) + encoded, err := json.Marshal(api.PartyInfoResponse{Payload:encodedPartyInfo}) if err != nil { t.Errorf("Marshalling failed %v", err) } From 1747986b0ffb8b9c0721e07b769f50aadb0b58d8 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Mon, 16 Jul 2018 15:12:58 +0100 Subject: [PATCH 49/85] Check for free port for starting REST server Signed-off-by: Puneetha --- server/proto_server.go | 6 +++++- server/server_test.go | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/server/proto_server.go b/server/proto_server.go index c771912..5756b7d 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -41,7 +41,11 @@ func (tm *TransactionManager) startRpcServer(port int, ipcPath string, tls bool, } func (tm *TransactionManager) startRestServer(port int) error { - grpcAddress := fmt.Sprintf("%s:%d", "localhost", port-1) + freePort, err := GetFreePort() + if err != nil { + log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) + } + grpcAddress := fmt.Sprintf("%s:%d", "localhost", freePort) lis, err := net.Listen("tcp", grpcAddress) s := Server{Enclave : tm.Enclave} diff --git a/server/server_test.go b/server/server_test.go index ea4bc7b..64ac276 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -17,6 +17,7 @@ import ( "path" "io/ioutil" "fmt" + log "github.com/sirupsen/logrus" ) const sender = "BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo=" @@ -149,10 +150,14 @@ func TestGRPCSend(t *testing.T) { } expected := SendResponse{Key: payload} - ipcPath := InitgRPCServer(t, true, 9005) + freePort, err := GetFreePort() + if err != nil { + log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) + } + ipcPath := InitgRPCServer(t, true, freePort) var conn *grpc.ClientConn - conn, err := grpc.Dial(fmt.Sprintf("passthrough:///unix://%s", ipcPath), grpc.WithInsecure()) + conn, err = grpc.Dial(fmt.Sprintf("passthrough:///unix://%s", ipcPath), grpc.WithInsecure()) if err != nil { t.Fatalf("Connection to gRPC server failed with error %s", err) } @@ -210,10 +215,13 @@ func TestGRPCReceive(t *testing.T) { }, } expected := ReceiveResponse{Payload: payload} - - ipcPath := InitgRPCServer(t, true, 9010) + freePort, err := GetFreePort() + if err != nil { + log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) + } + ipcPath := InitgRPCServer(t, true, freePort) var conn *grpc.ClientConn - conn, err := grpc.Dial(fmt.Sprintf("passthrough:///unix://%s", ipcPath), grpc.WithInsecure()) + conn, err = grpc.Dial(fmt.Sprintf("passthrough:///unix://%s", ipcPath), grpc.WithInsecure()) if err != nil { t.Fatalf("Connection to gRPC server failed with error %s", err) } From 5dd9ae96183888951043a906b4e3dd035551f949 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Thu, 19 Jul 2018 13:19:12 +0100 Subject: [PATCH 50/85] Implement partyinfo and push APIs using gRPC APIs Signed-off-by: Puneetha --- Makefile | 12 +-- api/internal.go | 118 +++++++++++++++++++++----- config/config.go | 2 + crux.go | 27 +++--- enclave/enclave.go | 10 ++- enclave/enclave_test.go | 6 +- {server => protofiles}/grpc.proto | 2 +- {server => protofiles}/messages.proto | 2 +- server/proto_server.go | 78 ++++++++++------- server/server.go | 6 +- server/server_handler.go | 42 ++++----- server/server_test.go | 25 +++--- 12 files changed, 214 insertions(+), 116 deletions(-) rename {server => protofiles}/grpc.proto (97%) rename {server => protofiles}/messages.proto (97%) diff --git a/Makefile b/Makefile index 48567c9..70713e8 100644 --- a/Makefile +++ b/Makefile @@ -14,16 +14,16 @@ all: build protofiles: proto grpc proto: - $Q protoc -I server/ \ + $Q protoc -I protofiles/ \ -I $(GOPATH)/../vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ - --go_out=plugins=grpc:server \ - server/*.proto + --go_out=plugins=grpc:protofiles \ + protofiles/*.proto grpc: - $Q protoc -I server/ \ + $Q protoc -I protofiles/ \ -I $(GOPATH)/../vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ - --grpc-gateway_out=logtostderr=true:server \ - server/grpc.proto + --grpc-gateway_out=logtostderr=true:protofiles \ + protofiles/grpc.proto .PHONY: build diff --git a/api/internal.go b/api/internal.go index 8d54706..893b62c 100644 --- a/api/internal.go +++ b/api/internal.go @@ -13,6 +13,10 @@ import ( "encoding/json" "net/http/httputil" "fmt" + "github.com/blk-io/crux/protofiles" + "golang.org/x/net/context" + "google.golang.org/grpc" + "net/url" ) // EncryptedPayload is the struct used for storing all data associated with an encrypted @@ -31,6 +35,7 @@ type PartyInfo struct { recipients map[[nacl.KeySize]byte]string // public key -> URL parties map[string]bool // Node (or party) URLs client utils.HttpClient + grpc string } // GetRecipient retrieves the URL associated with the provided recipient. @@ -39,12 +44,12 @@ func (s *PartyInfo) GetRecipient(key nacl.Key) (string, bool) { return value, ok } -func (s *PartyInfo) GetAllValues() (string, map[[nacl.KeySize]byte]string, map[string]bool){ +func (s *PartyInfo) GetAllValues() (string, map[[nacl.KeySize]byte]string, map[string]bool) { return s.url, s.recipients, s.parties } // InitPartyInfo initializes a new PartyInfo store. -func InitPartyInfo(rawUrl string, otherNodes []string, client utils.HttpClient) PartyInfo { +func InitPartyInfo(rawUrl string, otherNodes []string, client utils.HttpClient, grpc string) PartyInfo { parties := make(map[string]bool) for _, node := range otherNodes { parties[node] = true @@ -55,6 +60,7 @@ func InitPartyInfo(rawUrl string, otherNodes []string, client utils.HttpClient) recipients: make(map[[nacl.KeySize]byte]string), parties: parties, client: client, + grpc: grpc, } } @@ -87,9 +93,51 @@ func (s *PartyInfo) RegisterPublicKeys(pubKeys []nacl.Key) { } } +func (s *PartyInfo) GetPartyInfoGrpc() { + recipients := make(map[string][]byte) + for key, url := range s.recipients{ + recipients[url] = key[:] + } + urls := make(map[string]bool) + for k, v := range s.parties { + urls[k] = v + } + + for rawUrl := range urls{ + if rawUrl == s.url { + continue + } + var completeUrl url.URL + url, err := completeUrl.Parse(rawUrl) + conn, err := grpc.Dial(url.Host, grpc.WithInsecure()) + if err != nil { + log.Fatalf("Connection to gRPC server failed with error %s", err) + } + defer conn.Close() + cli := protofiles.NewClientClient(conn) + if cli == nil{ + log.Fatalf("Client is not intialised") + } + party := protofiles.PartyInfo{Url:rawUrl, Recipients:recipients, Parties:s.parties} + + partyInfoResp, err := cli.UpdatePartyInfo(context.Background(), &party) + if err != nil { + log.Errorf("Error in updating party info %s", err) + } + err = s.updatePartyInfoGrpc(*partyInfoResp, s.url) + if err != nil { + log.Errorf("Error: %s", err) + break + } + } +} // GetPartyInfo requests PartyInfo data from all remote nodes this node is aware of. The data // provided in each response is applied to this node. -func (s *PartyInfo) GetPartyInfo(grpc bool) { +func (s *PartyInfo) GetPartyInfo() { + if s.grpc != "" { + s.GetPartyInfoGrpc() + return + } encodedPartyInfo := EncodePartyInfo(*s) // First copy our endpoints as we update this map in place @@ -111,7 +159,7 @@ func (s *PartyInfo) GetPartyInfo(grpc bool) { } var req *http.Request - encoded := s.getEncoded(grpc, encodedPartyInfo) + encoded := s.getEncoded(encodedPartyInfo) req, err = http.NewRequest("POST", endPoint, bytes.NewBuffer(encoded)) if err != nil { @@ -135,26 +183,15 @@ func (s *PartyInfo) GetPartyInfo(grpc bool) { continue } - if grpc { - err = s.updatePartyInfoGrpc(resp, rawUrl) - } else { - err = s.updatePartyInfo(resp, rawUrl) - } + err = s.updatePartyInfo(resp, rawUrl) + if err != nil { break } } } -func (s *PartyInfo) updatePartyInfoGrpc(resp *http.Response, rawUrl string) error { - var partyInfoReq PartyInfoResponse - err := json.NewDecoder(resp.Body).Decode(&partyInfoReq) - resp.Body.Close() - if err != nil { - log.WithField("url", rawUrl).Errorf( - "Unable to read partyInfo response from host, %v", err) - return err - } +func (s *PartyInfo) updatePartyInfoGrpc(partyInfoReq protofiles.PartyInfoResponse, rawUrl string) error { pi, err := DecodePartyInfo(partyInfoReq.Payload) if err != nil { log.WithField("url", rawUrl).Errorf( @@ -178,8 +215,8 @@ func (s *PartyInfo) updatePartyInfo(resp *http.Response, rawUrl string) error { return nil } -func (s *PartyInfo) getEncoded(grpc bool, encodedPartyInfo []byte) []byte{ - if grpc{ +func (s *PartyInfo) getEncoded(encodedPartyInfo []byte) []byte{ + if s.grpc != "" { recipients := make(map[string][]byte) for key, url := range s.recipients{ recipients[url] = key[:] @@ -194,9 +231,9 @@ func (s *PartyInfo) getEncoded(grpc bool, encodedPartyInfo []byte) []byte{ return encodedPartyInfo[:] } -func (s *PartyInfo) PollPartyInfo(grpc bool) { +func (s *PartyInfo) PollPartyInfo() { time.Sleep(time.Duration(rand.Intn(16)) * time.Second) - s.GetPartyInfo(grpc) + s.GetPartyInfo() ticker := time.NewTicker(2 * time.Minute) quit := make(chan struct{}) @@ -204,7 +241,7 @@ func (s *PartyInfo) PollPartyInfo(grpc bool) { for { select { case <- ticker.C: - s.GetPartyInfo(grpc) + s.GetPartyInfo() case <- quit: ticker.Stop() return @@ -259,6 +296,41 @@ func (s *PartyInfo) UpdatePartyInfoGrpc(url string, recipients map[[nacl.KeySize } } +func PushGrpc(encoded []byte, path string, epl EncryptedPayload) error { + var completeUrl url.URL + url, err := completeUrl.Parse(path) + conn, err := grpc.Dial(url.Host, grpc.WithInsecure()) + if err != nil { + log.Fatalf("Connection to gRPC server failed with error %s", err) + } + defer conn.Close() + cli := protofiles.NewClientClient(conn) + if cli == nil{ + log.Fatalf("Client is not intialised") + } + + var sender [32]byte + var nonce [32]byte + var recipientNonce [32]byte + + copy(sender[:], (*epl.Sender)[:]) + copy(nonce[:], (*epl.Nonce)[:]) + copy(recipientNonce[:], (*epl.RecipientNonce)[:]) + encrypt := protofiles.EncryptedPayload{ + Sender:sender[:], + CipherText:epl.CipherText, + Nonce:nonce[:], + ReciepientNonce:recipientNonce[:], + ReciepientBoxes:epl.RecipientBoxes, + } + pushPayload := protofiles.PushPayload{Ep:&encrypt, Encoded:encoded} + _, err = cli.Push(context.Background(), &pushPayload) + if err != nil{ + log.Errorf("Push failed with %s", err) + return err + } + return nil +} // Push is responsible for propagating the encoded payload to the given remote node. func Push(encoded []byte, url string, client utils.HttpClient) (string, error) { diff --git a/config/config.go b/config/config.go index 46bf37b..58d4cfc 100644 --- a/config/config.go +++ b/config/config.go @@ -25,6 +25,7 @@ const ( BerkeleyDb = "berkeleydb" UseGRPC = "grpc" + GrpcJsonPort = "grpcport" Tls = "tls" TlsServerChain = "tlsserverchain" @@ -59,6 +60,7 @@ func InitFlags() { flag.Bool(Tls, false, "Use TLS to secure HTTP communications") flag.String(TlsServerCert, "", "The server certificate to be used") flag.String(TlsServerKey, "", "The server private key") + flag.Int(GrpcJsonPort, -1, "The local port to listen on for JSON extensions of gRPC") // storage not currently supported as we use LevelDB diff --git a/crux.go b/crux.go index e9e5c40..f571360 100644 --- a/crux.go +++ b/crux.go @@ -12,6 +12,7 @@ import ( "github.com/blk-io/crux/storage" log "github.com/sirupsen/logrus" "time" + "fmt" ) func main() { @@ -64,7 +65,6 @@ func main() { ipcFile := config.GetString(config.Socket) storagePath := path.Join(workDir, dbStorage) ipcPath := path.Join(workDir, ipcFile) - var db storage.DataStore var err error if config.GetBool(config.BerkeleyDb) { @@ -83,11 +83,19 @@ func main() { if url == "" { log.Fatalln("URL must be specified") } - + port := config.GetInt(config.Port) + if port < 0 { + log.Fatalln("Port must be specified") + } httpClient := &http.Client{ Timeout: time.Second * 10, } - pi := api.InitPartyInfo(url, otherNodes, httpClient) + grpc := config.GetBool(config.UseGRPC) + grpcIpcPath := "" + if grpc { + grpcIpcPath = fmt.Sprintf("localhost:%d", port) + } + pi := api.InitPartyInfo(url, otherNodes, httpClient, grpcIpcPath) privKeyFiles := config.GetStringSlice(config.PrivateKeys) pubKeyFiles := config.GetStringSlice(config.PublicKeys) @@ -108,16 +116,10 @@ func main() { pubKeyFiles[i] = path.Join(workDir, keyFile) } - enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient) + enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient, grpcIpcPath) pi.RegisterPublicKeys(enc.PubKeys) - port := config.GetInt(config.Port) - if port < 0 { - log.Fatalln("Port must be specified") - } - - grpc := config.GetBool(config.UseGRPC) tls := config.GetBool(config.Tls) var tlsCertFile, tlsKeyFile string if tls { @@ -131,12 +133,13 @@ func main() { tlsCertFile = path.Join(workDir, servCert) tlsKeyFile = path.Join(workDir, servKey) } - _, err = server.Init(enc, port, ipcPath, grpc, tls, tlsCertFile, tlsKeyFile) + grpcJsonport := config.GetInt(config.GrpcJsonPort) + _, err = server.Init(enc, port, ipcPath, grpc, grpcJsonport, tls, tlsCertFile, tlsKeyFile) if err != nil { log.Fatalf("Error starting server: %v\n", err) } - pi.PollPartyInfo(grpc) + pi.PollPartyInfo() select {} } diff --git a/enclave/enclave.go b/enclave/enclave.go index dd89eaf..198fd82 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -30,6 +30,7 @@ type SecureEnclave struct { PartyInfo api.PartyInfo // Details of all other nodes (or parties) on the network keyCache map[nacl.Key]map[nacl.Key]nacl.Key // Maps sender -> recipient -> shared key client utils.HttpClient // The underlying HTTP client used to propagate requests + grpc string } // Init creates a new instance of the SecureEnclave. @@ -37,7 +38,7 @@ func Init( db storage.DataStore, pubKeyFiles, privKeyFiles []string, pi api.PartyInfo, - client utils.HttpClient) *SecureEnclave { + client utils.HttpClient, grpc string) *SecureEnclave { // Key format: // BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= @@ -59,6 +60,7 @@ func Init( PrivKeys: privKeys, PartyInfo: pi, client: client, + grpc: grpc, } // We use shared keys for encrypting data. The keys between a specific sender and recipient are @@ -233,7 +235,11 @@ func (s *SecureEnclave) publishPayload(epl api.EncryptedPayload, recipient []byt if url, ok := s.PartyInfo.GetRecipient(key); ok { encoded := api.EncodePayloadWithRecipients(epl, [][]byte{}) - api.Push(encoded, url, s.client) + if s.grpc != "" { + api.PushGrpc(encoded, url, epl) + } else { + api.Push(encoded, url, s.client) + } } else { log.WithField("recipientKey", hex.EncodeToString(recipient)).Error("Unable to resolve host") } diff --git a/enclave/enclave_test.go b/enclave/enclave_test.go index d3f2226..e9d5ed5 100644 --- a/enclave/enclave_test.go +++ b/enclave/enclave_test.go @@ -58,7 +58,7 @@ func initEnclave( []string{"testdata/key.pub"}, []string{"testdata/key"}, pi, - client) + client, "") } func initDefaultEnclave(t *testing.T, @@ -68,7 +68,7 @@ func initDefaultEnclave(t *testing.T, client = &MockClient{} pi := api.InitPartyInfo( "http://localhost:8000", - []string{"http://localhost:8001"}, client) + []string{"http://localhost:8001"}, client, "") return initEnclave(t, dbPath, pi, client) } @@ -146,7 +146,7 @@ func TestStoreAndRetrieve(t *testing.T) { []string{"testdata/rcpt1.pub"}, []string{"testdata/rcpt1"}, pi, - client) + client, "") var digest2 []byte digest2, err = enc2.StorePayload(propagatedPl) diff --git a/server/grpc.proto b/protofiles/grpc.proto similarity index 97% rename from server/grpc.proto rename to protofiles/grpc.proto index 274a087..56545eb 100644 --- a/server/grpc.proto +++ b/protofiles/grpc.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -package server; +package protofiles; import "google/api/annotations.proto"; import "messages.proto"; diff --git a/server/messages.proto b/protofiles/messages.proto similarity index 97% rename from server/messages.proto rename to protofiles/messages.proto index 849f5f2..a9354d8 100644 --- a/server/messages.proto +++ b/protofiles/messages.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -package server; +package protofiles; import "google/api/annotations.proto"; diff --git a/server/proto_server.go b/server/proto_server.go index 5756b7d..967dae3 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -10,16 +10,17 @@ import ( "github.com/blk-io/crux/utils" "net" "google.golang.org/grpc/credentials" + "github.com/blk-io/crux/protofiles" ) -func (tm *TransactionManager) startRpcServer(port int, ipcPath string, tls bool, certFile, keyFile string) error { +func (tm *TransactionManager) startRpcServer(port int, grpcJsonPort int, ipcPath string, tls bool, certFile, keyFile string) error { lis, err := utils.CreateIpcSocket(ipcPath) if err != nil { log.Fatalf("failed to listen: %v", err) } s := Server{Enclave : tm.Enclave} grpcServer := grpc.NewServer() - RegisterClientServer(grpcServer, &s) + protofiles.RegisterClientServer(grpcServer, &s) go func() { log.Fatal(grpcServer.Serve(lis)) }() @@ -31,6 +32,13 @@ func (tm *TransactionManager) startRpcServer(port int, ipcPath string, tls bool, } else { err = tm.startRestServer(port) } + if grpcJsonPort != -1 { + if tls { + err = tm.startJsonServerTLS(port, grpcJsonPort, certFile, keyFile, certFile) + } else { + err = tm.startJsonServer(port, grpcJsonPort) + } + } if err != nil { log.Fatalf("failed to start gRPC REST server: %s", err) } @@ -40,42 +48,59 @@ func (tm *TransactionManager) startRpcServer(port int, ipcPath string, tls bool, return err } -func (tm *TransactionManager) startRestServer(port int) error { - freePort, err := GetFreePort() +func (tm *TransactionManager) startJsonServer(port int, grpcJsonPort int) error { + address := fmt.Sprintf("%s:%d", "localhost", grpcJsonPort) + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + mux := runtime.NewServeMux() + opts := []grpc.DialOption{grpc.WithInsecure()} + err := protofiles.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", "localhost", port), opts) if err != nil { - log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) + return fmt.Errorf("could not register service: %s", err) } - grpcAddress := fmt.Sprintf("%s:%d", "localhost", freePort) - lis, err := net.Listen("tcp", grpcAddress) + log.Printf("starting HTTP/1.1 REST server on %s", address) + err = http.ListenAndServe(address, mux) + if err != nil { + return fmt.Errorf("could not listen on %s due to: %s", address, err) + } + return nil +} +func (tm *TransactionManager) startRestServer(port int) error { + grpcAddress := fmt.Sprintf("%s:%d", "localhost", port) + lis, err := net.Listen("tcp", grpcAddress) + if err != nil { + panic(err) + } s := Server{Enclave : tm.Enclave} grpcServer := grpc.NewServer() - RegisterClientServer(grpcServer, &s) + protofiles.RegisterClientServer(grpcServer, &s) go func() { log.Fatal(grpcServer.Serve(lis)) }() + return nil +} - address := fmt.Sprintf("%s:%d", "localhost", port) +func (tm *TransactionManager) startJsonServerTLS(port int, grpcJsonPort int, certFile, keyFile,ca string) error { + address := fmt.Sprintf("%s:%d", "localhost", grpcJsonPort) ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() mux := runtime.NewServeMux() - opts := []grpc.DialOption{grpc.WithInsecure()} - err = RegisterClientHandlerFromEndpoint(ctx, mux, grpcAddress, opts) + creds, err := credentials.NewServerTLSFromFile(certFile, keyFile) + err = protofiles.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", "localhost", port), []grpc.DialOption{grpc.WithTransportCredentials(creds)}) if err != nil { - return fmt.Errorf("could not register service Ping: %s", err) + log.Fatalf("could not register service Ping: %s", err) + return err } - log.Printf("starting HTTP/1.1 REST server on %s", address) http.ListenAndServe(address, mux) + log.Printf("started HTTPS REST server on %s", address) return nil } -func (tm *TransactionManager) startRestServerTLS(port int, certFile, keyFile, ca string) error { - freePort, err := GetFreePort() - if err != nil { - log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) - } - grpcAddress := fmt.Sprintf("%s:%d", "localhost", freePort) +func (tm *TransactionManager) startRestServerTLS(port int, certFile, keyFile,ca string) error { + grpcAddress := fmt.Sprintf("%s:%d", "localhost", port) lis, err := net.Listen("tcp", grpcAddress) if err != nil { log.Fatalf("failed to start gRPC REST server: %s", err) @@ -87,23 +112,10 @@ func (tm *TransactionManager) startRestServerTLS(port int, certFile, keyFile, ca log.Fatalf("failed to load credentials : %v", err) } grpcServer := grpc.NewServer(opts...) - RegisterClientServer(grpcServer, &s) + protofiles.RegisterClientServer(grpcServer, &s) go func() { log.Fatal(grpcServer.Serve(lis)) }() - - address := fmt.Sprintf("%s:%d", "localhost", port) - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - defer cancel() - mux := runtime.NewServeMux() - err = RegisterClientHandlerFromEndpoint(ctx, mux, grpcAddress, []grpc.DialOption{grpc.WithTransportCredentials(creds)}) - if err != nil { - log.Fatalf("could not register service Ping: %s", err) - return err - } - http.ListenAndServeTLS(address, certFile, keyFile, mux) - log.Printf("started HTTPS REST server on %s", address) return nil } diff --git a/server/server.go b/server/server.go index fa5463e..7c69429 100644 --- a/server/server.go +++ b/server/server.go @@ -75,12 +75,12 @@ func requestLogger(handler http.Handler) http.Handler { } // Init initializes a new TransactionManager instance. -func Init(enc Enclave, port int, ipcPath string, grpc bool, tls bool, certFile, keyFile string) (TransactionManager, error) { +func Init(enc Enclave, port int, ipcPath string, grpc bool, grpcJsonPort int, tls bool, certFile, keyFile string) (TransactionManager, error) { tm := TransactionManager{Enclave : enc} var err error - +//err = tm.startRestServer(port) if grpc == true { - err = tm.startRpcServer(port, ipcPath, tls, certFile, keyFile) + err = tm.startRpcServer(port, grpcJsonPort, ipcPath, tls, certFile, keyFile) } else { err = tm.startHttpserver(port, ipcPath, tls, certFile, keyFile) diff --git a/server/server_handler.go b/server/server_handler.go index 390285a..1f66e29 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -9,26 +9,27 @@ import ( "github.com/kevinburke/nacl" "encoding/json" "github.com/blk-io/crux/api" + "github.com/blk-io/crux/protofiles" ) type Server struct { Enclave Enclave } -func (s *Server) Version(ctx context.Context, in *ApiVersion) (*ApiVersion, error) { - return &ApiVersion{Version:apiVersion}, nil +func (s *Server) Version(ctx context.Context, in *protofiles.ApiVersion) (*protofiles.ApiVersion, error) { + return &protofiles.ApiVersion{Version:apiVersion}, nil } -func (s *Server) Upcheck(ctx context.Context, in *UpCheckResponse) (*UpCheckResponse, error) { - return &UpCheckResponse{Message:upCheckResponse}, nil +func (s *Server) Upcheck(ctx context.Context, in *protofiles.UpCheckResponse) (*protofiles.UpCheckResponse, error) { + return &protofiles.UpCheckResponse{Message:upCheckResponse}, nil } -func (s *Server) Send(ctx context.Context, in *SendRequest) (*SendResponse, error) { +func (s *Server) Send(ctx context.Context, in *protofiles.SendRequest) (*protofiles.SendResponse, error) { key, err := s.processSend(in.GetFrom(), in.GetTo(), &in.Payload) - var sendResp SendResponse + var sendResp protofiles.SendResponse if err != nil { log.Error(err) } else { - sendResp = SendResponse{Key: key} + sendResp = protofiles.SendResponse{Key: key} } return &sendResp, err } @@ -60,13 +61,13 @@ func (s *Server) processSend(b64from string, b64recipients []string, payload *[] return s.Enclave.Store(payload, sender, recipients) } -func (s *Server) Receive(ctx context.Context, in *ReceiveRequest) (*ReceiveResponse, error) { +func (s *Server) Receive(ctx context.Context, in *protofiles.ReceiveRequest) (*protofiles.ReceiveResponse, error) { payload, err := s.processReceive(in.Key, in.To) - var receiveResp ReceiveResponse + var receiveResp protofiles.ReceiveResponse if err != nil { log.Error(err) } else { - receiveResp = ReceiveResponse{Payload: payload} + receiveResp = protofiles.ReceiveResponse{Payload: payload} } return &receiveResp, err } @@ -84,7 +85,7 @@ func (s *Server) processReceive(b64Key []byte, b64To string) ([]byte, error) { } } -func (s *Server) UpdatePartyInfo(ctx context.Context, in *PartyInfo) (*PartyInfoResponse, error) { +func (s *Server) UpdatePartyInfo(ctx context.Context, in *protofiles.PartyInfo) (*protofiles.PartyInfoResponse, error) { recipients := make(map[[nacl.KeySize]byte]string) for url, key := range in.Recipients{ var as [32]byte @@ -93,21 +94,22 @@ func (s *Server) UpdatePartyInfo(ctx context.Context, in *PartyInfo) (*PartyInfo } s.Enclave.UpdatePartyInfoGrpc(in.Url, recipients, in.Parties) encoded := s.Enclave.GetEncodedPartyInfoGrpc() - var decodedPartyInfo PartyInfoResponse + var decodedPartyInfo protofiles.PartyInfoResponse err := json.Unmarshal(encoded, &decodedPartyInfo) if err != nil{ log.Errorf("Unmarshalling failed with %v", err) } - return &PartyInfoResponse{Payload: decodedPartyInfo.Payload}, nil + return &protofiles.PartyInfoResponse{Payload: decodedPartyInfo.Payload}, nil } -func (s *Server) Push(ctx context.Context, in *PushPayload) (*PartyInfoResponse, error){ - var sender *[nacl.KeySize]byte - var nonce, recipientNonce *[nacl.NonceSize]byte - copy(sender[:], in.Ep.Sender) - copy(nonce[:], in.Ep.Nonce) - copy(recipientNonce[:], in.Ep.ReciepientNonce) +func (s *Server) Push(ctx context.Context, in *protofiles.PushPayload) (*protofiles.PartyInfoResponse, error) { + sender := new([nacl.KeySize]byte) + nonce := new([nacl.NonceSize]byte) + recipientNonce := new([nacl.NonceSize]byte) + copy((*sender)[:], in.Ep.Sender) + copy((*nonce)[:], in.Ep.Nonce) + copy((*recipientNonce)[:], in.Ep.ReciepientNonce) encyptedPayload := api.EncryptedPayload{ Sender:sender, @@ -122,7 +124,7 @@ func (s *Server) Push(ctx context.Context, in *PushPayload) (*PartyInfoResponse, log.Fatalf("Unable to store payload, error: %s\n", err) } - return &PartyInfoResponse{Payload: digestHash}, nil + return &protofiles.PartyInfoResponse{Payload: digestHash}, nil } func decodeErrorGRPC(name string, value string, err error) { diff --git a/server/server_test.go b/server/server_test.go index 64ac276..518ee05 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -12,6 +12,7 @@ import ( "github.com/kevinburke/nacl" "github.com/blk-io/crux/enclave" "github.com/blk-io/crux/storage" + "github.com/blk-io/crux/protofiles" "golang.org/x/net/context" "google.golang.org/grpc" "path" @@ -134,7 +135,7 @@ func TestSend(t *testing.T) { } func TestGRPCSend(t *testing.T) { - sendReqs := []SendRequest{ + sendReqs := []protofiles.SendRequest{ { Payload: payload, From: sender, @@ -148,7 +149,7 @@ func TestGRPCSend(t *testing.T) { Payload: payload, }, } - expected := SendResponse{Key: payload} + expected := protofiles.SendResponse{Key: payload} freePort, err := GetFreePort() if err != nil { @@ -162,14 +163,14 @@ func TestGRPCSend(t *testing.T) { t.Fatalf("Connection to gRPC server failed with error %s", err) } defer conn.Close() - c := NewClientClient(conn) + c := protofiles.NewClientClient(conn) for _, sendReq := range sendReqs { resp, err:= c.Send(context.Background(), &sendReq) if err != nil { t.Fatalf("gRPC send failed with %s", err) } - response := SendResponse{Key:resp.Key} + response := protofiles.SendResponse{Key:resp.Key} if !reflect.DeepEqual(response, expected) { t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) } @@ -208,13 +209,13 @@ func TestReceive(t *testing.T) { } func TestGRPCReceive(t *testing.T) { - receiveReqs := []ReceiveRequest{ + receiveReqs := []protofiles.ReceiveRequest{ { Key: payload, To: receiver, }, } - expected := ReceiveResponse{Payload: payload} + expected := protofiles.ReceiveResponse{Payload: payload} freePort, err := GetFreePort() if err != nil { log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) @@ -226,14 +227,14 @@ func TestGRPCReceive(t *testing.T) { t.Fatalf("Connection to gRPC server failed with error %s", err) } defer conn.Close() - c := NewClientClient(conn) + c := protofiles.NewClientClient(conn) for _, receiveReq := range receiveReqs { resp, err:= c.Receive(context.Background(), &receiveReq) if err != nil { t.Fatalf("gRPC receive failed with %s", err) } - response := ReceiveResponse{Payload:resp.Payload} + response := protofiles.ReceiveResponse{Payload:resp.Payload} if !reflect.DeepEqual(response, expected) { t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) } @@ -472,7 +473,7 @@ func TestPartyInfo(t *testing.T) { api.InitPartyInfo( "http://localhost:8000", []string{"http://localhost:8001"}, - http.DefaultClient), + http.DefaultClient, ""), } for _, pi := range partyInfos { @@ -510,7 +511,7 @@ func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { func InitgRPCServer(t *testing.T, grpc bool, port int) (string) { ipcPath, err := ioutil.TempDir("", "TestInitIpc") - tm, err := Init(&MockEnclave{}, port, ipcPath, grpc, false, "", "") + tm, err := Init(&MockEnclave{}, port, ipcPath, grpc, -1,false, "", "") if err != nil { t.Errorf("Error starting server: %v\n", err) @@ -547,14 +548,14 @@ func TestInit(t *testing.T) { key, http.DefaultClient) - enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient) + enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient, "") ipcPath, err := ioutil.TempDir("", "TestInitIpc") if err != nil { t.Error(err) } certFile, keyFile := "../enclave/testdata/cert/server.crt", "../enclave/testdata/cert/server.key" - tm, err := Init(enc, 9001, ipcPath, false, true, certFile, keyFile) + tm, err := Init(enc, 9001, ipcPath, false, -1, true, certFile, keyFile) if err != nil { t.Errorf("Error starting server: %v\n", err) } From 137aeca649703465d492601e1debf19ecbd4da92 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Fri, 20 Jul 2018 14:33:01 +0100 Subject: [PATCH 51/85] et gRPC flag default to true Signed-off-by: Puneetha --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 58d4cfc..ea4b2fa 100644 --- a/config/config.go +++ b/config/config.go @@ -56,7 +56,7 @@ func InitFlags() { flag.Int(Verbosity, 1, "Verbosity level of logs") flag.String(AlwaysSendTo, "", "List of public keys for nodes to send all transactions too") - flag.Bool(UseGRPC, false, "Use gRPC server") + flag.Bool(UseGRPC, true, "Use gRPC server") flag.Bool(Tls, false, "Use TLS to secure HTTP communications") flag.String(TlsServerCert, "", "The server certificate to be used") flag.String(TlsServerKey, "", "The server private key") From 0dfb72b56c28dbb69cad8f86f2dd13bd32854023 Mon Sep 17 00:00:00 2001 From: Puneetha Date: Fri, 20 Jul 2018 15:06:14 +0100 Subject: [PATCH 52/85] Change gRPC variable(api & enclave) from string to bool. Signed-off-by: Puneetha --- api/internal.go | 8 ++++---- crux.go | 10 +++------- enclave/enclave.go | 6 +++--- enclave/enclave_test.go | 6 +++--- server/server.go | 1 - server/server_test.go | 4 ++-- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/api/internal.go b/api/internal.go index 893b62c..c96a3ea 100644 --- a/api/internal.go +++ b/api/internal.go @@ -35,7 +35,7 @@ type PartyInfo struct { recipients map[[nacl.KeySize]byte]string // public key -> URL parties map[string]bool // Node (or party) URLs client utils.HttpClient - grpc string + grpc bool } // GetRecipient retrieves the URL associated with the provided recipient. @@ -49,7 +49,7 @@ func (s *PartyInfo) GetAllValues() (string, map[[nacl.KeySize]byte]string, map[s } // InitPartyInfo initializes a new PartyInfo store. -func InitPartyInfo(rawUrl string, otherNodes []string, client utils.HttpClient, grpc string) PartyInfo { +func InitPartyInfo(rawUrl string, otherNodes []string, client utils.HttpClient, grpc bool) PartyInfo { parties := make(map[string]bool) for _, node := range otherNodes { parties[node] = true @@ -134,7 +134,7 @@ func (s *PartyInfo) GetPartyInfoGrpc() { // GetPartyInfo requests PartyInfo data from all remote nodes this node is aware of. The data // provided in each response is applied to this node. func (s *PartyInfo) GetPartyInfo() { - if s.grpc != "" { + if s.grpc { s.GetPartyInfoGrpc() return } @@ -216,7 +216,7 @@ func (s *PartyInfo) updatePartyInfo(resp *http.Response, rawUrl string) error { } func (s *PartyInfo) getEncoded(encodedPartyInfo []byte) []byte{ - if s.grpc != "" { + if s.grpc { recipients := make(map[string][]byte) for key, url := range s.recipients{ recipients[url] = key[:] diff --git a/crux.go b/crux.go index f571360..1a883eb 100644 --- a/crux.go +++ b/crux.go @@ -12,7 +12,6 @@ import ( "github.com/blk-io/crux/storage" log "github.com/sirupsen/logrus" "time" - "fmt" ) func main() { @@ -91,11 +90,8 @@ func main() { Timeout: time.Second * 10, } grpc := config.GetBool(config.UseGRPC) - grpcIpcPath := "" - if grpc { - grpcIpcPath = fmt.Sprintf("localhost:%d", port) - } - pi := api.InitPartyInfo(url, otherNodes, httpClient, grpcIpcPath) + + pi := api.InitPartyInfo(url, otherNodes, httpClient, grpc) privKeyFiles := config.GetStringSlice(config.PrivateKeys) pubKeyFiles := config.GetStringSlice(config.PublicKeys) @@ -116,7 +112,7 @@ func main() { pubKeyFiles[i] = path.Join(workDir, keyFile) } - enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient, grpcIpcPath) + enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient, grpc) pi.RegisterPublicKeys(enc.PubKeys) diff --git a/enclave/enclave.go b/enclave/enclave.go index 198fd82..99b3606 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -30,7 +30,7 @@ type SecureEnclave struct { PartyInfo api.PartyInfo // Details of all other nodes (or parties) on the network keyCache map[nacl.Key]map[nacl.Key]nacl.Key // Maps sender -> recipient -> shared key client utils.HttpClient // The underlying HTTP client used to propagate requests - grpc string + grpc bool } // Init creates a new instance of the SecureEnclave. @@ -38,7 +38,7 @@ func Init( db storage.DataStore, pubKeyFiles, privKeyFiles []string, pi api.PartyInfo, - client utils.HttpClient, grpc string) *SecureEnclave { + client utils.HttpClient, grpc bool) *SecureEnclave { // Key format: // BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= @@ -235,7 +235,7 @@ func (s *SecureEnclave) publishPayload(epl api.EncryptedPayload, recipient []byt if url, ok := s.PartyInfo.GetRecipient(key); ok { encoded := api.EncodePayloadWithRecipients(epl, [][]byte{}) - if s.grpc != "" { + if s.grpc { api.PushGrpc(encoded, url, epl) } else { api.Push(encoded, url, s.client) diff --git a/enclave/enclave_test.go b/enclave/enclave_test.go index e9d5ed5..12c8357 100644 --- a/enclave/enclave_test.go +++ b/enclave/enclave_test.go @@ -58,7 +58,7 @@ func initEnclave( []string{"testdata/key.pub"}, []string{"testdata/key"}, pi, - client, "") + client, false) } func initDefaultEnclave(t *testing.T, @@ -68,7 +68,7 @@ func initDefaultEnclave(t *testing.T, client = &MockClient{} pi := api.InitPartyInfo( "http://localhost:8000", - []string{"http://localhost:8001"}, client, "") + []string{"http://localhost:8001"}, client, false) return initEnclave(t, dbPath, pi, client) } @@ -146,7 +146,7 @@ func TestStoreAndRetrieve(t *testing.T) { []string{"testdata/rcpt1.pub"}, []string{"testdata/rcpt1"}, pi, - client, "") + client, false) var digest2 []byte digest2, err = enc2.StorePayload(propagatedPl) diff --git a/server/server.go b/server/server.go index 7c69429..8684be3 100644 --- a/server/server.go +++ b/server/server.go @@ -78,7 +78,6 @@ func requestLogger(handler http.Handler) http.Handler { func Init(enc Enclave, port int, ipcPath string, grpc bool, grpcJsonPort int, tls bool, certFile, keyFile string) (TransactionManager, error) { tm := TransactionManager{Enclave : enc} var err error -//err = tm.startRestServer(port) if grpc == true { err = tm.startRpcServer(port, grpcJsonPort, ipcPath, tls, certFile, keyFile) diff --git a/server/server_test.go b/server/server_test.go index 518ee05..ac6c330 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -473,7 +473,7 @@ func TestPartyInfo(t *testing.T) { api.InitPartyInfo( "http://localhost:8000", []string{"http://localhost:8001"}, - http.DefaultClient, ""), + http.DefaultClient, false), } for _, pi := range partyInfos { @@ -548,7 +548,7 @@ func TestInit(t *testing.T) { key, http.DefaultClient) - enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient, "") + enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient, false) ipcPath, err := ioutil.TempDir("", "TestInitIpc") if err != nil { From c9bc8aac92578de0b8ee48668bf312167285f91a Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Tue, 24 Jul 2018 18:41:10 +0100 Subject: [PATCH 53/85] Use chimera-api for protofiles (#18) * Use chimera-api for protofiles Signed-off-by: Puneetha * Fix testcases for protofiles in Chimera-API Signed-off-by: Puneetha --- Gopkg.lock | 15 ++++++++++++- Makefile | 26 +--------------------- api/internal.go | 2 +- protofiles/grpc.proto | 40 ---------------------------------- protofiles/messages.proto | 45 --------------------------------------- server/proto_server.go | 2 +- server/server_handler.go | 2 +- server/server_test.go | 2 +- 8 files changed, 19 insertions(+), 115 deletions(-) delete mode 100644 protofiles/grpc.proto delete mode 100644 protofiles/messages.proto diff --git a/Gopkg.lock b/Gopkg.lock index 9d19580..f9a7509 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,14 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + branch = "master" + digest = "1:62e073e81a669097d786be83fee428b0584af9e521b3543063b6f7c599b35517" + name = "github.com/blk-io/chimera-api" + packages = ["protofiles"] + pruneopts = "T" + revision = "deee5615cef7315a5206205e046a815cdaf29ba2" + [[projects]] digest = "1:1834c1bcecde8bc847628b7c316b8258315749995f0c3a398b1370db8c98a1b3" name = "github.com/fsnotify/fsnotify" @@ -15,6 +23,7 @@ packages = [ "jsonpb", "proto", + "protoc-gen-go/descriptor", "ptypes", "ptypes/any", "ptypes/duration", @@ -251,7 +260,10 @@ branch = "master" digest = "1:5ecad12ed407b601c1cdf780e84b095d57a59343589a12a385d3828ecb2cc0b2" name = "google.golang.org/genproto" - packages = ["googleapis/rpc/status"] + packages = [ + "googleapis/api/annotations", + "googleapis/rpc/status", + ] pruneopts = "T" revision = "694d95ba50e67b2e363f3483057db5d4910c18f9" @@ -300,6 +312,7 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ + "github.com/blk-io/chimera-api/protofiles", "github.com/grpc-ecosystem/grpc-gateway/runtime", "github.com/jsimonetti/berkeleydb", "github.com/kevinburke/nacl", diff --git a/Makefile b/Makefile index 70713e8..3b89b73 100644 --- a/Makefile +++ b/Makefile @@ -11,23 +11,8 @@ IGNORED_PACKAGES := /vendor/ .PHONY: all all: build -protofiles: proto grpc - -proto: - $Q protoc -I protofiles/ \ - -I $(GOPATH)/../vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ - --go_out=plugins=grpc:protofiles \ - protofiles/*.proto - -grpc: - $Q protoc -I protofiles/ \ - -I $(GOPATH)/../vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ - --grpc-gateway_out=logtostderr=true:protofiles \ - protofiles/grpc.proto - - .PHONY: build -build: protofiles .GOPATH/.ok +build: .GOPATH/.ok $Q go install $(if $V,-v) $(VERSION_FLAGS) $(IMPORT_PATH) ### Code not in the repository root? Another binary? Add to the path like this. @@ -41,7 +26,6 @@ build: protofiles .GOPATH/.ok clean: $Q rm -rf bin .GOPATH - $Q rm -f server/*\.pb* test: .GOPATH/.ok $Q go test $(if $V,-v) -i -race $(allpackages) # install -race libs to speed up next run @@ -90,17 +74,9 @@ setup: clean .GOPATH/.ok echo "/.GOPATH" >> .gitignore; \ echo "/bin" >> .gitignore; \ fi - $Q curl -OL https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip - $Q unzip protoc-3.5.1-linux-x86_64.zip -d protoc3 - $Q mv protoc3/bin/* .GOPATH/bin/ - $Q mv protoc3/include/* .GOPATH/include/ - $Q rm -r protoc3 - $Q rm protoc-3.5.1-linux-x86_64.zip go get -u github.com/golang/dep/cmd/dep go get -u golang.org/x/tools/cmd/goimports go get -u github.com/wadey/gocovmerge - go get -u github.com/golang/protobuf/protoc-gen-go - go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway @test -f Gopkg.toml || \ (cd $(CURDIR)/.GOPATH/src/$(IMPORT_PATH) && ./bin/dep init) (cd $(CURDIR)/.GOPATH/src/$(IMPORT_PATH) && ./bin/dep ensure) diff --git a/api/internal.go b/api/internal.go index c96a3ea..d58a0db 100644 --- a/api/internal.go +++ b/api/internal.go @@ -13,7 +13,7 @@ import ( "encoding/json" "net/http/httputil" "fmt" - "github.com/blk-io/crux/protofiles" + "github.com/blk-io/chimera-api/protofiles" "golang.org/x/net/context" "google.golang.org/grpc" "net/url" diff --git a/protofiles/grpc.proto b/protofiles/grpc.proto deleted file mode 100644 index 56545eb..0000000 --- a/protofiles/grpc.proto +++ /dev/null @@ -1,40 +0,0 @@ -syntax = "proto3"; -package protofiles; - -import "google/api/annotations.proto"; -import "messages.proto"; - - -service Client { - rpc Version(ApiVersion) returns (ApiVersion) { - option (google.api.http) = { - post: "/version" - body: "*" - }; - } - - rpc Upcheck(UpCheckResponse) returns (UpCheckResponse) { - option (google.api.http) = { - post: "/upcheck" - body: "*" - }; - } - - rpc Send(SendRequest) returns (SendResponse); - - rpc Receive(ReceiveRequest) returns (ReceiveResponse); - - rpc UpdatePartyInfo(PartyInfo) returns (PartyInfoResponse) { - option (google.api.http) = { - post: "/partyinfo" - body: "*" - }; - } - - rpc Push(PushPayload) returns (PartyInfoResponse) { - option (google.api.http) = { - post: "/push" - body: "*" - }; - } -} diff --git a/protofiles/messages.proto b/protofiles/messages.proto deleted file mode 100644 index a9354d8..0000000 --- a/protofiles/messages.proto +++ /dev/null @@ -1,45 +0,0 @@ -syntax = "proto3"; -package protofiles; - -import "google/api/annotations.proto"; - -message ApiVersion{ - string version = 1; -} -message UpCheckResponse{ - string message = 1; -} -message SendRequest{ - bytes payload = 1; - string from = 2; - repeated string to = 3; -} -message SendResponse{ - bytes key = 1; -} -message ReceiveRequest{ - bytes key = 1; - string to = 2; -} -message ReceiveResponse{ - bytes payload = 1; -} -message PartyInfo{ - string url = 1; - map recipients = 2; - map parties = 3; -} -message PartyInfoResponse{ - bytes payload = 1; -} -message EncryptedPayload{ - bytes sender = 1; - bytes cipherText = 2; - bytes nonce = 3; - repeated bytes reciepientBoxes = 4; - bytes reciepientNonce = 5; -} -message PushPayload{ - EncryptedPayload ep = 1; - bytes encoded = 2; -} diff --git a/server/proto_server.go b/server/proto_server.go index 967dae3..a6076dd 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -10,7 +10,7 @@ import ( "github.com/blk-io/crux/utils" "net" "google.golang.org/grpc/credentials" - "github.com/blk-io/crux/protofiles" + "github.com/blk-io/chimera-api/protofiles" ) func (tm *TransactionManager) startRpcServer(port int, grpcJsonPort int, ipcPath string, tls bool, certFile, keyFile string) error { diff --git a/server/server_handler.go b/server/server_handler.go index 1f66e29..317561b 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -9,7 +9,7 @@ import ( "github.com/kevinburke/nacl" "encoding/json" "github.com/blk-io/crux/api" - "github.com/blk-io/crux/protofiles" + "github.com/blk-io/chimera-api/protofiles" ) type Server struct { diff --git a/server/server_test.go b/server/server_test.go index ac6c330..ae49f96 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -12,7 +12,7 @@ import ( "github.com/kevinburke/nacl" "github.com/blk-io/crux/enclave" "github.com/blk-io/crux/storage" - "github.com/blk-io/crux/protofiles" + "github.com/blk-io/chimera-api/protofiles" "golang.org/x/net/context" "google.golang.org/grpc" "path" From f909becd6719614423bf38c757cb317485c186a9 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Thu, 26 Jul 2018 13:02:24 +0100 Subject: [PATCH 54/85] Update Crux to accomodate Chimera API changes (#20) Signed-off-by: Puneetha --- api/internal.go | 14 +++++++------- server/proto_server.go | 12 ++++++------ server/server_handler.go | 32 ++++++++++++++++---------------- server/server_test.go | 18 +++++++++--------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/api/internal.go b/api/internal.go index d58a0db..6d2b349 100644 --- a/api/internal.go +++ b/api/internal.go @@ -13,7 +13,7 @@ import ( "encoding/json" "net/http/httputil" "fmt" - "github.com/blk-io/chimera-api/protofiles" + "github.com/blk-io/chimera-api/chimera" "golang.org/x/net/context" "google.golang.org/grpc" "net/url" @@ -114,11 +114,11 @@ func (s *PartyInfo) GetPartyInfoGrpc() { log.Fatalf("Connection to gRPC server failed with error %s", err) } defer conn.Close() - cli := protofiles.NewClientClient(conn) + cli := chimera.NewClientClient(conn) if cli == nil{ log.Fatalf("Client is not intialised") } - party := protofiles.PartyInfo{Url:rawUrl, Recipients:recipients, Parties:s.parties} + party := chimera.PartyInfo{Url:rawUrl, Recipients:recipients, Parties:s.parties} partyInfoResp, err := cli.UpdatePartyInfo(context.Background(), &party) if err != nil { @@ -191,7 +191,7 @@ func (s *PartyInfo) GetPartyInfo() { } } -func (s *PartyInfo) updatePartyInfoGrpc(partyInfoReq protofiles.PartyInfoResponse, rawUrl string) error { +func (s *PartyInfo) updatePartyInfoGrpc(partyInfoReq chimera.PartyInfoResponse, rawUrl string) error { pi, err := DecodePartyInfo(partyInfoReq.Payload) if err != nil { log.WithField("url", rawUrl).Errorf( @@ -304,7 +304,7 @@ func PushGrpc(encoded []byte, path string, epl EncryptedPayload) error { log.Fatalf("Connection to gRPC server failed with error %s", err) } defer conn.Close() - cli := protofiles.NewClientClient(conn) + cli := chimera.NewClientClient(conn) if cli == nil{ log.Fatalf("Client is not intialised") } @@ -316,14 +316,14 @@ func PushGrpc(encoded []byte, path string, epl EncryptedPayload) error { copy(sender[:], (*epl.Sender)[:]) copy(nonce[:], (*epl.Nonce)[:]) copy(recipientNonce[:], (*epl.RecipientNonce)[:]) - encrypt := protofiles.EncryptedPayload{ + encrypt := chimera.EncryptedPayload{ Sender:sender[:], CipherText:epl.CipherText, Nonce:nonce[:], ReciepientNonce:recipientNonce[:], ReciepientBoxes:epl.RecipientBoxes, } - pushPayload := protofiles.PushPayload{Ep:&encrypt, Encoded:encoded} + pushPayload := chimera.PushPayload{Ep:&encrypt, Encoded:encoded} _, err = cli.Push(context.Background(), &pushPayload) if err != nil{ log.Errorf("Push failed with %s", err) diff --git a/server/proto_server.go b/server/proto_server.go index a6076dd..fc7fd99 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -10,7 +10,7 @@ import ( "github.com/blk-io/crux/utils" "net" "google.golang.org/grpc/credentials" - "github.com/blk-io/chimera-api/protofiles" + "github.com/blk-io/chimera-api/chimera" ) func (tm *TransactionManager) startRpcServer(port int, grpcJsonPort int, ipcPath string, tls bool, certFile, keyFile string) error { @@ -20,7 +20,7 @@ func (tm *TransactionManager) startRpcServer(port int, grpcJsonPort int, ipcPath } s := Server{Enclave : tm.Enclave} grpcServer := grpc.NewServer() - protofiles.RegisterClientServer(grpcServer, &s) + chimera.RegisterClientServer(grpcServer, &s) go func() { log.Fatal(grpcServer.Serve(lis)) }() @@ -55,7 +55,7 @@ func (tm *TransactionManager) startJsonServer(port int, grpcJsonPort int) error defer cancel() mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithInsecure()} - err := protofiles.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", "localhost", port), opts) + err := chimera.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", "localhost", port), opts) if err != nil { return fmt.Errorf("could not register service: %s", err) } @@ -75,7 +75,7 @@ func (tm *TransactionManager) startRestServer(port int) error { } s := Server{Enclave : tm.Enclave} grpcServer := grpc.NewServer() - protofiles.RegisterClientServer(grpcServer, &s) + chimera.RegisterClientServer(grpcServer, &s) go func() { log.Fatal(grpcServer.Serve(lis)) }() @@ -89,7 +89,7 @@ func (tm *TransactionManager) startJsonServerTLS(port int, grpcJsonPort int, cer defer cancel() mux := runtime.NewServeMux() creds, err := credentials.NewServerTLSFromFile(certFile, keyFile) - err = protofiles.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", "localhost", port), []grpc.DialOption{grpc.WithTransportCredentials(creds)}) + err = chimera.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", "localhost", port), []grpc.DialOption{grpc.WithTransportCredentials(creds)}) if err != nil { log.Fatalf("could not register service Ping: %s", err) return err @@ -112,7 +112,7 @@ func (tm *TransactionManager) startRestServerTLS(port int, certFile, keyFile,ca log.Fatalf("failed to load credentials : %v", err) } grpcServer := grpc.NewServer(opts...) - protofiles.RegisterClientServer(grpcServer, &s) + chimera.RegisterClientServer(grpcServer, &s) go func() { log.Fatal(grpcServer.Serve(lis)) }() diff --git a/server/server_handler.go b/server/server_handler.go index 317561b..de5e3bd 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -9,27 +9,27 @@ import ( "github.com/kevinburke/nacl" "encoding/json" "github.com/blk-io/crux/api" - "github.com/blk-io/chimera-api/protofiles" + "github.com/blk-io/chimera-api/chimera" ) type Server struct { Enclave Enclave } -func (s *Server) Version(ctx context.Context, in *protofiles.ApiVersion) (*protofiles.ApiVersion, error) { - return &protofiles.ApiVersion{Version:apiVersion}, nil +func (s *Server) Version(ctx context.Context, in *chimera.ApiVersion) (*chimera.ApiVersion, error) { + return &chimera.ApiVersion{Version:apiVersion}, nil } -func (s *Server) Upcheck(ctx context.Context, in *protofiles.UpCheckResponse) (*protofiles.UpCheckResponse, error) { - return &protofiles.UpCheckResponse{Message:upCheckResponse}, nil +func (s *Server) Upcheck(ctx context.Context, in *chimera.UpCheckResponse) (*chimera.UpCheckResponse, error) { + return &chimera.UpCheckResponse{Message:upCheckResponse}, nil } -func (s *Server) Send(ctx context.Context, in *protofiles.SendRequest) (*protofiles.SendResponse, error) { +func (s *Server) Send(ctx context.Context, in *chimera.SendRequest) (*chimera.SendResponse, error) { key, err := s.processSend(in.GetFrom(), in.GetTo(), &in.Payload) - var sendResp protofiles.SendResponse + var sendResp chimera.SendResponse if err != nil { log.Error(err) } else { - sendResp = protofiles.SendResponse{Key: key} + sendResp = chimera.SendResponse{Key: key} } return &sendResp, err } @@ -61,13 +61,13 @@ func (s *Server) processSend(b64from string, b64recipients []string, payload *[] return s.Enclave.Store(payload, sender, recipients) } -func (s *Server) Receive(ctx context.Context, in *protofiles.ReceiveRequest) (*protofiles.ReceiveResponse, error) { +func (s *Server) Receive(ctx context.Context, in *chimera.ReceiveRequest) (*chimera.ReceiveResponse, error) { payload, err := s.processReceive(in.Key, in.To) - var receiveResp protofiles.ReceiveResponse + var receiveResp chimera.ReceiveResponse if err != nil { log.Error(err) } else { - receiveResp = protofiles.ReceiveResponse{Payload: payload} + receiveResp = chimera.ReceiveResponse{Payload: payload} } return &receiveResp, err } @@ -85,7 +85,7 @@ func (s *Server) processReceive(b64Key []byte, b64To string) ([]byte, error) { } } -func (s *Server) UpdatePartyInfo(ctx context.Context, in *protofiles.PartyInfo) (*protofiles.PartyInfoResponse, error) { +func (s *Server) UpdatePartyInfo(ctx context.Context, in *chimera.PartyInfo) (*chimera.PartyInfoResponse, error) { recipients := make(map[[nacl.KeySize]byte]string) for url, key := range in.Recipients{ var as [32]byte @@ -94,16 +94,16 @@ func (s *Server) UpdatePartyInfo(ctx context.Context, in *protofiles.PartyInfo) } s.Enclave.UpdatePartyInfoGrpc(in.Url, recipients, in.Parties) encoded := s.Enclave.GetEncodedPartyInfoGrpc() - var decodedPartyInfo protofiles.PartyInfoResponse + var decodedPartyInfo chimera.PartyInfoResponse err := json.Unmarshal(encoded, &decodedPartyInfo) if err != nil{ log.Errorf("Unmarshalling failed with %v", err) } - return &protofiles.PartyInfoResponse{Payload: decodedPartyInfo.Payload}, nil + return &chimera.PartyInfoResponse{Payload: decodedPartyInfo.Payload}, nil } -func (s *Server) Push(ctx context.Context, in *protofiles.PushPayload) (*protofiles.PartyInfoResponse, error) { +func (s *Server) Push(ctx context.Context, in *chimera.PushPayload) (*chimera.PartyInfoResponse, error) { sender := new([nacl.KeySize]byte) nonce := new([nacl.NonceSize]byte) recipientNonce := new([nacl.NonceSize]byte) @@ -124,7 +124,7 @@ func (s *Server) Push(ctx context.Context, in *protofiles.PushPayload) (*protofi log.Fatalf("Unable to store payload, error: %s\n", err) } - return &protofiles.PartyInfoResponse{Payload: digestHash}, nil + return &chimera.PartyInfoResponse{Payload: digestHash}, nil } func decodeErrorGRPC(name string, value string, err error) { diff --git a/server/server_test.go b/server/server_test.go index ae49f96..8324bfd 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -12,7 +12,7 @@ import ( "github.com/kevinburke/nacl" "github.com/blk-io/crux/enclave" "github.com/blk-io/crux/storage" - "github.com/blk-io/chimera-api/protofiles" + "github.com/blk-io/chimera-api/chimera" "golang.org/x/net/context" "google.golang.org/grpc" "path" @@ -135,7 +135,7 @@ func TestSend(t *testing.T) { } func TestGRPCSend(t *testing.T) { - sendReqs := []protofiles.SendRequest{ + sendReqs := []chimera.SendRequest{ { Payload: payload, From: sender, @@ -149,7 +149,7 @@ func TestGRPCSend(t *testing.T) { Payload: payload, }, } - expected := protofiles.SendResponse{Key: payload} + expected := chimera.SendResponse{Key: payload} freePort, err := GetFreePort() if err != nil { @@ -163,14 +163,14 @@ func TestGRPCSend(t *testing.T) { t.Fatalf("Connection to gRPC server failed with error %s", err) } defer conn.Close() - c := protofiles.NewClientClient(conn) + c := chimera.NewClientClient(conn) for _, sendReq := range sendReqs { resp, err:= c.Send(context.Background(), &sendReq) if err != nil { t.Fatalf("gRPC send failed with %s", err) } - response := protofiles.SendResponse{Key:resp.Key} + response := chimera.SendResponse{Key:resp.Key} if !reflect.DeepEqual(response, expected) { t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) } @@ -209,13 +209,13 @@ func TestReceive(t *testing.T) { } func TestGRPCReceive(t *testing.T) { - receiveReqs := []protofiles.ReceiveRequest{ + receiveReqs := []chimera.ReceiveRequest{ { Key: payload, To: receiver, }, } - expected := protofiles.ReceiveResponse{Payload: payload} + expected := chimera.ReceiveResponse{Payload: payload} freePort, err := GetFreePort() if err != nil { log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) @@ -227,14 +227,14 @@ func TestGRPCReceive(t *testing.T) { t.Fatalf("Connection to gRPC server failed with error %s", err) } defer conn.Close() - c := protofiles.NewClientClient(conn) + c := chimera.NewClientClient(conn) for _, receiveReq := range receiveReqs { resp, err:= c.Receive(context.Background(), &receiveReq) if err != nil { t.Fatalf("gRPC receive failed with %s", err) } - response := protofiles.ReceiveResponse{Payload:resp.Payload} + response := chimera.ReceiveResponse{Payload:resp.Payload} if !reflect.DeepEqual(response, expected) { t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) } From 9b3c884e0a806efb817ee017bf982e4291df1e92 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Tue, 31 Jul 2018 17:38:36 +0100 Subject: [PATCH 55/85] Fix seg fault while updating partyinfo when othernodes is not up yet. (#22) Signed-off-by: Puneetha --- api/internal.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/internal.go b/api/internal.go index 6d2b349..7a425e3 100644 --- a/api/internal.go +++ b/api/internal.go @@ -111,18 +111,21 @@ func (s *PartyInfo) GetPartyInfoGrpc() { url, err := completeUrl.Parse(rawUrl) conn, err := grpc.Dial(url.Host, grpc.WithInsecure()) if err != nil { - log.Fatalf("Connection to gRPC server failed with error %s", err) + log.Errorf("Connection to gRPC server failed with error %s", err) + continue } defer conn.Close() cli := chimera.NewClientClient(conn) if cli == nil{ - log.Fatalf("Client is not intialised") + log.Errorf("Client is not intialised") + continue } party := chimera.PartyInfo{Url:rawUrl, Recipients:recipients, Parties:s.parties} partyInfoResp, err := cli.UpdatePartyInfo(context.Background(), &party) if err != nil { log.Errorf("Error in updating party info %s", err) + continue } err = s.updatePartyInfoGrpc(*partyInfoResp, s.url) if err != nil { From 1a124b3b021c8b2161f85962641cecc4299a40e7 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Wed, 8 Aug 2018 10:20:22 +0100 Subject: [PATCH 56/85] Update the gRPC address and add logs when connected to other nodes (#23) --- api/internal.go | 2 ++ server/proto_server.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/internal.go b/api/internal.go index 7a425e3..7646655 100644 --- a/api/internal.go +++ b/api/internal.go @@ -126,6 +126,8 @@ func (s *PartyInfo) GetPartyInfoGrpc() { if err != nil { log.Errorf("Error in updating party info %s", err) continue + } else { + log.Printf("Connected to the other node %s", rawUrl) } err = s.updatePartyInfoGrpc(*partyInfoResp, s.url) if err != nil { diff --git a/server/proto_server.go b/server/proto_server.go index fc7fd99..d8f6fb2 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -68,7 +68,7 @@ func (tm *TransactionManager) startJsonServer(port int, grpcJsonPort int) error } func (tm *TransactionManager) startRestServer(port int) error { - grpcAddress := fmt.Sprintf("%s:%d", "localhost", port) + grpcAddress := fmt.Sprintf(":%d", port) lis, err := net.Listen("tcp", grpcAddress) if err != nil { panic(err) From 0921494013f43a070b648b61d0a8ef33ac6eac55 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Thu, 9 Aug 2018 13:07:48 +0100 Subject: [PATCH 57/85] Add Docker files (#24) * Update the gRPC address and add logs when connected to other nodes * Add docker files to create crux & quorum-crux containers * Update Readme to include references to docker build --- README.md | 11 +- docker/crux/Dockerfile | 28 +++++ docker/crux/docker-compose.yml | 29 +++++ docker/crux/start.sh | 6 ++ docker/quorum-crux/Dockerfile | 46 ++++++++ docker/quorum-crux/bootstrap.sh | 20 ++++ docker/quorum-crux/crux-start.sh | 21 ++++ docker/quorum-crux/docker-compose.yaml | 101 ++++++++++++++++++ docker/quorum-crux/istanbul-genesis.json | 50 +++++++++ docker/quorum-crux/istanbul-init.sh | 14 +++ docker/quorum-crux/istanbul-start.sh | 15 +++ docker/quorum-crux/passwords.txt | 0 docker/quorum-crux/scripts/simpleContract.js | 22 ++++ .../quorum-crux/scripts/test_transaction.sh | 3 + docker/quorum-crux/start.sh | 6 ++ 15 files changed, 367 insertions(+), 5 deletions(-) create mode 100644 docker/crux/Dockerfile create mode 100644 docker/crux/docker-compose.yml create mode 100644 docker/crux/start.sh create mode 100644 docker/quorum-crux/Dockerfile create mode 100644 docker/quorum-crux/bootstrap.sh create mode 100644 docker/quorum-crux/crux-start.sh create mode 100644 docker/quorum-crux/docker-compose.yaml create mode 100644 docker/quorum-crux/istanbul-genesis.json create mode 100644 docker/quorum-crux/istanbul-init.sh create mode 100644 docker/quorum-crux/istanbul-start.sh create mode 100644 docker/quorum-crux/passwords.txt create mode 100644 docker/quorum-crux/scripts/simpleContract.js create mode 100644 docker/quorum-crux/scripts/test_transaction.sh create mode 100644 docker/quorum-crux/start.sh diff --git a/README.md b/README.md index 002f18b..4acf1ef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Crux -Quorum Slack +Quorum Slack Build Status Data privacy for Quorum. @@ -12,11 +12,12 @@ secure enclave component of [Quorum](https://github.com/jpmorganchase/quorum/), ## Getting started -The best way to start is to run the Crux -[7 Nodes Quorum example](https://github.com/blk-io/quorum-examples). This is a fork of the JP -Morgan version. +The best way to start is to run the Quorum-Crux Docker image is +[4 Nodes Quorum example](https://github.com/blk-io/crux/docker/quorum-crux). This pulls a fork of Quorum and uses Crux as the secure enclave. You can also run the [7 Nodes Quorum example](https://github.com/blk-io/quorum-examples) which is an updated version of JP Morgan's Quorum 7 Nodes example to use Crux as the secure enclave. -If you you'd prefer to run just a client, you can build using the below instructions and run as per +[2 Crux nodes example](https://github.com/blk-io/crux/docker/crux) is simple docker image to just bring up 2 Crux nodes which communicate with each other. + +If you'd prefer to run just a client, you can build using the below instructions and run as per the below. ```bash diff --git a/docker/crux/Dockerfile b/docker/crux/Dockerfile new file mode 100644 index 0000000..fa36e15 --- /dev/null +++ b/docker/crux/Dockerfile @@ -0,0 +1,28 @@ +FROM alpine:3.8 + +RUN apk add --no-cache --update unzip db zlib wrk wget libsodium-dev go bash libpthread-stubs db-dev && \ + apk -X http://dl-cdn.alpinelinux.org/alpine/edge/testing add --no-cache leveldb && \ + apk --no-cache --update add build-base cmake boost-dev git + +ENV CRUX_PUB="" +ENV CRUX_PRIV="" +ENV OWN_URL="" +ENV OTHER_NODES="" +ENV PORT="" + +RUN git clone https://github.com/blk-io/crux.git + +WORKDIR /crux + +RUN make setup && \ + make build && \ + apk del sed make git cmake build-base gcc g++ musl-dev curl-dev boost-dev +# fails https://github.com/golang/go/issues/14481 +# RUN make test + +EXPOSE 9000 + +COPY start.sh start.sh +RUN chmod +x start.sh + +ENTRYPOINT ["./start.sh"] \ No newline at end of file diff --git a/docker/crux/docker-compose.yml b/docker/crux/docker-compose.yml new file mode 100644 index 0000000..b103196 --- /dev/null +++ b/docker/crux/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3.4" +services: + node1: + image: blk.io/quorum/crux + build: + context: . + container_name: crux1 + ports: + - 9001:9000 + environment: + - CRUX_PUB=BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= + - CRUX_PRIV={"data":{"bytes":"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA="},"type":"unlocked"} + - OTHER_NODES=http://node2:9000/ + - OWN_URL=node1 + - PORT=9000 + + node2: + image: blk.io/quorum/crux + build: + context: . + container_name: crux2 + ports: + - 9002:9000 + environment: + - CRUX_PUB=QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= + - CRUX_PRIV={"data":{"bytes":"nDFwJNHSiT1gNzKBy9WJvMhmYRkW3TzFUmPsNzR6oFk="},"type":"unlocked"} + - OTHER_NODES=http://node1:9000/ + - OWN_URL=node2 + - PORT=9000 diff --git a/docker/crux/start.sh b/docker/crux/start.sh new file mode 100644 index 0000000..98dc8f1 --- /dev/null +++ b/docker/crux/start.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +echo $CRUX_PUB >> key.pub +echo $CRUX_PRIV >> key.priv +CMD="./bin/crux --url=http://$OWN_URL:$PORT/ --port=$PORT --publickeys=key.pub --privatekeys=key.priv --othernodes=$OTHER_NODES --verbosity=3" +$CMD >> "crux.log" 2>&1 \ No newline at end of file diff --git a/docker/quorum-crux/Dockerfile b/docker/quorum-crux/Dockerfile new file mode 100644 index 0000000..1eac569 --- /dev/null +++ b/docker/quorum-crux/Dockerfile @@ -0,0 +1,46 @@ +FROM alpine:3.8 + +RUN apk add --no-cache --update unzip db zlib wrk wget libsodium-dev go bash libpthread-stubs db-dev && \ + apk -X http://dl-cdn.alpinelinux.org/alpine/edge/testing add --no-cache leveldb + +RUN \ + apk --no-cache --update add build-base cmake boost-dev git && \ + sed -i -E -e 's/include /include /' /usr/include/boost/asio/detail/socket_types.hpp && \ + git clone --depth 1 --recursive -b release https://github.com/ethereum/solidity && \ + cd /solidity && cmake -DCMAKE_BUILD_TYPE=Release -DTESTS=0 -DSTATIC_LINKING=1 && \ + cd /solidity && make solc && install -s solc/solc /usr/bin && \ + cd / && rm -rf solidity && \ + rm -rf /var/cache/apk/* + +ENV PORT="" +ENV NODE_KEY="" +ENV CRUX_PUB="" +ENV GETH_KEY="" +ENV OWN_URL="" +ENV CRUX_PRIV="" +ENV OTHER_NODES="" +ENV GETH_RPC_PORT="" +ENV GETH_PORT="" + +WORKDIR /quorum + +COPY bootstrap.sh bootstrap.sh +COPY istanbul-genesis.json istanbul-genesis.json +COPY passwords.txt passwords.txt +COPY istanbul-init.sh istanbul-init.sh +COPY crux-start.sh crux-start.sh +COPY istanbul-start.sh istanbul-start.sh +COPY start.sh start.sh +COPY scripts/simpleContract.js simpleContract.js +COPY scripts/test_transaction.sh test_transaction.sh + +RUN chmod +x start.sh crux-start.sh istanbul-start.sh istanbul-init.sh && \ + chmod +x test_transaction.sh && \ + chmod +x bootstrap.sh && \ + ./bootstrap.sh && \ + apk del sed make git cmake build-base gcc g++ musl-dev curl-dev boost-dev + +EXPOSE 9000 21000 22000 + +# Entrypoint for container +ENTRYPOINT ["./start.sh"] diff --git a/docker/quorum-crux/bootstrap.sh b/docker/quorum-crux/bootstrap.sh new file mode 100644 index 0000000..03e396a --- /dev/null +++ b/docker/quorum-crux/bootstrap.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -eu -o pipefail + +# make/install quorum +git clone https://github.com/ConsenSys/quorum.git +pushd quorum >/dev/null +git checkout tags/v2.0.2-grpc +make all +cp build/bin/geth /usr/local/bin +cp build/bin/bootnode /usr/local/bin +rm -r build +popd >/dev/null + +# make/install crux +git clone https://github.com/blk-io/crux.git +cd crux +git checkout tags/v1.0.0 +make setup && make +cp bin/crux /usr/local/bin +rm -r bin diff --git a/docker/quorum-crux/crux-start.sh b/docker/quorum-crux/crux-start.sh new file mode 100644 index 0000000..0aebb04 --- /dev/null +++ b/docker/quorum-crux/crux-start.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -u +set -e + +DDIR="qdata/c" +mkdir -p $DDIR +mkdir -p qdata/logs +echo $CRUX_PUB >> "$DDIR/tm.pub" +echo $CRUX_PRIV >> "$DDIR/tm.key" +rm -f "$DDIR/tm.ipc" +CMD="crux --url=http://$OWN_URL:$PORT/ --port=$PORT --workdir=$DDIR --socket=tm.ipc --publickeys=tm.pub --privatekeys=tm.key --othernodes=$OTHER_NODES --verbosity=3" +$CMD >> "qdata/logs/crux.log" 2>&1 & + +DOWN=true +while $DOWN; do + sleep 0.1 + DOWN=false + if [ ! -S "qdata/c/tm.ipc" ]; then + DOWN=true + fi +done diff --git a/docker/quorum-crux/docker-compose.yaml b/docker/quorum-crux/docker-compose.yaml new file mode 100644 index 0000000..1e0ab7a --- /dev/null +++ b/docker/quorum-crux/docker-compose.yaml @@ -0,0 +1,101 @@ +version: "3.4" +services: + + node1: &quorum_crux_node + image: blk.io/quorum/quorum-crux + build: + context: . + container_name: quorum1 + ports: + - 22001:22000 + - 21001:21000 + - 9001:9000 + restart: always + networks: + quorum_net: + ipv4_address: 10.5.0.11 + environment: + - GETH_KEY={"address":"ed9d02e382b34818e88b88a309c7fe71e65f419d","crypto":{"cipher":"aes-128-ctr","ciphertext":"4e77046ba3f699e744acb4a89c36a3ea1158a1bd90a076d36675f4c883864377","cipherparams":{"iv":"a8932af2a3c0225ee8e872bc0e462c11"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8ca49552b3e92f79c51f2cd3d38dfc723412c212e702bd337a3724e8937aff0f"},"mac":"6d1354fef5aa0418389b1a5d1f5ee0050d7273292a1171c51fd02f9ecff55264"},"id":"a65d1ac3-db7e-445d-a1cc-b6c5eeaa05e0","version":3} + - NODE_KEY=633b2ef3ed5306f05a532eb44eb84858aca470d4fde039c1c988088e48070e64 + - CRUX_PUB=BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= + - CRUX_PRIV={"data":{"bytes":"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA="},"type":"unlocked"} + - PORT=9000 + - OWN_URL=node1 + - GETH_PORT=21000 + - GETH_RPC_PORT=22000 + - OTHER_NODES=http://node2:9000/ + - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] + + node2: + <<: *quorum_crux_node + container_name: quorum2 + ports: + - 22002:22000 + - 21002:21000 + - 9002:9000 + networks: + quorum_net: + ipv4_address: 10.5.0.12 + environment: + - GETH_KEY={"address":"ca843569e3427144cead5e4d5999a3d0ccf92b8e","crypto":{"cipher":"aes-128-ctr","ciphertext":"01d409941ce57b83a18597058033657182ffb10ae15d7d0906b8a8c04c8d1e3a","cipherparams":{"iv":"0bfb6eadbe0ab7ffaac7e1be285fb4e5"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"7b90f455a95942c7c682e0ef080afc2b494ef71e749ba5b384700ecbe6f4a1bf"},"mac":"4cc851f9349972f851d03d75a96383a37557f7c0055763c673e922de55e9e307"},"id":"354e3b35-1fed-407d-a358-889a29111211","version":3} + - NODE_KEY=d5b8dfced693dbb7bf858dce40e3b2a0373696ac88ccf5e1c0052e26b7d77e49 + - CRUX_PUB=QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= + - CRUX_PRIV={"data":{"bytes":"nDFwJNHSiT1gNzKBy9WJvMhmYRkW3TzFUmPsNzR6oFk="},"type":"unlocked"} + - PORT=9000 + - OWN_URL=node2 + - GETH_PORT=21000 + - GETH_RPC_PORT=22000 + - OTHER_NODES=http://node3:9000/ + - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] + + node3: + <<: *quorum_crux_node + container_name: quorum3 + ports: + - 22003:22000 + - 9003:9000 + - 21003:21000 + networks: + quorum_net: + ipv4_address: 10.5.0.13 + environment: + - GETH_KEY={"address":"0fbdc686b912d7722dc86510934589e0aaf3b55a","crypto":{"cipher":"aes-128-ctr","ciphertext":"6b2c72c6793f3da8185e36536e02f574805e41c18f551f24b58346ef4ecf3640","cipherparams":{"iv":"582f27a739f39580410faa108d5cc59f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"1a79b0db3f8cb5c2ae4fa6ccb2b5917ce446bd5e42c8d61faeee512b97b4ad4a"},"mac":"cecb44d2797d6946805d5d744ff803805477195fab1d2209eddc3d1158f2e403"},"id":"f7292e90-af71-49af-a5b3-40e8493f4681","version":3} + - NODE_KEY=45d87820ad7792f86592a8e3494c3e78b4755a480cfad1799d6c3f28c3f0d87c + - CRUX_PUB=1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= + - CRUX_PRIV={"data":{"bytes":"tMxUVR8bX7aq/TbpVHc2QV3SN2iUuExBwefAuFsO0Lg="},"type":"unlocked"} + - PORT=9000 + - OWN_URL=node3 + - GETH_PORT=21000 + - GETH_RPC_PORT=22000 + - OTHER_NODES=http://node4:9000/ + - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] + + node4: + <<: *quorum_crux_node + container_name: quorum4 + ports: + - 22004:22000 + - 9004:9000 + - 21004:21000 + networks: + quorum_net: + ipv4_address: 10.5.0.14 + environment: + - GETH_KEY={"address":"9186eb3d20cbd1f5f992a950d808c4495153abd5","crypto":{"cipher":"aes-128-ctr","ciphertext":"d160a630a39be3ff35556055406d8ff2a635f0535fe298d62ccc812d8f7b3bd5","cipherparams":{"iv":"82fce06bc6e1658a5e81ccef3b753329"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8d0c486db4c942721f4f5e96d48e9344805d101dad8159962b8a2008ac718548"},"mac":"4a92bda949068968d470320260ae1a825aa22f6a40fb8567c9f91d700c3f7e91"},"id":"bdb3b4f6-d8d0-4b00-8473-e223ef371b5c","version":3} + - NODE_KEY=daa89d4ae250d24b33847343d0cc0116c48331b81e28514522bb7f77f2be5676 + - CRUX_PUB=oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= + - CRUX_PRIV={"data":{"bytes":"grQjd3dBp4qFs8/5Jdq7xjz++aUx/LXAqISFyPWaCRw="},"type":"unlocked"} + - PORT=9000 + - OWN_URL=node4 + - GETH_PORT=21000 + - GETH_RPC_PORT=22000 + - OTHER_NODES=http://node1:9000/ + - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] + +networks: + quorum_net: + driver: bridge + ipam: + driver: default + config: + - subnet: 10.5.0.0/24 \ No newline at end of file diff --git a/docker/quorum-crux/istanbul-genesis.json b/docker/quorum-crux/istanbul-genesis.json new file mode 100644 index 0000000..b7d27e6 --- /dev/null +++ b/docker/quorum-crux/istanbul-genesis.json @@ -0,0 +1,50 @@ +{ + "config": { + "chainId": 2017, + "homesteadBlock": 1, + "eip150Block": 2, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 3, + "eip158Block": 3, + "istanbul": { + "epoch": 30000, + "policy": 0 + } + }, + "nonce": "0x0", + "timestamp": "0x5b68584d", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000f89af85494c816ef5f432f9aff47b74120a9d6b41ea9a3bb1b946e98452e06d5158133490981a66f626538ec2c44947785902068b03c3820f8168af31845ffb313bdcd94c5263ce01b7c1fc0bc81d348a84df9f7bca54492b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", + "gasLimit": "0x47b760", + "difficulty": "0x1", + "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "6e98452e06d5158133490981a66f626538ec2c44": { + "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" + }, + "7785902068b03c3820f8168af31845ffb313bdcd": { + "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" + }, + "c5263ce01b7c1fc0bc81d348a84df9f7bca54492": { + "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" + }, + "c816ef5f432f9aff47b74120a9d6b41ea9a3bb1b": { + "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" + }, + "ed9d02e382b34818e88b88a309c7fe71e65f419d": { + "balance": "1000000000000000000000000000" + }, + "ca843569e3427144cead5e4d5999a3d0ccf92b8e": { + "balance": "1000000000000000000000000000" + }, + "0fbdc686b912d7722dc86510934589e0aaf3b55a": { + "balance": "1000000000000000000000000000" + }, + "9186eb3d20cbd1f5f992a950d808c4495153abd5": { + "balance": "1000000000000000000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/docker/quorum-crux/istanbul-init.sh b/docker/quorum-crux/istanbul-init.sh new file mode 100644 index 0000000..700fddd --- /dev/null +++ b/docker/quorum-crux/istanbul-init.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -u +set -e + +echo "[*] Cleaning up temporary data directories" +rm -rf qdata +mkdir -p qdata/logs + +echo "[*] Configuring node" +mkdir -p qdata/dd/{keystore,geth} +echo $PERMISSIONED_NODES >> qdata/dd/static-nodes.json +echo $PERMISSIONED_NODES >> qdata/dd/permissioned-nodes.json +echo $GETH_KEY >> qdata/dd/keystore/key +geth --datadir qdata/dd init istanbul-genesis.json diff --git a/docker/quorum-crux/istanbul-start.sh b/docker/quorum-crux/istanbul-start.sh new file mode 100644 index 0000000..84795b4 --- /dev/null +++ b/docker/quorum-crux/istanbul-start.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -u +set -e + +mkdir -p qdata/logs +echo "[*] Starting Crux nodes" +./crux-start.sh + +echo "[*] Starting Ethereum nodes" +set -v +ARGS="--syncmode full --mine --rpc --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul " +PRIVATE_CONFIG=qdata/c/tm.ipc nohup geth --datadir qdata/dd $ARGS --rpcport $GETH_RPC_PORT --port $GETH_PORT --nodekeyhex $NODE_KEY --unlock 0 --password passwords.txt --verbosity=6 2>>qdata/logs/node.log & +set +v + + diff --git a/docker/quorum-crux/passwords.txt b/docker/quorum-crux/passwords.txt new file mode 100644 index 0000000..e69de29 diff --git a/docker/quorum-crux/scripts/simpleContract.js b/docker/quorum-crux/scripts/simpleContract.js new file mode 100644 index 0000000..238f5d4 --- /dev/null +++ b/docker/quorum-crux/scripts/simpleContract.js @@ -0,0 +1,22 @@ +a = eth.accounts[0] +web3.eth.defaultAccount = a; + +// abi and bytecode generated from simplestorage.sol: +// > solcjs --bin --abi simplestorage.sol +var abi = [{"constant":true,"inputs":[],"name":"storedData","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"retVal","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[{"name":"initVal","type":"uint256"}],"payable":false,"type":"constructor"}]; + +var bytecode = "0x6060604052341561000f57600080fd5b604051602080610149833981016040528080519060200190919050505b806000819055505b505b610104806100456000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632a1afcd914605157806360fe47b11460775780636d4ce63c146097575b600080fd5b3415605b57600080fd5b606160bd565b6040518082815260200191505060405180910390f35b3415608157600080fd5b6095600480803590602001909190505060c3565b005b341560a157600080fd5b60a760ce565b6040518082815260200191505060405180910390f35b60005481565b806000819055505b50565b6000805490505b905600a165627a7a72305820d5851baab720bba574474de3d09dbeaabc674a15f4dd93b974908476542c23f00029"; + +var simpleContract = web3.eth.contract(abi); +var simple = simpleContract.new(42, {from:web3.eth.accounts[0], data: bytecode, gas: 0x47b760, privateFor: ["QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc="]}, function(e, contract) { + if (e) { + console.log("err creating contract", e); + } else { + if (!contract.address) { + console.log("Contract transaction send: TransactionHash: " + contract.transactionHash + " waiting to be mined..."); + } else { + console.log("Contract mined! Address: " + contract.address); + console.log(contract); + } + } +}); diff --git a/docker/quorum-crux/scripts/test_transaction.sh b/docker/quorum-crux/scripts/test_transaction.sh new file mode 100644 index 0000000..a22f0b4 --- /dev/null +++ b/docker/quorum-crux/scripts/test_transaction.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "Sending private transaction" +PRIVATE_CONFIG=qdata/c/tm.ipc geth --exec "loadScript(\"simpleContract.js\")" attach ipc:qdata/dd/geth.ipc diff --git a/docker/quorum-crux/start.sh b/docker/quorum-crux/start.sh new file mode 100644 index 0000000..4d9eda1 --- /dev/null +++ b/docker/quorum-crux/start.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +./istanbul-init.sh +./istanbul-start.sh + +tail -f /dev/null \ No newline at end of file From 3f2d8eb5dac4e115e37a3d9110410aff9e146d5c Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 9 Aug 2018 14:04:22 +0100 Subject: [PATCH 58/85] - Update instructions for using Docker images in README. - Updated CHANGELOG. --- CHANGELOG.md | 4 +++- README.md | 35 +++++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f06881..f25cb5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,4 +2,6 @@ ### Added - Crux, a secure enclave for Quorum written in Golang - Protobuf and gRPC support. - - TLS can be enabled. + - TLS support + - Docker images + \ No newline at end of file diff --git a/README.md b/README.md index 4acf1ef..ca538fb 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,32 @@ secure enclave component of [Quorum](https://github.com/jpmorganchase/quorum/), ## Getting started The best way to start is to run the Quorum-Crux Docker image is -[4 Nodes Quorum example](https://github.com/blk-io/crux/docker/quorum-crux). This pulls a fork of Quorum and uses Crux as the secure enclave. You can also run the [7 Nodes Quorum example](https://github.com/blk-io/quorum-examples) which is an updated version of JP Morgan's Quorum 7 Nodes example to use Crux as the secure enclave. +[4 Nodes Quorum example](https://github.com/blk-io/crux/tree/master/docker/quorum-crux). This +image runs up a 4 node Quorum network using Crux as the secure enclave. -[2 Crux nodes example](https://github.com/blk-io/crux/docker/crux) is simple docker image to just bring up 2 Crux nodes which communicate with each other. +```bash +docker-compose -f docker/quorum-crux/docker-compose.yaml up +``` + +Where the node details are as follows: + +| Name | Address | Account key | Crux node key | +| ------- | ----------------------- | ------------------------------------------ | -------------------------------------------- | +| quorum1 | http://localhost:220001 | 0xed9d02e382b34818e88b88a309c7fe71e65f419d | BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= | +| quorum2 | http://localhost:220002 | 0xca843569e3427144cead5e4d5999a3d0ccf92b8e | QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= | +| quorum3 | http://localhost:220003 | 0x0fbdc686b912d7722dc86510934589e0aaf3b55a | 1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= | +| quorum4 | http://localhost:220004 | 0x9186eb3d20cbd1f5f992a950d808c4495153abd5 | oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= | + +[2 Crux nodes example](https://github.com/blk-io/crux/tree/master/docker/crux) is simple Docker +image to just bring up 2 Crux nodes which communicate with each other. + +```bash +docker-compose -f docker/quorum-crux/docker-compose.yaml up +``` + +You can also run the +[7 Nodes Quorum example](https://github.com/blk-io/quorum-examples) which is an updated version +of JP Morgan's Quorum 7 Nodes example to use Crux as the secure enclave. If you'd prefer to run just a client, you can build using the below instructions and run as per the below. @@ -31,16 +54,16 @@ Usage of ./bin/crux: --alwayssendto string List of public keys for nodes to send all transactions too --berkeleydb Use Berkeley DB for storage --generate-keys string Generate a new keypair - --othernodes string "Boot nodes" to connect to to discover the network + --othernodes string `Boot nodes` to connect to to discover the network --port int The local port to listen on (default -1) --privatekeys string Private keys hosted by this node --publickeys string Public keys hosted by this node --socket string IPC socket to create for access to the Private API - --storage string Database storage file name (default "crux.db") + --storage string Database storage file name (default `crux.db`) --url string The URL to advertise to other nodes (reachable by them) --verbosity int Verbosity level of logs (default 1) - --workdir string The folder to put stuff in (default: .) (default ".") - --grpc Use protobuf + gRPC for communication between nodes + --workdir string The folder to put stuff in (default: .) (default `.`) + --grpc Use protobuf + gRPC for communication between nodes (default `true`) --tls Use TLS to secure HTTP communications --tlsservercert TLS server certificate --tlsserverkey TLS server key From 0b944712b06970e7e5b2dd57087c09c8072db515 Mon Sep 17 00:00:00 2001 From: Conor Date: Thu, 9 Aug 2018 14:46:39 +0100 Subject: [PATCH 59/85] Further README updates. --- README.md | 80 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index ca538fb..eb60c84 100644 --- a/README.md +++ b/README.md @@ -12,33 +12,76 @@ secure enclave component of [Quorum](https://github.com/jpmorganchase/quorum/), ## Getting started -The best way to start is to run the Quorum-Crux Docker image is -[4 Nodes Quorum example](https://github.com/blk-io/crux/tree/master/docker/quorum-crux). This -image runs up a 4 node Quorum network using Crux as the secure enclave. +### 4-node Quorum network with Crux + +The best way to start is to run the +[Quorum-Crux Docker image](https://github.com/blk-io/crux/tree/master/docker/quorum-crux). This +image runs a 4 node Quorum network using Crux as the secure enclave communicating over gRPC. ```bash +git clone https://github.com/blk-io/crux.git docker-compose -f docker/quorum-crux/docker-compose.yaml up ``` Where the node details are as follows: -| Name | Address | Account key | Crux node key | +| Name | Quorum node address | Account key | Crux node key | | ------- | ----------------------- | ------------------------------------------ | -------------------------------------------- | | quorum1 | http://localhost:220001 | 0xed9d02e382b34818e88b88a309c7fe71e65f419d | BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= | | quorum2 | http://localhost:220002 | 0xca843569e3427144cead5e4d5999a3d0ccf92b8e | QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= | | quorum3 | http://localhost:220003 | 0x0fbdc686b912d7722dc86510934589e0aaf3b55a | 1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= | | quorum4 | http://localhost:220004 | 0x9186eb3d20cbd1f5f992a950d808c4495153abd5 | oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= | +### 2-node Crux only-network + [2 Crux nodes example](https://github.com/blk-io/crux/tree/master/docker/crux) is simple Docker image to just bring up 2 Crux nodes which communicate with each other. ```bash +git clone https://github.com/blk-io/crux.git docker-compose -f docker/quorum-crux/docker-compose.yaml up ``` -You can also run the +Where the Crux node keys are the same as `quorum1` and `quorum2` above, and are listening on ports +9001 and 9002 for gRPC requests. + +### Vagrant VM + +For those of you who are unable to use Docker, you can run the [7 Nodes Quorum example](https://github.com/blk-io/quorum-examples) which is an updated version -of JP Morgan's Quorum 7 Nodes example to use Crux as the secure enclave. +of JP Morgan's Quorum 7 Nodes example using Crux as the secure enclave. + +### Download the latest binary + +The latest binaries for different platforms are available on the +[release](https://github.com/blk-io/crux/releases/latest) page. + +## Generating keys + +Each Crux instance requires at least one key-pair to be associated with it. The key-pair is used +to ensure transaction privacy. Crux uses the [NaCl cryptography library](https://nacl.cr.yp.to/). + +You use the `--generate-keys` argument to generate a new key-pair with Crux: + +```bash +crux --generate-keys myKey +``` + +This will produce two files, named `myKey` and `myKey.pub` reflecting the private and public keys +respectively. + +## Core configuration + +At a minimum, Crux requires the following configuration parameters. This tells the Crux instance +what port it is running on and what ip address it should advertise to other peers. + +Details of at least one key-pair must be provided for the Crux node to store requests on behalf of. + +```bash +crux --url=http://127.0.0.1:9001/ --port=9001 --workdir=crux --publickeys=tm.pub --privatekeys=tm.key --othernodes=https://127.0.0.1:9001/ +``` + +## Build instructions If you'd prefer to run just a client, you can build using the below instructions and run as per the below. @@ -69,31 +112,6 @@ Usage of ./bin/crux: --tlsserverkey TLS server key ``` -## Generating keys - -Each Crux instance requires at least one key-pair to be associated with it. The key-pair is used -to ensure transaction privacy. Crux uses the [NaCl cryptography library](https://nacl.cr.yp.to/). - -You use the `--generate-keys` argument to generate a new key-pair with Crux: - -```bash -crux --generate-keys myKey -``` - -This will produce two files, named `myKey` and `myKey.pub` reflecting the private and public keys -respectively. - -## Core configuration - -At a minimum, Crux requires the following configuration parameters. This tells the Crux instance -what port it is running on and what ip address it should advertise to other peers. - -Details of at least one key-pair must be provided for the Crux node to store requests on behalf of. - -```bash -crux --url=http://127.0.0.1:9001/ --port=9001 --workdir=crux --publickeys=tm.pub --privatekeys=tm.key --othernodes=https://127.0.0.1:9001/ -``` - ## How does it work? At present, Crux performs its cryptographic operations in a manner identical to Constellation. You From ef2ce8c18a15fab94589702709483468c47a8c61 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Thu, 9 Aug 2018 17:04:00 +0100 Subject: [PATCH 60/85] Update docker-compose to pull from registry (#25) --- docker/quorum-crux/Dockerfile | 12 ++---------- docker/quorum-crux/docker-compose.yaml | 9 ++++++--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/docker/quorum-crux/Dockerfile b/docker/quorum-crux/Dockerfile index 1eac569..0d48d5e 100644 --- a/docker/quorum-crux/Dockerfile +++ b/docker/quorum-crux/Dockerfile @@ -1,16 +1,8 @@ FROM alpine:3.8 RUN apk add --no-cache --update unzip db zlib wrk wget libsodium-dev go bash libpthread-stubs db-dev && \ - apk -X http://dl-cdn.alpinelinux.org/alpine/edge/testing add --no-cache leveldb - -RUN \ - apk --no-cache --update add build-base cmake boost-dev git && \ - sed -i -E -e 's/include /include /' /usr/include/boost/asio/detail/socket_types.hpp && \ - git clone --depth 1 --recursive -b release https://github.com/ethereum/solidity && \ - cd /solidity && cmake -DCMAKE_BUILD_TYPE=Release -DTESTS=0 -DSTATIC_LINKING=1 && \ - cd /solidity && make solc && install -s solc/solc /usr/bin && \ - cd / && rm -rf solidity && \ - rm -rf /var/cache/apk/* + apk -X http://dl-cdn.alpinelinux.org/alpine/edge/testing add --no-cache leveldb && \ + apk --no-cache --update add build-base cmake boost-dev git ENV PORT="" ENV NODE_KEY="" diff --git a/docker/quorum-crux/docker-compose.yaml b/docker/quorum-crux/docker-compose.yaml index 1e0ab7a..1ecd1f1 100644 --- a/docker/quorum-crux/docker-compose.yaml +++ b/docker/quorum-crux/docker-compose.yaml @@ -2,9 +2,12 @@ version: "3.4" services: node1: &quorum_crux_node - image: blk.io/quorum/quorum-crux - build: - context: . + # Pull image down from Docker Hub + image: blkio10/quorum-crux:v1.0.0 + # Uncomment the below, and comment out the above line to build the Docker images yourself +# image: blk.io/quorum/quorum-crux +# build: +# context: . container_name: quorum1 ports: - 22001:22000 From 28ee2e29380ec2bc500a1ce403d22939252b2ed4 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Fri, 10 Aug 2018 09:58:11 +0100 Subject: [PATCH 61/85] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb60c84..df2230f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ image to just bring up 2 Crux nodes which communicate with each other. ```bash git clone https://github.com/blk-io/crux.git -docker-compose -f docker/quorum-crux/docker-compose.yaml up +docker-compose -f docker/crux/docker-compose.yaml up ``` Where the Crux node keys are the same as `quorum1` and `quorum2` above, and are listening on ports @@ -67,7 +67,7 @@ You use the `--generate-keys` argument to generate a new key-pair with Crux: crux --generate-keys myKey ``` -This will produce two files, named `myKey` and `myKey.pub` reflecting the private and public keys +This will produce two files, named `myKey.key` and `myKey.pub` reflecting the private and public keys respectively. ## Core configuration From adfa5789cd3466e6a88a0a21d899b3c4ccdc8c68 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Mon, 13 Aug 2018 17:04:22 +0100 Subject: [PATCH 62/85] Update bootstrap.sh (#26) --- docker/quorum-crux/bootstrap.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/quorum-crux/bootstrap.sh b/docker/quorum-crux/bootstrap.sh index 03e396a..ace6e43 100644 --- a/docker/quorum-crux/bootstrap.sh +++ b/docker/quorum-crux/bootstrap.sh @@ -4,7 +4,7 @@ set -eu -o pipefail # make/install quorum git clone https://github.com/ConsenSys/quorum.git pushd quorum >/dev/null -git checkout tags/v2.0.2-grpc +git checkout tags/v2.0.3-grpc make all cp build/bin/geth /usr/local/bin cp build/bin/bootnode /usr/local/bin From 8a6b3a5a5316b0ec361287cdf6082c78f8a9233d Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Thu, 16 Aug 2018 09:21:40 +0100 Subject: [PATCH 63/85] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index df2230f..6e23e4c 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ Where the node details are as follows: | Name | Quorum node address | Account key | Crux node key | | ------- | ----------------------- | ------------------------------------------ | -------------------------------------------- | -| quorum1 | http://localhost:220001 | 0xed9d02e382b34818e88b88a309c7fe71e65f419d | BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= | -| quorum2 | http://localhost:220002 | 0xca843569e3427144cead5e4d5999a3d0ccf92b8e | QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= | -| quorum3 | http://localhost:220003 | 0x0fbdc686b912d7722dc86510934589e0aaf3b55a | 1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= | -| quorum4 | http://localhost:220004 | 0x9186eb3d20cbd1f5f992a950d808c4495153abd5 | oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= | +| quorum1 | http://localhost:22001 | 0xed9d02e382b34818e88b88a309c7fe71e65f419d | BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= | +| quorum2 | http://localhost:22002 | 0xca843569e3427144cead5e4d5999a3d0ccf92b8e | QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= | +| quorum3 | http://localhost:22003 | 0x0fbdc686b912d7722dc86510934589e0aaf3b55a | 1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= | +| quorum4 | http://localhost:22004 | 0x9186eb3d20cbd1f5f992a950d808c4495153abd5 | oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= | ### 2-node Crux only-network From 74e468791eb2df570d8cbd370e198b2ed338bdff Mon Sep 17 00:00:00 2001 From: drandreaskrueger Date: Thu, 16 Aug 2018 12:07:51 +0100 Subject: [PATCH 64/85] higher gasLimit, larger txpool, blockperiod 1 seconds --- docker/quorum-crux/istanbul-genesis.json | 4 ++-- docker/quorum-crux/istanbul-start.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/quorum-crux/istanbul-genesis.json b/docker/quorum-crux/istanbul-genesis.json index b7d27e6..4d782c2 100644 --- a/docker/quorum-crux/istanbul-genesis.json +++ b/docker/quorum-crux/istanbul-genesis.json @@ -14,7 +14,7 @@ "nonce": "0x0", "timestamp": "0x5b68584d", "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000f89af85494c816ef5f432f9aff47b74120a9d6b41ea9a3bb1b946e98452e06d5158133490981a66f626538ec2c44947785902068b03c3820f8168af31845ffb313bdcd94c5263ce01b7c1fc0bc81d348a84df9f7bca54492b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", - "gasLimit": "0x47b760", + "gasLimit": "0x1312D00", "difficulty": "0x1", "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", "coinbase": "0x0000000000000000000000000000000000000000", @@ -47,4 +47,4 @@ "number": "0x0", "gasUsed": "0x0", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" -} \ No newline at end of file +} diff --git a/docker/quorum-crux/istanbul-start.sh b/docker/quorum-crux/istanbul-start.sh index 84795b4..dbaf60f 100644 --- a/docker/quorum-crux/istanbul-start.sh +++ b/docker/quorum-crux/istanbul-start.sh @@ -8,7 +8,7 @@ echo "[*] Starting Crux nodes" echo "[*] Starting Ethereum nodes" set -v -ARGS="--syncmode full --mine --rpc --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul " +ARGS="--txpool.globalslots 20000 --txpool.globalqueue 20000 --istanbul.blockperiod 1 --syncmode full --mine --rpc --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul " PRIVATE_CONFIG=qdata/c/tm.ipc nohup geth --datadir qdata/dd $ARGS --rpcport $GETH_RPC_PORT --port $GETH_PORT --nodekeyhex $NODE_KEY --unlock 0 --password passwords.txt --verbosity=6 2>>qdata/logs/node.log & set +v From 809fe67d0c88bf433b569583589c74b2b82447dd Mon Sep 17 00:00:00 2001 From: drandreaskrueger Date: Thu, 16 Aug 2018 13:08:26 +0100 Subject: [PATCH 65/85] docker-compose-local.yaml for building local docker container --- docker/quorum-crux/docker-compose-local.yaml | 104 +++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docker/quorum-crux/docker-compose-local.yaml diff --git a/docker/quorum-crux/docker-compose-local.yaml b/docker/quorum-crux/docker-compose-local.yaml new file mode 100644 index 0000000..9922890 --- /dev/null +++ b/docker/quorum-crux/docker-compose-local.yaml @@ -0,0 +1,104 @@ +version: "3.4.1" # build the docker container locally +services: + + node1: &quorum_crux_node + # Pull image down from Docker Hub + # image: blkio10/quorum-crux:v1.0.0 + # Uncomment the below, and comment out the above line to build the Docker images yourself + image: blk.io/quorum/quorum-crux + build: + context: . + container_name: quorum1 + ports: + - 22001:22000 + - 21001:21000 + - 9001:9000 + restart: always + networks: + quorum_net: + ipv4_address: 10.5.0.11 + environment: + - GETH_KEY={"address":"ed9d02e382b34818e88b88a309c7fe71e65f419d","crypto":{"cipher":"aes-128-ctr","ciphertext":"4e77046ba3f699e744acb4a89c36a3ea1158a1bd90a076d36675f4c883864377","cipherparams":{"iv":"a8932af2a3c0225ee8e872bc0e462c11"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8ca49552b3e92f79c51f2cd3d38dfc723412c212e702bd337a3724e8937aff0f"},"mac":"6d1354fef5aa0418389b1a5d1f5ee0050d7273292a1171c51fd02f9ecff55264"},"id":"a65d1ac3-db7e-445d-a1cc-b6c5eeaa05e0","version":3} + - NODE_KEY=633b2ef3ed5306f05a532eb44eb84858aca470d4fde039c1c988088e48070e64 + - CRUX_PUB=BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= + - CRUX_PRIV={"data":{"bytes":"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA="},"type":"unlocked"} + - PORT=9000 + - OWN_URL=node1 + - GETH_PORT=21000 + - GETH_RPC_PORT=22000 + - OTHER_NODES=http://node2:9000/ + - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] + + node2: + <<: *quorum_crux_node + container_name: quorum2 + ports: + - 22002:22000 + - 21002:21000 + - 9002:9000 + networks: + quorum_net: + ipv4_address: 10.5.0.12 + environment: + - GETH_KEY={"address":"ca843569e3427144cead5e4d5999a3d0ccf92b8e","crypto":{"cipher":"aes-128-ctr","ciphertext":"01d409941ce57b83a18597058033657182ffb10ae15d7d0906b8a8c04c8d1e3a","cipherparams":{"iv":"0bfb6eadbe0ab7ffaac7e1be285fb4e5"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"7b90f455a95942c7c682e0ef080afc2b494ef71e749ba5b384700ecbe6f4a1bf"},"mac":"4cc851f9349972f851d03d75a96383a37557f7c0055763c673e922de55e9e307"},"id":"354e3b35-1fed-407d-a358-889a29111211","version":3} + - NODE_KEY=d5b8dfced693dbb7bf858dce40e3b2a0373696ac88ccf5e1c0052e26b7d77e49 + - CRUX_PUB=QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= + - CRUX_PRIV={"data":{"bytes":"nDFwJNHSiT1gNzKBy9WJvMhmYRkW3TzFUmPsNzR6oFk="},"type":"unlocked"} + - PORT=9000 + - OWN_URL=node2 + - GETH_PORT=21000 + - GETH_RPC_PORT=22000 + - OTHER_NODES=http://node3:9000/ + - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] + + node3: + <<: *quorum_crux_node + container_name: quorum3 + ports: + - 22003:22000 + - 9003:9000 + - 21003:21000 + networks: + quorum_net: + ipv4_address: 10.5.0.13 + environment: + - GETH_KEY={"address":"0fbdc686b912d7722dc86510934589e0aaf3b55a","crypto":{"cipher":"aes-128-ctr","ciphertext":"6b2c72c6793f3da8185e36536e02f574805e41c18f551f24b58346ef4ecf3640","cipherparams":{"iv":"582f27a739f39580410faa108d5cc59f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"1a79b0db3f8cb5c2ae4fa6ccb2b5917ce446bd5e42c8d61faeee512b97b4ad4a"},"mac":"cecb44d2797d6946805d5d744ff803805477195fab1d2209eddc3d1158f2e403"},"id":"f7292e90-af71-49af-a5b3-40e8493f4681","version":3} + - NODE_KEY=45d87820ad7792f86592a8e3494c3e78b4755a480cfad1799d6c3f28c3f0d87c + - CRUX_PUB=1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= + - CRUX_PRIV={"data":{"bytes":"tMxUVR8bX7aq/TbpVHc2QV3SN2iUuExBwefAuFsO0Lg="},"type":"unlocked"} + - PORT=9000 + - OWN_URL=node3 + - GETH_PORT=21000 + - GETH_RPC_PORT=22000 + - OTHER_NODES=http://node4:9000/ + - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] + + node4: + <<: *quorum_crux_node + container_name: quorum4 + ports: + - 22004:22000 + - 9004:9000 + - 21004:21000 + networks: + quorum_net: + ipv4_address: 10.5.0.14 + environment: + - GETH_KEY={"address":"9186eb3d20cbd1f5f992a950d808c4495153abd5","crypto":{"cipher":"aes-128-ctr","ciphertext":"d160a630a39be3ff35556055406d8ff2a635f0535fe298d62ccc812d8f7b3bd5","cipherparams":{"iv":"82fce06bc6e1658a5e81ccef3b753329"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8d0c486db4c942721f4f5e96d48e9344805d101dad8159962b8a2008ac718548"},"mac":"4a92bda949068968d470320260ae1a825aa22f6a40fb8567c9f91d700c3f7e91"},"id":"bdb3b4f6-d8d0-4b00-8473-e223ef371b5c","version":3} + - NODE_KEY=daa89d4ae250d24b33847343d0cc0116c48331b81e28514522bb7f77f2be5676 + - CRUX_PUB=oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= + - CRUX_PRIV={"data":{"bytes":"grQjd3dBp4qFs8/5Jdq7xjz++aUx/LXAqISFyPWaCRw="},"type":"unlocked"} + - PORT=9000 + - OWN_URL=node4 + - GETH_PORT=21000 + - GETH_RPC_PORT=22000 + - OTHER_NODES=http://node1:9000/ + - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] + +networks: + quorum_net: + driver: bridge + ipam: + driver: default + config: + - subnet: 10.5.0.0/24 From 4c9b07dd01acea63a2460d0ffdec54c78727cbf4 Mon Sep 17 00:00:00 2001 From: drandreaskrueger Date: Thu, 16 Aug 2018 13:21:47 +0100 Subject: [PATCH 66/85] that version number is the docker compose file version, reverted to original 3.4 --- docker/quorum-crux/docker-compose-local.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/quorum-crux/docker-compose-local.yaml b/docker/quorum-crux/docker-compose-local.yaml index 9922890..94ed4e8 100644 --- a/docker/quorum-crux/docker-compose-local.yaml +++ b/docker/quorum-crux/docker-compose-local.yaml @@ -1,4 +1,5 @@ -version: "3.4.1" # build the docker container locally +# build the docker container locally +version: "3.4" services: node1: &quorum_crux_node From afee37160acc2fa6447fb1af3f59e94a95a80b9c Mon Sep 17 00:00:00 2001 From: drandreaskrueger Date: Thu, 16 Aug 2018 13:28:20 +0100 Subject: [PATCH 67/85] for some reason it does not start mining when with '--istanbul.blockperiod 1' so dropping that parameter for now --- docker/quorum-crux/istanbul-start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/quorum-crux/istanbul-start.sh b/docker/quorum-crux/istanbul-start.sh index dbaf60f..eb9a0e4 100644 --- a/docker/quorum-crux/istanbul-start.sh +++ b/docker/quorum-crux/istanbul-start.sh @@ -8,7 +8,7 @@ echo "[*] Starting Crux nodes" echo "[*] Starting Ethereum nodes" set -v -ARGS="--txpool.globalslots 20000 --txpool.globalqueue 20000 --istanbul.blockperiod 1 --syncmode full --mine --rpc --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul " +ARGS="--txpool.globalslots 20000 --txpool.globalqueue 20000 --syncmode full --mine --rpc --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul " PRIVATE_CONFIG=qdata/c/tm.ipc nohup geth --datadir qdata/dd $ARGS --rpcport $GETH_RPC_PORT --port $GETH_PORT --nodekeyhex $NODE_KEY --unlock 0 --password passwords.txt --verbosity=6 2>>qdata/logs/node.log & set +v From 6def7aec802b38e20a7ab1cba09bad4adf313b81 Mon Sep 17 00:00:00 2001 From: drandreaskrueger Date: Thu, 16 Aug 2018 13:37:21 +0100 Subject: [PATCH 68/85] it just needed patience. '--istanbul.blockperiod 1' is actually working --- docker/quorum-crux/istanbul-start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/quorum-crux/istanbul-start.sh b/docker/quorum-crux/istanbul-start.sh index eb9a0e4..dbaf60f 100644 --- a/docker/quorum-crux/istanbul-start.sh +++ b/docker/quorum-crux/istanbul-start.sh @@ -8,7 +8,7 @@ echo "[*] Starting Crux nodes" echo "[*] Starting Ethereum nodes" set -v -ARGS="--txpool.globalslots 20000 --txpool.globalqueue 20000 --syncmode full --mine --rpc --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul " +ARGS="--txpool.globalslots 20000 --txpool.globalqueue 20000 --istanbul.blockperiod 1 --syncmode full --mine --rpc --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul " PRIVATE_CONFIG=qdata/c/tm.ipc nohup geth --datadir qdata/dd $ARGS --rpcport $GETH_RPC_PORT --port $GETH_PORT --nodekeyhex $NODE_KEY --unlock 0 --password passwords.txt --verbosity=6 2>>qdata/logs/node.log & set +v From 3b8eb226d40292900f99c3e902525fd462f381ce Mon Sep 17 00:00:00 2001 From: drandreaskrueger Date: Mon, 20 Aug 2018 11:58:57 +0100 Subject: [PATCH 69/85] still TPS drop after 14k transactions even with --cache 4096 --trie-cache-gens 1000, so switching that off again --- docker/quorum-crux/istanbul-start.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/quorum-crux/istanbul-start.sh b/docker/quorum-crux/istanbul-start.sh index dbaf60f..11d2f2f 100644 --- a/docker/quorum-crux/istanbul-start.sh +++ b/docker/quorum-crux/istanbul-start.sh @@ -8,7 +8,9 @@ echo "[*] Starting Crux nodes" echo "[*] Starting Ethereum nodes" set -v -ARGS="--txpool.globalslots 20000 --txpool.globalqueue 20000 --istanbul.blockperiod 1 --syncmode full --mine --rpc --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul " +ARGS="--syncmode full --mine --rpc --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul " +ARGS=$ARGS"--txpool.globalslots 20000 --txpool.globalqueue 20000 --istanbul.blockperiod 1 " +# ARGS=$ARGS"--cache 4096 --trie-cache-gens 1000 " PRIVATE_CONFIG=qdata/c/tm.ipc nohup geth --datadir qdata/dd $ARGS --rpcport $GETH_RPC_PORT --port $GETH_PORT --nodekeyhex $NODE_KEY --unlock 0 --password passwords.txt --verbosity=6 2>>qdata/logs/node.log & set +v From 8fa90c5c6d0704ef53e521db457d55ba649fd32b Mon Sep 17 00:00:00 2001 From: drandreaskrueger Date: Mon, 20 Aug 2018 11:59:44 +0100 Subject: [PATCH 70/85] command how to build locally --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 6e23e4c..0862136 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,11 @@ Where the node details are as follows: | quorum3 | http://localhost:22003 | 0x0fbdc686b912d7722dc86510934589e0aaf3b55a | 1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= | | quorum4 | http://localhost:22004 | 0x9186eb3d20cbd1f5f992a950d808c4495153abd5 | oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= | +#### local docker +If you want to make changes to e.g. istanbul-start.sh then build the docker image locally: + + docker-compose -f docker-compose-local.yaml up --build + ### 2-node Crux only-network [2 Crux nodes example](https://github.com/blk-io/crux/tree/master/docker/crux) is simple Docker From a71365e0b9991fdd72be3ea4f1a2ac484eda86b6 Mon Sep 17 00:00:00 2001 From: Conor Date: Tue, 21 Aug 2018 20:40:47 +0100 Subject: [PATCH 71/85] Fixed issue with config file load --- crux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crux.go b/crux.go index 1a883eb..c6c1623 100644 --- a/crux.go +++ b/crux.go @@ -25,7 +25,7 @@ func main() { for _, arg := range args[1:] { if strings.Contains(arg, ".conf") { - err := config.LoadConfig(os.Args[0]) + err := config.LoadConfig(arg) if err != nil { log.Fatalln(err) } From 9a73e024f1881418df1a2eed5d2f2c86f5b61d4d Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Thu, 23 Aug 2018 15:34:41 +0100 Subject: [PATCH 72/85] Add integration test (#30) * Update docker-compose to pull from registry * Add integration test --- Makefile | 3 ++ test/client_test.go | 82 +++++++++++++++++++++++++++++++++++ test/test1/testdata/key | 1 + test/test1/testdata/key.pub | 1 + test/test2/testdata/rcpt1 | 1 + test/test2/testdata/rcpt1.pub | 1 + 6 files changed, 89 insertions(+) create mode 100644 test/client_test.go create mode 100644 test/test1/testdata/key create mode 100644 test/test1/testdata/key.pub create mode 100644 test/test2/testdata/rcpt1 create mode 100644 test/test2/testdata/rcpt1.pub diff --git a/Makefile b/Makefile index 3b89b73..600abc3 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,8 @@ clean: $Q rm -rf bin .GOPATH test: .GOPATH/.ok + $Q ./bin/crux --url=http://127.0.0.1:9020/ --port=9020 --workdir=test/test1 --publickeys=testdata/key.pub --privatekeys=testdata/key & + $Q ./bin/crux --url=http://127.0.0.1:9025/ --port=9025 --workdir=test/test2 --publickeys=testdata/rcpt1.pub --privatekeys=testdata/rcpt1 & $Q go test $(if $V,-v) -i -race $(allpackages) # install -race libs to speed up next run ifndef CI $Q go vet $(allpackages) @@ -38,6 +40,7 @@ else $Q ( GODEBUG=cgocheck=2 go test -v -race $(allpackages); echo $$? ) | \ tee .GOPATH/test/output.txt | sed '$$ d'; exit $$(tail -1 .GOPATH/test/output.txt) endif + $Q pkill crux list: .GOPATH/.ok @echo $(allpackages) diff --git a/test/client_test.go b/test/client_test.go new file mode 100644 index 0000000..32d1319 --- /dev/null +++ b/test/client_test.go @@ -0,0 +1,82 @@ +package client + +import ( + "golang.org/x/net/context" + "google.golang.org/grpc" + "github.com/blk-io/chimera-api/chimera" + "encoding/base64" + "reflect" + "testing" + ) + +const sender = "zSifTnkv5r4K67Dq304eVcM4FpxGfHLe1yTCBm0/7wg=" +const receiver = "I/EbshW61ykJ+qTivXPaKyQ5WmQDUFfMNGEBj2E2uUs=" +var payload = []byte("payload") + + +func TestIntegration(t *testing.T) { + var conn1 *grpc.ClientConn + var conn2 *grpc.ClientConn + // Initiate a connection with the first server + conn1, err := grpc.Dial(":9020", grpc.WithInsecure()) + if err != nil { + t.Fatalf("did not connect: %s", err) + } + defer conn1.Close() + c1 := chimera.NewClientClient(conn1) + // Initiate a connection with the second server + conn2, err = grpc.Dial(":9025", grpc.WithInsecure()) + if err != nil { + t.Fatalf("did not connect: %s", err) + } + defer conn2.Close() + c2 := chimera.NewClientClient(conn2) + + Upcheckresponse1, err := c1.Upcheck(context.Background(), &chimera.UpCheckResponse{Message: "foo"}) + if err != nil { + t.Fatalf("error when calling Upcheck: %s", err) + } + t.Logf("Response from server: %s", Upcheckresponse1.Message) + + Upcheckresponse2, err := c2.Upcheck(context.Background(), &chimera.UpCheckResponse{Message: "foo"}) + if err != nil { + t.Fatalf("error when calling Upcheck: %s", err) + } + t.Logf("Response from server: %s", Upcheckresponse2.Message) + + sendReqs := []chimera.SendRequest{ + { + Payload: []byte("payload"), + From: sender, + To: []string{receiver}, + }, + { + Payload: []byte("test"), + To: []string{}, + }, + { + Payload: []byte("blk-io crux"), + }, + } + + sendResponse := chimera.SendResponse{} + for _, sendReq := range sendReqs { + sendResp, err:= c1.Send(context.Background(), &sendReq) + if err != nil { + t.Fatalf("gRPC send failed with %s", err) + } + sendResponse = chimera.SendResponse{Key:sendResp.Key} + t.Logf("The response for Send request is: %s", base64.StdEncoding.EncodeToString(sendResponse.Key)) + + recResp, err:= c1.Receive(context.Background(), &chimera.ReceiveRequest{Key:sendResponse.Key, To:receiver}) + if err != nil { + t.Fatalf("gRPC receive failed with %s", err) + } + receiveResponse := chimera.ReceiveResponse{Payload:recResp.Payload} + if !reflect.DeepEqual(receiveResponse.Payload, sendReq.Payload) { + t.Fatalf("handler returned unexpected response: %v, expected: %v\n", receiveResponse.Payload, sendReq.Payload) + } else { + t.Logf("The payload return is %v", receiveResponse.Payload) + } + } +} diff --git a/test/test1/testdata/key b/test/test1/testdata/key new file mode 100644 index 0000000..fdb841b --- /dev/null +++ b/test/test1/testdata/key @@ -0,0 +1 @@ +{"data":{"bytes":"W1n0C+NfjcU/cUBXsP5FQ/frU+qpvKQ7Pi/Mu5Hf/Ic="},"type":"unlocked"} \ No newline at end of file diff --git a/test/test1/testdata/key.pub b/test/test1/testdata/key.pub new file mode 100644 index 0000000..e8d6224 --- /dev/null +++ b/test/test1/testdata/key.pub @@ -0,0 +1 @@ +zSifTnkv5r4K67Dq304eVcM4FpxGfHLe1yTCBm0/7wg= \ No newline at end of file diff --git a/test/test2/testdata/rcpt1 b/test/test2/testdata/rcpt1 new file mode 100644 index 0000000..6d0736c --- /dev/null +++ b/test/test2/testdata/rcpt1 @@ -0,0 +1 @@ +{"data":{"bytes":"9e0UrkhdfGY0kBYuk3Nv3g4FlYXjTHpXORWO2r1An/A="},"type":"unlocked"} \ No newline at end of file diff --git a/test/test2/testdata/rcpt1.pub b/test/test2/testdata/rcpt1.pub new file mode 100644 index 0000000..6599ad6 --- /dev/null +++ b/test/test2/testdata/rcpt1.pub @@ -0,0 +1 @@ +I/EbshW61ykJ+qTivXPaKyQ5WmQDUFfMNGEBj2E2uUs= \ No newline at end of file From 44eda5a9cc3961cc3130eae38a5aa8e517a3ddc1 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Thu, 23 Aug 2018 17:36:05 +0100 Subject: [PATCH 73/85] Add delete and resend API implementation in Crux (#31) * Add delete and resend API implementation in Crux * Fix function names --- server/server_handler.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/server/server_handler.go b/server/server_handler.go index de5e3bd..88bfedf 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -127,6 +127,37 @@ func (s *Server) Push(ctx context.Context, in *chimera.PushPayload) (*chimera.Pa return &chimera.PartyInfoResponse{Payload: digestHash}, nil } +func (s *Server) Delete(ctx context.Context, in *chimera.DeleteRequest) (*chimera.DeleteRequest, error) { + var deleteReq chimera.DeleteRequest + err := s.Enclave.Delete(&deleteReq.Key) + if err != nil { + log.Fatalf("Unable to delete payload, error: %s\n", err) + } + return &chimera.DeleteRequest{Key: deleteReq.Key}, nil +} + +func (s *Server) Resend(ctx context.Context, in *chimera.ResendRequest) (*chimera.ResendResponse, error) { + var resendReq chimera.ResendRequest + var err error + + if resendReq.Type == "all" { + err = s.Enclave.RetrieveAllFor(&resendReq.PublicKey) + if err != nil { + log.Fatalf("Invalid body, exited with %s", err) + } + return nil, err + } else if resendReq.Type == "individual" { + var encodedPl *[]byte + encodedPl, err = s.Enclave.RetrieveFor(&resendReq.Key, &resendReq.PublicKey) + if err != nil { + log.Fatalf("Invalid body, exited with %s", err) + return nil, err + } + return &chimera.ResendResponse{Encoded: *encodedPl}, nil + } + return nil, err +} + func decodeErrorGRPC(name string, value string, err error) { log.Error(fmt.Sprintf("Invalid request: unable to decode %s: %s, error: %s\n", name, value, err)) From abb98b1b88a92527f81371625f0ee2ad40d2489a Mon Sep 17 00:00:00 2001 From: Puneetha Date: Wed, 5 Sep 2018 09:50:32 +0100 Subject: [PATCH 74/85] Update Gopkg.toml --- Gopkg.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gopkg.toml b/Gopkg.toml index a84569f..5d33ed1 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -59,3 +59,7 @@ [[constraint]] name = "github.com/grpc-ecosystem/grpc-gateway" version = "1.4.0" + +[[constraint]] + name = "github.com/blk-io/chimera-api" + revision = "ebd4db90873296427420c2fe2acec18c127b401d" From 9ddcfda357ae1dead5d2c7db22494c7ceb3a01ee Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Mon, 10 Sep 2018 12:55:26 +0100 Subject: [PATCH 75/85] Update bootstrap.sh --- docker/quorum-crux/bootstrap.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/quorum-crux/bootstrap.sh b/docker/quorum-crux/bootstrap.sh index ace6e43..ef33188 100644 --- a/docker/quorum-crux/bootstrap.sh +++ b/docker/quorum-crux/bootstrap.sh @@ -14,7 +14,7 @@ popd >/dev/null # make/install crux git clone https://github.com/blk-io/crux.git cd crux -git checkout tags/v1.0.0 +git checkout tags/v1.0.2 make setup && make cp bin/crux /usr/local/bin rm -r bin From f62b69b30a6dce7b4de602fbb47885a54a31bb08 Mon Sep 17 00:00:00 2001 From: CarlosFaria94 Date: Wed, 19 Sep 2018 14:22:01 +0100 Subject: [PATCH 76/85] Fix "apk WARNING Ignoring APKINDEX" --- docker/crux/Dockerfile | 8 +++++--- docker/quorum-crux/Dockerfile | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docker/crux/Dockerfile b/docker/crux/Dockerfile index fa36e15..7d22fd8 100644 --- a/docker/crux/Dockerfile +++ b/docker/crux/Dockerfile @@ -1,8 +1,10 @@ FROM alpine:3.8 -RUN apk add --no-cache --update unzip db zlib wrk wget libsodium-dev go bash libpthread-stubs db-dev && \ - apk -X http://dl-cdn.alpinelinux.org/alpine/edge/testing add --no-cache leveldb && \ - apk --no-cache --update add build-base cmake boost-dev git +RUN apk update --no-cache && \ + # Update and then install dependencies + apk add unzip db zlib wrk wget libsodium-dev go bash libpthread-stubs db-dev && \ + apk -X http://dl-cdn.alpinelinux.org/alpine/edge/testing add leveldb && \ + apk add build-base cmake boost-dev git ENV CRUX_PUB="" ENV CRUX_PRIV="" diff --git a/docker/quorum-crux/Dockerfile b/docker/quorum-crux/Dockerfile index 0d48d5e..fefc57a 100644 --- a/docker/quorum-crux/Dockerfile +++ b/docker/quorum-crux/Dockerfile @@ -1,8 +1,10 @@ FROM alpine:3.8 -RUN apk add --no-cache --update unzip db zlib wrk wget libsodium-dev go bash libpthread-stubs db-dev && \ - apk -X http://dl-cdn.alpinelinux.org/alpine/edge/testing add --no-cache leveldb && \ - apk --no-cache --update add build-base cmake boost-dev git +RUN apk update --no-cache && \ + # Update and then install dependencies + apk add unzip db zlib wrk wget libsodium-dev go bash libpthread-stubs db-dev && \ + apk -X http://dl-cdn.alpinelinux.org/alpine/edge/testing add leveldb && \ + apk add build-base cmake boost-dev git ENV PORT="" ENV NODE_KEY="" From 15cec205333a53df9ca2b59d879745c362784c2d Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Thu, 27 Sep 2018 10:02:57 +0100 Subject: [PATCH 77/85] Fix issues (#45) * Fix shorthand and CLI commands * Format code using gofmt * Update README.md --- .gitignore | 2 + Gopkg.lock | 45 +++++++++-------- README.md | 21 ++++---- api/client.go | 34 ++++++------- api/encoding.go | 16 +++---- api/encoding_test.go | 31 ++++++------ api/internal.go | 62 ++++++++++++------------ api/internal_test.go | 6 +-- config/config.go | 52 ++++++++++---------- config/config_test.go | 46 +++++++++--------- crux.go | 25 ++++++---- enclave/enclave.go | 94 ++++++++++++++++++------------------ enclave/enclave_test.go | 20 ++++---- server/proto_server.go | 22 ++++----- server/server.go | 21 ++++---- server/server_handler.go | 33 +++++++------ server/server_test.go | 101 +++++++++++++++++++-------------------- storage/berkleydb.go | 2 +- storage/leveldb.go | 2 +- test/client_test.go | 22 ++++----- utils/file_test.go | 4 +- utils/key.go | 4 +- utils/math.go | 2 +- utils/math_test.go | 22 ++++----- utils/url_test.go | 2 +- 25 files changed, 351 insertions(+), 340 deletions(-) diff --git a/.gitignore b/.gitignore index 322e8b3..7d48497 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ /.GOPATH /bin /vendor +/test/test1 +/test/test2 diff --git a/Gopkg.lock b/Gopkg.lock index f9a7509..79cf469 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,15 +2,14 @@ [[projects]] - branch = "master" - digest = "1:62e073e81a669097d786be83fee428b0584af9e521b3543063b6f7c599b35517" + digest = "1:86840754a6a45d993a441d23094f501eb0cb0b12fe71f4c6cab2f3a826cb8725" name = "github.com/blk-io/chimera-api" - packages = ["protofiles"] + packages = ["chimera"] pruneopts = "T" - revision = "deee5615cef7315a5206205e046a815cdaf29ba2" + revision = "ebd4db90873296427420c2fe2acec18c127b401d" [[projects]] - digest = "1:1834c1bcecde8bc847628b7c316b8258315749995f0c3a398b1370db8c98a1b3" + digest = "1:7fc160b460a6fc506b37fcca68332464c3f2cd57b6e3f111f26c5bbfd2d5518e" name = "github.com/fsnotify/fsnotify" packages = ["."] pruneopts = "T" @@ -18,7 +17,7 @@ version = "v1.4.7" [[projects]] - digest = "1:c97cc0085879c38dbe7cfc84cca26a8d8d6f2a93f671acdd066208985a85d4b3" + digest = "1:832e17df5ff8bbe0e0693d2fb46c5e53f96c662ee804049ce3ab6557df74e3ab" name = "github.com/golang/protobuf" packages = [ "jsonpb", @@ -36,14 +35,14 @@ [[projects]] branch = "master" - digest = "1:e99dbc9bee79a1c2406af764186ac826ec502f73de63ee69999866b7d1b412f2" + digest = "1:cf5bb7d7c59d8313289e5b756e24462cacd958d1e6db3bdbe1c800c677ad0f94" name = "github.com/golang/snappy" packages = ["."] pruneopts = "T" revision = "553a641470496b2327abcac10b36396bd98e45c9" [[projects]] - digest = "1:1709a0c115bdec713416bed33eee5df8c5d614b8886a102151377919c1f67a9b" + digest = "1:69cd81163a00bb8405194d47b8be19283744779b6104f2d6b3735e2a01cdb6fa" name = "github.com/grpc-ecosystem/grpc-gateway" packages = [ "runtime", @@ -56,7 +55,7 @@ [[projects]] branch = "master" - digest = "1:8f7d764c7172f13e906e664bdd6a1304a91ae0b3fbffbfd36675fd9b0738bc48" + digest = "1:17a030e647213ad422df723c8ee8902328040dd74b7fc68cb016784baaf95657" name = "github.com/hashicorp/hcl" packages = [ ".", @@ -82,7 +81,7 @@ revision = "5cde5eaaf78c6510c5f64f5347244806a06ba87b" [[projects]] - digest = "1:08f48150e800c09ee719ab61baa8ed8161b6896d1d9bc40cd92fe1fe62b7a5c2" + digest = "1:599ec2ed1b0ab8e5b2b6d6d849c47cbbf5733d7988aee541fd1e5befe69b6095" name = "github.com/kevinburke/nacl" packages = [ ".", @@ -97,7 +96,7 @@ version = "0.5" [[projects]] - digest = "1:23efb83c2ae6eb987f4187ea73315db2aea24d849f950a4d9ca7fd6cef91e4f1" + digest = "1:32ce5f79bec2865f25ae331a39ad67e507c9a6ed0b0da298db2c29efa8fe366f" name = "github.com/magiconair/properties" packages = ["."] pruneopts = "T" @@ -113,7 +112,7 @@ revision = "00c29f56e2386353d58c599509e8dc3801b0d716" [[projects]] - digest = "1:b1e91eaa4f0bb9ef59cc342f1f3df6160957cd882c5c18b8bd6dc2d0857fdc87" + digest = "1:63fc640566e87c5a5f2f3646725f308cf1a6b8e30fdba2d49941d978c3cb9b4d" name = "github.com/pelletier/go-toml" packages = ["."] pruneopts = "T" @@ -121,7 +120,7 @@ version = "v1.1.0" [[projects]] - digest = "1:f2d6485978c29de1bf447577aed1e53ff4d7cb0000361bbbbbde23a87fae298a" + digest = "1:047075087eff9bcda0806c636c479fae29b9ae55da320d3d961eb5d3203e2a75" name = "github.com/sirupsen/logrus" packages = ["."] pruneopts = "T" @@ -129,7 +128,7 @@ version = "v1.0.5" [[projects]] - digest = "1:6d5669c75d8044093981de5b0955e582e24978c4a2a04dfef3a7f5fbe3c6ee9e" + digest = "1:ed2a3679c070c90f83aacc19d114536db7c5b739980837fbdc86cbf937493400" name = "github.com/spf13/afero" packages = [ ".", @@ -164,7 +163,7 @@ version = "v1.0.0" [[projects]] - digest = "1:a49ffa1d09595ac8729e1e772cba6d60ea26ed10f0c4be4820b7230c6cfc12ed" + digest = "1:fbfebb70b35ccd17f5a91ba29f3d9dfeea149a98b44780a14efade3526c09bb3" name = "github.com/spf13/viper" packages = ["."] pruneopts = "T" @@ -173,7 +172,7 @@ [[projects]] branch = "master" - digest = "1:a2c06f269d9dc602ad583db7b6dd19d19e94c5a509cbe1d94d9e0f794ba9e305" + digest = "1:ef0753127ee10562925146777ed5f5f41431c0ccc8fa63da1cfae678cc7b8c74" name = "github.com/syndtr/goleveldb" packages = [ "leveldb", @@ -194,7 +193,7 @@ [[projects]] branch = "master" - digest = "1:68e7337ca5407b8da2b49b21142d47bce8517a31152c488efafd9c6ad6dce48e" + digest = "1:dbe2585d9a08433ff9d1951bab1df0bc8c1bbd50c9fb866f92b661d88beb5694" name = "golang.org/x/crypto" packages = [ "curve25519", @@ -208,7 +207,7 @@ [[projects]] branch = "master" - digest = "1:f49302030f7a716cb6c5973a77d6377f0ee4bbf5a250a48f8c9072b2ded10792" + digest = "1:1591a31d3ebd0f8d083ec588de4fb0b540809cf75edee9467c554cc17da0620a" name = "golang.org/x/net" packages = [ "context", @@ -224,7 +223,7 @@ [[projects]] branch = "master" - digest = "1:6cb6853c6c658ef6eb0ddec84764f0b9a1e6728e6d6ec1a61c8757d4634f75ae" + digest = "1:0c0ea93b25f3ef36c9662a679ba5fddae56ade049701857d020e3e73ae707e4b" name = "golang.org/x/sys" packages = [ "unix", @@ -234,7 +233,7 @@ revision = "3b87a42e500a6dc65dae1a55d0b641295971163e" [[projects]] - digest = "1:24db346d9931fe01f1e9a02aba78ba22c1ecd55bf0f79dd10ba5169719cf002d" + digest = "1:6164911cb5e94e8d8d5131d646613ff82c14f5a8ce869de2f6d80d9889df8c5a" name = "golang.org/x/text" packages = [ "collate", @@ -258,7 +257,7 @@ [[projects]] branch = "master" - digest = "1:5ecad12ed407b601c1cdf780e84b095d57a59343589a12a385d3828ecb2cc0b2" + digest = "1:baedbfe72924071a9c7a4f8f12819d3b18a87743f645e5b638ba7a25865f00b1" name = "google.golang.org/genproto" packages = [ "googleapis/api/annotations", @@ -268,7 +267,7 @@ revision = "694d95ba50e67b2e363f3483057db5d4910c18f9" [[projects]] - digest = "1:025336210cc711d68bf3d61cab51ccf7b0b33ea2d08def98065c2014ee8cc6c9" + digest = "1:cb75df728d7afe7b7a7f546c19b8fd1c5fc801bdfea88fbd1a6a4a0af5072e3a" name = "google.golang.org/grpc" packages = [ ".", @@ -312,7 +311,7 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ - "github.com/blk-io/chimera-api/protofiles", + "github.com/blk-io/chimera-api/chimera", "github.com/grpc-ecosystem/grpc-gateway/runtime", "github.com/jsimonetti/berkeleydb", "github.com/kevinburke/nacl", diff --git a/README.md b/README.md index 6e23e4c..66c5d91 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Quorum Slack Build Status +Go Report Card Data privacy for Quorum. @@ -95,21 +96,23 @@ make setup && make Usage of ./bin/crux: crux.config Optional config file --alwayssendto string List of public keys for nodes to send all transactions too - --berkeleydb Use Berkeley DB for storage + --berkeleydb Use Berkeley DB for working with an existing Constellation data store [experimental] --generate-keys string Generate a new keypair - --othernodes string `Boot nodes` to connect to to discover the network + --grpc Use gRPC server (default true) + --grpcport int The local port to listen on for JSON extensions of gRPC (default -1) + --othernodes string "Boot nodes" to connect to to discover the network --port int The local port to listen on (default -1) --privatekeys string Private keys hosted by this node --publickeys string Public keys hosted by this node - --socket string IPC socket to create for access to the Private API - --storage string Database storage file name (default `crux.db`) + --socket string IPC socket to create for access to the Private API (default "crux.ipc") + --storage string Database storage file name (default "crux.db") + --tls Use TLS to secure HTTP communications + --tlsservercert string The server certificate to be used + --tlsserverkey string The server private key --url string The URL to advertise to other nodes (reachable by them) + -v, --v int Verbosity level of logs (shorthand) (default 1) --verbosity int Verbosity level of logs (default 1) - --workdir string The folder to put stuff in (default: .) (default `.`) - --grpc Use protobuf + gRPC for communication between nodes (default `true`) - --tls Use TLS to secure HTTP communications - --tlsservercert TLS server certificate - --tlsserverkey TLS server key + --workdir string The folder to put stuff in (default: .) (default ".") ``` ## How does it work? diff --git a/api/client.go b/api/client.go index c3ee76f..f56efe3 100644 --- a/api/client.go +++ b/api/client.go @@ -4,33 +4,33 @@ package api // recipients. type SendRequest struct { // Payload is the transaction payload data we wish to store. - Payload string `json:"payload"` + Payload string `json:"payload"` // From is the sender node identification. - From string `json:"from"` + From string `json:"from"` // To is a list of the recipient nodes that should be privy to this transaction payload. - To []string `json:"to"` + To []string `json:"to"` } // SendResponse is the response to the SendRequest type SendResponse struct { // Key is the key that can be used to retrieve the submitted transaction. - Key string `json:"key"` + Key string `json:"key"` } // ReceiveRequest type ReceiveRequest struct { - Key string `json:"key"` - To string `json:"to"` + Key string `json:"key"` + To string `json:"to"` } // ReceiveResponse returns the raw payload associated with the ReceiveRequest. type ReceiveResponse struct { - Payload string `json:"payload"` + Payload string `json:"payload"` } // DeleteRequest deletes the entry matching the given key from the enclave. type DeleteRequest struct { - Key string `json:"key"` + Key string `json:"key"` } // ResendRequest is used to resend previous transactions. @@ -40,26 +40,26 @@ type DeleteRequest struct { type ResendRequest struct { // Type is the resend request type. It should be either "all" or "individual" depending on if // you want to request an individual transaction, or all transactions associated with a node. - Type string `json:"type"` - PublicKey string `json:"publicKey"` - Key string `json:"key,omitempty"` + Type string `json:"type"` + PublicKey string `json:"publicKey"` + Key string `json:"key,omitempty"` } type UpdatePartyInfo struct { - Url string `json:"url"` - Recipients map[string][]byte `json:"recipients"` - Parties map[string]bool `json:"parties"` + Url string `json:"url"` + Recipients map[string][]byte `json:"recipients"` + Parties map[string]bool `json:"parties"` } type PartyInfoResponse struct { Payload []byte `json:"payload"` } type PrivateKeyBytes struct { - Bytes string `json:"bytes"` + Bytes string `json:"bytes"` } // PrivateKey is a container for a private key. type PrivateKey struct { - Data PrivateKeyBytes `json:"data"` - Type string `json:"type"` + Data PrivateKeyBytes `json:"data"` + Type string `json:"type"` } diff --git a/api/encoding.go b/api/encoding.go index 2e3880d..d1edb9d 100644 --- a/api/encoding.go +++ b/api/encoding.go @@ -1,9 +1,9 @@ package api import ( - "github.com/kevinburke/nacl" "encoding/binary" "github.com/blk-io/crux/utils" + "github.com/kevinburke/nacl" ) func EncodePayload(ep EncryptedPayload) []byte { @@ -23,8 +23,8 @@ func EncodePayload(ep EncryptedPayload) []byte { func DecodePayload(encoded []byte) EncryptedPayload { ep := EncryptedPayload{ - Sender: new([nacl.KeySize]byte), - Nonce: new([nacl.NonceSize]byte), + Sender: new([nacl.KeySize]byte), + Nonce: new([nacl.NonceSize]byte), RecipientNonce: new([nacl.NonceSize]byte), } @@ -128,7 +128,7 @@ func writeInt(v int, dest []byte, offset int) ([]byte, int) { func confirmCapacity(dest []byte, offset, required int) []byte { length := len(dest) - if length - offset < required { + if length-offset < required { var newLength int if required > length { newLength = utils.NextPowerOf2(required) @@ -157,14 +157,14 @@ func writeSlice(src []byte, dest []byte, offset int) ([]byte, int) { func readSliceToArray(src []byte, offset int, dest []byte) int { var length int length, offset = readInt(src, offset) - offset += copy(dest, src[offset:offset + length]) + offset += copy(dest, src[offset:offset+length]) return offset } func readSlice(src []byte, offset int) ([]byte, int) { var length int length, offset = readInt(src, offset) - return src[offset:offset + length], offset + length + return src[offset : offset+length], offset + length } func writeSliceOfSlice(src [][]byte, dest []byte, offset int) ([]byte, int) { @@ -186,8 +186,8 @@ func readSliceOfSlice(src []byte, offset int) ([][]byte, int) { var length int length, offset = readInt(src, offset) result[i] = append( - result[i], src[offset:offset + length]...) + result[i], src[offset:offset+length]...) offset += length } return result, offset -} \ No newline at end of file +} diff --git a/api/encoding_test.go b/api/encoding_test.go index 8a07141..54362cf 100644 --- a/api/encoding_test.go +++ b/api/encoding_test.go @@ -1,19 +1,19 @@ package api import ( + "github.com/blk-io/crux/utils" + "github.com/kevinburke/nacl" "reflect" "testing" - "github.com/kevinburke/nacl" - "github.com/blk-io/crux/utils" ) func TestEncodePayload(t *testing.T) { epl := EncryptedPayload{ - Sender: nacl.NewKey(), - CipherText: []byte("C1ph3r T3xt"), - Nonce: nacl.NewNonce(), - RecipientBoxes: [][]byte{ []byte("B0x1"), []byte("B0x2") }, + Sender: nacl.NewKey(), + CipherText: []byte("C1ph3r T3xt"), + Nonce: nacl.NewNonce(), + RecipientBoxes: [][]byte{[]byte("B0x1"), []byte("B0x2")}, RecipientNonce: nacl.NewNonce(), } @@ -29,17 +29,17 @@ func TestEncodePayloadWithRecipients(t *testing.T) { epls := []EncryptedPayload{ { - Sender: nacl.NewKey(), - CipherText: []byte("C1ph3r T3xt1"), - Nonce: nacl.NewNonce(), - RecipientBoxes: [][]byte{ []byte("B0x1"), []byte("B0x2"), []byte("B0x3") }, + Sender: nacl.NewKey(), + CipherText: []byte("C1ph3r T3xt1"), + Nonce: nacl.NewNonce(), + RecipientBoxes: [][]byte{[]byte("B0x1"), []byte("B0x2"), []byte("B0x3")}, RecipientNonce: nacl.NewNonce(), }, { - Sender: nacl.NewKey(), - CipherText: []byte("C1ph3r T3xt2"), - Nonce: nacl.NewNonce(), - RecipientBoxes: [][]byte{ []byte("B0x1") }, + Sender: nacl.NewKey(), + CipherText: []byte("C1ph3r T3xt2"), + Nonce: nacl.NewNonce(), + RecipientBoxes: [][]byte{[]byte("B0x1")}, RecipientNonce: nacl.NewNonce(), }, } @@ -68,7 +68,6 @@ func TestEncodePayloadWithRecipients(t *testing.T) { } } - func TestEncodePartyInfo(t *testing.T) { pi := PartyInfo{ @@ -112,4 +111,4 @@ func runEncodePartyInfoTest(t *testing.T, pi PartyInfo) { func toKey(encodedKey string) [nacl.KeySize]byte { key, _ := utils.LoadBase64Key(encodedKey) return *key -} \ No newline at end of file +} diff --git a/api/internal.go b/api/internal.go index 7646655..7d490d6 100644 --- a/api/internal.go +++ b/api/internal.go @@ -2,21 +2,21 @@ package api import ( "bytes" - "io/ioutil" - "math/rand" - "net/http" - "time" - log "github.com/sirupsen/logrus" - "github.com/kevinburke/nacl" - "github.com/blk-io/crux/utils" "encoding/hex" "encoding/json" - "net/http/httputil" "fmt" "github.com/blk-io/chimera-api/chimera" + "github.com/blk-io/crux/utils" + "github.com/kevinburke/nacl" + log "github.com/sirupsen/logrus" "golang.org/x/net/context" "google.golang.org/grpc" + "io/ioutil" + "math/rand" + "net/http" + "net/http/httputil" "net/url" + "time" ) // EncryptedPayload is the struct used for storing all data associated with an encrypted @@ -31,11 +31,11 @@ type EncryptedPayload struct { // PartyInfo is a struct that stores details of all enclave nodes (or parties) on the network. type PartyInfo struct { - url string // URL identifying this node - recipients map[[nacl.KeySize]byte]string // public key -> URL - parties map[string]bool // Node (or party) URLs + url string // URL identifying this node + recipients map[[nacl.KeySize]byte]string // public key -> URL + parties map[string]bool // Node (or party) URLs client utils.HttpClient - grpc bool + grpc bool } // GetRecipient retrieves the URL associated with the provided recipient. @@ -60,7 +60,7 @@ func InitPartyInfo(rawUrl string, otherNodes []string, client utils.HttpClient, recipients: make(map[[nacl.KeySize]byte]string), parties: parties, client: client, - grpc: grpc, + grpc: grpc, } } @@ -95,7 +95,7 @@ func (s *PartyInfo) RegisterPublicKeys(pubKeys []nacl.Key) { func (s *PartyInfo) GetPartyInfoGrpc() { recipients := make(map[string][]byte) - for key, url := range s.recipients{ + for key, url := range s.recipients { recipients[url] = key[:] } urls := make(map[string]bool) @@ -103,7 +103,7 @@ func (s *PartyInfo) GetPartyInfoGrpc() { urls[k] = v } - for rawUrl := range urls{ + for rawUrl := range urls { if rawUrl == s.url { continue } @@ -116,11 +116,11 @@ func (s *PartyInfo) GetPartyInfoGrpc() { } defer conn.Close() cli := chimera.NewClientClient(conn) - if cli == nil{ + if cli == nil { log.Errorf("Client is not intialised") continue } - party := chimera.PartyInfo{Url:rawUrl, Recipients:recipients, Parties:s.parties} + party := chimera.PartyInfo{Url: rawUrl, Recipients: recipients, Parties: s.parties} partyInfoResp, err := cli.UpdatePartyInfo(context.Background(), &party) if err != nil { @@ -136,6 +136,7 @@ func (s *PartyInfo) GetPartyInfoGrpc() { } } } + // GetPartyInfo requests PartyInfo data from all remote nodes this node is aware of. The data // provided in each response is applied to this node. func (s *PartyInfo) GetPartyInfo() { @@ -220,14 +221,14 @@ func (s *PartyInfo) updatePartyInfo(resp *http.Response, rawUrl string) error { return nil } -func (s *PartyInfo) getEncoded(encodedPartyInfo []byte) []byte{ +func (s *PartyInfo) getEncoded(encodedPartyInfo []byte) []byte { if s.grpc { recipients := make(map[string][]byte) - for key, url := range s.recipients{ + for key, url := range s.recipients { recipients[url] = key[:] } e, err := json.Marshal(UpdatePartyInfo{s.url, recipients, s.parties}) - if err != nil{ + if err != nil { log.Errorf("Marshalling failed %v", err) return nil } @@ -245,9 +246,9 @@ func (s *PartyInfo) PollPartyInfo() { go func() { for { select { - case <- ticker.C: + case <-ticker.C: s.GetPartyInfo() - case <- quit: + case <-quit: ticker.Stop() return } @@ -310,7 +311,7 @@ func PushGrpc(encoded []byte, path string, epl EncryptedPayload) error { } defer conn.Close() cli := chimera.NewClientClient(conn) - if cli == nil{ + if cli == nil { log.Fatalf("Client is not intialised") } @@ -322,20 +323,21 @@ func PushGrpc(encoded []byte, path string, epl EncryptedPayload) error { copy(nonce[:], (*epl.Nonce)[:]) copy(recipientNonce[:], (*epl.RecipientNonce)[:]) encrypt := chimera.EncryptedPayload{ - Sender:sender[:], - CipherText:epl.CipherText, - Nonce:nonce[:], - ReciepientNonce:recipientNonce[:], - ReciepientBoxes:epl.RecipientBoxes, + Sender: sender[:], + CipherText: epl.CipherText, + Nonce: nonce[:], + ReciepientNonce: recipientNonce[:], + ReciepientBoxes: epl.RecipientBoxes, } - pushPayload := chimera.PushPayload{Ep:&encrypt, Encoded:encoded} + pushPayload := chimera.PushPayload{Ep: &encrypt, Encoded: encoded} _, err = cli.Push(context.Background(), &pushPayload) - if err != nil{ + if err != nil { log.Errorf("Push failed with %s", err) return err } return nil } + // Push is responsible for propagating the encoded payload to the given remote node. func Push(encoded []byte, url string, client utils.HttpClient) (string, error) { diff --git a/api/internal_test.go b/api/internal_test.go index 5e3145d..29eb55f 100644 --- a/api/internal_test.go +++ b/api/internal_test.go @@ -1,9 +1,9 @@ package api import ( - "testing" - "net/http" "github.com/kevinburke/nacl" + "net/http" + "testing" ) func TestRegisterPublicKeys(t *testing.T) { @@ -21,7 +21,7 @@ func TestRegisterPublicKeys(t *testing.T) { pi.RegisterPublicKeys(expKey) url, ok := pi.GetRecipient(expKey[0]) - if !ok || url != expUrl{ + if !ok || url != expUrl { t.Errorf("Url is %s whereas %s is expected", url, expUrl) } diff --git a/config/config.go b/config/config.go index ea4b2fa..5adde2c 100644 --- a/config/config.go +++ b/config/config.go @@ -3,41 +3,42 @@ package config import ( "flag" + "fmt" "github.com/spf13/pflag" "github.com/spf13/viper" "os" - "fmt" ) const ( - Verbosity = "verbosity" - AlwaysSendTo = "alwayssendto" - Storage = "storage" - WorkDir = "workdir" - Url = "url" - OtherNodes = "othernodes" - PublicKeys = "publickeys" - PrivateKeys = "privatekeys" - Port = "port" - Socket = "socket" + Verbosity = "verbosity" + VerbosityShorthand = "v" + AlwaysSendTo = "alwayssendto" + Storage = "storage" + WorkDir = "workdir" + Url = "url" + OtherNodes = "othernodes" + PublicKeys = "publickeys" + PrivateKeys = "privatekeys" + Port = "port" + Socket = "socket" GenerateKeys = "generate-keys" - BerkeleyDb = "berkeleydb" - UseGRPC = "grpc" + BerkeleyDb = "berkeleydb" + UseGRPC = "grpc" GrpcJsonPort = "grpcport" - Tls = "tls" - TlsServerChain = "tlsserverchain" - TlsServerTrust = "tlsservertrust" + Tls = "tls" + TlsServerChain = "tlsserverchain" + TlsServerTrust = "tlsservertrust" TlsKnownServers = "tlsknownservers" - TlsClientCert = "tlsclientcert" - TlsServerCert = "tlsservercert" + TlsClientCert = "tlsclientcert" + TlsServerCert = "tlsservercert" TlsKnownClients = "tlsknownclients" - TlsClientChain = "tlsclientchain" - TlsClientKey = "tlsclientkey" - TlsClientTrust = "tlsclienttrust" - TlsServerKey = "tlsserverkey" + TlsClientChain = "tlsclientchain" + TlsClientKey = "tlsclientkey" + TlsClientTrust = "tlsclienttrust" + TlsServerKey = "tlsserverkey" ) // InitFlags initializes all supported command line flags. @@ -45,7 +46,7 @@ func InitFlags() { flag.String(GenerateKeys, "", "Generate a new keypair") flag.String(Url, "", "The URL to advertise to other nodes (reachable by them)") flag.Int(Port, -1, "The local port to listen on") - flag.String(WorkDir, ".", "The folder to put stuff in (default: .)") + flag.String(WorkDir, ".", "The folder to put stuff in ") flag.String(Socket, "crux.ipc", "IPC socket to create for access to the Private API") flag.String(OtherNodes, "", "\"Boot nodes\" to connect to to discover the network") flag.String(PublicKeys, "", "Public keys hosted by this node") @@ -55,6 +56,7 @@ func InitFlags() { "Use Berkeley DB for working with an existing Constellation data store [experimental]") flag.Int(Verbosity, 1, "Verbosity level of logs") + flag.Int(VerbosityShorthand, 1, "Verbosity level of logs (shorthand)") flag.String(AlwaysSendTo, "", "List of public keys for nodes to send all transactions too") flag.Bool(UseGRPC, true, "Use gRPC server") flag.Bool(Tls, false, "Use TLS to secure HTTP communications") @@ -65,7 +67,7 @@ func InitFlags() { // storage not currently supported as we use LevelDB pflag.CommandLine.AddGoFlagSet(flag.CommandLine) - viper.BindPFlags(pflag.CommandLine) // Binding the flags to test the initial configuration + viper.BindPFlags(pflag.CommandLine) // Binding the flags to test the initial configuration } // Usage prints usage instructions to the console. @@ -107,5 +109,3 @@ func GetString(key string) string { func GetStringSlice(key string) []string { return viper.GetStringSlice(key) } - - diff --git a/config/config_test.go b/config/config_test.go index faabe76..1c4bc7b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,8 +1,8 @@ package config import ( - "testing" "reflect" + "testing" ) const configFile = "config_testdata.conf" @@ -38,27 +38,27 @@ func TestLoadConfig(t *testing.T) { conf := AllSettings() expected := map[string]interface{}{ - Verbosity: 1, - AlwaysSendTo: []interface{}{}, - TlsServerChain: []interface{}{}, - Storage: "dir:storage", - WorkDir: "data", - Url: "http://127.0.0.1:9001/", - TlsServerTrust: "tofu", - PublicKeys: []interface{}{"foo.pub"}, - OtherNodes: []interface{}{"http://127.0.0.1:9000/"}, - TlsKnownServers:"tls-known-servers", - TlsClientCert: "tls-client-cert.pem", - PrivateKeys: []interface{}{"foo.key"}, - TlsServerCert: "tls-server-cert.pem", - Tls: "strict", + Verbosity: 1, + AlwaysSendTo: []interface{}{}, + TlsServerChain: []interface{}{}, + Storage: "dir:storage", + WorkDir: "data", + Url: "http://127.0.0.1:9001/", + TlsServerTrust: "tofu", + PublicKeys: []interface{}{"foo.pub"}, + OtherNodes: []interface{}{"http://127.0.0.1:9000/"}, + TlsKnownServers: "tls-known-servers", + TlsClientCert: "tls-client-cert.pem", + PrivateKeys: []interface{}{"foo.key"}, + TlsServerCert: "tls-server-cert.pem", + Tls: "strict", TlsKnownClients: "tls-known-clients", - TlsClientChain: []interface{}{}, - TlsClientKey: "tls-client-key.pem", - Socket: "constellation.ipc", - TlsClientTrust: "ca-or-tofu", - TlsServerKey: "tls-server-key.pem", - Port: 9001, + TlsClientChain: []interface{}{}, + TlsClientKey: "tls-client-key.pem", + Socket: "constellation.ipc", + TlsClientTrust: "ca-or-tofu", + TlsServerKey: "tls-server-key.pem", + Port: 9001, } verifyConfig(t, conf, expected) @@ -69,7 +69,7 @@ func verifyConfig(t *testing.T, conf map[string]interface{}, expected map[string //if conf[key] != value { if actV, ok := conf[expK]; ok { var eq bool - switch actV.(type) { // we cannot use == for equality with []interface{} + switch actV.(type) { // we cannot use == for equality with []interface{} case []interface{}: eq = reflect.DeepEqual(actV, expV) default: @@ -92,4 +92,4 @@ func TestGetIntConfig(t *testing.T) { if GetInt(Port) != 9001 { t.Errorf("Port num 9001 is expected but we got %d", GetInt(Port)) } -} \ No newline at end of file +} diff --git a/crux.go b/crux.go index c6c1623..2920ccb 100644 --- a/crux.go +++ b/crux.go @@ -1,16 +1,16 @@ package main import ( - "os" - "path" - "net/http" - "strings" "github.com/blk-io/crux/api" "github.com/blk-io/crux/config" "github.com/blk-io/crux/enclave" "github.com/blk-io/crux/server" "github.com/blk-io/crux/storage" log "github.com/sirupsen/logrus" + "net/http" + "os" + "path" + "strings" "time" ) @@ -34,7 +34,13 @@ func main() { } config.ParseCommandLine() - verbosity := config.GetInt(config.Verbosity) + verbosity := 1 + if config.GetInt(config.Verbosity) > config.GetInt(config.VerbosityShorthand) { + verbosity = config.GetInt(config.Verbosity) + } else { + verbosity = config.GetInt(config.VerbosityShorthand) + } + var level log.Level switch verbosity { @@ -77,7 +83,8 @@ func main() { } defer db.Close() - otherNodes := config.GetStringSlice(config.OtherNodes) + allOtherNodes := config.GetString(config.OtherNodes) + otherNodes := strings.Split(allOtherNodes, ",") url := config.GetString(config.Url) if url == "" { log.Fatalln("URL must be specified") @@ -93,8 +100,10 @@ func main() { pi := api.InitPartyInfo(url, otherNodes, httpClient, grpc) - privKeyFiles := config.GetStringSlice(config.PrivateKeys) - pubKeyFiles := config.GetStringSlice(config.PublicKeys) + privKeys := config.GetString(config.PrivateKeys) + pubKeys := config.GetString(config.PublicKeys) + pubKeyFiles := strings.Split(pubKeys, ",") + privKeyFiles := strings.Split(privKeys, ",") if len(privKeyFiles) != len(pubKeyFiles) { log.Fatalln("Private keys provided must have corresponding public keys") diff --git a/enclave/enclave.go b/enclave/enclave.go index 99b3606..91e5253 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -4,33 +4,33 @@ package enclave import ( "bytes" "crypto/rand" - "encoding/json" "encoding/base64" "encoding/hex" + "encoding/json" "errors" "fmt" - "io/ioutil" - "path/filepath" - "strings" + "github.com/blk-io/crux/api" + "github.com/blk-io/crux/storage" + "github.com/blk-io/crux/utils" "github.com/kevinburke/nacl" "github.com/kevinburke/nacl/box" "github.com/kevinburke/nacl/secretbox" log "github.com/sirupsen/logrus" - "github.com/blk-io/crux/storage" - "github.com/blk-io/crux/api" - "github.com/blk-io/crux/utils" + "io/ioutil" + "path/filepath" + "strings" ) // SecureEnclave is the secure transaction enclave. type SecureEnclave struct { - Db storage.DataStore // The underlying key-value datastore for encrypted transactions - PubKeys []nacl.Key // Public keys associated with this enclave - PrivKeys []nacl.Key // Private keys associated with this enclave - selfPubKey nacl.Key // An ephemeral key used for transactions only intended for this enclave - PartyInfo api.PartyInfo // Details of all other nodes (or parties) on the network - keyCache map[nacl.Key]map[nacl.Key]nacl.Key // Maps sender -> recipient -> shared key - client utils.HttpClient // The underlying HTTP client used to propagate requests - grpc bool + Db storage.DataStore // The underlying key-value datastore for encrypted transactions + PubKeys []nacl.Key // Public keys associated with this enclave + PrivKeys []nacl.Key // Private keys associated with this enclave + selfPubKey nacl.Key // An ephemeral key used for transactions only intended for this enclave + PartyInfo api.PartyInfo // Details of all other nodes (or parties) on the network + keyCache map[nacl.Key]map[nacl.Key]nacl.Key // Maps sender -> recipient -> shared key + client utils.HttpClient // The underlying HTTP client used to propagate requests + grpc bool } // Init creates a new instance of the SecureEnclave. @@ -55,12 +55,12 @@ func Init( } enc := SecureEnclave{ - Db : db, - PubKeys: pubKeys, - PrivKeys: privKeys, + Db: db, + PubKeys: pubKeys, + PrivKeys: privKeys, PartyInfo: pi, - client: client, - grpc: grpc, + client: client, + grpc: grpc, } // We use shared keys for encrypting data. The keys between a specific sender and recipient are @@ -108,32 +108,32 @@ func Init( func (s *SecureEnclave) Store( message *[]byte, sender []byte, recipients [][]byte) ([]byte, error) { - var err error - var senderPubKey, senderPrivKey nacl.Key + var err error + var senderPubKey, senderPrivKey nacl.Key - if len(sender) == 0 { - // from address is either default or specified on communication - senderPubKey = s.PubKeys[0] - senderPrivKey = s.PrivKeys[0] - } else { - senderPubKey, err = utils.ToKey(sender) - if err != nil { - log.WithField("senderPubKey", sender).Errorf( - "Unable to load sender public key, %v", err) - return nil, err - } - - senderPrivKey, err = s.resolvePrivateKey(senderPubKey) - if err != nil { - log.WithField("senderPubKey", sender).Errorf( - "Unable to locate private key for sender public key, %v", err) - return nil, err - } + if len(sender) == 0 { + // from address is either default or specified on communication + senderPubKey = s.PubKeys[0] + senderPrivKey = s.PrivKeys[0] + } else { + senderPubKey, err = utils.ToKey(sender) + if err != nil { + log.WithField("senderPubKey", sender).Errorf( + "Unable to load sender public key, %v", err) + return nil, err } - return s.store(message, senderPubKey, senderPrivKey, recipients) + senderPrivKey, err = s.resolvePrivateKey(senderPubKey) + if err != nil { + log.WithField("senderPubKey", sender).Errorf( + "Unable to locate private key for sender public key, %v", err) + return nil, err + } } + return s.store(message, senderPubKey, senderPrivKey, recipients) +} + func (s *SecureEnclave) store( message *[]byte, senderPubKey, senderPrivKey nacl.Key, @@ -181,7 +181,7 @@ func (s *SecureEnclave) store( sharedKey := s.resolveSharedKey(senderPrivKey, senderPubKey, recipientKey) sealedBox := sealPayload(epl.RecipientNonce, masterKey, sharedKey) - epl.RecipientBoxes = [][]byte{ sealedBox } + epl.RecipientBoxes = [][]byte{sealedBox} encodedEpl := api.EncodePayloadWithRecipients(epl, recipients) digest, err := s.storePayload(epl, encodedEpl) @@ -197,7 +197,7 @@ func (s *SecureEnclave) store( } log.WithFields(log.Fields{ - "recipient": hex.EncodeToString(recipient),"digest": hex.EncodeToString(digest), + "recipient": hex.EncodeToString(recipient), "digest": hex.EncodeToString(digest), }).Debug("Publishing payload") s.publishPayload(recipientEpl, recipient) @@ -216,7 +216,7 @@ func createEncryptedPayload( sealedMessage := secretbox.Seal([]byte{}, *message, nonce, masterKey) - return api.EncryptedPayload { + return api.EncryptedPayload{ Sender: senderPubKey, CipherText: sealedMessage, Nonce: nonce, @@ -445,7 +445,7 @@ func (s *SecureEnclave) GetEncodedPartyInfoGrpc() []byte { return encoded } -func (s *SecureEnclave) GetPartyInfo() (string, map[[nacl.KeySize]byte]string, map[string]bool){ +func (s *SecureEnclave) GetPartyInfo() (string, map[[nacl.KeySize]byte]string, map[string]bool) { return s.PartyInfo.GetAllValues() } @@ -517,7 +517,7 @@ func DoKeyGeneration(keyFile string) error { b64PubKey := base64.StdEncoding.EncodeToString((*pubKey)[:]) b64PrivKey := base64.StdEncoding.EncodeToString((*privKey)[:]) - err = ioutil.WriteFile(keyFile + ".pub", []byte(b64PubKey), 0600) + err = ioutil.WriteFile(keyFile+".pub", []byte(b64PubKey), 0600) if err != nil { return fmt.Errorf("unable to write public key: %s, error: %v", keyFile, err) } @@ -535,7 +535,7 @@ func DoKeyGeneration(keyFile string) error { return fmt.Errorf("unable to encode private key: %v, error: %v", jsonKey, err) } - err = ioutil.WriteFile(keyFile + ".key", encoded, 0600) + err = ioutil.WriteFile(keyFile+".key", encoded, 0600) if err != nil { return fmt.Errorf("unable to write private key: %s, error: %v", keyFile, err) } diff --git a/enclave/enclave_test.go b/enclave/enclave_test.go index 12c8357..10ea595 100644 --- a/enclave/enclave_test.go +++ b/enclave/enclave_test.go @@ -2,6 +2,10 @@ package enclave import ( "bytes" + "github.com/blk-io/crux/api" + "github.com/blk-io/crux/storage" + "github.com/blk-io/crux/utils" + "github.com/kevinburke/nacl" "io/ioutil" "net/http" "os" @@ -9,17 +13,13 @@ import ( "sync" "testing" "time" - "github.com/blk-io/crux/storage" - "github.com/blk-io/crux/api" - "github.com/blk-io/crux/utils" - "github.com/kevinburke/nacl" ) var message = []byte("Test message") type MockClient struct { serviceMu sync.Mutex - requests [][]byte + requests [][]byte } func (c *MockClient) Do(req *http.Request) (*http.Response, error) { @@ -111,7 +111,7 @@ func TestStoreAndRetrieve(t *testing.T) { if !bytes.Equal(message, returned) { t.Errorf( - "Retrieved message is not the same as original:\n" + + "Retrieved message is not the same as original:\n"+ "Original: %v\nRetrieved: %v", message, returned) } @@ -162,7 +162,7 @@ func TestStoreAndRetrieve(t *testing.T) { if !bytes.Equal(message, returned2) { t.Errorf( - "Retrieved message is not the same as original:\n" + + "Retrieved message is not the same as original:\n"+ "Original: %v\nRetrieved: %v", message, returned) } @@ -189,7 +189,7 @@ func TestStoreAndRetrieveSelf(t *testing.T) { if !bytes.Equal(message, returned) { t.Errorf( - "Retrieved message is not the same as original:\n" + + "Retrieved message is not the same as original:\n"+ "Original: %v\nRetrieved: %v", message, returned) } @@ -270,7 +270,7 @@ func TestRetrieveNotAuthorised(t *testing.T) { if !bytes.Equal(message, returned) { t.Errorf( - "Retrieved message is not the same as original:\n" + + "Retrieved message is not the same as original:\n"+ "Original: %v\nRetrieved: %v", message, returned) } @@ -297,7 +297,7 @@ func TestDelete(t *testing.T) { if !bytes.Equal(message, returned) { t.Errorf( - "Retrieved message is not the same as original:\n" + + "Retrieved message is not the same as original:\n"+ "Original: %v\nRetrieved: %v", message, returned) } diff --git a/server/proto_server.go b/server/proto_server.go index d8f6fb2..b6f0362 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -2,15 +2,15 @@ package server import ( "fmt" - "google.golang.org/grpc" + "github.com/blk-io/chimera-api/chimera" + "github.com/blk-io/crux/utils" "github.com/grpc-ecosystem/grpc-gateway/runtime" log "github.com/sirupsen/logrus" "golang.org/x/net/context" - "net/http" - "github.com/blk-io/crux/utils" - "net" + "google.golang.org/grpc" "google.golang.org/grpc/credentials" - "github.com/blk-io/chimera-api/chimera" + "net" + "net/http" ) func (tm *TransactionManager) startRpcServer(port int, grpcJsonPort int, ipcPath string, tls bool, certFile, keyFile string) error { @@ -18,7 +18,7 @@ func (tm *TransactionManager) startRpcServer(port int, grpcJsonPort int, ipcPath if err != nil { log.Fatalf("failed to listen: %v", err) } - s := Server{Enclave : tm.Enclave} + s := Server{Enclave: tm.Enclave} grpcServer := grpc.NewServer() chimera.RegisterClientServer(grpcServer, &s) go func() { @@ -73,7 +73,7 @@ func (tm *TransactionManager) startRestServer(port int) error { if err != nil { panic(err) } - s := Server{Enclave : tm.Enclave} + s := Server{Enclave: tm.Enclave} grpcServer := grpc.NewServer() chimera.RegisterClientServer(grpcServer, &s) go func() { @@ -82,7 +82,7 @@ func (tm *TransactionManager) startRestServer(port int) error { return nil } -func (tm *TransactionManager) startJsonServerTLS(port int, grpcJsonPort int, certFile, keyFile,ca string) error { +func (tm *TransactionManager) startJsonServerTLS(port int, grpcJsonPort int, certFile, keyFile, ca string) error { address := fmt.Sprintf("%s:%d", "localhost", grpcJsonPort) ctx := context.Background() ctx, cancel := context.WithCancel(ctx) @@ -99,13 +99,13 @@ func (tm *TransactionManager) startJsonServerTLS(port int, grpcJsonPort int, cer return nil } -func (tm *TransactionManager) startRestServerTLS(port int, certFile, keyFile,ca string) error { +func (tm *TransactionManager) startRestServerTLS(port int, certFile, keyFile, ca string) error { grpcAddress := fmt.Sprintf("%s:%d", "localhost", port) lis, err := net.Listen("tcp", grpcAddress) if err != nil { log.Fatalf("failed to start gRPC REST server: %s", err) } - s := Server{Enclave : tm.Enclave} + s := Server{Enclave: tm.Enclave} creds, err := credentials.NewServerTLSFromFile(certFile, keyFile) opts := []grpc.ServerOption{grpc.Creds(creds)} if err != nil { @@ -131,4 +131,4 @@ func GetFreePort() (int, error) { } defer l.Close() return l.Addr().(*net.TCPAddr).Port, nil -} \ No newline at end of file +} diff --git a/server/server.go b/server/server.go index 8684be3..e9a8b1f 100644 --- a/server/server.go +++ b/server/server.go @@ -3,19 +3,19 @@ package server import ( "encoding/base64" + "encoding/hex" "encoding/json" "fmt" - "io/ioutil" - "net/http" - "strconv" "github.com/blk-io/crux/api" "github.com/blk-io/crux/utils" + "github.com/kevinburke/nacl" log "github.com/sirupsen/logrus" - "encoding/hex" - "net/textproto" + "io/ioutil" + "net/http" "net/http/httputil" + "net/textproto" "os" - "github.com/kevinburke/nacl" + "strconv" ) // Enclave is the interface used by the transaction enclaves. @@ -76,7 +76,7 @@ func requestLogger(handler http.Handler) http.Handler { // Init initializes a new TransactionManager instance. func Init(enc Enclave, port int, ipcPath string, grpc bool, grpcJsonPort int, tls bool, certFile, keyFile string) (TransactionManager, error) { - tm := TransactionManager{Enclave : enc} + tm := TransactionManager{Enclave: enc} var err error if grpc == true { err = tm.startRpcServer(port, grpcJsonPort, ipcPath, tls, certFile, keyFile) @@ -97,7 +97,7 @@ func (tm *TransactionManager) startHttpserver(port int, ipcPath string, tls bool httpServer.HandleFunc(partyInfo, tm.partyInfo) serverUrl := "localhost:" + strconv.Itoa(port) - if tls{ + if tls { err := CheckCertFiles(certFile, keyFile) if err != nil { log.Fatal(err) @@ -224,9 +224,9 @@ func (s *TransactionManager) processSend( payload *[]byte) ([]byte, error) { log.WithFields(log.Fields{ - "b64From" : b64from, + "b64From": b64from, "b64Recipients": b64recipients, - "payload": hex.EncodeToString(*payload),}).Debugf( + "payload": hex.EncodeToString(*payload)}).Debugf( "Processing send request") sender, err := base64.StdEncoding.DecodeString(b64from) @@ -421,4 +421,3 @@ func internalServerError(w http.ResponseWriter, message string) { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, message) } - diff --git a/server/server_handler.go b/server/server_handler.go index 88bfedf..cd6fb61 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -1,15 +1,15 @@ package server import ( - "golang.org/x/net/context" - log "github.com/sirupsen/logrus" "encoding/base64" "encoding/hex" - "fmt" - "github.com/kevinburke/nacl" "encoding/json" - "github.com/blk-io/crux/api" + "fmt" "github.com/blk-io/chimera-api/chimera" + "github.com/blk-io/crux/api" + "github.com/kevinburke/nacl" + log "github.com/sirupsen/logrus" + "golang.org/x/net/context" ) type Server struct { @@ -17,11 +17,11 @@ type Server struct { } func (s *Server) Version(ctx context.Context, in *chimera.ApiVersion) (*chimera.ApiVersion, error) { - return &chimera.ApiVersion{Version:apiVersion}, nil + return &chimera.ApiVersion{Version: apiVersion}, nil } func (s *Server) Upcheck(ctx context.Context, in *chimera.UpCheckResponse) (*chimera.UpCheckResponse, error) { - return &chimera.UpCheckResponse{Message:upCheckResponse}, nil + return &chimera.UpCheckResponse{Message: upCheckResponse}, nil } func (s *Server) Send(ctx context.Context, in *chimera.SendRequest) (*chimera.SendResponse, error) { key, err := s.processSend(in.GetFrom(), in.GetTo(), &in.Payload) @@ -36,9 +36,9 @@ func (s *Server) Send(ctx context.Context, in *chimera.SendRequest) (*chimera.Se func (s *Server) processSend(b64from string, b64recipients []string, payload *[]byte) ([]byte, error) { log.WithFields(log.Fields{ - "b64From" : b64from, + "b64From": b64from, "b64Recipients": b64recipients, - "payload": hex.EncodeToString(*payload),}).Debugf( + "payload": hex.EncodeToString(*payload)}).Debugf( "Processing send request") sender, err := base64.StdEncoding.DecodeString(b64from) @@ -87,7 +87,7 @@ func (s *Server) processReceive(b64Key []byte, b64To string) ([]byte, error) { func (s *Server) UpdatePartyInfo(ctx context.Context, in *chimera.PartyInfo) (*chimera.PartyInfoResponse, error) { recipients := make(map[[nacl.KeySize]byte]string) - for url, key := range in.Recipients{ + for url, key := range in.Recipients { var as [32]byte copy(as[:], key) recipients[as] = url @@ -96,13 +96,12 @@ func (s *Server) UpdatePartyInfo(ctx context.Context, in *chimera.PartyInfo) (*c encoded := s.Enclave.GetEncodedPartyInfoGrpc() var decodedPartyInfo chimera.PartyInfoResponse err := json.Unmarshal(encoded, &decodedPartyInfo) - if err != nil{ + if err != nil { log.Errorf("Unmarshalling failed with %v", err) } return &chimera.PartyInfoResponse{Payload: decodedPartyInfo.Payload}, nil } - func (s *Server) Push(ctx context.Context, in *chimera.PushPayload) (*chimera.PartyInfoResponse, error) { sender := new([nacl.KeySize]byte) nonce := new([nacl.NonceSize]byte) @@ -112,11 +111,11 @@ func (s *Server) Push(ctx context.Context, in *chimera.PushPayload) (*chimera.Pa copy((*recipientNonce)[:], in.Ep.ReciepientNonce) encyptedPayload := api.EncryptedPayload{ - Sender:sender, - CipherText:in.Ep.CipherText, - Nonce:nonce, - RecipientBoxes:in.Ep.ReciepientBoxes, - RecipientNonce:recipientNonce, + Sender: sender, + CipherText: in.Ep.CipherText, + Nonce: nonce, + RecipientBoxes: in.Ep.ReciepientBoxes, + RecipientNonce: recipientNonce, } digestHash, err := s.Enclave.StorePayloadGrpc(encyptedPayload, in.Encoded) diff --git a/server/server_test.go b/server/server_test.go index 8324bfd..9af45b0 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1,24 +1,24 @@ package server import ( - "testing" - "net/http" - "net/http/httptest" - "github.com/blk-io/crux/api" - "encoding/json" - "encoding/base64" "bytes" - "reflect" - "github.com/kevinburke/nacl" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/blk-io/chimera-api/chimera" + "github.com/blk-io/crux/api" "github.com/blk-io/crux/enclave" "github.com/blk-io/crux/storage" - "github.com/blk-io/chimera-api/chimera" + "github.com/kevinburke/nacl" + log "github.com/sirupsen/logrus" "golang.org/x/net/context" "google.golang.org/grpc" - "path" "io/ioutil" - "fmt" - log "github.com/sirupsen/logrus" + "net/http" + "net/http/httptest" + "path" + "reflect" + "testing" ) const sender = "BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo=" @@ -27,52 +27,52 @@ const receiver = "QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc=" var payload = []byte("payload") var encodedPayload = base64.StdEncoding.EncodeToString(payload) -type MockEnclave struct {} +type MockEnclave struct{} -func (s* MockEnclave) Store(message *[]byte, sender []byte, recipients [][]byte) ([]byte, error) { +func (s *MockEnclave) Store(message *[]byte, sender []byte, recipients [][]byte) ([]byte, error) { return *message, nil } -func (s* MockEnclave) StorePayload(encoded []byte) ([]byte, error) { +func (s *MockEnclave) StorePayload(encoded []byte) ([]byte, error) { return encoded, nil } -func (s* MockEnclave) StorePayloadGrpc(epl api.EncryptedPayload, encoded []byte) ([]byte, error) { +func (s *MockEnclave) StorePayloadGrpc(epl api.EncryptedPayload, encoded []byte) ([]byte, error) { return encoded, nil } -func (s* MockEnclave) Retrieve(digestHash *[]byte, to *[]byte) ([]byte, error) { +func (s *MockEnclave) Retrieve(digestHash *[]byte, to *[]byte) ([]byte, error) { return *digestHash, nil } -func (s* MockEnclave) RetrieveDefault(digestHash *[]byte) ([]byte, error) { +func (s *MockEnclave) RetrieveDefault(digestHash *[]byte) ([]byte, error) { return *digestHash, nil } -func (s* MockEnclave) RetrieveFor(digestHash *[]byte, reqRecipient *[]byte) (*[]byte, error) { +func (s *MockEnclave) RetrieveFor(digestHash *[]byte, reqRecipient *[]byte) (*[]byte, error) { return digestHash, nil } -func (s* MockEnclave) RetrieveAllFor(reqRecipient *[]byte) error { +func (s *MockEnclave) RetrieveAllFor(reqRecipient *[]byte) error { return nil } -func (s* MockEnclave) Delete(digestHash *[]byte) error { +func (s *MockEnclave) Delete(digestHash *[]byte) error { return nil } -func (s* MockEnclave) UpdatePartyInfo(encoded []byte) {} +func (s *MockEnclave) UpdatePartyInfo(encoded []byte) {} -func (s* MockEnclave) UpdatePartyInfoGrpc(string, map[[nacl.KeySize]byte]string, map[string]bool) {} +func (s *MockEnclave) UpdatePartyInfoGrpc(string, map[[nacl.KeySize]byte]string, map[string]bool) {} -func (s* MockEnclave) GetEncodedPartyInfo() []byte { +func (s *MockEnclave) GetEncodedPartyInfo() []byte { return payload } -func (s* MockEnclave) GetEncodedPartyInfoGrpc() []byte{ +func (s *MockEnclave) GetEncodedPartyInfoGrpc() []byte { return payload } -func (s *MockEnclave) GetPartyInfo() (string, map[[nacl.KeySize]byte]string, map[string]bool){ +func (s *MockEnclave) GetPartyInfo() (string, map[[nacl.KeySize]byte]string, map[string]bool) { return "", nil, nil } @@ -112,12 +112,12 @@ func TestSend(t *testing.T) { sendReqs := []api.SendRequest{ { Payload: encodedPayload, - From: sender, - To: []string{receiver}, + From: sender, + To: []string{receiver}, }, { Payload: encodedPayload, - To: []string{}, + To: []string{}, }, { Payload: encodedPayload, @@ -138,12 +138,12 @@ func TestGRPCSend(t *testing.T) { sendReqs := []chimera.SendRequest{ { Payload: payload, - From: sender, - To: []string{receiver}, + From: sender, + To: []string{receiver}, }, { Payload: payload, - To: []string{}, + To: []string{}, }, { Payload: payload, @@ -166,11 +166,11 @@ func TestGRPCSend(t *testing.T) { c := chimera.NewClientClient(conn) for _, sendReq := range sendReqs { - resp, err:= c.Send(context.Background(), &sendReq) + resp, err := c.Send(context.Background(), &sendReq) if err != nil { t.Fatalf("gRPC send failed with %s", err) } - response := chimera.SendResponse{Key:resp.Key} + response := chimera.SendResponse{Key: resp.Key} if !reflect.DeepEqual(response, expected) { t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) } @@ -194,7 +194,7 @@ func TestReceive(t *testing.T) { receiveReqs := []api.ReceiveRequest{ { Key: encodedPayload, - To: receiver, + To: receiver, }, } @@ -212,7 +212,7 @@ func TestGRPCReceive(t *testing.T) { receiveReqs := []chimera.ReceiveRequest{ { Key: payload, - To: receiver, + To: receiver, }, } expected := chimera.ReceiveResponse{Payload: payload} @@ -230,11 +230,11 @@ func TestGRPCReceive(t *testing.T) { c := chimera.NewClientClient(conn) for _, receiveReq := range receiveReqs { - resp, err:= c.Receive(context.Background(), &receiveReq) + resp, err := c.Receive(context.Background(), &receiveReq) if err != nil { t.Fatalf("gRPC receive failed with %s", err) } - response := chimera.ReceiveResponse{Payload:resp.Payload} + response := chimera.ReceiveResponse{Payload: resp.Payload} if !reflect.DeepEqual(response, expected) { t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) } @@ -264,9 +264,9 @@ func TestNilKeyReceivedRaw(t *testing.T) { func TestPush(t *testing.T) { epl := api.EncryptedPayload{ - Sender: nacl.NewKey(), - CipherText: []byte(payload), - Nonce: nacl.NewNonce(), + Sender: nacl.NewKey(), + CipherText: []byte(payload), + Nonce: nacl.NewNonce(), RecipientBoxes: [][]byte{[]byte(payload)}, RecipientNonce: nacl.NewNonce(), } @@ -405,12 +405,11 @@ func runRawHandlerTest( } } - func TestResendIndividual(t *testing.T) { resendReq := api.ResendRequest{ - Type: "individual", - PublicKey: sender, - Key: encodedPayload, + Type: "individual", + PublicKey: sender, + Key: encodedPayload, } body := runResendTest(t, resendReq) @@ -423,8 +422,8 @@ func TestResendIndividual(t *testing.T) { func TestResendAll(t *testing.T) { resendReq := api.ResendRequest{ - Type: "all", - PublicKey: sender, + Type: "all", + PublicKey: sender, } body := runResendTest(t, resendReq) @@ -483,7 +482,7 @@ func TestPartyInfo(t *testing.T) { func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { encodedPartyInfo := api.EncodePartyInfo(pi) - encoded, err := json.Marshal(api.PartyInfoResponse{Payload:encodedPartyInfo}) + encoded, err := json.Marshal(api.PartyInfoResponse{Payload: encodedPartyInfo}) if err != nil { t.Errorf("Marshalling failed %v", err) } @@ -509,9 +508,9 @@ func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { } } -func InitgRPCServer(t *testing.T, grpc bool, port int) (string) { +func InitgRPCServer(t *testing.T, grpc bool, port int) string { ipcPath, err := ioutil.TempDir("", "TestInitIpc") - tm, err := Init(&MockEnclave{}, port, ipcPath, grpc, -1,false, "", "") + tm, err := Init(&MockEnclave{}, port, ipcPath, grpc, -1, false, "", "") if err != nil { t.Errorf("Error starting server: %v\n", err) @@ -555,7 +554,7 @@ func TestInit(t *testing.T) { t.Error(err) } certFile, keyFile := "../enclave/testdata/cert/server.crt", "../enclave/testdata/cert/server.key" - tm, err := Init(enc, 9001, ipcPath, false, -1, true, certFile, keyFile) + tm, err := Init(enc, 9001, ipcPath, false, -1, true, certFile, keyFile) if err != nil { t.Errorf("Error starting server: %v\n", err) } diff --git a/storage/berkleydb.go b/storage/berkleydb.go index b2cb610..d2ee2a7 100644 --- a/storage/berkleydb.go +++ b/storage/berkleydb.go @@ -84,4 +84,4 @@ func (db *berkleyDb) Delete(key *[]byte) error { func (db *berkleyDb) Close() error { return db.conn.Close() -} \ No newline at end of file +} diff --git a/storage/leveldb.go b/storage/leveldb.go index 84940ff..da89a2a 100644 --- a/storage/leveldb.go +++ b/storage/leveldb.go @@ -46,4 +46,4 @@ func (db *levelDb) Delete(key *[]byte) error { func (db *levelDb) Close() error { return db.conn.Close() -} \ No newline at end of file +} diff --git a/test/client_test.go b/test/client_test.go index 32d1319..b91ae0c 100644 --- a/test/client_test.go +++ b/test/client_test.go @@ -1,18 +1,18 @@ package client import ( + "encoding/base64" + "github.com/blk-io/chimera-api/chimera" "golang.org/x/net/context" "google.golang.org/grpc" - "github.com/blk-io/chimera-api/chimera" - "encoding/base64" "reflect" "testing" - ) +) const sender = "zSifTnkv5r4K67Dq304eVcM4FpxGfHLe1yTCBm0/7wg=" const receiver = "I/EbshW61ykJ+qTivXPaKyQ5WmQDUFfMNGEBj2E2uUs=" -var payload = []byte("payload") +var payload = []byte("payload") func TestIntegration(t *testing.T) { var conn1 *grpc.ClientConn @@ -47,12 +47,12 @@ func TestIntegration(t *testing.T) { sendReqs := []chimera.SendRequest{ { Payload: []byte("payload"), - From: sender, - To: []string{receiver}, + From: sender, + To: []string{receiver}, }, { Payload: []byte("test"), - To: []string{}, + To: []string{}, }, { Payload: []byte("blk-io crux"), @@ -61,18 +61,18 @@ func TestIntegration(t *testing.T) { sendResponse := chimera.SendResponse{} for _, sendReq := range sendReqs { - sendResp, err:= c1.Send(context.Background(), &sendReq) + sendResp, err := c1.Send(context.Background(), &sendReq) if err != nil { t.Fatalf("gRPC send failed with %s", err) } - sendResponse = chimera.SendResponse{Key:sendResp.Key} + sendResponse = chimera.SendResponse{Key: sendResp.Key} t.Logf("The response for Send request is: %s", base64.StdEncoding.EncodeToString(sendResponse.Key)) - recResp, err:= c1.Receive(context.Background(), &chimera.ReceiveRequest{Key:sendResponse.Key, To:receiver}) + recResp, err := c1.Receive(context.Background(), &chimera.ReceiveRequest{Key: sendResponse.Key, To: receiver}) if err != nil { t.Fatalf("gRPC receive failed with %s", err) } - receiveResponse := chimera.ReceiveResponse{Payload:recResp.Payload} + receiveResponse := chimera.ReceiveResponse{Payload: recResp.Payload} if !reflect.DeepEqual(receiveResponse.Payload, sendReq.Payload) { t.Fatalf("handler returned unexpected response: %v, expected: %v\n", receiveResponse.Payload, sendReq.Payload) } else { diff --git a/utils/file_test.go b/utils/file_test.go index d2f39f5..9abb25d 100644 --- a/utils/file_test.go +++ b/utils/file_test.go @@ -1,8 +1,8 @@ package utils import ( - "testing" "io/ioutil" + "testing" ) func TestCreateIpcSocket(t *testing.T) { @@ -13,7 +13,7 @@ func TestCreateIpcSocket(t *testing.T) { listener, err := CreateIpcSocket(dbPath) - if err != nil{ + if err != nil { t.Error(err) } diff --git a/utils/key.go b/utils/key.go index 4a1dffa..7b8431f 100644 --- a/utils/key.go +++ b/utils/key.go @@ -1,9 +1,9 @@ package utils import ( - "github.com/kevinburke/nacl" - "fmt" "encoding/base64" + "fmt" + "github.com/kevinburke/nacl" ) func ToKey(src []byte) (nacl.Key, error) { diff --git a/utils/math.go b/utils/math.go index d0eeccb..1b1841c 100644 --- a/utils/math.go +++ b/utils/math.go @@ -9,4 +9,4 @@ func NextPowerOf2(v int) int { v |= v >> 16 v++ return v -} \ No newline at end of file +} diff --git a/utils/math_test.go b/utils/math_test.go index bf04ae3..25f4fe4 100644 --- a/utils/math_test.go +++ b/utils/math_test.go @@ -5,17 +5,17 @@ import "testing" func TestNextPowerOf2(t *testing.T) { values := map[int]int{ - 0: 0, - 1: 1, - 2: 2, - 3: 4, - 4: 4, - 5: 8, - 7: 8, - 8: 8, - 9: 16, - 16: 16, - 17: 32, + 0: 0, + 1: 1, + 2: 2, + 3: 4, + 4: 4, + 5: 8, + 7: 8, + 8: 8, + 9: 16, + 16: 16, + 17: 32, 1023: 1024, 1024: 1024, 1025: 2048, diff --git a/utils/url_test.go b/utils/url_test.go index f1e92a2..bfa4d15 100644 --- a/utils/url_test.go +++ b/utils/url_test.go @@ -19,4 +19,4 @@ func runUrlTest(t *testing.T, baseUrl, path, expected string) { if url != expected { t.Errorf("Url created: %s, does not match expected: %s", url, expected) } -} \ No newline at end of file +} From f0c810819f9ab17cdffafeca7d59544226cf93b7 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Thu, 27 Sep 2018 10:33:10 +0100 Subject: [PATCH 78/85] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 66c5d91..d22144d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ Quorum Slack Build Status -Go Report Card Data privacy for Quorum. From f39db2345cf9d82e76d3905468e6e5ea1469b09d Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Thu, 27 Sep 2018 11:33:14 +0100 Subject: [PATCH 79/85] Include Goreport in README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d22144d..66c5d91 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Quorum Slack Build Status +Go Report Card Data privacy for Quorum. From eebdb98ffb46f36680544ab457d6b74890366a9f Mon Sep 17 00:00:00 2001 From: Antoine Toulme Date: Tue, 2 Oct 2018 08:58:59 -0700 Subject: [PATCH 80/85] Add network interface parameter to configuration (#46) --- README.md | 39 ++++++++++++++++---------------- config/config.go | 2 ++ crux.go | 3 ++- docker/crux/start.sh | 2 +- docker/quorum-crux/crux-start.sh | 2 +- server/proto_server.go | 34 ++++++++++++++-------------- server/server.go | 10 ++++---- server/server_test.go | 8 +++---- 8 files changed, 52 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 66c5d91..b1307ca 100644 --- a/README.md +++ b/README.md @@ -94,25 +94,26 @@ make setup && make ./bin/crux Usage of ./bin/crux: - crux.config Optional config file - --alwayssendto string List of public keys for nodes to send all transactions too - --berkeleydb Use Berkeley DB for working with an existing Constellation data store [experimental] - --generate-keys string Generate a new keypair - --grpc Use gRPC server (default true) - --grpcport int The local port to listen on for JSON extensions of gRPC (default -1) - --othernodes string "Boot nodes" to connect to to discover the network - --port int The local port to listen on (default -1) - --privatekeys string Private keys hosted by this node - --publickeys string Public keys hosted by this node - --socket string IPC socket to create for access to the Private API (default "crux.ipc") - --storage string Database storage file name (default "crux.db") - --tls Use TLS to secure HTTP communications - --tlsservercert string The server certificate to be used - --tlsserverkey string The server private key - --url string The URL to advertise to other nodes (reachable by them) - -v, --v int Verbosity level of logs (shorthand) (default 1) - --verbosity int Verbosity level of logs (default 1) - --workdir string The folder to put stuff in (default: .) (default ".") + crux.config Optional config file + --alwayssendto string List of public keys for nodes to send all transactions too + --berkeleydb Use Berkeley DB for working with an existing Constellation data store [experimental] + --generate-keys string Generate a new keypair + --grpc Use gRPC server (default true) + --grpcport int The local port to listen on for JSON extensions of gRPC (default -1) + --networkinterface string The network interface to bind the server to (default "localhost") + --othernodes string "Boot nodes" to connect to to discover the network + --port int The local port to listen on (default -1) + --privatekeys string Private keys hosted by this node + --publickeys string Public keys hosted by this node + --socket string IPC socket to create for access to the Private API (default "crux.ipc") + --storage string Database storage file name (default "crux.db") + --tls Use TLS to secure HTTP communications + --tlsservercert string The server certificate to be used + --tlsserverkey string The server private key + --url string The URL to advertise to other nodes (reachable by them) + -v, --v int Verbosity level of logs (shorthand) (default 1) + --verbosity int Verbosity level of logs (default 1) + --workdir string The folder to put stuff in (default: .) (default ".") ``` ## How does it work? diff --git a/config/config.go b/config/config.go index 5adde2c..02d8eb7 100644 --- a/config/config.go +++ b/config/config.go @@ -27,6 +27,7 @@ const ( BerkeleyDb = "berkeleydb" UseGRPC = "grpc" GrpcJsonPort = "grpcport" + NetworkInterface = "networkinterface" Tls = "tls" TlsServerChain = "tlsserverchain" @@ -63,6 +64,7 @@ func InitFlags() { flag.String(TlsServerCert, "", "The server certificate to be used") flag.String(TlsServerKey, "", "The server private key") flag.Int(GrpcJsonPort, -1, "The local port to listen on for JSON extensions of gRPC") + flag.String(NetworkInterface, "localhost", "The network interface to bind the server to") // storage not currently supported as we use LevelDB diff --git a/crux.go b/crux.go index 2920ccb..6c7cbc1 100644 --- a/crux.go +++ b/crux.go @@ -139,7 +139,8 @@ func main() { tlsKeyFile = path.Join(workDir, servKey) } grpcJsonport := config.GetInt(config.GrpcJsonPort) - _, err = server.Init(enc, port, ipcPath, grpc, grpcJsonport, tls, tlsCertFile, tlsKeyFile) + networkInterface := config.GetString(config.NetworkInterface) + _, err = server.Init(enc, networkInterface, port, ipcPath, grpc, grpcJsonport, tls, tlsCertFile, tlsKeyFile) if err != nil { log.Fatalf("Error starting server: %v\n", err) } diff --git a/docker/crux/start.sh b/docker/crux/start.sh index 98dc8f1..c31a713 100644 --- a/docker/crux/start.sh +++ b/docker/crux/start.sh @@ -2,5 +2,5 @@ echo $CRUX_PUB >> key.pub echo $CRUX_PRIV >> key.priv -CMD="./bin/crux --url=http://$OWN_URL:$PORT/ --port=$PORT --publickeys=key.pub --privatekeys=key.priv --othernodes=$OTHER_NODES --verbosity=3" +CMD="./bin/crux --url=http://$OWN_URL:$PORT/ --port=$PORT --networkinterface 0.0.0.0 --publickeys=key.pub --privatekeys=key.priv --othernodes=$OTHER_NODES --verbosity=3" $CMD >> "crux.log" 2>&1 \ No newline at end of file diff --git a/docker/quorum-crux/crux-start.sh b/docker/quorum-crux/crux-start.sh index 0aebb04..2d0fca0 100644 --- a/docker/quorum-crux/crux-start.sh +++ b/docker/quorum-crux/crux-start.sh @@ -8,7 +8,7 @@ mkdir -p qdata/logs echo $CRUX_PUB >> "$DDIR/tm.pub" echo $CRUX_PRIV >> "$DDIR/tm.key" rm -f "$DDIR/tm.ipc" -CMD="crux --url=http://$OWN_URL:$PORT/ --port=$PORT --workdir=$DDIR --socket=tm.ipc --publickeys=tm.pub --privatekeys=tm.key --othernodes=$OTHER_NODES --verbosity=3" +CMD="crux --url=http://$OWN_URL:$PORT/ --port=$PORT --networkinterface 0.0.0.0 --workdir=$DDIR --socket=tm.ipc --publickeys=tm.pub --privatekeys=tm.key --othernodes=$OTHER_NODES --verbosity=3" $CMD >> "qdata/logs/crux.log" 2>&1 & DOWN=true diff --git a/server/proto_server.go b/server/proto_server.go index b6f0362..01720f4 100644 --- a/server/proto_server.go +++ b/server/proto_server.go @@ -13,7 +13,7 @@ import ( "net/http" ) -func (tm *TransactionManager) startRpcServer(port int, grpcJsonPort int, ipcPath string, tls bool, certFile, keyFile string) error { +func (tm *TransactionManager) startRpcServer(networkInterface string, port int, grpcJsonPort int, ipcPath string, tls bool, certFile, keyFile string) error { lis, err := utils.CreateIpcSocket(ipcPath) if err != nil { log.Fatalf("failed to listen: %v", err) @@ -28,15 +28,15 @@ func (tm *TransactionManager) startRpcServer(port int, grpcJsonPort int, ipcPath go func() error { var err error if tls { - err = tm.startRestServerTLS(port, certFile, keyFile, certFile) + err = tm.startRestServerTLS(networkInterface, port, certFile, keyFile, certFile) } else { - err = tm.startRestServer(port) + err = tm.startRestServer(networkInterface, port) } if grpcJsonPort != -1 { if tls { - err = tm.startJsonServerTLS(port, grpcJsonPort, certFile, keyFile, certFile) + err = tm.startJsonServerTLS(networkInterface, port, grpcJsonPort, certFile, keyFile, certFile) } else { - err = tm.startJsonServer(port, grpcJsonPort) + err = tm.startJsonServer(networkInterface, port, grpcJsonPort) } } if err != nil { @@ -48,14 +48,14 @@ func (tm *TransactionManager) startRpcServer(port int, grpcJsonPort int, ipcPath return err } -func (tm *TransactionManager) startJsonServer(port int, grpcJsonPort int) error { - address := fmt.Sprintf("%s:%d", "localhost", grpcJsonPort) +func (tm *TransactionManager) startJsonServer(networkInterface string, port int, grpcJsonPort int) error { + address := fmt.Sprintf("%s:%d", networkInterface, grpcJsonPort) ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithInsecure()} - err := chimera.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", "localhost", port), opts) + err := chimera.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", networkInterface, port), opts) if err != nil { return fmt.Errorf("could not register service: %s", err) } @@ -67,8 +67,8 @@ func (tm *TransactionManager) startJsonServer(port int, grpcJsonPort int) error return nil } -func (tm *TransactionManager) startRestServer(port int) error { - grpcAddress := fmt.Sprintf(":%d", port) +func (tm *TransactionManager) startRestServer(networkInterface string, port int) error { + grpcAddress := fmt.Sprintf("%s:%d", networkInterface, port) lis, err := net.Listen("tcp", grpcAddress) if err != nil { panic(err) @@ -82,14 +82,14 @@ func (tm *TransactionManager) startRestServer(port int) error { return nil } -func (tm *TransactionManager) startJsonServerTLS(port int, grpcJsonPort int, certFile, keyFile, ca string) error { - address := fmt.Sprintf("%s:%d", "localhost", grpcJsonPort) +func (tm *TransactionManager) startJsonServerTLS(networkInterface string, port int, grpcJsonPort int, certFile, keyFile, ca string) error { + address := fmt.Sprintf("%s:%d", networkInterface, grpcJsonPort) ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() mux := runtime.NewServeMux() creds, err := credentials.NewServerTLSFromFile(certFile, keyFile) - err = chimera.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", "localhost", port), []grpc.DialOption{grpc.WithTransportCredentials(creds)}) + err = chimera.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", networkInterface, port), []grpc.DialOption{grpc.WithTransportCredentials(creds)}) if err != nil { log.Fatalf("could not register service Ping: %s", err) return err @@ -99,8 +99,8 @@ func (tm *TransactionManager) startJsonServerTLS(port int, grpcJsonPort int, cer return nil } -func (tm *TransactionManager) startRestServerTLS(port int, certFile, keyFile, ca string) error { - grpcAddress := fmt.Sprintf("%s:%d", "localhost", port) +func (tm *TransactionManager) startRestServerTLS(networkInterface string, port int, certFile, keyFile, ca string) error { + grpcAddress := fmt.Sprintf("%s:%d", networkInterface, port) lis, err := net.Listen("tcp", grpcAddress) if err != nil { log.Fatalf("failed to start gRPC REST server: %s", err) @@ -119,8 +119,8 @@ func (tm *TransactionManager) startRestServerTLS(port int, certFile, keyFile, ca return nil } -func GetFreePort() (int, error) { - addr, err := net.ResolveTCPAddr("tcp", "localhost:0") +func GetFreePort(networkInterface string) (int, error) { + addr, err := net.ResolveTCPAddr("tcp", networkInterface + ":0") if err != nil { return 0, err } diff --git a/server/server.go b/server/server.go index e9a8b1f..a7a7242 100644 --- a/server/server.go +++ b/server/server.go @@ -75,20 +75,20 @@ func requestLogger(handler http.Handler) http.Handler { } // Init initializes a new TransactionManager instance. -func Init(enc Enclave, port int, ipcPath string, grpc bool, grpcJsonPort int, tls bool, certFile, keyFile string) (TransactionManager, error) { +func Init(enc Enclave, networkInterface string, port int, ipcPath string, grpc bool, grpcJsonPort int, tls bool, certFile, keyFile string) (TransactionManager, error) { tm := TransactionManager{Enclave: enc} var err error if grpc == true { - err = tm.startRpcServer(port, grpcJsonPort, ipcPath, tls, certFile, keyFile) + err = tm.startRpcServer(networkInterface, port, grpcJsonPort, ipcPath, tls, certFile, keyFile) } else { - err = tm.startHttpserver(port, ipcPath, tls, certFile, keyFile) + err = tm.startHttpserver(networkInterface, port, ipcPath, tls, certFile, keyFile) } return tm, err } -func (tm *TransactionManager) startHttpserver(port int, ipcPath string, tls bool, certFile, keyFile string) error { +func (tm *TransactionManager) startHttpserver(networkInterface string, port int, ipcPath string, tls bool, certFile, keyFile string) error { httpServer := http.NewServeMux() httpServer.HandleFunc(upCheck, tm.upcheck) httpServer.HandleFunc(version, tm.version) @@ -96,7 +96,7 @@ func (tm *TransactionManager) startHttpserver(port int, ipcPath string, tls bool httpServer.HandleFunc(resend, tm.resend) httpServer.HandleFunc(partyInfo, tm.partyInfo) - serverUrl := "localhost:" + strconv.Itoa(port) + serverUrl := networkInterface + ":" + strconv.Itoa(port) if tls { err := CheckCertFiles(certFile, keyFile) if err != nil { diff --git a/server/server_test.go b/server/server_test.go index 9af45b0..ecc8665 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -151,7 +151,7 @@ func TestGRPCSend(t *testing.T) { } expected := chimera.SendResponse{Key: payload} - freePort, err := GetFreePort() + freePort, err := GetFreePort("localhost") if err != nil { log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) } @@ -216,7 +216,7 @@ func TestGRPCReceive(t *testing.T) { }, } expected := chimera.ReceiveResponse{Payload: payload} - freePort, err := GetFreePort() + freePort, err := GetFreePort("localhost") if err != nil { log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) } @@ -510,7 +510,7 @@ func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { func InitgRPCServer(t *testing.T, grpc bool, port int) string { ipcPath, err := ioutil.TempDir("", "TestInitIpc") - tm, err := Init(&MockEnclave{}, port, ipcPath, grpc, -1, false, "", "") + tm, err := Init(&MockEnclave{}, "localhost", port, ipcPath, grpc, -1, false, "", "") if err != nil { t.Errorf("Error starting server: %v\n", err) @@ -554,7 +554,7 @@ func TestInit(t *testing.T) { t.Error(err) } certFile, keyFile := "../enclave/testdata/cert/server.crt", "../enclave/testdata/cert/server.key" - tm, err := Init(enc, 9001, ipcPath, false, -1, true, certFile, keyFile) + tm, err := Init(enc, "localhost", 9001, ipcPath, false, -1, true, certFile, keyFile) if err != nil { t.Errorf("Error starting server: %v\n", err) } From f6f7b3a1b95ab468c0675485b63e64fd062758b1 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Wed, 17 Oct 2018 10:21:15 +0100 Subject: [PATCH 81/85] Fix sending to self (#49) --- CHANGELOG.md | 30 +++++++++++++++++++++++------- enclave/enclave.go | 16 ++++++++-------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f25cb5f..30dcc68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,23 @@ -## 1.0.0 - 2018-07-05 -### Added - - Crux, a secure enclave for Quorum written in Golang - - Protobuf and gRPC support. - - TLS support - - Docker images - \ No newline at end of file + ## 1.0.3 - 2018-10-17 + ### Added + - Network interface paramater to configuration + ### Changed + - Fix formatting of entire project using `go fmt` + - Fix "apk WARNING Ignoring APKINDEX" + - Fix sending payload to multiple recipients + + ## 1.0.2 - 2018-09-06 + ### Added + - Delete and Resend API from Chimera included. + + ## 1.0.1 - 2018-08-22 + ### Changed + - Fix issue with config file load + + ## 1.0.0 - 2018-07-05 + ### Added + - Crux, a secure enclave for Quorum written in Golang + - Protobuf and gRPC support. + - TLS support + - Docker images + \ No newline at end of file diff --git a/enclave/enclave.go b/enclave/enclave.go index 91e5253..bd0cf81 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -139,6 +139,14 @@ func (s *SecureEnclave) store( senderPubKey, senderPrivKey nacl.Key, recipients [][]byte) ([]byte, error) { + var toSelf bool + if len(recipients) == 0 { + toSelf = true + recipients = [][]byte{(*s.selfPubKey)[:]} + } else { + toSelf = false + } + epl, masterKey := createEncryptedPayload(message, senderPubKey, recipients) for i, recipient := range recipients { @@ -163,14 +171,6 @@ func (s *SecureEnclave) store( epl.RecipientBoxes[i] = sealedBox } - var toSelf bool - if len(recipients) == 0 { - toSelf = true - recipients = [][]byte{(*s.selfPubKey)[:]} - } else { - toSelf = false - } - // store locally recipientKey, err := utils.ToKey(recipients[0]) if err != nil { From a6416d922e1ac4891a09417e24d2c2370897ed69 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 28 Dec 2018 17:15:56 -0500 Subject: [PATCH 82/85] Document verbosity levels in CLI This allows users to understand the logging levels and how they correspond to verbosity levels without reading the source code. --- config/config.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 02d8eb7..561fae6 100644 --- a/config/config.go +++ b/config/config.go @@ -24,9 +24,9 @@ const ( GenerateKeys = "generate-keys" - BerkeleyDb = "berkeleydb" - UseGRPC = "grpc" - GrpcJsonPort = "grpcport" + BerkeleyDb = "berkeleydb" + UseGRPC = "grpc" + GrpcJsonPort = "grpcport" NetworkInterface = "networkinterface" Tls = "tls" @@ -56,7 +56,7 @@ func InitFlags() { flag.Bool(BerkeleyDb, false, "Use Berkeley DB for working with an existing Constellation data store [experimental]") - flag.Int(Verbosity, 1, "Verbosity level of logs") + flag.Int(Verbosity, 1, "Verbosity level of logs (0=fatal, 1=warn, 2=info, 3=debug)") flag.Int(VerbosityShorthand, 1, "Verbosity level of logs (shorthand)") flag.String(AlwaysSendTo, "", "List of public keys for nodes to send all transactions too") flag.Bool(UseGRPC, true, "Use gRPC server") From d2899367c1bacb2d07c919f9c17b9c2047318065 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 28 Dec 2018 17:25:56 -0500 Subject: [PATCH 83/85] Default to debug level logs for unexpected verbosity Before this change, a user specifying a verbosity > 3 would not hit any case in the verbosity switch statement, and the Go zero-valued default would result in FATAL level logs only. This is unlikely to be their intention. This change will cause the log level to default to DEBUG, which is likely what a user specifying a high verbosity wants. A user who does not specify a default will still get WARN level logging by default due to CLI defaults. --- crux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crux.go b/crux.go index 6c7cbc1..426491d 100644 --- a/crux.go +++ b/crux.go @@ -50,7 +50,7 @@ func main() { level = log.WarnLevel case 2: level = log.InfoLevel - case 3: + default: level = log.DebugLevel } log.SetLevel(level) From eeb63a91b7eda0180c8686f819c0dd29c0bc4d46 Mon Sep 17 00:00:00 2001 From: Puneetha Karamsetty Date: Fri, 18 Jan 2019 16:16:08 +0000 Subject: [PATCH 84/85] Checkout the latest version of Crux for docker build --- docker/quorum-crux/bootstrap.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/quorum-crux/bootstrap.sh b/docker/quorum-crux/bootstrap.sh index ef33188..a7b519f 100644 --- a/docker/quorum-crux/bootstrap.sh +++ b/docker/quorum-crux/bootstrap.sh @@ -14,7 +14,7 @@ popd >/dev/null # make/install crux git clone https://github.com/blk-io/crux.git cd crux -git checkout tags/v1.0.2 +git checkout tags/v1.0.3 make setup && make cp bin/crux /usr/local/bin rm -r bin From bc1a1b4fb92c0b560b536e84e05b343368a13f47 Mon Sep 17 00:00:00 2001 From: Ali Kefia Date: Thu, 21 Mar 2019 11:09:57 +0100 Subject: [PATCH 85/85] job done in the loop before --- enclave/enclave.go | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/enclave/enclave.go b/enclave/enclave.go index bd0cf81..adfc1d4 100644 --- a/enclave/enclave.go +++ b/enclave/enclave.go @@ -146,7 +146,7 @@ func (s *SecureEnclave) store( } else { toSelf = false } - + epl, masterKey := createEncryptedPayload(message, senderPubKey, recipients) for i, recipient := range recipients { @@ -171,18 +171,6 @@ func (s *SecureEnclave) store( epl.RecipientBoxes[i] = sealedBox } - // store locally - recipientKey, err := utils.ToKey(recipients[0]) - if err != nil { - log.WithField("recipientKey", recipientKey).Errorf( - "Unable to load recipient, %v", err) - } - - sharedKey := s.resolveSharedKey(senderPrivKey, senderPubKey, recipientKey) - - sealedBox := sealPayload(epl.RecipientNonce, masterKey, sharedKey) - epl.RecipientBoxes = [][]byte{sealedBox} - encodedEpl := api.EncodePayloadWithRecipients(epl, recipients) digest, err := s.storePayload(epl, encodedEpl)