diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3cd77b1cd..0e7f84179 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,11 +63,15 @@ jobs: - name: Copy binaries to cache folder run: | - mkdir -pv build_artifacts/${{ matrix.network }}/bin + mkdir -pv build_artifacts/${{ matrix.network }}/mirror cp /var/tmp/*.css build_artifacts/${{ matrix.network }} cp target/release/full-service build_artifacts/${{ matrix.network }} cp target/release/transaction-signer build_artifacts/${{ matrix.network }} cp target/release/validator-service build_artifacts/${{ matrix.network }} + cp target/release/wallet-service-mirror-private build_artifacts/${{ matrix.network }}/mirror + cp target/release/wallet-service-mirror-public build_artifacts/${{ matrix.network }}/mirror + cp target/release/generate-rsa-keypair build_artifacts/${{ matrix.network }}/mirror + cp mirror/EXAMPLE.md build_artifacts/${{ matrix.network }}/mirror - name: Create Artifact run: | diff --git a/Cargo.lock b/Cargo.lock index 3fe17b590..9abfe5042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,6 +278,25 @@ dependencies = [ "which 4.2.5", ] +[[package]] +name = "bindgen" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" +dependencies = [ + "bitflags", + "cexpr 0.6.0", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2 1.0.46", + "quote 1.0.21", + "regex", + "rustc-hash", + "shlex", +] + [[package]] name = "bit-vec" version = "0.5.1" @@ -320,6 +339,29 @@ dependencies = [ "generic-array", ] +[[package]] +name = "boring" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c713ad6d8d7a681a43870ac37b89efd2a08015ceb4b256d82707509c1f0b6bb" +dependencies = [ + "bitflags", + "boring-sys", + "foreign-types", + "lazy_static", + "libc", +] + +[[package]] +name = "boring-sys" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7663d3069437a5ccdb2b5f4f481c8b80446daea10fa8503844e89ac65fcdc363" +dependencies = [ + "bindgen 0.60.1", + "cmake", +] + [[package]] name = "boringssl-src" version = "0.5.1+b9232f9" @@ -1171,6 +1213,33 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8469d0d40519bc608ec6863f1cc88f3f1deee15913f2f3b3e573d81ed38cccc" +dependencies = [ + "proc-macro2 1.0.46", + "quote 1.0.21", + "syn 1.0.96", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -2884,6 +2953,35 @@ dependencies = [ "vergen", ] +[[package]] +name = "mc-full-service-mirror" +version = "2.0.0" +dependencies = [ + "boring", + "cargo-emit", + "futures", + "generic-array", + "grpcio", + "hex", + "mc-api", + "mc-common", + "mc-util-build-grpc", + "mc-util-build-script", + "mc-util-grpc", + "mc-util-uri", + "protobuf", + "rand 0.8.5", + "rand_core 0.6.3", + "rand_hc 0.3.1", + "reqwest", + "rocket 0.4.11", + "rocket_contrib", + "serde", + "serde_derive", + "serde_json", + "structopt", +] + [[package]] name = "mc-ledger-db" version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index 09b155cb8..9821c7567 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ cargo-features = ["resolver"] resolver = "2" members = [ "full-service", + "mirror", "transaction-signer", "validator/api", "validator/connection", diff --git a/mirror/Cargo.toml b/mirror/Cargo.toml new file mode 100644 index 000000000..5c418fab6 --- /dev/null +++ b/mirror/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "mc-full-service-mirror" +version = "2.0.0" +authors = ["MobileCoin"] +edition = "2018" +resolver = "2" + +[[bin]] +name = "wallet-service-mirror-private" +path = "src/private/main.rs" + +[[bin]] +name = "wallet-service-mirror-public" +path = "src/public/main.rs" + +[[bin]] +name = "generate-rsa-keypair" +path = "src/generate-rsa-keypair/main.rs" + +[dependencies] +mc-api = { path = "../mobilecoin/api" } +mc-common = { path = "../mobilecoin/common", features = ["loggers"] } +mc-util-grpc = { path = "../mobilecoin/util/grpc" } +mc-util-uri = { path = "../mobilecoin/util/uri" } + +boring = "2.0" +futures = "0.3" +generic-array = "0.14" +grpcio = "0.10.3" +hex = "0.4" +protobuf = "2.12" +rand = "0.8" +reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "gzip", "blocking"] } +rocket = { version = "0.4.5", default-features = false } +rocket_contrib = { version = "0.4.5", default-features = false, features = ["json"] } +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" +structopt = "0.3" + + +[dev-dependencies] +rand_hc = "0.3" +rand_core = { version = "0.6", default-features = false } + + +[build-dependencies] +# Even though this is unused, it needs to be here otherwise Cargo brings in some weird mixture of packages/features that refuses to compile. +# Go figure ¯\_(ツ)_/¯ +serde = { version = "1", default-features = false, features = ["alloc", "derive"] } + +mc-util-build-grpc = { path = "../mobilecoin/util/build/grpc" } +mc-util-build-script = { path = "../mobilecoin/util/build/script" } + +cargo-emit = "0.2.1" \ No newline at end of file diff --git a/mirror/EXAMPLE.md b/mirror/EXAMPLE.md new file mode 100644 index 000000000..26d36c0a3 --- /dev/null +++ b/mirror/EXAMPLE.md @@ -0,0 +1,182 @@ +# Full Service Mirror, Full Service, & Ledger Validator Node (LVN) + +## Requirements + +MobileCoin's full-service, full-service mirrors, and ledger-validator-node are developed using the environment specified in [this Dockerfile](https://github.com/mobilecoinfoundation/mobilecoin/blob/bdd5ded7aff9b8a86bd10c568a1f2bcf1ee20d27/docker/Dockerfile). + +## Ledger Validator Node & Full Service + +The first step is to launch Full Service and the Ledger Validator Node (LVN) + +A service that is capable of syncing the ledger from the consensus network, relaying transactions to it and proxying fog report resolution. + +The Ledger Validator Node exposes a GRPC service that provides access to its local ledger, transaction relaying and fog report request relaying. + +Using the `--validator` command line argument for `full-service`, this allows running `full-service` on a machine that is not allowed to make outside connections to the internet \ +but can connect to a host running the LVN. + +1. Run the Ledger Validator Node (LVN) + + NOTE: To run the Ledger Validator Node with TLS, see the section [TLS between full-service and LVN](#tls-between-full-service-and-lvn). + + ```sh + mkdir -p ./lvn-dbs + ./bin/mc-validator-service \ + --ledger-db ./lvn-dbs/ledger-db/ \ + --peer mc://node1.prod.mobilecoinww.com/ \ + --peer mc://node2.prod.mobilecoinww.com/ \ + --tx-source-url https://ledger.mobilecoinww.com/node1.prod.mobilecoinww.com \ + --tx-source-url https://ledger.mobilecoinww.com/node2.prod.mobilecoinww.com \ + --listen-uri insecure-validator://localhost:5554/ + ``` + + NOTE: the `insecure-` prefix indicates the connection is going over plaintext, as opposed to TLS. If you wish to run with TLS, skip to the next section. + + At this point the LVN is running and accepting connections on port 5554. + + +2. Run Full Service + + ```sh + mkdir -p ./fs-dbs/wallet-db/ + ./bin/full-service \ + --wallet-db ./fs-dbs/wallet-db/wallet.db \ + --ledger-db ./fs-dbs/ledger-db/ \ + --validator insecure-validator://localhost:5554/ \ + --fog-ingest-enclave-css $(pwd)/ingest-enclave.css + ``` + + Notice how `--validator` replaced `--peer` and `--tx-source-url`. + +### TLS between full-service and LVN + +1. The GRPC connection between `full-service` and `mc-ledger-validator` can optionally be TLS-encrypted. If you wish to use TLS for that, you'll need a certificate file and the matching private key for it. For testing purposes you can generate your own self-signed certificate: + + ``` + $ openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout server.key -out server.crt + + Generating a 2048 bit RSA private key + ....................+++ + .............+++ + writing new private key to 'server.key' + ----- + You are about to be asked to enter information that will be incorporated + into your certificate request. + What you are about to enter is what is called a Distinguished Name or a DN. + There are quite a few fields but you can leave some blank + For some fields there will be a default value, + If you enter '.', the field will be left blank. + ----- + Country Name (2 letter code) []:US + State or Province Name (full name) []:California + Locality Name (eg, city) []:San Francisco + Organization Name (eg, company) []:My Test Company + Organizational Unit Name (eg, section) []:Test Unit + Common Name (eg, fully qualified host name) []:localhost + Email Address []:test@test.com + ``` + + + Note that the `Common Name` needs to match the hostname which you would be using to connect to the public side (that has the GRPC listening port). + +2. Now, you can run the LVN with TLS enabled: + + ```sh + mkdir -p ./lvn-dbs + ./bin/mc-validator-service \ + --ledger-db ./lvn-dbs/ledger-db/ \ + --peer mc://node1.prod.mobilecoinww.com/ \ + --peer mc://node2.prod.mobilecoinww.com/ \ + --tx-source-url https://ledger.mobilecoinww.com/node1.prod.mobilecoinww.com \ + --tx-source-url https://ledger.mobilecoinww.com/node2.prod.mobilecoinww.com \ + --listen-uri "validator://localhost:5554/?tls-chain=server.crt&tls-key=server.key" + ``` + + Notice that the `--listen-uri` argument has changed and points to the key and certificate you generated. + +3. Once the LVN is running, you will need to run `full-service`: + + ```sh + mkdir -p ./fs-dbs/wallet-db/ + ./bin/full-service \ + --wallet-db ./fs-dbs/wallet-db/wallet.db \ + --ledger-db ./fs-dbs/ledger-db/ \ + --validator "validator://localhost:5554/?ca-bundle=server.crt&tls-hostname=localhost" \ + --fog-ingest-enclave-css $(pwd)/ingest-enclave.css \ + --listen-port 9090 + ``` + + The `--validator` argument has changed to point at the certificate file, and also specify the Common Name that is in the certficiate. Note that if the CN matches the hostname (as in the above example) then this is redundant.## TLS between full-service and LVN + +## Full Service Mirror + +To use, you will need to start both sides of the mirror. + +### End-to-end encryption + +It is possible to run the mirror in a mode that causes it to encrypt requests and responses between the private side and the client. In this mode, anyone having access to the public side of the mirror will be unable to tamper with requests/responses or view them. When running in this mode, which is enabled by passing the `--mirror-key` argument to the private side of the mirror, only encrypted requests will be processed and only encrypted responses will be returned. + +In order to use this mode, follow the following steps. + +1) Ensure that you have NodeJS installed. **The minimum supported version is v12.9.0** (`node -v`) + +1) Generate a keypair: `./bin/generate-rsa-keypair`. This will generate two files: `mirror-client.pem` and `mirror-private.pem`. + +### TLS Connection + +In order to have a tls connection between the public and private sides of the mirror, you need to use a certificate pair. For testing, you can generate these with `openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout server.key -out server.crt`. + +Note that the `Common Name` needs to match the hostname which you would be using to connect to the public side (that has the GRPC listening port). + +### Public Mirror + +If you would like to run this without end to end encryption use the following command + +```sh +./bin/wallet-service-mirror-public --client-listen-uri http://0.0.0.0:9091/ --mirror-listen-uri "insecure-wallet-service-mirror://0.0.0.0/" +``` + +To run with encryption, use the following command + +```sh +./bin/wallet-service-mirror-public --client-listen-uri http://0.0.0.0:9091/ --mirror-listen-uri "wallet-service-mirror://0.0.0.0/?tls-chain=server.crt&tls-key=server.key" --allow-self-signed-tls +``` + + +### Private Mirror + +If you would like to run this without end to end encryption use the following command + +```sh +./bin/wallet-service-mirror-private --mirror-public-uri "insecure-wallet-service-mirror://localhost/" --wallet-service-uri http://localhost:9090/wallet +``` + +To run with encryption, use the following command + +```sh +./bin/wallet-service-mirror-private --mirror-public-uri "wallet-service-mirror://localhost/?ca-bundle=server.crt&tls-hostname=localhost" --wallet-service-uri http://localhost:9090/wallet --mirror-key mirror-private.pem +``` + +NOTE: Notice the --mirror-key flag with the mirror-private.pem file, generated with the generate-rsa-keypair binary. + +Once launched, without end to end encryption, you can test it using curl: + +Get block information (for block 0): + +``` +curl -X POST -H 'Content-Type: application/json' -d '{"method": "get_block", "params": {"block_index": "0"}, "jsonrpc": "2.0", "id": 1}' http://localhost:9091/unencrypted-request +``` +Returns: +``` +{"method":"get_block","result":{"block":{"id":"dba9b5bb61dc3941c6730a4c5e9b81f30f9def32abd4251d0715100072a7425e","version":"0","parent_id":"0000000000000000000000000000000000000000000000000000000000000000","index":"0","cumulative_txo_count":"16","root_element":{"range":{"from":"0","to":"0"},"hash":"0000000000000000000000000000000000000000000000000000000000\ +000000"},"contents_hash":"882cea8bf5e082294ae1707ad2841c6f4846ece978d077f15bc090ac97885e81"},"block_contents":{"key_images":[],"outputs":[{"amount":{"commitment":"3a72e2231c1462354dfe6d4c289d05c67a528dfcdba52d8d87c07914c507dc5f","masked_value":"28067792405079518"},"target_key":"8c43d0e80adcf7c8a59f6350d010f7b257f2d6454efa7ca693eb92180a06ee6c","public_key":\ +"50c5916be94c0dcba5054fe2852422ec7c5e208cb31355b8e74e8c4ed007a60b","e_fog_hint":"05e32fee11b4612c9fd54f97e9662c8e576ab91d062c62295974cdd940d0a257eb8ce687e9bbbf8e6dccb0ec16bf15ad6902f9c249d2fe1ed198918ec1c614a48b299c657aa32b9e5c3580f24c07e354b31e0100"},{"amou... +``` + +For the full API documentation, please see the [Full Service API](https://mobilecoin.gitbook.io/full-service-api/). + +To test with encryption, please use the [example client](https://github.com/mobilecoinofficial/full-service-mirror/blob/master/example-client.js). + +``` +node example-client.js 127.0.0.1 9091 mirror-client.pem '{"method": "get_block", "params": {"block_index": "0"}, "jsonrpc": "2.0", "id": 1}' +``` diff --git a/mirror/README.md b/mirror/README.md new file mode 100644 index 000000000..1e5451987 --- /dev/null +++ b/mirror/README.md @@ -0,0 +1,117 @@ +## Wallet Service Mirror + +The `wallet-service-mirror` crate consists of two standalone executables, that when used together allow for exposing limited, read-only data from a `full-service` wallet service instance. As explained below, this allows exposing some data from `full-service` from a machine that does not require any incoming connections from the outside world. + +The mirror consists of two sides: + 1) A private side. The private side of the mirror runs alongside `full-service` and forms outgoing connections to both `full-service` and to the public side of the mirror. It then proceeds to poll the public side for any requests that should be forwarded to `full-service`, forwards them, and at the next poll opportunity returns any replies. Note how the private side only forms outgoing connections and does not open any listening ports. + + Please Note: + The set of available requests is defined in the variable `SUPPORTED_ENDPOINTS`, in the [private main file](src/private/main.rs). It is likely you will want to change the `SUPPORTED_ENDPOINTS` to include desired features like sending transactions. + 2) A public side. The public side of the mirror accepts incoming HTTP connections from clients, and poll requests from the private side over GRPC. The client requests are then forwarded over the GRPC channel to the private side, which in turn forwards them to `full-service` and returns the responses. + + +### Example usage + +In the examples below we assume that full-service, and both the public and private sides are all running on the same machine. In a real-world scenario the public and private sides would run on separate machines. The following TCP ports are in play: + - 9090: The port full-service listens on for incoming connecitons. + - 9091: The default port `wallet-service-mirror-public` listens on for incoming HTTP client requests. + - 10080: The default port the mirror uses for GRPC connections. + +The first step in running the mirror is to have a full-service instance running, and accessible from where the private side of the mirror would be running. Once full-service is running and set up, start the private side of the mirror: + +``` +cargo run -p mc-wallet-service-mirror --bin wallet-service-mirror-private -- --mirror-public-uri insecure-wallet-service-mirror://127.0.0.1/ --wallet-service-uri http://localhost:9090/wallet +``` + + +This starts the private side of the mirror, telling it to connect to `full-service` on localhost:9090 and the public side on `127.0.0.1:10080` (10080 is the default port for the `insecure-wallet-service-mirror` URI scheme). + +At this point you would see a bunch of error messages printed as the private side attemps to initiate outgoing GRPC connections to the public side. This is expected since the public side is not running yet. + +Now, start the public side of the mirror: + +``` +cargo run -p mc-wallet-service-mirror --bin wallet-service-mirror-public -- --client-listen-uri http://0.0.0.0:9091/ --mirror-listen-uri insecure-wallet-service-mirror://127.0.0.1/ +``` + +Once started, the private side should no longer show errors and the mirror should be up and running. You can now send client requests, for example - query the genesis block information: + +Query block details: + +``` +curl -s localhost:9091/unencrypted-request \ + -d '{ + "method": "get_block", + "params": { + "block_index": "0" + }, + "jsonrpc": "2.0", + "id": 1 + }' \ + -X POST -H 'Content-type: application/json' | jq +``` + +For supported requests, the response types are identical to the ones used by `full-service`. + + +### TLS between the mirror sides + +The GRPC connection between the public and private side of the mirror can optionally be TLS-encrypted. If you wish to use TLS for that, you'll a certificate file and the matching private key for it. For testing purposes you can generate your own self-signed certificate: + +``` +$ openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout server.key -out server.crt + +Generating a 2048 bit RSA private key +....................+++ +.............+++ +writing new private key to 'server.key' +----- +You are about to be asked to enter information that will be incorporated +into your certificate request. +What you are about to enter is what is called a Distinguished Name or a DN. +There are quite a few fields but you can leave some blank +For some fields there will be a default value, +If you enter '.', the field will be left blank. +----- +Country Name (2 letter code) []:US +State or Province Name (full name) []:California +Locality Name (eg, city) []:San Francisco +Organization Name (eg, company) []:My Test Company +Organizational Unit Name (eg, section) []:Test Unit +Common Name (eg, fully qualified host name) []:localhost +Email Address []:test@test.com +``` + +Note that the `Common Name` needs to match the hostname which you would be using to connect to the public side (that has the GRPC listening port). + +After the certificate has been geneerated, you can start the public side of the mirror and instruct it to listen using TLS: +``` +cargo run -p mc-wallet-service-mirror --bin wallet-service-mirror-public -- --client-listen-uri http://0.0.0.0:9091/ --mirror-listen-uri 'wallet-service-mirror://127.0.0.1/?tls-chain=server.crt&tls-key=server.key' --allow-self-signed-tls +``` + +Notice that the `mirror-listen-uri` has been changed from `insecure-wallet-service-mirror` to `wallet-service-mirror`, and that it now contains a `tls-chain` and `tls-key` parameters pointing at the certificate chain file and the matching private key file. The default port for the `wallet-service-mirror` scheme is 10043. + +The private side of the bridge also needs to be aware that TLS is now used: +``` +cargo run -p mc-wallet-service-mirror --bin wallet-service-mirror-private -- --mirror-public-uri 'wallet-service-mirror://localhost/?ca-bundle=server.crt' --wallet-service-uri http://localhost:9090/wallet +``` + +Notice that the `mirror-public-uri` parameter has changed to reflect the TLS certificate chain. + + +### TLS between the public side of the mirror and its HTTP clients + +Currently, due to `ring` crate version conflicts, it is is not possible to enable the `tls` feature on `rocket` (the HTTP server used by `mc-wallet-service-mirror-public`). If you want to provide TLS encryption for clients, you would need to put `mc-wallet-service-mirror-public` behind a reverse proxy such as `nginx` and have that take care of your TLS needs. + + +### End-to-end encryption + +It is possible to run the mirror in a mode that causes it to encrypt requests and responses between the private side and the client. In this mode, anyone having access to the public side of the mirror will be unable to tamper with requests/responses or view them. When running in this mode, which is enabled by passing the `--mirror-key` argument to the private side of the mirror, only encrypted requests will be processed and only encrypted responses will be returned. + +In order to use this mode, follow the following steps. +1) Ensure that you have NodeJS installed. **The minimum supported version is v12.9.0** (`node -v`) +1) Generate a keypair by running the `generate-rsa-keypair` binary. This will generate two files: `mirror-client.pem` and `mirror-private.pem`. +1) Run the public side of the mirror as usual, for example: `cargo run -p mc-wallet-service-mirror --bin wallet-service-mirror-public -- --client-listen-uri http://0.0.0.0:9091/ --mirror-listen-uri insecure-wallet-service-mirror://127.0.0.1/` +1) Copy the `mirror-private.pem` file to where you would be running the private side of the mirror, and run it: `cargo run -p mc-wallet-service-mirror --bin wallet-service-mirror-private -- --mirror-public-uri insecure-wallet-service-mirror://127.0.0.1/ --wallet-service-uri http://localhost:9090/wallet --mirror-key mirror-private.pem`. Notice the addition of the `--mirror-key` argument. +1) Issue a response using the sample client: + - To get block data: `node example-client.js 127.0.0.1 9091 mirror-client.pem '{"method": "get_block", "params": {"block_index": "0"}, "jsonrpc": "2.0", "id": 1}' | jq` diff --git a/mirror/build.rs b/mirror/build.rs new file mode 100644 index 000000000..9774b19d9 --- /dev/null +++ b/mirror/build.rs @@ -0,0 +1,27 @@ +// Copyright (c) 2018-2020 MobileCoin Inc. + +use mc_util_build_script::Environment; + +fn main() { + let env = Environment::default(); + + let proto_dir = env.dir().join("proto"); + let proto_str = proto_dir + .as_os_str() + .to_str() + .expect("Invalid UTF-8 in proto dir"); + cargo_emit::pair!("PROTOS_PATH", "{}", proto_str); + + let api_proto_path = env + .depvar("MC_API_PROTOS_PATH") + .expect("Could not read api's protos path") + .to_owned(); + + let mut all_proto_dirs = api_proto_path.split(':').collect::>(); + all_proto_dirs.push(proto_str); + + mc_util_build_grpc::compile_protos_and_generate_mod_rs( + all_proto_dirs.as_slice(), + &["wallet_service_mirror_api.proto"], + ); +} diff --git a/mirror/proto/wallet_service_mirror_api.proto b/mirror/proto/wallet_service_mirror_api.proto new file mode 100644 index 000000000..735f71525 --- /dev/null +++ b/mirror/proto/wallet_service_mirror_api.proto @@ -0,0 +1,65 @@ +// Copyright (c) 2018-2020 MobileCoin Inc. + +// Wallet service mirror data types and service descriptors. + +syntax = "proto3"; + +package wallet_service_mirror_api; + +service WalletServiceMirror { + // The periodic poll method that queries for requests and returns replies. + // This is sent from the private host to the public host. + rpc Poll (PollRequest) returns (PollResponse) {} +} + +// A single query request. +// When adding request types, remember to add them in `src/private/request.rs` as well. +message QueryRequest { + oneof request { + UnencryptedRequest unencrypted_request = 1; + EncryptedRequest encrypted_request = 2; + } +} + +// A single query response. +message QueryResponse { + oneof response { + string error = 1; + UnencryptedResponse unencrypted_response = 2; + EncryptedResponse encrypted_response = 3; + } +} + +// A polling request (sent from the private side to the public side) includes responses to queries. +message PollRequest { + // Map of query id -> response. + map query_responses = 1; +} + +// A polling response (sent from the public side to the private side) includes queries the public side wants +// the private side to execute. +message PollResponse { + // Map of query id -> request. + map query_requests = 1; +} + +// A plaintext request. +message UnencryptedRequest { + string json_request = 1; +} + +// A request thas has been encrypted by the client. +message EncryptedRequest { + bytes payload = 1; +} + +// A normal response. +message UnencryptedResponse { + string json_response = 1; +} + +// A response that has been encrypted, to be handed back to the client. +message EncryptedResponse { + // The encrypted data holds a JSON object. + bytes payload = 1; +} diff --git a/mirror/src/generate-rsa-keypair/main.rs b/mirror/src/generate-rsa-keypair/main.rs new file mode 100644 index 000000000..d3b2266ea --- /dev/null +++ b/mirror/src/generate-rsa-keypair/main.rs @@ -0,0 +1,49 @@ +// Copyright (c) 2018-2022 MobileCoin Inc. + +//! Utility to generate a 4096-bit passphrase-less RSA keypair, meant to be used for private<->client end to end encryption. + +use boring::rsa::Rsa; +use std::{fs, path::Path}; + +const PRIVATE_KEY_FILENAME: &str = "mirror-private.pem"; +const PUBLIC_KEY_FILENAME: &str = "mirror-client.pem"; + +fn main() { + if Path::new(PUBLIC_KEY_FILENAME).exists() { + panic!("{} already exists", PUBLIC_KEY_FILENAME); + } + let priv_key = if Path::new(PRIVATE_KEY_FILENAME).exists() { + println!("Reading existing private key file {}", PRIVATE_KEY_FILENAME); + let key_str = std::fs::read_to_string(PRIVATE_KEY_FILENAME).unwrap_or_else(|err| { + panic!( + "failed reading private key file {}: {}", + PRIVATE_KEY_FILENAME, err + ) + }); + Rsa::private_key_from_pem_passphrase(key_str.as_bytes(), &[]).unwrap_or_else(|err| { + panic!( + "failed parsing private key file {}: {}", + PRIVATE_KEY_FILENAME, err + ) + }) + } else { + println!("Generating private key, this might take a few seconds..."); + Rsa::generate(4096).expect("failed generating private key") + }; + + let priv_key_pem = priv_key + .private_key_to_pem() + .expect("Failed getting privte key as PEM"); + let pub_key_pem = priv_key + .public_key_to_pem() + .expect("Failed getting public key as PEM"); + + fs::write(PRIVATE_KEY_FILENAME, priv_key_pem).expect("Failed writing private key to file"); + println!("Wrote {} - use this file with the private side of the mirror. See README.md for more details'", PRIVATE_KEY_FILENAME); + + fs::write(PUBLIC_KEY_FILENAME, pub_key_pem).expect("Failed writing public key to file"); + println!( + "Wrote {} - use this file with client, see example-client.js for example", + PUBLIC_KEY_FILENAME + ); +} diff --git a/mirror/src/lib.rs b/mirror/src/lib.rs new file mode 100644 index 000000000..40ee22ec7 --- /dev/null +++ b/mirror/src/lib.rs @@ -0,0 +1,19 @@ +// Copyright (c) 2018-2021 MobileCoin Inc. + +mod autogenerated_code { + // Expose proto data types from included third-party/external proto files. + pub use mc_api::{blockchain, external}; + pub use protobuf::well_known_types::Empty; + + // Needed due to how to the auto-generated code references the Empty message. + pub mod empty { + pub use protobuf::well_known_types::Empty; + } + + // Include the auto-generated code. + include!(concat!(env!("OUT_DIR"), "/protos-auto-gen/mod.rs")); +} + +pub use autogenerated_code::{wallet_service_mirror_api::*, *}; + +pub mod uri; diff --git a/mirror/src/private/crypto.rs b/mirror/src/private/crypto.rs new file mode 100644 index 000000000..3d0db3046 --- /dev/null +++ b/mirror/src/private/crypto.rs @@ -0,0 +1,263 @@ +// Copyright (c) 2018-2022 MobileCoin Inc. + +//! Cryptographic primitives. + +use boring::{ + pkey::Private, + rsa::{Padding, Rsa}, +}; + +const PKCS1_PADDING_LEN: usize = 11; + +/// Encrypt a payload of arbitrary length using a private key. +pub fn encrypt(key: &Rsa, payload: &[u8]) -> Result, String> { + // Each encrypted chunk must be no longer than the length of the public modulus minus 11 (PKCS1 padding size). + // (Taken from `rsa::oaep::encrypt`). + let key_size = key.size() as usize; + let max_chunk_size = key_size - PKCS1_PADDING_LEN; + + let chunks: Vec> = payload + .chunks(max_chunk_size) + .map(|chunk| { + let mut output = vec![0u8; key_size]; + + key.private_encrypt(&chunk[..], &mut output, Padding::PKCS1) + .map_err(|e| format!("encrypt failed: {:?}", e))?; + + Ok(output) + }) + .collect::, String>>()?; + + Ok(chunks + .into_iter() + .flat_map(|chunk| chunk.into_iter()) + .collect()) +} + +/// Decrypt a payload of arbitrary length using a private key. +pub fn decrypt(key: &Rsa, payload: &[u8]) -> Result, String> { + let key_size = key.size() as usize; + + let chunks: Vec> = payload + .chunks(key_size) + .map(|chunk| { + let mut output = vec![0u8; key_size]; + let num_bytes = key + .private_decrypt(&chunk[..], &mut output, Padding::PKCS1) + .map_err(|e| format!("decrypt failed: {:?}", e))?; + output.truncate(num_bytes as usize); + Ok(output) + }) + .collect::, String>>()?; + + Ok(chunks + .into_iter() + .flat_map(|chunk| chunk.into_iter()) + .collect()) +} + +/// Load a private key from a file +pub fn load_private_key(src: &str) -> Result, String> { + let key_str = std::fs::read_to_string(src) + .map_err(|err| format!("failed reading key file {}: {:?}", src, err))?; + + Ok( + Rsa::private_key_from_pem_passphrase(key_str.as_bytes(), &[]) + .map_err(|err| format!("failed parsing key file {}: {:?}", src, err))?, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use boring::pkey::Public; + use rand_core::{RngCore, SeedableRng}; + use rand_hc::Hc128Rng; + + /// Encrypt a payload of arbitrary length using a public key. + pub fn encrypt_public(key: &Rsa, payload: &[u8]) -> Result, String> { + // Each encrypted chunk must be no longer than the length of the public modulus minus 11 (PKCS1 padding size). + // (Taken from `rsa::oaep::encrypt`). + let key_size = key.size() as usize; + let max_chunk_size = key_size - PKCS1_PADDING_LEN; + + let chunks: Vec> = payload + .chunks(max_chunk_size) + .map(|chunk| { + let mut output = vec![0u8; key_size]; + + key.public_encrypt(&chunk[..], &mut output, Padding::PKCS1) + .map_err(|e| format!("encrypt failed: {:?}", e))?; + + Ok(output) + }) + .collect::, String>>()?; + + Ok(chunks + .into_iter() + .flat_map(|chunk| chunk.into_iter()) + .collect()) + } + + /// Decrypt a payload of arbitrary length using a public key. + fn decrypt_public(key: &Rsa, payload: &[u8]) -> Result, String> { + let key_size = key.size() as usize; + + let chunks: Vec> = payload + .chunks(key_size) + .map(|chunk| { + let mut output = vec![0u8; key_size]; + let num_bytes = key + .public_decrypt(&chunk[..], &mut output, Padding::PKCS1) + .map_err(|e| format!("decrypt failed: {:?}", e))?; + output.truncate(num_bytes as usize); + Ok(output) + }) + .collect::, String>>()?; + + Ok(chunks + .into_iter() + .flat_map(|chunk| chunk.into_iter()) + .collect()) + } + + #[test] + fn encrypt_private_decrypt_public_works_with_short_message() { + let priv_key = Rsa::generate(2048).unwrap(); + + let pub_key_pem = priv_key.public_key_to_pem().unwrap(); + let pub_key = Rsa::public_key_from_pem(&pub_key_pem).unwrap(); + + let message = b"this message is less than the key size"; + + let encrypted = encrypt(&priv_key, message).unwrap(); + assert_eq!(encrypted.len(), priv_key.size() as usize); + + let decrypted = decrypt_public(&pub_key, &encrypted).unwrap(); + assert_eq!(message, &decrypted[..]); + } + + #[test] + fn encrypt_private_decrypt_public_works_with_long_message_that_isnt_a_multiple_of_key_size() { + let priv_key = Rsa::generate(2048).unwrap(); + + let pub_key_pem = priv_key.public_key_to_pem().unwrap(); + let pub_key = Rsa::public_key_from_pem(&pub_key_pem).unwrap(); + + let mut message = vec![0u8; priv_key.size() as usize * 5 + 123]; // message size that does not divide exactly by the key length + let mut rng = Hc128Rng::from_seed([0u8; 32]); + rng.fill_bytes(&mut message); + + let encrypted = encrypt(&priv_key, &message).unwrap(); + assert_eq!(encrypted.len(), priv_key.size() as usize * 6); + + let decrypted = decrypt_public(&pub_key, &encrypted).unwrap(); + assert_eq!(message, &decrypted[..]); + } + + #[test] + fn encrypt_private_decrypt_public_works_with_long_message_that_is_a_multiple_of_key_size() { + let priv_key = Rsa::generate(2048).unwrap(); + + let pub_key_pem = priv_key.public_key_to_pem().unwrap(); + let pub_key = Rsa::public_key_from_pem(&pub_key_pem).unwrap(); + + let mut message = vec![0u8; priv_key.size() as usize * 4]; // message size that divides exactly by the key length + let mut rng = Hc128Rng::from_seed([0u8; 32]); + rng.fill_bytes(&mut message); + + let encrypted = encrypt(&priv_key, &message).unwrap(); + assert_eq!(encrypted.len(), priv_key.size() as usize * 5); // longer than message because of padding + + let decrypted = decrypt_public(&pub_key, &encrypted).unwrap(); + assert_eq!(message, &decrypted[..]); + } + + #[test] + fn encrypt_private_decrypt_public_works_with_long_message_that_is_a_multiple_of_chunk_size() { + let priv_key = Rsa::generate(2048).unwrap(); + + let pub_key_pem = priv_key.public_key_to_pem().unwrap(); + let pub_key = Rsa::public_key_from_pem(&pub_key_pem).unwrap(); + + let mut message = vec![0u8; 3 * (priv_key.size() as usize - PKCS1_PADDING_LEN)]; // message size that divides exactly by the chunk size + let mut rng = Hc128Rng::from_seed([0u8; 32]); + rng.fill_bytes(&mut message); + + let encrypted = encrypt(&priv_key, &message).unwrap(); + assert_eq!(encrypted.len(), priv_key.size() as usize * 3); + + let decrypted = decrypt_public(&pub_key, &encrypted).unwrap(); + assert_eq!(message, &decrypted[..]); + } + + #[test] + fn encrypt_public_decrypt_private_works_with_short_message() { + let priv_key = Rsa::generate(2048).unwrap(); + + let pub_key_pem = priv_key.public_key_to_pem().unwrap(); + let pub_key = Rsa::public_key_from_pem(&pub_key_pem).unwrap(); + + let message = b"this message is less than the key size"; + + let encrypted = encrypt_public(&pub_key, message).unwrap(); + assert_eq!(encrypted.len(), priv_key.size() as usize); + + let decrypted = decrypt(&priv_key, &encrypted).unwrap(); + assert_eq!(message, &decrypted[..]); + } + + #[test] + fn encrypt_public_decrypt_private_works_with_long_message_that_isnt_a_multiple_of_key_size() { + let priv_key = Rsa::generate(2048).unwrap(); + + let pub_key_pem = priv_key.public_key_to_pem().unwrap(); + let pub_key = Rsa::public_key_from_pem(&pub_key_pem).unwrap(); + + let mut message = vec![0u8; priv_key.size() as usize * 5 + 123]; // message size that does not divide exactly by the key length + let mut rng = Hc128Rng::from_seed([0u8; 32]); + rng.fill_bytes(&mut message); + + let encrypted = encrypt_public(&pub_key, &message).unwrap(); + assert_eq!(encrypted.len(), priv_key.size() as usize * 6); + + let decrypted = decrypt(&priv_key, &encrypted).unwrap(); + assert_eq!(message, &decrypted[..]); + } + + #[test] + fn encrypt_public_decrypt_private_works_with_long_message_that_is_a_multiple_of_key_size() { + let priv_key = Rsa::generate(2048).unwrap(); + + let pub_key_pem = priv_key.public_key_to_pem().unwrap(); + let pub_key = Rsa::public_key_from_pem(&pub_key_pem).unwrap(); + + let mut message = vec![0u8; priv_key.size() as usize * 4]; // message size that divides exactly by the key length + let mut rng = Hc128Rng::from_seed([0u8; 32]); + rng.fill_bytes(&mut message); + + let encrypted = encrypt_public(&pub_key, &message).unwrap(); + assert_eq!(encrypted.len(), priv_key.size() as usize * 5); // longer than message because of padding + + let decrypted = decrypt(&priv_key, &encrypted).unwrap(); + assert_eq!(message, &decrypted[..]); + } + + #[test] + fn encrypt_public_decrypt_private_works_with_long_message_that_is_a_multiple_of_chunk_size() { + let priv_key = Rsa::generate(2048).unwrap(); + + let pub_key_pem = priv_key.public_key_to_pem().unwrap(); + let pub_key = Rsa::public_key_from_pem(&pub_key_pem).unwrap(); + + let mut message = vec![0u8; 3 * (priv_key.size() as usize - PKCS1_PADDING_LEN)]; // message size that divides exactly by the chunk size + let mut rng = Hc128Rng::from_seed([0u8; 32]); + rng.fill_bytes(&mut message); + + let encrypted = encrypt_public(&pub_key, &message).unwrap(); + assert_eq!(encrypted.len(), priv_key.size() as usize * 3); + + let decrypted = decrypt(&priv_key, &encrypted).unwrap(); + assert_eq!(message, &decrypted[..]); + } +} diff --git a/mirror/src/private/main.rs b/mirror/src/private/main.rs new file mode 100644 index 000000000..08511e8ed --- /dev/null +++ b/mirror/src/private/main.rs @@ -0,0 +1,326 @@ +// Copyright (c) 2018-2021 MobileCoin Inc. + +//! The private side of wallet-service-mirror. +//! This program forms outgoing connections to both a wallet service instance, +//! as well as a public wallet-service-mirror instance. It then proceeds to poll +//! the public side of the mirror for requests which it then forwards to the +//! wallet service. When a response is received it is then forwarded back to the +//! mirror. + +mod crypto; + +use crate::crypto::{decrypt, encrypt, load_private_key}; +use boring::{pkey::Private, rsa::Rsa}; +use grpcio::ChannelBuilder; +use mc_common::logger::{create_app_logger, log, o, Logger}; +use mc_full_service_mirror::{ + uri::WalletServiceMirrorUri, + wallet_service_mirror_api::{ + EncryptedResponse, PollRequest, QueryRequest, QueryResponse, UnencryptedResponse, + }, + wallet_service_mirror_api_grpc::WalletServiceMirrorClient, +}; +use mc_util_grpc::ConnectionUriGrpcioChannel; +use std::{collections::HashMap, str::FromStr, sync::Arc, thread::sleep, time::Duration}; +use structopt::StructOpt; + +const SUPPORTED_ENDPOINTS: &[&str] = &[ + "check_receiver_receipt_status", + "create_payment_request", + "get_account", + "get_account_status", + "get_address_for_account", + "get_addresses_for_account", + "get_address_status", + "get_accounts", + "get_transaction_logs", + "get_block", + "get_confirmations", + "get_network_status", + "get_transaction_log", + "get_wallet_status", + "validate_confirmation", + "verify_address", + "get_txos", +]; + +/// How long do we wait for full-service to reply? +const FULL_SERVICE_TIMEOUT: Duration = Duration::from_secs(120); + +/// A wrapper to ease monitor id parsing from a hex string when using +/// `StructOpt`. +#[derive(Clone, Debug)] +pub struct MonitorId(pub Vec); +impl FromStr for MonitorId { + type Err = String; + fn from_str(src: &str) -> Result { + let bytes = + hex::decode(src).map_err(|err| format!("Error decoding monitor id: {:?}", err))?; + if bytes.len() != 32 { + return Err("monitor id needs to be exactly 32 bytes".into()); + } + Ok(Self(bytes)) + } +} + +/// Command line config +#[derive(Clone, Debug, StructOpt)] +#[structopt( + name = "wallet-service-mirror-private", + about = "The private side of wallet-service-mirror, receiving requests from the public side and forwarding them to the wallet service." +)] +pub struct Config { + /// Wallet service URI. + #[structopt(long, default_value = "http://127.0.0.1:9090/")] + pub wallet_service_uri: String, + + /// URI for the public side of the mirror. + #[structopt(long)] + pub mirror_public_uri: WalletServiceMirrorUri, + + /// How many milliseconds to wait between polling. + #[structopt(long, default_value = "100", parse(try_from_str=parse_duration_in_milliseconds))] + pub poll_interval: Duration, + + /// Optional encryption public key. If provided, only encrypted requests are + /// accepted. See `example-client.js` for an example on how to submit + /// encrypted requests through the mirror. + #[structopt(long, parse(try_from_str=load_private_key))] + pub mirror_key: Option>, +} + +fn main() { + mc_common::setup_panic_handler(); + let _sentry_guard = mc_common::sentry::init(); + + let config = Config::from_args(); + + let (logger, _global_logger_guard) = create_app_logger(o!()); + log::info!( + logger, + "Starting wallet-service-mirror private forwarder on {}, connecting to wallet service at {}", + config.mirror_public_uri, + config.wallet_service_uri, + ); + + // Set up the gRPC connection to the public side of the mirror. + let mirror_api_client = { + let env = Arc::new(grpcio::EnvBuilder::new().build()); + let ch = ChannelBuilder::new(env) + .max_receive_message_len(-1) + .max_send_message_len(-1) + .max_reconnect_backoff(Duration::from_millis(2000)) + .initial_reconnect_backoff(Duration::from_millis(1000)) + .connect_to_uri(&config.mirror_public_uri, &logger); + + WalletServiceMirrorClient::new(ch) + }; + + // Main polling loop. + log::debug!(logger, "Entering main loop"); + + let mut pending_responses: HashMap = HashMap::new(); + + loop { + // Communicate with the public side of the mirror. + let mut request = PollRequest::new(); + request.set_query_responses(pending_responses.clone()); + + log::debug!( + logger, + "Calling poll with {} queued responses", + pending_responses.len() + ); + match mirror_api_client.poll(&request) { + Ok(response) => { + log::debug!( + logger, + "Poll succeeded, got back {} requests", + response.query_requests.len() + ); + + // Clear pending responses since we successfully delivered them to the other + // side. + pending_responses.clear(); + + // Process requests. + for (query_id, query_request) in response.query_requests.iter() { + let query_logger = logger.new(o!("query_id" => query_id.clone())); + + let response = { + if let Some(mirror_key) = config.mirror_key.as_ref() { + process_encrypted_request( + &config.wallet_service_uri, + mirror_key, + query_request, + &query_logger, + ) + .unwrap_or_else(|err| { + log::error!( + query_logger, + "process_encrypted_request failed: {:?}", + err + ); + + let mut err_query_response = QueryResponse::new(); + err_query_response.set_error(err); + err_query_response + }) + } else { + process_unencrypted_request( + &config.wallet_service_uri, + query_request, + &query_logger, + ) + .unwrap_or_else(|err| { + log::error!( + query_logger, + "process_unencrypted_request failed: {:?}", + err + ); + + let mut err_query_response = QueryResponse::new(); + err_query_response.set_error(err); + err_query_response + }) + } + }; + + pending_responses.insert(query_id.clone(), response); + } + } + + Err(err) => { + log::error!( + logger, + "Polling the public side of the mirror failed: {:?}", + err + ); + } + } + + sleep(config.poll_interval); + } +} + +fn validate_method(json: &str) -> serde_json::Result { + let json: serde_json::Value = serde_json::from_str(json)?; + let method = json["method"].as_str().unwrap_or(""); + Ok(SUPPORTED_ENDPOINTS.iter().any(|&s| s == method)) +} + +fn process_unencrypted_request( + wallet_service_uri: &str, + query_request: &QueryRequest, + logger: &Logger, +) -> Result { + if !query_request.has_unencrypted_request() { + return Err("Only processing unencrypted requests".into()); + } + + let unencrypted_request = query_request.get_unencrypted_request(); + + log::debug!( + logger, + "Incoming unencrypted request ({})", + unencrypted_request.json_request + ); + + // Check that the request is of an allowed type. + match validate_method(&unencrypted_request.json_request) { + Ok(true) => (), + Ok(false) => return Err("Unsupported request".into()), + Err(err) => { + let mut err_query_response = QueryResponse::new(); + err_query_response.set_error(format!("Error parsing JSON request: {}", err)); + return Ok(err_query_response); + } + } + + // Pass request along to full-service. + let client = reqwest::blocking::Client::builder() + .timeout(FULL_SERVICE_TIMEOUT) + .build() + .map_err(|e| e.to_string())?; + let res = client + .post(wallet_service_uri) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body(unencrypted_request.json_request.clone()) + .send() + .map_err(|e| e.to_string())?; + let json_response = res.text().map_err(|e| e.to_string())?; + + let mut unencrypted_response = UnencryptedResponse::new(); + unencrypted_response.set_json_response(json_response); + + let mut mirror_response = QueryResponse::new(); + mirror_response.set_unencrypted_response(unencrypted_response); + Ok(mirror_response) +} + +fn process_encrypted_request( + wallet_service_uri: &str, + mirror_key: &Rsa, + query_request: &QueryRequest, + logger: &Logger, +) -> Result { + if !query_request.has_encrypted_request() { + return Err("Only processing encrypted requests".into()); + } + + let encrypted_request = query_request.get_encrypted_request(); + + // Decrypt the request. + let json_request = match decrypt(mirror_key, &encrypted_request.payload) + .map_err(|err| format!("Error decrypting request: {}", err)) + .and_then(|decrypted| { + String::from_utf8(decrypted).map_err(|err| format!("Error parsing utf8: {}", err)) + }) { + Ok(json_request) => json_request, + Err(err) => { + let mut err_query_response = QueryResponse::new(); + err_query_response.set_error(err); + return Ok(err_query_response); + } + }; + + log::debug!(logger, "Incoming encrypted request ({})", json_request,); + + // Check that the request is of an allowed type. + match validate_method(&json_request) { + Ok(true) => (), + Ok(false) => return Err("Unsupported request".into()), + Err(err) => { + let mut err_query_response = QueryResponse::new(); + err_query_response.set_error(format!("Error parsing JSON request: {}", err)); + return Ok(err_query_response); + } + } + + // Pass request along to full-service. + let client = reqwest::blocking::Client::builder() + .timeout(FULL_SERVICE_TIMEOUT) + .build() + .map_err(|e| e.to_string())?; + let res = client + .post(wallet_service_uri) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body(json_request.clone()) + .send() + .map_err(|e| e.to_string())?; + let json_response = res.text().map_err(|e| e.to_string())?; + + let encrypted_payload = + encrypt(mirror_key, &json_response.as_bytes()).map_err(|_e| "Encryption failed")?; + + let mut encrypted_response = EncryptedResponse::new(); + encrypted_response.set_payload(encrypted_payload); + + let mut mirror_response = QueryResponse::new(); + mirror_response.set_encrypted_response(encrypted_response); + Ok(mirror_response) +} + +fn parse_duration_in_milliseconds(src: &str) -> Result { + Ok(Duration::from_millis(u64::from_str(src)?)) +} diff --git a/mirror/src/public/main.rs b/mirror/src/public/main.rs new file mode 100644 index 000000000..90468470d --- /dev/null +++ b/mirror/src/public/main.rs @@ -0,0 +1,304 @@ +// Copyright (c) 2018-2022 MobileCoin Inc. + +//! The public side of wallet-service-mirror. +//! This program opens two listening ports: +//! 1) A GRPC server for receiving incoming poll requests from the private side +//! of the mirror 2) An http(s) server for receiving client requests which will +//! then be forwarded to the wallet service instance sitting behind the +//! private part of the mirror. + +#![feature(decl_macro)] + +mod mirror_service; +mod query; +mod utils; + +use grpcio::{ChannelBuilder, EnvBuilder, ServerBuilder}; +use mc_common::logger::{create_app_logger, log, o, Logger}; +use mc_full_service_mirror::{ + uri::WalletServiceMirrorUri, + wallet_service_mirror_api::{EncryptedRequest, QueryRequest, UnencryptedRequest}, +}; +use mc_util_grpc::{BuildInfoService, ConnectionUriGrpcioServer, HealthService}; +use mc_util_uri::{ConnectionUri, Uri, UriScheme}; +use mirror_service::MirrorService; +use query::QueryManager; +use rocket::{ + config::{Config as RocketConfig, Environment as RocketEnvironment}, + http::Status, + post, + response::Responder, + routes, Data, Request, Response, +}; +use std::{io::Read, sync::Arc}; +use structopt::StructOpt; + +pub type ClientUri = Uri; + +/// Wallet Service Mirror Uri Scheme +#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq, Clone)] +pub struct ClientUriScheme {} +impl UriScheme for ClientUriScheme { + /// The part before the '://' of a URL. + const SCHEME_SECURE: &'static str = "https"; + const SCHEME_INSECURE: &'static str = "http"; + + /// Default port numbers + const DEFAULT_SECURE_PORT: u16 = 8443; + const DEFAULT_INSECURE_PORT: u16 = 8000; +} + +/// Command line config +#[derive(Clone, Debug, StructOpt)] +#[structopt( + name = "wallet-service-mirror-public", + about = "The public side of wallet-service-mirror, receiving requests from clients and forwarding them to the wallet service through the private side of the mirror" +)] +pub struct Config { + /// Listening URI for the private-public interface connection (GRPC). + #[structopt(long)] + pub mirror_listen_uri: WalletServiceMirrorUri, + + /// Listening URI for client requests (HTTP(S)). + #[structopt(long)] + pub client_listen_uri: ClientUri, + + /// Override the number of workers used for the client http server. + /// This controls how many concurrent requests the server can process. + #[structopt(long)] + pub num_workers: Option, + + /// Allow using self-signed TLS certificate for GRPC connections. + #[structopt(long)] + pub allow_self_signed_tls: bool, +} + +/// State that is accessible by all rocket requests +struct State { + query_manager: QueryManager, + logger: Logger, +} + +/// Sets the status of the response to 400 (Bad Request). +#[derive(Debug, Clone, PartialEq)] +pub struct BadRequest(pub String); + +/// Sets the status code of the response to 400 Bad Request and include an error +/// message in the response. +impl<'r> Responder<'r> for BadRequest { + fn respond_to(self, req: &Request) -> Result, Status> { + let mut build = Response::build(); + build.merge(self.0.respond_to(req)?); + + build.status(Status::BadRequest).ok() + } +} +impl From<&str> for BadRequest { + fn from(src: &str) -> Self { + Self(src.to_owned()) + } +} +impl From for BadRequest { + fn from(src: String) -> Self { + Self(src) + } +} + +#[post("/unencrypted-request", format = "json", data = "")] +fn unencrypted_request( + state: rocket::State, + request_data: rocket::Data, +) -> Result { + let mut request = String::new(); + let res = request_data.open().read_to_string(&mut request); + if res.is_err() { + let msg = "Could not read request data for unencrypted request."; + log::error!(state.logger, "{}", msg,); + return Err(msg.into()); + } + + log::debug!(state.logger, "Enqueueing UnencryptedRequest({})", &request); + + let mut unencrypted_request = UnencryptedRequest::new(); + unencrypted_request.set_json_request(request.clone()); + + let mut query_request = QueryRequest::new(); + query_request.set_unencrypted_request(unencrypted_request); + + let query = state.query_manager.enqueue_query(query_request); + let query_response = query.wait()?; + + if query_response.has_error() { + log::error!( + state.logger, + "UnencryptedRequest({}) failed: {}", + request, + query_response.get_error() + ); + return Err(query_response.get_error().into()); + } + if !query_response.has_unencrypted_response() { + log::error!( + state.logger, + "UnencryptedRequest({}) returned incorrect response type", + request, + ); + return Err("Incorrect response type received".into()); + } + + log::info!( + state.logger, + "UnencryptedRequest({}) completed successfully", + request, + ); + + let response = query_response.get_unencrypted_response(); + Ok(response.get_json_response().to_string()) +} + +#[post( + "/encrypted-request", + format = "application/octet-stream", + data = "" +)] +fn encrypted_request(state: rocket::State, data: Data) -> Result, BadRequest> { + let mut payload = Vec::new(); + if let Err(err) = data.open().read_to_end(&mut payload) { + let msg = format!( + "Could not read request data for unencrypted request: {}", + err + ); + log::error!(state.logger, "{}", msg); + return Err(msg.into()); + } + let payload_len = payload.len(); + + let mut encrypted_request = EncryptedRequest::new(); + encrypted_request.set_payload(payload); + + let mut query_request = QueryRequest::new(); + query_request.set_encrypted_request(encrypted_request); + + log::debug!( + state.logger, + "Enqueueing EncryptedRequest({} bytes)", + payload_len, + ); + let query = state.query_manager.enqueue_query(query_request); + let query_response = query.wait()?; + + if query_response.has_error() { + log::error!( + state.logger, + "EncryptedRequest({} bytes) failed: {}", + payload_len, + query_response.get_error() + ); + return Err(query_response.get_error().into()); + } + if !query_response.has_encrypted_response() { + log::error!( + state.logger, + "EncryptedRequest({} bytes) returned incorrect response type", + payload_len, + ); + return Err("Incorrect response type received".into()); + } + + log::info!( + state.logger, + "EncryptedRequest({} bytes) completed successfully", + payload_len, + ); + + let response = query_response.get_encrypted_response(); + Ok(response.get_payload().to_vec()) +} + +fn main() { + mc_common::setup_panic_handler(); + let _sentry_guard = mc_common::sentry::init(); + + let config = Config::from_args(); + // if !config.allow_self_signed_tls + // && utils::is_tls_self_signed(&config.mirror_listen_uri).expect(" + // is_tls_self_signed failed") { + // panic!("Refusing to start with self-signed TLS certificate. Use + // --allow-self-signed-tls to override this check."); } + + let (logger, _global_logger_guard) = create_app_logger(o!()); + log::info!( + logger, + "Starting wallet service mirror public forwarder, listening for mirror requests on {} and client requests on {}", + config.mirror_listen_uri.addr(), + config.client_listen_uri.addr(), + ); + + // Common state. + let query_manager = QueryManager::default(); + + // Start the mirror-facing GRPC server. + log::info!(logger, "Starting mirror GRPC server"); + + let build_info_service = BuildInfoService::new(logger.clone()).into_service(); + let health_service = HealthService::new(None, logger.clone()).into_service(); + let mirror_service = MirrorService::new(query_manager.clone(), logger.clone()).into_service(); + + let env = Arc::new( + EnvBuilder::new() + .name_prefix("Mirror-RPC".to_string()) + .build(), + ); + + let ch_builder = ChannelBuilder::new(env.clone()) + .max_receive_message_len(-1) + .max_send_message_len(-1); + + let server_builder = ServerBuilder::new(env) + .register_service(build_info_service) + .register_service(health_service) + .register_service(mirror_service) + .channel_args(ch_builder.build_args()) + .bind_using_uri(&config.mirror_listen_uri, logger.clone()); + + let mut server = server_builder.build().unwrap(); + server.start(); + + // Start the client-facing webserver. + if config.client_listen_uri.use_tls() { + panic!("Client-listening using TLS is currently not supported due to `ring` crate version compatibility issues."); + } + + let mut rocket_config = RocketConfig::build( + RocketEnvironment::active().expect("Failed getitng rocket environment"), + ) + .address(config.client_listen_uri.host()) + .port(config.client_listen_uri.port()); + if config.client_listen_uri.use_tls() { + rocket_config = rocket_config.tls( + config + .client_listen_uri + .tls_chain_path() + .expect("failed getting tls chain path"), + config + .client_listen_uri + .tls_key_path() + .expect("failed getting tls key path"), + ); + } + if let Some(num_workers) = config.num_workers { + rocket_config = rocket_config.workers(num_workers); + } + let rocket_config = rocket_config + .finalize() + .expect("Failed creating client http server config"); + + log::info!(logger, "Starting client web server"); + rocket::custom(rocket_config) + .mount("/", routes![unencrypted_request, encrypted_request]) + .manage(State { + query_manager, + logger, + }) + .launch(); +} diff --git a/mirror/src/public/mirror_service.rs b/mirror/src/public/mirror_service.rs new file mode 100644 index 000000000..dbb34eea7 --- /dev/null +++ b/mirror/src/public/mirror_service.rs @@ -0,0 +1,61 @@ +use crate::query::QueryManager; +use grpcio::{RpcContext, RpcStatus, Service, UnarySink}; +use mc_common::logger::{log, Logger}; +use mc_full_service_mirror::{ + wallet_service_mirror_api::{PollRequest, PollResponse}, + wallet_service_mirror_api_grpc::{create_wallet_service_mirror, WalletServiceMirror}, +}; +use mc_util_grpc::{rpc_logger, send_result}; + +#[derive(Clone)] +pub struct MirrorService { + /// Query manager. + query_manager: QueryManager, + + /// Logger. + logger: Logger, +} + +impl MirrorService { + pub fn new(query_manager: QueryManager, logger: Logger) -> Self { + Self { + query_manager, + logger, + } + } + + pub fn into_service(self) -> Service { + create_wallet_service_mirror(self) + } + + fn poll_impl(&self, request: PollRequest, logger: &Logger) -> Result { + // Go over any responses we may have received and attempt to resolve them. + for (query_id, query_response) in request.get_query_responses().iter() { + match self.query_manager.resolve_query(query_id, query_response) { + Ok(()) => log::info!(logger, "Query {} resolved", query_id), + Err(err) => log::error!(logger, "Query {} failed resolving: {}", query_id, err), + } + } + + // Return any queries we have received. + let pending_requests = self.query_manager.get_pending_requests(); + + log::debug!( + logger, + "Polled with {} returned responses and {} new requests", + request.get_query_responses().len(), + pending_requests.len() + ); + + let mut response = PollResponse::new(); + response.set_query_requests(pending_requests); + Ok(response) + } +} + +impl WalletServiceMirror for MirrorService { + fn poll(&mut self, ctx: RpcContext, request: PollRequest, sink: UnarySink) { + let logger = rpc_logger(&ctx, &self.logger); + send_result(ctx, sink, self.poll_impl(request, &logger), &logger) + } +} diff --git a/mirror/src/public/query.rs b/mirror/src/public/query.rs new file mode 100644 index 000000000..a65200ec1 --- /dev/null +++ b/mirror/src/public/query.rs @@ -0,0 +1,163 @@ +// Copyright (c) 2018-2021 MobileCoin Inc. + +//! Utility entity for managing queries submitted over our rocket endpoint and +//! resolved by the GRPC polling mechanism. + +use mc_full_service_mirror::wallet_service_mirror_api::{QueryRequest, QueryResponse}; +use rand::RngCore; +use std::{ + collections::HashMap, + sync::{Arc, Condvar, Mutex}, + time::Duration, +}; + +/// The length of the randomly generated query id that is used to tie requests +/// and responses together. +const QUERY_ID_LEN: usize = 8; + +/// The maximum amount of time to wait for a query to complete. +const QUERY_MAX_DURATION: Duration = Duration::from_secs(120); + +/// The state held by each individual query. +struct QueryInner { + request: QueryRequest, + response: Option, +} + +/// An individual query that can be asynchronously resolved and waited on. +#[derive(Clone)] +pub struct Query { + inner: Arc>, + condvar: Arc, +} + +impl Query { + pub fn new(request: QueryRequest) -> Self { + Self { + inner: Arc::new(Mutex::new(QueryInner { + request, + response: None, + })), + condvar: Arc::new(Condvar::new()), + } + } + + pub fn request(&self) -> QueryRequest { + self.inner.lock().expect("mutex poisoned").request.clone() + } + + pub fn resolve(&self, response: QueryResponse) { + let mut inner = self.inner.lock().expect("mutex poisoned"); + inner.response = Some(response); + self.condvar.notify_one(); + } + + pub fn wait(self) -> Result { + let (mut inner, wait_result) = self + .condvar + .wait_timeout_while( + self.inner.lock().expect("muted poisoned"), + QUERY_MAX_DURATION, + |inner| inner.response.is_none(), + ) + .expect("waiting on condvar failed"); + + if wait_result.timed_out() { + return Err("timeout".into()); + } + + assert!(inner.response.is_some()); + + Ok(inner + .response + .take() + .expect("response should've had something in it")) + } +} + +struct QueryManagerInner { + /// Map of query id -> query of queries that need to get sent to the private + /// side of the mirror. + pending_requests: HashMap, + + /// Map of query id -> query of queries that were resolved by the mirror. + pending_responses: HashMap, +} + +impl QueryManagerInner { + pub fn generate_query_id(&self) -> String { + let mut rng = rand::thread_rng(); + + const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ + abcdefghijklmnopqrstuvwxyz\ + 0123456789"; + + loop { + let query_id: String = (0..QUERY_ID_LEN) + .map(|_| { + let idx = (rng.next_u64() % CHARSET.len() as u64) as usize; + char::from(CHARSET[idx]) + }) + .collect(); + + if !self.pending_requests.contains_key(&query_id) + && !self.pending_responses.contains_key(&query_id) + { + return query_id; + } + } + } +} + +#[derive(Clone)] +pub struct QueryManager { + inner: Arc>, +} + +impl Default for QueryManager { + fn default() -> Self { + Self { + inner: Arc::new(Mutex::new(QueryManagerInner { + pending_requests: HashMap::new(), + pending_responses: HashMap::new(), + })), + } + } +} + +impl QueryManager { + pub fn enqueue_query(&self, request: QueryRequest) -> Query { + let mut inner = self.inner.lock().expect("mutex poisoned"); + let query_id = inner.generate_query_id(); + let query = Query::new(request); + inner.pending_requests.insert(query_id, query.clone()); + query + } + + pub fn get_pending_requests(&self) -> HashMap { + let mut inner = self.inner.lock().expect("mutex poisoned"); + let mut pending_requests = HashMap::new(); + let mut pending_responses = HashMap::new(); + + for (query_id, query) in inner.pending_requests.drain() { + pending_requests.insert(query_id.clone(), query.request()); + pending_responses.insert(query_id, query); + } + + for (query_id, query) in pending_responses.drain() { + inner.pending_responses.insert(query_id, query); + } + + pending_requests + } + + pub fn resolve_query(&self, query_id: &str, response: &QueryResponse) -> Result<(), String> { + let mut inner = self.inner.lock().expect("mutex poisoned"); + let query = inner + .pending_responses + .remove(query_id) + .ok_or_else(|| format!("Unknown query id {}", query_id))?; + query.resolve(response.clone()); + Ok(()) + } +} diff --git a/mirror/src/public/utils.rs b/mirror/src/public/utils.rs new file mode 100644 index 000000000..b5017bcd9 --- /dev/null +++ b/mirror/src/public/utils.rs @@ -0,0 +1,43 @@ +/* +// Copyright (c) 2018-2021 MobileCoin Inc. + +//! Misc utility methods. + +use mc_util_uri::ConnectionUri; +use x509_parser::{error::X509Error, parse_x509_der, pem::pem_to_der}; + +/// Checks if an optionally-provided TLS certificate is self-signed. Returns false if no TLS is +/// configured for the URI. +pub fn is_tls_self_signed(uri: &impl ConnectionUri) -> Result { + // Short-circuit if no TLS is configured. + if !uri.use_tls() { + return Ok(false); + } + + // Must have a TLS certificate, and must be able to read it. + let cert_pem_bytes = uri.tls_chain()?; + + let (rem, pem) = + pem_to_der(&cert_pem_bytes).map_err(|err| format!("pem_to_der failed: {}", err))?; + if !rem.is_empty() || pem.label != "CERTIFICATE" { + return Err(format!( + "Failed parsing PEM: rem={:?}, pem.label={}", + rem, pem.label + )); + } + + let (rem, cert) = + parse_x509_der(&pem.contents).map_err(|err| format!("parse_x509_der failed: {}", err))?; + if !rem.is_empty() { + return Err(format!("Failed parsing DER: rem={:?}", rem)); + } + + // Check if we are self signed. If we are, the veritifcation would pass. + // (Passing None defaults to cert.tbs_certificate.subject_pki) + match cert.verify_signature(Some(&cert.tbs_certificate.subject_pki)) { + Ok(()) => Ok(true), + Err(X509Error::SignatureVerificationError) => Ok(false), + Err(err) => Err(format!("Error verifying certificate: {:?}", err)), + } +} +*/ diff --git a/mirror/src/uri.rs b/mirror/src/uri.rs new file mode 100644 index 000000000..f647378da --- /dev/null +++ b/mirror/src/uri.rs @@ -0,0 +1,18 @@ +// Copyright (c) 2018-2021 MobileCoin Inc. + +use mc_util_uri::{Uri, UriScheme}; + +pub type WalletServiceMirrorUri = Uri; + +/// Wallet Service Mirror Uri Scheme +#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq, Clone)] +pub struct WalletServiceMirrorScheme {} +impl UriScheme for WalletServiceMirrorScheme { + /// The part before the '://' of a URL. + const SCHEME_SECURE: &'static str = "wallet-service-mirror"; + const SCHEME_INSECURE: &'static str = "insecure-wallet-service-mirror"; + + /// Default port numbers + const DEFAULT_SECURE_PORT: u16 = 10443; + const DEFAULT_INSECURE_PORT: u16 = 10080; +} diff --git a/mirror/test/Dockerfile b/mirror/test/Dockerfile new file mode 100644 index 000000000..ba82c5e98 --- /dev/null +++ b/mirror/test/Dockerfile @@ -0,0 +1,48 @@ +FROM ubuntu:18.04 + +ENV mirversion="1.5.0" + +RUN apt-get update && apt-get -yy install curl openssl + +# Install NVM +SHELL ["/bin/bash", "-c"] +# nvm environment variables +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION 16.14.0 + +# install nvm +# https://github.com/creationix/nvm#install-script +RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash + +# install node and npm +RUN source $NVM_DIR/nvm.sh \ + && nvm install $NODE_VERSION \ + && nvm alias default $NODE_VERSION \ + && nvm use default + +# add node and npm to path so the commands are available +ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules +ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH + +# confirm installation +RUN node -v +RUN npm -v +SHELL ["/bin/sh", "-c"] + +# Download and install the full service mirror package +RUN curl -L -o full-service-mirror.tar.gz https://github.com/mobilecoinofficial/full-service-mirror/releases/download/v$mirversion/linux-v$mirversion-testnet.tar.gz +RUN tar xf full-service-mirror.tar.gz && rm full-service-mirror.tar.gz +WORKDIR /linux-v$mirversion-testnet + +# Copy the useful test scripts into the docker image. +COPY ./run.sh . +COPY ./test_lib ./test_lib +COPY ./test_suite ./test_suite +WORKDIR /linux-v$mirversion-testnet/test_lib +RUN npm install +WORKDIR /linux-v$mirversion-testnet/test_suite +RUN npm install +WORKDIR /linux-v$mirversion-testnet + +# Run the script +ENTRYPOINT ["/bin/bash", "./run.sh"] diff --git a/mirror/test/README.md b/mirror/test/README.md new file mode 100644 index 000000000..fca83206b --- /dev/null +++ b/mirror/test/README.md @@ -0,0 +1,19 @@ +# Testing for Full Service Mirror + +This directory contains a shell script + associated files for testing a full service mirror release. + +This can be run with varying degress of automation. Either with docker, or by running run.sh with a binary you downloaded yourself + +## Test with Docker + +To test a release, change the Dockerfile mirversion to point to the release of the fullservicemirror that you want to test. Then call the following: +```sh +docker built -t fullservice-mirror-test . +``` +```sh +docker run fullservice-mirror-test +``` + +## Test without Docker using run.sh + +Download the release and unzip it to the same directory that run.sh is in, then you can call run.sh and it should start LVN, Full Service, Full Service Mirror, and run all the tests against it. diff --git a/mirror/test/example-client.js b/mirror/test/example-client.js new file mode 100644 index 000000000..99e9cd782 --- /dev/null +++ b/mirror/test/example-client.js @@ -0,0 +1,17 @@ + +const client = require('./test_lib/send-request-encrypted'); + +// Command line parsing +if (process.argv.length != 6) { + console.log(`Usage: node example-client.js `); + console.log(`For example: node example-client.js 127.0.0.1 9091 mirror-client.pem '{"method": "get_block", "params": {"block_index": "0"}, "jsonrpc": "2.0", "id": 1}'`); + console.log('To generate keys please run the generate-rsa-keypair binary. See README.md for more details') + throw "invalid arguments"; +} + +let public_mirror_host = process.argv[2]; +let public_mirror_port = process.argv[3]; +let key_file = process.argv[4]; +let request = process.argv[5]; + +client.sendRequest(public_mirror_host, public_mirror_port, key_file, request); diff --git a/mirror/test/run.sh b/mirror/test/run.sh new file mode 100755 index 000000000..f752b9766 --- /dev/null +++ b/mirror/test/run.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -eu +export RUST_LOG=DEBUG + +mnemonic="MNEMONIC HERE" + +echo "generate private key for tls" + +openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout server.key -out server.crt -subj "/C=US/ST=CA/L=SF/O=MobileCoin/OU=IT/CN=localhost" + +echo "generate keypair for mirror" + +./bin/generate-rsa-keypair + + +echo "Calling wallet-service-mirror-public-tls.sh. This starts the public mirror." +./wallet-service-mirror-public-tls.sh \ + > /tmp/mobilecoin-public-mirror.log 2>&1 & + + +echo "Calling wallet-service-mirror-private-tls-encrypted.sh. This starts the validator and full service as well as the private mirror." +./wallet-service-mirror-private-tls-encrypted.sh localhost \ + > /tmp/mobilecoin-private-mirror.log 2>&1 & + + +echo "Checking that these methods are unsupported." +declare -a MethodList=("assign_address_for_account" "build_and_submit_transaction" "build_gift_code" "build_split_txo_transaction" "build_transaction" "check_b58_type" "check_gift_code_status" "check_receiver_receipt_status" "claim_gift_code" "create_account" "create_receiver_receipts" "export_account_secrets" "get_all_addresses_for_account" "get_all_gift_codes" "get_all_transaction_logs_for_account" "get_all_transaction_logs_ordered_by_block" "get_all_txos_for_account" "get_all_txos_for_address" "get_gift_code" "get_mc_protocol_transaction" "get_mc_protocol_txo" "get_txo" "get_txos_for_account" "import_account" "import_account_from_legacy_root_entropy" "remove_account" "remove_gift_code" "submit_gift_code" "submit_transaction" "update_account_name") +for method in ${MethodList[@]}; do +response=$(node example-client.js 0.0.0.0 9091 mirror-client.pem "{ + \"method\": \"${method}\", + \"params\": {}, + \"jsonrpc\": \"2.0\", + \"api_version\": \"2\", + \"id\": 1 + }") +if [ "$response" != 'Http error, status: 400: Unsupported request' ] +then +echo "$method return $response which was not 'Http error, status: 400: Unsupported request'" +exit 42 +fi +done +echo "Unsupported methods have been verified, testing support methods" + +response=$(node ./test_suite/test_script.js 0.0.0.0 9091 127.0.0.1 5554 mirror-client.pem "${mnemonic}") +echo "Test result: $response" + +exit 0 + + diff --git a/mirror/test/test_lib/package.json b/mirror/test/test_lib/package.json new file mode 100644 index 000000000..4ca57fb11 --- /dev/null +++ b/mirror/test/test_lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "test_lib", + "version": "1.0.0", + "description": "", + "main": "send-request-encrypted.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Mobile Coin", + "license": "MIT" +} diff --git a/mirror/test/test_lib/send-request-encrypted.js b/mirror/test/test_lib/send-request-encrypted.js new file mode 100644 index 000000000..4fd6232db --- /dev/null +++ b/mirror/test/test_lib/send-request-encrypted.js @@ -0,0 +1,165 @@ +// Minimum supported NodeJS version: v12.9.0 +const NODE_MAJOR_VERSION = process.versions.node.split('.')[0]; +if (NODE_MAJOR_VERSION < 12) { + throw new Error('Requires Node 12 (or higher)'); +} + +// Imports +const http = require('http'); +const fs = require('fs'); +const crypto = require('crypto'); +const KEY_SIZE = 512; + + +function sendRequestPromise(params, postData, processSuccessfulResponse) { + return new Promise(function(resolve, reject) { + var req = http.request(params, (response) => { + // cumulate data + var buf = []; + var result; + response.on('data', function(chunk) { + buf.push(chunk); + }); + // resolve on end + response.on('end', function() { + if (response.statusCode == 200) { + try { + result = processSuccessfulResponse(buf); + } catch(error) { + reject(`Failed to process a successful request: ${error}`); + } + resolve(result); + } else { + reject (`Http error, status: ${response.statusCode}: ${buf}`); + } + }); + //reject on response error + response.on('error', (error) => { + reject(`Error reading response: ${error}`); + }); + + }); + // reject on request error + req.on('error', function(error) { + reject(`Error sending request: ${error}`); + }); + req.write(postData); + req.end(); + }); +} + + + +function sendRequest(host, port, key_file, request) { + return sendRequestEncrypted(host, port, "/encrypted-request", key_file, request); +} + + +function sendRequestEncrypted(host, port, path, key_file, msg) { + // Load key + let key_bytes = fs.readFileSync(key_file) + if (!key_bytes) { + throw 'Failed loading key'; + } + let key = crypto.createPublicKey(key_bytes); + if (!key) { + throw 'Failed creating key'; + } + + + // Ensure the key is 4096 bits (outputs 512-byte chunks). + let test_data = encrypt(key, [1, 2, 3]); + if (test_data.length != KEY_SIZE) { + throw `Key is not 4096-bit, encrypted output chunk size returned was ${test_data.length}`; + } + + // Prepare request + let encrypted_msg = encrypt(key, msg); + + // Send request to server + let params = { + host: host, + port: port, + timeout: 120000, + path: path, + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream', + 'Content-Length': Buffer.byteLength(encrypted_msg) + } + }; + let processSuccessfulResponse = (buf) => { + let result = decrypt(key, Buffer.concat(buf)).toString(); + console.log(result); + return result; + }; + return sendRequestPromise(params, encrypted_msg, processSuccessfulResponse); +} + +function sendRequestUnencrypted(host, port, path, msg) { + // Send request to server + let params = { + host: host, + port: port, + timeout: 120000, + path: path, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(msg) + } + }; + let processSuccessfulResponse = (buf) => { + let result = Buffer.concat(buf).toString(); + console.log(result); + return result; + }; + return sendRequestPromise(params, msg, processSuccessfulResponse); +} + + + +// Crypto utilities +function encrypt(key, buf) { + let res = []; + + // Each encrypted chunk must be no longer than the length of the public modulus minus padding size. + // PKCS1 is 11 bytes of padding (which is also defined as PKCS1_PADDING_LEN in the rust code). + const MAX_CHUNK_SIZE = KEY_SIZE - 11; + + while (buf.length > 0) { + let data = buf.slice(0, MAX_CHUNK_SIZE); + buf = buf.slice(data.length); + + res.push(crypto.publicEncrypt({ + key: key, + padding: crypto.constants.RSA_PKCS1_PADDING, + }, Buffer.from(data))); + } + + return Buffer.concat(res) +} + +function decrypt(key, buf) { + let res = []; + + while (buf.length > 0) { + let data = buf.slice(0, KEY_SIZE); + buf = buf.slice(data.length); + + res.push(crypto.publicDecrypt({ + key, + padding: crypto.constants.RSA_PKCS1_PADDING, + }, Buffer.from(data))); + } + + return Buffer.concat(res) +} + +function sign(buf) { + return crypto.sign(null, Buffer.from(buf), { key, passphrase: '' }) +} + +exports.sendRequest = sendRequest; +exports.sendRequestUnencrypted = sendRequestUnencrypted; +exports.sendRequestEncrypted = sendRequestEncrypted; diff --git a/mirror/test/test_suite/package.json b/mirror/test/test_suite/package.json new file mode 100644 index 000000000..0e579c767 --- /dev/null +++ b/mirror/test/test_suite/package.json @@ -0,0 +1,14 @@ +{ + "name": "test_suite", + "version": "1.0.0", + "description": "run tests for lvn", + "main": "test_script.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "test_lib": "file:../test_lib" + } +} diff --git a/mirror/test/test_suite/test_script.js b/mirror/test/test_suite/test_script.js new file mode 100644 index 000000000..d4e3c469a --- /dev/null +++ b/mirror/test/test_suite/test_script.js @@ -0,0 +1,420 @@ +// Minimum supported NodeJS version: v12.9.0 +const NODE_MAJOR_VERSION = process.versions.node.split('.')[0]; +if (NODE_MAJOR_VERSION < 12) { + throw new Error('Requires Node 12 (or higher)'); +} + +// Imports +const client = require('../test_lib/send-request-encrypted'); +const full_service_path = "/wallet/v2"; +const public_mirror_path = "/encrypted-request"; +const wait_time_ms = 10000; +const fields = ["method", "params", "jsonrpc", "id", "block_index", "mnemonic", "key_derivation_version", "name", "account_id", "recipient_public_address", "value_pmob", "offset", "limit", "subaddress_index", "memo", "amount_pmob", "address", "transaction_log_id", "txo_id", "confirmation", "amount", "value", "token_id"]; + +function getFuncName() { + return getFuncName.caller.name +} +const timer = (ms) => new Promise((res) => setTimeout(res, ms)); +async function runAllTests(public_mirror_host, public_mirror_port, full_service_host, full_service_port, key_file, mnemonic) { + try { + let blockJSON = await testGetBlock(public_mirror_host, public_mirror_port, key_file); + let accountJSON = await testImportAccount(full_service_host, full_service_port, mnemonic); + let accountInfo = JSON.parse(accountJSON); + let accountId = accountInfo["result"]["account"]["id"]; + let mainAddress = accountInfo["result"]["account"]["main_address"]; + console.log(`account_id: ${accountId}, main_address: ${mainAddress}`); + let balanceJSON = await waitForBalanceToBeSynced(public_mirror_host, public_mirror_port, key_file, accountId); + let balanceInfo = JSON.parse(balanceJSON); + localBlockHeight = balanceInfo["result"]["local_block_height"]; + accountBlockHeight = balanceInfo["result"]["account_block_height"]; + console.log(balanceJSON); + + let transactionJSON = await testBuildAndSubmitTransaction(full_service_host, full_service_port, accountId, mainAddress, "1"); + let transactionInfo = JSON.parse(transactionJSON); + let transactionLogId = transactionInfo["result"]["transaction_log"]["id"]; + let transactionBlockIndex = transactionInfo["result"]["transaction_log"]["submitted_block_index"]; + let outputTxoHex = transactionInfo["result"]["transaction_log"]["output_txos"][0]["txo_id_hex"]; + console.log(`transactionLogId: ${transactionLogId}`); + console.log(`transaction block: ${transactionBlockIndex}`); + console.log(`outputTxo: ${outputTxoHex}`); + let addresses = await testGetAddressesForAccount(public_mirror_host, public_mirror_port, key_file, accountId); + let accountStatus = await testGetAccountStatus(public_mirror_host, public_mirror_port, key_file, accountId); + let paymentRequest = await testCreatePaymentRequest(public_mirror_host, public_mirror_port, key_file, accountId, 1, 1); + let balance = await testGetAddressStatus(public_mirror_host, public_mirror_port, key_file, mainAddress); + let addressVerification = await testVerifyAddress(public_mirror_host, public_mirror_port, key_file, mainAddress); + let walletStatus = await testWalletStatus(public_mirror_host, public_mirror_port, key_file); + let networkStatus = await testNetworkStatus(public_mirror_host, public_mirror_port, key_file); + let transactionLogs = await testGetTransactionLogsForBlock(public_mirror_host, public_mirror_port, key_file, transactionBlockIndex); + transactionLogs = await testGetTransactionLogsForAccount(public_mirror_host, public_mirror_port, key_file, accountId, "1", "1"); + transactionLogs = await waitForTransactionToBeSynced(public_mirror_host, public_mirror_port, key_file, transactionLogId); + let confirmationJSON = await testGetConfirmations(public_mirror_host, public_mirror_port, key_file, transactionLogId); + let confirmationInfo = JSON.parse(confirmationJSON); + console.log(`confirmationJson: ${confirmationJSON}`); + let confirmation = confirmationInfo["result"]["confirmations"][0]["confirmation"]; + console.log(`confirmation: ${confirmation}`); + let validation = await testValidateConfirmations(public_mirror_host, public_mirror_port, key_file, accountId, outputTxoHex, confirmation);; + + } + catch (error) { + console.error(`Test failed: ${error}`); + throw "Failed"; + } +} + + +async function testImportAccount(full_service_host, full_service_port, mnemonic) { + try { + let request = { + method: "import_account", + params: { + mnemonic: mnemonic, + key_derivation_version: "2", + name: "Bob" + }, + jsonrpc: "2.0", + id: 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequestUnencrypted(full_service_host, full_service_port, full_service_path, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} +async function testBuildAndSubmitTransaction(full_service_host, full_service_port, account_id, recipient_address, value_pmob) { + try { + let request = { + "method": "build_and_submit_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": recipient_address, + "amount": { "value": value_pmob, "token_id": "0" } + }, + "jsonrpc": "2.0", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequestUnencrypted(full_service_host, full_service_port, full_service_path, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} + + +async function testGetBlock(public_mirror_host, public_mirror_port, key_file) { + try { + let request = { + method: "get_block", + params: { + block_index: "0" + }, + jsonrpc: "2.0", + id: 1 + }; + let requestString = JSON.stringify(request, fields); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} +async function testGetAccountStatus(public_mirror_host, public_mirror_port, key_file, account_id) { + try { + let request = { + "method": "get_account_status", + "params": { + "account_id": account_id + }, + "jsonrpc": "2.0", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} +async function waitForBalanceToBeSynced(public_mirror_host, public_mirror_port, key_file, account_id) { + let balanceJSON = await testGetAccountStatus(public_mirror_host, public_mirror_port, key_file, account_id); + let balanceInfo = JSON.parse(balanceJSON); + let balance_synced = balanceInfo["result"]["account"]["next_block_index"] >= balanceInfo["result"]["network_block_height"]; + while (!balance_synced) { + await timer(wait_time_ms); + balanceJSON = await testGetAccountStatus(public_mirror_host, public_mirror_port, key_file, account_id); + balanceInfo = JSON.parse(balanceJSON); + balance_synced = balanceInfo["result"]["account"]["next_block_index"] >= balanceInfo["result"]["network_block_height"]; + } + return balanceJSON; +} +async function testGetAddressesForAccount(public_mirror_host, public_mirror_port, key_file, account_id) { + try { + let request = { + "method": "get_addresses_for_account", + "params": { + "account_id": account_id, + "offset": "1", + "limit": "1000" + }, + "jsonrpc": "2.0", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} +async function testGetAccountStatus(public_mirror_host, public_mirror_port, key_file, account_id) { + try { + let request = { + "method": "get_account_status", + "params": { + "account_id": account_id + }, + "jsonrpc": "2.0", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} +async function testCreatePaymentRequest(public_mirror_host, public_mirror_port, key_file, account_id, amount_pmob, subaddress_index) { + try { + let request = { + "method": "create_payment_request", + "params": { + "account_id": account_id, + "amount_pmob": amount_pmob, + "subaddress_index": subaddress_index, + "memo": "testCreatePaymentRequest" + }, + "jsonrpc": "2.0", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} +async function testGetAddressStatus(public_mirror_host, public_mirror_port, key_file, address) { + try { + let request = { + "method": "get_address_status", + "params": { + "address": address + }, + "jsonrpc": "2.0", + "api_version": "2", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} +async function testVerifyAddress(public_mirror_host, public_mirror_port, key_file, address) { + try { + let request = { + "method": "verify_address", + "params": { + "address": address + }, + "jsonrpc": "2.0", + "id": 1 + }; + + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} + +async function testWalletStatus(public_mirror_host, public_mirror_port, key_file) { + try { + let request = { + "method": "get_wallet_status", + "jsonrpc": "2.0", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} + +async function testNetworkStatus(public_mirror_host, public_mirror_port, key_file) { + try { + let request = { + "method": "get_network_status", + "jsonrpc": "2.0", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} +async function testGetTransactionLogsForBlock(public_mirror_host, public_mirror_port, key_file, block_index) { + try { + let request = { + "method": "get_transaction_logs", + "params": { + "min_block_index": block_index, + "max_block_index": block_index + }, + "jsonrpc": "2.0", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} + +async function testGetTransactionLogsForAccount(public_mirror_host, public_mirror_port, key_file, account_id, offset, limit) { + try { + let request = { + "method": "get_transaction_logs", + "params": { + "account_id": account_id, + "offset": offset, + "limit": limit + }, + "jsonrpc": "2.0", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} + +async function testGetTransactionLogsById(public_mirror_host, public_mirror_port, key_file, transaction_log_id) { + try { + let request = { + "method": "get_transaction_log", + "params": { + "transaction_log_id": transaction_log_id + }, + "jsonrpc": "2.0", + "id": 1 + }; + + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} +async function waitForTransactionToBeSynced(public_mirror_host, public_mirror_port, key_file, transaction_log_id) { + let transactionLogsJSON = await testGetTransactionLogsById(public_mirror_host, public_mirror_port, key_file, transaction_log_id); + let transactionInfo = JSON.parse(transactionLogsJSON); + let transaction_status = transactionInfo["result"]["transaction_log"]["status"]; + while (transaction_status === "pending") { + await timer(wait_time_ms); + transactionLogsJSON = await testGetTransactionLogsById(public_mirror_host, public_mirror_port, key_file, transaction_log_id); + transactionInfo = JSON.parse(transactionLogsJSON); + transaction_status = transactionInfo["result"]["transaction_log"]["status"]; + } + return transactionLogsJSON; +} +async function testGetConfirmations(public_mirror_host, public_mirror_port, key_file, transaction_log_id) { + try { + let request = { + "method": "get_confirmations", + "params": { + "transaction_log_id": transaction_log_id + }, + "jsonrpc": "2.0", + "id": 1 + }; + + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + throw `Error in ${getFuncName()}: ${error}`; + } +} +async function testValidateConfirmations(public_mirror_host, public_mirror_port, key_file, account_id, txo_id, confirmation) { + for (let i = 0; i < 3; i++) { + try { + let request = { + "method": "validate_confirmation", + "params": { + "account_id": account_id, + "txo_id": txo_id, + "confirmation": confirmation + }, + "jsonrpc": "2.0", + "id": 1 + }; + let requestString = JSON.stringify(request, fields); + console.log(requestString); + return await client.sendRequest(public_mirror_host, public_mirror_port, key_file, requestString); + } + catch (error) { + if (i >= 3) { + throw `Error in ${getFuncName()}: ${error}`; + } + await timer(wait_time_ms); + } + } +} +console.log("Starting test script") +// Command line parsing +if (process.argv.length != 8) { + console.log(`Usage: node test_script.js `); + console.log(`For example: node test_script.js 127.0.0.1 9091 127.0.0.1 5554 mirror-client.pem ''`); + console.log('To generate keys please run the generate-rsa-keypair binary. See README.md for more details') + throw "invalid arguments"; +} + +let public_mirror_host = process.argv[2]; +let public_mirror_port = process.argv[3]; +let full_service_host = process.argv[4]; +let full_service_port = process.argv[5]; +let key_file = process.argv[6]; +let mnemonic = process.argv[7]; + + +runAllTests(public_mirror_host, public_mirror_port, full_service_host, full_service_port, key_file, mnemonic).then(result => { + console.log("Run all tests succeeded"); +}).catch((error) => { + console.log("Run all tests had an error: " + error) +}); \ No newline at end of file diff --git a/tools/mirror/run-full-service.sh b/tools/mirror/run-full-service.sh new file mode 100755 index 000000000..c1d910aee --- /dev/null +++ b/tools/mirror/run-full-service.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Copyright (c) 2022 The MobileCoin Foundation + +NET="$1" + +if [ "$NET" == "main" ]; then + NAMESPACE="prod" + PEER_DOMAIN="prod.mobilecoinww.com/" + TX_SOURCE_URL="https://ledger.mobilecoinww.com" + INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) +elif [ "$NET" == "test" ]; then + NAMESPACE=$NET + PEER_DOMAIN="test.mobilecoin.com/" + TX_SOURCE_URL="https://s3-us-west-1.amazonaws.com/mobilecoin.chain" + INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) +elif [ "$NET" == "alpha" ]; then + NAMESPACE=$NET + PEER_DOMAIN="alpha.development.mobilecoin.com/" + TX_SOURCE_URL="https://s3-eu-central-1.amazonaws.com/mobilecoin.eu.development.chain" + INGEST_SIGSTRUCT_URI="" +else + # TODO: add support for local network + echo "Unknown network" + echo "Usage: run-fs.sh {main|test|alpha} [--no-build]" + exit 1 +fi + +WORK_DIR="$HOME/.mobilecoin/${NET}" +WALLET_DB_DIR="${WORK_DIR}/wallet-db" +LEDGER_DB_DIR="${WORK_DIR}/ledger-db" +INGEST_DOWNLOAD_LOCATION="$WORK_DIR/ingest-enclave.css" +mkdir -p ${WORK_DIR} + + +if ! test -f "$INGEST_DOWNLOAD_LOCATION" && [ "$INGEST_SIGSTRUCT_URI" != "" ]; then + (cd ${WORK_DIR} && curl -O https://enclave-distribution.${NAMESPACE}.mobilecoin.com/${INGEST_SIGSTRUCT_URI}) +fi + +if [ -z "$INGEST_ENCLAVE_CSS" ]; then + export INGEST_ENCLAVE_CSS=$INGEST_DOWNLOAD_LOCATION +fi + +if ! test -f "$INGEST_ENCLAVE_CSS"; then + echo "Missing ingest enclave at $INGEST_ENCLAVE_CSS" + exit 1 +fi + +# Pass "--no-build" if the user just wants to run what they have in +# WORK_DIR instead of building and copying over a new exectuable +if [ "$2" != "--no-build" ]; then + echo "Building" + SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + $SCRIPT_DIR/build-fs.sh $NET + cp $SCRIPT_DIR/../target/release/full-service $WORK_DIR +fi + +mkdir -p ${WALLET_DB_DIR} +$WORK_DIR/full-service \ + --wallet-db ${WALLET_DB_DIR}/wallet.db \ + --ledger-db ${LEDGER_DB_DIR} \ + --validator "validator://localhost:5554/?ca-bundle=$WORK_DIR/server.crt&tls-hostname=localhost" \ + --fog-ingest-enclave-css $INGEST_ENCLAVE_CSS \ + --chain-id $NET diff --git a/tools/mirror/run-mirror-private.sh b/tools/mirror/run-mirror-private.sh new file mode 100755 index 000000000..5f647da4c --- /dev/null +++ b/tools/mirror/run-mirror-private.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright (c) 2022 The MobileCoin Foundation + +NET="$1" +WORK_DIR="$HOME/.mobilecoin/${NET}" + +$WORK_DIR/wallet-service-mirror-private --mirror-public-uri "wallet-service-mirror://localhost/?ca-bundle=$WORK_DIR/server.crt&tls-hostname=localhost" --wallet-service-uri http://localhost:9090/wallet --mirror-key $WORK_DIR/mirror-private.pem \ No newline at end of file diff --git a/tools/mirror/run-mirror-public.sh b/tools/mirror/run-mirror-public.sh new file mode 100755 index 000000000..9bb7a33ee --- /dev/null +++ b/tools/mirror/run-mirror-public.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright (c) 2022 The MobileCoin Foundation + +NET="$1" +WORK_DIR="$HOME/.mobilecoin/${NET}" + +$WORK_DIR/wallet-service-mirror-public --client-listen-uri http://0.0.0.0:9091/ --mirror-listen-uri "wallet-service-mirror://0.0.0.0/?tls-chain=$WORK_DIR/server.crt&tls-key=$WORK_DIR/server.key" --allow-self-signed-tls \ No newline at end of file diff --git a/tools/mirror/run-validator.sh b/tools/mirror/run-validator.sh new file mode 100755 index 000000000..29c417bb2 --- /dev/null +++ b/tools/mirror/run-validator.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Copyright (c) 2022 The MobileCoin Foundation + +NET="$1" + +if [ "$NET" == "main" ]; then + NAMESPACE="prod" + PEER_DOMAIN="prod.mobilecoinww.com/" + TX_SOURCE_URL="https://ledger.mobilecoinww.com" + INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) +elif [ "$NET" == "test" ]; then + NAMESPACE=$NET + PEER_DOMAIN="test.mobilecoin.com/" + TX_SOURCE_URL="https://s3-us-west-1.amazonaws.com/mobilecoin.chain" + INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) +elif [ "$NET" == "alpha" ]; then + NAMESPACE=$NET + PEER_DOMAIN="alpha.development.mobilecoin.com/" + TX_SOURCE_URL="https://s3-eu-central-1.amazonaws.com/mobilecoin.eu.development.chain" + INGEST_SIGSTRUCT_URI="" +else + # TODO: add support for local network + echo "Unknown network" + echo "Usage: run-fs.sh {main|test|alpha} [--no-build]" + exit 1 +fi + +WORK_DIR="$HOME/.mobilecoin/${NET}" +LEDGER_DB_DIR="${WORK_DIR}/validator-ledger-db" +INGEST_DOWNLOAD_LOCATION="$WORK_DIR/ingest-enclave.css" +mkdir -p ${WORK_DIR} + + +if ! test -f "$INGEST_DOWNLOAD_LOCATION" && [ "$INGEST_SIGSTRUCT_URI" != "" ]; then + (cd ${WORK_DIR} && curl -O https://enclave-distribution.${NAMESPACE}.mobilecoin.com/${INGEST_SIGSTRUCT_URI}) +fi + +if [ -z "$INGEST_ENCLAVE_CSS" ]; then + export INGEST_ENCLAVE_CSS=$INGEST_DOWNLOAD_LOCATION +fi + +if ! test -f "$INGEST_ENCLAVE_CSS"; then + echo "Missing ingest enclave at $INGEST_ENCLAVE_CSS" + exit 1 +fi + +# Pass "--no-build" if the user just wants to run what they have in +# WORK_DIR instead of building and copying over a new exectuable +if [ "$2" != "--no-build" ]; then + echo "Building" + SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + $SCRIPT_DIR/build-fs.sh $NET + cp $SCRIPT_DIR/../target/release/validator-service $WORK_DIR +fi + +mkdir -p ${WALLET_DB_DIR} +$WORK_DIR/validator-service \ + --ledger-db ${LEDGER_DB_DIR} \ + --peer mc://node1.${PEER_DOMAIN} \ + --peer mc://node2.${PEER_DOMAIN} \ + --tx-source-url ${TX_SOURCE_URL}/node1.${PEER_DOMAIN} \ + --tx-source-url ${TX_SOURCE_URL}/node2.${PEER_DOMAIN} \ + --listen-uri "validator://localhost:5554/?tls-chain=$WORK_DIR/server.crt&tls-key=$WORK_DIR/server.key" \ + --chain-id $NET + \ No newline at end of file