From bceca6256b2ad9a6ccc1b88c109687365677f0c9 Mon Sep 17 00:00:00 2001 From: sugargoat Date: Sat, 18 Apr 2020 18:10:11 -0700 Subject: [PATCH] TestNet 04-18-20 (#33) * Roll up 4-14-20 (#1) * only generate enclave pem when needed, use absolute path (#1) * switch local network to not use ssl (#6) * This commit moves mobilecoin to the mobilecoinofficial fork of prost (#8) * This commit moves mobilecoin to the mobilecoinofficial fork of prost Removing cbeck88 permissions controls I have ensured that there are branch protection rules covering the commits * Fix view enclave cargo toml to have the same revision as others * Fix cargo.lock files * Remove selfsigned from README (#9) * Adds Java namespaces to protocol buffers (#12) * fix ecies MC-1216 (#11) * fix ecies MC-1216 was https://github.com/mobilecoinofficial/mobilecoin-internal/pull/321 changes since then: - Removed alloc feature - Added *_in_place_detached api (like aead crate) This reduces the amount of noise in the actual crypto part, the noise being "which bytes go where in the buffer" - Marked the `encrypt_into` and `decrypt_into` apis as not public, because those APIs suck, it should really be as much like aead crate as possible, which is a better thought-out API * add comments about fixing part of API * those APIs have to be public for now, sigh. maybe they aren't so bad * Add comments about API * [MC-1172] rm tranasction::encoders * Reorganize SCP to Cargo standards (#18) * Make /opt/intel/sgxsdk/lib64 part of LD_LIBRARY_PATH in dockerfile (#21) * Make /opt/intel/sgxsdk/lib64 part of LD_LIBRARY_PATH in dockerfile and uprev the dockerfile. This intended to fix ci in PR 14 This fixes issues like `...epid_sim.so` not being found by the test targets. It is not getting installed in `/opt/intel/sgxsdk/sdk_libs`, it is getting installed in the path mentioned ``` Running target/debug/deps/tx_recovery-3449d1ea71010602 /tmp/mobilenode/target/debug/deps/tx_recovery-3449d1ea71010602: error while loading shared libraries: libsgx_epid_sim.so: cannot open shared object file: No such file or directory error: test failed, to rerun pass '-p fog_ingest_server --test tx_recovery' root@cb7f949bccb2:/tmp/mobilenode# ldd /tmp/mobilenode/target/debug/deps/tx_recovery-3449d1ea71010602 linux-vdso.so.1 (0x00007ffc62df8000) libsgx_epid_sim.so => not found libsgx_urts_sim.so => /opt/intel/sgxsdk/sdk_libs/libsgx_urts_sim.so (0x00007fc5c8061000) libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fc5c5fd9000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fc5c5dd5000) librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007fc5c5bcd000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc5c59ae000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fc5c5796000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc5c53a5000) /lib64/ld-linux-x86-64.so.2 (0x00007fc5c7e6c000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fc5c5007000) libcrypto.so.1.1 => /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007fc5c4b3c000) libsgx_uae_service_sim.so => /opt/intel/sgxsdk/sdk_libs/libsgx_uae_service_sim.so (0x00007fc5c8059000) root@cb7f949bccb2:/tmp/mobilenode# ls /opt/intel/sgxsdk/sdk_libs/libsgx_u libsgx_uae_service_sim.so libsgx_urts_sim.so root@cb7f949bccb2:/tmp/mobilenode# ls /opt/intel/sgxsdk/sdk_libs/ libsgx_uae_service_sim.so libsgx_urts_sim.so root@cb7f949bccb2:/tmp/mobilenode# ``` * Make circle ci not source the intel sgx environment As much as possible, the setting up of the build enviornment should be done in the Dockerfile. Not duplicating these lines throughout jenkins, k8s, mob tool, and README is a good thing. (they can be in readme for people who don't want to use the container.) * mobilecoind: b58 endpoints (#22) * mobilecoind b58 rpc endpoints * mobilecoind2: implement generate transfer code tx * test_generate_transfer_code_tx * Basic framework for Java mobilecoind client based on Gradle (#23) * Fixes class name in build (#24) * Replaces "Mobilenode" with "MobileCoin" in several READMEs (#4) * Replaces Mobilenode with MobileCoin in several READMEs * Adds src/README.md * avoid "consensus node" language * Update cloudbuild/README.md Co-Authored-By: Robb Walters Co-authored-by: Robb Walters * Move ledger_enclave_server.proto from mobilecoin_api to fog_api (#25) * Move ledger_enclave_server.proto from mobilecoin_api to fog_api * fix build * Actually fix build * Check outbuf_used for null in mobileenclave_call (#17) Merging this. * Tx uses SignatureRctBulletproofs (#2) * Applies patch from mobilecoin-internal * unit tests encodings * Removes unwrap in validate_transaction_signature * Re-enables test_validate_key_images_are_unique_rejects_duplicate * Removes unwraps in TransactionBuilder::build * fmt * Increases proptest cases, reorders imports * Adds CL params to Java code along with monitors and balance check (#33) * Adds CL params to Java code along with monitors and balance check * Fix help message * Build comments * Prettify * Unused import * Added a README, changed the language and flags around entropy * parameter fix * README format * Update proto to reflect current mobilecoind API * First round of suggested fixes * Adds ssl flag * Fix the 'no-vars-given' case. (#38) * Upgrade sentry to 0.18 (#36) * upgrade sentry to 0.18 * lock files * Implement mob client python session (#30) * Implement mob client python session * Reference sign up rather than create account * Start testnet client script * Rename and introduce exchange * start-testnet-mobilecoind * Add public_address * Transfer to public address * Versioned ecies (#28) * Do another pass on ecies API, `encrypt_into` -> `encrypt_in_place` This also allows `encrypt` to return an error, previously I didn't allow that, but I looked in tarcieri's actual aes-gcm crate, and it does return an error if the plaintext is larger than a huge number. I think it might be more sensible to panic there, but anyways, I'd like to make the `ecies` API close to `aead` and make it generic. In order to make that change, I needed to fix the places that were doing `encrypt_into` and `decrypt_into`, which were in the fog hints in transaction crate. So this is a good case study of how the API wokrs out. I also added a special wrapper over `&mut [u8]` called `FixedBuffer` to try to make using it nicer and close to how it worked without the `encrypt_into` functions. If we like `FixedBuffer` then I think we should try to open a PR to `aead` crate and see if Tony wants it. LMK what you think -- if we're happy with this, then in the next PR I'm going to turn this into a trait, then make a version of it that includes two "version tag" bytes so that we can have a nice forwards-and-backwards-compatible wire format for the ECIES ciphertexts. Once they get into the blockchain and into the recovery db we cannot easliy change the algorithm if we don't have that. * fix bug * fix tests * fix clippy * Create versioned Ecies wire format, integrated with Ecies trait * Add and use encrypt_fixed_length APIs for ECIES This is much cleaner than the FixedBuffer thing * Add better docu, references, naming, per code review comments trait ECIES -> RistrettoEcies * fixup previous * Rename `ecies` to `ristretto-box`, and better README / docu * Rename again per @jcape * Move crate `public/crypto/mc-crypto-box` to `public/crypto/box` per discussion * Additional functions in Java client for request codes and transfers (#42) * Additional functions in Java client for request codes and transfers * Document transfer function * Change target to recipient * 'host' = 'server' * Client subaddress (#40) * Use account/subaddress syntax * Flesh out new account * introduce mc-grpc-build and use it mobilecoind-api (#41) * introduce mc-grpc-build and use it mobilecoind-api * delete old autogenerated code * build issue fix and comments * grpc-build -> build-grpc * comment and lock file * readme and lock file * use mcbuild-utils Co-authored-by: Eran Rundstein Co-authored-by: Chris Beck Co-authored-by: tsegaran Co-authored-by: Robb Walters Co-authored-by: m a t t f a u l k n e r Co-authored-by: Brian Anderson Co-authored-by: James Cape <35878+jcape@users.noreply.github.com> * Roll up 4-15-20 (#2) * introduce mc-grpc-build and use it mobilecoind-api (#41) * introduce mc-grpc-build and use it mobilecoind-api * delete old autogenerated code * build issue fix and comments * grpc-build -> build-grpc * comment and lock file * readme and lock file * use mcbuild-utils * Propose Values in Slot (#16) * Adds a status call to the Java client (#44) * Adds transaction receipt and status checks to Java client * Document parameters for status function * Balance fix (#46) * Client fixes * Better error display * Pep8 * Small fixups in mc-crypto-box README, per joey feedback (#45) I tried to make the commentary about the user-provided nonce more accurate as well * Rewrite the README for digestible crate (#47) * Rewrite the README for digestible crate * Add another sentence * Add another sentence about framing, after reading again * Fix typo * Update public/crypto/digestible/README.md Co-Authored-By: sugargoat * Update public/crypto/digestible/README.md Co-Authored-By: sugargoat * Update public/crypto/digestible/README.md Co-Authored-By: sugargoat * Try to fix the sentences sarah commented on, fixup conclusion Co-authored-by: sugargoat * Updates README for java client and a couple of minor fixes (#54) * Fix missing check (#51) * Fix missing check * Speed up grpc install * Better wording * Update path for test network (#55) * Update READMEs with sigstruct (#7) * Update READMEs with sigstruct * Add css info * Add signed enclave info * Remove aws * Fix typo * Remove privkey option and provide signed and css to consensus * Add IAS_MODE to mobilecoind Co-authored-by: Eran Rundstein Co-authored-by: Robb Walters Co-authored-by: tsegaran Co-authored-by: garbageslam * Switch to build-grpc (#7) * switch mobilecoin-api to using the new build-grpc crate * switch attest-api to using the new build-grpc crate * switch grpc-util to using the new build-grpc crate * CircleCI build improvements (#8) * add circleci task for running "cargo build", dedupe pem file generation * fix spacing * call check-dirty-git * lint and save caches in the faster job * rename job * fix typo in check-dirty-git * fix typo * Changes from internal repo (#5) * Removes MAX_TINY_MOB (#9) * Fix crypto directory README (#3) * MC-1283: Export protos directories as cargo depvars (#10) * moves tombstone_block from Tx to TxPrefix (#13) * Nicer mob behavior (check for docker being installed) (#12) This might help the user experience in issues like https://github.com/mobilecoinofficial/mobilecoin/issues/6 * Improvements to build instructions in README.md (#16) * Improvements to build instructions in README.md * Fixup SGX_MODE=SIM vs. SGX_MODE=SW, and give explanation about env vars * small tweak * Expand upon the enclave build part * Spelling and grammar * Simplify python example (#14) * Simplify python example * Update README * Reference java example on top level readme * Fixup markdown rendering in various readme's (#19) This is what I get for not rendering them locally before making PR * Update PROD endpoint (#21) * Unit tests that TransactionBuilder returns error if value is not conserved (#22) * Amount and signatures use CompressedRistretto (#17) * Amount and signatures use CompressedRstretto * Comment cleanup * Unit test for Commitment * Unit test for CompressedCommitment * Comment fixes * Extracts ring decompression into a function * Comment fix * RingMLSAG derives Message (#23) * MC-1292 Replace MIN_RING_SIZE and MAX_RING_SIZE with RING_SIZE (#20) * Uses single RING_Size constant * Changes MIN_RING_SIZE to RING_SIZE * Restores test_validate_ring_sizes * Update consensus/api/proto/consensus_common.proto Co-Authored-By: James Cape <35878+jcape@users.noreply.github.com> * Restores error variant Co-authored-by: James Cape <35878+jcape@users.noreply.github.com> * Improve readme (#27) * Improve readme Thanks to @joekottke for suggestion to use markdown footnotes in the earlier PR comments * Remove footnote extension, it's not supported in githug-flavored-markdown * SignatureRctBulletproofs derives Message (#25) * SignatureRctBulletproofs derives Message * Removes dead code * Update command with missing params (#26) * Update command * peer-responder-id does not have public key * Typo fix * Example testnet client (#29) * wip * wip * mob and such * begin rpc integration * iterate on the flow * flow works * command line args, comments * check that mobilecoind is running * clippy Co-authored-by: Eran Rundstein Co-authored-by: Chris Beck Co-authored-by: tsegaran Co-authored-by: Robb Walters Co-authored-by: m a t t f a u l k n e r Co-authored-by: Brian Anderson Co-authored-by: James Cape <35878+jcape@users.noreply.github.com> --- .circleci/config.yml | 67 +- .gitignore | 20 - BUILD.md | 105 ++ Cargo.lock | 910 +++++++++++---- Cargo.toml | 14 +- README.md | 12 +- attest/api/.gitignore | 3 - attest/api/Cargo.toml | 8 +- attest/api/build.rs | 28 +- attest/api/src/lib.rs | 18 +- attest/net/src/ias.rs | 7 +- attest/untrusted/README.md | 4 +- common/Cargo.toml | 2 +- consensus/api/.gitignore | 11 - consensus/api/Cargo.toml | 8 +- consensus/api/build.rs | 52 +- consensus/api/proto/blockchain.proto | 3 +- consensus/api/proto/consensus_common.proto | 2 +- consensus/api/proto/external.proto | 83 +- .../api/proto/ledger_enclave_server.proto | 71 -- consensus/api/proto/transaction.proto | 52 - consensus/api/src/conversions.rs | 730 +++++------- consensus/api/src/lib.rs | 27 +- consensus/enclave/api/README.md | 4 +- consensus/enclave/api/src/lib.rs | 4 +- consensus/enclave/impl/README.md | 2 +- consensus/enclave/impl/src/lib.rs | 36 +- consensus/enclave/mock/src/lib.rs | 2 +- consensus/enclave/trusted/Cargo.lock | 59 +- consensus/enclave/trusted/src/lib.rs | 2 +- consensus/scp/play/README.md | 8 +- consensus/scp/src/lib.rs | 8 +- consensus/scp/src/node.rs | 105 +- consensus/scp/src/slot.rs | 306 +++-- .../mod.rs => tests/slow_timebase_tests.rs} | 29 +- consensus/service/BUILD.md | 48 +- consensus/service/README.md | 29 +- consensus/service/src/validators.rs | 6 +- crypto/README.md | 2 +- crypto/{ecies => box}/Cargo.toml | 10 +- crypto/{ecies => box}/LICENSE | 0 crypto/box/README.md | 140 +++ crypto/box/src/fixed_buffer.rs | 89 ++ crypto/box/src/hkdf_blake2b_aes_128_gcm.rs | 144 +++ crypto/box/src/lib.rs | 107 ++ crypto/box/src/traits.rs | 179 +++ crypto/box/src/versioned.rs | 161 +++ crypto/digestible/README.md | 240 ++-- crypto/ecies/README.md | 32 - crypto/ecies/src/lib.rs | 289 ----- docker/Dockerfile | 2 +- docker/Dockerfile-version | 2 +- ledger/db/src/lib.rs | 4 +- ledger/distribution/src/main.rs | 12 +- .../sync/src/reqwest_transactions_fetcher.rs | 8 +- ledger/sync/src/test_app/main.rs | 8 +- mcbuild/enclave/Cargo.toml | 6 +- mcbuild/enclave/src/lib.rs | 169 ++- mob | 21 + mobilecoind/Cargo.toml | 1 + mobilecoind/README.md | 38 +- mobilecoind/api/Cargo.toml | 2 +- mobilecoind/api/build.rs | 25 +- mobilecoind/api/proto/mobilecoind_api.proto | 17 +- mobilecoind/api/src/.gitignore | 2 - mobilecoind/api/src/lib.rs | 20 +- .../clients/java/mob_client/.gitattributes | 6 + .../clients/java/mob_client/.gitignore | 6 + mobilecoind/clients/java/mob_client/README.md | 52 + .../clients/java/mob_client/build.gradle | 55 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58694 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + mobilecoind/clients/java/mob_client/gradlew | 183 +++ .../clients/java/mob_client/gradlew.bat | 103 ++ .../clients/java/mob_client/settings.gradle | 10 + .../java/com/mobilecoin/mob_client/App.java | 328 ++++++ .../mob_client/src/main/proto/external.proto | 159 +++ .../src/main/proto/mobilecoind_api.proto | 490 ++++++++ .../com/mobilecoin/mob_client/AppTest.java | 21 + mobilecoind/clients/python/README.md | 92 +- mobilecoind/clients/python/Wallet.ipynb | 185 +++ mobilecoind/clients/python/accounts.json | 12 - mobilecoind/clients/python/main.py | 622 +--------- mobilecoind/clients/python/mob_client.py | 203 ++-- .../python/start-testnet-mobilecoind.sh | 31 + mobilecoind/src/config.rs | 2 +- mobilecoind/src/payments.rs | 4 +- mobilecoind/src/service.rs | 464 +++++++- testnet-client/Cargo.toml | 17 + testnet-client/src/main.rs | 674 +++++++++++ tools/local-network/local-network.py | 14 +- transaction/core/Cargo.toml | 5 +- transaction/core/src/amount.rs | 71 +- transaction/core/src/commitment.rs | 93 ++ transaction/core/src/compressed_commitment.rs | 106 ++ transaction/core/src/constants.rs | 25 +- transaction/core/src/encoders.rs | 244 ---- transaction/core/src/encrypted_fog_hint.rs | 66 +- transaction/core/src/fog_hint.rs | 87 +- transaction/core/src/lib.rs | 5 +- .../core/src/ring_signature/curve_scalar.rs | 65 +- transaction/core/src/ring_signature/mlsag.rs | 241 ++-- transaction/core/src/ring_signature/mod.rs | 50 +- .../src/ring_signature/rct_bulletproofs.rs | 173 ++- .../core/src/ring_signature/rct_type_full.rs | 1033 ----------------- transaction/core/src/tx.rs | 106 +- transaction/core/src/validation/mod.rs | 2 +- transaction/core/src/validation/validate.rs | 292 ++--- transaction/core/test-utils/src/lib.rs | 9 +- transaction/std/src/transaction_builder.rs | 299 ++--- util/build-grpc/Cargo.toml | 10 + util/build-grpc/README.md | 4 + util/build-grpc/src/lib.rs | 52 + util/generate-sample-ledger/src/lib.rs | 12 +- util/grpc/Cargo.toml | 6 +- util/grpc/build.rs | 17 +- util/grpc/src/.gitignore | 2 - util/grpc/src/lib.rs | 15 +- 118 files changed, 6744 insertions(+), 4699 deletions(-) create mode 100644 BUILD.md delete mode 100644 attest/api/.gitignore delete mode 100644 consensus/api/.gitignore delete mode 100644 consensus/api/proto/ledger_enclave_server.proto delete mode 100644 consensus/api/proto/transaction.proto rename consensus/scp/{src/tests/mod.rs => tests/slow_timebase_tests.rs} (99%) rename crypto/{ecies => box}/Cargo.toml (50%) rename crypto/{ecies => box}/LICENSE (100%) create mode 100644 crypto/box/README.md create mode 100644 crypto/box/src/fixed_buffer.rs create mode 100644 crypto/box/src/hkdf_blake2b_aes_128_gcm.rs create mode 100644 crypto/box/src/lib.rs create mode 100644 crypto/box/src/traits.rs create mode 100644 crypto/box/src/versioned.rs delete mode 100644 crypto/ecies/README.md delete mode 100644 crypto/ecies/src/lib.rs delete mode 100644 mobilecoind/api/src/.gitignore create mode 100644 mobilecoind/clients/java/mob_client/.gitattributes create mode 100644 mobilecoind/clients/java/mob_client/.gitignore create mode 100644 mobilecoind/clients/java/mob_client/README.md create mode 100644 mobilecoind/clients/java/mob_client/build.gradle create mode 100644 mobilecoind/clients/java/mob_client/gradle/wrapper/gradle-wrapper.jar create mode 100644 mobilecoind/clients/java/mob_client/gradle/wrapper/gradle-wrapper.properties create mode 100755 mobilecoind/clients/java/mob_client/gradlew create mode 100644 mobilecoind/clients/java/mob_client/gradlew.bat create mode 100644 mobilecoind/clients/java/mob_client/settings.gradle create mode 100644 mobilecoind/clients/java/mob_client/src/main/java/com/mobilecoin/mob_client/App.java create mode 100644 mobilecoind/clients/java/mob_client/src/main/proto/external.proto create mode 100644 mobilecoind/clients/java/mob_client/src/main/proto/mobilecoind_api.proto create mode 100644 mobilecoind/clients/java/mob_client/src/test/java/com/mobilecoin/mob_client/AppTest.java create mode 100644 mobilecoind/clients/python/Wallet.ipynb delete mode 100644 mobilecoind/clients/python/accounts.json create mode 100755 mobilecoind/clients/python/start-testnet-mobilecoind.sh create mode 100644 testnet-client/Cargo.toml create mode 100644 testnet-client/src/main.rs create mode 100644 transaction/core/src/commitment.rs create mode 100644 transaction/core/src/compressed_commitment.rs delete mode 100644 transaction/core/src/encoders.rs delete mode 100644 transaction/core/src/ring_signature/rct_type_full.rs create mode 100644 util/build-grpc/Cargo.toml create mode 100644 util/build-grpc/README.md create mode 100644 util/build-grpc/src/lib.rs delete mode 100644 util/grpc/src/.gitignore diff --git a/.circleci/config.yml b/.circleci/config.yml index cdf007f268..2169501076 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 defaults: - builder-install: &builder-install gcr.io/mobilenode-211420/builder-install:1_8 + builder-install: &builder-install gcr.io/mobilenode-211420/builder-install:1_9 executors: build-executor: @@ -103,11 +103,26 @@ commands: paths: - "~/.cargo" - build_setup: + generate-pem-file: + steps: + - run: + name: Generating Enclave_private.pem + command: | + openssl genrsa -out $(pwd)/Enclave_private.pem -3 3072 + export CONSENSUS_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem + export INGEST_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem + export LEDGER_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem + export VIEW_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem + + prepare-for-build: steps: - checkout - print_versions - env_setup + - enable_sccache + - restore-cargo-cache + - restore-sccache-cache + - generate-pem-file check-dirty-git: steps: @@ -121,6 +136,7 @@ commands: echo "repo is dirty" git status exit 1 + fi jobs: # A job that builds all the tests in the workspace, and stores them in a test-bins/ directory. @@ -129,11 +145,7 @@ jobs: build-parallel-tests: executor: build-executor steps: - - build_setup - - enable_sccache - - restore-cargo-cache - - restore-sccache-cache - + - prepare-for-build - run: name: Build/prepare unit tests command: | @@ -202,7 +214,6 @@ jobs: # Crates that define macros (e.g. `digestible_derive`) link dynamically against libtest*.so, which sits here. export LD_LIBRARY_PATH="$HOME/.rustup/toolchains/$(rustup show active-toolchain | awk '{print $1}')/lib" - source /opt/intel/sgxsdk/environment echo "LD_LIBRARY_PATH = $LD_LIBRARY_PATH" # Run the test binaries for all the packages we're assigned by CircleCI's test splitting mechanism, @@ -235,44 +246,44 @@ jobs: executor: build-executor parallelism: 1 steps: - - build_setup - - enable_sccache - - restore-cargo-cache - - restore-sccache-cache + - prepare-for-build - run: name: Run all unit tests command: | - openssl genrsa -out $(pwd)/Enclave_private.pem -3 3072 - export CONSENSUS_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem - export INGEST_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem - export LEDGER_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem - export VIEW_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem - cargo test + - check-dirty-git - rm $(pwd)/Enclave_private.pem + # Build and lint in debug mode + build-all-and-lint-debug: + executor: build-executor + steps: + - prepare-for-build - run: - name: Lint/fmt + name: Cargo build command: | - openssl genrsa -out $(pwd)/Enclave_private.pem -3 3072 - export CONSENSUS_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem - export INGEST_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem - export LEDGER_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem - export VIEW_ENCLAVE_PRIVKEY=$(pwd)/Enclave_private.pem + cargo build + - check-dirty-git + # The lint and saving of caches happens here since this job is faster than the run-all-tests job. + # This results in shorter CI times. + - run: + name: Lint/fmt + command: | ./tools/lint.sh - - rm $(pwd)/Enclave_private.pem - save-cargo-cache - save-sccache-cache workflows: version: 2 # Build and run tests on a single container - run-all-tests: + build-and-run-all-tests: jobs: + # Run tests on a single container - run-all-tests + # Build everything in debug + - build-all-and-lint-debug + # Build and run tests in parallel - not needed at the moment since the test suite is fast enough. # build-and-run-tests: # jobs: diff --git a/.gitignore b/.gitignore index b818c06e5f..254dd933e2 100644 --- a/.gitignore +++ b/.gitignore @@ -33,26 +33,6 @@ logs *.so *.a -# grpc -attest_api/src/attest.rs -attest_api/src/attest_grpc.rs -grpc_util/src/health_api.rs -grpc_util/src/health_api_grpc.rs -consensus/api/src/blockchain.rs -consensus/api/src/blockchain_grpc.rs -consensus/api/src/consensus_client.rs -consensus/api/src/consensus_client_grpc.rs -consensus/api/src/consensus_common.rs -consensus/api/src/consensus_peer.rs -consensus/api/src/consensus_peer_grpc.rs -consensus/api/src/external.rs -consensus/api/src/ledger_enclave_server.rs -consensus/api/src/ledger_enclave_server_grpc.rs -consensus/api/src/ledger_server.rs -consensus/api/src/ledger_server_grpc.rs -mobilecoind/api/src/mobilecoind_api.rs -mobilecoind/api/src/mobilecoind_api_grpc.rs - # Random junk .DS_Store .factorypath diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000000..4258905ee5 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,105 @@ +Build +===== + +## Build environment + +Services that create SGX enclaves depend on the Intel SGX SDK. This must be installed +in the build environment, as well as the runtime environment. + +#### Dockerized build + +An easy way to get this environment is to build in the docker image that we use for CI. +The dockerfile for this image lives in `docker/Dockerfile`. + +You can use `./mob prompt` to pull this image, (or to build it locally), and get a prompt +in this environment. + +``` +# From the root of the repo +./mob prompt + +# At the container prompt +cargo build +``` + +If you have SGX-enabled hardware (activated in BIOS, and with SGX kernel module installed), +you can use `./mob prompt --hw` to get SGX in the container. Then you can both build and +run the tests in `SGX_MODE=HW`. (See below for an explanation.) + +#### No-docker build + +A docker-less build also works fine for development: +- Follow instructions [consensus/service/BUILD.md](consensus/service/BUILD.md) +- Set up your environment like the [Dockerfile](docker/Dockerfile) + +## Build configuration + +There are two project-wide SGX-related configuration variables `SGX_MODE` and `IAS_MODE`. + +These are set by environment variables, and they must be the same for all artifacts, +even those that don't depend directly on SGX. E.g. `mobilecoind` must have the same configuration +as `consensus_service` for Intel Remote Attestation to work, otherwise an error will occur at runtime. + +For testing, you should usually use `SGX_MODE=SW` and `IAS_MODE=DEV`. + +#### SGX_MODE + +`SGX_MODE=SW` means that the enclaves won't be "real" enclaves -- consensus service will link +to Intel-provided "_sim" versions of the Intel SGX SDK, and the enclave will be loaded approximately +like a shared library being `dlopen`'ed. This means that you will be able to use `gdb` and get +backtraces normally through the enclave code. In this mode, the CPU does not securely compute +measurements of the enclave, and attestation doesn't prove the integrity of the enclave. + +`SGX_MODE=HW` means that the real Intel libraries are used, and the enclave is loaded securely. +This mode is required for Intel Remote Attestation to work and provide security. + +The clients and servers must all agree about this setting, or attestation will fail. + +#### IAS_MODE + +`IAS_MODE=DEV` means that we will hit the Intel provided "dev endpoints" during remote attestation. +These won't require the real production signing key in connection to the MRENCLAVE measurements. + +`IAS_MODE=PROD` means that we will hit the real Intel provided endpoints for remote attestation. + +In code, this discrepancy is largely handled by the `attest-net` crate. + +The clients and servers must all agree about this setting, or attestation will fail. + +#### Why are these environment variables? + +`cargo` supports crate-level features, and feature unification across the build plan. +`cargo` does not support any notion of "global project-wide configuration". + +In practice, it's too hard invoke cargo to get all the features enabled exactly correctly on +all the right crates, if every crate has an `sgx_mode` and `ias_mode` feature. + +Even if cargo had workspace-level features, which it doesn't, that wouldn't be good enough for us +because our build requires using multiple workspaces. We must keep the cargo features on some +targets separated and not unified. +Unifying cargo features across enclave targets and server targets will break the enclave builds. +This is because the enclave builds in a special `no_std` environment. + +Making `SGX_MODE` and `IAS_MODE` environment variables, and making `build.rs` scripts that read +them and set features on these crates as needed, is the simplest way to make sure that there is +one source of truth for these values for all of the artifacts in the whole build. + +The `SGX_MODE` environment variable configuration is also used throughout Intel SGX SDK examples. + +## Building the enclave + +For technical reasons, the `consensus_enclave` must be in a separate workspace. +It is also built using `cargo build`. + +The enclave build is invoked *automatically* if needed from the `consensus_service` `build.rs`. + +To reproducibly build the enclave, (get exactly the right MRENCLAVE value), you must build +in the container. + +For local testing, you don't need to get exactly the right MRENCLAVE value. You can set up +test networks with whatever MRENCLAVE your build produces, and clients that check this value +using the Remote Attestation process. + +If you want to download a prebuilt enclave, signed using the production signing key, in order use `IAS_MODE=PROD` +and participate in a production-environment network, check out the `enclave-signing-material` instructions: +https://github.com/mobilecoinofficial/mobilecoin/blob/master/consensus/service/BUILD.md#enclave-signing-material diff --git a/Cargo.lock b/Cargo.lock index dd74932b65..ae50d221b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,17 +23,6 @@ dependencies = [ "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "aes-ctr" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "aes-soft 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "aesni 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ctr 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "aes-gcm" version = "0.3.0" @@ -63,7 +52,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -151,7 +139,7 @@ dependencies = [ "mcrand 1.0.0", "mcserial 0.1.0", "pem 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -187,18 +175,20 @@ dependencies = [ [[package]] name = "attest-api" -version = "0.1.0" +version = "0.1.1" dependencies = [ "aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "attest-ake 0.1.0", "attest-enclave-api 0.1.0", + "cargo-emit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "grpcio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "keys 0.1.0", + "mc-build-grpc 1.0.0", + "mcbuild-utils 0.2.0", "mcnoise 0.1.0", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "protoc-grpcio 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -228,7 +218,7 @@ dependencies = [ "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "sgx_build 0.1.0", "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -240,7 +230,7 @@ dependencies = [ "attest 0.1.0", "common 0.1.0", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "sgx_compat 1.0.0", "sgx_types 1.0.1", ] @@ -276,10 +266,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "backtrace" -version = "0.3.42" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace-sys 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", @@ -287,7 +277,7 @@ dependencies = [ [[package]] name = "backtrace-sys" -version = "0.1.32" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", @@ -337,7 +327,7 @@ dependencies = [ "peeking_take_while 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hash 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "which 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -361,6 +351,14 @@ name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "blake2" version = "0.8.1" @@ -538,7 +536,7 @@ dependencies = [ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -549,7 +547,7 @@ dependencies = [ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -626,6 +624,17 @@ dependencies = [ "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "clicolors-control" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -646,7 +655,7 @@ dependencies = [ name = "common" version = "0.1.0" dependencies = [ - "backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", "binascii 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "build-info 0.1.0", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -665,7 +674,7 @@ dependencies = [ "proptest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "sentry 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sentry 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "sha3 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "siphasher 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -752,7 +761,7 @@ dependencies = [ "mcrand 1.0.0", "mcserial 0.1.0", "message-cipher 0.1.0", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -797,7 +806,7 @@ name = "consensus-service" version = "1.0.0" dependencies = [ "attest 0.1.0", - "attest-api 0.1.0", + "attest-api 0.1.1", "attest-enclave-api 0.1.0", "attest-net 0.1.0", "attest-untrusted 0.1.0", @@ -826,11 +835,11 @@ dependencies = [ "mcuri 0.1.0", "metered-channel 0.1.0", "metrics 0.1.0", - "mobilecoin-api 0.1.0", + "mobilecoin-api 0.1.1", "peers 1.0.0", "peers-tests 0.1.0", "prometheus 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -840,7 +849,7 @@ dependencies = [ "rouille 3.0.0 (git+https://github.com/tomaka/rouille/?rev=db66a3b47af4271939e1aba21d0f36ccba3d1b70)", "scp 0.1.0", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "sgx_build 0.1.0", "sgx_slog 1.0.0", "sgx_urts 1.0.0", @@ -852,6 +861,21 @@ dependencies = [ "transaction-test-utils 0.1.0", ] +[[package]] +name = "console" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "clicolors-control 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "encode_unicode 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "termios 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -877,12 +901,26 @@ dependencies = [ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "publicsuffix 1.5.4 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", "try_from 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "crc" version = "1.8.1" @@ -915,10 +953,10 @@ dependencies = [ "oorandom 11.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "plotters 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "tinytemplate 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1070,15 +1108,6 @@ dependencies = [ "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "ctr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "curve25519-dalek" version = "2.0.0" @@ -1095,13 +1124,13 @@ dependencies = [ [[package]] name = "debugid" -version = "0.4.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1114,6 +1143,16 @@ dependencies = [ "gzip-header 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "dialoguer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "console 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "difference" version = "2.0.0" @@ -1189,7 +1228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1197,18 +1236,6 @@ name = "dtoa" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "ecies" -version = "0.1.0" -dependencies = [ - "aes-ctr 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "keys 0.1.0", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "test_helper 0.1.0", -] - [[package]] name = "ed25519" version = "1.0.0-pre.4" @@ -1246,6 +1273,11 @@ dependencies = [ "sgx_types 1.0.1", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "encoding_rs" version = "0.8.22" @@ -1262,7 +1294,7 @@ dependencies = [ "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1271,7 +1303,7 @@ name = "error-chain" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1280,7 +1312,7 @@ name = "failure" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1356,6 +1388,19 @@ name = "futures" version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "futures-channel" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "futures-cpupool" version = "0.1.8" @@ -1365,6 +1410,48 @@ dependencies = [ "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "futures-io" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-sink" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-task" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-util" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-macro 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-nested 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "generate-sample-ledger" version = "0.1.0" @@ -1437,11 +1524,11 @@ dependencies = [ "grpcio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "hex_fmt 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mc-build-grpc 1.0.0", "mcserial 0.1.0", "metrics 0.1.0", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "protoc-grpcio 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1505,6 +1592,24 @@ dependencies = [ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "h2" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "half" version = "1.4.1" @@ -1521,7 +1626,7 @@ dependencies = [ "pest_derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1604,6 +1709,16 @@ dependencies = [ "winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "match_cfg 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "http" version = "0.1.21" @@ -1614,6 +1729,16 @@ dependencies = [ "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "http" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "http-body" version = "0.1.0" @@ -1625,6 +1750,15 @@ dependencies = [ "tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "httparse" version = "1.3.4" @@ -1672,6 +1806,29 @@ dependencies = [ "want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "hyper" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "h2 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "hyper-rustls" version = "0.17.1" @@ -1688,6 +1845,23 @@ dependencies = [ "webpki-roots 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "hyper-rustls" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls-native-certs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-rustls 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "idna" version = "0.1.5" @@ -1710,12 +1884,15 @@ dependencies = [ [[package]] name = "im" -version = "12.3.4" +version = "14.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "sized-chunks 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "bitmaps 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_xoshiro 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sized-chunks 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1726,6 +1903,17 @@ dependencies = [ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "indicatif" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "console 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "iovec" version = "0.1.4" @@ -1791,7 +1979,7 @@ dependencies = [ "mcserial 0.1.0", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "transaction 0.1.0", @@ -1813,12 +2001,12 @@ dependencies = [ "hex_fmt 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "mcserial 0.1.0", "pem 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "signature 1.0.0-pre.5 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1864,13 +2052,13 @@ dependencies = [ "dotenv 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "ledger-db 0.1.0", - "mobilecoin-api 0.1.0", + "mobilecoin-api 0.1.1", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "retry 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rusoto_core 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusoto_s3 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "transaction 0.1.0", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1893,7 +2081,7 @@ dependencies = [ "mcconnection-tests 0.1.0", "mcuri 0.1.0", "metrics 0.1.0", - "mobilecoin-api 0.1.0", + "mobilecoin-api 0.1.1", "peers 1.0.0", "peers-tests 0.1.0", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1922,11 +2110,6 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "libm" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "libz-sys" version = "1.0.25" @@ -1995,6 +2178,11 @@ name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "matches" version = "0.1.8" @@ -2047,6 +2235,28 @@ dependencies = [ "transaction-std 0.1.0", ] +[[package]] +name = "mc-build-grpc" +version = "1.0.0" +dependencies = [ + "mcbuild-utils 0.2.0", + "protoc-grpcio 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mc-crypto-box" +version = "0.1.0" +dependencies = [ + "aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "aes-gcm 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "blake2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "keys 0.1.0", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "test_helper 0.1.0", +] + [[package]] name = "mc-encodings" version = "0.1.0" @@ -2056,20 +2266,34 @@ dependencies = [ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "mc-testnet-client" +version = "0.1.0" +dependencies = [ + "dialoguer 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "grpcio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "indicatif 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mc-b58-payloads 0.1.0", + "mobilecoind-api 0.1.0", + "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rust_decimal 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "mcbuild-enclave" version = "0.1.0" dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "cargo-emit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "cargo_metadata 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mbedtls 0.5.1 (git+https://github.com/mobilecoinofficial/rust-mbedtls.git?tag=mc-0.2)", + "mbedtls-sys-auto 2.18.1 (git+https://github.com/mobilecoinofficial/rust-mbedtls.git?tag=mc-0.2)", "mcbuild-sgx-utils 0.2.0", "mcbuild-utils 0.2.0", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rsa 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "sgx_css 0.1.0", - "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2101,7 +2325,7 @@ dependencies = [ "aes-gcm 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "attest 0.1.0", "attest-ake 0.1.0", - "attest-api 0.1.0", + "attest-api 0.1.1", "common 0.1.0", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "grpcio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2111,7 +2335,7 @@ dependencies = [ "mcrand 1.0.0", "mcserial 0.1.0", "mcuri 0.1.0", - "mobilecoin-api 0.1.0", + "mobilecoin-api 0.1.1", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "retry 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2176,7 +2400,7 @@ dependencies = [ name = "mcserial" version = "0.1.0" dependencies = [ - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2285,7 +2509,7 @@ dependencies = [ "prometheus 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2300,7 +2524,7 @@ dependencies = [ "prometheus 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2405,19 +2629,21 @@ dependencies = [ [[package]] name = "mobilecoin-api" -version = "0.1.0" +version = "0.1.1" dependencies = [ - "attest-api 0.1.0", + "attest-api 0.1.1", + "cargo-emit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "common 0.1.0", "curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "grpcio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "keys 0.1.0", + "mc-build-grpc 1.0.0", + "mcbuild-utils 0.2.0", "mcrand 1.0.0", "mcserial 0.1.0", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "protoc-grpcio 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "transaction 0.1.0", @@ -2445,22 +2671,23 @@ dependencies = [ "ledger-sync 0.1.0", "lmdb 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "lru 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "mc-b58-payloads 0.1.0", "mcconnection 0.1.0", "mcconnection-tests 0.1.0", "mcrand 1.0.0", "mcserial 0.1.0", "mcuri 0.1.0", - "mobilecoin-api 0.1.0", + "mobilecoin-api 0.1.1", "mobilecoind-api 0.1.0", "more-asserts 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "retry 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "scp 0.1.0", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "sha3 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2478,9 +2705,9 @@ dependencies = [ "grpcio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "hex_fmt 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "keys 0.1.0", - "mobilecoin-api 0.1.0", + "mc-build-grpc 1.0.0", + "mobilecoin-api 0.1.1", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "protoc-grpcio 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "transaction 0.1.0", ] @@ -2526,24 +2753,6 @@ dependencies = [ "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "num-bigint-dig" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libm 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "num-integer" version = "0.1.42" @@ -2553,16 +2762,6 @@ dependencies = [ "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "num-iter" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "num-traits" version = "0.2.11" @@ -2580,6 +2779,11 @@ dependencies = [ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "number_prefix" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "oorandom" version = "11.1.0" @@ -2590,6 +2794,11 @@ name = "opaque-debug" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "output_vt100" version = "0.1.2" @@ -2639,7 +2848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "peers" version = "1.0.0" dependencies = [ - "attest-api 0.1.0", + "attest-api 0.1.1", "attest-enclave-api 0.1.0", "common 0.1.0", "consensus-enclave-api 0.1.0", @@ -2653,7 +2862,7 @@ dependencies = [ "mcconnection-tests 0.1.0", "mcserial 0.1.0", "mcuri 0.1.0", - "mobilecoin-api 0.1.0", + "mobilecoin-api 0.1.1", "peers-tests 0.1.0", "protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2695,7 +2904,7 @@ dependencies = [ "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2782,6 +2991,34 @@ dependencies = [ "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "pin-project" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "pin-project-internal 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pin-utils" +version = "0.1.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "pkg-config" version = "0.3.17" @@ -2847,6 +3084,16 @@ dependencies = [ "syn-mid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro-nested" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "proc-macro2" version = "0.4.30" @@ -2890,7 +3137,7 @@ dependencies = [ "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)", "rusty-fork 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2898,16 +3145,16 @@ dependencies = [ [[package]] name = "prost" version = "0.6.1" -source = "git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95#4e1905329369ca7a1cac3eda978ee9379167ee95" +source = "git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95#4e1905329369ca7a1cac3eda978ee9379167ee95" dependencies = [ "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "prost-derive 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost-derive 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", ] [[package]] name = "prost-derive" version = "0.6.1" -source = "git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95#4e1905329369ca7a1cac3eda978ee9379167ee95" +source = "git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95#4e1905329369ca7a1cac3eda978ee9379167ee95" dependencies = [ "anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2958,7 +3205,7 @@ dependencies = [ "error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3137,6 +3384,14 @@ dependencies = [ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand_xoshiro" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rayon" version = "1.3.0" @@ -3184,12 +3439,12 @@ dependencies = [ [[package]] name = "regex" -version = "1.3.3" +version = "1.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)", "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3203,7 +3458,7 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.13" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -3234,7 +3489,7 @@ dependencies = [ "mime_guess 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "rustls 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3249,6 +3504,42 @@ dependencies = [ "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "reqwest" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-rustls 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "mime_guess 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-rustls 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-futures 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki-roots 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "retry" version = "0.5.1" @@ -3259,7 +3550,7 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.9" +version = "0.16.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3292,7 +3583,7 @@ dependencies = [ "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3301,23 +3592,6 @@ dependencies = [ "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "rsa" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-bigint-dig 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "rusoto_core" version = "0.42.0" @@ -3336,7 +3610,7 @@ dependencies = [ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-timer 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3353,10 +3627,10 @@ dependencies = [ "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-process 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-timer 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3407,6 +3681,16 @@ dependencies = [ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rust_decimal" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustc-demangle" version = "0.1.16" @@ -3432,11 +3716,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)", "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rustls" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)", + "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustls-native-certs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "schannel 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustversion" version = "1.0.2" @@ -3476,6 +3783,15 @@ dependencies = [ "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "schannel" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -3501,7 +3817,7 @@ dependencies = [ "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "serial_test 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "serial_test_derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "test_helper 0.1.0", @@ -3516,7 +3832,7 @@ dependencies = [ "mcuri 0.1.0", "scp 0.1.0", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "transaction 0.1.0", ] @@ -3526,7 +3842,7 @@ name = "sct" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)", "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3538,6 +3854,26 @@ dependencies = [ "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "security-framework" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework-sys 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "security-framework-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "semver" version = "0.9.0" @@ -3554,38 +3890,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "sentry" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "hostname 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "httpdate 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "im 12.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "im 14.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "sentry-types 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sentry-types 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", "uname 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "sentry-types" -version = "0.11.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", - "debugid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "debugid 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "url_serde 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3617,7 +3952,7 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.45" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3636,6 +3971,17 @@ dependencies = [ "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serde_urlencoded" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "serial_test" version = "0.1.0" @@ -3718,7 +4064,7 @@ name = "sgx_slog" version = "1.0.0" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "slog-scope 4.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3836,9 +4182,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "sized-chunks" -version = "0.1.3" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "bitmaps 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3852,7 +4199,7 @@ dependencies = [ "error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "pulldown-cmark 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3892,7 +4239,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "slog-async 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "slog-scope 4.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3909,7 +4256,7 @@ dependencies = [ "flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "skeptic 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", "slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3921,7 +4268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3992,14 +4339,6 @@ name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "stream-cipher" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "string" version = "0.2.1" @@ -4140,6 +4479,14 @@ dependencies = [ "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "termios" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "test_helper" version = "0.1.0" @@ -4217,7 +4564,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -4243,6 +4590,23 @@ dependencies = [ "tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tokio" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tokio-buf" version = "0.1.1" @@ -4350,6 +4714,17 @@ dependencies = [ "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tokio-rustls" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tokio-signal" version = "0.2.7" @@ -4446,6 +4821,19 @@ dependencies = [ "tokio-reactor 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "toml" version = "0.5.6" @@ -4454,10 +4842,16 @@ dependencies = [ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tower-service" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "transaction" version = "0.1.0" dependencies = [ + "aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "blake2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "bs58 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "bulletproofs 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4469,7 +4863,6 @@ dependencies = [ "curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "digestible 0.1.0", - "ecies 0.1.0", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", "hex_fmt 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4477,11 +4870,12 @@ dependencies = [ "keys 0.1.0", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "ledger-db 0.1.0", + "mc-crypto-box 0.1.0", "mcrand 1.0.0", "mcserial 0.1.0", "merlin 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "proptest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4505,7 +4899,7 @@ dependencies = [ "hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "keys 0.1.0", "mcserial 0.1.0", - "prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", + "prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4649,23 +5043,23 @@ dependencies = [ "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "url_serde" -version = "0.2.0" +name = "uuid" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "uuid" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -4722,6 +5116,15 @@ dependencies = [ "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -4733,6 +5136,8 @@ version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen-macro 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -4750,6 +5155,17 @@ dependencies = [ "wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.58" @@ -4808,7 +5224,7 @@ name = "webpki" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)", "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -4820,6 +5236,14 @@ dependencies = [ "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "webpki-roots" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "weedle" version = "0.10.0" @@ -4946,7 +5370,6 @@ dependencies = [ "checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" "checksum aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4cf01b9b56e767bb57b94ebf91a58b338002963785cdd7013e21c0d4679471e4" "checksum aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "54eb1d8fe354e5fc611daf4f2ea97dd45a765f4f1e4512306ec183ae2e8f20c9" -"checksum aes-ctr 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d2e5b0458ea3beae0d1d8c0f3946564f8e10f90646cf78c06b4351052058d1ee" "checksum aes-gcm 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4001f31800fc9b8c774728f82c7348f4f91474afd38c12a0e7dfa8303ae2dbd6" "checksum aes-soft 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cfd7e7ae3f9a1fb5c03b389fc6bb9a51400d0c13053f0dca698c832bfd893a0d" "checksum aesni 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100" @@ -4960,8 +5383,8 @@ dependencies = [ "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" -"checksum backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b4b1549d804b6c73f4817df2ba073709e96e426f12987127c48e6745568c350b" -"checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" +"checksum backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)" = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e" +"checksum backtrace-sys 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "7de8aba10a69c8e8d7622c5710229485ec32e9d55fdad160ea559c086fdcd118" "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" "checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" "checksum bigint 4.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ebecac13b3c745150d7b6c3ea7572d372f09d627c2077e893bf26c5c7f70d282" @@ -4970,6 +5393,7 @@ dependencies = [ "checksum bit-set 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e84c238982c4b1e1ee668d136c510c67a13465279c0cb367ea6baf6310620a80" "checksum bit-vec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f59bbe95d4e52a6398ec21238d31577f2b28a9d86807f06ca59d191d8440d0bb" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +"checksum bitmaps 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" "checksum blake2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" "checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" "checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" @@ -5001,11 +5425,15 @@ dependencies = [ "checksum clang-sys 0.28.1 (registry+https://github.com/rust-lang/crates.io-index)" = "81de550971c976f176130da4b2978d3b524eaa0fd9ac31f3ceb5ae1231fb4853" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" "checksum clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "97276801e127ffb46b66ce23f35cc96bd454fa311294bced4bbace7baa8b1d17" +"checksum clicolors-control 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90082ee5dcdd64dc4e9e0d37fbf3ee325419e39c0092191e0393df65518f741e" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" "checksum cmake 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "81fb25b677f8bf1eb325017cb6bb8452f87969db0fedb4f757b297bee78a7c62" +"checksum console 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6728a28023f207181b193262711102bfbaf47cc9d13bc71d0736607ef8efe88c" "checksum constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" "checksum cookie 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" "checksum cookie_store 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46750b3f362965f197996c4448e4a0935e791bf7d6631bfce9ee0af3d24c919c" +"checksum core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +"checksum core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" "checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" "checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" "checksum criterion 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1fc755679c12bda8e5523a71e4d654b6bf2e14bd838dfc48cde6559a05caf7d1" @@ -5025,10 +5453,10 @@ dependencies = [ "checksum csv-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" "checksum ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" "checksum ctor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc" -"checksum ctr 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "022cd691704491df67d25d006fe8eca083098253c4d43516c2206479c58c6736" "checksum curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839" -"checksum debugid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "088c9627adec1e494ff9dea77377f1e69893023d631254a0ec68b16ee20be3e9" +"checksum debugid 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "36294832663d7747e17832f32492daedb65ae665d5ae1b369edabf52a2a92afc" "checksum deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" +"checksum dialoguer 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94616e25d2c04fc97253d145f6ca33ad84a584258dc70c4e621cc79a57f903b6" "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" "checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" "checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" @@ -5039,6 +5467,7 @@ dependencies = [ "checksum ed25519 1.0.0-pre.4 (registry+https://github.com/rust-lang/crates.io-index)" = "01a416ad364359365b649fb28d7a6fab7f998d13282182241134beb014a9a378" "checksum ed25519-dalek 1.0.0-pre.3 (git+https://github.com/cbeck88/ed25519-dalek?rev=c0b0ab31d3572de6fb01d6b4a4f052784034b0b2)" = "" "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +"checksum encode_unicode 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" "checksum encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8d03faa7fe0c1431609dfad7bbe827af30f82e1e2ae6f7ee4fca6bd764bc28" "checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" "checksum error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9" @@ -5053,7 +5482,14 @@ dependencies = [ "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)" = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" +"checksum futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" +"checksum futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" "checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +"checksum futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" +"checksum futures-macro 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" +"checksum futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6" +"checksum futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" +"checksum futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" "checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" "checksum genio 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f4e26859a808ffa83a83f20c7e3c9366afea91edae637a6ac203051885882dc8" "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" @@ -5065,6 +5501,7 @@ dependencies = [ "checksum grpcio-sys 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1b3080bdcfde08451cc7994f0b7c0c71c638629f8fb0049217af76a69d0742c3" "checksum gzip-header 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0131feb3d3bb2a5a238d8a4d09f6353b7ebfdc52e77bccbf4ea6eaa751dde639" "checksum h2 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" +"checksum h2 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "377038bf3c89d18d6ca1431e7a5027194fbd724ca10592b9487ede5e8e144f42" "checksum half 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20d6a47d6e4b8559729f58287efa8e6f767e603c068fea7a5e4d9f1cebe2bebb" "checksum handlebars 2.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "af92141a22acceb515fb6b13ac59d6d0b3dd3437e13832573af8e0d3247f29d5" "checksum hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e1de41fb8dba9714efd92241565cdff73f78508c95697dd56787d3cba27e2353" @@ -5077,17 +5514,23 @@ dependencies = [ "checksum hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3fa08a006102488bd9cd5b8013aabe84955cf5ae22e304c2caf655b633aefae3" "checksum hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" "checksum hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" +"checksum hostname 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" "checksum http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" +"checksum http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" "checksum http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" +"checksum http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" "checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" "checksum httpdate 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" "checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" "checksum hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)" = "9dbe6ed1438e1f8ad955a4701e9a944938e9519f6888d12d8558b645e247d5f6" +"checksum hyper 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ed6081100e960d9d74734659ffc9cc91daf1c0fc7aceb8eaa94ee1a3f5046f2e" "checksum hyper-rustls 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)" = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" +"checksum hyper-rustls 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac965ea399ec3a25ac7d13b8affd4b8f39325cca00858ddf5eb29b79e6b14b08" "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" -"checksum im 12.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "de38d1511a0ce7677538acb1e31b5df605147c458e061b2cdb89858afb1cd182" +"checksum im 14.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "696059c87b83c5a258817ecd67c3af915e3ed141891fc35a1e79908801cf0ce7" "checksum indexmap 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b54058f0a6ff80b6803da8faf8997cde53872b38f4023728f6830b06cd3c0dc" +"checksum indicatif 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a68371cf417889c9d7f98235b7102ea7c54fc59bcbd22f3dea785be9d27e40" "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" "checksum itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" "checksum itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" @@ -5099,7 +5542,6 @@ dependencies = [ "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" "checksum libloading 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" -"checksum libm 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" "checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" "checksum lmdb 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b0908efb5d6496aa977d96f91413da2635a902e5e31dbef0bfb88986c248539" "checksum lmdb-sys 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d5b392838cfe8858e86fac37cf97a0e8c55cc60ba0a18365cadc33092f128ce9" @@ -5108,6 +5550,7 @@ dependencies = [ "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum lru 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "5d8f669d42c72d18514dfca8115689c5f6370a17d980cb5bd777a67f404594c8" "checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +"checksum match_cfg 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" "checksum mbedtls 0.5.1 (git+https://github.com/mobilecoinofficial/rust-mbedtls.git?tag=mc-0.2)" = "" @@ -5130,13 +5573,13 @@ dependencies = [ "checksum multipart 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)" = "136eed74cadb9edd2651ffba732b19a450316b680e4f48d6c79e905799e19d01" "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" "checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" -"checksum num-bigint-dig 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3d03c330f9f7a2c19e3c0b42698e48141d0809c78cd9b6219f85bd7d7e892aa" "checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" -"checksum num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" "checksum num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" +"checksum number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" "checksum oorandom 11.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ebcec7c9c2a95cacc7cd0ecb89d8a8454eca13906f6deb55258ffff0adeb9405" "checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" "checksum output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" "checksum packed_simd 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a85ea9fc0d4ac0deb6fe7911d38786b32fc11119afd9e9d38b84ff691ce64220" "checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" @@ -5153,6 +5596,10 @@ dependencies = [ "checksum phf_codegen 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" "checksum phf_generator 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" "checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +"checksum pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7804a463a8d9572f13453c516a5faea534a2403d7ced2f0c7e100eeff072772c" +"checksum pin-project-internal 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f" +"checksum pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" +"checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" "checksum plotters 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "4e3bb8da247d27ae212529352020f3e5ee16e83c0c258061d27b08ab92675eeb" "checksum polyval 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ec3341498978de3bfd12d1b22f1af1de22818f5473a11e8a6ef997989e3a212" @@ -5160,12 +5607,14 @@ dependencies = [ "checksum pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" "checksum proc-macro-error 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1b79a464461615532fcc8a6ed8296fa66cc12350c18460ab3f4594a6cee0fcb6" "checksum proc-macro-error-attr 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "23832e5eae6bac56bbac190500eef1aaede63776b5cd131eaa4ee7fe120cd892" +"checksum proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)" = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63" +"checksum proc-macro-nested 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" "checksum prometheus 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5567486d5778e2c6455b1b90ff1c558f29e751fc018130fa182e15828e728af1" "checksum proptest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bf6147d103a7c9d7598f4105cf049b15c99e2ecd93179bf024f0fd349be5ada4" -"checksum prost 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)" = "" -"checksum prost-derive 0.6.1 (git+https://github.com/cbeck88/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)" = "" +"checksum prost 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)" = "" +"checksum prost-derive 0.6.1 (git+https://github.com/mobilecoinofficial/prost?rev=4e1905329369ca7a1cac3eda978ee9379167ee95)" = "" "checksum protobuf 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "71964f34fd51cf04882d7ae3325fa0794d4cad66a03d0003f38d8ae4f63ba126" "checksum protobuf-codegen 2.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f122718d40a717194a9b6df594ab8f8718a31f40787a98606fcdc32fc308fdb6" "checksum protoc 2.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fd83d2547a9e2c8bc6016607281b3ec7ef4871c55be6930915481d80350ab88" @@ -5190,48 +5639,56 @@ dependencies = [ "checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" "checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" "checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +"checksum rand_xoshiro 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9fcdd2e881d02f1d9390ae47ad8e5696a9e4be7b547a1da2afbc61973217004" "checksum rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" "checksum rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" -"checksum regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87" +"checksum regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" "checksum regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" -"checksum regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90" +"checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +"checksum reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)" = "02b81e49ddec5109a9dcfc5f2a317ff53377c915e9ae9d4f2fb50914b85614e2" "checksum reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)" = "f88643aea3c1343c804950d7bf983bd2067f5ab59db6d613a08e05572f2714ab" "checksum retry 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c8ac83b31b3831aa4b07608db4170f6555ab12942197037c38570dc4c5ba5028" -"checksum ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6747f8da1f2b1fabbee1aaa4eb8a11abf9adef0bf58a41cee45db5d59cecdfac" +"checksum ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)" = "1ba5a8ec64ee89a76c98c549af81ff14813df09c3e6dc4766c3856da48597a0c" "checksum rjson 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5510dbde48c4c37bf69123b1f636b6dd5f8dffe1f4e358af03c46a4947dca219" "checksum rouille 3.0.0 (git+https://github.com/tomaka/rouille/?rev=db66a3b47af4271939e1aba21d0f36ccba3d1b70)" = "" -"checksum rsa 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6ed8692d8e0ea3baae03f0f32ecfc13a6c6f1f85fcd6d9fdefcdf364e70f4df9" "checksum rusoto_core 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f1d1ecfe8dac29878a713fbc4c36b0a84a48f7a6883541841cdff9fdd2ba7dfb" "checksum rusoto_credential 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8632e41d289db90dd40d0389c71a23c5489e3afd448424226529113102e2a002" "checksum rusoto_s3 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3fedcadf3d73c2925b05d547b66787f2219c5e727a98c893fff5cf2197dbd678" "checksum rusoto_signature 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7063a70614eb4b36f49bcf4f6f6bb30cc765e3072b317d6afdfe51e7a9f482d1" "checksum rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" +"checksum rust_decimal 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1989cf75ea3463a3584ad69f92642e8046083482f519017aa2258333e5dc61cd" "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" "checksum rustc-hash 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum rustls 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" +"checksum rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1" +"checksum rustls-native-certs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a75ffeb84a6bd9d014713119542ce415db3a3e4748f0bfce1e1416cd224a23a5" "checksum rustversion 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bba175698996010c4f6dce5e7f173b6eb781fce25d2cfc45e27091ce0b79f6" "checksum rusty-fork 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3dd93264e10c577503e926bd1430193eeb5d21b059148910082245309b424fae" "checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" "checksum safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" "checksum same-file 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +"checksum schannel 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "039c25b130bd8c1321ee2d7de7fde2659fa9c2744e4bb29711cfc852ea53cd19" "checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" "checksum sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" "checksum secrecy 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9eb052cf770a381fa9a6ee63038ff9a0b11d30abb53be970672e950649ff0bfb" +"checksum security-framework 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "97bbedbe81904398b6ebb054b3e912f99d55807125790f3198ac990d98def5b0" +"checksum security-framework-sys 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8ddb15a5fec93b7021b8a9e96009c5d8d51c15673569f7c0f6b7204e5b7b404f" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum sentry 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "509c5fbb9e875fafcd9c4612c0e49d476083b848bf87380cfe1126ebc745c140" -"checksum sentry-types 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b23e3d9c8c6e4a1523f24df6753c4088bfe16c44a73c8881c1d23c70f28ae280" +"checksum sentry 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efe1c6c258797410d14ef90993e00916318d17461b201538d76fd8d3031cad4e" +"checksum sentry-types 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "12ec406c11c060c8a7d5d67fc6f4beb2888338dcb12b9af409451995f124749d" "checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" "checksum serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622" "checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" -"checksum serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)" = "eab8f15f15d6c41a154c1b128a22f2dfabe350ef53c40953d84e36155c91192b" +"checksum serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9" "checksum serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a" +"checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" "checksum serial_test 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba22db78004fb4f112cdb92895a65395ca0aa25215a467316e064ac2e487dc63" "checksum serial_test_derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2cdabca97cecf57fa82170ca1689ead2933f38f106a5a91a947a0ca156c6d39d" "checksum sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" @@ -5245,7 +5702,7 @@ dependencies = [ "checksum signature 1.0.0-pre.5 (registry+https://github.com/rust-lang/crates.io-index)" = "7633506bfb9bb78fd228f8fa51d46802fc72fe642e707b42a64f57b07eaf8fb8" "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" "checksum siphasher 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "83da420ee8d1a89e640d0948c646c1c088758d3a3c538f943bfa97bdac17929d" -"checksum sized-chunks 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d3e7f23bad2d6694e0f46f5e470ec27eb07b8f3e8b309a4b0dc17501928b9f2" +"checksum sized-chunks 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d59044ea371ad781ff976f7b06480b9f0180e834eda94114f2afb4afc12b7718" "checksum skeptic 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6fb8ed853fdc19ce09752d63f3a2e5b5158aeb261520cd75eb618bd60305165" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cc9c640a4adbfbcc11ffb95efe5aa7af7309e002adab54b185507dbf2377b99" @@ -5262,7 +5719,6 @@ dependencies = [ "checksum socket2 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e8b74de517221a2cb01a53349cf54182acdc31a074727d3079068448c0676d85" "checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" "checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -"checksum stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c" "checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum structopt 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "df136b42d76b1fbea72e2ab3057343977b04b4a2e00836c3c7c0673829572713" @@ -5279,6 +5735,7 @@ dependencies = [ "checksum term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" "checksum term 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" "checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +"checksum termios 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6f0fcee7b24a25675de40d5bb4de6e41b0df07bc9856295e7e2b3a3600c400c2" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" @@ -5287,6 +5744,7 @@ dependencies = [ "checksum tiny_http 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1661fa0a44c95d01604bd05c66732a446c657efb62b5164a7a083a3b552b4951" "checksum tinytemplate 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "57a3c6667d3e65eb1bc3aed6fd14011c6cbc3a0665218ab7f5daf040b9ec371a" "checksum tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" +"checksum tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "34ef16d072d2b6dc8b4a56c70f5c5ced1a37752116f8e7c1e80c659aa7cb6713" "checksum tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" "checksum tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c501eceaf96f0e1793cf26beb63da3d11c738c4a943fdf3746d81d64684c39f" "checksum tokio-current-thread 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "d16217cad7f1b840c5a97dfb3c43b0c871fef423a6e8d2118c604e843662a443" @@ -5296,6 +5754,7 @@ dependencies = [ "checksum tokio-process 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afbd6ef1b8cc2bd2c2b580d882774d443ebb1c6ceefe35ba9ea4ab586c89dbe8" "checksum tokio-reactor 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "6732fe6b53c8d11178dcb77ac6d9682af27fc6d4cb87789449152e5377377146" "checksum tokio-rustls 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" +"checksum tokio-rustls 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4adb8b3e5f86b707f1b54e7c15b6de52617a823608ccda98a15d3a24222f265a" "checksum tokio-signal 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "dd6dc5276ea05ce379a16de90083ec80836440d5ef8a6a39545a3207373b8296" "checksum tokio-sync 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "d06554cce1ae4a50f42fba8023918afa931413aded705b560e29600ccf7c6d76" "checksum tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1d14b10654be682ac43efee27401d792507e30fd8d26389e1da3b185de2e4119" @@ -5303,7 +5762,9 @@ dependencies = [ "checksum tokio-timer 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "1739638e364e558128461fc1ad84d997702c8e31c2e6b18fb99842268199e827" "checksum tokio-udp 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f02298505547f73e60f568359ef0d016d5acd6e830ab9bc7c4a5b3403440121b" "checksum tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "037ffc3ba0e12a0ab4aca92e5234e0dedeb48fddf6ccd260f1f150a36a9f2445" +"checksum tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" "checksum toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +"checksum tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" "checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" "checksum try_from 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" "checksum twoway 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" @@ -5322,8 +5783,8 @@ dependencies = [ "checksum untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60369ef7a31de49bcb3f6ca728d4ba7300d9a1658f94c727d4cab8c8d9f4aece" "checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" "checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" -"checksum url_serde 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "74e7d099f1ee52f823d4bdd60c93c3602043c728f5db3b97bdb548467f7bddea" "checksum uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" +"checksum uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" "checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" @@ -5332,9 +5793,11 @@ dependencies = [ "checksum wait-timeout 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" "checksum walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" "checksum want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" +"checksum want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" "checksum wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "5205e9afdf42282b192e2310a5b463a6d1c1d774e30dc3c791ac37ab42d2616c" "checksum wasm-bindgen-backend 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "11cdb95816290b525b32587d76419facd99662a07e59d3cdb560488a819d9a45" +"checksum wasm-bindgen-futures 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8bbdd49e3e28b40dec6a9ba8d17798245ce32b019513a845369c641b275135d9" "checksum wasm-bindgen-macro 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "574094772ce6921576fb6f2e3f7497b8a76273b6db092be18fc48a082de09dc3" "checksum wasm-bindgen-macro-support 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "e85031354f25eaebe78bb7db1c3d86140312a911a106b2e29f9cc440ce3e7668" "checksum wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "f5e7e61fc929f4c0dddb748b102ebf9f632e2b8d739f2016542b4de2965a9601" @@ -5342,6 +5805,7 @@ dependencies = [ "checksum web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "aaf97caf6aa8c2b1dac90faf0db529d9d63c93846cca4911856f78a83cebf53b" "checksum webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d7e664e770ac0110e2384769bcc59ed19e329d81f555916a6e072714957b81b4" "checksum webpki-roots 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" +"checksum webpki-roots 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "91cd5736df7f12a964a5067a12c62fa38e1bd8080aff1f80bc29be7c80d19ab4" "checksum weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" "checksum which 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" diff --git a/Cargo.toml b/Cargo.toml index f3ed8fcf86..acb0867ddb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,25 @@ [workspace] members = [ - "attest/core", "attest/ake", "attest/api", + "attest/core", "attest/net", "attest/trusted", "attest/untrusted", "common", "consensus/api", "consensus/enclave", - "consensus/enclave/edl", - "consensus/enclave/measurement", "consensus/enclave/api", + "consensus/enclave/edl", "consensus/enclave/impl", + "consensus/enclave/measurement", "consensus/enclave/mock", "consensus/scp", "consensus/scp/play", "consensus/service", "crypto/ake/mcnoise", + "crypto/box", "crypto/digestible", - "crypto/ecies", "crypto/keys", "crypto/mcrand", "crypto/message-cipher", @@ -45,10 +45,12 @@ members = [ "sgx/sgx_debug_edl", "sgx/sgx_panic_edl", "sgx/sgx_slog_edl", + "testnet-client", "transaction/core", "transaction/core/test-utils", "transaction/std", "util/b58-payloads", + "util/build-grpc", "util/encodings", "util/generate-sample-ledger", "util/grpc", @@ -86,5 +88,5 @@ rpath = true [patch.crates-io] rouille = { git = "https://github.com/tomaka/rouille/", rev = "db66a3b47af4271939e1aba21d0f36ccba3d1b70" } -prost = { git = "https://github.com/cbeck88/prost", rev = "4e1905329369ca7a1cac3eda978ee9379167ee95" } -prost-derive = { git = "https://github.com/cbeck88/prost", rev = "4e1905329369ca7a1cac3eda978ee9379167ee95" } +prost = { git = "https://github.com/mobilecoinofficial/prost", rev = "4e1905329369ca7a1cac3eda978ee9379167ee95" } +prost-derive = { git = "https://github.com/mobilecoinofficial/prost", rev = "4e1905329369ca7a1cac3eda978ee9379167ee95" } diff --git a/README.md b/README.md index 79cc6154b7..b0ea23cb2d 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,13 @@ To validate the blockchain, see [The MobileCoin Daemon](#the-mobilecoin-daemon). This workspace is built with `cargo build`, and tested with `cargo test`. -Some crates, such as consensus-service, build binaries that have special runtime requirements, such as Intel's Software Guard eXtensions (SGX). You will need to provide an environment variable to indicate whether to build for hardware mode (`HW`) or simulation mode (`SW`) mode, as well as which attestation service to use, e.g. `SGX_MODE=HW IAS_MODE=DEV cargo build`. You will find more information in the BUILD.md in these crates, for example, [consensus/service/BUILD.md](consensus/service/BUILD.md). +Some crates depend on SGX, which creates build and runtime requirements. (Specifically, `consensus-service`.) -To ease the process of building, we provide a tool which starts a docker container with all the correct dependencies, including the necessary versions of SGX and proto libraries. +For detailed information about setting up a build environment, how enclaves are built, and configuring +the build, check out [BUILD.md](BUILD.md). -You can use it with the following: +For a quick start, you can build in the same docker image that we use for CI, using the `mob` tool. +This requires you to install docker. ``` # From the root of the repo @@ -51,8 +53,6 @@ You can use it with the following: cargo build ``` -The consensus_enclave is a nested workspace, which is built with `cargo build.` - #### License The components of MobileCoin require different licenses. Look for the LICENSE file in each crate for more information. @@ -93,7 +93,7 @@ To build and run ledger distribution, see the [ledger/distribution](./ledger/dis To send and receive transactions, run a [wallet client](./mobilecoind/clients) alongside [`mobilecoind`](./mobilecoind/README.md). ->Note: `mobilecoind` provides the wallet bindings in gRPC, so that you can write a wallet client in any language. We provide a [python example](./mobilecoind/clients/python). +>Note: `mobilecoind` provides the wallet bindings in gRPC, so that you can write a wallet client in any language. We provide both a [python and java example](./mobilecoind/clients/). ##### The MobileCoin Daemon diff --git a/attest/api/.gitignore b/attest/api/.gitignore deleted file mode 100644 index acade0c83c..0000000000 --- a/attest/api/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# gRPC-generated files -src/attest.rs -src/attest_grpc.rs \ No newline at end of file diff --git a/attest/api/Cargo.toml b/attest/api/Cargo.toml index 7cefca8586..bdb8dc9995 100644 --- a/attest/api/Cargo.toml +++ b/attest/api/Cargo.toml @@ -1,15 +1,19 @@ [package] name = "attest-api" -version = "0.1.0" +version = "0.1.1" authors = ["MobileCoin"] license = "MIT/Apache-2.0" edition = "2018" description = "gRPC APIs for encrypted communications with an enclave" keywords = ["SIGMA", "Cryptography", "Key Exchange", "Diffie-Hellman", "SGX", "Attestation"] readme = "README.md" +links = "mc-attest-api" [build-dependencies] -protoc-grpcio = "0.3.1" +mc-build-grpc = { path = "../../util/build-grpc" } +mcbuild-utils = { path = "../../mcbuild/utils" } + +cargo-emit = "0.1.1" [dependencies] attest-ake = { path = "../ake" } diff --git a/attest/api/build.rs b/attest/api/build.rs index b1234e11ad..a0eda3da25 100644 --- a/attest/api/build.rs +++ b/attest/api/build.rs @@ -1,20 +1,18 @@ // Copyright (c) 2018-2020 MobileCoin Inc. -extern crate protoc_grpcio; - -fn compile_protos() { - let proto_root = "./proto"; - let proto_files = ["attest.proto"]; - let output_destination = "src"; - println!("cargo:rerun-if-changed={}", proto_root); - for file in &proto_files { - println!("cargo:rerun-if-changed={}/{}", proto_root, file); - } - - protoc_grpcio::compile_grpc_protos(&proto_files, &[proto_root], output_destination) - .expect("Failed to compile gRPC definitions!"); -} +use mcbuild_utils::Environment; fn main() { - compile_protos(); + let env = Environment::default(); + let proto_dir = env.dir().join("proto"); + cargo_emit::pair!( + "PROTOS_PATH", + "{}", + proto_dir + .as_os_str() + .to_str() + .expect("Invalid UTF-8 in proto dir path") + ); + + mc_build_grpc::compile_protos_and_generate_mod_rs(&["./proto"], &["attest.proto"]); } diff --git a/attest/api/src/lib.rs b/attest/api/src/lib.rs index 7b3e638b97..4dac8742e9 100644 --- a/attest/api/src/lib.rs +++ b/attest/api/src/lib.rs @@ -2,10 +2,18 @@ //! A gRPC API module for attestation -pub mod attest; -pub mod attest_grpc; -pub mod conversions; - -pub mod empty { +mod autogenerated_code { + // Expose proto data types from included third-party/external proto files. 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 mod conversions; +pub use autogenerated_code::*; diff --git a/attest/net/src/ias.rs b/attest/net/src/ias.rs index 34aff68dc2..c1a38f89f7 100644 --- a/attest/net/src/ias.rs +++ b/attest/net/src/ias.rs @@ -20,7 +20,7 @@ cfg_if! { if #[cfg(feature = "ias-dev")] { const IAS_BASEURI: &str = "https://api.trustedservices.intel.com/sgx/dev/attestation/v3"; } else { - const IAS_BASEURI: &str = "https://api.trustedservices.intel.com/attestation/sgx/v3"; + const IAS_BASEURI: &str = "https://api.trustedservices.intel.com/sgx/attestation/v3"; } } @@ -82,9 +82,10 @@ impl RaClient for IasClient { }; global_log::trace!( - "Submitting JSON request for {:?} to IAS: '{}'", + "Submitting JSON request for {:?} to IAS: '{}' at {}", quote, - jsvalue.to_string() + jsvalue.to_string(), + format!("{}/report", IAS_BASEURI), ); let mut response = self diff --git a/attest/untrusted/README.md b/attest/untrusted/README.md index 0cc8300700..a1eea68636 100644 --- a/attest/untrusted/README.md +++ b/attest/untrusted/README.md @@ -1,6 +1,6 @@ -# Mobilenode Enclave API +# MobileCoin Enclave API -This crate contains the untrusted-facing APIs for a Mobilenode enclave. The goal is to provide an API to the enclave that interacts as a special-case of the more commonly understood object remoting. In particular, there should be an "untrusted" implementation of these APIs which lives in the node, and a "trusted" implementation of these APIs which lives in the enclave. +This crate contains the untrusted-facing APIs for a MobileCoin enclave. The goal is to provide an API to the enclave that interacts as a special-case of the more commonly understood object remoting. In particular, there should be an "untrusted" implementation of these APIs which lives in the node, and a "trusted" implementation of these APIs which lives in the enclave. This particular use of remoting, where we simply want to cross a trust boundary that lives within the same process on the same machine, is significantly simplified from the typically-maligned *networked* remoting, and is therefore significantly less insane than most remoting frameworks. In this model, the typical workflow is something akin to this: diff --git a/common/Cargo.toml b/common/Cargo.toml index c5af013703..113bdf86df 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -55,7 +55,7 @@ chrono = { version = "0.4", optional = true } hostname = { version = "0.1", optional = true } lazy_static = { version = "1.4", optional = true } mclogger-macros = { path = "../util/mclogger-macros", optional = true } -sentry = { version = "0.16", optional = true, default-features = false, features = ["with_client_implementation", "with_default_transport", "with_panic", "with_failure", "with_device_info", "with_rust_info", "with_rustls"] } +sentry = { version = "0.18", optional = true, default-features = false, features = ["with_client_implementation", "with_reqwest_transport", "with_panic", "with_failure", "with_device_info", "with_rust_info", "with_rustls"] } slog = { version = "2.5", features = ["dynamic-keys", "max_level_trace", "release_max_level_trace"], optional = true } slog-async = { version = "2.3", optional = true } slog-atomic = { version = "3.0", optional = true } diff --git a/consensus/api/.gitignore b/consensus/api/.gitignore deleted file mode 100644 index 1f8db64e5d..0000000000 --- a/consensus/api/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -src/blockchain.rs -src/blockchain_grpc.rs -src/consensus_client.rs -src/consensus_client_grpc.rs -src/consensus_common.rs -src/consensus_peer.rs -src/consensus_peer_grpc.rs -src/external.rs -src/ledger_enclave_server.rs -src/ledger_enclave_server_grpc.rs -src/transaction.rs diff --git a/consensus/api/Cargo.toml b/consensus/api/Cargo.toml index f56b73f283..3fbe6f32df 100644 --- a/consensus/api/Cargo.toml +++ b/consensus/api/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "mobilecoin-api" -version = "0.1.0" +version = "0.1.1" authors = ["MobileCoin"] build = "build.rs" edition = "2018" +links = "mc-consensus-api" [lib] name = "mobilecoin_api" @@ -27,7 +28,10 @@ curve25519-dalek = { version = "2.0", default-features = false, features = ["sim curve25519-dalek = { version = "2.0", default-features = false, features = ["nightly", "u64_backend"] } [build-dependencies] -protoc-grpcio = "0.3.1" +mc-build-grpc = { path = "../../util/build-grpc" } +mcbuild-utils = { path = "../../mcbuild/utils" } + +cargo-emit = "0.1.1" [dev-dependencies] mcrand = { path = "../../crypto/mcrand" } diff --git a/consensus/api/build.rs b/consensus/api/build.rs index 160f6528f0..c630bad92f 100644 --- a/consensus/api/build.rs +++ b/consensus/api/build.rs @@ -1,32 +1,32 @@ // Copyright (c) 2018-2020 MobileCoin Inc. -extern crate protoc_grpcio; +use mcbuild_utils::Environment; -fn compile_protos() { - let proto_root = "./proto"; - let proto_files = [ - "transaction.proto", - "blockchain.proto", - "external.proto", - "ledger_enclave_server.proto", - "consensus_client.proto", - "consensus_common.proto", - "consensus_peer.proto", - ]; - let output_destination = "src"; - println!("cargo:rerun-if-changed={}", proto_root); - for file in &proto_files { - println!("cargo:rerun-if-changed={}/{}", proto_root, file); - } +fn main() { + let env = Environment::default(); - protoc_grpcio::compile_grpc_protos( - &proto_files, - &[proto_root, "../../attest/api/proto"], - output_destination, - ) - .expect("Failed to compile gRPC definitions!"); -} + 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); -fn main() { - compile_protos(); + let attest_proto_path = env + .depvar("MC_ATTEST_API_PROTOS_PATH") + .expect("Could not read attest api's protos path") + .to_owned(); + let mut all_proto_dirs = attest_proto_path.split(':').collect::>(); + all_proto_dirs.push(proto_str); + + mc_build_grpc::compile_protos_and_generate_mod_rs( + all_proto_dirs.as_slice(), + &[ + "blockchain.proto", + "external.proto", + "consensus_client.proto", + "consensus_common.proto", + "consensus_peer.proto", + ], + ); } diff --git a/consensus/api/proto/blockchain.proto b/consensus/api/proto/blockchain.proto index 4a2a25f676..91c247e242 100644 --- a/consensus/api/proto/blockchain.proto +++ b/consensus/api/proto/blockchain.proto @@ -4,7 +4,6 @@ syntax = "proto3"; import "google/protobuf/empty.proto"; -import "transaction.proto"; import "external.proto"; package blockchain; @@ -53,7 +52,7 @@ message S3Block { Block block = 1; // Transactions in this block - repeated transaction.RedactedTx transactions = 2; + repeated external.RedactedTx transactions = 2; // Block signature, when available. BlockSignature signature = 3; diff --git a/consensus/api/proto/consensus_common.proto b/consensus/api/proto/consensus_common.proto index ad60fc1e9b..6ea6795b27 100644 --- a/consensus/api/proto/consensus_common.proto +++ b/consensus/api/proto/consensus_common.proto @@ -24,7 +24,7 @@ enum ProposeTxResult { ExcessiveRingSize = 22; DuplicateRingElements = 23; UnsortedRingElements = 24; - UnequalRingSizes = 25; + UnequalRingSizes = 25 [deprecated=true]; UnsortedKeyImages = 26; ContainsSpentKeyImage = 27; DuplicateKeyImages = 28; diff --git a/consensus/api/proto/external.proto b/consensus/api/proto/external.proto index bfaffdb0f1..6e26a36a7c 100644 --- a/consensus/api/proto/external.proto +++ b/consensus/api/proto/external.proto @@ -6,6 +6,9 @@ syntax = "proto3"; package external; +option java_package = "com.mobilecoin.consensus"; +option java_outer_classname = "ConsensusAPI"; + /////////////////////////////////////////////////////////////////////////////// // `keys` crate /////////////////////////////////////////////////////////////////////////////// @@ -22,6 +25,10 @@ message CurvePoint { bytes data = 1; } +message CompressedRistretto { + bytes data = 1; +} + message CurveScalar { bytes data = 1; } @@ -38,40 +45,9 @@ message Ed25519Signature { bytes data = 1; } -/////////////////////////////////////////////////////////////////////////////// -// `common` crate -/////////////////////////////////////////////////////////////////////////////// - -message EncryptedFogHint { - bytes data = 1; -} - -/////////////////////////////////////////////////////////////////////////////// -// `ringct` crate -/////////////////////////////////////////////////////////////////////////////// - -message RingCtInput { - RistrettoPublic address = 1; - CurvePoint commitment = 2; -} - -message RingCtInputRow { - repeated RingCtInput row = 1; -} - -message RingCtChallengeResponse { - repeated CurveScalar response = 1; -} - -message RingCtSignature { - repeated KeyImage key_images = 1; - repeated RingCtChallengeResponse challenge_responses = 2; - CurveScalar challenge = 3; -} - /////////////////////////////////////////////////////////////////////////////// -// `ledger` crate +// `trasaction/core` crate /////////////////////////////////////////////////////////////////////////////// message Range { @@ -106,6 +82,10 @@ message Amount { CurveScalar masked_blinding = 3; } +message EncryptedFogHint { + bytes data = 1; +} + // A Transaction Output. message TxOut { // Amount. @@ -139,18 +119,43 @@ message TxPrefix { // Fee paid to the foundation for this transaction uint64 fee = 3; + + // The block index at which this transaction is no longer valid. + uint64 tombstone_block = 4; +} + +message RingMLSAG { + CurveScalar c_zero = 1; + repeated CurveScalar responses = 2; + KeyImage key_image = 3; +} + +message SignatureRctBulletproofs { + repeated RingMLSAG ring_signatures = 1; + repeated CompressedRistretto pseudo_output_commitments = 2; + bytes range_proofs = 3; } message Tx { - // The actual contents of the transaction + // The actual contents of the transaction. TxPrefix prefix = 1; - // The RingCT signature on the prefix - RingCtSignature signature = 2; + // The RingCT signature on the prefix. + SignatureRctBulletproofs signature = 2; +} - // The range proofs to show the values are in the proper range - bytes range_proofs = 3; +message TxHash { + // Hash of a single transaction. + bytes hash = 1; +} - // The block index past which this submitted transaction is no longer valid - uint64 tombstone_block = 4; +// A redacted transaction. +message RedactedTx { + uint32 version = 1; + + // Outputs created by this transaction. + repeated TxOut outputs = 2; + + // Key images "spent" by this transaction. + repeated KeyImage key_images = 3; } diff --git a/consensus/api/proto/ledger_enclave_server.proto b/consensus/api/proto/ledger_enclave_server.proto deleted file mode 100644 index e6c78bc9cc..0000000000 --- a/consensus/api/proto/ledger_enclave_server.proto +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2018-2020 MobileCoin Inc. - -syntax = "proto3"; -import "attest.proto"; -import "external.proto"; - -package ledger_enclave_server; - -service LedgerEnclaveServerAPI { - /// Check if key images have appeared in the ledger - rpc CheckKeyImages (attest.Message) returns (attest.Message) {} - /// Get rings and merkle proofs of membership - rpc GetOutputs (attest.Message) returns (attest.Message) {} -} - -message GetOutputsRequest { - repeated uint64 indexes = 1; - uint64 merkle_root_block = 2; -} - -message GetOutputsResponse { - repeated external.TxOut outputs = 1; - repeated external.TxOutMembershipProof proofs = 2; -} - -message CheckKeyImagesRequest { - /// A list of key images queries, to check if they have appeared in the ledger - /// already, and if so, in what block. - repeated KeyImageQuery queries = 1; -} - -message KeyImageQuery { - /// The key image to check. Should be exactly 32 bytes - bytes key_image = 1; - /// An optional start_block to start searching forward from when performing the check. - /// - /// Note: - /// A correct implementation of the server may ignore this, it is an optimization. - /// This may help scaling because then for daily active users we won't have to - /// obliviously scan the whole set to support their queries. - fixed64 start_block = 2; -} - -/// A response to a request for key image checks -/// -/// Contracts: -/// If a KeyImageResult comes back with spent_at != 0, then it was spent in that block index. -/// If a KeyImageResult comes back with spent_at == 0, then it was NOT spent, AT LEAST as of -/// resp.block_height (the top level block height number). It is possible that it WAS actually -/// spent in resp.block_height + 1, but the server didn't know, or didn't figure that out. -message CheckKeyImagesResponse { - /// The block height at the time that the request was evaluated - /// - /// Note: This may be a conservative estimate, in the sense of being a lower bound. - /// It's allowed that the data is "more fresh" than we are telling the client, but not less fresh. - /// - /// Implementation note: If the server does not evaluate all the key image checks as one - /// database transaction, then this number should be a lower bound on the block height across - /// all of those transactions. - uint64 block_height = 1; - /// The results for each key image query - repeated KeyImageResult results = 2; -} - -message KeyImageResult { - /// The key image that was queried. Exactly 32 bytes - bytes key_image = 1; - /// The block index at which it was spent - /// 0 indicates that it has not been spent (nothing can be spent in genesis block) - fixed64 spent_at = 2; -} diff --git a/consensus/api/proto/transaction.proto b/consensus/api/proto/transaction.proto deleted file mode 100644 index c7f2631f80..0000000000 --- a/consensus/api/proto/transaction.proto +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2018-2020 MobileCoin Inc. - -// Transaction-related data types - -syntax = "proto3"; - -package transaction; - -// A spent KeyImage. -message KeyImage{ - // 32 bytes - bytes value = 1; -} - -message TxHash { - // Hash of a single transaction. - bytes hash = 1; -} - - -// A redacted transaction. -message RedactedTx { - uint32 version = 1; - - // Outputs created by this transaction. - repeated TxOut outs = 2; - - // Key images "spent" by this transaction. - repeated KeyImage key_images = 3; -} - - -// A Transaction Output. -message TxOut { - // Onetime key. - bytes target_key = 1; - - // Public key. - bytes public_key = 2; - - // The Amount's value (u64), "masked" with the shared secret rB. - bytes masked_value = 3; - - // The Amount's blinding (Scalar), "masked" with the shared secret rB. - bytes masked_blinding = 4; - - // RistrettoPoint encoded natively - bytes commitment = 5; - - // 128 byte encrypted account hint - bytes e_account_hint = 6; -} diff --git a/consensus/api/src/conversions.rs b/consensus/api/src/conversions.rs index 0e5cd68771..7ffbbd1871 100644 --- a/consensus/api/src/conversions.rs +++ b/consensus/api/src/conversions.rs @@ -6,8 +6,9 @@ //! values stored in the ledger and values transmitted over the API. This module provides conversions //! between "equivalent" types, such as `mobilecoin_api::blockchain::Block` and `transaction::Block`. -use crate::{blockchain, consensus_common::ProposeTxResult, external, transaction as tx_grpc}; +use crate::{blockchain, consensus_common::ProposeTxResult, external}; use common::HashMap; +use curve25519_dalek::ristretto::CompressedRistretto; use keys::{ CompressedRistrettoPublic, Ed25519Public, Ed25519Signature, RistrettoPrivate, RistrettoPublic, }; @@ -24,13 +25,13 @@ use transaction::{ encrypted_fog_hint::EncryptedFogHint, range::Range, ring_signature::{ - Blinding, ChallengeResponse, Commitment, CurvePoint, CurveScalar, Error as RingSigError, - KeyImage, SignatureRctFull, + Blinding, CurvePoint, CurveScalar, Error as RingSigError, KeyImage, RingMLSAG, + SignatureRctBulletproofs, }, tx, tx::{TxOutMembershipElement, TxOutMembershipHash, TxOutMembershipProof}, validation::TransactionValidationError, - BlockSignature, RedactedTx, + BlockSignature, CompressedCommitment, RedactedTx, }; #[derive(Debug, Eq, PartialEq, Copy, Clone)] @@ -114,20 +115,20 @@ impl TryFrom<&blockchain::Block> for transaction::Block { } } -/// Convert ledger_types::TxHash --> tx_grpc::TxHash. -impl From<&tx::TxHash> for tx_grpc::TxHash { +/// Convert tx::TxHash --> external::TxHash. +impl From<&tx::TxHash> for external::TxHash { fn from(other: &tx::TxHash) -> Self { - let mut tx_hash = tx_grpc::TxHash::new(); + let mut tx_hash = external::TxHash::new(); tx_hash.set_hash(other.to_vec()); tx_hash } } -/// Convert tx_grpc::TxHash --> ledger_types::TxHash. -impl TryFrom<&tx_grpc::TxHash> for tx::TxHash { +/// Convert external::TxHash --> tx::TxHash. +impl TryFrom<&external::TxHash> for tx::TxHash { type Error = ConversionError; - fn try_from(value: &tx_grpc::TxHash) -> Result { + fn try_from(value: &external::TxHash) -> Result { let hash_bytes: &[u8] = value.get_hash(); tx::TxHash::try_from(hash_bytes).or(Err(ConversionError::ArrayCastError)) } @@ -152,6 +153,27 @@ impl TryFrom<&external::CurvePoint> for CurvePoint { } } +impl From<&CompressedCommitment> for external::CompressedRistretto { + fn from(source: &CompressedCommitment) -> Self { + let mut compressed_ristretto = external::CompressedRistretto::new(); + compressed_ristretto.set_data(source.point.as_bytes().to_vec()); + compressed_ristretto + } +} + +impl TryFrom<&external::CompressedRistretto> for CompressedCommitment { + type Error = ConversionError; + + fn try_from(source: &external::CompressedRistretto) -> Result { + let bytes: &[u8] = source.get_data(); + if bytes.len() != 32 { + return Err(ConversionError::ArrayCastError); + } + let point = CompressedRistretto::from_slice(bytes); + Ok(CompressedCommitment { point }) + } +} + /// Convert CurveScalar --> external::CurveScalar. impl From<&CurveScalar> for external::CurveScalar { fn from(other: &CurveScalar) -> Self { @@ -161,6 +183,16 @@ impl From<&CurveScalar> for external::CurveScalar { } } +/// Convert external::CurveScalar --> CurveScalar. +impl TryFrom<&external::CurveScalar> for CurveScalar { + type Error = ConversionError; + + fn try_from(source: &external::CurveScalar) -> Result { + let bytes: &[u8] = source.get_data(); + CurveScalar::try_from(bytes).map_err(|_| ConversionError::ArrayCastError) + } +} + /// Convert RistrettoPrivate --> external::CurveScalar. impl From<&RistrettoPrivate> for external::CurveScalar { fn from(other: &RistrettoPrivate) -> Self { @@ -180,6 +212,16 @@ impl From<&RistrettoPublic> for external::RistrettoPublic { } } +/// Convert external::RistrettoPublic --> RistrettoPublic. +impl TryFrom<&external::RistrettoPublic> for RistrettoPublic { + type Error = ConversionError; + + fn try_from(source: &external::RistrettoPublic) -> Result { + let bytes: &[u8] = source.get_data(); + RistrettoPublic::try_from(bytes).map_err(|_| ConversionError::ArrayCastError) + } +} + /// Convert CompressedRistrettoPublic --> external::RistrettoPublic impl From for external::RistrettoPublic { fn from(other: CompressedRistrettoPublic) -> Self { @@ -208,35 +250,6 @@ impl TryFrom<&external::RistrettoPrivate> for RistrettoPrivate { } } -/// Convert external::CurveScalar --> CurveScalar. -impl TryFrom<&external::CurveScalar> for CurveScalar { - type Error = ConversionError; - - fn try_from(source: &external::CurveScalar) -> Result { - let bytes: &[u8] = source.get_data(); - CurveScalar::try_from(bytes).map_err(|_| ConversionError::ArrayCastError) - } -} - -/// Convert KeyImage --> external::KeyImage. -impl From<&KeyImage> for external::KeyImage { - fn from(other: &KeyImage) -> Self { - let mut point = external::KeyImage::new(); - point.set_data(other.to_vec()); - point - } -} - -/// Convert external::KeyImage --> KeyImage. -impl TryFrom<&external::KeyImage> for KeyImage { - type Error = ConversionError; - - fn try_from(source: &external::KeyImage) -> Result { - let bytes: &[u8] = source.get_data(); - Ok(KeyImage::try_from(bytes)?) - } -} - /// Convert Ed25519Signature --> external::Ed25519Signature. impl From<&Ed25519Signature> for external::Ed25519Signature { fn from(src: &Ed25519Signature) -> Self { @@ -297,63 +310,64 @@ impl TryFrom<&blockchain::BlockSignature> for BlockSignature { } } -/// Convert KeyImage --> tx_grpc::KeyImage. -impl From<&KeyImage> for tx_grpc::KeyImage { +/// Convert KeyImage --> external::KeyImage. +impl From<&KeyImage> for external::KeyImage { fn from(other: &KeyImage) -> Self { - let mut key_image = tx_grpc::KeyImage::new(); - key_image.set_value(other.to_vec()); + let mut key_image = external::KeyImage::new(); + key_image.set_data(other.to_vec()); key_image } } -/// Convert tx_grpc::KeyImage --> KeyImage. -impl TryFrom<&tx_grpc::KeyImage> for KeyImage { +/// Convert external::KeyImage --> KeyImage. +impl TryFrom<&external::KeyImage> for KeyImage { type Error = ConversionError; - fn try_from(source: &tx_grpc::KeyImage) -> Result { - let bytes: &[u8] = source.get_value(); + fn try_from(source: &external::KeyImage) -> Result { + let bytes: &[u8] = source.get_data(); Ok(KeyImage::try_from(bytes)?) } } -/// Convert RedactedTx --> tx_grpc::RedactedTx. -impl From<&RedactedTx> for tx_grpc::RedactedTx { +/// Convert RedactedTx --> external::RedactedTx. +impl From<&RedactedTx> for external::RedactedTx { fn from(redacted_tx: &RedactedTx) -> Self { - let mut transaction = tx_grpc::RedactedTx::new(); + let mut transaction = external::RedactedTx::new(); //transaction.set_version(tx_stored.version as u32); - let tx_outs: Vec = redacted_tx + let tx_outs: Vec = redacted_tx .outputs .iter() - .map(tx_grpc::TxOut::from) + .map(external::TxOut::from) .collect(); - transaction.set_outs(RepeatedField::from_vec(tx_outs)); + transaction.set_outputs(RepeatedField::from_vec(tx_outs)); - let key_images: Vec = redacted_tx + let key_images: Vec = redacted_tx .key_images .iter() - .map(tx_grpc::KeyImage::from) + .map(external::KeyImage::from) .collect(); transaction.set_key_images(RepeatedField::from_vec(key_images)); transaction } } -/// Convert tx_grpc::RedactedTx --> transaction::RedactedTx -impl TryFrom<&tx_grpc::RedactedTx> for RedactedTx { +/// Convert external::RedactedTx --> transaction::RedactedTx +impl TryFrom<&external::RedactedTx> for RedactedTx { type Error = ConversionError; - fn try_from(source: &tx_grpc::RedactedTx) -> Result { - // Convert blockchain::TxOut --> ledger_types::TxOutStored. - let output_conversions: Result, ConversionError> = - source.get_outs().iter().map(tx::TxOut::try_from).collect(); - - let outputs = output_conversions?; + fn try_from(source: &external::RedactedTx) -> Result { + let mut outputs: Vec = Vec::new(); + for source_output in source.get_outputs() { + let tx_out = tx::TxOut::try_from(source_output)?; + outputs.push(tx_out); + } let mut key_images: Vec = Vec::with_capacity(source.get_key_images().len()); for source_key_image in source.get_key_images() { let key_image = KeyImage::try_from(source_key_image)?; key_images.push(key_image); } + let redacted_tx = RedactedTx::new(outputs, key_images); Ok(redacted_tx) } @@ -414,6 +428,8 @@ impl From<&tx::TxPrefix> for external::TxPrefix { tx_prefix.set_fee(source.fee); + tx_prefix.set_tombstone_block(source.tombstone_block); + tx_prefix } } @@ -439,6 +455,7 @@ impl TryFrom<&external::TxPrefix> for tx::TxPrefix { inputs, outputs, fee: source.get_fee(), + tombstone_block: source.get_tombstone_block(), }; Ok(tx_prefix) } @@ -448,12 +465,8 @@ impl TryFrom<&external::TxPrefix> for tx::TxPrefix { impl From<&tx::Tx> for external::Tx { fn from(source: &tx::Tx) -> Self { let mut tx = external::Tx::new(); - tx.set_prefix(external::TxPrefix::from(&source.prefix)); - tx.set_signature(external::RingCtSignature::from(&source.signature)); - tx.set_range_proofs(source.range_proofs.clone()); - tx.set_tombstone_block(source.tombstone_block); - + tx.set_signature(external::SignatureRctBulletproofs::from(&source.signature)); tx } } @@ -464,156 +477,139 @@ impl TryFrom<&external::Tx> for tx::Tx { fn try_from(source: &external::Tx) -> Result { let prefix = tx::TxPrefix::try_from(source.get_prefix())?; - let signature = SignatureRctFull::try_from(source.get_signature())?; - let range_proofs = source.get_range_proofs().to_vec(); - let tombstone_block = source.get_tombstone_block(); - - Ok(tx::Tx { - prefix, - signature, - range_proofs, - tombstone_block, + let signature = SignatureRctBulletproofs::try_from(source.get_signature())?; + Ok(tx::Tx { prefix, signature }) + } +} + +impl From<&RingMLSAG> for external::RingMLSAG { + fn from(source: &RingMLSAG) -> Self { + let mut ring_mlsag = external::RingMLSAG::new(); + ring_mlsag.set_c_zero(external::CurveScalar::from(&source.c_zero)); + let responses: Vec = source + .responses + .iter() + .map(external::CurveScalar::from) + .collect(); + ring_mlsag.set_responses(responses.into()); + ring_mlsag.set_key_image(external::KeyImage::from(&source.key_image)); + ring_mlsag + } +} + +impl TryFrom<&external::RingMLSAG> for RingMLSAG { + type Error = ConversionError; + + fn try_from(source: &external::RingMLSAG) -> Result { + let c_zero = CurveScalar::try_from(source.get_c_zero())?; + let mut responses: Vec = Vec::new(); + for response in source.get_responses() { + responses.push(CurveScalar::try_from(response)?); + } + let key_image = KeyImage::try_from(source.get_key_image())?; + + Ok(RingMLSAG { + c_zero, + responses, + key_image, }) } } -/// Convert Signature --> external::RingCtSignature. -impl From<&SignatureRctFull> for external::RingCtSignature { - fn from(source: &SignatureRctFull) -> Self { - let mut signature = external::RingCtSignature::new(); +impl From<&SignatureRctBulletproofs> for external::SignatureRctBulletproofs { + fn from(source: &SignatureRctBulletproofs) -> Self { + let mut signature = external::SignatureRctBulletproofs::new(); - let key_images: Vec = source - .key_images + let ring_signatures: Vec = source + .ring_signatures .iter() - .map(external::KeyImage::from) + .map(external::RingMLSAG::from) .collect(); - signature.set_key_images(key_images.into()); + signature.set_ring_signatures(ring_signatures.into()); - let challenge_responses: Vec = source - .challenge_responses + let pseudo_output_commitments: Vec = source + .pseudo_output_commitments .iter() - .map(|challenge_response| { - let mut ringct_challenge_response = external::RingCtChallengeResponse::new(); - - let response: Vec = challenge_response - .response - .iter() - .map(external::CurveScalar::from) - .collect(); - ringct_challenge_response.set_response(response.into()); - - ringct_challenge_response - }) + .map(external::CompressedRistretto::from) .collect(); - signature.set_challenge_responses(challenge_responses.into()); + signature.set_pseudo_output_commitments(pseudo_output_commitments.into()); - signature - .mut_challenge() - .set_data(source.challenge.as_bytes().to_vec()); + signature.set_range_proofs(source.range_proof_bytes.clone()); signature } } -/// Convert external::RingCtSignature --> Signature. -impl TryFrom<&external::RingCtSignature> for SignatureRctFull { +impl TryFrom<&external::SignatureRctBulletproofs> for SignatureRctBulletproofs { type Error = ConversionError; - fn try_from(source: &external::RingCtSignature) -> Result { - let mut key_images: Vec = Vec::new(); - for image in source.get_key_images() { - let key_image = - KeyImage::try_from(image.get_data()).map_err(|_e| ConversionError::Other)?; - key_images.push(key_image); + fn try_from(source: &external::SignatureRctBulletproofs) -> Result { + let mut ring_signatures: Vec = Vec::new(); + for ring_signature in source.get_ring_signatures() { + ring_signatures.push(RingMLSAG::try_from(ring_signature)?); } - let mut challenge_responses: Vec = Vec::new(); - for response in source.get_challenge_responses() { - let mut challenge_response: Vec = Vec::new(); - for scalar in response.get_response() { - let curve_scalar = CurveScalar::try_from(scalar.get_data()) - .map_err(|_e| ConversionError::Other)?; - - challenge_response.push(curve_scalar); - } - challenge_responses.push(ChallengeResponse { - response: challenge_response, - }); + let mut pseudo_output_commitments: Vec = Vec::new(); + for pseudo_output_commitment in source.get_pseudo_output_commitments() { + pseudo_output_commitments + .push(CompressedCommitment::try_from(pseudo_output_commitment)?); } - let challenge = CurveScalar::try_from(source.get_challenge().get_data()) - .map_err(|_e| ConversionError::Other)?; + let range_proof_bytes = source.get_range_proofs().to_vec(); - let tx_prefix = SignatureRctFull { - key_images, - challenge_responses, - challenge, - }; - Ok(tx_prefix) + Ok(SignatureRctBulletproofs { + ring_signatures, + pseudo_output_commitments, + range_proof_bytes, + }) } } -/// Convert tx::TxOut --> tx_grpc::TxOut. -impl From<&tx::TxOut> for tx_grpc::TxOut { - fn from(source: &tx::TxOut) -> Self { - let mut tx_out = tx_grpc::TxOut::new(); - let target_key_bytes: &[u8] = source.target_key.as_ref(); - tx_out.set_target_key(Vec::from(target_key_bytes)); - let public_key_bytes: &[u8] = source.public_key.as_ref(); - tx_out.set_public_key(Vec::from(public_key_bytes)); - let masked_value_bytes = source.amount.masked_value.as_bytes().to_vec(); - tx_out.set_masked_value(masked_value_bytes); - let masked_blinding_bytes = source.amount.masked_blinding.as_bytes().to_vec(); - tx_out.set_masked_blinding(masked_blinding_bytes); - tx_out.set_commitment(source.amount.commitment.to_bytes().to_vec()); - tx_out.set_e_account_hint(source.e_account_hint.as_ref().to_vec()); - tx_out +impl From<&CompressedCommitment> for external::CurvePoint { + fn from(source: &CompressedCommitment) -> Self { + let bytes = source.to_bytes().to_vec(); + let mut curve_point = external::CurvePoint::new(); + curve_point.set_data(bytes); + curve_point } } -/// Convert tx::TxOut --> external::TxOut. -impl From<&tx::TxOut> for external::TxOut { - fn from(source: &tx::TxOut) -> Self { - let mut tx_out = external::TxOut::new(); +impl TryFrom<&external::CurvePoint> for CompressedCommitment { + type Error = ConversionError; - let target_key_bytes: &[u8] = source.target_key.as_ref(); - tx_out - .mut_target_key() - .set_data(Vec::from(target_key_bytes)); + fn try_from(source: &external::CurvePoint) -> Result { + let bytes = source.get_data(); + let mut arr = [0u8; 32]; + if bytes.len() != arr.len() { + return Err(ConversionError::ArrayCastError); + } + arr.copy_from_slice(bytes); + CompressedCommitment::from_bytes(&arr).map_err(|_e| ConversionError::Other) + } +} - let public_key_bytes: &[u8] = source.public_key.as_ref(); - tx_out - .mut_public_key() - .set_data(Vec::from(public_key_bytes)); +impl From<&Amount> for external::Amount { + fn from(source: &Amount) -> Self { + let mut amount = external::Amount::new(); - let masked_value_bytes = source.amount.masked_value.as_bytes().to_vec(); - tx_out - .mut_amount() - .mut_masked_value() - .set_data(masked_value_bytes); - let masked_blinding_bytes = source.amount.masked_blinding.as_bytes().to_vec(); - tx_out - .mut_amount() - .mut_masked_blinding() - .set_data(masked_blinding_bytes); - tx_out - .mut_amount() - .mut_commitment() - .set_data(source.amount.commitment.to_bytes().to_vec()); - tx_out - .mut_e_account_hint() - .set_data(source.e_account_hint.as_ref().to_vec()); - tx_out + let commitment_bytes = source.commitment.to_bytes().to_vec(); + amount.mut_commitment().set_data(commitment_bytes); + + let masked_value_bytes = source.masked_value.as_bytes().to_vec(); + amount.mut_masked_value().set_data(masked_value_bytes); + + let masked_blinding_bytes = source.masked_blinding.as_bytes().to_vec(); + amount.mut_masked_blinding().set_data(masked_blinding_bytes); + + amount } } -/// Convert tx_grpc::TxOut --> tx::TxOut. -impl TryFrom<&tx_grpc::TxOut> for tx::TxOut { +impl TryFrom<&external::Amount> for Amount { type Error = ConversionError; - fn try_from(source: &tx_grpc::TxOut) -> Result { - let commitment = Commitment::try_from(source.get_commitment()) - .map_err(|_| ConversionError::KeyCastError)?; + fn try_from(source: &external::Amount) -> Result { + let commitment = CompressedCommitment::try_from(source.get_commitment())?; fn vec_to_curve_scalar(bytes: &[u8]) -> Result { if bytes.len() != 32 { @@ -625,12 +621,12 @@ impl TryFrom<&tx_grpc::TxOut> for tx::TxOut { }; let masked_value: CurveScalar = { - let bytes = source.get_masked_value(); + let bytes = source.get_masked_value().get_data(); vec_to_curve_scalar(bytes)? }; let masked_blinding: Blinding = { - let bytes = source.get_masked_blinding(); + let bytes = source.get_masked_blinding().get_data(); vec_to_curve_scalar(bytes)? }; @@ -640,24 +636,28 @@ impl TryFrom<&tx_grpc::TxOut> for tx::TxOut { masked_blinding, }; - let target_key_bytes: &[u8] = source.get_target_key(); - let target_key = RistrettoPublic::try_from(target_key_bytes) - .map_err(|_| ConversionError::KeyCastError)? - .into(); - let public_key_bytes: &[u8] = source.get_public_key(); - let public_key = RistrettoPublic::try_from(public_key_bytes) - .map_err(|_| ConversionError::KeyCastError)? - .into(); - let e_account_hint = EncryptedFogHint::try_from(source.get_e_account_hint()) - .map_err(|_| ConversionError::ArrayCastError)?; + Ok(amount) + } +} - let tx_out_stored = tx::TxOut { - amount, - target_key, - public_key, - e_account_hint, - }; - Ok(tx_out_stored) +/// Convert tx::TxOut --> external::TxOut. +impl From<&tx::TxOut> for external::TxOut { + fn from(source: &tx::TxOut) -> Self { + let mut tx_out = external::TxOut::new(); + + let amount = external::Amount::from(&source.amount); + tx_out.set_amount(amount); + + let target_key_bytes = source.target_key.as_bytes().to_vec(); + tx_out.mut_target_key().set_data(target_key_bytes); + + let public_key_bytes = source.public_key.as_bytes().to_vec(); + tx_out.mut_public_key().set_data(public_key_bytes); + + let hint_bytes = source.e_account_hint.as_ref().to_vec(); + tx_out.mut_e_account_hint().set_data(hint_bytes); + + tx_out } } @@ -666,52 +666,28 @@ impl TryFrom<&external::TxOut> for tx::TxOut { type Error = ConversionError; fn try_from(source: &external::TxOut) -> Result { - let commitment = Commitment::try_from(source.get_amount().get_commitment()) - .map_err(|_| ConversionError::KeyCastError)?; - - fn vec_to_curve_scalar(bytes: &[u8]) -> Result { - if bytes.len() != 32 { - return Err(ConversionError::Other); - } - let mut curve_bytes = [0u8; 32]; - curve_bytes.copy_from_slice(&bytes); - Ok(CurveScalar::from_bytes_mod_order(curve_bytes)) - }; - - let masked_value: CurveScalar = { - let bytes = source.get_amount().get_masked_value().get_data(); - vec_to_curve_scalar(bytes)? - }; - - let masked_blinding: Blinding = { - let bytes = source.get_amount().get_masked_blinding().get_data(); - vec_to_curve_scalar(bytes)? - }; - - let amount = Amount { - commitment, - masked_value, - masked_blinding, - }; + let amount = Amount::try_from(source.get_amount())?; let target_key_bytes: &[u8] = source.get_target_key().get_data(); - let target_key = RistrettoPublic::try_from(target_key_bytes) + let target_key: CompressedRistrettoPublic = RistrettoPublic::try_from(target_key_bytes) .map_err(|_| ConversionError::KeyCastError)? .into(); + let public_key_bytes: &[u8] = source.get_public_key().get_data(); - let public_key = RistrettoPublic::try_from(public_key_bytes) + let public_key: CompressedRistrettoPublic = RistrettoPublic::try_from(public_key_bytes) .map_err(|_| ConversionError::KeyCastError)? .into(); + let e_account_hint = EncryptedFogHint::try_from(source.get_e_account_hint().get_data()) .map_err(|_| ConversionError::ArrayCastError)?; - let tx_out_stored = tx::TxOut { + let tx_out = tx::TxOut { amount, target_key, public_key, e_account_hint, }; - Ok(tx_out_stored) + Ok(tx_out) } } @@ -906,19 +882,18 @@ pub fn block_num_to_s3block_path(block_index: transaction::BlockIndex) -> PathBu } #[cfg(test)] -mod tests { +mod conversion_tests { extern crate rand; use self::rand::{rngs::StdRng, SeedableRng}; use super::*; use curve25519_dalek::ristretto::RistrettoPoint; use keys::FromRandom; - use protobuf::Message; use transaction::{ account_keys::{AccountKey, PublicAddress}, onetime_keys::recover_onetime_private_key, ring_signature::Blinding, - tx::{TxOut, TxOutMembershipProof}, + tx::{Tx, TxOut, TxOutMembershipProof}, }; use transaction_std::*; @@ -1007,17 +982,17 @@ mod tests { } #[test] - // ledger_types::TxHash --> blockchain::TxHash. + // tx::TxHash --> external::TxHash. fn test_tx_hash_from() { let source: tx::TxHash = tx::TxHash::from([7u8; 32]); - let converted = tx_grpc::TxHash::from(&source); + let converted = external::TxHash::from(&source); assert_eq!(converted.hash.as_slice(), source.as_bytes()); } #[test] - // blockchain::TxHash --> ledger_types::TxHash + // blockchain::TxHash --> tx::TxHash fn test_tx_hash_try_from() { - let mut source = tx_grpc::TxHash::new(); + let mut source = external::TxHash::new(); source.set_hash([7u8; 32].to_vec()); let converted = tx::TxHash::try_from(&source).unwrap(); assert_eq!(converted.0, [7u8; 32]); @@ -1026,7 +1001,7 @@ mod tests { #[test] // Unmarshalling too many bytes into a TxHash should produce an error. fn test_tx_hash_try_from_too_many_bytes() { - let mut source = tx_grpc::TxHash::new(); + let mut source = external::TxHash::new(); source.set_hash([7u8; 99].to_vec()); // Too many bytes. assert!(tx::TxHash::try_from(&source).is_err()); } @@ -1034,7 +1009,7 @@ mod tests { #[test] // Unmarshalling too few bytes into a TxHash should produce an error. fn test_tx_hash_try_from_too_few_bytes() { - let mut source = tx_grpc::TxHash::new(); + let mut source = external::TxHash::new(); source.set_hash([7u8; 3].to_vec()); // Too few bytes. assert!(tx::TxHash::try_from(&source).is_err()); } @@ -1093,11 +1068,11 @@ mod tests { } #[test] - // tx::TxOutStored -> blockchain::TxOut + // tx::TxOut -> blockchain::TxOut --> tx::TxOut fn test_tx_out_from_tx_out_stored() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let tx_out_stored = tx::TxOut { + let source = tx::TxOut { amount: Amount::new( 1u64 << 13, Blinding::from(9u64), @@ -1109,88 +1084,79 @@ mod tests { e_account_hint: (&[0u8; 128]).into(), }; - let tx_out = tx_grpc::TxOut::from(&tx_out_stored); - let tx_out_ledger = tx::TxOut::try_from(&tx_out).unwrap(); + let converted = external::TxOut::from(&source); - assert_eq!(tx_out_stored.amount, tx_out_ledger.amount); - - assert_eq!( - tx_out.target_key, - tx_out_stored.target_key.to_bytes().to_vec() - ); - assert_eq!( - tx_out.e_account_hint, - (&tx_out_stored.e_account_hint.to_bytes()[..]).to_vec() - ); + let recovered_tx_out = tx::TxOut::try_from(&converted).unwrap(); + assert_eq!(source.amount, recovered_tx_out.amount); } #[test] - // A RedactedTx that contains zero outputs or key images. - fn test_transaction_from_tx_stored_no_outs() { - let redacted_tx = RedactedTx::new(vec![], vec![]); - let transaction = tx_grpc::RedactedTx::from(&redacted_tx); - assert_eq!(transaction.outs.len(), 0); + // Empty RedactedTx --> external::RedactedTx + fn test_empty_redacted_tx() { + let source = RedactedTx::new(vec![], vec![]); + let redacted_tx = external::RedactedTx::from(&source); + assert_eq!(redacted_tx.outputs.len(), 0); } #[test] - // RedactedTx -> blockchain::Transaction + // RedactedTx -> external::RedactedTx fn test_transaction_from_tx_stored() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let tx_out_a = tx::TxOut { - amount: Amount::new( - 1u64 << 17, - Blinding::from(9u64), - &RistrettoPublic::from_random(&mut rng), - ) - .unwrap(), - target_key: RistrettoPublic::from_random(&mut rng).into(), - public_key: RistrettoPublic::from_random(&mut rng).into(), - e_account_hint: (&[0u8; 128]).into(), + let source: RedactedTx = { + let tx_out_a = tx::TxOut { + amount: Amount::new( + 1u64 << 17, + Blinding::from(9u64), + &RistrettoPublic::from_random(&mut rng), + ) + .unwrap(), + target_key: RistrettoPublic::from_random(&mut rng).into(), + public_key: RistrettoPublic::from_random(&mut rng).into(), + e_account_hint: (&[0u8; 128]).into(), + }; + + let tx_out_b = tx::TxOut { + amount: Amount::new( + 1u64 << 18, + Blinding::from(9u64), + &RistrettoPublic::from_random(&mut rng), + ) + .unwrap(), + target_key: RistrettoPublic::from_random(&mut rng).into(), + public_key: RistrettoPublic::from_random(&mut rng).into(), + e_account_hint: (&[0u8; 128]).into(), + }; + + let outputs = vec![tx_out_a, tx_out_b]; + let key_images: Vec = vec![KeyImage::from(RistrettoPoint::random(&mut rng))]; + RedactedTx::new(outputs, key_images) }; - let tx_out_b = tx::TxOut { - amount: Amount::new( - 1u64 << 18, - Blinding::from(9u64), - &RistrettoPublic::from_random(&mut rng), - ) - .unwrap(), - target_key: RistrettoPublic::from_random(&mut rng).into(), - public_key: RistrettoPublic::from_random(&mut rng).into(), - e_account_hint: (&[0u8; 128]).into(), - }; - - let outputs = vec![tx_out_a, tx_out_b]; - let key_images: Vec = vec![KeyImage::from(RistrettoPoint::random(&mut rng))]; - let redacted_tx = RedactedTx::new(outputs, key_images); - - let transaction = tx_grpc::RedactedTx::from(&redacted_tx); - assert_eq!(transaction.outs.len(), 2); - assert_eq!(transaction.key_images.len(), 1); + let redacted_tx = external::RedactedTx::from(&source); + assert_eq!(redacted_tx.outputs.len(), 2); + assert_eq!(redacted_tx.key_images.len(), 1); } #[test] + // KeyImage --> external::KeyImage fn test_key_image_from() { let source: KeyImage = KeyImage::from(7); - let converted = tx_grpc::KeyImage::from(&source); - assert_eq!(converted.value, source.to_vec()); + let converted = external::KeyImage::from(&source); + assert_eq!(converted.data, source.to_vec()); } #[test] + // external::keyImage --> KeyImage fn test_key_image_try_from() { - let key_image = KeyImage::from(11); - let mut source = tx_grpc::KeyImage::new(); - source.set_value(key_image.to_vec()); + let mut source = external::KeyImage::new(); + source.set_data(KeyImage::from(11).to_vec()); - match KeyImage::try_from(&source) { - Ok(image) => { - assert_eq!(image.to_vec(), source.take_value()); - } - Err(_e) => { - panic!(); - } - } + // try_from should succeed. + let key_image = KeyImage::try_from(&source).unwrap(); + + // key_image should have the correct value. + assert_eq!(key_image, KeyImage::from(11)); } #[test] @@ -1199,8 +1165,8 @@ mod tests { fn test_key_image_try_from_conversion_errors() { // Helper function asserts that a ConversionError::ArrayCastError is produced. fn expects_array_cast_error(bytes: &[u8]) { - let mut source = tx_grpc::KeyImage::new(); - source.set_value(bytes.to_vec()); + let mut source = external::KeyImage::new(); + source.set_data(bytes.to_vec()); match KeyImage::try_from(&source).unwrap_err() { ConversionError::ArrayCastError => {} // Expected outcome. _ => panic!(), @@ -1245,8 +1211,8 @@ mod tests { } #[test] - /// Check that external.proto is synced with our protobufy structs - fn test_external_proto() { + /// Tx --> externalTx --> Tx should be the identity function. + fn test_convert_tx() { // Generate a Tx to test with. This is copied from // transaction_builder.rs::test_simple_transaction let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); @@ -1303,147 +1269,19 @@ mod tests { let tx = transaction_builder.build(&mut rng).unwrap(); - // Serialize the tx into bytes using Prost - let tx_prost_bytes = mcserial::encode(&tx); - - // Deserialize using rust-protobuf external.proto data type. - let protobuf_tx = - protobuf::parse_from_bytes::(&tx_prost_bytes).unwrap(); - - // Serialize into bytes using protobuf - let tx_protobuf_bytes = protobuf_tx.write_to_bytes().unwrap(); - - // Make sure our bytes are not all zeros. - assert!(!tx_prost_bytes.iter().all(|b| *b == 0)); - - // Compare bytes. If this fails it means external.proto is no longer in sync with the #[prost()] - // decorations. - assert_eq!(tx_prost_bytes, tx_protobuf_bytes); - - // Compare some select fields. - assert_eq!( - tx.prefix.inputs[0].ring[0].amount.commitment.to_bytes(), - &protobuf_tx.get_prefix().get_inputs()[0].get_ring()[0] - .get_amount() - .get_commitment() - .get_data()[..] - ); - - assert_eq!( - tx.prefix.inputs[0].ring[0].amount.masked_value.to_bytes(), - &protobuf_tx.get_prefix().get_inputs()[0].get_ring()[0] - .get_amount() - .get_masked_value() - .get_data()[..] - ); - - assert_eq!( - tx.prefix.inputs[0].ring[0] - .amount - .masked_blinding - .to_bytes(), - &protobuf_tx.get_prefix().get_inputs()[0].get_ring()[0] - .get_amount() - .get_masked_blinding() - .get_data()[..] - ); - - assert_eq!( - tx.key_images()[0].to_bytes(), - &protobuf_tx.signature.get_ref().key_images[0].data[..] - ); - - assert_eq!( - tx.prefix.outputs[0].amount.commitment.to_bytes(), - &protobuf_tx.prefix.get_ref().outputs[0] - .amount - .get_ref() - .commitment - .get_ref() - .data[..] - ); - - assert_eq!( - tx.prefix.outputs[0].amount.masked_value.to_bytes(), - &protobuf_tx.prefix.get_ref().outputs[0] - .amount - .get_ref() - .masked_value - .get_ref() - .data[..] - ); - - assert_eq!( - tx.prefix.outputs[0].amount.masked_blinding.to_bytes(), - &protobuf_tx.prefix.get_ref().outputs[0] - .amount - .get_ref() - .masked_blinding - .get_ref() - .data[..] - ); - - assert_eq!( - tx.prefix.outputs[0].target_key.to_bytes(), - &protobuf_tx.prefix.get_ref().outputs[0] - .target_key - .get_ref() - .data[..] - ); - - assert_eq!( - tx.prefix.outputs[0].public_key.to_bytes(), - &protobuf_tx.prefix.get_ref().outputs[0] - .public_key - .get_ref() - .data[..] - ); - - assert_eq!( - tx.prefix.outputs[0].e_account_hint.as_ref(), - &protobuf_tx.prefix.get_ref().outputs[0] - .e_account_hint - .get_ref() - .data[..] - ); - - assert_eq!(tx.prefix.fee, protobuf_tx.prefix.get_ref().fee); - - assert_eq!(tx.range_proofs, &protobuf_tx.range_proofs[..]); - - assert_eq!(tx.tombstone_block, protobuf_tx.tombstone_block); - - let external_ring: Vec = ring.iter().map(external::TxOut::from).collect(); - - let ledger_ring: Vec = external_ring - .iter() - .map(|tx_out| tx::TxOut::try_from(tx_out).unwrap()) - .collect(); - - assert_eq!(ring, ledger_ring); - - let input = tx.prefix.inputs[0].clone(); - let external_input = external::TxIn::from(&input); - let ledger_input = tx::TxIn::try_from(&external_input).unwrap(); - - assert_eq!(input, ledger_input); - - let prefix = tx.prefix.clone(); - let external_prefix = external::TxPrefix::from(&prefix); - let ledger_prefix = tx::TxPrefix::try_from(&external_prefix).unwrap(); - - assert_eq!(prefix, ledger_prefix); - - let signature = tx.signature.clone(); - let external_signature = external::RingCtSignature::from(&signature); - let ledger_signature = SignatureRctFull::try_from(&external_signature).unwrap(); - - assert_eq!(signature, ledger_signature); - - let external_tx = external::Tx::from(&tx); - let ledger_tx = tx::Tx::try_from(&external_tx).unwrap(); + // decode(encode(tx)) should be the identity function. + { + let bytes = mcserial::encode(&tx); + let recovered_tx = mcserial::decode(&bytes).unwrap(); + assert_eq!(tx, recovered_tx); + } - assert_eq!(tx, ledger_tx); + // Converting transaction::Tx -> external::Tx -> transaction::Tx should be the identity function. + { + let external_tx: external::Tx = external::Tx::from(&tx); + let recovered_tx: Tx = Tx::try_from(&external_tx).unwrap(); + assert_eq!(tx, recovered_tx); + } } #[test] diff --git a/consensus/api/src/lib.rs b/consensus/api/src/lib.rs index e16d8b327e..1e037cbddc 100644 --- a/consensus/api/src/lib.rs +++ b/consensus/api/src/lib.rs @@ -2,23 +2,22 @@ //! MobileCoin gRPC API. -pub mod blockchain; -pub mod blockchain_grpc; -pub mod consensus_client; -pub mod consensus_client_grpc; -pub mod consensus_common; -pub mod consensus_peer; -pub mod consensus_peer_grpc; -pub mod conversions; -pub mod external; -pub mod ledger_enclave_server; -pub mod ledger_enclave_server_grpc; -pub mod transaction; +mod autogenerated_code { + // Expose proto data types from included third-party/external proto files. + pub use attest_api::attest; -pub mod empty { 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 attest_api::attest; +pub mod conversions; +pub use autogenerated_code::*; pub use conversions::ConversionError; diff --git a/consensus/enclave/api/README.md b/consensus/enclave/api/README.md index 0cc8300700..a1eea68636 100644 --- a/consensus/enclave/api/README.md +++ b/consensus/enclave/api/README.md @@ -1,6 +1,6 @@ -# Mobilenode Enclave API +# MobileCoin Enclave API -This crate contains the untrusted-facing APIs for a Mobilenode enclave. The goal is to provide an API to the enclave that interacts as a special-case of the more commonly understood object remoting. In particular, there should be an "untrusted" implementation of these APIs which lives in the node, and a "trusted" implementation of these APIs which lives in the enclave. +This crate contains the untrusted-facing APIs for a MobileCoin enclave. The goal is to provide an API to the enclave that interacts as a special-case of the more commonly understood object remoting. In particular, there should be an "untrusted" implementation of these APIs which lives in the node, and a "trusted" implementation of these APIs which lives in the enclave. This particular use of remoting, where we simply want to cross a trust boundary that lives within the same process on the same machine, is significantly simplified from the typically-maligned *networked* remoting, and is therefore significantly less insane than most remoting frameworks. In this model, the typical workflow is something akin to this: diff --git a/consensus/enclave/api/src/lib.rs b/consensus/enclave/api/src/lib.rs index 6de14fc0a8..45982c2da1 100644 --- a/consensus/enclave/api/src/lib.rs +++ b/consensus/enclave/api/src/lib.rs @@ -85,8 +85,8 @@ impl From<&Tx> for WellFormedTxContext { Self { tx_hash: tx.tx_hash(), fee: tx.prefix.fee, - tombstone_block: tx.tombstone_block, - key_images: tx.key_images().clone(), + tombstone_block: tx.prefix.tombstone_block, + key_images: tx.key_images(), highest_indices: tx.get_membership_proof_highest_indices(), } } diff --git a/consensus/enclave/impl/README.md b/consensus/enclave/impl/README.md index 7bf198fc93..40f31d5703 100644 --- a/consensus/enclave/impl/README.md +++ b/consensus/enclave/impl/README.md @@ -1,4 +1,4 @@ -# MobileNode Enclave API Implementation +# MobileCoin Enclave API Implementation This is the in-enclave implementation of the traits defined in `enclave_api`. In particular, it provides the `ReportingEnclave` and `PeeringEnclave` structs, which implement the inside-the-enclave version of the `ReportableEnclave` and `PeerableEnclave` structures. diff --git a/consensus/enclave/impl/src/lib.rs b/consensus/enclave/impl/src/lib.rs index 4bae089536..cd56c6883f 100644 --- a/consensus/enclave/impl/src/lib.rs +++ b/consensus/enclave/impl/src/lib.rs @@ -238,7 +238,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { let tx_hash = tx.tx_hash(); let highest_indices = tx.get_membership_proof_highest_indices(); - let key_images: Vec = tx.key_images().clone(); + let key_images: Vec = tx.key_images(); Ok(TxContext { locally_encrypted_tx, @@ -268,7 +268,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { let locally_encrypted_tx = maybe_locally_encrypted_tx?; let tx_hash = tx.tx_hash(); let highest_indices = tx.get_membership_proof_highest_indices(); - let key_images: Vec = tx.key_images().clone(); + let key_images: Vec = tx.key_images(); Ok(TxContext { locally_encrypted_tx, @@ -415,13 +415,13 @@ impl ConsensusEnclave for SgxConsensusEnclave { let mut used_key_images = BTreeSet::default(); for tx in &transactions { for key_image in tx.key_images() { - if used_key_images.contains(key_image) { + if used_key_images.contains(&key_image) { return Err(Error::RedactTxs(format!( "Duplicate key image: {:?}", key_image ))); } - used_key_images.insert(key_image.clone()); + used_key_images.insert(key_image); } } @@ -601,11 +601,11 @@ mod tests { // Check that the context we got back is correct. assert_eq!(well_formed_tx_context.tx_hash(), &tx.tx_hash()); assert_eq!(well_formed_tx_context.fee(), tx.prefix.fee); - assert_eq!(well_formed_tx_context.tombstone_block(), tx.tombstone_block); assert_eq!( - well_formed_tx_context.key_images(), - &tx.key_images().into_iter().cloned().collect::>() + well_formed_tx_context.tombstone_block(), + tx.prefix.tombstone_block ); + assert_eq!(*well_formed_tx_context.key_images(), tx.key_images()); // All three tx representations should be different. assert_ne!(tx_bytes, locally_encrypted_tx.0); @@ -861,7 +861,7 @@ mod tests { let num_transactions = 5; let recipient = AccountKey::random(&mut rng); - // The first block contains a single transaction with MIN_RING_SIZE outputs. + // The first block contains a single transaction with RING_SIZE outputs. let block_zero_transactions = ledger.get_transactions_by_block(0).unwrap(); let block_zero_redacted_tx = block_zero_transactions.get(0).unwrap(); @@ -880,10 +880,20 @@ mod tests { new_transactions.push(tx); } - // Create a double-spend by duplicating one of the transactions. - let mut duplicate_spend = new_transactions[0].clone(); - duplicate_spend.tombstone_block += 1; // Ensure txs have a different hash - new_transactions.push(duplicate_spend); + // Create another transaction that spends the zero^th output in block zero. + let double_spend = { + let tx_out = &block_zero_redacted_tx.outputs[0]; + + create_transaction( + &mut ledger, + tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ) + }; + new_transactions.push(double_spend); // Create WellFormedEncryptedTxs + proofs let well_formed_encrypted_txs_with_proofs: Vec<_> = new_transactions @@ -936,7 +946,7 @@ mod tests { let num_transactions = 6; let recipient = AccountKey::random(&mut rng); - // The first block contains a single transaction with MIN_RING_SIZE outputs. + // The first block contains a single transaction with RING_SIZE outputs. let block_zero_transactions = ledger.get_transactions_by_block(0).unwrap(); let block_zero_redacted_tx = block_zero_transactions.get(0).unwrap(); diff --git a/consensus/enclave/mock/src/lib.rs b/consensus/enclave/mock/src/lib.rs index c8ab413a4c..0a4ff50860 100644 --- a/consensus/enclave/mock/src/lib.rs +++ b/consensus/enclave/mock/src/lib.rs @@ -43,7 +43,7 @@ impl ConsensusServiceMockEnclave { let locally_encrypted_tx = LocallyEncryptedTx(mcserial::encode(tx)); let tx_hash = tx.tx_hash(); let highest_indices = tx.get_membership_proof_highest_indices(); - let key_images: Vec = tx.key_images().clone(); + let key_images: Vec = tx.key_images(); TxContext { locally_encrypted_tx, diff --git a/consensus/enclave/trusted/Cargo.lock b/consensus/enclave/trusted/Cargo.lock index 012fc2378f..0bcf11b143 100644 --- a/consensus/enclave/trusted/Cargo.lock +++ b/consensus/enclave/trusted/Cargo.lock @@ -18,17 +18,6 @@ dependencies = [ "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "aes-ctr" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "aes-soft 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "aesni 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ctr 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "aes-gcm" version = "0.3.0" @@ -58,7 +47,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -508,15 +496,6 @@ dependencies = [ "subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "ctr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "curve25519-dalek" version = "2.0.0" @@ -560,17 +539,6 @@ dependencies = [ "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "ecies" -version = "0.1.0" -dependencies = [ - "aes-ctr 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "keys 0.1.0", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "ed25519" version = "1.0.0-pre.4" @@ -841,6 +809,19 @@ dependencies = [ "cmake 0.1.42 (git+https://github.com/mobilecoinofficial/cmake-rs?tag=0.1.42.rev7)", ] +[[package]] +name = "mc-crypto-box" +version = "0.1.0" +dependencies = [ + "aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "aes-gcm 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "blake2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "keys 0.1.0", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "mc-encodings" version = "0.1.0" @@ -1340,14 +1321,6 @@ name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "stream-cipher" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "strsim" version = "0.8.0" @@ -1432,6 +1405,7 @@ dependencies = [ name = "transaction" version = "0.1.0" dependencies = [ + "aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "blake2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "bs58 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "bulletproofs 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1442,13 +1416,13 @@ dependencies = [ "curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "digestible 0.1.0", - "ecies 0.1.0", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", "hex_fmt 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "keys 0.1.0", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mc-crypto-box 0.1.0", "mcrand 1.0.0", "mcserial 0.1.0", "merlin 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1616,7 +1590,6 @@ dependencies = [ [metadata] "checksum aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4cf01b9b56e767bb57b94ebf91a58b338002963785cdd7013e21c0d4679471e4" "checksum aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "54eb1d8fe354e5fc611daf4f2ea97dd45a765f4f1e4512306ec183ae2e8f20c9" -"checksum aes-ctr 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d2e5b0458ea3beae0d1d8c0f3946564f8e10f90646cf78c06b4351052058d1ee" "checksum aes-gcm 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4001f31800fc9b8c774728f82c7348f4f91474afd38c12a0e7dfa8303ae2dbd6" "checksum aes-soft 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cfd7e7ae3f9a1fb5c03b389fc6bb9a51400d0c13053f0dca698c832bfd893a0d" "checksum aesni 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100" @@ -1651,7 +1624,6 @@ dependencies = [ "checksum cmake 0.1.42 (git+https://github.com/mobilecoinofficial/cmake-rs?tag=0.1.42.rev7)" = "" "checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" "checksum crypto-mac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" -"checksum ctr 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "022cd691704491df67d25d006fe8eca083098253c4d43516c2206479c58c6736" "checksum curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839" "checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" "checksum ed25519 1.0.0-pre.4 (registry+https://github.com/rust-lang/crates.io-index)" = "01a416ad364359365b649fb28d7a6fab7f998d13282182241134beb014a9a378" @@ -1724,7 +1696,6 @@ dependencies = [ "checksum slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cc9c640a4adbfbcc11ffb95efe5aa7af7309e002adab54b185507dbf2377b99" "checksum smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" "checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -"checksum stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" "checksum subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941" diff --git a/consensus/enclave/trusted/src/lib.rs b/consensus/enclave/trusted/src/lib.rs index 286e72caca..cd31860aa2 100644 --- a/consensus/enclave/trusted/src/lib.rs +++ b/consensus/enclave/trusted/src/lib.rs @@ -114,7 +114,7 @@ pub extern "C" fn mobileenclave_call( ) -> sgx_status_t { if inbuf.is_null() || outbuf.is_null() - || outbuf.is_null() + || outbuf_used.is_null() || outbuf_retry_id.is_null() || unsafe { sgx_is_outside_enclave(inbuf as *const c_void, inbuf_len) } != 1 || unsafe { sgx_is_outside_enclave(outbuf as *const c_void, outbuf_len) } != 1 diff --git a/consensus/scp/play/README.md b/consensus/scp/play/README.md index c9e14fb8ff..cef706ee06 100644 --- a/consensus/scp/play/README.md +++ b/consensus/scp/play/README.md @@ -14,7 +14,7 @@ Notes: ## Usage with a Jenkins cloud deployed network -1. Log files are stored in `/home/deploybot/scp-debug-dump/`, e.g. `/home/deploybot/scp-debug-dump/node3.alpha.mobilecoin.com:8443/`. -1. You will need to SSH into the machine (as the `mobilecoin` user), and grab the logs: `sudo tar -czvf /home/mobilecoin/scp.tgz -C /home/deploybot/scp-debug-dump/ .` -1. From your machine, scp the files: `scp mobilecoin@node3.alpha.mobilecoin.com:~/scp.tgz .` -1. Extract the archive and run `scp_play` (inside `public/`): `MC_LOG=trace cargo run -p scp_play -- --scp-debug-dump /tmp/node3.alpha.mobilecoin.com:8443/` +1. Log files are stored in `$HOME/scp-debug-dump/`, e.g. `$HOME/scp-debug-dump/node3.test.mobilecoin.com:8443/`. +1. You will need to SSH into the machine (as the `mobilecoin` user), and grab the logs: `sudo tar -czvf /home/mobilecoin/scp.tgz -C $HOME/scp-debug-dump/ .` +1. From your machine, scp the files: `scp mobilecoin@node3.test.mobilecoin.com:~/scp.tgz .` +1. Extract the archive and run `scp_play` (inside `public/`): `MC_LOG=trace cargo run -p scp_play -- --scp-debug-dump /tmp/node3.test.mobilecoin.com:8443/` diff --git a/consensus/scp/src/lib.rs b/consensus/scp/src/lib.rs index 32dc42c949..b4b638a057 100644 --- a/consensus/scp/src/lib.rs +++ b/consensus/scp/src/lib.rs @@ -3,7 +3,6 @@ #![feature(external_doc)] #![doc(include = "../README.md")] #![allow(non_snake_case)] -#![allow(unused_attributes)] #![deny(missing_docs)] pub mod core_types; @@ -13,6 +12,7 @@ pub mod predicates; pub mod quorum_set; pub mod scp_log; pub mod slot; +pub mod test_utils; mod utils; #[doc(inline)] @@ -22,9 +22,3 @@ pub use self::{ node::{Node, ScpNode}, quorum_set::{QuorumSet, QuorumSetMember}, }; - -#[cfg(test)] -mod tests; - -#[cfg(any(test, feature = "test_utils"))] -pub mod test_utils; diff --git a/consensus/scp/src/node.rs b/consensus/scp/src/node.rs index a2675c6ac8..175a1e30ff 100644 --- a/consensus/scp/src/node.rs +++ b/consensus/scp/src/node.rs @@ -3,7 +3,7 @@ //! A node determines whether transactions are valid, and participates in voting with the members of its quorum set. use crate::{ core_types::{CombineFn, SlotIndex, ValidityFn, Value}, - msg::{ExternalizePayload, Msg, NominatePayload, Topic}, + msg::{ExternalizePayload, Msg, Topic}, quorum_set::QuorumSet, slot::{Slot, SlotMetrics}, }; @@ -101,6 +101,33 @@ impl Node { // Return slot. self.pending.get_mut(&slot_index).unwrap() } + + fn externalize( + &mut self, + slot_index: SlotIndex, + payload: &ExternalizePayload, + ) -> Result<(), String> { + // Check for invalid values. This should be redundant, but may be helpful during development. + let mut externalized_invalid_values = false; + for value in &payload.C.X { + if let Err(e) = (self.validity_fn)(value) { + externalized_invalid_values = true; + log::error!( + self.logger, + "Slot {} externalized invalid value: {:?}, {}", + slot_index, + value, + e + ); + } + } + if externalized_invalid_values { + return Err("Slot Externalized invalid values.".to_string()); + } + + self.externalized.put(slot_index, payload.clone()); + Ok(()) + } } /// A node capable of participating in SCP. @@ -171,19 +198,31 @@ impl ScpNode for Node return Ok(None); } - self.handle(&Msg::new( - self.ID.clone(), - self.Q.clone(), - slot_index, - Topic::Nominate(NominatePayload { - X: valid_values, - Y: Default::default(), - }), - )) + let slot = self.get_or_create_pending_slot(slot_index); + let outbound = slot.propose_values(&valid_values)?; + + match &outbound { + None => Ok(None), + Some(msg) => { + if let Topic::Externalize(ext_payload) = &msg.topic { + self.externalize(msg.slot_index, ext_payload)?; + } + Ok(outbound) + } + } } /// Handle incoming message from the network. fn handle(&mut self, msg: &Msg) -> Result>, String> { + if msg.sender_id == self.ID { + log::error!( + self.logger, + "node.handle received message from self: {:?}", + msg + ); + return Ok(None); + } + // Log an error if another node Externalizes different values. if let Topic::Externalize(received_externalized_payload) = &msg.topic { if let Some(our_externalized_payload) = self.externalized.get(&msg.slot_index) { @@ -204,24 +243,19 @@ impl ScpNode for Node } } - // If the node messaged itself, it means someone called `.nominate()`. We always forward - // those messages down to the slot. If the message came from anywhere else, we'd only pass - // it down if we haven't seen it before. - if self.ID != msg.sender_id { - // Calculate message hash. - let serialized_msg = mcserial::serialize(&msg).expect("failed serializing msg"); - let msg_hash = fast_hash(&serialized_msg); - - // If we've already seen this message, we don't need to do anything. - // We use `get()` instead of `contains()` to update LRU state. - if self.seen_msg_hashes.get(&msg_hash).is_some() { - return Ok(None); - } + // Calculate message hash. + let serialized_msg = mcserial::serialize(&msg).expect("failed serializing msg"); + let msg_hash = fast_hash(&serialized_msg); - // Store message so it doesn't get processed again. - self.seen_msg_hashes.put(msg_hash, ()); + // If we've already seen this message, we don't need to do anything. + // We use `get()` instead of `contains()` to update LRU state. + if self.seen_msg_hashes.get(&msg_hash).is_some() { + return Ok(None); } + // Store message so it doesn't get processed again. + self.seen_msg_hashes.put(msg_hash, ()); + // Process message using the Slot. let slot = self.get_or_create_pending_slot(msg.slot_index); let outbound = slot.handle(msg)?; @@ -230,27 +264,8 @@ impl ScpNode for Node None => Ok(None), Some(msg) => { if let Topic::Externalize(ext_payload) = &msg.topic { - let invalid_ext_values: Vec<(&V, String)> = ext_payload - .C - .X - .iter() - .map(|val| (val, (self.validity_fn)(val))) - .filter(|(_val, result)| result.is_err()) - .map(|(val, result)| (val, result.unwrap_err().to_string())) - .collect(); - - if !invalid_ext_values.is_empty() { - let err = format!( - "Slot {} externalized invalid values! {:?}", - msg.slot_index, invalid_ext_values - ); - log::error!(self.logger, "{}", err); - return Err(err); - } else { - self.externalized.put(msg.slot_index, ext_payload.clone()); - } + self.externalize(msg.slot_index, ext_payload)?; } - Ok(outbound) } } diff --git a/consensus/scp/src/slot.rs b/consensus/scp/src/slot.rs index ade4f01309..1dcce3d08f 100644 --- a/consensus/scp/src/slot.rs +++ b/consensus/scp/src/slot.rs @@ -57,6 +57,9 @@ pub struct Slot { /// Map of Node ID -> highest message from each node, including the local node. M: HashMap>, + /// Set of values that have been proposed, but not yet voted for. + W: BTreeSet, + /// Set of values we have voted to nominate. X: BTreeSet, @@ -166,6 +169,7 @@ impl Slot { node_id, quorum_set, M: HashMap::default(), + W: BTreeSet::default(), X: BTreeSet::default(), Y: BTreeSet::default(), Z: BTreeSet::new(), @@ -285,34 +289,35 @@ impl Slot { if timeout_occurred { if let Some(emitted) = self.out_msg() { - if let Some(previous_emitted) = &self.last_sent_msg { - if emitted != *previous_emitted { - // A new emitted message. - self.last_sent_msg = Some(emitted.clone()); - self.M.insert(emitted.sender_id.clone(), emitted.clone()); - msgs.push(emitted); - } else { - // Ignoring duplicate outgoing messages. - } - } else { - // The first emitted message. - self.last_sent_msg = Some(emitted.clone()); - self.M.insert(emitted.sender_id.clone(), emitted.clone()); - msgs.push(emitted); - } - } else { - // No message to emit yet. + msgs.push(emitted); } } msgs } - /// Handles an incoming message from a peer, or from this node. + /// Propose values for this node to nominate. + pub fn propose_values(&mut self, values: &BTreeSet) -> Result>, String> { + // Only accept values during the Nominate phase and if no other values have been confirmed nominated. + if !(self.phase == Phase::NominatePrepare && self.Z.is_empty()) { + return Ok(self.out_msg()); + } + + // Reject invalid values. + for value in values { + self.is_valid(value)?; + } + + self.W.extend(values.iter().cloned()); + self.do_nominate_phase(); + self.do_ballot_protocol(); + Ok(self.out_msg()) + } + + /// Handles an incoming message from a peer. /// /// Returns: - /// * Ok(out_msg) - `out_msg` is a new outgoing message from this node. - /// * Ok(None) - This node may not yet emit a new message. + /// * Ok(out_msg) - `out_msg` is an outgoing message from this node, if any. /// * Err(e) - Something went wrong while processing `msg`. pub fn handle(&mut self, msg: &Msg) -> Result>, String> { // Reject messages for other slots. @@ -330,15 +335,10 @@ impl Slot { // TODO: Reject messages with incorrectly ordered values. - // Special case for nominate messages from self. - if msg.sender_id == self.node_id && self.phase != Phase::NominatePrepare { - return Ok(None); - } - // Ignore the message if it not higher than a previous message from the same peer. if let Some(existing_msg) = self.M.get(&msg.sender_id) { if msg.topic <= existing_msg.topic { - return Ok(None); + return Ok(self.out_msg()); } } @@ -350,29 +350,7 @@ impl Slot { self.do_ballot_protocol(); - // This stuff is funky. It might be better to let Slot return whatever it is - // capable of, and let higher layers decide what gets broadcast. - if let Some(emitted) = self.out_msg() { - if let Some(previous_emitted) = &self.last_sent_msg { - if emitted != *previous_emitted { - // A new emitted message. - self.last_sent_msg = Some(emitted.clone()); - self.M.insert(emitted.sender_id.clone(), emitted.clone()); - Ok(Some(emitted)) - } else { - // Ignoring duplicate outgoing messages. - Ok(None) - } - } else { - // The first emitted message. - self.last_sent_msg = Some(emitted.clone()); - self.M.insert(emitted.sender_id.clone(), emitted.clone()); - Ok(Some(emitted)) - } - } else { - // No message to emit yet. - Ok(None) - } + Ok(self.out_msg()) } fn is_valid(&mut self, value: &V) -> Result<(), String> { @@ -492,9 +470,12 @@ impl Slot { // If no values have been confirmed nominated, the node may add new values to its voted set. if self.Z.is_empty() { - // Gather all nominate payloads. + // Gather all nominate payloads from other nodes. let mut nominate_payloads: HashMap> = Default::default(); for (node_id, msg) in &self.M { + if *node_id == self.node_id { + continue; + } match &msg.topic { Topic::Nominate(nominate_payload) | Topic::NominatePrepare(nominate_payload, _) => { @@ -504,6 +485,15 @@ impl Slot { } } + // This node may nominate new values when it is among max_priority_peers. + if self.max_priority_peers.contains(&self.node_id) { + for value in &self.W { + if !self.Y.contains(value) { + self.X.insert(value.clone()); + } + } + } + // Add voted or accepted values from max_priority_peers to self.X for (node_id, payload) in &nominate_payloads { if self.max_priority_peers.contains(node_id) { @@ -522,7 +512,8 @@ impl Slot { self.update_YZ(); if !self.Z.is_empty() && self.B.is_zero() { - self.B = Ballot::new(1, &self.Z.iter().cloned().collect::>()[..]); + let values: Vec<_> = self.Z.iter().cloned().collect(); + self.B = Ballot::new(1, &values); } } @@ -1139,7 +1130,8 @@ impl Slot { } /// Calculate the message to send to the network based on our current state. - fn out_msg(&self) -> Option> { + /// Any duplicate messages are suppressed. + fn out_msg(&mut self) -> Option> { // Prepared is " the highest accepted prepared ballot not exceeding the "ballot" field... // if "ballot = " and the highest prepared ballot is "" where "x < y", // then the "prepared" field in sent messages must be set to "" instead of """ @@ -1181,12 +1173,12 @@ impl Slot { None } else { let HN: u32 = if let Some(h) = &self.H { - // If "h" is the highest confirmed prepared ballot and "h.value == - // ballot.value", then this field is set to "h.counter". Otherwise, - // if no ballot is confirmed prepared or if "h.value != - // ballot.value", then this field is 0. Note that by the rules - // above, if "h" exists, then "ballot.value" will be set to "h.value" - // the next time "ballot" is updated. + // If "h" is the highest confirmed prepared ballot and "h.value == + // ballot.value", then this field is set to "h.counter". Otherwise, + // if no ballot is confirmed prepared or if "h.value != ballot.value", + // then this field is 0. Note that by the rules above, if "h" exists, + // then "ballot.value" will be set to "h.value" the next time "ballot" + // is updated. if h.X == self.B.X { h.N } else { @@ -1200,9 +1192,9 @@ impl Slot { let CN: u32 = if let Some(c) = &self.C { // The value "cCounter" is maintained based on an internally- - // maintained _commit ballot_ "c", initially "NULL". "cCounter" is 0 - // while "c == NULL" or "hCounter == 0", and is "c.counter" - // otherwise. + // maintained _commit ballot_ "c", initially "NULL". "cCounter" is 0 + // while "c == NULL" or "hCounter == 0", and is "c.counter" + // otherwise. if HN != 0 { c.N } else { @@ -1243,12 +1235,12 @@ impl Slot { Phase::Prepare => { let HN: u32 = if let Some(h) = &self.H { - // If "h" is the highest confirmed prepared ballot and "h.value == - // ballot.value", then this field is set to "h.counter". Otherwise, - // if no ballot is confirmed prepared or if "h.value != - // ballot.value", then this field is 0. Note that by the rules - // above, if "h" exists, then "ballot.value" will be set to "h.value" - // the next time "ballot" is updated. + // If "h" is the highest confirmed prepared ballot and "h.value == + // ballot.value", then this field is set to "h.counter". Otherwise, + // if no ballot is confirmed prepared or if "h.value != + // ballot.value", then this field is 0. Note that by the rules + // above, if "h" exists, then "ballot.value" will be set to "h.value" + // the next time "ballot" is updated. if h.X == self.B.X { h.N } else { @@ -1262,9 +1254,9 @@ impl Slot { let CN: u32 = if let Some(c) = &self.C { // The value "cCounter" is maintained based on an internally- - // maintained _commit ballot_ "c", initially "NULL". "cCounter" is 0 - // while "c == NULL" or "hCounter == 0", and is "c.counter" - // otherwise. + // maintained _commit ballot_ "c", initially "NULL". "cCounter" is 0 + // while "c == NULL" or "hCounter == 0", and is "c.counter" + // otherwise. if HN != 0 { c.N } else { @@ -1305,11 +1297,26 @@ impl Slot { ) }); - if let Some(msg) = &msg_opt { + // Suppress duplicate outgoing messages. + if let Some(msg) = msg_opt { assert_eq!(msg.validate(), Ok(())); + + if let Some(last_msg) = &self.last_sent_msg { + if msg != *last_msg { + self.last_sent_msg = Some(msg.clone()); + return Some(msg); + } else { + // Ignore duplicate outgoing message. + return None; + } + } else { + // The first emitted message. + self.last_sent_msg = Some(msg.clone()); + return Some(msg); + } } - msg_opt + None } /// Checks that at least one node in each quorum slice satisfies pred @@ -2179,6 +2186,94 @@ mod nominate_protocol_tests { slot.update_YZ(); assert_eq!(slot.Z, BTreeSet::from_iter(vec!["A", "B", "C", "D"])); } + + #[test_with_logger] + /// A node should not nominate proposed values if it is not in max_priority_peers. + fn test_wait_to_nominate_proposed_values(logger: Logger) { + let (local_node, _node_2, _node_3) = three_node_cycle(); + + let slot_index = 2; + let mut slot = Slot::::new( + local_node.0.clone(), + local_node.1.clone(), + slot_index, + Arc::new(trivial_validity_fn), + Arc::new(trivial_combine_fn), + logger, + ); + + println!("max_priority_peers: {:?}", slot.max_priority_peers); + // Ensure that the local node **is not** in max_priority_peers. + assert!(!slot.max_priority_peers.contains(&local_node.0)); + + let values: BTreeSet = BTreeSet::from_iter(vec![1000, 2000]); + let msg_opt = slot + .propose_values(&values) + .expect("slot.propose_values failed"); + assert_eq!(msg_opt, None); + } + + #[test_with_logger] + /// A node should nominate proposed values if it is in max_priority_peers. + fn test_nominate_proposed_values(logger: Logger) { + let (local_node, _node_2, _node_3) = three_node_cycle(); + + let slot_index = 2; + let mut slot = Slot::::new( + local_node.0.clone(), + local_node.1.clone(), + slot_index, + Arc::new(trivial_validity_fn), + Arc::new(trivial_combine_fn), + logger, + ); + + println!("max_priority_peers: {:?}", slot.max_priority_peers); + // Ensure that the local node **is** in max_priority_peers. + slot.max_priority_peers.insert(local_node.0.clone()); + + { + // The node should nominate proposed values. + let values: BTreeSet = BTreeSet::from_iter(vec![1000, 2000]); + let emitted = slot + .propose_values(&values) + .expect("slot.propose_values failed") + .expect("No message emitted"); + + let expected = Msg::new( + local_node.0.clone(), + local_node.1.clone(), + slot_index, + Topic::Nominate(NominatePayload { + X: BTreeSet::from_iter(vec![1000, 2000]), + Y: BTreeSet::default(), + }), + ); + + assert_eq!(emitted, expected); + } + + { + // The node should continue to nominate new proposed values. + let values: BTreeSet = BTreeSet::from_iter(vec![777, 4242]); + let emitted = slot + .propose_values(&values) + .expect("slot.propose_values failed") + .expect("No message emitted"); + + let expected = Msg::new( + local_node.0.clone(), + local_node.1.clone(), + slot_index, + Topic::Nominate(NominatePayload { + X: BTreeSet::from_iter(vec![777, 1000, 2000, 4242]), + Y: BTreeSet::default(), + }), + ); + + assert_eq!(emitted, expected); + } + } } #[cfg(test)] @@ -2207,32 +2302,22 @@ mod ballot_protocol_tests { logger, ); - let msg = Msg::new( + let values = BTreeSet::from_iter(vec![5678, 1234, 1337, 1338]); + let emitted_msg = slot + .propose_values(&values) + .unwrap() + .expect("No message emitted."); + + let expected = Msg::new( local_node.0.clone(), local_node.1.clone(), slot_index, - Topic::Nominate(NominatePayload { - X: BTreeSet::from_iter(vec![5678, 1234, 1337, 1338]), - Y: BTreeSet::default(), + Topic::Externalize(ExternalizePayload { + C: Ballot::new(1, &vec![1234, 1337, 1338, 5678]), + HN: 1, }), ); - let emitted_msg = slot - .handle(&msg) - .expect("failed handling msg") - .expect("no msg emitted"); - - assert_eq!( - emitted_msg, - Msg::new( - local_node.0.clone(), - local_node.1, - slot_index, - Topic::Externalize(ExternalizePayload { - C: Ballot::new(1, &[1234, 1337, 1338, 5678]), - HN: 1, - }), - ) - ); + assert_eq!(emitted_msg, expected); } #[test_with_logger] @@ -2253,18 +2338,10 @@ mod ballot_protocol_tests { // Ensure our node id is inside max priority peers list. slot.max_priority_peers.insert(node_id.clone()); - // Submit a nomination message + let values: BTreeSet = BTreeSet::from_iter(vec![1000, 2000]); let emitted_msg = slot - .handle(&Msg::new( - node_id.clone(), - quorum_set.clone(), - 1, - Topic::Nominate(NominatePayload { - X: BTreeSet::from_iter(vec![1000, 2000]), - Y: BTreeSet::default(), - }), - )) - .expect("failed handling message") + .propose_values(&values) + .expect("slot.propose_values failed") .expect("expected emitted message, got None"); let expected_msg = Msg::new( @@ -2319,21 +2396,26 @@ mod ballot_protocol_tests { // Vote nominate on 1337, 1338 and confirm nominate 5678, 1234. { - let initial_msg = Msg::new( + let values: BTreeSet = BTreeSet::::from_iter(vec![5678, 1234, 1337, 1338]); + let emitted_msg = slot + .propose_values(&values) + .expect("slot.propose failed") + .expect("no msg emitted"); + + let expected = Msg::new( node_1.0.clone(), node_1.1.clone(), slot_index, Topic::Nominate(NominatePayload { - X: BTreeSet::from_iter(vec![5678, 1234, 1337, 1338]), + X: BTreeSet::from_iter(values), Y: BTreeSet::default(), }), ); - let emitted_msg = slot - .handle(&initial_msg) - .expect("failed handling msg") - .expect("no msg emitted"); - assert_eq!(emitted_msg, initial_msg); + // Node 1 issues vote nominate [5678, 1234, 1337, 1338]. + assert_eq!(emitted_msg, expected); + + // Node 2 issues confirm nominate [5678, 1234]. let confirm_nominate_msg = Msg::new( node_2.0.clone(), node_2.1.clone(), @@ -2347,6 +2429,7 @@ mod ballot_protocol_tests { .handle(&confirm_nominate_msg) .expect("failed handling msg") .expect("no msg emitted"); + let expected_msg = Msg::new( node_1.0.clone(), node_1.1.clone(), @@ -2365,6 +2448,7 @@ mod ballot_protocol_tests { }, ), ); + // Node 1 confirms [5678, 1234] nominated, and votes to prepare them. assert_eq!(emitted_msg, expected_msg); } diff --git a/consensus/scp/src/tests/mod.rs b/consensus/scp/tests/slow_timebase_tests.rs similarity index 99% rename from consensus/scp/src/tests/mod.rs rename to consensus/scp/tests/slow_timebase_tests.rs index 3514923364..485911045f 100644 --- a/consensus/scp/src/tests/mod.rs +++ b/consensus/scp/tests/slow_timebase_tests.rs @@ -1,8 +1,20 @@ // Copyright (c) 2018-2020 MobileCoin Inc. +#![allow(unused_attributes)] +//TODO -- which attribute is unused??? -extern crate rand; - +use common::{ + logger::{log, o, test_with_logger, Logger}, + HashMap, HashSet, NodeID, +}; use rand::{rngs::StdRng, Rng, RngCore, SeedableRng}; +use scp::{ + core_types::{CombineFn, SlotIndex, ValidityFn}, + msg::Msg, + node::{Node, ScpNode}, + quorum_set::QuorumSet, + test_utils, + test_utils::{test_node_id, TransactionValidationError}, +}; use serial_test_derive::serial; use std::{ collections::BTreeSet, @@ -13,19 +25,6 @@ use std::{ time::{Duration, Instant}, }; -use crate::{ - core_types::{CombineFn, SlotIndex, ValidityFn}, - msg::Msg, - node::{Node, ScpNode}, - quorum_set::QuorumSet, - test_utils, - test_utils::{test_node_id, TransactionValidationError}, -}; -use common::{ - logger::{log, o, test_with_logger, Logger}, - HashMap, HashSet, NodeID, -}; - #[derive(Debug)] struct NodeOptions { thread_name: String, diff --git a/consensus/service/BUILD.md b/consensus/service/BUILD.md index f04e7beb4b..cfb10d611e 100644 --- a/consensus/service/BUILD.md +++ b/consensus/service/BUILD.md @@ -5,6 +5,7 @@ - [Building for SGX](#building-for-sgx) - [Hardware Mode](#hardware-mode) - [Simulation Mode](#simulation-mode) + - [Enclave Signing Material](#enclave-signing-material) - [Build](#build) #### Requirements @@ -138,22 +139,57 @@ For local testing, it is possible to run in simulation mode as well as with SGX Install the packages specified in [Hardware Mode](#hardware-mode) without running the daemons. -#### Signing the Enclave +#### Enclave Signing Material The enclave needs to be signed in order to run in production. The MobileCoin Foundation manages the key that signs the enclave which is used in the production MobileCoin Consensus Validator. You can pull down the publicly available signature material in order to run the enclave that will attest with other MobileCoin consensus validators. -If you want to build a signed enclave locally, you can provide a private key with which to sign the enclave, as CONSENSUS_ENCLAVE_PRIVKEY=./Enclave_private.pem. +##### Building without Signing Material -You can generate a private key with the appropriate length and exponent with: +Building locally does not require providing a private key, as a random key will be generated during build. +##### Using a Signed Enclave + +There are two ways to use materials from a previously signed enclave to build your enclave locally. + +The TestNet signature artifacts are available via + +``` +curl -O https://enclave-distribution.test.mobilecoin.com/production.json ``` -openssl genrsa -out Enclave_private.pem -3 3072 + +This retrieves a json record of: + +```json +{ + "enclave": "pool/", + "sigstruct": "pool/", +} ``` +The git revision refers to the TestNet release version. + +Once you have the desired artifact, you will need to extract both the signed enclave and the sigstruct file to build: + +MobileCoin's TestNet Signed Enclave materials are available at, for example: + +``` + curl -O https://enclave-distribution.test.mobilecoin.com/pool/e57b6902aee60be45b78b496c1bef781746e4389/bf7fa957a6a94acb588851bc8767eca5776c79f4fc2aa6bcb99312c3c386c/libconsensus-enclave.signed.so + curl -O https://enclave-distribution.test.mobilecoin.com/pool/e57b6902aee60be45b78b496c1bef781746e4389/bf7fa957a6a94acb588851bc8767eca5776c79f4fc2aa6bcb99312c3c386c/consensus-enclave.css +``` + +Then, when you build, you will provide both `CONSENSUS_ENCLAVE_SIGNED=$(pwd)/libconsensus-enclave.signed.so CONSENSUS_ENCLAVE_CSS=$(pwd)/consensus-enclave.css`. + #### Build -To build consensus, specify the desired `SGX_MODE` (either `HW` for hardware or `SW` for simulation), as well as the desired IAS_MODE (depending on which EPID policy you registered for), and then run: +To build consensus, you will need to specify the following: + +* `SGX_MODE` (either `HW` for hardware or `SW` for simulation) +* `IAS_MODE` (depending on which EPID policy you registered for, either `DEV` or `PROD`) +* (Optional) Signing material, `CONSENSUS_ENCLAVE_SIGNED` and `CONSENSUS_ENCLAVE_CSS` (see [Enclave Signing Material](#enclave-signing-material) above) + +And then you can build with: ``` -SGX_MODE=HW IAS_MODE=DEV CONSENSUS_ENCLAVE_PRIVKEY=./Enclave_private.pem cargo build --release -p consensus-service +SGX_MODE=HW IAS_MODE=DEV CONSENSUS_ENCLAVE_SIGNED=$(pwd)/libconsensus-enclave.signed.so CONSENSUS_ENCLAVE_CSS=$(pwd)/consensus-enclave.css \ + cargo build --release -p consensus-service ``` diff --git a/consensus/service/README.md b/consensus/service/README.md index 39e1fcf197..fb93d19021 100644 --- a/consensus/service/README.md +++ b/consensus/service/README.md @@ -38,7 +38,7 @@ Follow the steps below: An example URI is: ``` - mcp://node1.test.mobilecoin.com:8443/?consensus-msg-key=MCowBQYDK2VwAyEA-21ShHmvuuynH7EcIgkdH2dWxCojgnWYbHxLrRseQ1s=&ca-bundle=/root/mobilenode/public/attest/test_certs/selfsigned_mobilecoin.crt&tls-hostname=www.mobilecoin.com + mcp://node1.test.mobilecoin.com:8443/?consensus-msg-key=MCowBQYDK2VwAyEA-21ShHmvuuynH7EcIgkdH2dWxCojgnWYbHxLrRseQ1s= ``` The quorum set chosen represents a json dictionary specifying the `threshold` of nodes that you consider necessary to reach agreement, followed by the `members` of your quorum. An example quorum set is: @@ -102,16 +102,20 @@ Follow the steps below: 1. Start the SGX daemons. +>Note: Check your aesm location. It is either at `/opt/intel/libsgx-enclave-common/aesm` or `/opt/intel/sgx-aesm-service/aesm`. Update the commands below accordingly. + ``` + source /opt/intel/sgxsdk/environment + export AESM_PATH=/opt/intel/libsgx-enclave-common/aesm - export LD_LIBRARY_PATH=/opt/intel/libsgx-enclave-common/aesm + export LD_LIBRARY_PATH=${AESM_PATH} - /opt/intel/libsgx-enclave-common/aesm/linksgx.sh + ${AESM_PATH}/linksgx.sh /bin/mkdir -p /var/run/aesmd/ /bin/chown -R aesmd:aesmd /var/run/aesmd/ /bin/chmod 0755 /var/run/aesmd/ /bin/chown -R aesmd:aesmd /var/opt/aesmd/ - /opt/intel/libsgx-enclave-common/aesm/aesm_service & + ${AESM_PATH}/aesm_service & ``` 1. Set up your network.toml file. @@ -138,17 +142,26 @@ Follow the steps below: #### Run -An example run command is the following: +An example run command is the below. + +>Note: The environment variables, `SGX_MODE`, `IAS_MODE`, `CONSENSUS_ENCLAVE_CSS` and `CONSENSUS_ENCLAVE_SIGNED` indicate important parameters to the SGX Enclave build. Please see [BUILD.md](./BUILD.md) for more details. + +>Note: Running in `IAS_MODE=DEV` runs a debug enclave. ``` -SGX_MODE=HW IAS_MODE=DEV cargo run --release -p consensus-service -- \ +SGX_MODE=HW IAS_MODE=DEV \ + CONSENSUS_ENCLAVE_CSS=$(pwd)/consensus-enclave.css \ + CONSENSUS_ENCLAVE_SIGNED=$(pwd)/libconsensus-enclave.signed.so \ + cargo run --release -p consensus-service -- \ --client-responder-id my_node.my_domain.com:443 \ - --local-node-id node1.my_domain.com:8443 \ + --peer-responder-id node1.my_domain.com:8443 \ --network /etc/mc-network.toml \ --ias-api-key="${IAS_API_KEY}" \ --ias-spid="${IAS_SPID}" \ --ledger-path /tmp/ledger-db-1 \ - --peer-listen-uri='mcp://0.0.0.0:8443/?tls-chain=/root/mobilenode/public/attest/test_certs/selfsigned_mobilecoin.crt&tls-key=/root/mobilenode/public/attest/test_certs/selfsigned_mobilecoin.key' \ + --peer-listen-uri='mcp://0.0.0.0:8443/' \ + --msg-signer-key MC4CAQAwBQYDK2VwBCIEIGz4xR7wuPKjwM1EK0MKrc9ukTjiDqvKKREITPXPkNku \ + --sealed-block-signing-key /sealed \ --management-listen-addr=0.0.0.0:9090 ``` diff --git a/consensus/service/src/validators.rs b/consensus/service/src/validators.rs index 9e65a9a0dc..b5ccc4df50 100644 --- a/consensus/service/src/validators.rs +++ b/consensus/service/src/validators.rs @@ -164,7 +164,7 @@ pub mod well_formed_tests { let untrusted = DefaultTxManagerUntrustedInterfaces::new(ledger.clone()); - let key_images: Vec = tx.key_images().into_iter().cloned().collect(); + let key_images: Vec = tx.key_images(); let membership_proof_highest_indices = tx.get_membership_proof_highest_indices(); let (cur_block_index, membership_proofs) = @@ -231,7 +231,7 @@ pub mod well_formed_tests { assert_eq!(is_well_formed(&tx, &ledger), Ok(())); // Corrupt the signature. - tx.signature.key_images[0] = KeyImage::from(77); + tx.signature.ring_signatures[0].key_image = KeyImage::from(77); assert_eq!( Err(TransactionValidationError::InvalidTransactionSignature), is_well_formed(&tx, &ledger) @@ -580,7 +580,7 @@ mod is_valid_tests { &mut rng, ); - tx.signature.key_images[0] = tx_stored.key_images[0].clone(); + tx.signature.ring_signatures[0].key_image = tx_stored.key_images[0].clone(); assert_eq!( Err(TransactionValidationError::ContainsSpentKeyImage), is_valid(&tx, &ledger) diff --git a/crypto/README.md b/crypto/README.md index b34be1b3e9..611b3af4ee 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -6,8 +6,8 @@ Provides implementations of cryptography primitives and wrappers around primitiv | ------- | ----------- | | [`ake/enclave`](./ake/enclave/) | Authenticated key exchange enclave. | | [`ake/mcnoise`](./ake/mcnoise/) | Noise protocol for authenticated key exchange. | +| [`box`](./box/README.md) | crypto_box style asymmetric key cryptography. | | [`digestible`](./digestible/README.md) | Cryptographic hashing. | -| [`ecies`](./ecies/README.md) | Asymmetric key cryptography. | | [`keys`](./keys/README.md) | Public key cryptography. | | [`mcrand`](./mcrand/README.md) | `no_std` random number generator. | | [`message-cipher`](./message-cipher/README.md) | Encryption cipher. | diff --git a/crypto/ecies/Cargo.toml b/crypto/box/Cargo.toml similarity index 50% rename from crypto/ecies/Cargo.toml rename to crypto/box/Cargo.toml index fa3d87d79c..142d3c6619 100644 --- a/crypto/ecies/Cargo.toml +++ b/crypto/box/Cargo.toml @@ -1,15 +1,17 @@ [package] -name = "ecies" +name = "mc-crypto-box" version = "0.1.0" authors = ["MobileCoin"] edition = "2018" [dependencies] -aes-ctr = { version = "0.3.0", default-features = false } -keys = { path = "../../crypto/keys", default-features = false } +aead = "0.2" +aes-gcm = { version = "0.3.0", default-features = false } +blake2 = { version = "0.8", default-features = false } +failure = { version = "0.1.5", default-features = false } hkdf = { version = "0.8.0", default-features = false } +keys = { path = "../keys", default-features = false } rand_core = { version = "0.5", default-features = false } -sha2 = { version = "0.8", default-features = false } [dev_dependencies] test_helper = { path = "../../util/test-helper" } diff --git a/crypto/ecies/LICENSE b/crypto/box/LICENSE similarity index 100% rename from crypto/ecies/LICENSE rename to crypto/box/LICENSE diff --git a/crypto/box/README.md b/crypto/box/README.md new file mode 100644 index 0000000000..deceadb84c --- /dev/null +++ b/crypto/box/README.md @@ -0,0 +1,140 @@ +McCryptoBox +=========== + +Provides a simple rust interface for doing authenticated asymmetric key cryptography, +using the Ristretto group. + +Quick Start +----------- + +To use, first instantiate the `VersionedCryptoBox` object. + +If possible, when encrypting, negotiate a version +using `VersionedCryptoBox::select_version`, to ensure compatibility with the recipient. +Otherwise you can `default()` to the latest version. + +To encrypt or decrypt, use the `CryptoBox` trait and exercise the `encrypt` or +`decrypt` APIs or their variations. + +Encryption takes an rng, a public key, and a message, and produces a "cryptogram", +which includes the ciphertext, an ephemeral public key, an aes mac value, and a small versioning tag. + +Decryption takes the private key and the cryptogram and repoduces the message. + +Properties +---------- + +The scheme aims for semantic security at a 128-bit security level, and non-malleability +of the cryptograms. The primitives used at current version are: + +- Ristretto elliptic curve (`curve25519-dalek` crate) for key exchange +- HKDF + Blake2b for the KDF step +- aes-gcm for authenticated encryption + +The wire-format is intended to be stable, with forwards and backwards compatibility +if we must change the primitives. + +Comparison to related schemes +----------------------------- + +This can be compared with many "hybrid public key encryption" systems that have +been proposed in the literature or exist in established cryptographic libraries: + +- DHIES (Abdalla, Bellare, Rogaway, 2001) [1] +- ECIES (SECG-Sec1 v2.0, 2009, IEEE P1363a published 2004-09-02 withdrawn 2019-11-07) [2] +- NaCl Cryptobox (Daniel J. Bernstein, Tanja Lange, Peter Schwabe, latest 2019) [3] + +(This list is not exhaustive. Skip to the bottom for links to these and other references.) + +All these schemes have in common that there is a Diffie-Hellman key-exchange element, +followed by a KDF-step extracting suitable key material from the shared secret, followed by an +AEAD implementation. + +The current version of McCryptoBox conforms quite closely to the diagram and explanation +of ECIES in Svetlin Nakov's [7] "Practical Cryptography for Developers": +https://cryptobook.nakov.com/asymmetric-key-ciphers/ecies-public-key-encryption + +However, none of the standardization efforts related to ECIES have specified Ristretto +as an elliptic curve that could be used in the scheme. All of these standardization +efforts are much older than the Ristretto group. + +NaCl cryptobox is specified [3] as `curve25519xsalsa20poly1305`, that is, to use +curve25519 + salsa20 + poly1305. However, it is mentioned as a TODO to also implement +`crypto_box_nistp256aes256gcm`, that is, using the nistp256 curve and +AES-256-GCM for authenticated encryption. + +In "Cryptography in NaCl" [4] it is explained that in the current version of cryptobox, curve25519 +is used for key exchange, then Hsalsa20 is used to extract entropy from the shared secret. +Hsalsa20 is then used as a CSPRNG and this pseudorandom sequence is xor'd with the plaintext +to achieve encryption. Poly1305 is used to produce a MAC. + +So, `mc-crypto-box` can be viewed as a variation on NaCl cryptobox. +For technical reasons, it is a requirement in Mobilecoin to have a version of +cryptobox based on the Ristretto group. + +Choice to use random nonces derived from key exchange +----------------------------------------------------- + +In NaCl cryptobox, the nonce used to drive authenticated encryption is NOT derived +exclusively from the shared secret, as it was in all previous IES designs. Instead, +there is a nonce value which is input from the user, and users are expected to choose +nonces such that a nonce is never reused when sending to a particular recipient. + +NaCl cryptobox documentation specifies that randomly generated nonces have negligible +chance of collision, but that counter-based nonces work also in their design and can +moreover prevent replay attacks. + +It is explained in "The security impact of a new cryptographic library"[5] that part of +the idea with the nonces is that if Alice wants to send a massive payload to Bob +using NaCl cryptobox, she would do key exchange once (using the two-step cryptobox +API), then break her payload into 4k-sized chunks (depending on transport layer), +and apply cryptobox to each of these chunks, counting up the nonce in sequence. +This ensures that each packet that Bob recieves has its own mac -- there is not one +mac value for the entire payload, and it ensures that we don't have to do an elliptic +curve operation once for each packet, which is what a naive implementation would do. + +In our use-cases right now, we have no need for sending very large messages this way, +and it would present operational difficulties to establish and preserve information +about these nonces. + +Choosing exclusively random nonces derived from key exchange avoids these practical +operational concerns and simplifies the API. + +In a future revision, we may wish to +extend the API to support the two-step construction + user-provided nonce idea. + +Comparison to `aead` crate +-------------------------- + +The API is meant to be not too different from the rust `aead` crate [8], but it can't +be exactly the same as that, for several reasons. + +- The API requires to implement low-level functions`encrypt_in_place_detached` + and `decrypt_in_place_detached`: https://docs.rs/aead/0.2.0/aead/trait.Aead.html#required-methods +- These take the plaintext as a mutable buffer and transform it in-place to the ciphertext +- The message authentication code requires additional space in the "actual" ciphertext payload, + so it gets returned as a "detached" byte buffer from the `encrypt` function, and the `decrypt` + function requires a reference to it. +- High-level helpers are implemented in terms of this, which create a wire-format where this tag + just gets stuck at the end of the ciphertext buffer. + +There are a couple of major differences in our setting: +- CryptoBox is public key cryptography -- the encrypt function must take a public key, and the + encrypt function must take a private key. +- The nonce is derived from the shared secret using the KDF, it's not an input from the user. +- The AEAD trait emits only the MAC value from `encrypt_in_place_detached`. CryptoBox must + emit the ephemeral public key and the MAC value. We choose to concatenate these + into a "footer" of fixed size with a fixed format. This is okay because the cryptogram is meant + to be opaque to the user anyways. + +References +---------- + +1. DHIES (Abdalla, Bellare, Rogaway, 2001): https://web.cs.ucdavis.edu/~rogaway/papers/dhies.pdf +2. SECG-Sec1 v2.0 (Certicom Research, 2009): http://www.secg.org/sec1-v2.pdf +3. NaCl Cryptobox (Bernstien, Lange, Schwabe, 2019): https://nacl.cr.yp.to/box.html +4. Cryptography in Nacl (Bernstein, 2009): https://cr.yp.to/highspeed/naclcrypto-20090310.pdf +5. The security impact of a new cryptographic library: (Bernstein, Lange, Schwabe, 2012): https://cr.yp.to/highspeed/coolnacl-20120725.pdf +6. Authenticated Encryption in the Public-Key Setting (Jee Hea An, 2001): https://eprint.iacr.org/2001/079 +7. Practical Cryptography for Developers (Svetlin Nakov, 2018): https://cryptobook.nakov.com/asymmetric-key-ciphers/ecies-public-key-encryption +8. Rust Aead crate: https://docs.rs/aead/0.2.0/aead/ diff --git a/crypto/box/src/fixed_buffer.rs b/crypto/box/src/fixed_buffer.rs new file mode 100644 index 0000000000..2da3b4408c --- /dev/null +++ b/crypto/box/src/fixed_buffer.rs @@ -0,0 +1,89 @@ +use aead::{Buffer, Error}; + +/// The rust aead crate is organized around a Buffer trait which abstracts +/// commonalities of alloc::vec::Vec and heapless::Vec which are useful for +/// aead abstractions. +/// +/// The needed functionalities are: +/// - Getting the bytes that have been written as a &mut [u8] (or &[u8]) +/// - Extending the buffer (which is allowed to fail) +/// - Truncating the buffer +/// +/// A drawback of heapless is that it is strictly an "owning" data-structure, +/// it doesn't have light-weight "views" or "reference" types. +/// +/// This provides a zero-overhead abstraction over &mut [u8] which does this, +/// so that applications can easily use the aead trait to encrypt into e.g. +/// [u8; 128] without using vec, making allocations, or using heapless, which +/// might commit them to storing extra counters in their structures. +/// +/// This represents a view of a fixed capacity buffer, where len() indicates +/// how many bytes, from the beginning of the buffer, have been "used". +/// +/// It is expected that this type will be used to wrap e.g. [u8;128] briefly +/// in order to interact with interfaces like Aead, and then discarded. +pub struct FixedBuffer<'a> { + buf: &'a mut [u8], + length: usize, +} + +impl<'a> AsRef<[u8]> for FixedBuffer<'a> { + fn as_ref(&self) -> &[u8] { + &self.buf[..self.length] + } +} + +impl<'a> AsMut<[u8]> for FixedBuffer<'a> { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.buf[..self.length] + } +} + +impl<'a> Buffer for FixedBuffer<'a> { + fn len(&self) -> usize { + self.length + } + fn is_empty(&self) -> bool { + self.length == 0 + } + + fn extend_from_slice(&mut self, other: &[u8]) -> Result<(), Error> { + if self.length + other.len() > self.buf.len() { + return Err(Error); + } + self.buf[self.length..self.length + other.len()].copy_from_slice(other); + self.length += other.len(); + Ok(()) + } + + fn truncate(&mut self, len: usize) { + self.length = core::cmp::min(self.length, len); + } +} + +impl<'a> FixedBuffer<'a> { + /// Create a new FixedBuffer "view" over a mutable slice of bytes, + /// with length set to zero, so that we will be overwriting those bytes. + pub fn overwriting(target: &'a mut [u8]) -> Self { + Self { + buf: target, + length: 0, + } + } + + /// Test if there is no more space to extend the buffer, + /// i.e. we have completely exhausted the capacity. + pub fn is_exhausted(&self) -> bool { + self.buf.len() == self.length + } +} + +impl<'a> From<&'a mut [u8]> for FixedBuffer<'a> { + /// Initialize a fixed buffer from a mutable slice, which is initially + /// "exhausted", so all of the initial values of those bytes are in the buffer. + /// This buffer can then be modified or truncated etc. + fn from(buf: &'a mut [u8]) -> Self { + let length = buf.len(); + Self { buf, length } + } +} diff --git a/crypto/box/src/hkdf_blake2b_aes_128_gcm.rs b/crypto/box/src/hkdf_blake2b_aes_128_gcm.rs new file mode 100644 index 0000000000..6365fee693 --- /dev/null +++ b/crypto/box/src/hkdf_blake2b_aes_128_gcm.rs @@ -0,0 +1,144 @@ +use crate::traits::{CryptoBox, Error}; + +use aead::{ + generic_array::{ + sequence::{Concat, Split}, + typenum::{Sum, U12, U16, U32}, + GenericArray, + }, + Aead, Error as AeadError, NewAead, +}; +use aes_gcm::Aes128Gcm; +use blake2::Blake2b; +use core::convert::TryFrom; +use hkdf::Hkdf; +use keys::{CompressedRistrettoPublic, RistrettoPrivate, RistrettoPublic, RISTRETTO_PUBLIC_LEN}; +use rand_core::{CryptoRng, RngCore}; + +type RistrettoLen = U32; +type AesMacLen = ::TagSize; + +/// Represents an implementation of Ristretto-Box using Hkdf and Aes128Gcm +/// +/// This structure contains the actual cryptographic primitive details, and +/// specifies part of the wire format of the "footer" where the ephemeral +/// public key comes first, and the mac comes second. +#[derive(Default)] +pub struct RistrettoHkdfBlake2bAes128Gcm {} + +impl CryptoBox for RistrettoHkdfBlake2bAes128Gcm { + type FooterSize = Sum; + + fn encrypt_in_place_detached( + &self, + rng: &mut T, + key: &RistrettoPublic, + buffer: &mut [u8], + ) -> Result, AeadError> { + // ECDH + use keys::KexPublic; + let (our_public, shared_secret) = key.new_secret(rng); + + let compressed_public = CompressedRistrettoPublic::from(our_public); + let curve_point_bytes = + GenericArray::::clone_from_slice(compressed_public.as_ref()); + + // KDF + let (aes_key, aes_nonce) = kdf_step(shared_secret.as_ref()); + + // AES + let aead = Aes128Gcm::new(aes_key); + let mac = aead.encrypt_in_place_detached(&aes_nonce, &[], buffer)?; + + // Tag is curve_point_bytes || aes_mac_bytes + Ok(curve_point_bytes.concat(mac)) + } + + fn decrypt_in_place_detached( + &self, + key: &RistrettoPrivate, + tag: &GenericArray, + buffer: &mut [u8], + ) -> Result<(), Error> { + // ECDH + use keys::KexReusablePrivate; + let public_key = + RistrettoPublic::try_from(&tag[..RISTRETTO_PUBLIC_LEN]).map_err(Error::Key)?; + let shared_secret = key.key_exchange(&public_key); + + // KDF + let (aes_key, aes_nonce) = kdf_step(shared_secret.as_ref()); + + // AES + let mac_ref = <&GenericArray>::from(&tag[RISTRETTO_PUBLIC_LEN..]); + let aead = Aes128Gcm::new(aes_key); + aead.decrypt_in_place_detached(&aes_nonce, &[], buffer, mac_ref) + .map_err(|_| Error::MacFailed)?; + + Ok(()) + } +} + +/// KDF part, factored out to avoid duplication +/// This part must produce the key and IV/nonce for aes-gcm +/// Blake2b produces 64 bytes of private key material which is more than we need, +/// so we don't do the HKDF-EXPAND step. +fn kdf_step(dh_shared_secret: &[u8; 32]) -> (GenericArray, GenericArray) { + let (prk, _) = Hkdf::::extract(Some(b"dei-salty-box"), dh_shared_secret); + // Split the prk into a 16 byte and a 12 byte piece + let (sixteen, remainder): (GenericArray, _) = prk.split(); + let (twelve, _): (GenericArray, _) = remainder.split(); + (sixteen, twelve) +} + +#[cfg(test)] +mod test { + use super::*; + use keys::FromRandom; + + extern crate test_helper; + + #[test] + fn test_round_trip() { + let algo = RistrettoHkdfBlake2bAes128Gcm::default(); + let plaintext1 = b"01234567".to_vec(); + let plaintext2 = plaintext1.repeat(50); + + test_helper::run_with_several_seeds(|mut rng| { + let a = RistrettoPrivate::from_random(&mut rng); + let a_pub = RistrettoPublic::from(&a); + + for plaintext in &[&plaintext1[..], &plaintext2[..]] { + for _reps in 0..50 { + let ciphertext = algo.encrypt(&mut rng, &a_pub, plaintext).unwrap(); + let decrypted = algo.decrypt(&a, &ciphertext).expect("decryption failed!"); + assert_eq!(plaintext.len(), decrypted.len()); + assert_eq!(plaintext, &&decrypted[..]); + } + } + }); + } + + #[test] + fn test_expected_failure() { + let algo = RistrettoHkdfBlake2bAes128Gcm::default(); + let plaintext1 = b"01234567".to_vec(); + let plaintext2 = plaintext1.repeat(50); + + test_helper::run_with_several_seeds(|mut rng| { + let a = RistrettoPrivate::from_random(&mut rng); + let a_pub = RistrettoPublic::from(&a); + + let not_a = RistrettoPrivate::from_random(&mut rng); + + for plaintext in &[&plaintext1[..], &plaintext2[..]] { + for _reps in 0..50 { + let ciphertext = algo.encrypt(&mut rng, &a_pub, plaintext).unwrap(); + let decrypted = algo.decrypt(¬_a, &ciphertext); + assert!(decrypted.is_err()); + assert_eq!(decrypted, Err(Error::MacFailed)); + } + } + }); + } +} diff --git a/crypto/box/src/lib.rs b/crypto/box/src/lib.rs new file mode 100644 index 0000000000..de3dc5cb0a --- /dev/null +++ b/crypto/box/src/lib.rs @@ -0,0 +1,107 @@ +// Copyright (c) 2018-2020 MobileCoin Inc. + +#![no_std] + +//! This crate implements a simple authenticated public-key crypto API, for +//! messages of arbitrary length. +//! - Ristretto Curvepoints used for ECDH +//! - HKDF used to extract key material from dh_shared_secret +//! - Aes-128-Gcm used to encrypt and mac the payload +//! +//! There is also a versioning tag used to allow for a wire-stable format +//! +//! To use, create the object `VersionedCryptoBox`, then use the CryptoBox trait to +//! encrypt and decrypt. + +extern crate alloc; + +pub use aead::generic_array; + +mod hkdf_blake2b_aes_128_gcm; +mod traits; +mod versioned; + +pub use aead::Error as AeadError; +pub use traits::{CryptoBox, Error}; +pub use versioned::{VersionError, VersionedCryptoBox}; + +// FixedBuffer allows to use a &mut [u8] slice as a fixed-capacity aead::Buffer +mod fixed_buffer; +pub use fixed_buffer::FixedBuffer; + +#[cfg(test)] +mod test { + use super::*; + use aead::generic_array::{arr, arr_impl}; + use keys::{FromRandom, RistrettoPrivate, RistrettoPublic}; + + extern crate test_helper; + + #[test] + fn test_round_trip() { + let algo = VersionedCryptoBox::default(); + let plaintext1 = b"01234567".to_vec(); + let plaintext2 = plaintext1.repeat(50); + + test_helper::run_with_several_seeds(|mut rng| { + let a = RistrettoPrivate::from_random(&mut rng); + let a_pub = RistrettoPublic::from(&a); + + for plaintext in &[&plaintext1[..], &plaintext2[..]] { + for _reps in 0..50 { + let ciphertext = algo.encrypt(&mut rng, &a_pub, plaintext).unwrap(); + let decrypted = algo.decrypt(&a, &ciphertext).expect("decryption failed!"); + assert_eq!(plaintext.len(), decrypted.len()); + assert_eq!(plaintext, &&decrypted[..]); + } + } + }); + } + + #[test] + fn test_expected_failure() { + let algo = VersionedCryptoBox::default(); + let plaintext1 = b"01234567".to_vec(); + let plaintext2 = plaintext1.repeat(50); + + test_helper::run_with_several_seeds(|mut rng| { + let a = RistrettoPrivate::from_random(&mut rng); + let a_pub = RistrettoPublic::from(&a); + + let not_a = RistrettoPrivate::from_random(&mut rng); + + for plaintext in &[&plaintext1[..], &plaintext2[..]] { + for _reps in 0..50 { + let ciphertext = algo.encrypt(&mut rng, &a_pub, plaintext).unwrap(); + let decrypted = algo.decrypt(¬_a, &ciphertext); + assert!(decrypted.is_err()); + assert_eq!(decrypted, Err(Error::MacFailed)); + } + } + }); + } + + #[test] + fn test_round_trip_fixed_length() { + let algo = VersionedCryptoBox::default(); + let plaintext1 = arr![u8; 0, 1, 2, 3, 4, 4, 3, 2]; + let plaintext2 = arr![u8; 42, 42, 42, 42, 78, 78, 78, 78]; + + test_helper::run_with_several_seeds(|mut rng| { + let a = RistrettoPrivate::from_random(&mut rng); + let a_pub = RistrettoPublic::from(&a); + + for plaintext in &[plaintext1, plaintext2] { + for _reps in 0..10 { + let ciphertext = algo + .encrypt_fixed_length(&mut rng, &a_pub, plaintext) + .unwrap(); + let decrypted = algo + .decrypt_fixed_length(&a, &ciphertext) + .expect("decryption failed!"); + assert_eq!(plaintext, &decrypted); + } + } + }); + } +} diff --git a/crypto/box/src/traits.rs b/crypto/box/src/traits.rs new file mode 100644 index 0000000000..abecd62c74 --- /dev/null +++ b/crypto/box/src/traits.rs @@ -0,0 +1,179 @@ +use alloc::vec::Vec; + +use aead::{ + generic_array::{ + sequence::{Concat, Split}, + typenum::{Diff, Sum, Unsigned}, + ArrayLength, GenericArray, + }, + Error as AeadError, +}; +use core::ops::{Add, Sub}; +use failure::Fail; +use keys::{KeyError, RistrettoPrivate, RistrettoPublic}; +use rand_core::{CryptoRng, RngCore}; + +/// Error type for decryption +#[derive(PartialEq, Eq, Fail, Debug)] +pub enum Error { + #[fail(display = "Error decoding curvepoint: {}", _0)] + Key(KeyError), + #[fail( + display = "Too short, ciphertext is shorter than a footer: {} < {}", + _0, _1 + )] + TooShort(usize, usize), + #[fail(display = "Mac failed")] + MacFailed, + #[fail(display = "Unknown algorithm code: {}", _0)] + UnknownAlgorithm(usize), + #[fail(display = "Wrong magic bytes")] + WrongMagicBytes, +} + +/// Trait defining the high-level interface to Crypto-Box in-terms of low-level +/// This assumes use of keys::Ristretto* types, but could be more generic +pub trait CryptoBox: Default { + type FooterSize: ArrayLength; + + // Required functions + + /// Encrypt a buffer in place against a public key, and return the footer + /// Fails only if the underlying AEAD fails + /// + /// Meant to mirror aead::encrypt_in_place_detached + fn encrypt_in_place_detached( + &self, + rng: &mut T, + key: &RistrettoPublic, + buffer: &mut [u8], + ) -> Result, AeadError>; + + /// Decrypt a buffer in place given the private key, and given the footer also + /// + /// Meant to mirror aead::decrypt_in_place_detached + /// + /// Fails if: + /// - Curvepoint cannot be decoded + /// - MAC check fails + /// - Anything is wrong with the footer (magic bytes? version code?) + fn decrypt_in_place_detached( + &self, + key: &RistrettoPrivate, + footer: &GenericArray, + buffer: &mut [u8], + ) -> Result<(), Error>; + + // Provided functions + // These functions consume and produce "cryptograms" where the footer bytes + // are placed after the ciphertext bytes on the wire. + + /// Encrypt contents of a slice, returning the cryptogram in a Vec + /// + /// Meant to mirror aead::encrypt + fn encrypt( + &self, + rng: &mut T, + key: &RistrettoPublic, + plaintext: &[u8], + ) -> Result, AeadError> { + let mut result = Vec::::with_capacity(plaintext.len() + Self::FooterSize::USIZE); + result.extend_from_slice(plaintext); + self.encrypt_in_place(rng, key, &mut result)?; + Ok(result) + } + + /// Decrypt a slice pointing to the cryptogram, returning a Vec plaintext. + /// + /// Meant to mirror aead::decrypt + fn decrypt(&self, key: &RistrettoPrivate, cryptogram: &[u8]) -> Result, Error> { + let mut result = cryptogram.to_vec(); + self.decrypt_in_place(key, &mut result)?; + Ok(result) + } + + /// Encrypt a buffer, extending the buffer to place the footer at the end. + /// + /// Meant to mirror aead::encrypt_in_place + /// + /// Fails if the underlying AEAD fails. + fn encrypt_in_place( + &self, + rng: &mut T, + key: &RistrettoPublic, + buffer: &mut impl aead::Buffer, + ) -> Result<(), AeadError> { + let footer = self.encrypt_in_place_detached(rng, key, buffer.as_mut())?; + buffer.extend_from_slice(&footer) + } + + /// Counterpart to encrypt_in_place, which finds the footer at the end of + /// the cryptogram. + /// + /// Meant to mirror aead::decrypt_in_place + /// + /// Decryption can fail if: + /// - The buffer is too short to be interpretted + /// - The curvepoint cannot be deserialized + /// - The mac check fails + fn decrypt_in_place( + &self, + key: &RistrettoPrivate, + cryptogram: &mut impl aead::Buffer, + ) -> Result<(), Error> { + // Extract the footer from end of ciphertext, doing bounds checks + if cryptogram.len() < Self::FooterSize::USIZE { + return Err(Error::TooShort(cryptogram.len(), Self::FooterSize::USIZE)); + } + let footer_pos = cryptogram.len() - Self::FooterSize::USIZE; + let (ciphertext, footer) = cryptogram.as_mut().split_at_mut(footer_pos); + // Note: this is modifying the cryptogram via the mutable slice ciphertext + self.decrypt_in_place_detached(key, GenericArray::from_slice(footer), ciphertext)?; + cryptogram.truncate(footer_pos); + Ok(()) + } + + /// Encrypt a fixed-length buffer, producing a fixed-length buffer containing + /// the cryptogram. + /// + /// A non-allocating counterpart to encrypt + fn encrypt_fixed_length( + &self, + rng: &mut T, + key: &RistrettoPublic, + buffer: &GenericArray, + ) -> Result>, AeadError> + where + T: RngCore + CryptoRng, + L: ArrayLength + Add, + Sum: ArrayLength, + { + let mut buffer = buffer.clone(); + let footer = self.encrypt_in_place_detached(rng, key, buffer.as_mut_slice())?; + Ok(buffer.concat(footer)) + } + + /// Decrypt a cryptogram stored in a fixed-length buffer, producing + /// the plaintext in a fixed-length buffer. + /// + /// A non-allocating counterpart to decrypt + fn decrypt_fixed_length( + &self, + key: &RistrettoPrivate, + cryptogram: &GenericArray, + ) -> Result>, Error> + // generic_array/typenum can be really annoying... + // we have to convince it that not only is L - FooterSize a number, + // and an array length, but also that L - (L - FooterSize) is FooterSize + where + L: ArrayLength + + Sub + + Sub, Output = Self::FooterSize>, + Diff: ArrayLength, + { + let (mut ciphertext, footer) = + Split::>::split(cryptogram.clone()); + self.decrypt_in_place_detached(key, &footer, ciphertext.as_mut_slice())?; + Ok(ciphertext) + } +} diff --git a/crypto/box/src/versioned.rs b/crypto/box/src/versioned.rs new file mode 100644 index 0000000000..8835b96907 --- /dev/null +++ b/crypto/box/src/versioned.rs @@ -0,0 +1,161 @@ +//! This represents a versioning scheme for Ristretto-Box encrypted ciphertexts +//! that are supposed to be shooting for 128-bit security. It is intended to be +//! a wire-stable format. +//! +//! The point of the version numbers is to allow clients to decrypt arbitrarily +//! old ciphertexts from the recovery database. +//! (And also, to allow to upgrade the ingest server separately from the clients.) +//! +//! The idea here is, take one or several implementations of CryptoBox trait, +//! with FooterSize = 48, then stick two additional bytes in the footer. The +//! first is a major version (or "magic byte"), and the second is a minor version. +//! The minor version selects which algorithm we will use. +//! A major version mismatch means we can't proceed at all. This might happen +//! if we decide that the FooterSize must increase. +//! +//! Minor version mapping: +//! 0 = hkdf_blake2b_aes_128_gcm + +use crate::{ + hkdf_blake2b_aes_128_gcm::RistrettoHkdfBlake2bAes128Gcm, + traits::{CryptoBox, Error}, +}; + +use aead::{ + generic_array::{ + arr, arr_impl, + sequence::Concat, + typenum::{Unsigned, U50}, + GenericArray, + }, + Error as AeadError, +}; +use alloc::vec::Vec; +use failure::Fail; +use keys::{RistrettoPrivate, RistrettoPublic}; +use rand_core::{CryptoRng, RngCore}; + +//// +// CONFIGURATION +//// + +/// A "magic byte" value checked during this process, but not interpretted. +const MAJOR_VERSION: u8 = 0; +/// The "default" version that we would use for encryption lacking any version negotiation. +const LATEST_MINOR_VERSION: u8 = 0; +/// The versions that we would find "acceptable" during version negotiation. +/// This list allows clients and servers to be upgraded at different times. +/// Items should be removed from this list if found insecure. +const ACCEPTABLE_MINOR_VERSIONS: &[u8] = &[0]; +/// The list of algos used. +/// Minor version numbers correspond to indexes into this tuple. +/// Items should NOT be removed from this list, it will break compatibility, +/// and make it impossible for users to read old data from the recovery db. +/// Note: When extending this tuple, you must add additional arms to the match +/// statements in the implementation below. +type ImplTuple = (RistrettoHkdfBlake2bAes128Gcm,); + +//// +// Implementation +//// + +/// An object implementing CryptoBox trait that calls out to one of several other +/// implementations, then attaches versioning tags. When decrypting, it interprets +/// those versioning tags. +pub struct VersionedCryptoBox { + /// The version that this cipher object will use for encryption. + /// Decryption will always work for any implemented scheme. + selected_version: u8, + /// The different implementations, represented as a tuple + algos: ImplTuple, +} + +impl VersionedCryptoBox { + pub fn major_version() -> u8 { + MAJOR_VERSION + } + /// The list of versions that are acceptable during version negotiation + pub fn acceptable_minor_versions() -> Vec { + ACCEPTABLE_MINOR_VERSIONS.to_vec() + } + /// Called by a client to select an acceptable version based on what a server adverstised + pub fn select_version(others_acceptable_versions: &[u8]) -> Result { + Self::acceptable_minor_versions() + .iter() + .filter(|x| others_acceptable_versions.contains(x)) + .max() + .ok_or(VersionError::NoAcceptableVersions) + .map(|ver| Self { + selected_version: *ver, + algos: Default::default(), + }) + } +} + +/// Default to the latest version for encryption, lacking any version negotiation info +/// This is typical in the ingest node, which cannot negotiate with all clients. +impl Default for VersionedCryptoBox { + fn default() -> Self { + Self { + selected_version: LATEST_MINOR_VERSION, + algos: Default::default(), + } + } +} + +impl CryptoBox for VersionedCryptoBox { + // The footer size is: + // 32 for curve point + // 16 for mac, at 128 bit sec level + // 2 for version info + type FooterSize = U50; + + // Choose the algo based on self.selected_version + fn encrypt_in_place_detached( + &self, + rng: &mut T, + key: &RistrettoPublic, + buffer: &mut [u8], + ) -> Result, AeadError> { + // Match is used because we cannot index into a tuple with run-time values, + // but there might be some macro trickery that could clean this up more. + // If we want to be generic over rng, we cannot use arrays of fn ptr's here. + let footer = match self.selected_version { + // Add additional arms to this match if adding new versions + 0u8 => self.algos.0.encrypt_in_place_detached(rng, key, buffer)?, + _ => panic!( + "self.selected_version is holding an illegal value: {}", + self.selected_version + ), + }; + Ok(footer.concat(arr![u8; MAJOR_VERSION, self.selected_version])) + } + + // Choose the algo based on the version data in the ciphertext + fn decrypt_in_place_detached( + &self, + key: &RistrettoPrivate, + footer: &GenericArray, + buffer: &mut [u8], + ) -> Result<(), Error> { + // Note: When generic_array is upreved, this can be tidier using this: + // https://docs.rs/generic-array/0.14.1/src/generic_array/sequence.rs.html#302-320 + // For now we have to split as a slice, then convert back to Generic Array. + let (footer, version_data) = footer.split_at(::FooterSize::USIZE - 2); + let footer = GenericArray::from_slice(footer); + if MAJOR_VERSION != version_data[0] { + return Err(Error::WrongMagicBytes); + } + match version_data[1] { + // Add additional arms to this match if adding new versions + 0u8 => self.algos.0.decrypt_in_place_detached(key, footer, buffer), + _ => Err(Error::UnknownAlgorithm(version_data[1] as usize)), + } + } +} + +#[derive(Fail, Debug)] +pub enum VersionError { + #[fail(display = "No mutually acceptable CryptoBox versions could be found")] + NoAcceptableVersions, +} diff --git a/crypto/digestible/README.md b/crypto/digestible/README.md index 435ada29bf..3ec6daa6c3 100644 --- a/crypto/digestible/README.md +++ b/crypto/digestible/README.md @@ -1,75 +1,175 @@ digestible ========== -`Digestible` is a trait that can be used to specify how an object can be cryptographically -hashed in a secure way. - -Specifically, this means, I have a structured object e.g. a Block in my blockchain, which -I want to compute a Sha256 hash of. Naively, I could serialize it and then hash the result, -but this involves copying a lot of bytes around. I could derive `std::hash::Hash`, -but `Hash` only supports an output of `u64` and not a full Sha256 output. Moreover, `Hash` -doesn't include any domain separation or protection against length extension attacks. - -Background +NOTE: This crate is WIP, use at your own risk! + +`digestible` and its companion crate `digestible-derive`, represent a scheme for +secure (nonmalleable) hashing of common rust objects using a collision-resistant +hash function. This represents critical infrastructure for blockchain projects, +because if e.g. an attacker can find two different blocks with the same hash, +they can subvert the integrity of the blockchain. + +The `Digestible` trait is provided, which allows that the contents of the entire +object can be hashed together in a non-malleable way, after an implementation of +`Digest` trait is provided. + +The scheme implicitly defines an "encoding" whereby your object is turned into a +byte sequence which is fed into the hasher. + +This has a few benefits: +- The encoding is stable and canonical, not depending on implementation details of + a serialization library, which typically do not provide byte-for-byte stability + guarantees. +- Bringing the bytes directly to the hasher as they are produced is faster than + marshalling them to a temporary buffer and then hashing the buffer. +- Digestible trait is not rooted in serde -- since many types implement serde traits + without concern for cryptographic issues, basing digestible trait on serde risks + creating problems. Secure hashing really is a different issue from serialization, + close though they may seem. + +Typically, serialization libraries offer a stable wire format, and then +progressively try to improve the efficiency of serialization and deserialization +over time, without breaking the wire format. This generally means that the byte +representation is not canonical, which makes this a bad way to hash objects. + +Overview +-------- + +To achieve its goals, `digestible` must specify an encoding for any type which +you derive `Digestible` on. + +Ultimately, the engineering requirement is to show that the encoding function, +as it acts on objects of any particular type, is a "faithful" encoding -- that is, +no two objects correspond to the same bytes. If this is true, then under the assumption +that your `Digest` algo is second pre-image resistant, it is also hard to find two +instances of any structure with the same hash. + +Our strategy is to work "inductively" over the structure of your types. + +- We take as our correctness property called "prefix-free". Prefix-free is stronger + than saying that an encoding is one-to-one / faithful, so if we have this for + all types that we implement Digestible for, then we have achieved our goal. + For a good overview of prefix codes, see wikipedia: https://en.wikipedia.org/wiki/Prefix_code +- We implement `Digestible` for primitive types in a way that accomplishes this. + This is generally easy because most primitive types of interest have fixed length encodings, + which are trivially prefix-free. +- For "compound" types like structures, enums, sequences, etc. we appeal to one of several + abstract rules specifying how a prefix-free encoding can be built assuming that the children + have prefix-free encodings. + - These rules will be explained in detail in a separate document, but roughly, we think + of each compound type as either a "product type" or a "sum type" and apply the corresponding + rule. + - This actual mapping is done either by generic implementations of `trait Digestible` e.g. + for slices or `Vec`, or it is done in the proc-macro logic in `digestible-derive`, e.g. + for `struct` and `enum`. + +Roughly, the five categories that everything gets interpretted as, in this analysis, are: +- Primitives +- Aggregates (structs, tuples, fixed-length arrays) +- Sequences (variable length arrays, strings) +- Variant (including `Option`, `enum`) +- "Custom primitives" (generally means external types with canonical fixed-width representations, + e.g. curve25519-dalek curvepoints and scalars.) + +(It's possible to extend this to include something like e.g. protobuf map types, but we haven't +implemented it and won't describe it here.) + +If one applies this strategy naively and considers the results, it turns out that it corresponds +roughly to a "canonical" version of bincode, and is thus very efficient, adding very little "fluff" +to the encoding. This also makes it very believable that no two objects *of exactly the same type* +have the same encoding unless they are semantically equal, since bincode can actually be +deserialized, which is another way to demonstrate that the encoding is faithful. +https://docs.rs/bincode/1.2.1/bincode/ + +However, bincode would not normally be considered a suitable encoding for non-malleable hashing. +It's more common to use "self-describing" data-formats based on ASN.1 in cryptographic contexts, +where the serialized data essentially carries a schema that could be used to interpret it. The +purpose of this is to try to ensure that objects that have different schema are guaranteed to have +different hashes, and get the "semantics" of the data into the hash. + +This is sometimes called the "Horton Principle": https://en.wikipedia.org/wiki/Horton_Principle + +In the DER encoding rules, a strict Type-Length-Value protocol is used when encoding structures. +Types are mapped in some standardized way to a "type code", typically a fixed small number of bytes, +and this becomes part of the ASN.1 module specification. A struct is treated as a "group" and a +specific protocol is used for opening and closing a group, and listing the TLV bytes for its +members consecutively. + +Creating type codes on a per-type basis generally has to be done manually, and so creates a maintanence +burden. There is very little tooling in rust (or indeed, most programming language ecosystems) to support +this. + +One useful insight is that while DER is required to support deserialization, and so minimizing size-on-the-wire +is of critical interest, in the case when the encoding is only being made in order to hash the structure, +size on the wire is much less important. Modern hash functions are generally much faster on a per-byte +basis than elliptic curve operations. In our context, hashing transactions, hashing blocks, etc. is generally +not a performance bottleneck if done reasonably efficiently -- transaction validation is. So it's not +extremely important here to get an optimal or even near-optimal encoded representation in terms of the rate, +or number of bytes on the wire. It's much more important to get a non-malleable encoding. + +In many modern crypto libraries based on the `dalek-cryptography` ecosystem, the merlin library is used to +generate hashes of the cryptographic transcript to use as challenges, when employing the Fiat-Shamir heuristic. +- https://merlin.cool/use/protocol.html +This means roughly that the "contents to be hashed" are visited and mapped to a STROBE `AD` operation: + +``` +AD[label || LE32(message.len())](message); +``` +- https://merlin.cool/transcript/ops.html#appending-messages + +Inspecting the actual source code shows that at the STROBE layer, this actually looks roughly like +``` +strobe.meta_AD(label); +strobe.meta_AD(LE32(...)); +strobe.AD(message); +``` + +This has some of the characteristics of a type-length-value encoding, in that the "label" +is often playing the role of the data-type descriptor. However, here the label is not escaped using +length encoding, even though it is a user-provided variable-length string, nor is there any standardized list +of labels to be synchronized across applications. So, none of these strategies is really producing a +"distinguished encoding", and it's possible that other protocols with pathologically chosen labels and +pathologically chosen values could happen to have merlin transcript hashes that collide with theirs. + +Rather, the idea is that as long as the labels are "descriptive" and hence unlikely to collide by chance +with labels from another application, and all of the bytes in the actual application in question which +are potentially controlled by an adversary are properly framed, this represents "sound domain separation". +As long as the encodings of each actual message object are canonical, the overall protocol hash will be +canonical (and non-malleable), and so it should be hard for an adversary to trick the user's programs +by finding different values that those programs think are the same due to the hashes. + +At present revision, `digestible` incorporates this idea in the following way: +- The naive "canonical bincode"-like strategy is extended to incorporate fixed labels. + It is easy to see that adding fixed string constants as labels does not impact the prefix-free property, + so this can do no harm. +- Fixed labels are used whenever a `struct` is encoded: The rust name of the structure is the outer label. + Every struct member is also prefixed with a label corresponding to the member name. +- Digestible always incorporates proper framing: fixed-size objects are generally not framed, + but anything not known statically to have a fixed size is framed, at any layer of the hierarchy where + that is the case. + +This domain separation scheme is not perfect -- it is not as good as e.g. hashing a DER encoding. +There are several straightforward ways to improve it, at the cost of somewhat increased complexity. +At the same time, it seems not much different from the ideas around domain separation in Bulletproofs and +Schnorrkel, which are critical dependencies of ours. It also seems difficult to imagine a realistic way to +create a collision that would impact the application. + +We would like to improve this over time so that a more rigorous statement can be made -- we would like to +provably prevent collisions in the encoding even when the structure types are distinct in an appropriate sense, +and it's not clear that that is provable at this revision. It merely seems likely. + +It would also be of interest to integrate with merlin, if only to support the `dalek-cryptography` ecosystem +which is building up around it. Structure hashing is full of pitfalls and yet it is a common need. + +`Digestible` is potentially valuable because it provides a principled, systematic approach to the problem, seems comparable +to other practical solutions, and is easy to use especially by non-cryptographers, who can easily modify +structures and make new ones which derive digestible, and don't have to try to +determine appropriate domain separation labels, or figure out when to insert framing, manually. +Nevertheless it is WIP and the present revision is not going to be the ending state of the project. + +FIXME: Find and mention the bitcoin and cryptonote and merkle tree examples re: framing and domain separation + +References ---------- -For background, the following discussions explain why `Hash` is not adequate / should not be -extended to try to cover this use-case: - -> https://github.com/RustCrypto/traits/issues/13 -> -> https://github.com/RustCrypto/hashes/issues/29 -> -> https://github.com/RustCrypto/utils/issues/2 -> -> https://crates.io/crates/digest-hash -> -> As it stands now, the current recommendation from the RFC you mentioned boils down to "use STROBE", which is also not algorithm agnostic. - -We built `Digestible` because we think it's the simplest thing that meets our needs. - -We use the `Digest` trait from the `digest` crate which has been embaced by `RustCrypto` -to model a generic cryptographic hash function. - -Goals ------ - -`Digestible` is a trait that can be derived on your structs. - -`Digestible` attempts to ensure that if you use a secure hash function (implementing trait `Digest`), -then an attacker will not be able to produce an instance of a struct in your program that has the same hash -as another instance of a struct in your program unless they are identical. (Because some padding bytes will -be different if they are not the same type, and all of the bytes of the struct will be used.) - -(Here, we are really only interested in structs that form the basis of a protocol or serialization format. -We're not interested in adding magic to hashes of [u8; 32] or making sure that a [u8; 32] doesn't have the -same hash as GenericArray.) - -`Digestible` attempts to ensure that digests are endian agnostic. - -`Digestible` introduces little-to-no performance penalty, and does not make dynamic memory allocations. - -`Digestible` is `no_std` compatible, and has optional integration with `alloc` crate. - -Differences with `digest-hash` ------------------------------- - -Our crate is similar at a glance to `digest-hash` but it has a few major differences - -(1) We insert bytes depending on the struct name and between members, `digest-hash` does not, - so it doesn't achieve our goals. -(2) `digest-hash` does not provide a derive macro. But implementing the trait is sensitive, - and if implemented by hand, probably requires review by a cryptographer. By providing - `derive(Digestible)`, we reduce the opportunity for mistakes and make it less onerous to use - in a large team. -(3) `digest-hash` supports configurable endianness, but we don't want or need this. -(4) `digest-hash` doesn't support a bunch of core types that we need like `Vec`, `Option`, - and so we would need to patch or wrap everything in newtypes, and then ourselves add things like - "hash the length of the vec" in order to proect against length extension attacks. - -Finally we think the long term goal of basing `digest-hash` on `serde` is not a great idea, as `serde` is -extremely complicated, and we are not actually interested in serialization, just hashing. -It's far simpler to just write our own proc macro that does the thing that we need -- `digestible_derive` -is only about one hundred lines of code at time of writing, and with no dependence on `serde`, we are free -to uprev serialization libs without worrying about any interaction with `digestible` trait, and to e.g. build -our SGX enclave without any dependency on `serde`. +TODO diff --git a/crypto/ecies/README.md b/crypto/ecies/README.md deleted file mode 100644 index 1c6302d764..0000000000 --- a/crypto/ecies/README.md +++ /dev/null @@ -1,32 +0,0 @@ -ECIES -===== - -Provides a simple rust interface for doing asymmetric key cryptography, -using the ECIES encryption scheme. - -Uses the `curve25519_dalek` types as keys (`Scalar` and `RistrettoPoint`). - -This crate is `no_std`. - -`encrypt(rng: RngCore::Rng, key: CurvePoint , plaintext: &[u8]) -> Vec` -`decrypt(key: CurveScalar, ciphertext: &[u8]) -> Result, ()>` - -In order to work, the decryption key `a` must match the encryption key `A` via -`A = a * G`, as in everything else. Any key generation mechanism consistent -with this is fine. - -Implementation --------------- - -At present revision we are implementing the interface using ECIES scheme: -- During encryption we perform DH against the public key (creating a Pubkey of - our own), followed by a KDF of the resulting shared secret, to get input to - the AES block cipher. The public key that we generated, and the block cipher - output, form the ciphertext. -- During decryption we read the pub key first, use the private key to recover - the shared secret, and then decrypt the block cipher output. - -It's possible that we should actually be using x25519 crate directly. -However, it seems to me that that would require using MontgomeryPoint instead of -RistrettoPoint, and we seem to be trying to use RistrettoPoint in most places, -e.g. for cryptonote. So I'm trying to follow that lead. diff --git a/crypto/ecies/src/lib.rs b/crypto/ecies/src/lib.rs deleted file mode 100644 index ca10571e12..0000000000 --- a/crypto/ecies/src/lib.rs +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) 2018-2020 MobileCoin Inc. - -#![no_std] -// Following curve25519 ristretto comments -#![allow(non_snake_case)] - -// Note(chris): In SGX maybe should use sgx_tcrypto? -// Note(chris): In discussion, we wanted to use aes-gcm -// but at time of writing it's not available in no_std -// aes-ctr is very close to aes-gcm, but lacks -// stream-cipher authentication functionality. -// -// Since we only plan -// to use this crate to send messages that are less than -// a block, and there's no possibility that mallory could -// get in between the blocks and change them in flight, -// stream-cipher functionality seems overkill anyways -// -// Projected uses are: account hints, encrypted tx's in -// recovery ledger. -// -// In these cases tampering with the encrypted value -// only means Bob won't be able to find his transactions -// at the next step, not that Mallory can steal them or -// trick Bob into telling them to her. -extern crate alloc; - -#[cfg(test)] -extern crate test_helper; - -use aes_ctr::{ - stream_cipher::{generic_array::GenericArray, NewStreamCipher, SyncStreamCipher}, - Aes256Ctr, -}; -use alloc::{vec, vec::Vec}; -use core::convert::TryFrom; -use hkdf::Hkdf; -use keys::{CompressedRistrettoPublic, RistrettoPrivate, RistrettoPublic, RISTRETTO_PUBLIC_LEN}; -use rand_core::{CryptoRng, RngCore}; -use sha2::Sha256; - -/// The amount of bytes by which the cipher text is longer then plaintext. -/// This is a constant and at this revision is equal to the length of one ristretto curve point. -/// Users of ecies crate can use this constant to future proof against changes in ECIES crate. -pub const ECIES_EXTRA_SPACE: usize = RISTRETTO_PUBLIC_LEN; - -/// Encrypt -/// -/// # Arguments -/// * rng: Rng to use for the encryption operation -/// * key: Public key to encrypt against -/// * plaintext: The messages to be encrypted -/// -/// # Returns -/// * Vec the encrypted payload (ciphertext) -/// -/// Encryption can fail only if the input is not a valid compressed Ristretto point -pub fn encrypt( - rng: &mut T, - key: &RistrettoPublic, - plaintext: &[u8], -) -> Vec { - encrypt_with_salt(rng, key, plaintext, &DEFAULT_HKDF_SALT) -} - -/// encrypt_with_salt -/// Same as encrypt, but takes an explicit salt value -/// See https://tools.ietf.org/html/rfc5869 Sec 3.1 for discussion -/// The salt is an optional random, non-secret value which can be sent in the -/// clear, and reused, but it improves security properties if it is provided, -/// even if it is not random. -/// By default we use a hard-coded salt value, unless you can provide one. -/// -/// # Arguments -/// * rng: Rng to use for encryption operation -/// * key: Public key to encrypt against -/// * plaintext: The message to be encrypted -/// * hkdf_salt: The (public) salt to use with hkdf function -/// -#[inline] -pub fn encrypt_with_salt( - rng: &mut T, - key: &RistrettoPublic, - plaintext: &[u8], - hkdf_salt: &[u8; 32], -) -> Vec { - let mut result = vec![0u8; RISTRETTO_PUBLIC_LEN + plaintext.len()]; - - encrypt_into(rng, &key, plaintext, hkdf_salt, &mut result); - - result -} - -/// encrypt_into -/// Same as encrypt, but cannot fail, and doesn't make an allocation. -/// The output buffer must be exactly 32 bytes longer than input buffer. -/// This version is easier to use when constant time operation is required. -/// -/// # Arguments -/// * rng: Rng to use for encryption operation -/// * key: Public key to encrypt against -/// * plaintext: The message to be encrypted -/// * hkdf_salt: The (public) salt to use with hkdf function -/// -#[inline] -pub fn encrypt_into( - rng: &mut T, - key: &RistrettoPublic, - plaintext: &[u8], - hkdf_salt: &[u8; 32], - output: &mut [u8], -) { - debug_assert!(plaintext.len() + RISTRETTO_PUBLIC_LEN == output.len()); - // ECDH - use keys::KexPublic; - let (our_public, shared_secret) = key.new_secret(rng); - - let compressed_public = CompressedRistrettoPublic::from(our_public); - let compressed_public_bytes: &[u8] = compressed_public.as_ref(); - output[0..RISTRETTO_PUBLIC_LEN].clone_from_slice(compressed_public_bytes); - - // Copy plaintext to place where ciphertext will go - let dst = &mut output[RISTRETTO_PUBLIC_LEN..]; - dst.clone_from_slice(plaintext); - - // KDF + AES - common_part_with_salt(shared_secret.as_ref(), hkdf_salt, dst); -} - -/// decrypt -/// -/// # Arguments -/// * key: Private key to decrypt with -/// * ciphertext: The encrypted payload to decipher -/// -/// # Returns -/// * Vec the plaintext, or an error -/// -/// Decryption can fail if the decryption key doesn't match the encryption key -/// Depending on cryptosystem this can be detected by some MAC or by AES-GCM, etc. -/// The details of the failure are generally unhelpful, and cannot be usefully -/// distinguished programmatically at runtime, so the error type is () -/// -pub fn decrypt(key: &RistrettoPrivate, ciphertext: &[u8]) -> Result, ()> { - decrypt_with_salt(key, ciphertext, &DEFAULT_HKDF_SALT) -} - -/// decrypt_with_salt -/// Counterpart to encrypt_with_salt -/// -/// # Arguments -/// * key: Private key to decrypt with -/// * ciphertext: The encrypted payload to decipher -/// * hkdf_salt: The (public) salt to use with hkdf function -/// -#[inline] -pub fn decrypt_with_salt( - key: &RistrettoPrivate, - ciphertext: &[u8], - hkdf_salt: &[u8; 32], -) -> Result, ()> { - if ciphertext.len() < RISTRETTO_PUBLIC_LEN { - return Err(()); - } - - let mut result = vec![0u8; ciphertext.len() - RISTRETTO_PUBLIC_LEN]; - - decrypt_into(key, ciphertext, hkdf_salt, &mut result)?; - - Ok(result) -} - -/// decrypt_into -/// Counterpart to encrypt_into -/// Output buffer must be exactly 32 bytes smaller than ciphertext buffer -/// -/// # Arguments -/// * key: Private key (dalek Scalar) to decrypt with -/// * ciphertext: The encrypted payload to decipher -/// * hkdf_salt: The (public) salt to use with hkdf function -/// * output: Where to place the plaintext -/// -/// # Returns -/// * Ok on success, Err if the first 32 bytes were malformed -#[inline] -pub fn decrypt_into( - key: &RistrettoPrivate, - ciphertext: &[u8], - hkdf_salt: &[u8; 32], - output: &mut [u8], -) -> Result<(), ()> { - debug_assert!(output.len() + RISTRETTO_PUBLIC_LEN == ciphertext.len()); - - // ECDH - use keys::KexReusablePrivate; - let B = RistrettoPublic::try_from(&ciphertext[0..RISTRETTO_PUBLIC_LEN]).map_err(|_| ())?; - let shared_secret = key.key_exchange(&B); - - // Copy ciphertext to place where plaintext will go - let dst = &mut output[..]; - dst.clone_from_slice(&ciphertext[RISTRETTO_PUBLIC_LEN..]); - - // KDF + AES - common_part_with_salt(shared_secret.as_ref(), hkdf_salt, dst); - - Ok(()) -} - -// Symmetric part, common to encrypt and decrypt -// Factored out to avoid duplication -// -// This includes both the KDF step and the AES step -// -// Arguments: -// shared_secret: The DH shared secret bytes -// hkdf_salt: The 32 byte salt to use with hkdf -// data: The mutable data buffer to which we will apply the aes cipher -// -#[inline] -fn common_part_with_salt(shared_secret: &[u8; 32], hkdf_salt: &[u8; 32], data: &mut [u8]) { - // KDF - let (_, hk) = Hkdf::::extract(None, shared_secret); - let mut key = [0u8; 32]; - hk.expand(hkdf_salt, &mut key).unwrap(); // This can never fail as 32 bytes is a valid amount of data that Sha256 can output - - // AES - let nonce = [0u8; 16]; // 16 because using ctr128 - let aes_key = GenericArray::from_slice(&key); - let aes_nonce = GenericArray::from_slice(&nonce); - let mut cipher = Aes256Ctr::new(&aes_key, &aes_nonce); - cipher.apply_keystream(data); -} - -// DEFAULT_HKDF_SALT: -// Using a salt with hkdf is optional, see discussion at -// https://tools.ietf.org/html/rfc5869 Sec 3.1 for discussion -// I chose these using random.org, for the case when the user cannot provide one -pub const DEFAULT_HKDF_SALT: [u8; 32] = [ - 21, 67, 69, 69, 93, 127, 39, 5, 45, 76, 45, 193, 107, 91, 70, 182, 44, 43, 174, 32, 88, 22, - 190, 170, 242, 187, 148, 63, 195, 2, 164, 188, -]; - -#[cfg(test)] -mod test { - use super::*; - use keys::FromRandom; - - #[test] - fn test_round_trip() { - let plaintext1 = b"01234567".to_vec(); - let plaintext2 = plaintext1.repeat(50); - - test_helper::run_with_several_seeds(|mut rng| { - let a = RistrettoPrivate::from_random(&mut rng); - let A = RistrettoPublic::from(&a); - - for plaintext in &[&plaintext1[..], &plaintext2[..]] { - for _reps in 0..50 { - let ciphertext = encrypt(&mut rng, &A, plaintext); - let decrypted = decrypt(&a, &ciphertext).expect("decryption failed!"); - assert_eq!(plaintext.len(), decrypted.len()); - assert_eq!(plaintext, &&decrypted[..]); - } - } - }); - } - - #[test] - fn test_expected_failure() { - let plaintext1 = b"01234567".to_vec(); - let plaintext2 = plaintext1.repeat(50); - - test_helper::run_with_several_seeds(|mut rng| { - let a = RistrettoPrivate::from_random(&mut rng); - let A = RistrettoPublic::from(&a); - - let not_a = RistrettoPrivate::from_random(&mut rng); - - for plaintext in &[&plaintext1[..], &plaintext2[..]] { - for _reps in 0..50 { - let ciphertext = encrypt(&mut rng, &A, plaintext); - let decrypted = decrypt(¬_a, &ciphertext).expect("decryption failed!"); - assert_eq!(plaintext.len(), decrypted.len()); - assert_ne!(plaintext, &&decrypted[..]); - } - } - }); - } -} diff --git a/docker/Dockerfile b/docker/Dockerfile index 4af02c7eec..dd617bf38e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -120,6 +120,6 @@ RUN if [ "$DOCKER_NO_SGX" = "false" ] ; then cd /tmp && chmod +x install_sgx.sh ENV SGX_SDK=/opt/intel/sgxsdk ENV PATH=$PATH:$SGX_SDK/bin:$SGX_SDK/bin/x64 ENV PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$SGX_SDK/pkgconfig -ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$SGX_SDK/sdk_libs +ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$SGX_SDK/sdk_libs:$SGX_SDK/lib64 WORKDIR / diff --git a/docker/Dockerfile-version b/docker/Dockerfile-version index ecee7aa058..620d6ad397 100644 --- a/docker/Dockerfile-version +++ b/docker/Dockerfile-version @@ -1 +1 @@ -1_8 +1_9 diff --git a/ledger/db/src/lib.rs b/ledger/db/src/lib.rs index 4dff0604e7..bd538bcb00 100644 --- a/ledger/db/src/lib.rs +++ b/ledger/db/src/lib.rs @@ -715,7 +715,7 @@ mod ledger_db_test { ledger_db.get_transactions_by_block(1).unwrap() ); - let key_images: &Vec = tx.key_images(); + let key_images: Vec = tx.key_images(); assert_eq!(1, key_images.len()); assert!(ledger_db @@ -723,7 +723,7 @@ mod ledger_db_test { .unwrap()); let block_one_key_images = ledger_db.get_key_images_by_block(1).unwrap(); - assert_eq!(*key_images, block_one_key_images); + assert_eq!(key_images, block_one_key_images); } #[test] diff --git a/ledger/distribution/src/main.rs b/ledger/distribution/src/main.rs index 957e2a1da4..0b34a18f3d 100644 --- a/ledger/distribution/src/main.rs +++ b/ledger/distribution/src/main.rs @@ -8,7 +8,7 @@ pub mod uri; use crate::uri::{Destination, Uri}; use common::logger::{create_app_logger, log, o, Logger}; use ledger_db::{Error as LedgerDbError, Ledger, LedgerDB}; -use mobilecoin_api::{blockchain, conversions::block_num_to_s3block_path, transaction as tx_grpc}; +use mobilecoin_api::{blockchain, conversions::block_num_to_s3block_path, external}; use protobuf::Message; use rusoto_core::{Region, RusotoError}; use rusoto_s3::{PutObjectError, PutObjectRequest, S3Client, S3}; @@ -151,7 +151,10 @@ impl BlockHandler for S3BlockWriter { log::info!(self.logger, "S3: Handling block {}", block.index); let bc_block = blockchain::Block::from(block); - let bc_transactions = transactions.iter().map(tx_grpc::RedactedTx::from).collect(); + let bc_transactions = transactions + .iter() + .map(external::RedactedTx::from) + .collect(); let mut s3_block = blockchain::S3Block::new(); s3_block.set_block(bc_block); @@ -203,7 +206,10 @@ impl BlockHandler for LocalBlockWriter { log::info!(self.logger, "S3: Handling block {}", block.index); let bc_block = blockchain::Block::from(block); - let bc_transactions = transactions.iter().map(tx_grpc::RedactedTx::from).collect(); + let bc_transactions = transactions + .iter() + .map(external::RedactedTx::from) + .collect(); let mut s3_block = blockchain::S3Block::new(); s3_block.set_block(bc_block); diff --git a/ledger/sync/src/reqwest_transactions_fetcher.rs b/ledger/sync/src/reqwest_transactions_fetcher.rs index 219a58b8c1..6f778efb59 100644 --- a/ledger/sync/src/reqwest_transactions_fetcher.rs +++ b/ledger/sync/src/reqwest_transactions_fetcher.rs @@ -129,15 +129,15 @@ impl ReqwestTransactionsFetcher { format!("block conversion failed: {:?}", err), ) })?; - let mut tx_stored = Vec::new(); + let mut redacted_transactions = Vec::new(); for tx in bc_block.get_transactions().iter() { - let txs = RedactedTx::try_from(tx).map_err(|err| { + let redacted_tx = RedactedTx::try_from(tx).map_err(|err| { ReqwestTransactionsFetcherError::InvalidBlockReceived( url.to_string(), format!("tx conversion failed: {:?}", err), ) })?; - tx_stored.push(txs); + redacted_transactions.push(redacted_tx); } let signature = bc_block @@ -164,7 +164,7 @@ impl ReqwestTransactionsFetcher { let s3_block_data = S3BlockData { block: lg_block, - transactions: tx_stored, + transactions: redacted_transactions, signature, }; Ok(s3_block_data) diff --git a/ledger/sync/src/test_app/main.rs b/ledger/sync/src/test_app/main.rs index 3dc922932d..4ab5abeace 100644 --- a/ledger/sync/src/test_app/main.rs +++ b/ledger/sync/src/test_app/main.rs @@ -14,7 +14,7 @@ use std::{convert::TryFrom, path::PathBuf, str::FromStr, sync::Arc}; use tempdir::TempDir; use transaction::account_keys::AccountKey; -const NETWORK: &str = "master"; +const NETWORK: &str = "test"; fn _make_ledger_long(ledger: &mut LedgerDB) { use rand::{rngs::StdRng, SeedableRng}; @@ -138,13 +138,13 @@ fn main() { let transactions_fetcher = ledger_sync::ReqwestTransactionsFetcher::new( vec![ String::from( - "https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.master.mobilecoin.com/", + "https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/", ), String::from( - "https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node3.master.mobilecoin.com/", + "https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node3.test.mobilecoin.com/", ), String::from( - "https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node4.master.mobilecoin.com/", + "https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node4.test.mobilecoin.com/", ), ], logger.clone(), diff --git a/mcbuild/enclave/Cargo.toml b/mcbuild/enclave/Cargo.toml index 00dc7bb854..1d06b6bc57 100644 --- a/mcbuild/enclave/Cargo.toml +++ b/mcbuild/enclave/Cargo.toml @@ -11,10 +11,10 @@ mcbuild-utils = { path = "../utils" } mcbuild-sgx-utils = { path = "../sgx-utils" } sgx_css = { path = "../../sgx/sgx_css" } -base64 = "0.11" +mbedtls = { git = "https://github.com/mobilecoinofficial/rust-mbedtls.git", tag = "mc-0.2", default-features = false } +mbedtls-sys-auto = { git = "https://github.com/mobilecoinofficial/rust-mbedtls.git", tag = "mc-0.2", default-features = false } + cargo-emit = "0.1.1" cargo_metadata = "0.9" failure = { version = "0.1.6", default-features = false } rand = "0.7" -rsa = "0.2" -sha2 = "0.8" diff --git a/mcbuild/enclave/src/lib.rs b/mcbuild/enclave/src/lib.rs index 74eb329a93..bcecd035e6 100644 --- a/mcbuild/enclave/src/lib.rs +++ b/mcbuild/enclave/src/lib.rs @@ -3,30 +3,43 @@ #![feature(external_doc)] #![doc(include = "../README.md")] -use base64::encode; use cargo_emit::{rerun_if_changed, rustc_env, warning}; use cargo_metadata::{CargoOpt, Error as MetadataError, Metadata, MetadataCommand}; use failure::Fail; +use mbedtls::{pk::Pk, rng::RngCallback}; +use mbedtls_sys::types::{ + raw_types::{c_int, c_uchar, c_void}, + size_t, +}; use mcbuild_sgx_utils::{ConfigBuilder, IasMode, SgxEnvironment, SgxMode, SgxSign}; use mcbuild_utils::{rerun_if_path_changed, CargoBuilder, Environment}; -use rand::rngs::OsRng; -use rsa::{errors::Error as RsaError, hash::Hashes, PaddingScheme, PublicKey, RSAPrivateKey}; +use rand::{thread_rng, RngCore}; use sgx_css::{Error as SignatureError, Signature}; -use sha2::{digest::Digest, Sha256}; use std::{ convert::TryFrom, - fs::{self, File}, - io::{Error as IoError, Write}, + fs, + io::Error as IoError, path::{Path, PathBuf}, process::Command, + ptr, slice, sync::PoisonError, }; -const RSA_DER_PREFIX: [u8; 33] = [ - 0x30, 0x82, 0x01, 0xA2, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, - 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x8F, 0x00, 0x30, 0x82, 0x01, 0x8A, 0x02, 0x82, 0x01, 0x81, - 0x00, -]; +struct ThreadRngForMbedTls; + +impl RngCallback for ThreadRngForMbedTls { + #[inline(always)] + unsafe extern "C" fn call(_: *mut c_void, data: *mut c_uchar, len: size_t) -> c_int { + let outbuf = slice::from_raw_parts_mut(data, len); + let mut csprng = thread_rng(); + csprng.fill_bytes(outbuf); + 0 + } + + fn data_ptr(&mut self) -> *mut c_void { + ptr::null_mut() + } +} /// An enumeration of builder errors. #[derive(Debug, Fail)] @@ -63,10 +76,6 @@ pub enum Error { #[fail(display = "sgx_sign gendata failed")] SgxSignGendata, - /// There was an error generating an RSA private key. - #[fail(display = "RSA error: {}", _0)] - Rsa(RsaError), - /// The gendata to be signed doesn't match what the given unsigned enclave produces. #[fail(display = "The given gendata doesn't match the unsigned enclave")] BadGendata, @@ -102,12 +111,6 @@ impl From for Error { } } -impl From for Error { - fn from(src: RsaError) -> Error { - Error::Rsa(src) - } -} - impl From for Error { fn from(src: MetadataError) -> Error { Error::Metadata(src) @@ -429,78 +432,64 @@ impl Builder { }; } - let (pubkey, signature) = - if enclave_rebuilt || (self.pubkey.is_none() && self.signature.is_none()) { - warning!("Generating single-use key for insecure, one-shot signature"); - - let mut pubkey = self.out_dir.join(&self.name); - let mut signature = pubkey.clone(); - pubkey.set_extension("pub"); - signature.set_extension("sig"); - - // Get the hash of the gendata output - let data = fs::read(&gendata)?; - let mut hasher = Sha256::default(); - hasher.input(&data); - let digest = hasher.result(); - - // Generate a new key and sign the gendata hash - let mut rng = OsRng::default(); - let private = RSAPrivateKey::new(&mut rng, 3072)?; - let sig = private.sign( - PaddingScheme::PKCS1v15, - Some(&Hashes::SHA2_256), - digest.as_slice(), - )?; - fs::write(&signature, &sig)?; - - let public = private.to_public_key(); - - // Build the pubkey DER - let mut pubkey_der = Vec::with_capacity(512); - pubkey_der.extend_from_slice(&RSA_DER_PREFIX[..]); - pubkey_der.append(&mut public.n().to_bytes_be()); - let mut exponent_bytes = public.e().to_bytes_be(); - let exponent_len = exponent_bytes.len() as u8; - pubkey_der.extend_from_slice(&[0x02, exponent_len]); - pubkey_der.append(&mut exponent_bytes); - - // Write the PEM - let mut pubkey_file = File::create(&pubkey)?; - write!(&mut pubkey_file, "-----BEGIN PUBLIC KEY-----")?; - write!(&mut pubkey_file, "{}", encode(&pubkey_der))?; - write!(&mut pubkey_file, "-----END PUBLIC KEY-----")?; - (pubkey, signature) - } else { - let pubkey = self.pubkey.as_ref().unwrap(); - let signature = self.signature.as_ref().unwrap(); - rerun_if_changed!(signature - .as_os_str() - .to_str() - .expect("Invalid UTF-8 in signature path")); - rerun_if_changed!(pubkey - .as_os_str() - .to_str() - .expect("Invalid UTF-8 in pubkey path")); - (pubkey.clone(), signature.clone()) - }; + if enclave_rebuilt || (self.pubkey.is_none() && self.signature.is_none()) { + warning!("Generating single-use key for insecure, one-shot signature"); - if self - .signer - .catsig( - &unsigned_enclave, - &config_xml, - &pubkey, - &gendata, - &signature, - signed_enclave, + let mut csprng = ThreadRngForMbedTls {}; + + let mut privkey = + Pk::generate_rsa(&mut csprng, 3072, 3).expect("Could not generate privkey"); + + let mut private_key = self.out_dir.join(&self.name); + private_key.set_extension("key"); + + fs::write( + &private_key, + privkey + .write_private_pem_string() + .expect("Could not write PEM string for private key"), ) - .status()? - .success() - { - Ok(()) + .expect("Could not write PEM string to private key file"); + + if self + .signer + .sign(&unsigned_enclave, &config_xml, &private_key, signed_enclave) + .status()? + .success() + { + Ok(()) + } else { + Err(Error::SgxSign) + } } else { - Err(Error::SgxSignCatsig) + let pubkey = self.pubkey.as_ref().unwrap(); + let signature = self.signature.as_ref().unwrap(); + rerun_if_changed!(signature + .as_os_str() + .to_str() + .expect("Invalid UTF-8 in signature path")); + rerun_if_changed!(pubkey + .as_os_str() + .to_str() + .expect("Invalid UTF-8 in pubkey path")); + + if self + .signer + .catsig( + &unsigned_enclave, + &config_xml, + &pubkey, + &gendata, + &signature, + signed_enclave, + ) + .status()? + .success() + { + Ok(()) + } else { + Err(Error::SgxSignCatsig) + } } } diff --git a/mob b/mob index e00b13218d..23789e2ec8 100755 --- a/mob +++ b/mob @@ -170,6 +170,24 @@ def get_git_commit(): # Environment checks ## +# Check if docker is available and bail out early with a message if appropriate +if not args.dry_run: + cmd = "command -v docker > /dev/null 2>&1" + vprint(cmd) + try: + retcode = subprocess.call(cmd, shell=True) + except: + eprint("Warning: unable to check for docker being installed") + else: + if retcode != 0: + if not args.verbose: + eprint(cmd) + eprint("docker not found: retcode =", retcode) + eprint("Docker is required to use the mob tool.") + eprint("It is recommended to install from https://docs.docker.com/get-docker/") + sys.exit(1) + +# Check for any SSH handling if "SSH_AUTH_SOCK" in os.environ: if platform.system().lower() != "linux": vprint("SSH Auth Socket found, but not running on linux") @@ -379,6 +397,9 @@ if args.action == "prompt": "--expose", "3228", "--expose", "4444", ]) + docker_run.extend([ + "--publish", "4444:4444", + ]) if args.expose is not None: for port in args.expose: docker_run.extend(["--expose", port]) diff --git a/mobilecoind/Cargo.toml b/mobilecoind/Cargo.toml index 0a549f515f..926a2b0523 100644 --- a/mobilecoind/Cargo.toml +++ b/mobilecoind/Cargo.toml @@ -17,6 +17,7 @@ grpc-util = { path = "../util/grpc" } keys = { path = "../crypto/keys" } ledger-db = { path = "../ledger/db" } ledger-sync = { path = "../ledger/sync" } +mc-b58-payloads = { path = "../util/b58-payloads" } mcconnection = { path = "../mcconnection" } mcrand = { path = "../crypto/mcrand" } mcserial = { path = "../util/mcserial" } diff --git a/mobilecoind/README.md b/mobilecoind/README.md index 8e8be3425d..64454eb3a4 100644 --- a/mobilecoind/README.md +++ b/mobilecoind/README.md @@ -12,6 +12,7 @@ Wallet Clients, such as a CLI, wanting to use wallet services can register their - [Getting Started](#getting-started) - [Setup](#setup) + - [Verifying Signed Enclaves](#verifying-signed-enclaves) - [Example Invocation](#example-invocation) ### Getting Started @@ -29,14 +30,47 @@ mc://node1.test.mobilecoin.com/ You will need to specify a ledger location to which to sync the ledger. This directory can be empty (or non-existent), or can contain the origin block, created from [generate_sample_ledger](../generate_sample_ledger/README.md). You will also need to specify a directory for the MobileCoin Daemon database, where keys and transaction data would be stored. +#### Verifying Signed Enclaves + +When mobilecoind connects to consensus validators, it verifies the integrity of their software using Intel's Secure Guard eXtensions (SGX) via attestation evidence. + +The consensus validator provides a signed measurement of its internal state to assure that it is running exactly the software you expect. You must provide a specific file to mobilecoind on startup so that it has the materials it needs to validate the enclave's evidence. + +The TestNet signature artifacts are available via + +``` +curl -O https://enclave-distribution.test.mobilecoin.com/production.json +``` + +This retrieves a json record of: + +```json +{ + "enclave": "pool/, + "sigstruct": "pool/, +} +``` + +The git revision refers to the TestNet release version, and provides the full path to the production version of the artifact. + +For example, MobileCoin's TestNet enclave signature materials are available via: + +``` +curl -O https://enclave-distribution.test.mobilecoin.com/pool/e57b6902aee60be45b78b496c1bef781746e4389/bf7fa957a6a94acb588851bc8767eca5776c79f4fc2aa6bcb99312c3c386c/consensus-enclave.css +``` + +Once you fetch the sigstruct artifact, you must provide the sigstruct to mobilecoind via the environment variable `CONSENSUS_ENCLAVE_CSS=$(pwd)/consensus-enclave.css`. + #### Example Invocation This invocation connects to two consensus validators in the MobileCoin demo network, uses their respective S3 buckets to download new blocks, polls every second for updates and provides a MobileCoinD API on port 4444. ->Note: The MobileCoin Daemon validates attestation evidence from the Consensus Validators, and so needs to know whether those validators are running with hardware SGX or in simulation mode, via the SGX_MODE variable. +>Note: The MobileCoin Daemon validates attestation evidence from the Consensus Validators, and so needs to know whether those validators are running with hardware SGX or in simulation mode, via the `SGX_MODE` variable. In addiiton it needs to know whether the enclave was built in debug or relase, via the `IAS_MODE` variable. ``` -SGX_MODE=HW MC_LOG=debug,rustls=warn,hyper=warn,tokio_reactor=warn,mio=warn,want=warn,rusoto_core=error,h2=error,reqwest=error cargo run --release -p mobilecoind -- \ +SGX_MODE=HW IAS_MODE=PROD CONSENSUS_ENCLAVE_CSS=$(pwd)/consensus-enclave.css \ + MC_LOG=debug,rustls=warn,hyper=warn,tokio_reactor=warn,mio=warn,want=warn,rusoto_core=error,h2=error,reqwest=error \ + cargo run --release -p mobilecoind -- \ --ledger-db /path/to/ledger \ --poll-interval 1 \ --peer mc://node1.test.mobilecoin.com/ \ diff --git a/mobilecoind/api/Cargo.toml b/mobilecoind/api/Cargo.toml index 987a33db57..f78ef0e632 100644 --- a/mobilecoind/api/Cargo.toml +++ b/mobilecoind/api/Cargo.toml @@ -23,4 +23,4 @@ rand = "0.7" hex_fmt = "0.3" [build-dependencies] -protoc-grpcio = "0.3.1" +mc-build-grpc = { path = "../../util/build-grpc" } diff --git a/mobilecoind/api/build.rs b/mobilecoind/api/build.rs index 6beacf6d92..82dd6c5e4e 100644 --- a/mobilecoind/api/build.rs +++ b/mobilecoind/api/build.rs @@ -1,25 +1,8 @@ // Copyright (c) 2018-2020 MobileCoin Inc. -extern crate protoc_grpcio; - -fn compile_protos() { - let proto_root = "./proto"; - let external_proto_dir = "../../consensus/api/proto"; - let proto_files = ["mobilecoind_api.proto"]; - let output_destination = "src"; - println!("cargo:rerun-if-changed={}", proto_root); - for file in &proto_files { - println!("cargo:rerun-if-changed={}/{}", proto_root, file); - } - - protoc_grpcio::compile_grpc_protos( - &proto_files, - &[proto_root, external_proto_dir], - output_destination, - ) - .expect("Failed to compile gRPC definitions!"); -} - fn main() { - compile_protos(); + mc_build_grpc::compile_protos_and_generate_mod_rs( + &["./proto", "../../consensus/api/proto"], + &["mobilecoind_api.proto"], + ); } diff --git a/mobilecoind/api/proto/mobilecoind_api.proto b/mobilecoind/api/proto/mobilecoind_api.proto index e8c55dc213..898cb9e47a 100644 --- a/mobilecoind/api/proto/mobilecoind_api.proto +++ b/mobilecoind/api/proto/mobilecoind_api.proto @@ -10,6 +10,9 @@ import "external.proto"; package mobilecoind_api; +option java_package = "com.mobilecoin.mobilecoind"; +option java_outer_classname = "MobileCoinDAPI"; + service MobilecoindAPI { // Monitors rpc AddMonitor (AddMonitorRequest) returns (AddMonitorResponse) {} @@ -368,7 +371,6 @@ message GenerateOptimizationTxResponse { } // Generate a transaction that can be used for a "MobileCoin Transfer Code" QR. -// TODO is that correct? message GenerateTransferCodeTxRequest { bytes sender_monitor_id = 1; uint64 change_subaddress = 2; @@ -376,10 +378,23 @@ message GenerateTransferCodeTxRequest { uint64 value = 4; uint64 fee = 5; uint64 tombstone = 6; + string memo = 7; } message GenerateTransferCodeTxResponse { + // The tx proposal to submit to the network. TxProposal tx_proposal = 1; + + // The entropy for constructing the AccountKey that can access the funds. bytes entropy = 2; + + // The TxOut public key that has the funds. + external.RistrettoPublic tx_public_key = 3; + + // The memo (simply copied from the request). + string memo = 4; + + // The b58-encoded Transfer Code + string b58_code = 5; } // Submits a transaction to the network. diff --git a/mobilecoind/api/src/.gitignore b/mobilecoind/api/src/.gitignore deleted file mode 100644 index ce85e611f8..0000000000 --- a/mobilecoind/api/src/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -mobilecoind_api.rs -mobilecoind_api_grpc.rs diff --git a/mobilecoind/api/src/lib.rs b/mobilecoind/api/src/lib.rs index 80048847cc..5bcc1141c7 100644 --- a/mobilecoind/api/src/lib.rs +++ b/mobilecoind/api/src/lib.rs @@ -2,15 +2,21 @@ //! mobilecoind gRPC API. -pub mod mobilecoind_api; -pub mod mobilecoind_api_grpc; - -pub mod empty { +mod autogenerated_code { + // Expose proto data types from included third-party/external proto files. + pub use mobilecoin_api::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 crate::{conversions::ConversionError, mobilecoind_api::*}; -pub use mobilecoin_api::external; -pub use protobuf::well_known_types::Empty; +pub use crate::conversions::ConversionError; +pub use autogenerated_code::{mobilecoind_api::*, *}; pub mod conversions; diff --git a/mobilecoind/clients/java/mob_client/.gitattributes b/mobilecoind/clients/java/mob_client/.gitattributes new file mode 100644 index 0000000000..00a51aff5e --- /dev/null +++ b/mobilecoind/clients/java/mob_client/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/mobilecoind/clients/java/mob_client/.gitignore b/mobilecoind/clients/java/mob_client/.gitignore new file mode 100644 index 0000000000..d67a5bc660 --- /dev/null +++ b/mobilecoind/clients/java/mob_client/.gitignore @@ -0,0 +1,6 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build +bin diff --git a/mobilecoind/clients/java/mob_client/README.md b/mobilecoind/clients/java/mob_client/README.md new file mode 100644 index 0000000000..eab36e9245 --- /dev/null +++ b/mobilecoind/clients/java/mob_client/README.md @@ -0,0 +1,52 @@ +# Java sample client and CLI tool + +This code serves both as an example of how to use the services provided by mobilecoind +in Java through gRPC and as a CLI tool to interact with mobilecoind. + +To use it, you must have an instance of mobilecoind running, this is typically run +locally. You can find instructions at https://github.com/mobilecoinofficial/mobilecoin/tree/master/mobilecoind + +A gradle wrapper is included, `gradlew` (or `gradlew.bat` on Windows). The most simple call, assuming you are running +mobilecoind on port 4444 is to create a new root entropy key with the command + +```./gradlew run --args='-s localhost:4444 -c generate-entropy'``` + +This specifies a connection to `localhost` on port `4444` and the commaned `generate-entropy` + +If this works, you'll recieve a 256-bit key, encoded as a hex string that might look like this `7ecaf368fa0ce478987b33acd7f9fc9b8b7dacf05a8205668700772e11f98a8d` + +Rather than run all the commands using `gradlew`, you can build a distribution using `./gradlew distZip` which will build a usable binary. You can also get the distribution `mob_client.zip` from the releases page. + +To track the transactions in your account, you can add a monitor for this key. A monitor continually scans the ledger for transactions that belong to a key over a large range of subaddresses. The `monitor` command will tell mobilecoind to track a specific key. + +```./mob_client -s localhost:4444 -c monitor -e 7ecaf368fa0ce478987b33acd7f9fc9b8b7dacf05a8205668700772e11f98a8d``` + +This will return a monitor key, which is also a 256-bit hex encoded key: + +`3371207b834e40d9af2c16e762598d7a4e76c4c2d46f90038a374a8bfdff2c80` + +The monitor is now active and will take care of tracking all incoming and spent transactions. You can use this monitor ID to check +the balance for both the key and specific subaddress index: + +```./mob_client -s localhost:4444 -c balance -m 3371207b834e40d9af2c16e762598d7a4e76c4c2d46f90038a374a8bfdff2c80 -i 0``` + +Of course since this is a freshly generated account key the balance will be zero. We will be distributing test coins to those +who are interested in helping us with the testnet. + +To recieve a payment you'll need to generate a request code (or public address) using the `request` command which you +can then share with with someone else. +```./mob_client -s localhost:4444 -c request -m 3371207b834e40d9af2c16e762598d7a4e76c4c2d46f90038a374a8bfdff2c80 -i 1``` + +Every key and subaddress index has a unique code. These are represented as b58 encoded strings and include a checksum so +they will not accidentally be mistyped, e.g.: + +```3WkD1Caa5XtfogSX1k7tRpq7BUCLSgZdfhjXcZHYV9oj68G3ebjkRwqe8HvSeCobD2iEnAib8VssosjwXDE6btSGMXZ3pQnmKGWGqwSaJvLwWo``` + +You can use this request code to transfer between your own subaddress indices, or you can give it to someone else and +have them send you a payment using the `transfer` command: + +```./mob_client -s localhost:4444 -c transfer -m 3371207b834e40d9af2c16e762598d7a4e76c4c2d46f90038a374a8bfdff2c80 -i 0 -r 3WkD1Caa5XtfogSX1k7tRpq7BUCLSgZdfhjXcZHYV9oj68G3ebjkRwqe8HvSeCobD2iEnAib8VssosjwXDE6btSGMXZ3pQnmKGWGqwSaJvLwWo -a 50000``` + +This will print a `reciept` which you can use to check the status of your transfer using the `status` command: + +```./mob_client -s localhost:4444 -c status -r 1ad7dfb8a36a637d717aa7b272e49ce05d267ad1927898b7f2b5b9a40306a443:116``` diff --git a/mobilecoind/clients/java/mob_client/build.gradle b/mobilecoind/clients/java/mob_client/build.gradle new file mode 100644 index 0000000000..f818686459 --- /dev/null +++ b/mobilecoind/clients/java/mob_client/build.gradle @@ -0,0 +1,55 @@ +plugins { + // Apply the java plugin to add support for Java + id 'java' + // Apply the application plugin to add support for building a CLI application. + id 'application' + // Protocol buffers + id 'com.google.protobuf' version '0.8.8' +} +compileJava { + sourceCompatibility = JavaVersion.VERSION_1_10 + targetCompatibility = JavaVersion.VERSION_1_10 +} +repositories { + // Use jcenter for resolving dependencies. + // You can declare any Maven/Ivy/file repository here. + jcenter() +} +dependencies { + // Required for compiled protocol buffers + compile group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2' + + // Apache commons for CLI and hex-parsing + implementation 'commons-cli:commons-cli:1.4' + implementation 'commons-codec:commons-codec:1.14' + + // gRPC and protobuf libraries + implementation 'com.google.guava:guava:28.2-jre' + implementation 'io.grpc:grpc-netty-shaded:1.28.1' + implementation 'io.grpc:grpc-protobuf:1.28.1' + implementation 'io.grpc:grpc-stub:1.28.1' + + // Use JUnit test framework + testImplementation 'junit:junit:4.12' +} +application { + // Define the main class for the application. + mainClassName = 'com.mobilecoin.mob_client.App' +} + +// Compile the protocol buffers in src/main/proto +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.11.0" + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.28.1' + } + } + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} \ No newline at end of file diff --git a/mobilecoind/clients/java/mob_client/gradle/wrapper/gradle-wrapper.jar b/mobilecoind/clients/java/mob_client/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..490fda8577df6c95960ba7077c43220e5bb2c0d9 GIT binary patch literal 58694 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 literal 0 HcmV?d00001 diff --git a/mobilecoind/clients/java/mob_client/gradle/wrapper/gradle-wrapper.properties b/mobilecoind/clients/java/mob_client/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..a4b4429748 --- /dev/null +++ b/mobilecoind/clients/java/mob_client/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mobilecoind/clients/java/mob_client/gradlew b/mobilecoind/clients/java/mob_client/gradlew new file mode 100755 index 0000000000..2fe81a7d95 --- /dev/null +++ b/mobilecoind/clients/java/mob_client/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/mobilecoind/clients/java/mob_client/gradlew.bat b/mobilecoind/clients/java/mob_client/gradlew.bat new file mode 100644 index 0000000000..62bd9b9cce --- /dev/null +++ b/mobilecoind/clients/java/mob_client/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mobilecoind/clients/java/mob_client/settings.gradle b/mobilecoind/clients/java/mob_client/settings.gradle new file mode 100644 index 0000000000..20b1858381 --- /dev/null +++ b/mobilecoind/clients/java/mob_client/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.3/userguide/multi_project_builds.html + */ + +rootProject.name = 'mob_client' diff --git a/mobilecoind/clients/java/mob_client/src/main/java/com/mobilecoin/mob_client/App.java b/mobilecoind/clients/java/mob_client/src/main/java/com/mobilecoin/mob_client/App.java new file mode 100644 index 0000000000..f12fb895c7 --- /dev/null +++ b/mobilecoind/clients/java/mob_client/src/main/java/com/mobilecoin/mob_client/App.java @@ -0,0 +1,328 @@ +/** + * A simple command line application for interaction with mobilecoind which also shows how to call + * mobilecoind from Java. + * + * The `server` and `command` flags are required to generate gRPC calls. Additional parameters are required + * depending on the command used. + * + * An example invocation which simply returns a new root entropy is: + * ./gradlew run --args='-s localhost:4444 -c entropy' + * + */ +package com.mobilecoin.mob_client; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Empty; +import com.mobilecoin.consensus.ConsensusAPI.KeyImage; +import com.mobilecoin.mobilecoind.MobileCoinDAPI; +import com.mobilecoin.mobilecoind.MobilecoindAPIGrpc; +import com.mobilecoin.mobilecoind.MobileCoinDAPI.AccountKey; +import com.mobilecoin.mobilecoind.MobilecoindAPIGrpc.MobilecoindAPIBlockingStub; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.cli.*; + +public class App { + public static void main(String[] args) { + Options options = new Options(); + Option serverOption = new Option("s", "server", true, "hostname:port to connect to mobilecoind"); + serverOption.setRequired(true); + options.addOption(serverOption); + + Option commandOption = new Option("c", "command", true, "Command to run"); + commandOption.setRequired(true); + options.addOption(commandOption); + + Option entropyOption = new Option("e", "entropy", true, "Root entropy key for the account"); + options.addOption(entropyOption); + + Option monitorOption = new Option("m", "monitor", true, "Monitor ID"); + options.addOption(monitorOption); + + Option indexOption = new Option("i", "index", true, "Subaddress Index"); + options.addOption(indexOption); + + Option recipientOption = new Option("r", "recipient", true, "b58 code for recipient of a transfer"); + options.addOption(recipientOption); + + Option amountOption = new Option("a", "amount", true, "amount in picoMOB to transfer"); + options.addOption(amountOption); + + Option transferReceiptOption = new Option("t", "transfer-receipt", true, "receipt code for transfer status"); + options.addOption(transferReceiptOption); + + // It is assumed that mobilecoind and clients such as this will run on the same machine, however + // there may be cases where a remote SSL connection is required + Option sslOption = new Option("ssl", "Use SSL to connect to mobilecoind"); + options.addOption(sslOption); + + CommandLineParser parser = new DefaultParser(); + HelpFormatter formatter = new HelpFormatter(); + CommandLine cmd = null; + try { + cmd = parser.parse(options, args); + } catch (Exception e) { + // Prints the error and the help message for the CLI + System.out.println(e.getMessage()); + formatter.printHelp("mob_client", options); + System.exit(1); + } + + // Build a blocking gRPC connection + String target = cmd.getOptionValue("server"); + ManagedChannel channel; + if (cmd.hasOption("ssl")) { + channel = ManagedChannelBuilder.forTarget(target).build(); + } else { + channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); + } + var stub = MobilecoindAPIGrpc.newBlockingStub(channel); + + // Account keys are generated from root entropy, passed in as hex parameters + // on the command line + AccountKey accountKey = null; + String entropy = cmd.getOptionValue("entropy"); + if (entropy != null) { + try { + accountKey = getKeyFromHexEntropy(stub, entropy); + } catch (DecoderException e) { + System.out.println("entropy was not a valid hex string"); + System.exit(1); + } + } + + // Monitor ID is passed in as a hex string on the command line, would generally + // be the value returned from creating a monitor + ByteString monitorId = null; + String monitor = cmd.getOptionValue("monitor"); + if (monitor != null) { + try { + monitorId = ByteString.copyFrom(Hex.decodeHex(monitor)); + } catch (DecoderException e) { + System.out.println("monitor was not a valid hex string"); + System.exit(1); + } + } + + // Subaddress index defaults to zero, can be passed using the --index flag + long index = 0; + String indexStr = cmd.getOptionValue("index"); + if (indexStr != null) { + index = Long.parseLong(indexStr); + } + + // Recipient is a b58 code representing a public address + String recipient = cmd.getOptionValue("recipient"); + + // Amount should be a positive integer which is required for transfers + long amount = 0; + String amountStr = cmd.getOptionValue("amount"); + if (amountStr != null) { + amount = Long.parseLong(amountStr); + if (amount <= 0) { + System.out.println("amount must be a positive integer"); + System.exit(1); + } + } + + // Transfer Receipt consists of a hex-ecoded key image an a tombstone block + ByteString transferKeyImage = null; + long tombstoneBlock = 0; + String transerReceiptStr = cmd.getOptionValue("transfer-receipt"); + if (transerReceiptStr != null) { + String[] parts = transerReceiptStr.split(":"); + if (parts.length != 2) { + System.out.println("The transfer receipt format is KEYIMAGE:TOMBSTONE"); + System.exit(1); + } + try { + transferKeyImage = ByteString.copyFrom(Hex.decodeHex(parts[0])); + tombstoneBlock = Long.parseLong(parts[1]); + } catch (Exception e) { + System.out.println("The transfer receipt format is KEYIMAGE:TOMBSTONE"); + System.exit(1); + } + } + + // All the functions return strings which are printed as the result to the CLI + // tool + String output = ""; + switch (cmd.getOptionValue("command")) { + case "generate-entropy": + output = getEntropy(stub); + break; + case "monitor": + if (accountKey == null) { + output = "key flag is required for a montior"; + } else { + output = createMonitor(stub, accountKey); + } + break; + case "balance": + if (monitorId == null) { + output = "balance check requires a monitor"; + } else { + output = getBalance(stub, monitorId, index); + } + break; + case "request": + if (monitorId == null) { + output = "request code requires a monitor"; + } else { + output = getRequestCode(stub, monitorId, index); + } + break; + case "transfer": + if (monitorId == null || recipient == null || amount == 0) { + output = "transfer requires a monitor, recipient and amount"; + } else { + output = transfer(stub, monitorId, index, recipient, amount); + } + break; + case "status": + if (transferKeyImage == null) { + output = "status requires a transfer receipit"; + } else { + output = transferStatus(stub, transferKeyImage, tombstoneBlock); + } + break; + default: + output = "Command not recognized"; + break; + } + System.out.println(output); + } + + /** + * Calls mobilecoind to derive the AccountKey object from a hex form of the root + * entropy + * + * @param stub The gRPC stub connected to mobilecoind + * @param entropy A hex representation of the 256 bits of root entropy + * @return An account key derived from the root entropy + * @throws DecoderException If the provided entropy is not valid hex + */ + public static AccountKey getKeyFromHexEntropy(MobilecoindAPIBlockingStub stub, String entropy) + throws DecoderException { + var b = ByteString.copyFrom(Hex.decodeHex(entropy)); + var request = MobileCoinDAPI.GetAccountKeyRequest.newBuilder().setEntropy(b).build(); + return stub.getAccountKey(request).getAccountKey(); + } + + /** + * Generates 256-bits of random entropy that can be used to create a new account + * key + * + * @param stub The gRPC stub connected to mobilecoind + * @return A hex representation of random 256 bits + */ + static String getEntropy(MobilecoindAPIBlockingStub stub) { + var entropy = stub.generateEntropy(Empty.getDefaultInstance()).getEntropy(); + return Hex.encodeHexString(entropy.toByteArray()); + } + + /** + * Creates a monitor over a single account key and a range of subaddresses TODO: + * this is fixed to monitor 100,000 subaddresses + * + * @param stub The gRPC stub connected to mobilecoind + * @param accountKey An AccountKey object, usually returned by + * getKeyFromHexEntropy + * @return A hex string representation of the monitor ID which can be used for + * balance and transfer + */ + static String createMonitor(MobilecoindAPIBlockingStub stub, AccountKey accountKey) { + var request = MobileCoinDAPI.AddMonitorRequest.newBuilder().setAccountKey(accountKey).setFirstSubaddress(0) + .setNumSubaddresses(100000).build(); + var monitor = stub.addMonitor(request); + return Hex.encodeHexString(monitor.getMonitorId().toByteArray()); + } + + /** + * Gets the balance for a given monitor and subaddress index + * + * @param stub The gRPC stub connected to mobilecoind + * @param monitorId 256-bit ID of the monitor observing the account + * @param index The subaddress index for which to check the balance + * @return A string representing the current balance for the account + */ + static String getBalance(MobilecoindAPIBlockingStub stub, ByteString monitorId, long index) { + var request = MobileCoinDAPI.GetBalanceRequest.newBuilder().setMonitorId(monitorId).setSubaddressIndex(index) + .build(); + var balance = stub.getBalance(request).getBalance(); + return Long.toString(balance); + } + + /** + * Given a monitor ID and subaddress index, generates the b58 request code (public address) + * for a payment + * + * @param stub The gRPC stub connected to mobilecoind + * @param monitorId 256-bit ID of the monitor observing the account + * @param index The subaddress index for which get the request code + * @return A b58 encoded string specifying a target public address + */ + static String getRequestCode(MobilecoindAPIBlockingStub stub, ByteString monitorId, long index) { + // First get the public address + var paRequest = MobileCoinDAPI.GetPublicAddressRequest.newBuilder().setMonitorId(monitorId).setSubaddressIndex(index).build(); + var publicAddress = stub.getPublicAddress(paRequest).getPublicAddress(); + + // Generates a payment address with no specific request value + var rcRequest = MobileCoinDAPI.GetRequestCodeRequest.newBuilder(). + setReceiver(publicAddress). + build(); + + return stub.getRequestCode(rcRequest).getB58Code(); + } + + /** + * Creates a transfer to a given b58 request code for a stated amount + * @param stub The gRPC stub connected to mobilecoind + * @param monitorId 256-bit ID of the monitor from which funds should be drawn + * @param index The subaddress index from which the funds should be drawn + * @param requestCode The requestCode to which funds should be sent + * @param amount The amount of picoMOB to be sent + * @return A string to use in a future call to see if the transfer succeeded, consisting of KEYIMAGE:TOMBSTONE + */ + static String transfer(MobilecoindAPIBlockingStub stub, ByteString monitorId, long index, String requestCode, long amount) { + // Convert the b58 code into a public address + var rcRequest = MobileCoinDAPI.ReadRequestCodeRequest.newBuilder().setB58Code(requestCode).build(); + var publicAddress = stub.readRequestCode(rcRequest).getReceiver(); + + // Generate a single outlay for the given amount + var outlay = MobileCoinDAPI.Outlay.newBuilder().setReceiver(publicAddress).setValue(amount); + + // Send a payment + var spRequest = MobileCoinDAPI.SendPaymentRequest.newBuilder().setSenderMonitorId(monitorId).addOutlayList(outlay).setSenderSubaddress(index).build(); + MobileCoinDAPI.SenderTxReceipt txReceipt = null; + try { + txReceipt = stub.sendPayment(spRequest).getSenderTxReceipt(); + } catch (Exception e) { + System.out.println("Your payment failed with error " + e.getMessage()); + System.exit(1); + } + + // Generate a transaction receipt using the first key image and the tombstone block + return Hex.encodeHexString(txReceipt.getKeyImageList(0).getData().toByteArray()) + ":" + txReceipt.getTombstone(); + } + /** + * Checks the status of a transfer given a key image and a tombstone block + * @param stub The gRPC stub connected to mobilecoind + * @param keyImage Bytes of any of the key images used in the transaction + * @param tombstoneBlock The tombstone block for the transaction + * @return A string representing the current status of the transaction, one of 'Unknown', 'Verified' or 'TombstoneBlockExceeded' + */ + static String transferStatus(MobilecoindAPIBlockingStub stub, ByteString keyImageBytes, long tombstoneBlock) { + var keyImage = KeyImage.newBuilder().setData(keyImageBytes).build(); + var receipt = MobileCoinDAPI.SenderTxReceipt.newBuilder().addKeyImageList(keyImage).setTombstone(tombstoneBlock).build(); + var request = MobileCoinDAPI.GetTxStatusAsSenderRequest.newBuilder().setReceipt(receipt).build(); + + var status = stub.getTxStatusAsSender(request).getStatus(); + + return status.toString(); + } +} diff --git a/mobilecoind/clients/java/mob_client/src/main/proto/external.proto b/mobilecoind/clients/java/mob_client/src/main/proto/external.proto new file mode 100644 index 0000000000..dce543d2b5 --- /dev/null +++ b/mobilecoind/clients/java/mob_client/src/main/proto/external.proto @@ -0,0 +1,159 @@ +// Copyright (c) 2018-2020 MobileCoin Inc. + +// MUST BE KEPT IN SYNC WITH RUST CODE! + +syntax = "proto3"; + +package external; + +option java_package = "com.mobilecoin.consensus"; +option java_outer_classname = "ConsensusAPI"; + +/////////////////////////////////////////////////////////////////////////////// +// `keys` crate +/////////////////////////////////////////////////////////////////////////////// + +message RistrettoPublic { + bytes data = 1; +} + +message RistrettoPrivate { + bytes data = 1; +} + +message CurvePoint { + bytes data = 1; +} + +message CurveScalar { + bytes data = 1; +} + +message KeyImage { + bytes data = 1; +} + +message Ed25519Public { + bytes data = 1; +} + +message Ed25519Signature { + bytes data = 1; +} + +/////////////////////////////////////////////////////////////////////////////// +// `common` crate +/////////////////////////////////////////////////////////////////////////////// + +message EncryptedFogHint { + bytes data = 1; +} + +/////////////////////////////////////////////////////////////////////////////// +// `ringct` crate +/////////////////////////////////////////////////////////////////////////////// + +message RingCtInput { + RistrettoPublic address = 1; + CurvePoint commitment = 2; +} + +message RingCtInputRow { + repeated RingCtInput row = 1; +} + +message RingCtChallengeResponse { + repeated CurveScalar response = 1; +} + +message RingCtSignature { + repeated KeyImage key_images = 1; + repeated RingCtChallengeResponse challenge_responses = 2; + CurveScalar challenge = 3; +} + + +/////////////////////////////////////////////////////////////////////////////// +// `ledger` crate +/////////////////////////////////////////////////////////////////////////////// + +message Range { + uint64 from = 1; + uint64 to = 2; +} + +message TxOutMembershipHash { + bytes data = 1; +} + +message TxOutMembershipElement { + Range range = 1; + TxOutMembershipHash hash = 2; +} + +message TxOutMembershipProof { + uint64 index = 1; + uint64 highest_index = 2; + repeated TxOutMembershipElement elements = 3; +} + +// Amount. +message Amount { + // A Pedersen commitment `v*G + s*H` + CurvePoint commitment = 1; + + // `masked_value = value + SHA3-512_scalar(shared_secret || n)` + CurveScalar masked_value = 2; + + // `masked_blinding = value + SHA3-512_scalar(SHA3-512_scalar(shared_secret || n)) + CurveScalar masked_blinding = 3; +} + +// A Transaction Output. +message TxOut { + // Amount. + Amount amount = 1; + + // Public key. + RistrettoPublic target_key = 2; + + // Public key. + RistrettoPublic public_key = 3; + + // 128 byte encrypted fog hint + EncryptedFogHint e_account_hint = 4; +} + +message TxIn { + // "Ring" of inputs, one of which is actually being spent. + repeated TxOut ring = 1; + + // Proof that each TxOut in `ring` is in the ledger. + repeated TxOutMembershipProof proofs = 2; +} + +// A transaction that a client submits to consensus +message TxPrefix { + // Transaction inputs. + repeated TxIn inputs = 1; + + // Transaction outputs. + repeated TxOut outputs = 2; + + // Fee paid to the foundation for this transaction + uint64 fee = 3; +} + +message Tx { + // The actual contents of the transaction + TxPrefix prefix = 1; + + // The RingCT signature on the prefix + RingCtSignature signature = 2; + + // The range proofs to show the values are in the proper range + bytes range_proofs = 3; + + // The block index past which this submitted transaction is no longer valid + uint64 tombstone_block = 4; +} diff --git a/mobilecoind/clients/java/mob_client/src/main/proto/mobilecoind_api.proto b/mobilecoind/clients/java/mob_client/src/main/proto/mobilecoind_api.proto new file mode 100644 index 0000000000..898cb9e47a --- /dev/null +++ b/mobilecoind/clients/java/mob_client/src/main/proto/mobilecoind_api.proto @@ -0,0 +1,490 @@ +// Copyright (c) 2018-2020 MobileCoin Inc. + +// MUST BE KEPT IN SYNC WITH RUST CODE! + +// mobilecoind client data types and service descriptors. + +syntax = "proto3"; +import "google/protobuf/empty.proto"; +import "external.proto"; + +package mobilecoind_api; + +option java_package = "com.mobilecoin.mobilecoind"; +option java_outer_classname = "MobileCoinDAPI"; + +service MobilecoindAPI { + // Monitors + rpc AddMonitor (AddMonitorRequest) returns (AddMonitorResponse) {} + rpc RemoveMonitor (RemoveMonitorRequest) returns (google.protobuf.Empty) {} + rpc GetMonitorList (google.protobuf.Empty) returns (GetMonitorListResponse) {} + rpc GetMonitorStatus (GetMonitorStatusRequest) returns (GetMonitorStatusResponse) {} + rpc GetUnspentTxOutList (GetUnspentTxOutListRequest) returns (GetUnspentTxOutListResponse) {} + + // Utilities + rpc GenerateEntropy (google.protobuf.Empty) returns (GenerateEntropyResponse) {} + rpc GetAccountKey (GetAccountKeyRequest) returns (GetAccountKeyResponse) {} + rpc GetPublicAddress (GetPublicAddressRequest) returns (GetPublicAddressResponse) {} + + // QR-code + rpc ReadRequestCode (ReadRequestCodeRequest) returns (ReadRequestCodeResponse) {} + rpc GetRequestCode (GetRequestCodeRequest) returns (GetRequestCodeResponse) {} + rpc ReadTransferCode (ReadTransferCodeRequest) returns (ReadTransferCodeResponse) {} + rpc GetTransferCode (GetTransferCodeRequest) returns (GetTransferCodeResponse) {} + + // Txs + rpc GenerateTx (GenerateTxRequest) returns (GenerateTxResponse) {} + rpc GenerateOptimizationTx (GenerateOptimizationTxRequest) returns (GenerateOptimizationTxResponse) {} + rpc GenerateTransferCodeTx (GenerateTransferCodeTxRequest) returns (GenerateTransferCodeTxResponse) {} + rpc SubmitTx (SubmitTxRequest) returns (SubmitTxResponse) {} + + // Databases + rpc GetLedgerInfo (google.protobuf.Empty) returns (GetLedgerInfoResponse) {} + rpc GetBlockInfo (GetBlockInfoRequest) returns (GetBlockInfoResponse) {} + rpc GetTxStatusAsSender (GetTxStatusAsSenderRequest) returns (GetTxStatusAsSenderResponse) {} + rpc GetTxStatusAsReceiver (GetTxStatusAsReceiverRequest) returns (GetTxStatusAsReceiverResponse) {} + + // Convenience calls + rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {} + rpc SendPayment (SendPaymentRequest) returns (SendPaymentResponse) {} + // SendPayment (monitor id, subaddress, public address, value) --> simple payment with change back to subaddress +} + +//********************************* +//* +//* Structures +//* +//********************************* + + +// Possible transaction status values. Senders check with key images. Receivers check with tx public keys. +enum TxStatus { + // The transaction is not in the public ledger. + Unknown = 0; + + // The transaction is in the public ledger. + Verified = 1; + + // Error: The transaction is not in the public ledger, and the tombstone block has been exceeded. + TombstoneBlockExceeded = 2; +} + +// Complete AccountKey, containing the pair of secret keys, which can be used +// for spending, and optionally some account-server related info +// can be used for spending. +// This matches the Rust `transaction::AccountKey` struct. +message AccountKey { + // Private key 'a' used for view-key matching. + external.RistrettoPrivate view_private_key = 1; + + // Private key `b` used for spending. + external.RistrettoPrivate spend_private_key = 2; + + // Optional FQDN of fog server. Empty string when not in use. + string fog_fqdn = 3; +} + +// A public address, used to identify receipients. +message PublicAddress { + external.RistrettoPublic view_public_key = 1; + external.RistrettoPublic spend_public_key = 2; + string fog_fqdn = 3; +} + +// Structure used in specifying the list of outputs when generating a transaction. +message Outlay { + uint64 value = 1; + PublicAddress receiver = 2; +} + +// Structure used to refer to a TxOut in the ledger that is presumed to be spendable. +// The structure is annotated with extra information needed to spend the TxOut in a payment, calculated using the private keys that control the TxOut. +message UnspentTxOut { + // The actual TxOut object found in the ledger. + external.TxOut tx_out = 1; + + // The subaddress the TxOut was sent to. + uint64 subaddress_index = 2; + + // The key image of the TxOut. + external.KeyImage key_image = 3; + + // The value of the TxOut. + uint64 value = 4; + + // The block height at which this UnspentTxOut was last attempted to be spent. + uint64 attempted_spend_height = 5; + + // The tombstone block used when we attempted to spend the UTXO. + uint64 attempted_spend_tombstone = 6; + + // The monitor id this UnspentTxOut belongs to. + // Note that this field is not included in the Rust `utxo_store::UnspentTxOut` struct. + bytes monitor_id = 10; +} + +// Structure used to refer to a prepared transaction +message TxProposal { + // List of inputs being spent. + repeated UnspentTxOut input_list = 1; + + // List of outputs being created. + // This excludes the fee output. + repeated Outlay outlay_list = 2; + + // The actual transaction object. + // Together with the private view/spend keys, this structure contains all information in existance about the transaction. + external.Tx tx = 3; + + // The transaction fee. This is equal to `tx.prefix.fee`. + // Note that changing this fee will have no effect on the transaction. Changing the fee + // inside `tx` will invalidate the ring signature. + uint64 fee = 4; + + /// A map of outlay index -> TxOut index in the Tx object. + /// This is needed to map recipients to their respective TxOuts. + map outlay_index_to_tx_out_index = 5; + +} + +// Structure used to check transaction status as a Sender. +message SenderTxReceipt { + // Key images that are going to be added to the ledger once the transaction goes through. + repeated external.KeyImage key_image_list = 1; + + // Tombstone block set in the transaction. + uint64 tombstone = 2; +} + +// Structure used to check transaction status as a receipient. +// There exists one receipt per output, so a transaction having multiple outputs would have +// multiple ReceiverTxReceipts. +message ReceiverTxReceipt { + // The receipient this receipt refers to + PublicAddress receipient = 1; + + // The public key of the TxOut sent to this receipient. + external.RistrettoPublic tx_public_key = 2; + + // The hash of the TxOut sent to this recipient. + bytes tx_out_hash = 3; + + // Tombstone block set in the transaction. + uint64 tombstone = 4; +} + +// Structure used to report monitor status +message MonitorStatus { + // The account key the monitor is monitoring. + AccountKey account_key = 1; + + // The first subaddress being monitored. + uint64 first_subaddress = 2; + + // The number of subaddresses being monitored, starting at first_subaddress. + uint64 num_subaddresses = 3; + + // Block index we started scanning from. + uint64 first_block = 4; + + // Next block we are waiting to sync. + uint64 next_block = 5; +} + + +//********************************* +//* +//* Requests and Responses for API +//* +//********************************* + +// +// Monitors +// + +// Add a new Monitor. +message AddMonitorRequest { + // Account key to monitor. + AccountKey account_key = 1; + + // The first subaddress being monitored. + uint64 first_subaddress = 2; + + // The number of subaddresses being monitored, starting at first_subaddress. + uint64 num_subaddresses = 3; + + // Block index to start monitoring from. + uint64 first_block = 4; +} + +message AddMonitorResponse { + bytes monitor_id = 1; +} + +// Remove a monitor and all associated data. +message RemoveMonitorRequest { + bytes monitor_id = 1; +} + +// List of all known monitor ids. +message GetMonitorListResponse { + repeated bytes monitor_id_list = 1; +} + +// Get the status of a specific monitor. +message GetMonitorStatusRequest { + bytes monitor_id = 1; +} +message GetMonitorStatusResponse { + MonitorStatus status = 1; +} + +// Return the list of UnspentTxOuts for a given monitor belonging to a specific subadddress index. +message GetUnspentTxOutListRequest { + bytes monitor_id = 1; + uint64 subaddress_index = 2; +} +message GetUnspentTxOutListResponse { + repeated UnspentTxOut output_list = 1; +} + +// +// Utilities +// + +// Generated root entropy data. +message GenerateEntropyResponse { + // 32 bytes generated using a cryptographically secure RNG. + bytes entropy = 1; +} + +// Generate an AccountKey from 32 bytes of random entropy. +message GetAccountKeyRequest { + bytes entropy = 1; +} +message GetAccountKeyResponse { + AccountKey account_key = 1; +} + +// Get the public address for a given monitor id + subaddress tuple. +message GetPublicAddressRequest { + bytes monitor_id = 1; + uint64 subaddress_index = 2; +} +message GetPublicAddressResponse { + PublicAddress public_address = 1; +} + +// +// QR-code +// + +// Decode a base-58 encoded "MobileCoin Request Code" into receiver/value/memo. +// This code provides a mobile client with everything required to construct a payment, allowing funds to be deposited to an exchange or paid to a merchant by scanning a QR code. +message ReadRequestCodeRequest { + string b58_code = 1; +} +message ReadRequestCodeResponse { + PublicAddress receiver = 1; + uint64 value = 2; + string memo = 3; +} + +// Encode receiver/value/memo into a base-58 "MobileCoin Request Code". +message GetRequestCodeRequest { + PublicAddress receiver = 1; + uint64 value = 2; + string memo = 3; +} +message GetRequestCodeResponse { + string b58_code = 1; +} + +// Decode a base-58 encoded "MobileCoin Transfer Code" into entropy/tx_public_key/memo. +// This code provides a mobile client with everything required to construct a self-payment, allowing funds to be withdrawn from an exchange or ATM by scanning a QR code. +message ReadTransferCodeRequest { + string b58_code = 1; +} +message ReadTransferCodeResponse { + bytes entropy = 1; + external.RistrettoPublic tx_public_key = 2; + string memo = 3; +} + +message GetTransferCodeRequest { + bytes entropy = 1; + external.RistrettoPublic tx_public_key = 2; + string memo = 3; +} +message GetTransferCodeResponse { + string b58_code = 1; +} + +// +// Transactions +// + +// Generate a transaction proposal object. +// Notes: +// - Sum of inputs needs to be greater than sum of outlays and fee. +// - The set of inputs to use would be chosen automatically by mobilecoind. +// - The fee field could be set to zero, in which case mobilecoind would choose a fee. + // Right now that fee is hardcoded. +message GenerateTxRequest { + // Monitor id sending the funds. + bytes sender_monitor_id = 1; + + // Subaddress to return change to. + uint64 change_subaddress = 2; + + // List of UnspentTxOuts to be spent by the transaction. + // All UnspentTxOuts must belong to the same sender_monitor_id. + // Mobilecoind would choose a subset of these inputs to construct the transaction. + // Total input amount must be >= sum of outlays + fees. + repeated UnspentTxOut input_list = 3; + + // Outputs to be generated by the transaction. This excludes change and fee. + repeated Outlay outlay_list = 4; + + // Fee (optional, setting to 0 would cause mobilecoind to choose a fee). + uint64 fee = 5; + + // Tombstone block to use for the transaction. Note that this can later be changed by manipulating + // tx_proposal.tx.tombstoneb_clock + uint64 tombstone = 6; + +} +message GenerateTxResponse { + TxProposal tx_proposal = 1; +} + +// Generate a transaction that merges a few UnspentTxOuts into one, in order to reduce wallet fragmentation. +message GenerateOptimizationTxRequest { + // Monitor Id to operate on. + bytes monitor_id = 1; + + // Subaddress to operate on. + uint64 subaddress = 2; +} +message GenerateOptimizationTxResponse { + TxProposal tx_proposal = 1; +} + +// Generate a transaction that can be used for a "MobileCoin Transfer Code" QR. +message GenerateTransferCodeTxRequest { + bytes sender_monitor_id = 1; + uint64 change_subaddress = 2; + repeated UnspentTxOut input_list = 3; + uint64 value = 4; + uint64 fee = 5; + uint64 tombstone = 6; + string memo = 7; +} +message GenerateTransferCodeTxResponse { + // The tx proposal to submit to the network. + TxProposal tx_proposal = 1; + + // The entropy for constructing the AccountKey that can access the funds. + bytes entropy = 2; + + // The TxOut public key that has the funds. + external.RistrettoPublic tx_public_key = 3; + + // The memo (simply copied from the request). + string memo = 4; + + // The b58-encoded Transfer Code + string b58_code = 5; +} + +// Submits a transaction to the network. +message SubmitTxRequest { + TxProposal tx_proposal = 1; +} +message SubmitTxResponse { + SenderTxReceipt sender_tx_receipt = 1; + repeated ReceiverTxReceipt receiver_tx_receipt_list = 2; +} + +// +// Databases +// + +// Empty Request +message GetLedgerInfoResponse { + // Total number of blocks in the ledger. + uint64 block_count = 1; + + // Total number of TxOuts in the ledger. + uint64 txo_count = 2; +} + +message GetBlockInfoRequest { + uint64 block = 1; +} +message GetBlockInfoResponse { + // Number of key images in the block. + uint64 key_image_count = 1; + + // Number of TxOuts in the block. + uint64 txo_count = 2; +} + + +message GetTxStatusAsSenderRequest { + SenderTxReceipt receipt = 1; +} +message GetTxStatusAsSenderResponse { + TxStatus status = 1; +} + +message GetTxStatusAsReceiverRequest { + ReceiverTxReceipt receipt = 1; +} +message GetTxStatusAsReceiverResponse { + TxStatus status = 1; +} + +// +// Convenience calls +/// + +message GetBalanceRequest { + // Monitor id to query balance for. + bytes monitor_id = 1; + + // Subaddress to query balance for. + uint64 subaddress_index = 2; +} +message GetBalanceResponse { + // Sum of all utxos associated with the requested monitor_id/subaddress_index. + uint64 balance = 1; +} + +message SendPaymentRequest { + // Monitor id sending the funds. + bytes sender_monitor_id = 1; + + // Subaddress the funds are coming from. + uint64 sender_subaddress = 2; + + // Outputs to be generated by the transaction. This excludes change and fee. + repeated Outlay outlay_list = 3; + + // Fee (optional, setting to 0 would cause mobilecoind to choose a fee). + uint64 fee = 4; + + // Tombstone block to use for the transaction. Note that this can later be changed by manipulating + // tx_proposal.tx.tombstoneb_clock + uint64 tombstone = 5; +} +message SendPaymentResponse { + // Information the sender can use to check if the transaction landed in the ledger. + SenderTxReceipt sender_tx_receipt = 1; + + // Information receivers can use to check if the transaction landed in the ledger. + repeated ReceiverTxReceipt receiver_tx_receipt_list = 2; + + // The Tx Proposal that was submitted to the network. + TxProposal tx_proposal = 3; +} diff --git a/mobilecoind/clients/java/mob_client/src/test/java/com/mobilecoin/mob_client/AppTest.java b/mobilecoind/clients/java/mob_client/src/test/java/com/mobilecoin/mob_client/AppTest.java new file mode 100644 index 0000000000..ccdc884151 --- /dev/null +++ b/mobilecoind/clients/java/mob_client/src/test/java/com/mobilecoin/mob_client/AppTest.java @@ -0,0 +1,21 @@ +/* + * Tests for basic functionality of the protocol buffers and conversions + */ +package com.mobilecoin.mob_client; + +import com.mobilecoin.mobilecoind.MobileCoinDAPI; +import com.google.protobuf.ByteString; +import com.mobilecoin.consensus.ConsensusAPI; +import org.junit.Test; +import static org.junit.Assert.*; + +public class AppTest { + @Test public void testCanCreateAccountKey() { + byte[] b = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + var spend_key = ConsensusAPI.RistrettoPrivate.newBuilder().setData(ByteString.copyFrom(b)); + var view_key = ConsensusAPI.RistrettoPrivate.newBuilder().setData(ByteString.copyFrom(b)); + var account_key = MobileCoinDAPI.AccountKey.newBuilder().setViewPrivateKey(view_key).setSpendPrivateKey(spend_key).build(); + assert(account_key.hasViewPrivateKey()); + assert(account_key.hasSpendPrivateKey()); + } +} diff --git a/mobilecoind/clients/python/README.md b/mobilecoind/clients/python/README.md index ed076d97f7..f371e5e440 100644 --- a/mobilecoind/clients/python/README.md +++ b/mobilecoind/clients/python/README.md @@ -1,91 +1,5 @@ -## MobileCoin CLI client for use with `mobilecoind` +## MobileCoin Python Client for Use with `mobilecoind` -This code demonstrates a text-based client implementation in Python, that uses `mobilecoind` to interact with the MobileCoin network. +To send and receive transactions, you can use `mobilecoind`'s wallet bindings. -### Python client setup - -To install required packages for the example client: - -(from `mobilecoinofficial/mobilecoin/public/examples/python`) -``` -pip3 install -r ./requirements.txt -``` - -To generate the pb2 files from the protocol buffers: -``` -./compile_proto.sh -``` - -### Running the client - -To run the MobileCoin client: -``` -py ./main.py : -``` - -This document describes how to start a local `mobilecoind` instance below, for use with the command: -``` -py ./main.py localhost:4444 -``` - -If you do not want to run a local instance, there is a hosted instance of `mobilecoind` available online connected to a preproduction network: -``` -py ./main.py --ssl mobilecoind.master.mobilecoin.com:443 -``` - -### Client interaction - -There is a file containing accounts that control funds in the preproduction MobileCoin network provided in `accounts.json`. - -An example session: -``` -$ ./main.py localhost:4444 -# load accounts.json -Loaded 13 accounts. -# monitor alice [0] -Added a monitor for "alice" @ subaddress [0] -# monitor carol [0,1,2] -Added a monitor for "carol" @ subaddress [0,1,2] -# balance alice/0 -alice/0 has 5000000000000000 pMOB @ block 12 -# transfer 10000 alice/0 carol/1 -Transfer initiated. -# status -Transaction not found. -# status -Transaction not found. -# status -Transaction verified. -# balance alice/0 -alice/0 has 4999999999990000 pMOB @ block 14 -# balance carol/1 -carol/1 has 10000 pMOB @ block 15 -# balance carol/0 -carol/1 has 5000000000000000 pMOB @ block 16 -``` - -### Running a local instance of `mobilecoind` - -For security, users should prefer connecting to a local `mobilecoind` instance. This can be run using `cargo` with some variation of: - -(from `mobilecoinofficial/mobilecoin`) -``` -cargo run --bin `mobilecoind` -- \ - --ledger-db /tmp/ledger-db \ - --ledger-db-bootstrap target/sample_data/dev/ledger \ - --poll-interval 10 \ - --client-port 4444 \ - --peer mc://node1.dev.mobilecoin.com/ \ - --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.dev.mobilecoin.com/ \ - --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.dev.mobilecoin.com/ \ - --db /tmp/transaction_db \ - --service-port 4444 -``` - -This command will launch a local `mobilecoind` instance that syncs the ledger from two nodes in the dev network and hosts the wallet service running on port 4444. - -Note that it may be necessary to delete the previous transaction database for a clean run: - -``` -rm -rf /tmp/ledger-db; rm -rf /tmp/transaction_db; mkdir /tmp/transaction_db -``` +See the jupyter notebook at [Wallet.ipynb](./Wallet.ipynb) for a walk through of an example Python wallet client. diff --git a/mobilecoind/clients/python/Wallet.ipynb b/mobilecoind/clients/python/Wallet.ipynb new file mode 100644 index 0000000000..3c1820db11 --- /dev/null +++ b/mobilecoind/clients/python/Wallet.ipynb @@ -0,0 +1,185 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MobileCoin Example Wallet\n", + "\n", + "This is an example python client that interacts with `mobilecoind` to manage a MobileCoin wallet.\n", + "\n", + "You must start the `mobilecoind` daemon in order to run a wallet. See the mobilecoind README for more information.\n", + "\n", + "To run this notebook, make sure you have the requirements installed, and that you have compiled the grpc protos.\n", + "\n", + "```\n", + "pip3 install -r requirements.txt\n", + "./compile_protos.sh\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import os.path\n", + "import traceback\n", + "import json\n", + "import re\n", + "import grpc\n", + "\n", + "import cmd\n", + "\n", + "from mob_client import mob_client, MonitorNotFound" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Start the Mob Client\n", + "\n", + "The client talks to your local mobilecoind. See the mobilecoind/README.md for information on how to set it up." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client = mob_client(\"localhost:4444\", ssl=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Input Root Entropy for Account\n", + "\n", + "Note: The root entropy is sensitive material. It is used as the seed to create your account keys. Anyone with your root entropy can steal your MobileCoin." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "entropy = \"4ec2c081e764f4189afba528956c05804a448f55f24cc3d04c9ef7e807a93bcd\"\n", + "credentials = client.get_account_key(bytes.fromhex(entropy))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Monitor your Account\n", + "\n", + "Monitoring an account means that mobilecoind will persist the transactions that belong to you to a local database. This allows you to retrieve your funds and calculate your balance, as well as to construct and submit transactions.\n", + "\n", + "Note: MobileCoin uses accounts and subaddresses for managing funds. You can optionally specify a range of subaddresses to monitor. See mob_client.py for more information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "monitor_id = client.add_monitor(credentials)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Check Balance\n", + "\n", + "You will need to provide a subaddress index. Most people will only use one subaddress, and can default to 0. Exchanges or users who want to generate lots of new public addresses may use multiple subaddresses." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "subaddress_index = 0\n", + "client.get_balance(monitor_id, subaddress_index)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Send a Transaction\n", + "\n", + "MobileCoin uses \"request codes\" to wrap public addresses. See below for how to generate request codes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "request_code = \"2BKiJJkWpt2GNg4fBrqZt9uZ8Gkmtssp5kvDhKU6MRaKU7hnFhHcKuJS2H9pA4AS2XHcjReSqvH3yzkngHffRKvP5xDGVoF5LxcQ3WrdAbf6PZ\"\n", + "\n", + "# We don't care about the value and memo field, because we already know \n", + "# how much MobileCoin we want to send.\n", + "target_address, _value, _memo = client.read_request_code(request_code)\n", + "\n", + "# Construct the transaction\n", + "tx_list = client.get_unspent_tx_output_list(monitor_id, subaddress_index)\n", + "outlays = [{'value': 10, 'receiver': target_address}]\n", + "tx_proposal = client.generate_tx(monitor_id, subaddress_index, tx_list, outlays)\n", + "\n", + "# Send the transaction to consensus validators\n", + "client.submit_tx(tx_proposal)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Public Address (Request Code)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "public_address = client.get_public_address(monitor_id, subaddress_index)\n", + "request_code = client.get_request_code(public_address)\n", + "print(f\"Request code = {request_code}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/mobilecoind/clients/python/accounts.json b/mobilecoind/clients/python/accounts.json deleted file mode 100644 index 97267b4e61..0000000000 --- a/mobilecoind/clients/python/accounts.json +++ /dev/null @@ -1,12 +0,0 @@ -{ -"a": {"entropy":"5626b806e7736e568f67731e8a26d8e581c32f0aaffdc643fbbdab72a1eb5708"}, -"b": {"entropy":"727022e7d0b9fc7075f63be0287eb6d127825956664dcb49fd583bee55820fc8"}, -"c": {"entropy":"1dbae15960625090ca4696959d963c780ec889eb98e74d5047d4205245ce5137"}, -"d": {"entropy":"4fd57855482a09688fbafd908973252b9b2f3c4b9d6e7c379b65afa75feb3342"}, -"e": {"entropy":"1c7e4be6c1609fc5dfa63e6a9957b8b47e0cbc80ee4086cfc38e25147527f63f"}, -"f": {"entropy":"91e7f15bf090d6c1e625987745033c0e2b755acb368519d2216887d839433ed4"}, -"g": {"entropy":"4dbeecb5356950d2a6a8d8c7e4c8920bf31537bfa09bc24a6e8125154b714161"}, -"h": {"entropy":"4f2cb5a782ae9414141764919a8830a8777c5ba1bb359f75fc37c754cca42540"}, -"i": {"entropy":"7c7f2b33fd8296bcff6ff9695936372dce1346770aaf6f814f8f6ccb0b2facd0"}, -"j": {"entropy":"4ec2c081e764f4189afba528956c05804a448f55f24cc3d04c9ef7e807a93bcd"} -} \ No newline at end of file diff --git a/mobilecoind/clients/python/main.py b/mobilecoind/clients/python/main.py index ed34f3f146..1a06ed4520 100755 --- a/mobilecoind/clients/python/main.py +++ b/mobilecoind/clients/python/main.py @@ -3,17 +3,6 @@ # Copyright (c) 2018-2020 MobileCoin Inc. import argparse -import qrcode -import sys -import os.path -import traceback -import json -import time -import re -import grpc - -# provides command line progress indicator -from halo import Halo try: import readline @@ -21,618 +10,15 @@ # readline not required on Windows pass -from mob_client import mob_client - -# this regular expression matches valid base 58 strings -b58re = re.compile( - r'[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]*') - -# Keep track of known account aliases along with their root entropy -# example: {"a":{"entropy":"5626b806e7736e568f67731e8a26d8e581c32f0aaffdc643fbbdab72a1eb5708"},} -known_accounts = {} - - -""" Error handling. -""" - - -class ClientError(Exception): - pass - - -""" Parse a known account -""" - - -def parse_account(account_string): - account_key = account_string.lower().strip() - if not account_key in known_accounts: - raise ClientError( - 'failed to parse a known accout from "{}"'.format(account_string)) - return known_accounts[account_key] - - -""" Parse a positive integer index -""" - - -def parse_uint(index_string): - try: - index = int(index_string.lower().strip()) - assert (index >= 0) - return index - except Exception: - raise ClientError( - 'failed to parse a positive integer from "{}"'.format(index_string)) - - -""" Parse a subaddress of the form "account/#" -""" - - -def parse_subaddress(subaddress_string): - parts = subaddress_string.lower().strip().split('/') - if len(parts) != 2: - raise ClientError( - 'failed to parse subaddress from "{}"'.format(subaddress_string)) - account = parse_account(parts[0]) - index = parse_uint(parts[1]) - return account, index - - -""" Parse an index range of the form "[#,#]" -""" - - -def parse_index_range(index_range_string): - if index_range_string[0] != '[' or index_range_string[-1] != ']': - raise ClientError( - 'failed to parse index range from "{}"'.format(index_range_string)) - else: - indices = [] - for index_string in index_range_string[1:-1].split(','): - index = parse_uint(index_string) - indices.append(index) - return {"first_index": indices[0], "last_index": indices[1]} - - -""" Returns the credentials for an account -""" - - -def get_account_credentials(account): - if "entropy" in account: - return client.get_account_key(bytes.fromhex(account["entropy"])) - else: - raise ClientError('account "{}" is malformed'.format(account["name"])) - - -""" Add a QR code to the buffer -""" - - -def get_qrcode(b58code): - qr = qrcode.QRCode(border=0) - qr.add_data(b58code) - qr.make() - #qr.print_ascii(out=buffer) - result = '\n\nqrcode content: {}\n\n'.format(b58code) - return result - - -""" Loads global variable known_accounts from a json file. - Usage: load (opt) -""" - - -def load_accounts(args): - try: - if args: - json_file = args[0] - else: # allow lazy load for default - json_file = "accounts.json" - if not os.path.isfile(json_file): - json_file += ".json" # allow user to omit extension - with open(json_file) as f: - known_accounts.update(json.load(f)) - # add names for later use - for key, account in known_accounts.items(): - known_accounts[key]["name"] = key - return "Loaded {} accounts.".format(len(known_accounts)) - except ClientError as e: - return 'client error:\n{}:{}'.format(type(e).__name__, e.args) - except grpc.RpcError as e: - return 'mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - except Exception as e: - return 'Error loading file "{}":\n{}:{}\n{}'.format( - args[0] if args else "(empty)", type(e).__name__, e.args, traceback.format_exc()) - - -""" List known accounts. - Usage: accounts -""" - - -def list_accounts(args): - try: - if len(known_accounts) == 0: - return "There are no known accounts." - str = "" - for (i, account) in enumerate(known_accounts): - monitor_id_list = [] - if "monitors" in account: - monitor_id_list = account["monitors"] - for id in monitor_id_list: - status = client.get_monitor_status(id) - str += '#{} : account "{}" '.format(i, account["name"]) - str += 'is monitoring subaddress range [{},{}]\n'.format( - status.first_index, status.last_index) - else: - str += '#{} : account "{}" is known, but has no monitors.\n'.format( - i, known_accounts[account]["name"]) - str = str[0:-1] # remove the final '\n' - return str - except ClientError as e: - return 'client error:\n{}:{}'.format(type(e).__name__, e.args) - except grpc.RpcError as e: - return 'mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - except Exception as e: - return 'Error listing accounts:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - - -""" Monitor incoming transactions for a set of subaddresses for a known alias. - Usage: monitor (opt)<[min,max]> -""" - - -def add_monitor(args): - try: - account = parse_account(args[0]) - credentials = get_account_credentials(account) - id = b'' # empty bytes - if len(args) == 2: - index_range = parse_index_range(args[1]) - id = client.add_monitor( - credentials, first_subaddress=index_range['first_index'], num_subaddresses=index_range['last_index']) - else: - id = client.add_monitor(credentials) - account["monitors"] = [id] - status = client.get_monitor_status(id) - str = 'account "{}" '.format(account["name"]) - str += 'added a monitor for subaddress range [{},{}]'.format( - status.first_subaddress, status.num_subaddresses) - return str - except ClientError as e: - return 'client error:\n{}:{}'.format(type(e).__name__, e.args) - except grpc.RpcError as e: - return 'mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - except Exception as e: - return 'Error adding monitor:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - - -""" List active monitors. - Usage: monitors -""" - - -def list_monitors(args): - try: - monitor_id_list = client.get_monitor_list() - result = ('Account Monitor List:\n') - for (i, id) in enumerate(monitor_id_list): - status = client.get_monitor_status(id) - str = '#{} [{}]: account {} is monitoring subaddress range [{},{}]' - account = account_from_monitor_id - result += (str.format( - i, id.hex(), account["name"], status.first_index, status.last_index)) - return result - except ClientError as e: - return 'client error:\n{}:{}'.format(type(e).__name__, e.args) - except grpc.RpcError as e: - return 'mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - except Exception as e: - return 'Error listing monitors:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - - -""" Returns the balance for a subaddress. - Usage: balance -""" - - -def check_balance(args): - try: - account, index = parse_subaddress(args[0]) - credentials = get_account_credentials(account) - monitor_id = client.get_monitor_id(credentials, index) - balance = client.get_balance(monitor_id, index) - monitor_last_block = client.get_monitor_status(monitor_id).next_block - block_count = client.get_ledger_info()[0] - if block_count > 0: - subaddress_str = '{}/{}'.format(account["name"], index) - return '{} has {} pMOB @ block {} of {} available'.format( - args[0], balance, monitor_last_block, block_count) - else: - raise ClientError('no ledger blocks have been downloaded') - except ClientError as e: - return 'client error:\n{}:{}'.format(type(e).__name__, e.args) - except grpc.RpcError as e: - return 'mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - except Exception as e: - return 'Error checking balance:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - - -""" Send funds between accounts - Usage: transfer -""" - - -def transfer(args): - try: - # Get sender info and monitor - value = parse_uint(args[0]) - from_account, from_index = parse_subaddress(args[1]) - from_credentials = get_account_credentials(from_account) - from_monitor = client.get_monitor_id(from_credentials, from_index) - - # Get recipient info and monitor - to_account, to_index = parse_subaddress(args[2]) - to_credentials = get_account_credentials(to_account) - to_monitor = client.get_monitor_id(to_credentials, to_index) - target_address = client.get_public_address(to_monitor, to_index) - - # Construct the transaction - tx_list = client.get_unspent_tx_output_list(from_monitor, from_index) - outlays = [{'value': value, 'receiver': target_address}] - tx_proposal = client.generate_tx(from_monitor, from_index, tx_list, outlays) - - # Submit the transaction - sender_tx_receipt = client.submit_tx(tx_proposal) - return 'Transaction submitted with key_images: {}'.format( - sender_tx_receipt.key_image_list) - except ClientError as e: - return 'client error:\n{}:{}'.format(type(e).__name__, e.args) - except grpc.RpcError as e: - return 'mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - except Exception as e: - return 'Error transferring funds:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - - -""" Return the status of the most recent transaction for an account - Usage: status -""" -TRANSACTION_STATUS = { - 0: "Unknown", - 1: "Pending", - 2: "Verified", - 3: "TombstoneBlockExceeded", -} - - -def status(args): - try: - account = parse_account(args[0]) - if "sender_tx_receipt" in account: - status = client.get_tx_status_as_sender( - account["sender_tx_receipt"]) - elif "recipient_tx_receipt" in account: - status = client.get_tx_status_as_receiver( - account["recipient_tx_receipt"]) - else: - return 'No transaction has been sent.' - return "Transaction status is: " + TRANSACTION_STATUS[status] - except ClientError as e: - return 'client error:\n{}:{}'.format(type(e).__name__, e.args) - except grpc.RpcError as e: - return 'mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - except Exception as e: - return 'Error checking status:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - - -""" Create and display a withdrawal QR code - Usage: create-withdrawal -""" - - -def create_withdrawal(args): - try: - value = parse_uint(args[0]) - from_account, from_index = parse_subaddress(args[1]) - from_credentials = get_account_credentials(from_account) - sender = client.get_sender(from_credentials, from_index) - monitor_id = client.get_monitor_id(from_credentials, from_index) - all_outputs = client.get_tx_output_list(monitor_id, from_index) - spendable_outputs = client.get_spendable_outputs(sender, all_outputs) - to_credentials = client.generate_entropy() - receiver = client.get_receiver(to_credentials) - sender_tx_receipt, receiver_tx_receipt = client.send_payment( - sender, spendable_outputs, receiver, value)[0, 1] - from_account["sender_tx_receipt"] = sender_tx_receipt - del from_account["receiver_tx_receipt"] # clear any old tx - tx_public_key = receiver_tx_receipt.tx_public_key - result = ('Transaction submitted with key_image: {}'.format( - sender_tx_receipt.key_image)) - status = status(args[1]) - while(status == 0): - time.sleep(1) - status = status(args[1]) - assert(status == 1) - b58_code = client.get_transfer_code(to_credentials, tx_public_key) - result += _qrcode(b58code) - result += ("Scan this code to complete your withdrawal.") - except ClientError as e: - result += ('client error:\n{}:{}'.format(type(e).__name__, e.args)) - except grpc.RpcError as e: - result += ('mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc())) - except Exception as e: - result += ('Error creating withdrawal:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc())) - - -""" Withdraw funds to a subaddress - Usage: withdraw -""" - - -def withdraw(args): - try: - if not b58re.match(args[0]): - raise ClientError('invalid base58 code: "{}"'.format(args[0])) - to_account, to_index = parse_subaddress(args[1]) - from_credentials, tx_public_key, memo = client.read_transfer_code( - b58_code) - txo = client.get_tx_output(tx_public_key) - value = txo.value - sender = client.get_sender(from_credentials) - to_credentials = get_account_credentials(to_account) - receiver = client.get_receiver(to_credentials, to_index) - receiver_tx_receipt = client.send_payment( - sender, [txo], receiver, value)[1] - to_account["receiver_tx_receipt"] = receiver_tx_receipt - del to_account["sender_tx_receipt"] # clear any old tx - result = ('Transaction submitted with tx_public_key: {}'.format( - receiver_tx_receipt.tx_public_key)) - except ClientError as e: - result += ('client error:\n{}:{}'.format(type(e).__name__, e.args)) - except grpc.RpcError as e: - result += ('mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc())) - except Exception as e: - result += ('Error withdrawing funds:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc())) - - -""" create and display a deposit QR code - Usage: create-deposit -""" - - -def create_deposit(args): - try: - value = parse_uint(args[0]) - to_account, to_index = parse_subaddress(args[1]) - to_credentials = get_account_credentials(to_account) - receiver = client.get_receiver(to_credentials, to_index) - b58_code = client.get_request_code(receiver, value) - result = _qrcode(b58code) - result += ("Scan this code to complete your deposit.") - except ClientError as e: - result += ('client error:\n{}:{}'.format(type(e).__name__, e.args)) - except grpc.RpcError as e: - result += ('mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc())) - except Exception as e: - result += ('Error creating deposit:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc())) - - -""" deposit funds from a subaddress - Usage: deposit -""" - - -def deposit(args): - try: - if not b58re.match(args[0]): - raise ClientError('invalid base58 code: "{}"'.format(args[0])) - receiver, value, memo = client.read_request_code(b58_code) - from_account, from_index = parse_subaddress(args[1]) - from_credentials = get_account_credentials(from_account) - sender = client.get_sender(from_credentials, from_index) - monitor_id = client.get_monitor_id(from_credentials, from_index) - all_outputs = client.get_tx_output_list(monitor_id, from_index) - spendable_outputs = client.get_spendable_outputs(sender, all_outputs) - sender_tx_receipt = client.send_payment( - sender, spendable_outputs, receiver, value)[0] - from_account["sender_tx_receipt"] = sender_tx_receipt - del from_account["receiver_tx_receipt"] # clear any old tx - return 'Transaction submitted with key_image: {}'.format( - sender_tx_receipt.key_image) - except ClientError as e: - return 'client error:\n{}:{}'.format(type(e).__name__, e.args) - except grpc.RpcError as e: - return 'mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - except Exception as e: - return 'Error depositing funds:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - - - -""" Return the status of the most recent transaction - Usage: test -""" - - -def mobilecoind_test(args): - try: - delay_msec = parse_uint(args[0]) - start = client.get_ledger_info() - time.sleep(0.001 * delay_msec) - end = client.get_ledger_info() - new_blocks = end[0] - start[0] - new_txos = end[1] - start[1] - if new_blocks > 0: - result = ('downloaded {0:5d} new blocks ( {1:5d} per second )\n'.format( - new_blocks, new_blocks / (0.001 * delay_msec))) - result += ('downloaded {0:5d} new txos ( {1:5d} per second )\n'.format( - new_txos, new_txos / (0.001 * delay_msec))) - return result - else: - return 'No new blocks processed in {} milliseconds.'.format(delay_msec) - except ClientError as e: - return 'client error:\n{}:{}'.format(type(e).__name__, e.args) - except grpc.RpcError as e: - return 'mobilecoind error:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - except Exception as e: - return 'Error testing mobilecoind:\n{}:{}\n{}'.format( - type(e).__name__, e.args, traceback.format_exc()) - - -""" Display command list -""" - - -def help(args): - command_width = 20 - syntax_width = 52 - use_width = 50 - help_width = 3 + command_width + syntax_width + use_width - row_format = '* {:<%ds}{:<%ds}{:<%ds}*' % ( - command_width, syntax_width, use_width) - result = '*' * help_width + '\n' - result += row_format.format("Command", "Syntax", "Use") + '\n' - result += row_format.format("-" * command_width, - "-" * syntax_width, "-" * use_width) - unique_cmds = {} - for cmd in dispatch.keys(): - key = dispatch[cmd]["help"] - if key in unique_cmds.keys(): - if len(cmd) > len(unique_cmds[key]): - unique_cmds[key] = cmd - else: - unique_cmds[key] = cmd - cmds_to_display = {} - for key in unique_cmds.keys(): - cmd = unique_cmds[key] - cmds_to_display[cmd] = {dispatch[cmd]["usage"], dispatch[cmd]["help"]} - for cmd in sorted(cmds_to_display): - result += '\n' - result += (row_format.format( - cmd, dispatch[cmd]["usage"], dispatch[cmd]["help"])) - result += '\n' + '*' * help_width - return result - - -""" Command table -""" -dispatch = { - "help": {"fn": help, "args": [0], - "usage": "", - "help": "display a list of commands"}, - "load": {"fn": load_accounts, "args": [0, 1], - "usage": "load (opt)", - "help": "load a table of known accounts from a file"}, - "monitor": {"fn": add_monitor, "args": [1, 2], - "usage": "monitor (opt)<[min,max]>", - "help": "monitor txos for a set of subaddresses"}, - "balance": {"fn": check_balance, "args": [1], - "usage": "balance ", - "help": "return the balance for a subaddress"}, - "transfer": {"fn": transfer, "args": [3], - "usage": "transfer ", - "help": "transfer funds between subaddresses"}, - "status": {"fn": status, "args": [1], - "usage": "status ", - "help": "print the status of the last transfer"}, - "create-withdrawal": {"fn": create_withdrawal, "args": [2], - "usage": "create-withdrawal ", - "help": "create and display a withdrawal QR code"}, - "withdraw": {"fn": withdraw, "args": [2], - "usage": "withdraw ", - "help": "withdraw funds to a subaddress"}, - "create-deposit": {"fn": create_deposit, "args": [2], - "usage": "create-deposit ", - "help": "create and display a deposit QR code"}, - "deposit": {"fn": deposit, "args": [2], - "usage": "deposit ", - "help": "deposit funds from a subaddress"}, - "test": {"fn": mobilecoind_test, "args": [1], - "usage": "test ", - "help": "measure and display mobilecoind performance"}, - "monitors": {"fn": list_monitors, "args": [0], - "usage": "", - "help": "list active monitors"}, - "accounts": {"fn": list_accounts, "args": [0], - "usage": "", - "help": "list known accounts"}, -} - - -def add_cmd_aliases(cmd, aliases): - for cmd_alias in aliases: - dispatch[cmd_alias] = dispatch[cmd] - - -add_cmd_aliases("accounts", ["ls", "list"]) -add_cmd_aliases("help", ["?"]) -add_cmd_aliases("transfer", ["move", "mv", "send", "pay"]) -add_cmd_aliases("status", ["check"]) -add_cmd_aliases("balance", ["b"]) -add_cmd_aliases("monitor", ["m", "add"]) - -""" Interaction loop with mob_client -""" - - -def run(client): - spinner = Halo(text=' processing... ', spinner='dots') - while True: - print('# ', end='') - cmd = input().lower().strip().split(' ') - if not cmd[0]: - continue - elif cmd[0] in ('exit', 'quit', 'q'): - break - elif cmd[0] in dispatch.keys(): - fn = dispatch[cmd[0]]["fn"] - args = cmd[1:] - expected_args_len_list = dispatch[cmd[0]]["args"] - if not len(args) in expected_args_len_list: - print('Usage: ' + dispatch[cmd[0]]["usage"]) - elif not fn: - pass - else: - spinner.start() - result = fn(args) - spinner.stop() - print(result) - else: - print('Unknown command. Enter "help" to display a list of commands.') - +from wallet import Session -""" Parse the arguments and generate the mob_client -""" if __name__ == '__main__': + # Parse the arguments and generate the mob_client parser = argparse.ArgumentParser( description='Connect to a mobilecoind daemon') parser.add_argument('daemon', help='Address and port of daemon', type=str) parser.add_argument('--ssl', help='Use SSL', action='store_true') args = parser.parse_args() - client = mob_client(args.daemon, args.ssl) - run(client) + session = Session(args.daemon, args.ssl) + session.cmdloop() diff --git a/mobilecoind/clients/python/mob_client.py b/mobilecoind/clients/python/mob_client.py index a11c932ef1..d723dc36d5 100644 --- a/mobilecoind/clients/python/mob_client.py +++ b/mobilecoind/clients/python/mob_client.py @@ -9,13 +9,20 @@ from random import randint -class mob_client: +class MonitorNotFound(Exception): + """ When a Monitor is not Found""" + pass - """ Initializes the client and connects to a mobilecoind service. - daemon -- address and port of mobilecoind daemon - ssl -- use SSL to connect""" +class mob_client: + """ Manages the MobileCoin Wallet Client. + """ def __init__(self, daemon, ssl): + """ Initializes the client and connects to a mobilecoind service. + daemon -- address and port of mobilecoind daemon + ssl -- use SSL to connect + """ + if ssl: credentials = grpc.ssl_channel_credentials() self.channel = grpc.secure_channel(daemon, credentials) @@ -23,74 +30,65 @@ def __init__(self, daemon, ssl): self.channel = grpc.insecure_channel(daemon) self.stub = api_grpc.MobilecoindAPIStub(self.channel) - """ Close the gRPC connection upon deletion.""" - def __del__(self): + """ Close the gRPC connection upon deletion.""" try: self.channel.close() except Exception: pass - # - # Monitors - # - - """ Create a process that watches the ledger for tx outputs belonging to a - set of subaddresses, each specified by account_key and an index.""" - - def add_monitor(self, account_key, first_subaddress=0, num_subaddresses=100000, first_block=0): - request = api.AddMonitorRequest( - account_key=account_key, - first_subaddress=first_subaddress, - num_subaddresses=num_subaddresses, - first_block=first_block) + def add_monitor(self, + account_key, + first_subaddress=0, + num_subaddresses=100000, + first_block=0): + """ Create a process that watches the ledger for tx outputs belonging to a + set of subaddresses, each specified by account_key and an index. + """ + request = api.AddMonitorRequest(account_key=account_key, + first_subaddress=first_subaddress, + num_subaddresses=num_subaddresses, + first_block=first_block) return self.stub.AddMonitor(request).monitor_id - """ Remove an existing monitor and delete any data it has stored. - """ - def remove_monitor(self, monitor_id): + """ Remove an existing monitor and delete any data it has stored. + """ request = api.RemoveMonitorRequest(monitor_id) return self.stub.RemoveMonitor(request) - """ Returns a list of all active monitors. - """ - def get_monitor_list(self): + """ Returns a list of all active monitors. + """ return self.stub.GetMonitorList(empty_pb2.Empty()).monitor_id_list - """ Returns a status report for a monitor process. - """ - def get_monitor_status(self, monitor_id): + """ Returns a status report for a monitor process. + """ request = api.GetMonitorStatusRequest(monitor_id=monitor_id) response = self.stub.GetMonitorStatus(request) return response.status - """ Returns the list of tx outputs collected for a subaddress. - """ - def get_unspent_tx_output_list(self, monitor_id, index=0): - request = api.GetUnspentTxOutListRequest( - monitor_id=monitor_id, - subaddress_index=index) + """ Returns the list of tx outputs collected for a subaddress. + """ + request = api.GetUnspentTxOutListRequest(monitor_id=monitor_id, + subaddress_index=index) response = self.stub.GetUnspentTxOutList(request) return response.output_list - """ Returns the sum of unspent tx outputs collected for a subaddress. - """ - def get_balance(self, monitor_id, index=0): + """ Returns the sum of unspent tx outputs collected for a subaddress. + """ uoutput_list = self.get_unspent_tx_output_list(monitor_id, index) balance = 0 for utxo in uoutput_list: balance += utxo.value return balance - """ Returns the monitor for a given subaddress, if one exists. - """ - def get_monitor_id(self, account_key, index=0): + """ Returns the monitor for a given subaddress, if one exists. + """ monitor_id_list = self.get_monitor_list() target_vpk = account_key.view_private_key target_spk = account_key.spend_private_key @@ -107,60 +105,55 @@ def get_monitor_id(self, account_key, index=0): if best_monitor_id: return best_monitor_id else: - raise Exception('monitor not found') + raise MonitorNotFound # # Utilities # - """ Generate 32 bytes of entropy using a cryptographically secure RNG. - """ - def generate_entropy(self): + """ Generate 32 bytes of entropy using a cryptographically secure RNG. + """ return self.stub.GenerateEntropy(empty_pb2.Empty()).entropy - """ Get the private keys from entropy. - """ - def get_account_key(self, entropy): + """ Get the private keys from entropy. + """ request = api.GetAccountKeyRequest(entropy=entropy) return self.stub.GetAccountKey(request).account_key - """ Returns the public address for a given monitor and index - """ def get_public_address(self, monitor_id, subaddress_index): + """ Returns the public address for a given monitor and index + """ request = api.GetPublicAddressRequest( - monitor_id=monitor_id, - subaddress_index=subaddress_index) + monitor_id=monitor_id, subaddress_index=subaddress_index) return self.stub.GetPublicAddress(request).public_address - """ Process a b58 request code to recover content. - """ - def read_request_code(self, b58_code): - request = api.ReadRequestCodeRequest(b58_code) + """ Process a b58 request code to recover content. + """ + request = api.ReadRequestCodeRequest(b58_code=b58_code) response = self.stub.ReadRequestCode(request) return response.receiver, response.value, response.memo - """ Prepare a "request code" used to generate a QR code for wallet apps. - """ - def get_request_code(self, receiver, value=0, memo=""): - request = api.GetRequestCodeRequest(receiver, value, memo) + """ Prepare a "request code" used to generate a QR code for wallet apps. + """ + request = api.GetRequestCodeRequest(receiver=receiver, + value=value, + memo=memo) return self.stub.GetRequestCode(request).b58_code - """ Process a b58 transfer code to recover content. - """ - def read_transfer_code(self, b58_code): + """ Process a b58 transfer code to recover content. + """ request = api.ReadTransferCodeRequest(b58_code) response = self.stub.ReadTransferCode(request) return response.entropy, response.tx_public_key, response.memo - """ Prepare a "transfer code" used to generate a QR code for wallet apps. - """ - def get_transfer_code(self, entropy, tx_public_key, memo=""): + """ Prepare a "transfer code" used to generate a QR code for wallet apps. + """ request = api.GetTransferCodeRequest(entropy, tx_public_key, memo) return self.stub.GetTransferCode(request).b58_code @@ -168,75 +161,77 @@ def get_transfer_code(self, entropy, tx_public_key, memo=""): # Transactions # - """ Prepares a transaction. If the fee is zero, we use the default minimum fee. Mix-ins and other + def generate_tx(self, + sender_monitor_id, + change_subaddress, + input_list, + outlay_dict, + fee=0, + tombstone=0): + """ Prepares a transaction. If the fee is zero, we use the default minimum fee. Mix-ins and other complexities of the MobileCoin protocol are handled automatically. - """ - - def generate_tx(self, sender_monitor_id, change_subaddress, input_list, outlay_dict, fee=0, tombstone=0): - outlay_list = [api.Outlay(value=r['value'], receiver=r['receiver']) for r in outlay_dict] - request = api.GenerateTxRequest( - sender_monitor_id=sender_monitor_id, - change_subaddress=change_subaddress, - input_list=input_list, - outlay_list=outlay_list, - fee=fee) + """ + outlay_list = [ + api.Outlay(value=r['value'], receiver=r['receiver']) + for r in outlay_dict + ] + request = api.GenerateTxRequest(sender_monitor_id=sender_monitor_id, + change_subaddress=change_subaddress, + input_list=input_list, + outlay_list=outlay_list, + fee=fee) return self.stub.GenerateTx(request).tx_proposal - """ Due to limits on the number of inputs allowed for a transaction, a wallet can contain + def generate_optimization_tx(self, sender, output_list): + """ Due to limits on the number of inputs allowed for a transaction, a wallet can contain more value than is spendable in a single transaction. This generates a self-payment that combines small value tx outputs together. - """ - - def generate_optimization_tx(self, sender, output_list): + """ request = api.GenerateOptimizationTxRequest(sender, output_list) return self.stub.GenerateOptimizationTx(request).tx_proposal - """ Prepares a transaction that can be submitted to fund a transfer code for a new - one time account. - """ - def generate_transfer_code_tx(self, sender, output_list, value): + """ Prepares a transaction that can be submitted to fund a transfer code for a new + one time account. + """ request = api.GenerateTransferCodeTxRequest(sender, output_list, value) response = self.stub.GenerateTransferCodeTx(request) return response.tx_proposal, response.entropy - """ Submit a prepared transaction, optionall requesting a tombstone block. - """ - def submit_tx(self, tx_proposal): + """ Submit a prepared transaction, optionall requesting a tombstone block. + """ request = api.SubmitTxRequest(tx_proposal=tx_proposal) response = self.stub.SubmitTx(request) - return response.sender_tx_receipt + return response # # Databases # - """ Returns a status report for mobilecoind's ledger maintenance. - """ - def get_ledger_info(self): + """ Returns a status report for mobilecoind's ledger maintenance. + """ info = self.stub.GetLedgerInfo(empty_pb2.Empty()) return info.block_count, info.txo_count - """ Returns a status report for a ledger block. - """ - def get_block_info(self, block): + """ Returns a status report for a ledger block. + """ request = api.GetBlockInfoRequest(block=block) info = self.stub.GetBlockInfo(request) return info.key_image_count, info.txo_count - """ Check if a key image appears in the ledger. - """ - def get_tx_status_as_sender(self, sender_tx_receipt): - request = api.GetTxStatusAsSenderRequest(sender_tx_receipt) - return self.stub.GetTxStatusAsSender(request).status - - """ Check if a transaction public key appears in the ledger. - """ + """ Check if a key image appears in the ledger. + """ + request = api.GetTxStatusAsSenderRequest(receipt=sender_tx_receipt) + response = self.stub.GetTxStatusAsSender(request) + return response.status def get_tx_status_as_receiver(self, receiver_tx_receipt): - request = api.GetTxStatusAsReceiverRequest(receiver_tx_receipt) - return self.stub.GetTxStatusAsReceiver(request).status + """ Check if a transaction public key appears in the ledger. + """ + request = api.GetTxStatusAsReceiverRequest(receipt=receiver_tx_receipt) + response = self.stub.GetTxStatusAsReceiver(request) + return response.status diff --git a/mobilecoind/clients/python/start-testnet-mobilecoind.sh b/mobilecoind/clients/python/start-testnet-mobilecoind.sh new file mode 100755 index 0000000000..374ccc8c77 --- /dev/null +++ b/mobilecoind/clients/python/start-testnet-mobilecoind.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Copyright (c) 2018-2020 MobileCoin Inc. +# +# Launches a local `mobilecoind` instance that syncs the ledger from two nodes in the +# test network and hosts wallet service running on port 4444. + +set -e + +source "$HOME/.cargo/env" + +echo "Pulling down TestNet consensus validator signature material" +curl -O https://enclave-distribution.test.mobilecoin.com/pool/e57b6902aee60be45b78b496c1bef781746e4389/bf7fa957a6a94acb588851bc8767eca5776c79f4fc2aa6bcb99312c3c386c/consensus-enclave.css + +if [[ ! -f ../../../target/release/mobilecoind ]]; then + echo "Building mobilecoind. This will take a few moments." + SGX_MODE=HW IAS_MODE=PROD CONSENSUS_ENCLAVE_CSS=$(pwd)/consensus-enclave.css cargo build --release -p mobilecoind +fi + +# Note that it may be necessary to delete the previous transaction database for a clean run: +# rm -rf /tmp/ledger-db; rm -rf /tmp/transaction_db; mkdir /tmp/transaction_db + +echo "Starting local mobilecoind using TestNet servers for source of ledger. Check log at /tmp/mobilecoind.log." +SGX_MODE=HW IAS_MODE=PROD CONSENSUS_ENCLAVE_CSS=$(pwd)/consensus-enclave.css ../../../target/release/mobilecoind \ + --ledger-db /tmp/ledger-db \ + --poll-interval 10 \ + --peer mc://node1.test.mobilecoin.com/ \ + --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.test.mobilecoin.com/ \ + --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/ \ + --mobilecoind-db /tmp/transaction_db \ + --service-port 4444 diff --git a/mobilecoind/src/config.rs b/mobilecoind/src/config.rs index 70ddfcbd23..527008f1ff 100644 --- a/mobilecoind/src/config.rs +++ b/mobilecoind/src/config.rs @@ -33,7 +33,7 @@ pub struct Config { /// URLs to use for transaction data. /// - /// For example: https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.master.mobilecoin.com/ + /// For example: https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.test.mobilecoin.com/ #[structopt(long = "tx-source-url", required = true, min_values = 1)] pub tx_source_urls: Vec, diff --git a/mobilecoind/src/payments.rs b/mobilecoind/src/payments.rs index 41ddc41c48..dc038904dd 100644 --- a/mobilecoind/src/payments.rs +++ b/mobilecoind/src/payments.rs @@ -24,7 +24,7 @@ use std::{ }; use transaction::{ account_keys::{AccountKey, PublicAddress}, - constants::{BASE_FEE, MAX_INPUTS, MIN_RING_SIZE}, + constants::{BASE_FEE, MAX_INPUTS, RING_SIZE}, onetime_keys::{compute_key_image, recover_onetime_private_key}, tx::{Tx, TxOut, TxOutMembershipProof}, BlockIndex, @@ -36,7 +36,7 @@ use transaction_std::{InputCredentials, TransactionBuilder}; pub const DEFAULT_NEW_TX_BLOCK_ATTEMPTS: u64 = 50; /// Default ring size -pub const DEFAULT_RING_SIZE: usize = MIN_RING_SIZE; +pub const DEFAULT_RING_SIZE: usize = RING_SIZE; /// An outlay - the API representation of a desired transaction output. #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/mobilecoind/src/service.rs b/mobilecoind/src/service.rs index b821c55648..4d58a95caf 100644 --- a/mobilecoind/src/service.rs +++ b/mobilecoind/src/service.rs @@ -21,12 +21,18 @@ use common::{ }; use grpc_util::{rpc_internal_error, rpc_logger, send_result}; use grpcio::{RpcContext, RpcStatus, RpcStatusCode, UnarySink}; +use keys::RistrettoPublic; use ledger_db::{Ledger, LedgerDB}; +use mc_b58_payloads::payloads::{RequestPayload, TransferPayload}; use mcconnection::UserTxConnection; +use mcserial::ReprBytes32; use mobilecoind_api::mobilecoind_api_grpc::{create_mobilecoind_api, MobilecoindApi}; use protobuf::RepeatedField; use std::{convert::TryFrom, sync::Arc}; -use transaction::{account_keys::AccountKey, ring_signature::KeyImage}; +use transaction::{ + account_keys::{AccountKey, PublicAddress}, + ring_signature::KeyImage, +}; use transaction_std::identity::RootIdentity; pub struct Service { @@ -325,30 +331,88 @@ impl ServiceApi { fn read_request_code_impl( &mut self, - _request: mobilecoind_api::ReadRequestCodeRequest, + request: mobilecoind_api::ReadRequestCodeRequest, ) -> Result { - todo!(); + let request_payload = RequestPayload::decode(request.get_b58_code()) + .map_err(|err| rpc_internal_error("RequestPayload.decode", err, &self.logger))?; + + let mut response = mobilecoind_api::ReadRequestCodeResponse::new(); + response.set_receiver(mobilecoind_api::PublicAddress::from(&PublicAddress::from( + &request_payload, + ))); + response.set_value(request_payload.value); + response.set_memo(request_payload.memo); + Ok(response) } fn get_request_code_impl( &mut self, - _request: mobilecoind_api::GetRequestCodeRequest, + request: mobilecoind_api::GetRequestCodeRequest, ) -> Result { - todo!(); + let receiver = PublicAddress::try_from(request.get_receiver()) + .map_err(|err| rpc_internal_error("PublicAddress.try_from", err, &self.logger))?; + + let view_key = receiver.view_public_key().to_bytes(); + let spend_key = receiver.spend_public_key().to_bytes(); + let fog_url = receiver.fog_url().unwrap_or(""); + + let payload = RequestPayload::new_v3( + &view_key, + &spend_key, + fog_url, + request.get_value(), + request.get_memo(), + ) + .map_err(|err| rpc_internal_error("RequestPayload.new_v3", err, &self.logger))?; + let b58_code = payload.encode(); + + let mut response = mobilecoind_api::GetRequestCodeResponse::new(); + response.set_b58_code(b58_code); + Ok(response) } fn read_transfer_code_impl( &mut self, - _request: mobilecoind_api::ReadTransferCodeRequest, + request: mobilecoind_api::ReadTransferCodeRequest, ) -> Result { - todo!(); + let transfer_payload = TransferPayload::decode(request.get_b58_code()) + .map_err(|err| rpc_internal_error("TransferPayload.decode", err, &self.logger))?; + + let tx_public_key = RistrettoPublic::try_from(&transfer_payload.utxo) + .map_err(|err| rpc_internal_error("RistrettoPublic.try_from", err, &self.logger))?; + + let mut response = mobilecoind_api::ReadTransferCodeResponse::new(); + response.set_entropy(transfer_payload.entropy.to_vec()); + response.set_tx_public_key((&tx_public_key).into()); + response.set_memo(transfer_payload.memo); + Ok(response) } fn get_transfer_code_impl( &mut self, - _request: mobilecoind_api::GetTransferCodeRequest, + request: mobilecoind_api::GetTransferCodeRequest, ) -> Result { - todo!(); + let mut entropy: [u8; 32] = [0; 32]; + if request.entropy.len() != entropy.len() { + return Err(RpcStatus::new( + RpcStatusCode::INVALID_ARGUMENT, + Some("entropy".to_string()), + )); + } + entropy.copy_from_slice(request.get_entropy()); + + let tx_public_key = RistrettoPublic::try_from(request.get_tx_public_key()) + .map_err(|err| rpc_internal_error("RistrettoPublic.try_from", err, &self.logger))?; + + let payload = + TransferPayload::new_v1(&entropy, &tx_public_key.to_bytes(), request.get_memo()) + .map_err(|err| rpc_internal_error("TransferPayload.new_v1", err, &self.logger))?; + + let b58_code = payload.encode(); + + let mut response = mobilecoind_api::GetTransferCodeResponse::new(); + response.set_b58_code(b58_code); + Ok(response) } fn generate_tx_impl( @@ -472,9 +536,108 @@ impl ServiceApi { fn generate_transfer_code_tx_impl( &mut self, - _request: mobilecoind_api::GenerateTransferCodeTxRequest, + request: mobilecoind_api::GenerateTransferCodeTxRequest, ) -> Result { - todo!(); + // Generate entropy. + let entropy_response = self.generate_entropy_impl(mobilecoind_api::Empty::new())?; + let entropy = entropy_response.get_entropy().to_vec(); + + let mut entropy_bytes = [0; 32]; + if entropy.len() != entropy_bytes.len() { + return Err(RpcStatus::new( + RpcStatusCode::INTERNAL, + Some("entropy returned was not 32 bytes".to_owned()), + )); + } + entropy_bytes.copy_from_slice(&entropy); + + // Generate a new account using this entropy. + let mut account_key_request = mobilecoind_api::GetAccountKeyRequest::new(); + account_key_request.set_entropy(entropy.clone()); + + let account_key_response = self.get_account_key_impl(account_key_request)?; + let account_key = AccountKey::try_from(account_key_response.get_account_key()) + .map_err(|err| rpc_internal_error("account_key.try_from", err, &self.logger))?; + + // The outlay we are sending the money to. + let outlay = Outlay { + receiver: account_key.default_subaddress(), + value: request.value, + }; + + // Generate transaction. + let mut generate_tx_request = mobilecoind_api::GenerateTxRequest::new(); + generate_tx_request.set_sender_monitor_id(request.get_sender_monitor_id().to_vec()); + generate_tx_request.set_change_subaddress(request.change_subaddress); + generate_tx_request.set_input_list(RepeatedField::from_vec(request.input_list.to_vec())); + generate_tx_request.set_outlay_list(RepeatedField::from_vec(vec![(&outlay).into()])); + generate_tx_request.set_fee(request.fee); + generate_tx_request.set_tombstone(request.tombstone); + + let mut generate_tx_response = self.generate_tx_impl(generate_tx_request)?; + let tx_proposal = generate_tx_response.take_tx_proposal(); + + // Grab the public key of the relevant tx out. + let proto_tx_public_key = { + // We expect only a single outlay. + if tx_proposal.get_outlay_index_to_tx_out_index().len() != 1 { + return Err(RpcStatus::new( + RpcStatusCode::INTERNAL, + Some(format!( + "outlay_index_to_tx_out_index contains {} elements, was expecting 1", + tx_proposal.get_outlay_index_to_tx_out_index().len() + )), + )); + } + + // Get the TxOut index of our single outlay. + let tx_out_index = tx_proposal + .get_outlay_index_to_tx_out_index() + .get(&0) + .ok_or_else(|| { + RpcStatus::new( + RpcStatusCode::INTERNAL, + Some("outlay_index_to_tx_out_index doesn't contain index 0".to_owned()), + ) + })?; + + // Get the TxOut + let tx_out = tx_proposal + .get_tx() + .get_prefix() + .get_outputs() + .get(*tx_out_index as usize) + .ok_or_else(|| { + RpcStatus::new( + RpcStatusCode::INTERNAL, + Some(format!("tx out index {} not found", tx_out_index)), + ) + })?; + + // Get the public key + tx_out.get_public_key().clone() + }; + + let tx_public_key = RistrettoPublic::try_from(&proto_tx_public_key) + .map_err(|err| rpc_internal_error("ristretto_public.try_from", err, &self.logger))?; + + // Generate b58 code. + let transfer_payload = TransferPayload::new_v1( + &entropy_bytes, + &tx_public_key.to_bytes(), + request.get_memo(), + ) + .map_err(|err| rpc_internal_error("transfer_payload.new_v1", err, &self.logger))?; + let b58_code = transfer_payload.encode(); + + // Construct response. + let mut response = mobilecoind_api::GenerateTransferCodeTxResponse::new(); + response.set_tx_proposal(tx_proposal); + response.set_entropy(entropy); + response.set_tx_public_key(proto_tx_public_key); + response.set_memo(request.get_memo().to_owned()); + response.set_b58_code(b58_code); + Ok(response) } fn submit_tx_impl( @@ -501,7 +664,7 @@ impl ServiceApi { if let Err(err) = self.mobilecoind_db.update_attempted_spend( &utxo_ids, block_height, - tx_proposal.tx.tombstone_block, + tx_proposal.tx.prefix.tombstone_block, ) { log::error!( self.logger, @@ -520,7 +683,7 @@ impl ServiceApi { .map(|utxo| (&utxo.key_image).into()) .collect(), )); - sender_tx_receipt.set_tombstone(tx_proposal.tx.tombstone_block); + sender_tx_receipt.set_tombstone(tx_proposal.tx.prefix.tombstone_block); // Construct receiver receipts. let receiver_tx_receipts: Vec<_> = tx_proposal @@ -554,7 +717,7 @@ impl ServiceApi { receiver_tx_receipt.set_receipient((&outlay.receiver).into()); receiver_tx_receipt.set_tx_public_key(tx_out.public_key.into()); receiver_tx_receipt.set_tx_out_hash(tx_out.hash().to_vec()); - receiver_tx_receipt.set_tombstone(tx_proposal.tx.tombstone_block); + receiver_tx_receipt.set_tombstone(tx_proposal.tx.prefix.tombstone_block); Ok(receiver_tx_receipt) }) @@ -898,15 +1061,16 @@ mod test { utxo_store::UnspentTxOut, }; use common::{logger::test_with_logger, HashSet}; - use keys::RistrettoPublic; + use keys::FromRandom; use rand::{rngs::StdRng, SeedableRng}; use std::{convert::TryFrom, iter::FromIterator}; use transaction::{ account_keys::{AccountKey, PublicAddress, DEFAULT_SUBADDRESS_INDEX}, - constants::{BASE_FEE, MAX_INPUTS, MIN_RING_SIZE}, + constants::{BASE_FEE, MAX_INPUTS, RING_SIZE}, get_tx_out_shared_secret, onetime_keys::{compute_key_image, recover_onetime_private_key}, tx::{Tx, TxOut}, + Block, BlockIndex, BLOCK_VERSION, }; #[test_with_logger] @@ -1617,7 +1781,7 @@ mod test { // Sanity test tombstone block let num_blocks = ledger_db.num_blocks().unwrap(); assert_eq!( - tx_proposal.get_tx().tombstone_block, + tx_proposal.get_tx().get_prefix().tombstone_block, num_blocks + DEFAULT_NEW_TX_BLOCK_ATTEMPTS ); } @@ -1676,6 +1840,116 @@ mod test { } } + #[test_with_logger] + fn test_generate_transfer_code_tx(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([23u8; 32]); + + let sender = AccountKey::random(&mut rng); + let data = MonitorData::new( + sender.clone(), + 0, // first_subaddress + 20, // num_subaddresses + 0, // first_block + ) + .unwrap(); + + // 1 known recipient, 3 random recipients and no monitors. + let (mut ledger_db, mobilecoind_db, client, _server, _server_conn_manager) = + get_testing_environment( + 3, + &vec![sender.default_subaddress()], + &vec![], + logger.clone(), + &mut rng, + ); + + // Insert into database. + let monitor_id = mobilecoind_db.add_monitor(&data).unwrap(); + + // Allow the new monitor to process the ledger. + wait_for_monitors(&mobilecoind_db, &ledger_db, &logger); + + // Get list of unspent tx outs + let utxos = mobilecoind_db + .get_utxos_for_subaddress(&monitor_id, 0) + .unwrap(); + assert!(!utxos.is_empty()); + + // Call generate transfer code ctx. + let mut request = mobilecoind_api::GenerateTransferCodeTxRequest::new(); + request.set_sender_monitor_id(monitor_id.to_vec()); + request.set_change_subaddress(0); + request.set_input_list(RepeatedField::from_vec( + utxos + .iter() + .map(mobilecoind_api::UnspentTxOut::from) + .collect(), + )); + request.set_value(1337); + + let response = client.generate_transfer_code_tx(&request).unwrap(); + + // Test that the generated transaction can be picked up by mobilecoind. + { + // Get the transaction, and redact it so that we could append it to the ledger. + let tx_proposal = TxProposal::try_from(response.get_tx_proposal()).unwrap(); + let redacted_transactions = vec![tx_proposal.tx.redact()]; + + // Append to ledger. + let num_blocks = ledger_db.num_blocks().unwrap(); + let parent = ledger_db.get_block(num_blocks - 1).unwrap(); + let new_block = Block::new( + BLOCK_VERSION, + &parent.id, + num_blocks as BlockIndex, + &Default::default(), + &redacted_transactions, + ); + ledger_db + .append_block(&new_block, &redacted_transactions, None) + .unwrap(); + + // Add a monitor based on the entropy we received. + let mut root_entropy = [0; 32]; + root_entropy.copy_from_slice(response.get_entropy()); + let root_id = RootIdentity { + root_entropy, + fog_url: None, + }; + + // TODO: change to production AccountKey derivation + let account_key = AccountKey::from(&root_id); + let monitor_data = MonitorData::new( + account_key, + DEFAULT_SUBADDRESS_INDEX, // first_subaddress + 1, // num_subaddresses + 0, // first_block + ) + .unwrap(); + + let monitor_id = mobilecoind_db.add_monitor(&monitor_data).unwrap(); + + // Wait for sync to complete. + wait_for_monitors(&mobilecoind_db, &ledger_db, &logger); + + // Get utxos for the new account and verify we only have the one utxo we are looking forc. + let utxos = mobilecoind_db + .get_utxos_for_subaddress(&monitor_id, 0) + .unwrap(); + assert_eq!(utxos.len(), 1); + + let utxo = &utxos[0]; + + assert_eq!(utxo.value, 1337); + assert_eq!( + utxo.tx_out.public_key, + RistrettoPublic::try_from(response.get_tx_public_key()) + .unwrap() + .into() + ); + } + } + #[test_with_logger] fn test_generate_optimization_tx(logger: Logger) { let mut rng: StdRng = SeedableRng::from_seed([23u8; 32]); @@ -1692,7 +1966,7 @@ mod test { // 1 known recipient, and a bunch of random recipients and no monitors. // The random recipients are needed for mixins. - let num_random_recipients = MAX_INPUTS as u32 * MIN_RING_SIZE as u32 + let num_random_recipients = MAX_INPUTS as u32 * RING_SIZE as u32 / test_utils::GET_TESTING_ENVIRONMENT_NUM_BLOCKS as u32; let (mut ledger_db, mobilecoind_db, client, _server, _server_conn_manager) = get_testing_environment( @@ -1769,7 +2043,7 @@ mod test { // Sanity test tombstone block let num_blocks = ledger_db.num_blocks().unwrap(); assert_eq!( - tx_proposal.tx.tombstone_block, + tx_proposal.tx.prefix.tombstone_block, num_blocks + DEFAULT_NEW_TX_BLOCK_ATTEMPTS ); } @@ -1893,7 +2167,7 @@ mod test { assert_eq!( response.get_sender_tx_receipt().tombstone, - tx.tombstone_block + tx.prefix.tombstone_block ); // Sanity the receiver receipts. @@ -1907,7 +2181,7 @@ mod test { PublicAddress::try_from(receipt.get_receipient()).unwrap() ); - assert_eq!(receipt.tombstone, tx.tombstone_block); + assert_eq!(receipt.tombstone, tx.prefix.tombstone_block); } assert_eq!( @@ -2119,7 +2393,7 @@ mod test { assert_eq!( response.get_sender_tx_receipt().tombstone, - submitted_tx.tombstone_block + submitted_tx.prefix.tombstone_block ); // Sanity the receiver receipts. @@ -2133,7 +2407,7 @@ mod test { PublicAddress::try_from(receipt.get_receipient()).unwrap() ); - assert_eq!(receipt.tombstone, submitted_tx.tombstone_block); + assert_eq!(receipt.tombstone, submitted_tx.prefix.tombstone_block); } assert_eq!( @@ -2173,4 +2447,148 @@ mod test { } assert_eq!(matched_utxos, tx_proposal.utxos.len()); } + + #[test_with_logger] + fn test_request_code(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([23u8; 32]); + + // no known recipient, 3 random recipients and no monitors. + let (_ledger_db, _mobilecoind_db, client, _server, _server_conn_manager) = + get_testing_environment(3, &vec![], &vec![], logger.clone(), &mut rng); + + // Random receiver address. + let receiver = AccountKey::random(&mut rng).default_subaddress(); + + // Try with just a receiver + { + // Generate a request code + let mut request = mobilecoind_api::GetRequestCodeRequest::new(); + request.set_receiver(mobilecoind_api::PublicAddress::from(&receiver)); + + let response = client.get_request_code(&request).unwrap(); + let b58_code = response.get_b58_code(); + + // Attempt to decode it. + let mut request = mobilecoind_api::ReadRequestCodeRequest::new(); + request.set_b58_code(b58_code.to_owned()); + + let response = client.read_request_code(&request).unwrap(); + + // Check that input equals output. + assert_eq!( + PublicAddress::try_from(response.get_receiver()).unwrap(), + receiver + ); + assert_eq!(response.value, 0); + assert_eq!(response.get_memo(), ""); + } + // Try with receiver and value + { + // Generate a request code + let mut request = mobilecoind_api::GetRequestCodeRequest::new(); + request.set_receiver(mobilecoind_api::PublicAddress::from(&receiver)); + request.set_value(1234567890); + + let response = client.get_request_code(&request).unwrap(); + let b58_code = response.get_b58_code(); + + // Attempt to decode it. + let mut request = mobilecoind_api::ReadRequestCodeRequest::new(); + request.set_b58_code(b58_code.to_owned()); + + let response = client.read_request_code(&request).unwrap(); + + // Check that input equals output. + assert_eq!( + PublicAddress::try_from(response.get_receiver()).unwrap(), + receiver + ); + assert_eq!(response.value, 1234567890); + assert_eq!(response.get_memo(), ""); + } + // Try with receiver, value and memo + { + // Generate a request code + let mut request = mobilecoind_api::GetRequestCodeRequest::new(); + request.set_receiver(mobilecoind_api::PublicAddress::from(&receiver)); + request.set_value(1234567890); + request.set_memo("hello there".to_owned()); + + let response = client.get_request_code(&request).unwrap(); + let b58_code = response.get_b58_code(); + + // Attempt to decode it. + let mut request = mobilecoind_api::ReadRequestCodeRequest::new(); + request.set_b58_code(b58_code.to_owned()); + + let response = client.read_request_code(&request).unwrap(); + + // Check that input equals output. + assert_eq!( + PublicAddress::try_from(response.get_receiver()).unwrap(), + receiver + ); + assert_eq!(response.value, 1234567890); + assert_eq!(response.get_memo(), "hello there"); + } + + // Attempting to decode junk data should fail + { + let mut request = mobilecoind_api::ReadRequestCodeRequest::new(); + request.set_b58_code("junk".to_owned()); + + assert!(client.read_request_code(&request).is_err()); + } + } + + #[test_with_logger] + fn test_transfer_code(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([23u8; 32]); + + // no known recipient, 3 random recipients and no monitors. + let (_ledger_db, _mobilecoind_db, client, _server, _server_conn_manager) = + get_testing_environment(3, &vec![], &vec![], logger.clone(), &mut rng); + + // Text public key + let tx_public_key = RistrettoPublic::from_random(&mut rng); + + // An invalid request should fail. + { + let mut request = mobilecoind_api::GetTransferCodeRequest::new(); + request.set_entropy(vec![3; 8]); + request.set_tx_public_key((&tx_public_key).into()); + request.set_memo("memo".to_owned()); + assert!(client.get_transfer_code(&request).is_err()); + + let mut request = mobilecoind_api::GetTransferCodeRequest::new(); + request.set_memo("memo".to_owned()); + assert!(client.get_transfer_code(&request).is_err()); + } + + // A valid request should allow us to encode to b58 and back to the original data. + { + // Encode + let mut request = mobilecoind_api::GetTransferCodeRequest::new(); + request.set_entropy(vec![3; 32]); + request.set_tx_public_key((&tx_public_key).into()); + request.set_memo("test memo".to_owned()); + + let response = client.get_transfer_code(&request).unwrap(); + let b58_code = response.get_b58_code(); + + // Decode + let mut request = mobilecoind_api::ReadTransferCodeRequest::new(); + request.set_b58_code(b58_code.to_owned()); + + let response = client.read_transfer_code(&request).unwrap(); + + // Compare + assert_eq!(vec![3; 32], response.get_entropy()); + assert_eq!( + tx_public_key, + RistrettoPublic::try_from(response.get_tx_public_key()).unwrap() + ); + assert_eq!(response.get_memo(), "test memo"); + } + } } diff --git a/testnet-client/Cargo.toml b/testnet-client/Cargo.toml new file mode 100644 index 0000000000..868f6d310e --- /dev/null +++ b/testnet-client/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mc-testnet-client" +version = "0.1.0" +authors = ["MobileCoin"] +edition = "2018" + +[dependencies] +dialoguer = "0.5" +grpcio = "0.5.1" +hex = "0.4" +indicatif = "0.14" +protobuf = "2.12" +rust_decimal = { version = "1.4", default-features = false } +structopt = "0.3" + +mc-b58-payloads = { path = "../util/b58-payloads" } +mobilecoind-api = { path = "../mobilecoind/api" } diff --git a/testnet-client/src/main.rs b/testnet-client/src/main.rs new file mode 100644 index 0000000000..31c3cbfafb --- /dev/null +++ b/testnet-client/src/main.rs @@ -0,0 +1,674 @@ +// Copyright (c) 2018-2020 MobileCoin Inc. + +//! A demo client for interacting with the MobileCoin test network using mobilecoind. + +use dialoguer::{theme::ColorfulTheme, Input, Select, Validator}; +use grpcio::{ChannelBuilder, ChannelCredentialsBuilder}; +use indicatif::{ProgressBar, ProgressStyle}; +use mc_b58_payloads::payloads::RequestPayload; +use mobilecoind_api::mobilecoind_api_grpc::MobilecoindApiClient; +use protobuf::RepeatedField; +use rust_decimal::{prelude::ToPrimitive, Decimal}; +use std::{fmt, str::FromStr, sync::Arc, thread, time::Duration}; +use structopt::StructOpt; + +/// Command lien config. +#[derive(StructOpt)] +struct Config { + /// The host:port of the mobilecoind instance to connect to. + #[structopt(short = "s", long = "server", default_value = "127.0.0.1:4444")] + pub mobilecoind_host: String, + + /// Use SSL when connecting to mobilecoind. + #[structopt(long)] + pub use_ssl: bool, +} + +/// The main entry point. +fn main() { + let config = Config::from_args(); + + match TestnetClient::new(&config) { + Ok(mut client) => client.run(), + Err(_) => std::process::exit(1), + } +} + +/// Commands in the main menu. +enum Command { + Send, + Receive, + CheckBalance, + Quit, +} +impl fmt::Display for Command { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Command::Send => write!(f, "Pay a bill"), + Command::Receive => write!(f, "Create a bill"), + Command::CheckBalance => write!(f, "Check balance"), + Command::Quit => write!(f, "Quit"), + } + } +} + +/// The actual test-net client implementation. +struct TestnetClient { + client: MobilecoindApiClient, + monitor_id: Vec, +} + +impl TestnetClient { + pub fn new(config: &Config) -> Result { + // Construct GRPC connection to mobilecoind. + let env = Arc::new(grpcio::EnvBuilder::new().build()); + let ch_builder = ChannelBuilder::new(env) + .keepalive_permit_without_calls(true) + .keepalive_time(Duration::from_secs(1)) + .keepalive_timeout(Duration::from_secs(20)) + .max_reconnect_backoff(Duration::from_millis(2000)) + .initial_reconnect_backoff(Duration::from_millis(1000)) + .max_receive_message_len(std::i32::MAX) + .max_send_message_len(std::i32::MAX); + + let ch = if config.use_ssl { + let creds = ChannelCredentialsBuilder::new().build(); + ch_builder.secure_connect(&config.mobilecoind_host, creds) + } else { + ch_builder.connect(&config.mobilecoind_host) + }; + + let client = MobilecoindApiClient::new(ch); + + // Do a simple check to see if mobilecoind is alive. + if let Err(err) = client.get_ledger_info(&mobilecoind_api::Empty::new()) { + println!("Unable to connect to mobilecoind on {} - are you sure it is running and accepting connections?", config.mobilecoind_host); + println!(); + println!("The error was: {}", err); + return Err(format!("unable to connect to mobilecoind - {}", err)); + } + + // Return. + Ok(TestnetClient { + client, + monitor_id: Vec::new(), + }) + } + + /// The main UI loop. + pub fn run(&mut self) { + Self::print_intro(); + + loop { + let root_entropy = Self::get_root_entropy(); + match self.add_monitor_and_wait_for_sync(&root_entropy) { + Ok(_) => { + break; + } + Err(err) => { + println!("{}", err); + } + } + } + + loop { + self.print_balance(); + + let commands = [ + Command::Send, + Command::Receive, + Command::CheckBalance, + Command::Quit, + ]; + let selection = Select::with_theme(&ColorfulTheme::default()) + .default(0) + .items(&commands) + .interact() + .unwrap(); + match commands[selection] { + Command::Send => self.send(), + Command::Receive => self.receive(), + Command::CheckBalance => { + // Balance updates every loop iteration + } + Command::Quit => { + println!("Thanks for using the MobileCoin TestNet!"); + thread::sleep(Duration::from_secs(1)); + break; + } + } + } + } + + /// Print a short introductory message. + fn print_intro() { + let intro = r#" +********************************************************************** + + Welcome to the MobileCoin TestNet + +********************************************************************** + +You are now connected to: testnet-west.mobilecoin.com:444 + +Please enter the 32 byte root entropy for an account. If you received an email with an allocation of TestNet mobilecoins, this is the hexadecimal string we sent you. It should look something like + + dc74edf1d8892dfdf49d6db5d3d4e873665c2dd400c0955dd9729571826a26be +"#; + println!("{}", intro); + } + + /// Get root entropy from user. + fn get_root_entropy() -> [u8; 32] { + #[derive(Clone)] + struct EntropyBytes(pub [u8; 32]); + impl FromStr for EntropyBytes { + type Err = String; + fn from_str(src: &str) -> Result { + let bytes = hex::decode(src).map_err(|err| format!("Invalid input: {}", err))?; + if bytes.len() != 32 { + return Err(format!( + "Invalid input length, got {} bytes while expecting 32", + bytes.len() + )); + } + + let mut output = [0; 32]; + output.copy_from_slice(&bytes[..]); + Ok(Self(output)) + } + } + impl fmt::Display for EntropyBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } + } + + Input::::new() + .with_prompt("Enter your root entropy") + .interact() + .expect("failed getting root entropy") + .0 + } + + /// Add a monitor and wait for it to catch up. + fn add_monitor_and_wait_for_sync(&mut self, entropy: &[u8; 32]) -> Result<(), String> { + // Get account key from entropy + let mut req = mobilecoind_api::GetAccountKeyRequest::new(); + req.set_entropy(entropy.to_vec()); + + let mut resp = self + .client + .get_account_key(&req) + .map_err(|err| format!("Failed getting account key for entropy: {}", err))?; + + let account_key = resp.take_account_key(); + + // Add monitor for this account. + let mut req = mobilecoind_api::AddMonitorRequest::new(); + req.set_account_key(account_key); + req.set_first_subaddress(0); + req.set_num_subaddresses(1); + req.set_first_block(0); + + let resp = self + .client + .add_monitor(&req) + .map_err(|err| format!("Failed adding monitor: {}", err))?; + self.monitor_id = resp.get_monitor_id().to_vec(); + + // Get current number of blocks in ledger. + let resp = self + .client + .get_ledger_info(&mobilecoind_api::Empty::new()) + .map_err(|err| format!("Failed getting number of blocks in ledger: {}", err))?; + let num_blocks = resp.block_count; + + let pb = ProgressBar::new(num_blocks); + pb.set_style( + ProgressStyle::default_bar() + .template( + "Syncing account... {spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})", + ) + .progress_chars("#>-"), + ); + + let mut blocks_synced = 0; + while blocks_synced < num_blocks { + // Get current number of blocks synced. + let mut req = mobilecoind_api::GetMonitorStatusRequest::new(); + req.set_monitor_id(self.monitor_id.clone()); + + let resp = self + .client + .get_monitor_status(&req) + .map_err(|err| format!("Failed getting monitor status: {}", err))?; + + pb.set_position(blocks_synced); + blocks_synced = resp.get_status().next_block; + } + + // Done! + Ok(()) + } + + /// Print the current balance. + fn print_balance(&self) { + let mut req = mobilecoind_api::GetBalanceRequest::new(); + req.set_monitor_id(self.monitor_id.clone()); + req.set_subaddress_index(0); + + match self.client.get_balance(&req) { + Ok(resp) => { + let balance = resp.get_balance(); + + println!(); + println!( + " >>> Your balance is now {} <<<", + u64_to_mob_display(balance) + ); + println!(); + } + Err(err) => { + println!("Error getting balance: {}", err); + } + } + } + + /// Send coins flow. + fn send(&self) { + // Print intro text. + println!( + r#" +Please enter a payment request code. If you received an email with an allocation of TestNet mobilecoins, this is the longer alphanumeric string. It should look something like + + 3CioMy13rUrFWRCcXMjz4GayaVgRcqpRpz6JXzmryaN2NJjSv2YaKED33iYnUyAMa9vi1XLRoW8xVuzzJTsc6MArq5NBDHMZXDtYRSrA9AjFdfv6QzLF21AWc36yXcsiqGZkgLKk +"# + ); + + // Read and parse B58 request code. + #[derive(Clone)] + struct WrappedRequestPayload(pub Option); + impl FromStr for WrappedRequestPayload { + type Err = String; + fn from_str(src: &str) -> Result { + if src.is_empty() { + return Ok(Self(None)); + } + + Ok(Self(Some(RequestPayload::decode(src).map_err(|err| { + format!("Invalid request code: {}", err) + })?))) + } + } + impl fmt::Display for WrappedRequestPayload { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(inner) = &self.0 { + write!(f, "{}", inner.encode())?; + } + Ok(()) + } + } + + let opt_request_code = Input::::new() + .with_prompt("Enter your request code") + .allow_empty(true) + .interact() + .expect("failed getting request code") + .0; + if opt_request_code.is_none() { + return; + } + let mut request_code = opt_request_code.unwrap(); + + // Allow user to confirm, change amount or cancel. + let tx_proposal = loop { + println!(); + if request_code.memo.is_empty() { + println!( + "This request code is a bill for {}.", + u64_to_mob_display(request_code.value), + ); + } else { + println!( + "This request code is a bill for {}. It includes the memo:", + u64_to_mob_display(request_code.value), + ); + println!(); + println!("{}", request_code.memo); + println!(); + } + + // Construct TX to figure out the fee and whether we have enough funds. + let tx_proposal = match self.generate_tx(&request_code) { + Ok((tx_proposal, balance)) => { + let fee = tx_proposal.get_fee(); + let remaining_balance = balance - fee - request_code.value; + println!( + "You will be charged a fee of {} to send this payment. Your remaining balance after paying this bill will be {}.", + u64_to_mob_display(fee), + u64_to_mob_display(remaining_balance), + ); + println!(); + + tx_proposal + } + + Err(err) => { + println!("Error generating transaction: {}", err); + println!("You will not be able to send this payment. It is possible you do not have enough funds, in which case you can edit the payment amount."); + println!(); + + println!("Please select from the following available options:"); + let selection = Select::with_theme(&ColorfulTheme::default()) + .default(0) + .items(&[ + "Change payment amount".to_owned(), + "Cancel payment".to_owned(), + ]) + .interact() + .unwrap(); + match selection { + 0 => { + request_code.value = + Self::input_mob("Enter new amount (in MOB)", request_code.value); + continue; + } + 1 => { + return; + } + _ => unreachable!(), + }; + } + }; + + println!("Please select from the following available options:"); + let selection = Select::with_theme(&ColorfulTheme::default()) + .default(0) + .items(&[ + format!("Send payment of {}", u64_to_mob_display(request_code.value)), + "Change payment amount".to_owned(), + "Cancel payment".to_owned(), + ]) + .interact() + .unwrap(); + match selection { + 0 => { + break tx_proposal; + } + 1 => { + request_code.value = + Self::input_mob("Enter new amount (in MOB)", request_code.value); + } + 2 => { + return; + } + _ => unreachable!(), + } + }; + + // Send payment + let pb = ProgressBar::new_spinner(); + pb.enable_steady_tick(120); + + pb.set_message("Sending payment..."); + let mut req = mobilecoind_api::SubmitTxRequest::new(); + req.set_tx_proposal(tx_proposal); + + let mut resp = match self.client.submit_tx(&req) { + Ok(resp) => resp, + Err(err) => { + println!("Error submitting transaction: {}", err); + return; + } + }; + + let sender_tx_receipt = resp.take_sender_tx_receipt(); + + pb.set_message("Waiting for payment to complete..."); + let mut req = mobilecoind_api::GetTxStatusAsSenderRequest::new(); + req.set_receipt(sender_tx_receipt); + + loop { + let resp = match self.client.get_tx_status_as_sender(&req) { + Ok(resp) => resp, + Err(err) => { + println!("Failed checking tx status: {}", err); + thread::sleep(Duration::from_secs(1)); + continue; + } + }; + + match resp.get_status() { + mobilecoind_api::TxStatus::Unknown => { + thread::sleep(Duration::from_millis(250)); + } + mobilecoind_api::TxStatus::Verified => { + // Wait for monitor to sync so that we show the updated balance - this is a + // best effort attempt, if it fails we just skip it. + pb.set_message("Waiting for sync to complete..."); + let _ = self.wait_for_sync(); + + pb.finish_with_message("Payment was successful!"); + break; + } + mobilecoind_api::TxStatus::TombstoneBlockExceeded => { + pb.finish_with_message( + "Tombstone block exceeded - transaction did not go through!", + ); + println!(); + break; + } + } + } + + println!(); + } + + /// Wait for mobilecoind to finish syncing our monitor. + fn wait_for_sync(&self) -> Result<(), String> { + let resp = self + .client + .get_ledger_info(&mobilecoind_api::Empty::new()) + .map_err(|err| format!("Failed getting number of blocks in ledger: {}", err))?; + let num_blocks = resp.block_count; + + let mut blocks_synced = 0; + while blocks_synced < num_blocks { + // Get current number of blocks synced. + let mut req = mobilecoind_api::GetMonitorStatusRequest::new(); + req.set_monitor_id(self.monitor_id.clone()); + + let resp = self + .client + .get_monitor_status(&req) + .map_err(|err| format!("Failed getting monitor status: {}", err))?; + + blocks_synced = resp.get_status().next_block; + } + + Ok(()) + } + + /// Receive coins flow. + fn receive(&self) { + println!("You can create a request code to share with another MobileCoin user as a bill to receive a payment. You can meet other TestNet users and share request codes online at the MobileCoin forum."); + println!(); + + let amount = Self::input_mob( + "How many mobilecoins would you like to receive (in MOB)?", + 0, + ); + + println!(); + println!("Would you like to add a memo?"); + let selection = Select::with_theme(&ColorfulTheme::default()) + .default(0) + .items(&["Yes", "No"]) + .interact() + .unwrap(); + let memo = match selection { + 0 => Input::::new() + .with_prompt("Please enter your memo") + .allow_empty(true) + .interact() + .expect("failed getting memo"), + 1 => String::from(""), + _ => unreachable!(), + }; + println!(); + + // Get our public address. + let mut req = mobilecoind_api::GetPublicAddressRequest::new(); + req.set_monitor_id(self.monitor_id.clone()); + req.set_subaddress_index(0); + + let mut resp = match self.client.get_public_address(&req) { + Ok(resp) => resp, + Err(err) => { + println!("Failed getting our public address: {}", err); + return; + } + }; + + let public_address = resp.take_public_address(); + + // Generate b58 code + let mut req = mobilecoind_api::GetRequestCodeRequest::new(); + req.set_receiver(public_address); + req.set_value(amount); + req.set_memo(memo); + + let resp = match self.client.get_request_code(&req) { + Ok(resp) => resp, + Err(err) => { + println!("Failed generating request code: {}", err); + return; + } + }; + + println!("Your request code is:"); + println!(); + println!(" {}", resp.get_b58_code()); + println!(); + } + + /// Read an amount in MOB from the user. + fn input_mob(prompt: &str, default: u64) -> u64 { + // default is in picoMOB but we need it in MOB + let mob_default = Decimal::from(default) / Decimal::from_scientific("1e12").unwrap(); + + struct CanConvertToMOB; + impl Validator for CanConvertToMOB { + type Err = String; + fn validate(&self, text: &str) -> Result<(), Self::Err> { + let dec = Decimal::from_str(text).map_err(|err| format!("{}", err))?; + (dec * Decimal::from_scientific("1e12").unwrap()) + .to_u64() + .ok_or_else(|| "Value too big".to_owned())?; + Ok(()) + } + } + + let mob = Input::::new() + .with_prompt(prompt) + .default(mob_default) + .validate_with(CanConvertToMOB) + .interact() + .expect("failed getting request code"); + + // Convert MOB back to pMOB + (mob * Decimal::from_scientific("1e12").unwrap()) + .to_u64() + .expect("failed converting to u64") + } + + /// Helper method for generating a transaction from a B58 request code. + fn generate_tx( + &self, + request_payload: &RequestPayload, + ) -> Result<(mobilecoind_api::TxProposal, u64), String> { + let pb = ProgressBar::new_spinner(); + pb.enable_steady_tick(120); + pb.set_message("Preparing transaction..."); + + // Get our UnspentTxOuts. + let mut req = mobilecoind_api::GetUnspentTxOutListRequest::new(); + req.set_monitor_id(self.monitor_id.clone()); + req.set_subaddress_index(0); + + let resp = self + .client + .get_unspent_tx_out_list(&req) + .map_err(|err| format!("Unable to get unspent txouts: {}", err))?; + let utxos = resp.output_list; + let balance = utxos.iter().map(|utxo| utxo.get_value()).sum::(); + + // Create the outlay + let mut outlay = mobilecoind_api::Outlay::new(); + outlay.set_value(request_payload.value); + outlay.set_receiver(mobilecoind_api::PublicAddress::from( + &request_payload.into(), + )); + + // Construct the tx + let mut req = mobilecoind_api::GenerateTxRequest::new(); + req.set_sender_monitor_id(self.monitor_id.clone()); + req.set_change_subaddress(0); + req.set_input_list(utxos); + req.set_outlay_list(RepeatedField::from_vec(vec![outlay])); + + let mut resp = self + .client + .generate_tx(&req) + .map_err(|err| format!("Unable to generate transaction: {}", err))?; + Ok((resp.take_tx_proposal(), balance)) + } +} + +/// Helper method for converting a u64 picomob value into human-readable form. +fn u64_to_mob_display(val: u64) -> String { + let mut decimal_val: Decimal = val.into(); + + let kilo_mob = Decimal::from_scientific("1000e12").unwrap(); + let mob = Decimal::from_scientific("1e12").unwrap(); + let micro_mob = Decimal::from_scientific("1e6").unwrap(); + + if val == 0 { + "0 MOB".to_owned() + } else if decimal_val >= kilo_mob { + decimal_val /= kilo_mob; + format!("{:.3} kMOB", decimal_val) + } else if decimal_val >= mob { + decimal_val /= mob; + format!("{:.3} MOB", decimal_val) + } else if decimal_val >= micro_mob { + decimal_val /= micro_mob; + format!("{:.3} µMOB", decimal_val) + } else { + format!("{} pMOB", decimal_val) + } +} + +/// Tests. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_u64_to_mob_display() { + const MOB: u64 = 1_000_000_000_000; + assert_eq!(u64_to_mob_display(1), "1 pMOB"); + assert_eq!(u64_to_mob_display(123), "123 pMOB"); + + assert_eq!(u64_to_mob_display(MOB - 1), "999999.999 µMOB"); + assert_eq!(u64_to_mob_display(MOB), "1.000 MOB"); + assert_eq!(u64_to_mob_display(MOB + 1), "1.000 MOB"); + assert_eq!(u64_to_mob_display(MOB + 100_000_000), "1.000 MOB"); + assert_eq!(u64_to_mob_display(MOB + 1_000_000_000), "1.001 MOB"); + + assert_eq!(u64_to_mob_display(MOB * 1000), "1.000 kMOB"); + assert_eq!(u64_to_mob_display((MOB * 1000) + 1), "1.000 kMOB"); + assert_eq!(u64_to_mob_display((MOB * 1000) + MOB), "1.001 kMOB"); + } +} diff --git a/tools/local-network/local-network.py b/tools/local-network/local-network.py index 99160f2cbf..9759c1f78f 100755 --- a/tools/local-network/local-network.py +++ b/tools/local-network/local-network.py @@ -159,7 +159,7 @@ def __init__(self, name, node_num, client_port, peer_port, management_port, peer def peer_uri(self, broadcast_consensus_msgs=True): pub_key = subprocess.check_output(f'openssl pkey -in {self.msg_signer_key_file} -pubout | head -n-1 | tail -n+2 | sed "s/+/-/g; s/\//_/g"', shell=True).decode().strip() broadcast_consensus_msgs = '1' if broadcast_consensus_msgs else '0' - return f'mcp://localhost:{self.peer_port}/?ca-bundle=./attest/test_certs/selfsigned_mobilecoin.crt&tls-hostname=www.mobilecoin.com&consensus-msg-key={pub_key}&broadcast-consensus-msgs={broadcast_consensus_msgs}' + return f'insecure-mcp://localhost:{self.peer_port}/?consensus-msg-key={pub_key}&broadcast-consensus-msgs={broadcast_consensus_msgs}' def __repr__(self): return self.name @@ -214,7 +214,7 @@ def start(self, network): f'--origin-block-path {LEDGER_BASE}', f'--ledger-path {self.ledger_dir}', f'--client-listen-uri="insecure-mc://0.0.0.0:{self.client_port}/"', - f'--peer-listen-uri="mcp://0.0.0.0:{self.peer_port}/?tls-chain=./attest/test_certs/selfsigned_mobilecoin.crt&tls-key=./attest/test_certs/selfsigned_mobilecoin.key"', + f'--peer-listen-uri="insecure-mcp://0.0.0.0:{self.peer_port}/"', f'--scp-debug-dump {WORK_DIR}/scp-debug-dump-{self.node_num}', f'--management-listen-addr=0.0.0.0:{self.management_port}', f'--sealed-block-signing-key {WORK_DIR}/consensus-sealed-block-signing-key-{self.node_num}', @@ -333,8 +333,16 @@ def __init__(self): def build_binaries(self): print('Building binaries...') + enclave_pem = os.path.join(PROJECT_DIR, 'Enclave_private.pem') + if not os.path.exists(enclave_pem): + subprocess.run( + f'openssl genrsa -out {enclave_pem} -3 3072', + shell=True, + check=True, + ) + subprocess.run( - f'cd {PROJECT_DIR} && openssl genrsa -out Enclave_private.pem -3 3072 && CONSENSUS_ENCLAVE_PRIVKEY=Enclave_private.pem cargo build -p consensus-service -p ledger-distribution {CARGO_FLAGS}', + f'cd {PROJECT_DIR} && CONSENSUS_ENCLAVE_PRIVKEY="{enclave_pem}" cargo build -p consensus-service -p ledger-distribution {CARGO_FLAGS}', shell=True, check=True, ) diff --git a/transaction/core/Cargo.toml b/transaction/core/Cargo.toml index 0f7ec199e1..ff740d12e5 100644 --- a/transaction/core/Cargo.toml +++ b/transaction/core/Cargo.toml @@ -13,18 +13,19 @@ test-net-fee-keys = [] [dependencies] # External dependencies +aead = "0.2" bs58 = { version = "0.3.0", default-features = false, features = ["alloc"] } blake2 = { version = "0.8.1", default-features = false, features = ["simd"] } byteorder = { version = "1.3.4", default-features = false } crc = { version = "1.8.1", default-features = false } cfg-if = "0.1" digest = { version = "0.8.1", default-features = false } -ecies = { path = "../..//crypto/ecies" } failure = { version = "0.1.5", default-features = false, features = ["derive"] } generic-array = { version = "0.12", features = ["serde"] } hex_fmt = "0.3" hkdf = { version = "0.8.0", default-features = false } lazy_static = { version = "1.4.0", features = ["spin_no_std"] } +mc-crypto-box = { path = "../../crypto/box" } merlin = { version = "2.0", default-features = false } prost = { version = "0.6.1", default-features = false, features = ["prost-derive"] } rand_core = { version = "0.5", default-features = false } @@ -34,7 +35,7 @@ subtle = { version = "2.1", default-features = false } # MobileCoin dependencies common = { path = "../../common", default-features = false } -digestible = { path = "../..//crypto/digestible", features = ["dalek"] } +digestible = { path = "../../crypto/digestible", features = ["dalek"] } keys = { path = "../../crypto/keys", default-features = false } mcrand = { path = "../../crypto/mcrand" } mcserial = { path = "../../util/mcserial" } diff --git a/transaction/core/src/amount.rs b/transaction/core/src/amount.rs index 1fe4968cef..a691105743 100644 --- a/transaction/core/src/amount.rs +++ b/transaction/core/src/amount.rs @@ -1,6 +1,6 @@ // Copyright (c) 2018-2020 MobileCoin Inc. -//! A commitment to an output's amount. +//! A commitment to an output's amount, denominated in picoMOB. //! //! Amounts are implemented as Pedersen commitments. The associated private keys are "masked" using //! a shared secret. @@ -8,8 +8,8 @@ #![cfg_attr(test, allow(clippy::unnecessary_operation))] use crate::{ - constants::MAX_TINY_MOB, - ring_signature::{Blinding, Commitment, CurveScalar, GENERATORS}, + ring_signature::{Blinding, CurveScalar, GENERATORS}, + CompressedCommitment, }; use blake2::{Blake2b, Digest}; use curve25519_dalek::scalar::Scalar; @@ -23,21 +23,17 @@ use serde::{Deserialize, Serialize}; /// Errors that can occur when constructing an amount. #[derive(Debug, Fail, Eq, PartialEq)] pub enum AmountError { - /// The Amount is too damn high. - #[fail(display = "Amount exceeds MAX_TINY_MOB: {}", _0)] - ExceedsLimit(u64), - /// The masked value, masked blinding, or shared secret are not consistent with the commitment. #[fail(display = "Inconsistent Commitment")] InconsistentCommitment, } -/// A commitment to the amount of the `n^th` output in a transaction. +/// A commitment to an amount of MobileCoin, denominated in picoMOB. #[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Message, Digestible)] pub struct Amount { /// A Pedersen commitment `v*G + b*H` to a quantity `v` of MobileCoin, with blinding `b`, #[prost(message, required, tag = "1")] - pub commitment: Commitment, + pub commitment: CompressedCommitment, /// `masked_value = value + Blake2B(shared_secret)` #[prost(message, required, tag = "2")] @@ -53,7 +49,7 @@ impl Amount { /// so that they can be recovered by the recipient. /// /// # Arguments - /// * `value` - The committed value `v`. + /// * `value` - The committed value `v`, in picoMOB. /// * `blinding` - The blinding `b`. /// * `shared_secret` - The shared secret, e.g. `rB` for transaction private key `r` and recipient public key `B`. #[inline] @@ -62,17 +58,12 @@ impl Amount { blinding: Blinding, shared_secret: &RistrettoPublic, ) -> Result { - if value > MAX_TINY_MOB { - return Err(AmountError::ExceedsLimit(value)); - } - - let value: Scalar = Scalar::from(value); - // Pedersen commitment `v*G + b*H`. - let commitment: Commitment = Commitment::from(GENERATORS.commit(value, blinding.into())); + let commitment = CompressedCommitment::new(value, blinding.into()); // `v + Blake2B(shared_secret)` let masked_value: Scalar = { + let value: Scalar = Scalar::from(value); let mask = get_value_mask(&shared_secret); value + mask }; @@ -92,6 +83,8 @@ impl Amount { /// Returns the value `v` and blinding `b` in the commitment `v*G + b*H`. /// + /// Value is denominated in picoMOB. + /// /// # Arguments /// * `shared_secret` - The shared secret, e.g. `rB`. pub fn get_value( @@ -101,8 +94,7 @@ impl Amount { let value: u64 = self.unmask_value(shared_secret); let blinding = self.unmask_blinding(shared_secret); - let expected_commitment = - Commitment::from(GENERATORS.commit(Scalar::from(value), blinding.into())); + let expected_commitment = CompressedCommitment::new(value, blinding.into()); if self.commitment != expected_commitment { // The commitment does not agree with the provided value and blinding. // This either means that the commitment does not correspond to the shared secret, or @@ -165,13 +157,13 @@ fn get_mask(shared_secret: &RistrettoPublic) -> Scalar { #[cfg(test)] mod tests { - use crate::{proptest_fixtures::*, ring_signature::Commitment}; + use crate::proptest_fixtures::*; use proptest::prelude::*; use crate::{ amount::{Amount, AmountError}, - constants::MAX_TINY_MOB, ring_signature::{Scalar, GENERATORS}, + CompressedCommitment, }; proptest! { @@ -179,45 +171,28 @@ mod tests { #[test] /// Amount::new() should return Ok for valid values and blindings. fn test_new_ok( - value in (0u64..=MAX_TINY_MOB), + value in any::(), blinding in arbitrary_blinding(), shared_secret in arbitrary_ristretto_public()) { assert!(Amount::new(value, blinding, &shared_secret).is_ok()); } - #[test] - /// Amount::new() should return ExceedsLimit for values larger than MAX_TINY_MOB. - fn test_new_exceeds_limit_error( - value in ((MAX_TINY_MOB+1)..=core::u64::MAX), - blinding in arbitrary_blinding(), - shared_secret in arbitrary_ristretto_public()) { - - match Amount::new(value, blinding, &shared_secret){ - Err(AmountError::ExceedsLimit(_)) => {}, // This is expected. - _ => panic!(), - } - } - #[test] #[allow(non_snake_case)] /// amount.commitment should agree with the value and blinding. fn test_commitment( - value in (0u64..=MAX_TINY_MOB), + value in any::(), blinding in arbitrary_blinding(), shared_secret in arbitrary_ristretto_public()) { let amount = Amount::new(value, blinding, &shared_secret).unwrap(); - let G = GENERATORS.B; - let H = GENERATORS.B_blinding; - - let blinding: Scalar = blinding.into(); - let expected_commitment: Commitment = Commitment::from(Scalar::from(value) * G + blinding * H); + let expected_commitment = CompressedCommitment::new(value, blinding.into()); assert_eq!(amount.commitment, expected_commitment); } #[test] /// amount.unmask_value should return the value used to construct the amount. fn test_unmask_value( - value in (0u64..=MAX_TINY_MOB), + value in any::(), blinding in arbitrary_blinding(), shared_secret in arbitrary_ristretto_public()) { @@ -232,7 +207,7 @@ mod tests { #[test] /// amount.unmask_blinding should return the blinding used to construct the amount. fn test_unmask_blinding( - value in (0u64..=MAX_TINY_MOB), + value in any::(), blinding in arbitrary_blinding(), shared_secret in arbitrary_ristretto_public()) { @@ -246,7 +221,7 @@ mod tests { #[test] /// get_value should return the correct value and blinding. fn test_get_value_ok( - value in (0u64..=MAX_TINY_MOB), + value in any::(), blinding in arbitrary_blinding(), shared_secret in arbitrary_ristretto_public()) { @@ -260,7 +235,7 @@ mod tests { #[test] /// get_value should return InconsistentCommitment if the masked value is incorrect. fn test_get_value_incorrect_masked_value( - value in (0u64..=MAX_TINY_MOB), + value in any::(), other_masked_value in arbitrary_curve_scalar(), blinding in arbitrary_blinding(), shared_secret in arbitrary_ristretto_public()) @@ -277,9 +252,9 @@ mod tests { #[test] /// get_value should return InconsistentCommitment if the masked blinding is incorrect. fn test_get_value_incorrect_blinding( - value in (0u64..=MAX_TINY_MOB), + value in any::(), blinding in arbitrary_blinding(), - other_masked_blinding in arbitrary_curve_scalar(), + other_masked_blinding in arbitrary_curve_scalar(), shared_secret in arbitrary_ristretto_public()) { // Mutate amount to use a other_masked_blinding. @@ -293,7 +268,7 @@ mod tests { #[test] /// get_value should return an Error if shared_secret is incorrect. fn test_get_value_invalid_shared_secret( - value in (0u64..=MAX_TINY_MOB), + value in any::(), blinding in arbitrary_blinding(), shared_secret in arbitrary_ristretto_public(), other_shared_secret in arbitrary_ristretto_public(), diff --git a/transaction/core/src/commitment.rs b/transaction/core/src/commitment.rs new file mode 100644 index 0000000000..9c1be017c1 --- /dev/null +++ b/transaction/core/src/commitment.rs @@ -0,0 +1,93 @@ +use crate::{ + compressed_commitment::CompressedCommitment, + ring_signature::{Error, Scalar, GENERATORS}, +}; +use core::{convert::TryFrom, fmt}; +use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint}; +use digestible::Digestible; +use mcserial::{ + deduce_core_traits_from_public_bytes, prost_message_helper32, try_from_helper32, ReprBytes32, +}; +use serde::{Deserialize, Serialize}; + +/// A Pedersen commitment in uncompressed Ristretto format. +#[derive(Copy, Clone, Default, Digestible)] +pub struct Commitment { + /// A Pedersen commitment `v*G + b*H` to a quantity `v` with blinding `b`, + pub point: RistrettoPoint, +} + +impl Commitment { + pub fn new(value: u64, blinding: Scalar) -> Self { + Self { + point: GENERATORS.commit(Scalar::from(value), blinding), + } + } +} + +impl TryFrom<&CompressedCommitment> for Commitment { + type Error = crate::ring_signature::Error; + + fn try_from(src: &CompressedCommitment) -> Result { + let point = src.point.decompress().ok_or(Error::InvalidCurvePoint)?; + Ok(Self { point }) + } +} + +impl fmt::Debug for Commitment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Commitment({})", + hex_fmt::HexFmt(self.point.compress().as_bytes()) + ) + } +} + +impl ReprBytes32 for Commitment { + type Error = Error; + fn to_bytes(&self) -> [u8; 32] { + self.point.compress().to_bytes() + } + fn from_bytes(src: &[u8; 32]) -> Result { + let point = CompressedRistretto::from_slice(src) + .decompress() + .ok_or(Error::InvalidCurvePoint)?; + Ok(Self { point }) + } +} + +// Implements prost::Message. Requires Debug and ReprBytes32. +prost_message_helper32! { Commitment } + +// Implements try_from<&[u8;32]> and try_from<&[u8]>. Requires ReprBytes32. +try_from_helper32! { Commitment } + +#[cfg(test)] +#[allow(non_snake_case)] +mod commitment_tests { + use crate::{ + ring_signature::{Scalar, GENERATORS}, + Commitment, + }; + use curve25519_dalek::ristretto::RistrettoPoint; + use rand::{rngs::StdRng, RngCore, SeedableRng}; + + #[test] + // Commitment::new should create the correct RistrettoPoint. + fn test_new() { + let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); + let value = rng.next_u64(); + let blinding = Scalar::random(&mut rng); + + let commitment = Commitment::new(value, blinding); + + let expected_point: RistrettoPoint = { + let G = GENERATORS.B; + let H = GENERATORS.B_blinding; + Scalar::from(value) * G + blinding * H + }; + + assert_eq!(commitment.point, expected_point); + } +} diff --git a/transaction/core/src/compressed_commitment.rs b/transaction/core/src/compressed_commitment.rs new file mode 100644 index 0000000000..e913ebfba5 --- /dev/null +++ b/transaction/core/src/compressed_commitment.rs @@ -0,0 +1,106 @@ +use crate::{ + commitment::Commitment, + ring_signature::{Error, Scalar, GENERATORS}, +}; +use core::{convert::TryFrom, fmt}; +use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint}; +use digestible::Digestible; +use mcserial::{ + deduce_core_traits_from_public_bytes, prost_message_helper32, try_from_helper32, ReprBytes32, +}; +use serde::{Deserialize, Serialize}; + +/// A Pedersen commitment in compressed Ristretto format. +#[derive(Copy, Clone, Default, Eq, Serialize, Deserialize, Digestible)] +pub struct CompressedCommitment { + /// A Pedersen commitment `v*G + b*H` to a quantity `v` with blinding `b`, + pub point: CompressedRistretto, +} + +impl CompressedCommitment { + pub fn new(value: u64, blinding: Scalar) -> Self { + Self { + point: GENERATORS.commit(Scalar::from(value), blinding).compress(), + } + } +} + +impl From<&Commitment> for CompressedCommitment { + fn from(src: &Commitment) -> Self { + Self { + point: src.point.compress(), + } + } +} +impl From<&CompressedRistretto> for CompressedCommitment { + fn from(source: &CompressedRistretto) -> Self { + Self { point: *source } + } +} + +impl fmt::Debug for CompressedCommitment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CompressedCommitment({})", + hex_fmt::HexFmt(self.point.as_bytes()) + ) + } +} + +impl AsRef<[u8; 32]> for CompressedCommitment { + fn as_ref(&self) -> &[u8; 32] { + self.point.as_bytes() + } +} + +// Implements Ord, PartialOrd, PartialEq, Hash. Requires AsRef<[u8;32]>. +deduce_core_traits_from_public_bytes! { CompressedCommitment } + +impl ReprBytes32 for CompressedCommitment { + type Error = Error; + fn to_bytes(&self) -> [u8; 32] { + self.point.to_bytes() + } + fn from_bytes(src: &[u8; 32]) -> Result { + Ok(Self { + point: CompressedRistretto::from_slice(src), + }) + } +} + +// Implements prost::Message. Requires Debug and ReprBytes32. +prost_message_helper32! { CompressedCommitment } + +// Implements try_from<&[u8;32]> and try_from<&[u8]>. Requires ReprBytes32. +try_from_helper32! { CompressedCommitment } + +#[cfg(test)] +#[allow(non_snake_case)] +mod compressed_commitment_tests { + use crate::{ + ring_signature::{Scalar, GENERATORS}, + CompressedCommitment, + }; + use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint}; + use rand::{rngs::StdRng, RngCore, SeedableRng}; + + #[test] + // Commitment::new should create the correct RistrettoPoint. + fn test_new() { + let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); + let value = rng.next_u64(); + let blinding = Scalar::random(&mut rng); + + let commitment = CompressedCommitment::new(value, blinding); + + let expected_point: CompressedRistretto = { + let G = GENERATORS.B; + let H = GENERATORS.B_blinding; + let point = Scalar::from(value) * G + blinding * H; + point.compress() + }; + + assert_eq!(commitment.point, expected_point); + } +} diff --git a/transaction/core/src/constants.rs b/transaction/core/src/constants.rs index e4ce364062..3a9b7fb5b1 100644 --- a/transaction/core/src/constants.rs +++ b/transaction/core/src/constants.rs @@ -5,31 +5,23 @@ /// Maximum number of transactions that may be included in a Block. pub const MAX_TRANSACTIONS_PER_BLOCK: usize = 5000; -/// Each input ring must contain at least this many TxOuts. -pub const MIN_RING_SIZE: usize = 11; - -/// Each input ring must contain no more than this many TxOuts. -pub const MAX_RING_SIZE: usize = 11; +/// Each input ring must contain this many elements. +pub const RING_SIZE: usize = 11; /// Each transaction must contain no more than this many inputs (rings). -// TODO: Tweak this based on performance measurements. pub const MAX_INPUTS: u16 = 16; /// Each transaction must contain no more than this many outputs. -// TODO: Tweak this based on performance measurements/subaddress limitations. pub const MAX_OUTPUTS: u16 = 16; /// Maximum number of blocks in the future a transaction's tombstone block can be set to. pub const MAX_TOMBSTONE_BLOCKS: u64 = 100; -/// We are contractually obligated to create 250 million mobile coins (MOB) -pub const MAX_MOB: u64 = 250_000_000; - -/// 1 MOB = 2^{TINY_MOB_EXPONENT} TinyMOB -pub const TINY_MOB_EXPONENT: u8 = 34; +/// The MobileCoin network will contain a fixed supply of 250 million mobilecoins (MOB). +pub const TOTAL_MOB: u64 = 250_000_000; -/// The maximum number of MOB, denominated in TinyMOB. -pub const MAX_TINY_MOB: u64 = MAX_MOB << TINY_MOB_EXPONENT; +/// Minimum allowed fee, denominated in picoMOB. +pub const BASE_FEE: u64 = 10; cfg_if::cfg_if! { if #[cfg(any(test, feature="test-net-fee-keys"))] { @@ -38,7 +30,7 @@ cfg_if::cfg_if! { /// let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); /// let foundation_account_key = AccountKey::random(&mut rng); /// - /// This is available in the `generate_test_foundation_key` utilitiy. + /// This is available in the `generate_test_foundation_key` utility. pub const FEE_SPEND_PUBLIC_KEY: [u8; 32] = [ 160, 79, 78, 17, 132, 143, 209, 245, 178, 242, 129, 141, 206, 68, 64, 194, 71, 138, 167, 101, 214, 0, 76, 82, 159, 44, 114, 209, 83, 142, 35, 50, @@ -61,6 +53,3 @@ cfg_if::cfg_if! { compile_error!("must specify either main-net-fee-keys or test-net-fee-keys feature"); } } - -/// Minimum allowed fee. -pub const BASE_FEE: u64 = 10; diff --git a/transaction/core/src/encoders.rs b/transaction/core/src/encoders.rs deleted file mode 100644 index 2b5517dd81..0000000000 --- a/transaction/core/src/encoders.rs +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright (c) 2018-2020 MobileCoin Inc. - -//! Formats for encoding MobileCoin addresses. - -use crate::account_keys::PublicAddress; -use alloc::{ - string::{FromUtf8Error, String}, - vec::Vec, -}; -use bs58; -use byteorder::{ByteOrder, LittleEndian}; -use core::convert::TryFrom; -use crc::crc32; -use failure::Fail; -use keys::{CompressedRistrettoPublic, KeyError, RistrettoPublic}; -use serde::{Deserialize, Serialize}; - -/// Types of MobileCoin addresses. -enum AddressType { - PublicAddress = 0, -} -const ENCODING_VERSION: u8 = 1; - -/// Address encoding and decoding. -pub trait AddressEncoder: Sized { - /// Decodes an address. - fn decode(encoded_address: String) -> Result; - - /// Encodes an address. - fn encode(&self) -> String; -} - -/// A collection of errors encountered when parsing address display formats. -#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Fail, Ord, PartialEq, PartialOrd, Serialize)] -pub enum AddressParseError { - /// A public key is not a valid Ristretto point. - #[fail(display = "Parsing failed with key error {:?}", _0)] - KeyError(KeyError), - - /// Not enough bytes to create an address. - #[fail(display = "The address is not long enough")] - InsufficientBytes, - - /// The address is not a valid base58 string. - #[fail(display = "The address is not a valid base58 string")] - Base58DecodingError, - - /// Unable to parse FogUrL string. - #[fail(display = "Unable to parse FogURL string")] - FogURLParsingError, - - /// Checksum for payload is incorrect. - #[fail(display = "Checksum for payload is incorrect")] - ChecksumError, - - /// Encoded string is the wrong type. - #[fail(display = "Encoded string is the wrong type")] - TypeMismatchError, -} - -impl From for AddressParseError { - fn from(src: KeyError) -> Self { - AddressParseError::KeyError(src) - } -} - -impl From for AddressParseError { - fn from(_: bs58::decode::Error) -> Self { - AddressParseError::Base58DecodingError - } -} - -impl From for AddressParseError { - fn from(_: FromUtf8Error) -> Self { - AddressParseError::FogURLParsingError - } -} - -/// The checksum is prepended to any address data to confirm that there are -/// no typographical errors. Little-endian IEEE CRC32. -fn checksum(data: &[u8]) -> [u8; 4] { - let checksum = crc32::checksum_ieee(data); - let mut result = [0; 4]; - LittleEndian::write_u32(&mut result, checksum); - result -} - -impl AddressEncoder for PublicAddress { - /// Creates a new public address from a string formatted according to the MobileCoin - /// public address display format - /// - /// The address format is a base58 encoded byte array, with bytes: - /// [0..4] checksum of entire payload (litte-endian IEEE CRC32) - /// [4] type of address (PublicAddress = 0) - /// [5] version of address encoding - /// [6..38] view key - /// [38..70] spend key - /// [70] length of FogURL (f) - /// [71..71+f] FogURL as utf-8 encoded string - /// [71+f..] Potential additional data - /// - /// # Arguments - /// `encoded_address` - A string representing the encoded address - fn decode(encoded_address: String) -> Result { - let address_bytes = bs58::decode(encoded_address).into_vec()?; - if address_bytes.len() >= 70 { - let checksum = checksum(&address_bytes[4..]); - if checksum != address_bytes[0..4] { - return Err(AddressParseError::ChecksumError); - } - if address_bytes[4] != (AddressType::PublicAddress as u8) { - return Err(AddressParseError::TypeMismatchError); - } - let view_key = RistrettoPublic::try_from(&address_bytes[6..38])?; - let spend_key = RistrettoPublic::try_from(&address_bytes[38..70])?; - if address_bytes.len() == 70 { - Ok(Self::new(&spend_key, &view_key)) - } else { - let fqdn_len = address_bytes[70] as usize; - if address_bytes.len() < 71 + fqdn_len { - return Err(AddressParseError::InsufficientBytes); - } - let fog_url_bytes = &address_bytes[71..(71 + fqdn_len)]; - let fog_url = String::from_utf8(fog_url_bytes.to_vec())?; - Ok(Self::new_with_fog(&spend_key, &view_key, fog_url)) - } - } else { - Err(AddressParseError::InsufficientBytes) - } - } - - /// Encodes this public address to a string. See `decode` above for details - /// on the encoding. - fn encode(&self) -> String { - let mut encoded_vec = Vec::new(); - - encoded_vec.push(AddressType::PublicAddress as u8); - encoded_vec.push(ENCODING_VERSION); - - // keys - encoded_vec - .extend_from_slice(CompressedRistrettoPublic::from(self.view_public_key()).as_ref()); - encoded_vec - .extend_from_slice(CompressedRistrettoPublic::from(self.spend_public_key()).as_ref()); - - // Fog url - if let Some(fog_url) = self.fog_url() { - encoded_vec.push(fog_url.len() as u8); - encoded_vec.extend_from_slice(&fog_url.as_bytes()); - } - - // Prepend with checksum - let mut checksum_vec = Vec::new(); - checksum_vec.extend_from_slice(&checksum(&encoded_vec)[..]); - checksum_vec.extend_from_slice(&encoded_vec); - bs58::encode(&checksum_vec[..]).into_string() - } -} - -#[cfg(test)] -mod testing { - use super::*; - use crate::account_keys::AccountKey; - #[test] - fn public_address_encoding_roundtrip() { - test_helper::run_with_several_seeds(|mut rng| { - { - let acct = AccountKey::random(&mut rng); - let encoded = acct.default_subaddress().encode(); - let result = PublicAddress::decode(encoded).unwrap(); - assert_eq!(acct.default_subaddress(), result); - } - { - let acct = AccountKey::random_with_fog(&mut rng); - let encoded = acct.default_subaddress().encode(); - let result = PublicAddress::decode(encoded).unwrap(); - assert_eq!(acct.default_subaddress(), result); - } - }); - } - #[test] - fn sample_public_addresses() { - // These are the test cases used in other libraries, they should be consistent - // regardless of implementation - let alice_view = [ - 166, 74, 193, 46, 6, 55, 219, 137, 34, 216, 57, 161, 74, 3, 239, 221, 4, 18, 227, 206, - 47, 97, 22, 65, 183, 227, 61, 51, 113, 56, 24, 25, - ]; - let alice_spend = [ - 150, 146, 51, 240, 178, 213, 250, 183, 11, 84, 216, 245, 95, 116, 41, 121, 176, 45, 39, - 240, 198, 218, 32, 224, 10, 178, 70, 194, 198, 211, 21, 52, - ]; - let alice_public = PublicAddress::new_with_fog( - &RistrettoPublic::try_from(&alice_spend).unwrap(), - &RistrettoPublic::try_from(&alice_view).unwrap(), - "example.com", - ); - let alice_address = "ujop75aHu64WKZgYGEr4UJJZXk5j9jAUtnLdcdifcJ5nCrehWwEgNQZd3JLpLSV55WfUtsURxsghuoX8rpeLgF9xQZN4bDau3XztijShBMvtkqak"; - let alice_decoded = PublicAddress::decode(String::from(alice_address)).unwrap(); - assert_eq!(alice_public, alice_decoded); - - let bob_view = [ - 74, 212, 31, 106, 179, 194, 87, 189, 2, 248, 103, 65, 73, 73, 97, 130, 224, 178, 164, - 95, 242, 176, 49, 182, 201, 137, 235, 243, 253, 165, 159, 119, - ]; - let bob_spend = [ - 98, 4, 17, 200, 238, 250, 195, 28, 250, 227, 124, 56, 234, 222, 169, 21, 114, 123, 133, - 205, 242, 36, 50, 213, 149, 136, 172, 233, 99, 151, 152, 114, - ]; - let bob_public = PublicAddress::new_with_fog( - &RistrettoPublic::try_from(&bob_spend).unwrap(), - &RistrettoPublic::try_from(&bob_view).unwrap(), - "example.com", - ); - let bob_address = "wM1y2oMStbmRysFv1aABTFDjKT1zzfHzT8dDf1HGyigfduPmKj89CgAJhhnHTzAjuAU8ZN1Bv8S3qAWk6cW6piGsrP4sWRUuzrWCR4zqkAZ1C94g"; - let bob_decoded = PublicAddress::decode(String::from(bob_address)).unwrap(); - assert_eq!(bob_public, bob_decoded); - } - - #[test] - fn bad_public_address_encoding() { - assert_eq!( - PublicAddress::decode(String::from( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - )) - .unwrap_err(), - AddressParseError::InsufficientBytes - ); - assert_eq!( - PublicAddress::decode(String::from( - "0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o0o" - )) - .unwrap_err(), - AddressParseError::Base58DecodingError - ); - assert_eq!( - PublicAddress::decode(String::from( - "Ujop75aHu64WKZgYGEr4UJJZXk5j9jAUtnLdcdifcJ5nCrehWwEgNQZd3JLpLSV55WfUtsURxsghuoX8rpeLgF9xQZN4bDau3XztijShBMvtkqak" - )) - .unwrap_err(), - AddressParseError::ChecksumError - ); - } -} diff --git a/transaction/core/src/encrypted_fog_hint.rs b/transaction/core/src/encrypted_fog_hint.rs index 8a3ad7cbf9..1ca62cd61f 100644 --- a/transaction/core/src/encrypted_fog_hint.rs +++ b/transaction/core/src/encrypted_fog_hint.rs @@ -10,8 +10,12 @@ use alloc::{vec, vec::Vec}; use core::{convert::TryFrom, fmt}; use digestible::Digestible; -use generic_array::{typenum, GenericArray}; +use generic_array::{ + typenum::{Diff, Unsigned, U128}, + GenericArray, +}; use keys::FromRandom; +use mc_crypto_box::{CryptoBox, VersionedCryptoBox}; use prost::{ bytes::{Buf, BufMut}, encoding::{bytes, skip_field, DecodeContext, WireType}, @@ -20,31 +24,37 @@ use prost::{ use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; -// The length of the discovery hint field in the ledger. -// Must be at least as large as ecies::ECIES_EXTRA_SPACE, or it can't hold an ECIES encryption. -pub type EncryptedFogHintSize = typenum::U128; -// TODO: When generic array marks this function const, use this version -// use typenum::marker_traits::Unsigned; -///pub const ENCRYPTED_FOG_HINT_LEN: usize = EncryptedFogHintSize::to_usize(); -pub const ENCRYPTED_FOG_HINT_LEN: usize = 128; +// The length of the encrypted fog hint field in the ledger. +// Must be at least as large as McRistrettoBox::FooterSize, or it can't hold a +// Ristretto-Box encryption. +pub type EncryptedFogHintSize = U128; +pub const ENCRYPTED_FOG_HINT_LEN: usize = EncryptedFogHintSize::USIZE; + +type Bytes = GenericArray; #[derive( Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize, Default, Digestible, )] pub struct EncryptedFogHint { - bytes: GenericArray, + bytes: Bytes, } // AsRef and AsMut slice conversions -impl AsRef<[u8]> for EncryptedFogHint { - fn as_ref(&self) -> &[u8] { - self.bytes.as_slice() +impl AsRef for EncryptedFogHint { + fn as_ref(&self) -> &Bytes { + &self.bytes + } +} + +impl AsMut for EncryptedFogHint { + fn as_mut(&mut self) -> &mut Bytes { + &mut self.bytes } } -impl AsMut<[u8]> for EncryptedFogHint { - fn as_mut(&mut self) -> &mut [u8] { - self.bytes.as_mut_slice() +impl From for EncryptedFogHint { + fn from(bytes: Bytes) -> Self { + Self { bytes } } } @@ -89,21 +99,21 @@ impl EncryptedFogHint { /// To be used in prod when sending to a recipient with no known fog server /// This means it should be indistinguishable from an ecies encryption of a /// random plaintext. There are several ways we could sample that distribution - /// but the simplest is to do exactly that. This is also future proof if we later - /// tweak the ECIES implementation. + /// but the simplest is to do exactly that. This is also future-proof if we later + /// tweak the cryptobox implementation. pub fn fake_onetime_hint(rng: &mut T) -> Self { - let mut result = [0u8; ENCRYPTED_FOG_HINT_LEN]; - let mut plaintext = [0u8; ENCRYPTED_FOG_HINT_LEN - ecies::ECIES_EXTRA_SPACE]; - rng.fill_bytes(&mut plaintext); + // Make plaintext of the right size + let plaintext = GenericArray::< + u8, + Diff::FooterSize>, + >::default(); + // Make a random key let key = keys::RistrettoPublic::from_random(rng); - ecies::encrypt_into( - rng, - &key, - &plaintext[..], - &ecies::DEFAULT_HKDF_SALT, - &mut result, - ); - Self::from(&result) + // encrypt_in_place into the buffer + let bytes = VersionedCryptoBox::default() + .encrypt_fixed_length(rng, &key, &plaintext) + .expect("Encryption error"); + Self { bytes } } } diff --git a/transaction/core/src/fog_hint.rs b/transaction/core/src/fog_hint.rs index 01a292c455..17a7f22831 100644 --- a/transaction/core/src/fog_hint.rs +++ b/transaction/core/src/fog_hint.rs @@ -4,10 +4,14 @@ use crate::{ account_keys::PublicAddress, - encrypted_fog_hint::{EncryptedFogHint, ENCRYPTED_FOG_HINT_LEN}, + encrypted_fog_hint::{EncryptedFogHint, EncryptedFogHintSize}, }; use core::convert::TryFrom; use keys::{CompressedRistrettoPublic, RistrettoPrivate, RistrettoPublic, RISTRETTO_PUBLIC_LEN}; +use mc_crypto_box::{ + generic_array::{typenum::Diff, GenericArray}, + CryptoBox, Error as CryptoBoxError, VersionedCryptoBox, +}; use mcserial::ReprBytes32; use rand_core::{CryptoRng, RngCore}; @@ -20,15 +24,6 @@ pub struct FogHint { // detect more easily when decryption failed const MAGIC_NUMBER: u8 = 42; -// The ENCRYPTED_DISCOVERY_HINT is an ecies encryption of the data in account hint. -const FOG_HINT_DECRYPTED_LEN: usize = ENCRYPTED_FOG_HINT_LEN - ecies::ECIES_EXTRA_SPACE; -// The plaintext is, one curve point, plus some padding. There should be a nonzero amount of padding, -// but probably only 8 bytes is sufficient. The size of the field is set in ledger crate which is -// lower level than us, so we work backwards from their constant. -// If this constant is negative fail the build. -#[allow(unused)] -const FOG_HINT_PADDING_LEN: usize = FOG_HINT_DECRYPTED_LEN - keys::RISTRETTO_PUBLIC_LEN; - // Construct a (plaintext) FogHint appropriate to send to a PublicAddress impl From<&PublicAddress> for FogHint { fn from(src: &PublicAddress) -> Self { @@ -39,25 +34,21 @@ impl From<&PublicAddress> for FogHint { } impl FogHint { - #[inline] pub fn new(view_pubkey: RistrettoPublic) -> Self { Self { view_pubkey: CompressedRistrettoPublic::from(view_pubkey), } } - #[inline] - pub fn from_slice(bytes: &[u8]) -> Result { + pub fn from_slice(bytes: &[u8]) -> Result { Ok(Self { - view_pubkey: CompressedRistrettoPublic::try_from(bytes).or(Err(()))?, + view_pubkey: CompressedRistrettoPublic::try_from(bytes).map_err(CryptoBoxError::Key)?, }) } - #[inline] pub fn to_bytes(&self) -> [u8; keys::RISTRETTO_PUBLIC_LEN] { self.view_pubkey.to_bytes() } /// Get the view pubkey - #[inline] pub fn get_view_pubkey(&self) -> &CompressedRistrettoPublic { &self.view_pubkey } @@ -68,74 +59,58 @@ impl FogHint { /// Given an rng, and ingest server public key, /// produce an encrypted fog hint to attach to the TXO /// - /// The first 32 bytes of the output are the ECIES curve point - /// The second 96 bytes are the payload B,C padded with 32 MAGIC_NUMBER bytes, encrypted - /// using AES cipher (per ECIES design). - /// The MAGIC_NUMBER permit us to unambiguously determine when the decryption failed - /// due to i.e. key mismatch. + /// The first 32 bytes of the plaintext are the FogHint curvepoint + /// The MAGIC_NUMBER help us to determine when the decryption failed /// /// # Arguments /// * rng (for encryption) - /// * acct_server_pubkey (to encrypt against) + /// * ingest_server_pubkey (to encrypt against) /// /// # Returns /// * 128 byte payload, cannot fail - #[inline] pub fn encrypt( &self, ingest_server_pubkey: &RistrettoPublic, rng: &mut T, ) -> EncryptedFogHint { - let plaintext = { - let mut result = [MAGIC_NUMBER; FOG_HINT_DECRYPTED_LEN]; - result[0..RISTRETTO_PUBLIC_LEN].copy_from_slice(&self.view_pubkey.to_bytes()); - result - }; - let mut ciphertext = [0u8; ENCRYPTED_FOG_HINT_LEN]; - ecies::encrypt_into( - rng, - ingest_server_pubkey, - &plaintext, - &ecies::DEFAULT_HKDF_SALT, - &mut ciphertext, - ); - EncryptedFogHint::from(&ciphertext) + let mut plaintext = GenericArray::< + u8, + Diff::FooterSize>, + >::default(); + plaintext.as_mut()[..RISTRETTO_PUBLIC_LEN].copy_from_slice(&self.view_pubkey.to_bytes()); + for byte in &mut plaintext.as_mut()[RISTRETTO_PUBLIC_LEN..] { + *byte = MAGIC_NUMBER; + } + let bytes = VersionedCryptoBox::default() + .encrypt_fixed_length(rng, ingest_server_pubkey, &plaintext) + .expect("cryptobox encryption failed unexpectedly"); + EncryptedFogHint::from(bytes) } /// decrypt /// /// Try to decrypt an encrypted payload onto this FogHint object. - /// Fails if ECIES curve point is malformed, or the magic number is wrong. - /// - /// Note(chris): This is not constant time, but neither is ecies::decrypt right - /// now, because it short-circuits if ECIES curve point is malformed. - /// We need to re-evaluate later if that's a problem and refactor if so + /// Fails if decryption fails, or the magic number is wrong. /// /// # Arguments /// * acct_server_private_key /// * 128 byte encrypted payload /// /// # Returns - /// * Fog hint on success, () on error - #[inline] + /// * Fog hint on success, cryptobox error otherwise pub fn decrypt( ingest_server_private_key: &RistrettoPrivate, ciphertext: &EncryptedFogHint, - ) -> Result { - let mut temp = [0u8; FOG_HINT_DECRYPTED_LEN]; - ecies::decrypt_into( - ingest_server_private_key, - ciphertext.as_ref(), - &ecies::DEFAULT_HKDF_SALT, - &mut temp, - )?; + ) -> Result { + let plaintext = VersionedCryptoBox::default() + .decrypt_fixed_length(ingest_server_private_key, ciphertext.as_ref())?; // Check magic numbers - for byte in &temp[RISTRETTO_PUBLIC_LEN..FOG_HINT_DECRYPTED_LEN] { + for byte in &plaintext[RISTRETTO_PUBLIC_LEN..] { if *byte != MAGIC_NUMBER { - return Err(()); + return Err(CryptoBoxError::WrongMagicBytes); } } - FogHint::from_slice(&temp[0..RISTRETTO_PUBLIC_LEN]) + FogHint::from_slice(&plaintext.as_ref()[0..RISTRETTO_PUBLIC_LEN]) } } @@ -177,7 +152,7 @@ mod testing { let not_z = RistrettoPrivate::from_random(&mut rng); let result = FogHint::decrypt(¬_z, &ciphertext); - assert_eq!(Err(()), result); + assert_eq!(Err(CryptoBoxError::MacFailed), result); }); } } diff --git a/transaction/core/src/lib.rs b/transaction/core/src/lib.rs index 586e71b568..1dc2665509 100644 --- a/transaction/core/src/lib.rs +++ b/transaction/core/src/lib.rs @@ -20,8 +20,9 @@ pub mod account_keys; pub mod amount; pub mod blake2b_256; mod block; +mod commitment; +mod compressed_commitment; pub mod constants; -pub mod encoders; pub mod encrypted_fog_hint; pub mod fog_hint; pub mod membership_proofs; @@ -38,6 +39,8 @@ pub mod view_key; pub mod proptest_fixtures; pub use block::*; +pub use commitment::*; +pub use compressed_commitment::*; pub use redacted_tx::RedactedTx; /// Get the shared secret for a transaction output. diff --git a/transaction/core/src/ring_signature/curve_scalar.rs b/transaction/core/src/ring_signature/curve_scalar.rs index bd91cf9843..97630dc96d 100644 --- a/transaction/core/src/ring_signature/curve_scalar.rs +++ b/transaction/core/src/ring_signature/curve_scalar.rs @@ -17,75 +17,88 @@ use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; #[derive(Copy, Clone, Default, Eq, Serialize, Deserialize, Digestible)] -pub struct CurveScalar(pub(crate) Scalar); +pub struct CurveScalar { + pub scalar: Scalar, +} impl CurveScalar { /// Construct a `CurveScalar` by reducing a 256-bit little-endian integer /// modulo the group order \\( \ell \\). pub fn from_bytes_mod_order(bytes: [u8; 32]) -> Self { - Self(Scalar::from_bytes_mod_order(bytes)) + Self { + scalar: Scalar::from_bytes_mod_order(bytes), + } } /// The little-endian byte encoding of the integer representing this Scalar. pub fn as_bytes(&self) -> &[u8; 32] { - self.0.as_bytes() + self.scalar.as_bytes() } } impl keys::FromRandom for CurveScalar { fn from_random(csprng: &mut (impl CryptoRng + RngCore)) -> Self { - Self(Scalar::random(csprng)) + Self { + scalar: Scalar::random(csprng), + } } } -impl AsRef<[u8; 32]> for CurveScalar { +impl From for CurveScalar { #[inline] - fn as_ref(&self) -> &[u8; 32] { - self.0.as_bytes() + fn from(scalar: Scalar) -> Self { + Self { scalar } } } -impl AsRef<[u8]> for CurveScalar { +impl From for CurveScalar { #[inline] - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() + fn from(val: u64) -> Self { + Self { + scalar: Scalar::from(val), + } } } -impl AsRef for CurveScalar { +impl AsRef<[u8; 32]> for CurveScalar { #[inline] - fn as_ref(&self) -> &Scalar { - &self.0 + fn as_ref(&self) -> &[u8; 32] { + self.scalar.as_bytes() } } -impl From for CurveScalar { +// Implements Ord, PartialOrd, PartialEq, Hash. Requires AsRef<[u8;32]>. +deduce_core_traits_from_public_bytes! { CurveScalar } + +impl AsRef<[u8]> for CurveScalar { #[inline] - fn from(scalar: Scalar) -> Self { - Self(scalar) + fn as_ref(&self) -> &[u8] { + self.scalar.as_bytes() } } -impl From for CurveScalar { +impl AsRef for CurveScalar { #[inline] - fn from(val: u64) -> Self { - Self(Scalar::from(val)) + fn as_ref(&self) -> &Scalar { + &self.scalar } } impl Into for CurveScalar { fn into(self) -> Scalar { - self.0 + self.scalar } } impl ReprBytes32 for CurveScalar { type Error = Error; fn to_bytes(&self) -> [u8; 32] { - self.0.to_bytes() + self.scalar.to_bytes() } fn from_bytes(src: &[u8; 32]) -> Result { - Ok(Self(Scalar::from_bytes_mod_order(*src))) + Ok(Self { + scalar: Scalar::from_bytes_mod_order(*src), + }) } } @@ -95,9 +108,11 @@ impl fmt::Debug for CurveScalar { } } +// Implements prost::Message. Requires Debug and ReprBytes32. prost_message_helper32! { CurveScalar } + +// Implements try_from<&[u8;32]> and try_from<&[u8]>. Requires ReprBytes32. try_from_helper32! { CurveScalar } -deduce_core_traits_from_public_bytes! { CurveScalar } #[cfg(test)] mod tests { @@ -107,7 +122,7 @@ mod tests { fn test_to_from_bytes() { let one = Scalar::one(); let curve_scalar = CurveScalar::from_bytes_mod_order(*one.as_bytes()); - assert_eq!(curve_scalar.0, one); + assert_eq!(curve_scalar.scalar, one); assert_eq!(curve_scalar.as_bytes(), one.as_bytes()); } @@ -123,7 +138,7 @@ mod tests { let curve_scalar = CurveScalar::from_bytes_mod_order(l_plus_two_bytes); let two: Scalar = Scalar::one() + Scalar::one(); - assert_eq!(curve_scalar.0, two); + assert_eq!(curve_scalar.scalar, two); } #[test] diff --git a/transaction/core/src/ring_signature/mlsag.rs b/transaction/core/src/ring_signature/mlsag.rs index a69119cdef..092b4e7128 100644 --- a/transaction/core/src/ring_signature/mlsag.rs +++ b/transaction/core/src/ring_signature/mlsag.rs @@ -3,34 +3,48 @@ extern crate alloc; use alloc::{vec, vec::Vec}; -use core::convert::TryInto; +use core::convert::{TryFrom, TryInto}; use blake2::{Blake2b, Digest}; -use curve25519_dalek::ristretto::RistrettoPoint; +use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint}; +use digestible::Digestible; +use keys::{CompressedRistrettoPublic, RistrettoPrivate, RistrettoPublic}; +use mcserial::{ + prost::{ + bytes::{Buf, BufMut}, + encoding::{encoded_len_varint, key_len, skip_field, DecodeContext, WireType}, + Message, + }, + DecodeError, ReprBytes32, +}; use rand_core::{CryptoRng, RngCore}; - -use keys::RistrettoPrivate; -use mcserial::ReprBytes32; +use serde::{Deserialize, Serialize}; use crate::{ + commitment::Commitment, + compressed_commitment::CompressedCommitment, onetime_keys::compute_key_image, - ring_signature::{Address, Blinding, Commitment, Error, KeyImage, Scalar, GENERATORS}, + ring_signature::{Blinding, CurveScalar, Error, KeyImage, Scalar, GENERATORS}, }; -fn hash_to_point(address: &Address) -> RistrettoPoint { - RistrettoPoint::hash_from_bytes::(&address.to_bytes()) +fn hash_to_point(ristretto_public: &RistrettoPublic) -> RistrettoPoint { + RistrettoPoint::hash_from_bytes::(&ristretto_public.to_bytes()) } /// MLSAG for a ring of public keys and amount commitments. -#[derive(Clone, Debug)] +/// Note: Serialize and Deserialize appear to be cruft left over from sdk_json_interface. +#[derive(Clone, Digestible, PartialEq, Eq, Serialize, Deserialize, Message)] pub struct RingMLSAG { /// The initial challenge `c[0]`. - pub c_zero: Scalar, + #[prost(message, required, tag = "1")] + pub c_zero: CurveScalar, /// Responses `r_{0,0}, r_{0,1}, ... , r_{ring_size-1,0}, r_{ring_size-1,1}`. - pub responses: Vec, + #[prost(message, repeated, tag = "2")] + pub responses: Vec, /// Key image "spent" by this signature. + #[prost(message, required, tag = "3")] pub key_image: KeyImage, } @@ -51,7 +65,7 @@ impl RingMLSAG { // * `rng` - Randomness. pub fn sign( message: &[u8; 32], - ring: &[(Address, Commitment)], + ring: &[(CompressedRistrettoPublic, CompressedCommitment)], real_index: usize, onetime_private_key: &RistrettoPrivate, value: u64, @@ -89,7 +103,7 @@ impl RingMLSAG { // * `rng` - Randomness. fn sign_with_balance_check( message: &[u8; 32], - ring: &[(Address, Commitment)], + ring: &[(CompressedRistrettoPublic, CompressedCommitment)], real_index: usize, onetime_private_key: &RistrettoPrivate, value: u64, @@ -108,11 +122,16 @@ impl RingMLSAG { let H = GENERATORS.B_blinding; let key_image = compute_key_image(onetime_private_key); + // The uncompressed key_image. let I: RistrettoPoint = key_image.try_into().expect("key_image should decompress"); - let output_commitment = - Commitment::from(GENERATORS.commit(Scalar::from(value), *output_blinding)); + // Uncompressed output commitment. + // This ensures that each address and commitment encodes a valid Ristretto point. + let output_commitment: Commitment = Commitment::new(value, *output_blinding); + + // Ring must decompress. + let decompressed_ring = decompress_ring(ring)?; // Challenges `c_0, ... c_{ring_size - 1}`. let mut c: Vec = vec![Scalar::zero(); ring_size]; @@ -133,7 +152,7 @@ impl RingMLSAG { for n in 0..ring_size { // Iterate around the ring, starting at real_index. let i = (real_index + n) % ring_size; - let (P_i, input_commitment) = &ring[i]; + let (P_i, input_commitment) = &decompressed_ring[i]; let (L0, R0, L1) = if i == real_index { // c_{i+1} = Hn( m | alpha_0 * G | alpha_0 * Hp(P_i) | alpha_1 * H ) @@ -159,7 +178,8 @@ impl RingMLSAG { let L0 = r[2 * i] * G + c[i] * P_i.as_ref(); let R0 = r[2 * i] * hash_to_point(&P_i) + c[i] * I; - let L1 = r[2 * i + 1] * H + c[i] * (output_commitment.0 - input_commitment.0); + let L1 = + r[2 * i + 1] * H + c[i] * (output_commitment.point - input_commitment.point); (L0, R0, L1) }; @@ -182,16 +202,18 @@ impl RingMLSAG { r[2 * real_index + 1] = alpha_1 - c[real_index] * z; if check_value_is_preserved { - let input_commitment = ring[real_index].1; - let difference: RistrettoPoint = output_commitment.0 - input_commitment.0; + let (_, input_commitment) = decompressed_ring[real_index]; + let difference: RistrettoPoint = output_commitment.point - input_commitment.point; if difference != (z * H) { return Err(Error::ValueNotConserved); } } + let responses: Vec = r.into_iter().map(CurveScalar::from).collect(); + Ok(RingMLSAG { - c_zero: c[0], - responses: r, + c_zero: CurveScalar::from(c[0]), + responses, key_image, }) } @@ -205,8 +227,8 @@ impl RingMLSAG { pub fn verify( &self, message: &[u8; 32], - ring: &[(Address, Commitment)], - output_commitment: &Commitment, + ring: &[(CompressedRistrettoPublic, CompressedCommitment)], + output_commitment: &CompressedCommitment, ) -> Result<(), Error> { let ring_size = ring.len(); // `responses` must contain `2 * ring_size` elements. @@ -217,20 +239,33 @@ impl RingMLSAG { let G = GENERATORS.B; let H = GENERATORS.B_blinding; - // The uncompressed key image. + // The key image must decompress. + // This ensures that the key image encodes a valid Ristretto point. let I: RistrettoPoint = self .key_image .try_into() .map_err(|_e| Error::InvalidKeyImage)?; - let r = &self.responses; + + let r: Vec = self + .responses + .iter() + .map(|response| response.scalar) + .collect(); + + // Output commitment must decompress. + let output_commitment: Commitment = Commitment::try_from(output_commitment)?; + + // Ring must decompress. + // This ensures that each address and commitment encodes a valid Ristretto point. + let decompressed_ring = decompress_ring(ring)?; // Recompute challenges. let mut recomputed_c = vec![Scalar::zero(); ring.len()]; - for (i, (P_i, input_commitment)) in ring.iter().enumerate() { + for (i, (P_i, input_commitment)) in decompressed_ring.iter().enumerate() { let c_i = if i == 0 { // Initialize loop using the signature's c_0 term. - self.c_zero + self.c_zero.scalar } else { recomputed_c[i] }; @@ -244,8 +279,8 @@ impl RingMLSAG { // * Z_i is the i^th "commitment to zero" = output_commitment - i^th input_commitment. let L0 = r[2 * i] * G + c_i * P_i.as_ref(); - let R0 = r[2 * i] * hash_to_point(&P_i) + c_i * I; - let L1 = r[2 * i + 1] * H + c_i * (output_commitment.0 - input_commitment.0); + let R0 = r[2 * i] * hash_to_point(P_i) + c_i * I; + let L1 = r[2 * i + 1] * H + c_i * (output_commitment.point - input_commitment.point); recomputed_c[(i + 1) % ring_size] = { let mut hasher = Blake2b::new(); @@ -257,7 +292,7 @@ impl RingMLSAG { }; } - if self.c_zero == recomputed_c[0] { + if self.c_zero.scalar == recomputed_c[0] { Ok(()) } else { Err(Error::InvalidSignature) @@ -265,30 +300,42 @@ impl RingMLSAG { } } +fn decompress_ring( + ring: &[(CompressedRistrettoPublic, CompressedCommitment)], +) -> Result, Error> { + // Ring must decompress. + let mut decompressed_ring: Vec<(RistrettoPublic, Commitment)> = Vec::new(); + for (compressed_address, compressed_commitment) in ring { + let ristretto_public = RistrettoPublic::try_from(compressed_address)?; + let commitment = Commitment::try_from(compressed_commitment)?; + decompressed_ring.push((ristretto_public, commitment)); + } + Ok(decompressed_ring) +} + #[cfg(test)] mod mlsag_tests { - use keys::{FromRandom, RistrettoPrivate, RistrettoPublic}; + use alloc::vec::Vec; - use crate::{ - proptest_fixtures::*, - ring_signature::{Address, Commitment, Scalar, GENERATORS}, - }; + use keys::{CompressedRistrettoPublic, FromRandom, RistrettoPrivate, RistrettoPublic}; + use rand_core::RngCore; + + use proptest::{array::uniform32, prelude::*}; + use rand::{rngs::StdRng, CryptoRng, SeedableRng}; use crate::{ onetime_keys::compute_key_image, - ring_signature::{mlsag::RingMLSAG, Error}, + proptest_fixtures::*, + ring_signature::{mlsag::RingMLSAG, CurveScalar, Error, Scalar, GENERATORS}, + CompressedCommitment, }; - use alloc::vec::Vec; - use proptest::{array::uniform32, prelude::*}; - use rand::{rngs::StdRng, CryptoRng, SeedableRng}; - use rand_core::RngCore; extern crate std; #[derive(Debug)] struct RingMLSAGParameters { message: [u8; 32], - ring: Vec<(Address, Commitment)>, + ring: Vec<(CompressedRistrettoPublic, CompressedCommitment)>, real_index: usize, onetime_private_key: RistrettoPrivate, value: u64, @@ -305,24 +352,25 @@ mod mlsag_tests { let mut message = [0u8; 32]; rng.fill_bytes(&mut message); - let mut ring: Vec<(Address, Commitment)> = Vec::new(); + let mut ring: Vec<(CompressedRistrettoPublic, CompressedCommitment)> = Vec::new(); for _i in 0..num_mixins { - let address = RistrettoPublic::from_random(rng); + let address = CompressedRistrettoPublic::from(RistrettoPublic::from_random(rng)); let commitment = { - let value = Scalar::from(rng.next_u64()); + let value = rng.next_u64(); let blinding = Scalar::random(rng); - Commitment::from(GENERATORS.commit(value, blinding)) + CompressedCommitment::new(value, blinding) }; ring.push((address, commitment)); } // The real input. let onetime_private_key = RistrettoPrivate::from_random(rng); - let onetime_public_key = RistrettoPublic::from(&onetime_private_key); + let onetime_public_key = + CompressedRistrettoPublic::from(RistrettoPublic::from(&onetime_private_key)); let value = rng.next_u64(); let blinding = Scalar::random(rng); - let commitment = Commitment::from(GENERATORS.commit(Scalar::from(value), blinding)); + let commitment = CompressedCommitment::new(value, blinding); let real_index = rng.next_u64() as usize % (num_mixins + 1); ring.insert(real_index, (onetime_public_key, commitment)); @@ -341,7 +389,7 @@ mod mlsag_tests { } proptest! { - #![proptest_config(ProptestConfig::with_cases(10))] + #![proptest_config(ProptestConfig::with_cases(3))] #[test] // `sign` should return a signature with 2*ring_size responses. @@ -372,7 +420,7 @@ mod mlsag_tests { // All responses should be non-zero. for r in &signature.responses { - assert_ne!(*r, Scalar::zero()); + assert_ne!(r.scalar, Scalar::zero()); } } @@ -481,11 +529,7 @@ mod mlsag_tests { ) .unwrap(); - let output_commitment = { - let value = Scalar::from(params.value); - let blinding = params.pseudo_output_blinding; - Commitment::from(GENERATORS.commit(value, blinding)) - }; + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); assert!(signature .verify(¶ms.message, ¶ms.ring, &output_commitment) @@ -515,11 +559,7 @@ mod mlsag_tests { ) .unwrap(); - let output_commitment = { - let value = Scalar::from(params.value); - let blinding = params.pseudo_output_blinding; - Commitment::from(GENERATORS.commit(value, blinding)) - }; + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); match signature.verify(¶ms.message, ¶ms.ring, &output_commitment) { Err(Error::InvalidSignature) => {} // This is expected. @@ -554,10 +594,7 @@ mod mlsag_tests { ) .unwrap(); - let output_commitment = { - let blinding = params.pseudo_output_blinding; - Commitment::from(GENERATORS.commit(Scalar::from(wrong_value), blinding)) - }; + let output_commitment = CompressedCommitment::new(wrong_value, params.pseudo_output_blinding); let result = invalid_signature.verify(¶ms.message, ¶ms.ring, &output_commitment); @@ -586,10 +623,7 @@ mod mlsag_tests { ) .unwrap(); - let output_commitment = { - let value = Scalar::from(params.value); - Commitment::from(GENERATORS.commit(value, wrong_blinding)) - }; + let output_commitment = CompressedCommitment::new(params.value, wrong_blinding); let result = invalid_signature.verify(¶ms.message, ¶ms.ring, &output_commitment); @@ -627,11 +661,7 @@ mod mlsag_tests { let wrong_key_image = compute_key_image(&RistrettoPrivate::from_random(&mut rng)); signature.key_image = wrong_key_image; - let output_commitment = { - let value = Scalar::from(params.value); - let blinding = params.pseudo_output_blinding; - Commitment::from(GENERATORS.commit(value, blinding)) - }; + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); match signature.verify(¶ms.message, ¶ms.ring, &output_commitment) { Err(Error::InvalidSignature) => {} // This is expected. @@ -665,11 +695,7 @@ mod mlsag_tests { let mut wrong_message = [0u8; 32]; rng.fill_bytes(&mut wrong_message); - let output_commitment = { - let value = Scalar::from(params.value); - let blinding = params.pseudo_output_blinding; - Commitment::from(GENERATORS.commit(value, blinding)) - }; + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); let result = signature.verify(&wrong_message, ¶ms.ring, &output_commitment); @@ -701,16 +727,12 @@ mod mlsag_tests { ) .unwrap(); - let output_commitment = { - let value = Scalar::from(params.value); - let blinding = params.pseudo_output_blinding; - Commitment::from(GENERATORS.commit(value, blinding)) - }; + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); // Modify a ring element's public key. { let index = (rng.next_u64() as usize) % num_mixins; - params.ring[index].0 = RistrettoPublic::from_random(&mut rng); + params.ring[index].0 = CompressedRistrettoPublic::from(RistrettoPublic::from_random(&mut rng)); let result = signature.verify(¶ms.message, ¶ms.ring, &output_commitment); @@ -722,10 +744,10 @@ mod mlsag_tests { // Modify a ring element's amount commitment. { - let value = Scalar::from(rng.next_u64()); - let blinding = Scalar::random(&mut rng); let index = (rng.next_u64() as usize) % num_mixins; - params.ring[index].1 = Commitment::from(GENERATORS.commit(value, blinding)); + let value = rng.next_u64(); + let blinding = Scalar::random(&mut rng); + params.ring[index].1 = CompressedCommitment::new(value, blinding); let result = signature.verify(¶ms.message, ¶ms.ring, &output_commitment); @@ -760,11 +782,7 @@ mod mlsag_tests { // The output_commitment should match the value and pseudo_output_blinding used by the signature. // Here, the output_commitment uses a different value. - let wrong_output_commitment = { - let value = Scalar::random(&mut rng); - let blinding = params.pseudo_output_blinding; - Commitment::from(GENERATORS.commit(value, blinding)) - }; + let wrong_output_commitment = CompressedCommitment::new(rng.next_u64(), params.pseudo_output_blinding); let result = signature.verify(¶ms.message, ¶ms.ring, &wrong_output_commitment); @@ -796,11 +814,7 @@ mod mlsag_tests { ) .unwrap(); - let output_commitment = { - let value = Scalar::from(params.value); - let blinding = params.pseudo_output_blinding; - Commitment::from(GENERATORS.commit(value, blinding)) - }; + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); // Modify the signature to have too few responses. { @@ -819,7 +833,7 @@ mod mlsag_tests { // Modify the signature to have too many responses. { let mut invalid_signature = signature.clone(); - invalid_signature.responses.push(Scalar::random(&mut rng)); + invalid_signature.responses.push(CurveScalar::from_random(&mut rng)); let result = invalid_signature.verify(¶ms.message, ¶ms.ring, &output_commitment); @@ -831,5 +845,38 @@ mod mlsag_tests { } } + #[test] + // decode(encode(&signature)) should be the identity function. + fn test_encode_decode( + num_mixins in 1..17usize, + seed in any::<[u8; 32]>(), + ) { + let mut rng: StdRng = SeedableRng::from_seed(seed); + let pseudo_output_blinding = Scalar::random(&mut rng); + let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); + + let signature = RingMLSAG::sign( + ¶ms.message, + ¶ms.ring, + params.real_index, + ¶ms.onetime_private_key, + params.value, + ¶ms.blinding, + ¶ms.pseudo_output_blinding, + &mut rng, + ) + .unwrap(); + + use mcserial::prost::Message; + + // The encoded bytes should have the correct length. + let bytes = mcserial::encode(&signature); + assert_eq!(bytes.len(), signature.encoded_len()); + + // decode(encode(&signature)) should be the identity function. + let recovered_signature = mcserial::decode(&bytes).unwrap(); + assert_eq!(signature, recovered_signature); + } + } // end proptest! } diff --git a/transaction/core/src/ring_signature/mod.rs b/transaction/core/src/ring_signature/mod.rs index d996fc6970..6830676a5b 100644 --- a/transaction/core/src/ring_signature/mod.rs +++ b/transaction/core/src/ring_signature/mod.rs @@ -4,11 +4,23 @@ #![macro_use] extern crate alloc; -use crate::constants::{MAX_INPUTS, MAX_OUTPUTS}; +use alloc::{vec, vec::Vec}; use bulletproofs::{BulletproofGens, PedersenGens}; +use core::convert::TryFrom; use curve25519_dalek::ristretto::CompressedRistretto; pub use curve25519_dalek::scalar::Scalar; +pub use curve_point::*; +pub use curve_scalar::*; +pub use error::Error; +pub use key_image::*; use keys::RistrettoPublic; +pub use mlsag::*; +pub use rct_bulletproofs::*; + +use crate::{ + constants::{MAX_INPUTS, MAX_OUTPUTS}, + tx::TxIn, +}; mod curve_point; mod curve_scalar; @@ -16,17 +28,6 @@ mod error; mod key_image; mod mlsag; mod rct_bulletproofs; -mod rct_type_full; - -use crate::tx::TxIn; -use alloc::{vec, vec::Vec}; -use core::convert::TryFrom; -pub use curve_point::*; -pub use curve_scalar::*; -pub use error::Error; -pub use key_image::*; -pub use rct_bulletproofs::*; -pub use rct_type_full::*; lazy_static! { /// Generators (base points) for Pedersen commitments. @@ -37,33 +38,8 @@ lazy_static! { BulletproofGens::new(64, MAX_INPUTS as usize + MAX_OUTPUTS as usize); } -/// A Pedersen commitment. -pub type Commitment = CurvePoint; - // The "blinding factor" in a Pedersen commitment. pub type Blinding = CurveScalar; /// An output's one-time public address. pub type Address = RistrettoPublic; - -/// Collects one-time public keys and commitments into a matrix where each column is a ring. -pub fn get_input_rows(inputs: &[TxIn]) -> Result>, keys::KeyError> { - let m = inputs.len(); // number of inputs, e.g. 2 - let n = inputs[0].ring.len(); // ring size, e.g. 11 - - // Each ring is a column. input_rows[i] is the i^th row. - let mut input_rows: Vec> = - vec![vec![(Address::default(), Commitment::default()); m]; n]; - - // Populate input_rows. Each column is a ring. - // This assumes that the rings have already been checked to have equal length. - for (column_index, tx_in) in inputs.iter().enumerate() { - for (row_index, ring_element) in tx_in.ring.iter().enumerate() { - let address = RistrettoPublic::try_from(&ring_element.target_key)?; - let commitment: Commitment = ring_element.amount.commitment; - input_rows[row_index][column_index] = (address, commitment); - } - } - - Ok(input_rows) -} diff --git a/transaction/core/src/ring_signature/rct_bulletproofs.rs b/transaction/core/src/ring_signature/rct_bulletproofs.rs index 0bdc5f773c..cec2eff68d 100644 --- a/transaction/core/src/ring_signature/rct_bulletproofs.rs +++ b/transaction/core/src/ring_signature/rct_bulletproofs.rs @@ -9,35 +9,50 @@ extern crate alloc; use alloc::{vec, vec::Vec}; -use core::convert::TryInto; - use blake2::{Blake2b, Digest}; use bulletproofs::RangeProof; use common::HashSet; +use core::convert::{TryFrom, TryInto}; use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint}; -use keys::RistrettoPrivate; -use mcserial::ReprBytes32; +use digestible::Digestible; +use generic_array::GenericArray; +use keys::{CompressedRistrettoPublic, RistrettoPrivate, RistrettoPublic}; +use mcserial::{ + prost::{ + bytes::{Buf, BufMut}, + encoding::{bytes, encode_key, encoded_len_varint, key_len, skip_field}, + Message, + }, + DecodeError, ReprBytes32, +}; +use prost::encoding::{DecodeContext, WireType}; use rand_core::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; use crate::{ + commitment::Commitment, + compressed_commitment::CompressedCommitment, onetime_keys::compute_key_image, range_proofs::{check_range_proofs, generate_range_proofs}, - ring_signature::{ - mlsag::RingMLSAG, Address, Blinding, Commitment, Error, KeyImage, Scalar, GENERATORS, - }, + ring_signature::{mlsag::RingMLSAG, Blinding, Error, KeyImage, Scalar, GENERATORS}, }; /// An RCT_TYPE_BULLETPROOFS_2 signature. -#[allow(dead_code)] +#[derive(Clone, Digestible, Eq, PartialEq, Serialize, Deserialize, Message)] pub struct SignatureRctBulletproofs { /// Signature for each input ring. + #[prost(message, repeated, tag = "1")] pub ring_signatures: Vec, /// Commitments of value equal to each real input. - pub pseudo_output_commitments: Vec, + #[prost(message, repeated, tag = "2")] + pub pseudo_output_commitments: Vec, /// Proof that all pseudo_outputs and transaction outputs are in [0, 2^64). - pub range_proof: RangeProof, + /// This contains range_proof.to_bytes(). It is stored this way so that this struct may derive + /// Default, which is a requirement for serializing with Prost. + #[prost(bytes, tag = "3")] + pub range_proof_bytes: Vec, } impl SignatureRctBulletproofs { @@ -49,10 +64,9 @@ impl SignatureRctBulletproofs { /// * `real_input_indices` - The index of the real input in each ring. /// * `input_secrets` - One-time private key, amount value, and amount blinding for each real input. /// * `output_values_and_blindings` - Value and blinding for each output amount commitment. - #[allow(unused)] pub fn sign( message: &[u8; 32], - rings: &[Vec<(Address, Commitment)>], + rings: &[Vec<(CompressedRistrettoPublic, CompressedCommitment)>], real_input_indices: &[usize], input_secrets: &[(RistrettoPrivate, u64, Scalar)], output_values_and_blindings: &[(u64, Scalar)], @@ -76,12 +90,11 @@ impl SignatureRctBulletproofs { /// * `rings` - One or more rings of one-time addresses and amount commitments. /// * `output_commitments` - Output amount commitments. /// * `rng` - - #[allow(unused)] pub fn verify( &self, message: &[u8; 32], - rings: &[Vec<(Address, Commitment)>], - output_commitments: &[Commitment], + rings: &[Vec<(CompressedRistrettoPublic, CompressedCommitment)>], + output_commitments: &[CompressedCommitment], rng: &mut CSPRNG, ) -> Result<(), Error> { // Signature must contain one ring signature for each ring. @@ -111,53 +124,57 @@ impl SignatureRctBulletproofs { } } - // pseudo_output_commitments must decompress to RistrettoPoints. - let decompression_result: Result, Error> = self - .pseudo_output_commitments - .iter() - .map(|compressed_ristretto| { - compressed_ristretto - .decompress() - .ok_or(Error::InvalidSignature) - }) - .collect(); + // output_commitments must decompress. + // This ensures that each commitment encodes a valid Ristretto point. + let mut decompressed_output_commitments: Vec = Vec::new(); + for output_commitment in output_commitments { + let commitment = Commitment::try_from(output_commitment)?; + decompressed_output_commitments.push(commitment); + } - let decompressed_pseudo_output_commitments: Vec = decompression_result?; + // pseudo_output_commitments must decompress. + // This ensures that each commitment encodes a valid Ristretto point. + let mut decompressed_pseudo_output_commitments: Vec = Vec::new(); + for pseudo_output in &self.pseudo_output_commitments { + let commitment = Commitment::try_from(pseudo_output)?; + decompressed_pseudo_output_commitments.push(commitment); + } - // Range proof must be valid + // pseudo_output_commitments and output commitments must be in [0, 2^64). { - let compressed_output_commitments: Vec = output_commitments - .iter() - .map(|commitment| commitment.as_ref().compress()) - .collect(); - let commitments: Vec = self .pseudo_output_commitments .iter() - .chain(compressed_output_commitments.iter()) - .cloned() + .chain(output_commitments.iter()) + .map(|compressed_commitment| compressed_commitment.point) .collect(); - check_range_proofs(&self.range_proof, &commitments, rng) + let range_proof = RangeProof::from_bytes(&self.range_proof_bytes) + .map_err(|_e| Error::RangeProofError)?; + + check_range_proofs(&range_proof, &commitments, rng) .map_err(|_e| Error::InvalidSignature)?; } // Each MLSAG must be valid. for (i, ring) in rings.iter().enumerate() { - let pseudo_output = decompressed_pseudo_output_commitments[i]; let ring_signature = &self.ring_signatures[i]; - ring_signature.verify(message, ring, &Commitment::from(pseudo_output))?; + let pseudo_output = self.pseudo_output_commitments[i]; + ring_signature.verify(message, ring, &pseudo_output)?; } // Output commitments - pseudo_outputs must be zero. { - let sum_of_output_commitments: RistrettoPoint = output_commitments + let sum_of_output_commitments: RistrettoPoint = decompressed_output_commitments .iter() - .map(|commitment| commitment.0) + .map(|commitment| commitment.point) .sum(); let sum_of_pseudo_output_commitments: RistrettoPoint = - decompressed_pseudo_output_commitments.iter().sum(); + decompressed_pseudo_output_commitments + .iter() + .map(|commitment| commitment.point) + .sum(); let difference = sum_of_output_commitments - sum_of_pseudo_output_commitments; if difference != GENERATORS.commit(Scalar::zero(), Scalar::zero()) { @@ -170,7 +187,6 @@ impl SignatureRctBulletproofs { } /// Key images spent by this signature. - #[allow(unused)] pub fn key_images(&self) -> Vec { self.ring_signatures .iter() @@ -190,7 +206,7 @@ impl SignatureRctBulletproofs { /// * `check_value_is_preserved` - If true, check that the value of inputs equals value of outputs. fn sign_with_balance_check( message: &[u8; 32], - rings: &[Vec<(Address, Commitment)>], + rings: &[Vec<(CompressedRistrettoPublic, CompressedCommitment)>], real_input_indices: &[usize], input_secrets: &[(RistrettoPrivate, u64, Scalar)], output_values_and_blindings: &[(u64, Scalar)], @@ -295,13 +311,16 @@ fn sign_with_balance_check( } } - let pseudo_output_commitments: Vec = - commitments.into_iter().take(num_inputs).collect(); + let pseudo_output_commitments: Vec = commitments + .iter() + .take(num_inputs) + .map(CompressedCommitment::from) + .collect(); Ok(SignatureRctBulletproofs { ring_signatures, pseudo_output_commitments, - range_proof, + range_proof_bytes: range_proof.to_bytes(), }) } @@ -309,8 +328,8 @@ fn sign_with_balance_check( mod rct_bulletproofs_tests { use alloc::vec::Vec; - use curve25519_dalek::scalar::Scalar; - use keys::{FromRandom, RistrettoPrivate, RistrettoPublic}; + use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar}; + use keys::{CompressedRistrettoPublic, FromRandom, RistrettoPrivate, RistrettoPublic}; use rand_core::RngCore; use proptest::{array::uniform32, prelude::*}; @@ -319,13 +338,11 @@ mod rct_bulletproofs_tests { use crate::{ proptest_fixtures::*, range_proofs::generate_range_proofs, - ring_signature::{ - Address, Blinding, Commitment, Error, KeyImage, SignatureRctBulletproofs, GENERATORS, - }, + ring_signature::{Blinding, Error, KeyImage, SignatureRctBulletproofs, GENERATORS}, }; - use curve25519_dalek::ristretto::RistrettoPoint; use super::sign_with_balance_check; + use crate::{commitment::Commitment, compressed_commitment::CompressedCommitment}; extern crate std; @@ -334,7 +351,7 @@ mod rct_bulletproofs_tests { message: [u8; 32], /// Rings of input onetime addresses and amount commitments. - rings: Vec>, + rings: Vec>, /// The index of the real input in each ring. real_input_indices: Vec, @@ -360,23 +377,26 @@ mod rct_bulletproofs_tests { let mut input_secrets = Vec::new(); for _i in 0..num_inputs { - let mut ring: Vec<(Address, Commitment)> = Vec::new(); + let mut ring: Vec<(CompressedRistrettoPublic, CompressedCommitment)> = Vec::new(); + // Create random mixins. for _i in 0..num_mixins { - let address = RistrettoPublic::from_random(rng); + let address = + CompressedRistrettoPublic::from(RistrettoPublic::from_random(rng)); let commitment = { - let value = Scalar::from(rng.next_u64()); + let value = rng.next_u64(); let blinding = Scalar::random(rng); - Commitment::from(GENERATORS.commit(value, blinding)) + CompressedCommitment::new(value, blinding) }; ring.push((address, commitment)); } // The real input. let onetime_private_key = RistrettoPrivate::from_random(rng); - let onetime_public_key = RistrettoPublic::from(&onetime_private_key); + let onetime_public_key = + CompressedRistrettoPublic::from(RistrettoPublic::from(&onetime_private_key)); let value = rng.next_u64(); let blinding = Scalar::random(rng); - let commitment = Commitment::from(GENERATORS.commit(Scalar::from(value), blinding)); + let commitment = CompressedCommitment::new(value, blinding); let real_index = rng.next_u64() as usize % (num_mixins + 1); ring.insert(real_index, (onetime_public_key, commitment)); @@ -404,11 +424,10 @@ mod rct_bulletproofs_tests { } } - fn get_output_commitments(&self) -> Vec { + fn get_output_commitments(&self) -> Vec { self.output_values_and_blindings .iter() - .map(|(value, blinding)| GENERATORS.commit(Scalar::from(*value), *blinding)) - .map(Commitment::from) + .map(|(value, blinding)| CompressedCommitment::new(*value, *blinding)) .collect() } } @@ -632,7 +651,7 @@ mod rct_bulletproofs_tests { range_proof }; - signature.range_proof = wrong_range_proof; + signature.range_proof_bytes = wrong_range_proof.to_bytes(); let result = signature.verify( ¶ms.message, @@ -680,5 +699,35 @@ mod rct_bulletproofs_tests { assert_eq!(result, Err(Error::InvalidSignature)); } + #[test] + // decode(encode(&signature)) should be the identity function. + fn test_encode_decode( + num_inputs in 4..8usize, + num_mixins in 1..17usize, + seed in any::<[u8; 32]>(), + ) { + let mut rng: StdRng = SeedableRng::from_seed(seed); + let params = SignatureParams::random(num_inputs, num_mixins, &mut rng); + let signature = SignatureRctBulletproofs::sign( + ¶ms.message, + ¶ms.rings, + ¶ms.real_input_indices, + ¶ms.input_secrets, + ¶ms.output_values_and_blindings, + &mut rng, + ) + .unwrap(); + + use mcserial::prost::Message; + + // The encoded bytes should have the correct length. + let bytes = mcserial::encode(&signature); + assert_eq!(bytes.len(), signature.encoded_len()); + + // decode(encode(&signature)) should be the identity function. + let recovered_signature : SignatureRctBulletproofs = mcserial::decode(&bytes).unwrap(); + assert_eq!(signature, recovered_signature); + } + } // end proptest } diff --git a/transaction/core/src/ring_signature/rct_type_full.rs b/transaction/core/src/ring_signature/rct_type_full.rs deleted file mode 100644 index ef2cef365e..0000000000 --- a/transaction/core/src/ring_signature/rct_type_full.rs +++ /dev/null @@ -1,1033 +0,0 @@ -// Copyright (c) 2018-2020 MobileCoin Inc. - -//! An RCTTypeFull signature with bulletproofs. -//! -//! # Variable names: -//! * `I` for key images, -//! * `c` for the challenges, -//! * `x` for public keys, -//! * `y` for the public keys (`P[i,k]` in the paper), -//! * `r` for challenge responses (`s[i,k]` in the paper), -//! -//! # References -//! * [Ring Confidential Transactions](https://eprint.iacr.org/2015/1098.pdf) -//! * [Bulletproofs](https://eprint.iacr.org/2017/1066.pdf) - -#![allow(clippy::many_single_char_names, clippy::needless_range_loop)] - -extern crate alloc; -use crate::{onetime_keys::compute_key_image_uncompressed, ring_signature::*}; -use alloc::{vec, vec::Vec}; -use blake2::{Blake2b, Digest}; -use core::{ - convert::{AsRef, From}, - default::Default, -}; -use curve25519_dalek::ristretto::RistrettoPoint; -pub use curve25519_dalek::scalar::Scalar; -use digestible::Digestible; -use keys::RistrettoPrivate; -use mcserial::ReprBytes32; -use prost::Message; -use rand_core::{CryptoRng, RngCore}; -use serde::{Deserialize, Serialize}; - -/// An RCTTypeFull signature. -#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Message, Digestible)] -pub struct SignatureRctFull { - /// Key images `I_1, ... I_m` "spent" by this signature. - #[prost(message, repeated, tag = "1")] - pub key_images: Vec, - - /// The set of `s_i^j` terms in the ring signature. - #[prost(message, repeated, tag = "2")] - pub challenge_responses: Vec, - - /// The initial `c_1` term in the ring signature. - #[prost(message, required, tag = "3")] - pub challenge: CurveScalar, -} - -#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Message, Digestible)] -pub struct ChallengeResponse { - #[prost(message, repeated, tag = "1")] - pub response: Vec, -} - -impl From> for ChallengeResponse { - #[inline] - fn from(response: Vec) -> Self { - Self { response } - } -} - -/// Generates a RingCT signature. -/// -/// # Arguments -/// `prefix_hash` - Hash of a transaction prefix. -/// `input_rows` - Input addresses and commitments; one row (`real_input_row`) belongs to signer. -/// `out_commitments_and_blindings` - Output commitments with blindings. -/// `input_keys_and_blindings` - `in_secrets` - Input onetime private keys and blindings. -/// `real_input_row` - Row in the `input_rows` matrix owned by the signer. -/// `csprng` - Randomness. -pub fn sign( - prefix_hash: &[u8; 32], - input_rows: &[Vec<(Address, Commitment)>], - output_commitments_and_blindings: &[(Commitment, Blinding)], - input_keys_and_blindings: &[(RistrettoPrivate, Blinding)], - real_input_row: usize, - csprng: &mut RNG, -) -> Result { - sign_with_value_check( - prefix_hash, - input_rows, - output_commitments_and_blindings, - input_keys_and_blindings, - real_input_row, - true, - csprng, - ) -} - -/// Generates a RingCT signature, with optional check for inputs = outputs. -/// -/// This function exists to facilitate unit testing. -/// -/// # Arguments -/// `prefix_hash` - Hash of a transaction prefix. -/// `input_rows` - Input addresses and commitments; one row (`real_input_row`) belongs to signer. -/// `out_commitments_and_blindings` - Output commitments with blindings. -/// `input_keys_and_blindings` - `in_secrets` - Input onetime private keys and blindings. -/// `real_input_row` - Row in the `input_rows` matrix owned by the signer. -/// `check_value_is_preserved` - If true, check that the value of inputs equals value of outputs. -/// `csprng` - Randomness. -fn sign_with_value_check( - prefix_hash: &[u8; 32], - input_rows: &[Vec<(Address, Commitment)>], - output_commitments_and_blindings: &[(Commitment, Blinding)], - input_keys_and_blindings: &[(RistrettoPrivate, Blinding)], - real_input_row: usize, - check_value_is_preserved: bool, - csprng: &mut RNG, -) -> Result { - let ring_size = input_rows.len(); // N rows, aka ring size. - let num_inputs = input_rows[0].len(); // M columns, aka number of inputs. - - // Elements of `input_rows` must have the same length. - for row in input_rows { - if row.len() != num_inputs { - return Err(Error::LengthMismatch(num_inputs, row.len())); - } - } - - // `input_keys_and_blindings` must contain an element for each input. - if input_keys_and_blindings.len() != num_inputs { - return Err(Error::LengthMismatch( - num_inputs, - input_keys_and_blindings.len(), - )); - } - - // `real_input_row` must be in [0,N-1]. - if real_input_row >= ring_size { - return Err(Error::IndexOutOfBounds); - } - - let G = GENERATORS.B; - let H = GENERATORS.B_blinding; - - // Challenges generated from hashes of the L and R entries for each row. - let mut c: Vec = vec![Scalar::zero(); ring_size]; - - // Schnorr proof random blinding factor. - let v: Vec = (0..=num_inputs).map(|_| Scalar::random(csprng)).collect(); - - // Start at real_input_row (j), make all L(j,k), R(j,k), then L(j.M). - let j = real_input_row; - c[(j + 1) % ring_size] = { - let real_inputs = &input_rows[j]; - - // Start every hash with the prefix hash, so the signature signs the transaction. - let mut row_hash = Blake2b::new(); - row_hash.input(prefix_hash); - - for (k, (y, _blinding)) in real_inputs.iter().enumerate() { - // Standard linkable ring signature L/R terms for the owned inputs. - let Hy = RistrettoPoint::hash_from_bytes::(&y.to_bytes()); - let L_k = v[k] * G; - let R_k = v[k] * Hy; - row_hash.input(L_k.compress().as_bytes()); - row_hash.input(R_k.compress().as_bytes()); - } - - // Regular ring signature L term for balance proof. - let L_M = v[num_inputs] * H; - row_hash.input(L_M.compress().as_bytes()); - Scalar::from_hash::(row_hash) - }; - - // Challenge responses. For the real input row it is the standard `r = v - cx` - // for the mixin rows they are all random. - // - // r[j][k] = v[k] - c[j] * x - let mut r: Vec> = Vec::with_capacity(ring_size); - for i in 0..ring_size { - r.push(Vec::with_capacity(num_inputs + 1)); - r[i].resize(num_inputs + 1, Scalar::zero()); - } - - // Sum of output commitments. - let output_sum: RistrettoPoint = output_commitments_and_blindings - .iter() - .map(|(commitment, _blinding)| commitment.as_ref()) - .sum(); - - let uncompressed_key_images: Vec = input_keys_and_blindings - .iter() - .map(|(onetime_private_key, _blinding)| compute_key_image_uncompressed(onetime_private_key)) - .collect(); - - // Compute c[i] for i not equal to j. - for n in 1..ring_size { - let i = (j + n) % ring_size; - let row: &Vec<(Address, Commitment)> = &input_rows[i]; - - let mut row_hash = Blake2b::new(); - row_hash.input(prefix_hash); - - // Iterate over all the mixins in a row, appending their L/R to the hash buffer - for (k, (y, _commitment)) in row.iter().enumerate() { - // r is random for all mixins - r[i][k] = Scalar::random(csprng); - let Hy = RistrettoPoint::hash_from_bytes::(&y.to_bytes()); - let L = r[i][k] * G + c[i] * y.as_ref(); - let R = r[i][k] * Hy + c[i] * uncompressed_key_images[k]; - - row_hash.input(L.compress().as_bytes()); - row_hash.input(R.compress().as_bytes()); - } - - // The balance proof for this mixin row. - { - r[i][num_inputs] = Scalar::random(csprng); - let row_commitment_sum: RistrettoPoint = - row.iter().map(|(_, commitment)| commitment.0).sum(); - let y = output_sum - row_commitment_sum; - let L = r[i][num_inputs] * H + c[i] * y; - - row_hash.input(L.compress().as_bytes()); - } - - c[(i + 1) % ring_size] = Scalar::from_hash::(row_hash); - } - - // The private key `z` for the "commitment to zero" `z*H`. - // If the total value of outputs equals the total value of inputs, then the value components - // of their commitments will cancel, leaving - // sum_of_output_commitments - sum_of_input_commitments - // = (sum_of_output_blindings - sum_of_input_blindings) * H - // = z*H - let z: Scalar = { - let output_blinding_sum: Scalar = output_commitments_and_blindings - .iter() - .map(|(_, blinding)| blinding.0) - .sum(); - - let input_blinding_sum: Scalar = input_keys_and_blindings - .iter() - .map(|(_, blinding)| blinding.0) - .sum(); - - output_blinding_sum - input_blinding_sum - }; - - // Check that value is preserved. - if check_value_is_preserved { - let input_commitment_sum: RistrettoPoint = input_rows[real_input_row] - .iter() - .map(|(_address, commitment)| commitment.as_ref()) - .sum(); - - let output_commitment_sum: RistrettoPoint = output_commitments_and_blindings - .iter() - .map(|(commitment, _)| commitment.as_ref()) - .sum(); - - let difference: RistrettoPoint = output_commitment_sum - input_commitment_sum; - if difference != (z * H) { - return Err(Error::ValueNotConserved); - } - } - - // Create the real `r = v - cx` using the input address and balance proof secret keys. - for k in 0..num_inputs { - let x: Scalar = *input_keys_and_blindings[k].0.as_ref(); - r[j][k] = v[k] - c[j] * x; - } - - r[j][num_inputs] = v[num_inputs] - c[j] * z; - - let challenge_responses: Vec = r - .into_iter() - .map(|row| { - let curve_scalars: Vec = row.into_iter().map(CurveScalar::from).collect(); - ChallengeResponse::from(curve_scalars) - }) - .collect(); - - let key_images: Vec = uncompressed_key_images - .into_iter() - .map(KeyImage::from) - .collect(); - - Ok(SignatureRctFull { - key_images, - challenge_responses, - challenge: CurveScalar::from(c[0]), - }) -} - -/// Verify a signature. -/// -/// # Arguments - -/// * `prefix_hash` - Hash of transaction prefix. -/// * `input_rows` - Input addresses and commitments. -/// * `output_commitments` - Output commitments. -/// * `signature` - Signature. -pub fn verify( - prefix_hash: &[u8; 32], - input_rows: &[Vec<(Address, Commitment)>], - output_commitments: &[Commitment], - signature: &SignatureRctFull, -) -> bool { - let G = GENERATORS.B; - let H = GENERATORS.B_blinding; - let ring_size = input_rows.len(); // ring size = mixins_per_ring + 1. - let num_inputs = input_rows[0].len(); - - // Return false if the input_rows are of different lengths. - for row in input_rows { - if row.len() != num_inputs { - return false; - } - } - - // challenge_responses should contain `ring_size = mixins_per_ring + 1` elements. - if signature.challenge_responses.len() != ring_size { - return false; - } - - // Each ChallengeResponse should contain `num_inputs + 1` elements. - for challenge_response in &signature.challenge_responses { - if challenge_response.response.len() != num_inputs + 1 { - return false; - } - } - - // The signature should contain one key image for each input. - if signature.key_images.len() != num_inputs { - return false; - } - - // Sum the output commitments for the balance proofs. - let output_commitment_sum: RistrettoPoint = output_commitments.iter().map(|c| c.as_ref()).sum(); - - let maybe_I: Result, ()> = signature - .key_images - .iter() - .map(|k| { - >::as_ref(k) - .decompress() - .ok_or(()) - }) - .collect(); - - // Uncompressed key images. - let I = match maybe_I { - Ok(result) => result, - Err(()) => { - return false; - } - }; - - // Protocol challenges, we will recreate them from the c[0] in the signature. - let mut c: Vec = (0..ring_size).map(|_| Scalar::zero()).collect(); - - // Iterate over the rows, constructing the next row's hash from the L/R terms. wrap mod N. - for i in 0..ring_size { - let c_i = match i { - 0 => signature.challenge.into(), - _ => c[i], - }; - - let mut row_hash = Blake2b::new(); - row_hash.input(prefix_hash); - - // Sum the input commitments for the balance proof. - let input_commitment_sum: RistrettoPoint = input_rows[i] - .iter() - .map(|(_address, commitment)| commitment.as_ref()) - .sum(); - - // Iterate over the inputs in the row, constructing the L/R terms for the hash. - for k in 0..num_inputs { - let (y, _commitment) = &input_rows[i][k]; - let Hy = RistrettoPoint::hash_from_bytes::(&y.to_bytes()); - let r = signature.challenge_responses[i].response[k].as_ref(); - let L = r * G + c_i * y.as_ref(); - let R = r * Hy + c_i * I[k]; - row_hash.input(L.compress().as_bytes()); - row_hash.input(R.compress().as_bytes()); - } - - // Check the balance proof; subtract input commitments from outputs, use as key for Schnorr proof. - { - let y = output_commitment_sum - input_commitment_sum; - let r = signature.challenge_responses[i].response[num_inputs].as_ref(); - let L = r * H + c_i * y; - row_hash.input(L.compress().as_bytes()); - } - - // Assign the next c value in the ring. - c[(i + 1) % ring_size] = Scalar::from_hash::(row_hash); - } - - // The signature is valid if c[0] was recomputed correctly. - Into::::into(signature.challenge) == c[0] -} - -#[cfg(test)] -mod test { - use super::*; - extern crate std; - - use crate::onetime_keys::compute_key_image; - use keys::{FromRandom, RistrettoPrivate, RistrettoPublic}; - use mcrand::McRng; - - // Arguments for `sign`. - struct SignatureParams { - prefix_hash: [u8; 32], - input_keys_and_blindings: Vec<(RistrettoPrivate, Blinding)>, - input_rows: Vec>, - real_input_row: usize, - output_commitments_and_blindings: Vec<(Commitment, Blinding)>, - } - - impl SignatureParams { - pub fn get_output_commitments(&self) -> Vec { - self.output_commitments_and_blindings - .iter() - .map(|(commitment, _)| commitment.clone()) - .collect() - } - } - - // Returns valid parameters for creating a signature. - fn get_signature_params( - num_inputs: usize, - mixins_per_ring: usize, - rng: &mut RNG, - ) -> SignatureParams { - let mut prefix_hash = [0u8; 32]; - rng.fill_bytes(&mut prefix_hash); - - // One-time private key for each spent input. - let input_private_keys: Vec = (0..num_inputs) - .map(|_| RistrettoPrivate::from_random(rng)) - .collect(); - - // Corresponding one-time public key for each spent input. - let input_addresses: Vec = input_private_keys - .iter() - .map(RistrettoPublic::from) - .collect(); - - // Input Pedersen commitments. All inputs have random values. - let input_values: Vec = (0..num_inputs) - .map(|_| Scalar::from(rng.next_u64())) - .collect(); - let input_blindings: Vec = (0..num_inputs).map(|_| Scalar::random(rng)).collect(); - let input_commitments: Vec = input_values - .iter() - .zip(input_blindings.iter()) - .map(|(value, blinding)| Commitment::from(GENERATORS.commit(*value, *blinding))) - .collect(); - - let input_keys_and_blindings: Vec<(RistrettoPrivate, Blinding)> = input_private_keys - .iter() - .cloned() - .zip(input_blindings.iter().cloned().map(Blinding::from)) - .collect(); - - // Output Pedersen commitments, one for each input. - let output_values: Vec = input_values.clone(); - let output_blindings: Vec = (0..output_values.len()) - .map(|_| Scalar::random(rng)) - .collect(); - let output_commitments: Vec = output_values - .iter() - .zip(output_blindings.iter()) - .map(|(value, blinding)| Commitment::from(GENERATORS.commit(*value, *blinding))) - .collect(); - - let output_commitments_and_blindings: Vec<(Commitment, Blinding)> = output_commitments - .iter() - .cloned() - .zip(output_blindings.iter().cloned().map(Blinding::from)) - .collect(); - - let mut input_rows: Vec> = - Vec::with_capacity(mixins_per_ring + 1); - - let real_input_row = mixins_per_ring / 2; - for i in 0..=mixins_per_ring { - input_rows.push(Vec::with_capacity(num_inputs)); - if i == real_input_row { - // The row of true inputs. - for k in 0..num_inputs { - input_rows[i].push((input_addresses[k], input_commitments[k])); - } - } else { - // A row of mixins. - for _k in 0..num_inputs { - let address = RistrettoPublic::from_random(rng); - let commitment = { - let value = Scalar::random(rng); - let blinding = Scalar::random(rng); - Commitment::from(GENERATORS.commit(value, blinding)) - }; - input_rows[i].push((address, commitment)); - } - } - } - - SignatureParams { - prefix_hash, - input_keys_and_blindings, - input_rows, - real_input_row, - output_commitments_and_blindings, - } - } - - #[test] - // `verify` should accept valid signatures. - fn test_verify_accepts_valid_signatures() { - let mut rng = McRng::default(); - - for num_inputs in &[1, 2, 4, 8] { - for mixins_per_ring in &[1, 5, 10] { - let signature_params = - get_signature_params(*num_inputs, *mixins_per_ring, &mut rng); - - // A valid signature. - let signature = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ) - .unwrap(); - - let output_commitments = signature_params.get_output_commitments(); - - assert!(verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &signature - )); - } - } - } - - #[test] - // The signature should contain the correct number of key images and challenge responses. - fn test_sign_produces_signature_of_correct_size() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - - let signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - let signature = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ) - .unwrap(); - - // The signature should contain one key image for each input. - assert_eq!(signature.key_images.len(), num_inputs); - - // challenge_responses should contain `ring_size = mixins_per_ring + 1` elements. - assert_eq!(signature.challenge_responses.len(), (mixins_per_ring + 1)); - - // Each ChallengeResponse should contain `num_inputs + 1` elements. - for challenge_response in &signature.challenge_responses { - assert_eq!(challenge_response.response.len(), num_inputs + 1); - } - } - - #[test] - // Signing should fail if the sum of inputs does not equal the sum of outputs. - fn test_sign_requires_value_to_be_preserved() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - - let mut signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - // Remove one of the outputs. - signature_params.output_commitments_and_blindings.pop(); - - let result = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ); - - match result { - Err(Error::ValueNotConserved) => {} // expected - _ => panic!(), - } - } - - #[test] - // The signature should contain the correct key images for the spent inputs. - fn test_signature_contains_correct_key_images() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - let signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - let signature = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ) - .unwrap(); - - let expected_key_images: Vec<_> = signature_params - .input_keys_and_blindings - .iter() - .map(|(key, _)| compute_key_image(key)) - .collect(); - - assert_eq!(signature.key_images, expected_key_images); - } - - #[test] - // `sign` should return an error if input rows are of unequal sizes. - fn test_sign_returns_error_mismatched_input_row_sizes() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - let mut signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - // Make one of the input_rows shorter than the others. - signature_params.input_rows[0].pop(); - - let result = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ); - - match result { - Err(Error::LengthMismatch(_, _)) => {} // expected - _ => panic!(), - } - } - - #[test] - // `sign` should return an error if `real_input_row` is out of bounds. - fn test_sign_returns_error_real_input_row_out_of_bounds() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - let signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - // Out of bounds - let wrong_input_row = 743; - - let result = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - wrong_input_row, - &mut rng, - ); - - match result { - Err(Error::IndexOutOfBounds) => {} // expected - _ => panic!(), - } - } - - #[test] - // `verify` should reject a signature that was signed with a wrong input private key. - fn test_verify_rejects_signature_with_wrong_input_private_key() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - let mut signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - // Modify an input private key. - signature_params.input_keys_and_blindings[3].0 = RistrettoPrivate::from_random(&mut rng); - - let signature = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ) - .unwrap(); - - let output_commitments = signature_params.get_output_commitments(); - - assert_eq!( - false, - verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &signature - ) - ); - } - - #[test] - // `verify` should reject a signature that was signed with a wrong input blinding. - fn test_verify_rejects_signature_with_wrong_input_blinding() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - let mut signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - // Modify an input blinding. - signature_params.input_keys_and_blindings[3].1 = Blinding::from_random(&mut rng); - - // Create an invalid signature (bypassing value preservation check). - let invalid_signature = sign_with_value_check( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - false, - &mut rng, - ) - .unwrap(); - - let output_commitments = signature_params.get_output_commitments(); - - assert_eq!( - false, - verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &invalid_signature - ) - ); - } - - #[test] - // `verify` should return false if the number of key images and the number of challenge responses disagree. - fn test_verify_rejects_signature_with_wrong_number_of_fields() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - let signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - let signature = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ) - .unwrap(); - - // Remove a key image. - { - let mut signature = signature.clone(); - signature.key_images.pop(); - - let output_commitments = signature_params.get_output_commitments(); - - assert_eq!( - false, - verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &signature - ) - ); - } - - // Add a key image. - { - let mut signature = signature.clone(); - signature - .key_images - .push(compute_key_image(&RistrettoPrivate::from_random(&mut rng))); - - let output_commitments = signature_params.get_output_commitments(); - - assert_eq!( - false, - verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &signature - ) - ); - } - - // Remove a challenge response. - { - let mut signature = signature.clone(); - signature.challenge_responses.pop(); - - let output_commitments = signature_params.get_output_commitments(); - - assert_eq!( - false, - verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &signature - ) - ); - } - - // Add a challenge response. - { - let mut signature = signature.clone(); - let additional_challenge_response = signature.challenge_responses[0].clone(); - - signature - .challenge_responses - .push(additional_challenge_response); - - let output_commitments = signature_params.get_output_commitments(); - - assert_eq!( - false, - verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &signature - ) - ); - } - } - - #[test] - // `verify` should fail if the signature is over the wrong message. - fn test_verify_fails_for_wrong_message() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - - let signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - let signature = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ) - .unwrap(); - - let output_commitments = signature_params.get_output_commitments(); - - let mut wrong_message = [0u8; 32]; - rng.fill_bytes(&mut wrong_message); - - assert_eq!( - false, - verify( - &wrong_message, - &signature_params.input_rows, - &output_commitments, - &signature - ) - ); - } - - #[test] - // `verify` should fail if the signature disagrees with an input Address. - fn test_verify_fails_for_wrong_input_address() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - - let mut signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - let signature = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ) - .unwrap(); - - let output_commitments = signature_params.get_output_commitments(); - - // Modify an Address in the first input_row. - signature_params.input_rows[0][0].0 = Address::from_random(&mut rng); - - assert_eq!( - false, - verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &signature - ) - ); - } - - #[test] - // `verify` should fail if the signature disagrees with an input Commitment. - fn test_verify_fails_for_wrong_input_commitment() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - - let mut signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - let signature = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ) - .unwrap(); - - let output_commitments = signature_params.get_output_commitments(); - - // Modify a Commitment in the first input_row. - signature_params.input_rows[0][0].1 = Commitment::from_random(&mut rng); - - assert_eq!( - false, - verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &signature - ) - ); - } - - #[test] - // `verify` should fail if the signature disagrees with an output Commitment. - fn test_verify_fails_for_wrong_output_commitment() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - - let signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - let signature = sign( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - &mut rng, - ) - .unwrap(); - - let mut output_commitments = signature_params.get_output_commitments(); - - // Modify an output commitment. - output_commitments[5] = Commitment::from_random(&mut rng); - - assert_eq!( - false, - verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &signature - ) - ); - } - - #[test] - // `verify` rejects a signature if the sum of inputs does not equal the sum of outputs. - fn test_verify_requires_value_to_be_preserved() { - let mut rng = McRng::default(); - let num_inputs = 7; - let mixins_per_ring = 16; - - let mut signature_params = get_signature_params(num_inputs, mixins_per_ring, &mut rng); - - // Modify an output commitment. - let new_blinding = Scalar::random(&mut rng); - let new_value = Scalar::from(1_000_000u64); - let new_commitment = Commitment::from(GENERATORS.commit(new_value, new_blinding)); - - signature_params.output_commitments_and_blindings[3] = - (new_commitment, CurveScalar::from(new_blinding)); - - // Create an invalid signature (bypassing the value preservation check). - let invalid_signature = sign_with_value_check( - &signature_params.prefix_hash, - &signature_params.input_rows, - &signature_params.output_commitments_and_blindings, - &signature_params.input_keys_and_blindings, - signature_params.real_input_row, - false, - &mut rng, - ) - .unwrap(); - - let output_commitments = signature_params.get_output_commitments(); - - assert_eq!( - false, - verify( - &signature_params.prefix_hash, - &signature_params.input_rows, - &output_commitments, - &invalid_signature - ) - ); - } -} diff --git a/transaction/core/src/tx.rs b/transaction/core/src/tx.rs index 6258a4e645..d4c607e5d0 100644 --- a/transaction/core/src/tx.rs +++ b/transaction/core/src/tx.rs @@ -1,15 +1,5 @@ // Copyright (c) 2018-2020 MobileCoin Inc. -use crate::{ - account_keys::PublicAddress, - amount::{Amount, AmountError}, - blake2b_256::Blake2b256, - encrypted_fog_hint::EncryptedFogHint, - onetime_keys::{compute_shared_secret, compute_tx_pubkey, create_onetime_public_key}, - range::Range, - ring_signature::{Blinding, Commitment, KeyImage, SignatureRctFull, GENERATORS}, - RedactedTx, -}; use alloc::vec::Vec; use common::{Hash, HashMap}; use core::{ @@ -24,6 +14,18 @@ use prost::Message; use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; +use crate::{ + account_keys::PublicAddress, + amount::{Amount, AmountError}, + blake2b_256::Blake2b256, + constants::RING_SIZE, + encrypted_fog_hint::EncryptedFogHint, + onetime_keys::{compute_shared_secret, compute_tx_pubkey, create_onetime_public_key}, + range::Range, + ring_signature::{Blinding, KeyImage, SignatureRctBulletproofs, GENERATORS}, + CompressedCommitment, RedactedTx, +}; + /// Transaction hash length, in bytes. pub const TX_HASH_LEN: usize = 32; @@ -105,17 +107,7 @@ pub struct Tx { /// The transaction signature. #[prost(message, required, tag = "2")] - pub signature: SignatureRctFull, - - /// The proofs that output values are in the range [0,2^64) - /// These are serialized aggregated bulletproofs; must store as bytes - /// until we can use bulletproofs crate in enclave (nostd broken) - #[prost(bytes, tag = "3")] - pub range_proofs: Vec, - - /// The block number at which this transaction is no longer considered valid. - #[prost(uint64, tag = "4")] - pub tombstone_block: u64, + pub signature: SignatureRctBulletproofs, } impl fmt::Display for Tx { @@ -133,8 +125,8 @@ impl Tx { } /// Key images "spent" by this transaction. - pub fn key_images(&self) -> &Vec { - &self.signature.key_images + pub fn key_images(&self) -> Vec { + self.signature.key_images() } /// Get the highest index of each membership proof referenced by the transaction. @@ -144,7 +136,7 @@ impl Tx { /// Redacts all sensitive information. pub fn redact(self) -> RedactedTx { - let key_images: Vec = self.signature.key_images; + let key_images: Vec = self.signature.key_images(); let outputs = self.prefix.outputs; RedactedTx::new(outputs, key_images) } @@ -165,6 +157,10 @@ pub struct TxPrefix { /// Fee paid to the foundation for this transaction #[prost(uint64, tag = "3")] pub fee: u64, + + /// The block index at which this transaction is no longer valid. + #[prost(uint64, tag = "4")] + pub tombstone_block: u64, } impl TxPrefix { @@ -174,11 +170,13 @@ impl TxPrefix { /// * `inputs` - Inputs spent by the transaction. /// * `outputs` - Outputs created by the transaction. /// * `fee` - Transaction fee. - pub fn new(inputs: Vec, outputs: Vec, fee: u64) -> TxPrefix { + /// * `tombstone_block` - The block index at which this transaction is no longer valid. + pub fn new(inputs: Vec, outputs: Vec, fee: u64, tombstone_block: u64) -> TxPrefix { TxPrefix { inputs, outputs, fee, + tombstone_block, } } @@ -204,24 +202,23 @@ impl TxPrefix { } /// Get the commitment and blinding for the fee output. - pub fn fee_commitment_and_blinding(&self) -> (Commitment, Blinding) { - let fee_blinding = Blinding::from(0); - ( - Commitment::from( - GENERATORS.commit(Scalar::from(self.fee), fee_blinding.clone().into()), - ), - fee_blinding, - ) + pub fn fee_value_and_blinding(&self) -> (u64, Scalar) { + (self.fee, Scalar::zero()) } /// Get all output commitments (including explicit outputs and the implicit fee output). - pub fn output_commitments(&self) -> Vec { - let mut commitments: Vec = self + pub fn output_commitments(&self) -> Vec { + let mut commitments: Vec = self .outputs .iter() .map(|output| output.amount.commitment) .collect(); - commitments.push(self.fee_commitment_and_blinding().0); + + let fee_commitment = { + let (value, blinding) = self.fee_value_and_blinding(); + CompressedCommitment::new(value, blinding) + }; + commitments.push(fee_commitment); commitments } } @@ -229,11 +226,13 @@ impl TxPrefix { /// An "input" to a transaction. #[derive(Clone, Deserialize, Eq, PartialEq, Serialize, Message, Digestible)] pub struct TxIn { - /// A "ring" of outpuuts containing the single output that is being spent. + /// A "ring" of outputs containing the single output that is being spent. + /// It would be nice to use [TxOut; RING_SIZE] here, but Prost only works with Vec. #[prost(message, repeated, tag = "1")] pub ring: Vec, /// Proof that each TxOut in `ring` is in the ledger. + /// It would be nice to use [TxOutMembershipProof; RING_SIZE] here, but Prost only works with Vec. #[prost(message, repeated, tag = "2")] pub proofs: Vec, } @@ -399,19 +398,22 @@ prost_message_helper32! { TxOutMembershipHash } #[cfg(test)] mod tests { + use alloc::vec::Vec; + + use keys::RistrettoPublic; + use mcserial::ReprBytes32; + use prost::Message; + + use rand::{rngs::StdRng, SeedableRng}; + use crate::{ account_keys::AccountKey, amount::Amount, constants::{BASE_FEE, FEE_SPEND_PUBLIC_KEY, FEE_VIEW_PRIVATE_KEY, FEE_VIEW_PUBLIC_KEY}, encrypted_fog_hint::EncryptedFogHint, - ring_signature::{Blinding, CurvePoint, CurveScalar, KeyImage, SignatureRctFull}, + ring_signature::{Blinding, CurvePoint, CurveScalar, KeyImage, SignatureRctBulletproofs}, tx::{Tx, TxIn, TxOut, TxPrefix}, }; - use alloc::vec::Vec; - use keys::RistrettoPublic; - use mcserial::ReprBytes32; - use prost::Message; - use rand::{rngs::StdRng, SeedableRng}; #[test] // `serialize_tx` should create a Tx, encode/decode it, and compare @@ -449,6 +451,7 @@ mod tests { inputs: vec![tx_in], outputs: vec![tx_out], fee: BASE_FEE, + tombstone_block: 23, }; let mut buf = Vec::new(); @@ -458,22 +461,15 @@ mod tests { assert_eq!(prefix, TxPrefix::decode(&buf[..]).unwrap()); - let signature = SignatureRctFull { - key_images: vec![KeyImage::from(CurvePoint::from(23))], - challenge_responses: vec![], - challenge: CurveScalar::from(8), - }; + // TODO: use a meaningful signature. + let signature = SignatureRctBulletproofs::default(); - let tx = Tx { - prefix, - signature, - range_proofs: vec![8u8; 32], - tombstone_block: 23u64, - }; + let tx = Tx { prefix, signature }; let mut buf = Vec::new(); tx.encode(&mut buf).expect("failed to serialize into slice"); - assert_eq!(tx, Tx::decode(&buf[..]).unwrap()); + let recovered_tx: Tx = Tx::decode(&buf[..]).unwrap(); + assert_eq!(tx, recovered_tx); } #[test] diff --git a/transaction/core/src/validation/mod.rs b/transaction/core/src/validation/mod.rs index 3422213798..38c2c96d9b 100644 --- a/transaction/core/src/validation/mod.rs +++ b/transaction/core/src/validation/mod.rs @@ -4,4 +4,4 @@ mod error; mod validate; pub use error::{TransactionValidationError, TransactionValidationResult}; -pub use validate::{validate, validate_tombstone}; +pub use validate::{validate, validate_tombstone, validate_transaction_signature}; diff --git a/transaction/core/src/validation/validate.rs b/transaction/core/src/validation/validate.rs index 666acee2b7..9f32f1d2e1 100644 --- a/transaction/core/src/validation/validate.rs +++ b/transaction/core/src/validation/validate.rs @@ -8,15 +8,17 @@ use alloc::vec::Vec; use super::error::{TransactionValidationError, TransactionValidationResult}; use crate::{ + compressed_commitment::CompressedCommitment, constants::*, membership_proofs::{derive_proof_at_index, is_membership_proof_valid}, range_proofs::check_range_proofs, - ring_signature::{get_input_rows, verify}, tx::{Tx, TxOut, TxOutMembershipProof, TxPrefix}, }; use bulletproofs::RangeProof; use common::HashSet; +use core::convert::TryFrom; use curve25519_dalek::ristretto::CompressedRistretto; +use keys::{CompressedRistrettoPublic, RistrettoPublic}; use rand_core::{CryptoRng, RngCore}; /// Determines if the transaction is valid, with respect to the provided context. @@ -25,7 +27,7 @@ use rand_core::{CryptoRng, RngCore}; /// * `tx` - A pending transaction. /// * `current_block_index` - The index of the current block that is being built. /// * `root_proofs` - Membership proofs for each input ring element contained in `tx`. -/// * ` +/// * `csprng` - Cryptographically secure random number generator. pub fn validate( tx: &Tx, current_block_index: u64, @@ -36,25 +38,22 @@ pub fn validate( validate_number_of_outputs(&tx.prefix, MAX_OUTPUTS)?; - validate_ring_sizes(&tx.prefix, MIN_RING_SIZE, MAX_RING_SIZE)?; + validate_ring_sizes(&tx.prefix, RING_SIZE)?; validate_ring_elements_are_unique(&tx.prefix)?; validate_membership_proofs(&tx.prefix, &root_proofs)?; - validate_range_proofs(&tx, csprng)?; - - validate_transaction_signature(&tx)?; + validate_transaction_signature(&tx, csprng)?; validate_transaction_fee(&tx)?; validate_key_images_are_unique(&tx)?; - validate_tombstone(current_block_index, tx.tombstone_block)?; + validate_tombstone(current_block_index, tx.prefix.tombstone_block)?; - // TODO: The transaction must not contain a Key Image that has previously been spent. - // This should be implemented using proofs-of-non-membership, but could be implemented - // by a check in untrusted code. + // Note: The transaction must not contain a Key Image that has previously been spent. + // This must be checked outside the enclave. Ok(()) } @@ -99,42 +98,18 @@ fn validate_number_of_outputs( Ok(()) } -/// The transaction's input(s) must each have a ring with an allowable number of elements: -/// * All rings in a transaction must contain the same number of elements. -/// * Each input must contain a ring with no fewer than the minimum number of elements. -/// * Each input must contain a ring with no more than the maximum number of elements. -fn validate_ring_sizes( - tx_prefix: &TxPrefix, - minimum_ring_size: usize, - maximum_ring_size: usize, -) -> TransactionValidationResult<()> { - let ring_sizes: Vec = tx_prefix - .inputs - .iter() - .map(|tx_in| tx_in.ring.len()) - .collect(); - - // This should be enforced before this function is called by checking the number of inputs. - assert!(!ring_sizes.is_empty()); - - let first_ring_size = ring_sizes[0]; - for ring_size in &ring_sizes { - // All rings in a transaction must contain the same number of elements. - if *ring_size != first_ring_size { - return Err(TransactionValidationError::UnequalRingSizes); - } - - // Each input must contain a ring with no fewer than the minimum number of elements. - if *ring_size < minimum_ring_size as usize { - return Err(TransactionValidationError::InsufficientRingSize); - } - - // Each input must contain a ring with no more than the maximum number of elements. - if *ring_size > maximum_ring_size as usize { - return Err(TransactionValidationError::ExcessiveRingSize); +/// Each input must contain a ring containing `ring_size` elements. +fn validate_ring_sizes(tx_prefix: &TxPrefix, ring_size: usize) -> TransactionValidationResult<()> { + for input in &tx_prefix.inputs { + if input.ring.len() != ring_size { + let e = if input.ring.len() > ring_size { + TransactionValidationError::ExcessiveRingSize + } else { + TransactionValidationError::InsufficientRingSize + }; + return Err(e); } } - Ok(()) } @@ -167,34 +142,28 @@ fn validate_key_images_are_unique(tx: &Tx) -> TransactionValidationResult<()> { Ok(()) } -fn validate_range_proofs( +pub fn validate_transaction_signature( tx: &Tx, rng: &mut R, ) -> TransactionValidationResult<()> { - let commitments: Vec = tx - .prefix - .output_commitments() - .iter() - .map(|commitment| CompressedRistretto::from_slice(&commitment.to_bytes())) - .collect(); - - let range_proof = RangeProof::from_bytes(&tx.range_proofs) - .map_err(|_e| TransactionValidationError::InvalidRangeProof)?; + let tx_prefix_hash = tx.prefix.hash(); + let message = tx_prefix_hash.as_bytes(); - check_range_proofs(&range_proof, &commitments, rng) - .map_err(|_e| TransactionValidationError::InvalidRangeProof) -} - -fn validate_transaction_signature(tx: &Tx) -> TransactionValidationResult<()> { - let prefix_hash = tx.prefix.hash(); - let input_rows = get_input_rows(&tx.prefix.inputs)?; - let commitments = tx.prefix.output_commitments(); - - if !verify(&prefix_hash.0, &input_rows, &commitments, &tx.signature) { - return Err(TransactionValidationError::InvalidTransactionSignature); + let mut rings: Vec> = Vec::new(); + for input in &tx.prefix.inputs { + let ring: Vec<(CompressedRistrettoPublic, CompressedCommitment)> = input + .ring + .iter() + .map(|tx_out| (tx_out.target_key, tx_out.amount.commitment)) + .collect(); + rings.push(ring); } - Ok(()) + let output_commitments = tx.prefix.output_commitments(); + + tx.signature + .verify(message, &rings, &output_commitments, rng) + .map_err(|_e| TransactionValidationError::InvalidTransactionSignature) } /// The fee amount must be greater than or equal to `BASE_FEE`. @@ -326,32 +295,34 @@ pub fn validate_tombstone( } #[cfg(test)] -#[allow(unused)] mod tests { extern crate alloc; + use alloc::{string::ToString, vec::Vec}; use crate::{ account_keys::{AccountKey, PublicAddress}, - constants::{BASE_FEE, MIN_RING_SIZE}, + constants::{BASE_FEE, RING_SIZE}, get_tx_out_shared_secret, onetime_keys::recover_onetime_private_key, - ring_signature::{Commitment, KeyImage}, + ring_signature::{KeyImage, Scalar}, tx::{Tx, TxOut, TxOutMembershipHash, TxOutMembershipProof, TxPrefix}, validation::{ error::TransactionValidationError, validate::{ validate_key_images_are_unique, validate_membership_proofs, - validate_number_of_inputs, validate_number_of_outputs, validate_range_proofs, + validate_number_of_inputs, validate_number_of_outputs, validate_ring_elements_are_unique, validate_ring_sizes, validate_tombstone, validate_transaction_fee, validate_transaction_signature, MAX_TOMBSTONE_BLOCKS, }, }, + CompressedCommitment, }; use keys::{CompressedRistrettoPublic, RistrettoPublic}; use ledger_db::{Ledger, LedgerDB}; use mcserial::ReprBytes32; use rand::{rngs::StdRng, SeedableRng}; + use rand_core::RngCore; use serde::{de::DeserializeOwned, ser::Serialize}; use tempdir::TempDir; use transaction_test_utils::{ @@ -466,7 +437,7 @@ mod tests { for num_inputs in 0..100 { let mut tx_prefix = orig_tx.prefix.clone(); tx_prefix.inputs.clear(); - for i in 0..num_inputs { + for _i in 0..num_inputs { tx_prefix.inputs.push(orig_tx.prefix.inputs[0].clone()); } @@ -493,7 +464,7 @@ mod tests { for num_outputs in 0..100 { let mut tx_prefix = orig_tx.prefix.clone(); tx_prefix.outputs.clear(); - for i in 0..num_outputs { + for _i in 0..num_outputs { tx_prefix.outputs.push(orig_tx.prefix.outputs[0].clone()); } @@ -514,126 +485,84 @@ mod tests { #[test] fn test_validate_ring_sizes() { - let (orig_tx, _ledger) = create_test_tx(); - assert_eq!(orig_tx.prefix.inputs.len(), 1); - let min_ring_size = MIN_RING_SIZE; - let max_ring_size = min_ring_size + 3; + let (tx, _ledger) = create_test_tx(); + assert_eq!(tx.prefix.inputs.len(), 1); + assert_eq!(tx.prefix.inputs[0].ring.len(), RING_SIZE); - // A transaction with a single input and ring size of 0. + // A transaction with a single input containing RING_SIZE elements. + assert_eq!(validate_ring_sizes(&tx.prefix, RING_SIZE), Ok(())); + + // A single input containing zero elements. { - let mut tx_prefix = orig_tx.prefix.clone(); + let mut tx_prefix = tx.prefix.clone(); tx_prefix.inputs[0].ring.clear(); assert_eq!( - validate_ring_sizes(&tx_prefix, min_ring_size, max_ring_size), + validate_ring_sizes(&tx_prefix, RING_SIZE), Err(TransactionValidationError::InsufficientRingSize), ); } - // A transaction with a single input and ring size < minimum_ring_size. + // A single input containing too few elements. { - let mut tx_prefix = orig_tx.prefix.clone(); - tx_prefix.inputs[0].ring.truncate(min_ring_size - 1); + let mut tx_prefix = tx.prefix.clone(); + tx_prefix.inputs[0].ring.pop(); assert_eq!( - validate_ring_sizes(&tx_prefix, min_ring_size, max_ring_size), + validate_ring_sizes(&tx_prefix, RING_SIZE), Err(TransactionValidationError::InsufficientRingSize), ); } - // A transaction with a single input and ring size = minimum_ring_size. + // A single input containing too many elements. { - assert_eq!(orig_tx.prefix.inputs[0].ring.len(), min_ring_size); + let mut tx_prefix = tx.prefix.clone(); + let element = tx_prefix.inputs[0].ring[0].clone(); + tx_prefix.inputs[0].ring.push(element); assert_eq!( - validate_ring_sizes(&orig_tx.prefix, min_ring_size, max_ring_size), - Ok(()) + validate_ring_sizes(&tx_prefix, RING_SIZE), + Err(TransactionValidationError::ExcessiveRingSize), ); } - // A transaction with a single input and minimum_ring_size < ring size < maximum_ring_size. + // Two inputs each containing RING_SIZE elements. { - let mut tx_prefix = orig_tx.prefix.clone(); - tx_prefix.inputs[0] - .ring - .push(orig_tx.prefix.inputs[0].ring[0].clone()); + let mut tx_prefix = tx.prefix.clone(); + let input = tx_prefix.inputs[0].clone(); + tx_prefix.inputs.push(input); - assert_eq!( - validate_ring_sizes(&tx_prefix, min_ring_size, max_ring_size), - Ok(()) - ); + assert_eq!(validate_ring_sizes(&tx_prefix, RING_SIZE), Ok(())); } - // A transaction with a single input ring size = maximum_ring_size. + // The second input contains too few elements. { - let mut tx_prefix = orig_tx.prefix.clone(); - for _ in 0..max_ring_size - min_ring_size { - tx_prefix.inputs[0] - .ring - .push(orig_tx.prefix.inputs[0].ring[0].clone()); - } - assert_eq!(tx_prefix.inputs[0].ring.len(), max_ring_size); + let mut tx_prefix = tx.prefix.clone(); + let mut input = tx_prefix.inputs[0].clone(); + input.ring.pop(); + tx_prefix.inputs.push(input); assert_eq!( - validate_ring_sizes(&tx_prefix, min_ring_size, max_ring_size), - Ok(()) - ); - } - - // A transaction with a single input ring size > maximum_ring_size. - { - let mut tx_prefix = orig_tx.prefix.clone(); - for _ in 0..=max_ring_size - min_ring_size { - tx_prefix.inputs[0] - .ring - .push(orig_tx.prefix.inputs[0].ring[0].clone()); - } - assert_eq!(tx_prefix.inputs[0].ring.len(), max_ring_size + 1); - - assert_eq!( - validate_ring_sizes(&tx_prefix, min_ring_size, max_ring_size), - Err(TransactionValidationError::ExcessiveRingSize) - ); - } - - // A transaction with multiple inputs and unequal ring sizes. - { - let mut tx_prefix = orig_tx.prefix.clone(); - tx_prefix.inputs.push(orig_tx.prefix.inputs[0].clone()); - tx_prefix.inputs[1] - .ring - .push(orig_tx.prefix.inputs[0].ring[0].clone()); - - assert_eq!( - validate_ring_sizes(&tx_prefix, min_ring_size, max_ring_size), - Err(TransactionValidationError::UnequalRingSizes) + validate_ring_sizes(&tx_prefix, RING_SIZE), + Err(TransactionValidationError::InsufficientRingSize), ); } } #[test] fn test_validate_ring_elements_are_unique() { - let (orig_tx, _ledger) = create_test_tx(); - assert_eq!(orig_tx.prefix.inputs.len(), 1); - let min_ring_size = MIN_RING_SIZE; - let max_ring_size = min_ring_size + 3; + let (tx, _ledger) = create_test_tx(); + assert_eq!(tx.prefix.inputs.len(), 1); // A transaction with a single input and unique ring elements. - { - let mut tx_prefix = orig_tx.prefix.clone(); - assert_eq!(tx_prefix.inputs.len(), 1); - - assert_eq!(validate_ring_elements_are_unique(&tx_prefix), Ok(())); - } + assert_eq!(validate_ring_elements_are_unique(&tx.prefix), Ok(())); // A transaction with a single input and duplicate ring elements. { - let mut tx_prefix = orig_tx.prefix.clone(); - assert_eq!(tx_prefix.inputs.len(), 1); - + let mut tx_prefix = tx.prefix.clone(); tx_prefix.inputs[0] .ring - .push(orig_tx.prefix.inputs[0].ring[0].clone()); + .push(tx.prefix.inputs[0].ring[0].clone()); assert_eq!( validate_ring_elements_are_unique(&tx_prefix), @@ -643,8 +572,8 @@ mod tests { // A transaction with a multiple inputs and unique ring elements. { - let mut tx_prefix = orig_tx.prefix.clone(); - tx_prefix.inputs.push(orig_tx.prefix.inputs[0].clone()); + let mut tx_prefix = tx.prefix.clone(); + tx_prefix.inputs.push(tx.prefix.inputs[0].clone()); for mut tx_out in tx_prefix.inputs[1].ring.iter_mut() { let mut bytes = tx_out.target_key.to_bytes(); @@ -657,8 +586,8 @@ mod tests { // A transaction with a multiple inputs and duplicate ring elements in different rings. { - let mut tx_prefix = orig_tx.prefix.clone(); - tx_prefix.inputs.push(orig_tx.prefix.inputs[0].clone()); + let mut tx_prefix = tx.prefix.clone(); + tx_prefix.inputs.push(tx.prefix.inputs[0].clone()); assert_eq!( validate_ring_elements_are_unique(&tx_prefix), @@ -677,8 +606,10 @@ mod tests { /// validate_key_images_are_unique rejects duplicate key image. fn test_validate_key_images_are_unique_rejects_duplicate() { let (mut tx, _ledger) = create_test_tx(); - let key_image = tx.key_images()[0].clone(); - tx.signature.key_images.push(key_image); + // Tx only contains a single ring signature, which contains the key image. Duplicate the + // ring signature so that tx.key_images() returns a duplicate key image. + let ring_signature = tx.signature.ring_signatures[0].clone(); + tx.signature.ring_signatures.push(ring_signature); assert_eq!( validate_key_images_are_unique(&tx), @@ -693,44 +624,14 @@ mod tests { assert_eq!(validate_key_images_are_unique(&tx), Ok(()),); } - #[test] - fn test_validate_range_proofs() { - let (orig_tx, _ledger) = create_test_tx(); - let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - - // Valid range proofs - { - assert_eq!(validate_range_proofs(&orig_tx, &mut rng), Ok(())); - } - - // Range proofs that contain invalid data - { - let mut tx = orig_tx.clone(); - tx.range_proofs = vec![1, 2, 3]; - assert_eq!( - validate_range_proofs(&tx, &mut rng), - Err(TransactionValidationError::InvalidRangeProof) - ); - } - - // Invalid range proof - { - let mut tx = orig_tx.clone(); - tx.prefix.outputs[0].amount.commitment = Commitment::from(46); - assert_eq!( - validate_range_proofs(&tx, &mut rng), - Err(TransactionValidationError::InvalidRangeProof) - ); - } - } - #[test] fn test_validate_transaction_signature() { + let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); let (orig_tx, _ledger) = create_test_tx(); // Valid signature { - assert_eq!(validate_transaction_signature(&orig_tx), Ok(())); + assert_eq!(validate_transaction_signature(&orig_tx, &mut rng), Ok(())); } // Invalid signature due to altered input @@ -739,18 +640,23 @@ mod tests { tx.prefix.inputs[0].ring.pop(); assert_eq!( - validate_transaction_signature(&tx), + validate_transaction_signature(&tx, &mut rng), Err(TransactionValidationError::InvalidTransactionSignature) ); } - // Invalid signature due to altered output + // Invalid signature due to altered output. { let mut tx = orig_tx.clone(); - tx.prefix.outputs[0].amount.commitment = Commitment::from(46); + let wrong_commitment = { + let value = rng.next_u64(); + let blinding = Scalar::random(&mut rng); + CompressedCommitment::new(value, blinding) + }; + tx.prefix.outputs[0].amount.commitment = wrong_commitment; assert_eq!( - validate_transaction_signature(&tx), + validate_transaction_signature(&tx, &mut rng), Err(TransactionValidationError::InvalidTransactionSignature) ); } @@ -760,7 +666,7 @@ mod tests { // The amount here needs to be bigger than whatever is used in `initialize_ledger`. let (tx, _ledger) = create_test_tx_with_amount(INITIALIZE_LEDGER_AMOUNT - BASE_FEE, BASE_FEE); - assert_eq!(validate_transaction_signature(&tx), Ok(())); + assert_eq!(validate_transaction_signature(&tx, &mut rng), Ok(())); } } diff --git a/transaction/core/test-utils/src/lib.rs b/transaction/core/test-utils/src/lib.rs index db627466e8..1573fb336c 100644 --- a/transaction/core/test-utils/src/lib.rs +++ b/transaction/core/test-utils/src/lib.rs @@ -6,9 +6,10 @@ use ledger_db::{Ledger, LedgerDB}; use mcrand::{CryptoRng, RngCore}; use rand::{seq::SliceRandom, Rng}; use tempdir::TempDir; +use transaction::constants::RING_SIZE; pub use transaction::{ account_keys::{AccountKey, PublicAddress, DEFAULT_SUBADDRESS_INDEX}, - constants::{BASE_FEE, MIN_RING_SIZE}, + constants::BASE_FEE, get_tx_out_shared_secret, onetime_keys::recover_onetime_private_key, range::Range, @@ -90,7 +91,7 @@ pub fn create_transaction_with_amount( let origin_outputs = &origin_tx.outputs; // Populate a ring with mixins. - let mut ring: Vec = origin_outputs.iter().take(MIN_RING_SIZE).cloned().collect(); + let mut ring: Vec = origin_outputs.iter().take(RING_SIZE).cloned().collect(); if !ring.contains(tx_out) { ring[0] = tx_out.clone(); } @@ -140,7 +141,7 @@ pub fn create_transaction_with_amount( /// Populates the LedgerDB with initial data. /// /// Creates a number of blocks, each of which contains a single transaction. -/// The first contains MIN_RING_SIZE txos so we can create more valid transactions. +/// The first contains RING_SIZE txos so we can create more valid transactions. /// The rest have a single TxOut. /// /// The first block "mints" coins, and each subsequent block spends the TxOut produced by the @@ -193,7 +194,7 @@ pub fn initialize_ledger( } None => { // Create an origin block. - let outputs: Vec = (0..MIN_RING_SIZE) + let outputs: Vec = (0..RING_SIZE) .map(|_i| { TxOut::new( value, diff --git a/transaction/std/src/transaction_builder.rs b/transaction/std/src/transaction_builder.rs index b92275f570..f22dbc471b 100644 --- a/transaction/std/src/transaction_builder.rs +++ b/transaction/std/src/transaction_builder.rs @@ -4,21 +4,21 @@ //! //! See https://cryptonote.org/img/cryptonote_transaction.png -use keys::{FromRandom, RistrettoPrivate, RistrettoPublic}; +use keys::{CompressedRistrettoPublic, FromRandom, RistrettoPrivate, RistrettoPublic}; use std::collections::HashSet; use crate::{InputCredentials, TxBuilderError}; +use curve25519_dalek::scalar::Scalar; use rand_core::{CryptoRng, RngCore}; use transaction::{ account_keys::PublicAddress, - amount::AmountError, constants::BASE_FEE, encrypted_fog_hint::EncryptedFogHint, fog_hint::FogHint, onetime_keys::compute_shared_secret, - range_proofs::generate_range_proofs, - ring_signature::{get_input_rows, sign, Blinding, Commitment}, + ring_signature::SignatureRctBulletproofs, tx::{Tx, TxIn, TxOut, TxPrefix}, + CompressedCommitment, }; /// Helper utility for building and signing a CryptoNote-style transaction. @@ -54,7 +54,7 @@ impl TransactionBuilder { /// Add an output to the transaction. /// /// # Arguments - /// * `value` - The value of this output. + /// * `value` - The value of this output, in picoMOB. /// * `recipient` - The recipient's public address /// * `recipient_fog_ingest_key` - The recipient's fog server's public key /// * `rng` - RNG used to generate blinding for commitment @@ -86,7 +86,7 @@ impl TransactionBuilder { /// Sets the transaction fee. /// /// # Arguments - /// * `fee` - Transaction fee. + /// * `fee` - Transaction fee, in picoMOB. pub fn set_fee(&mut self, fee: u64) { self.fee = fee; } @@ -109,113 +109,78 @@ impl TransactionBuilder { } } - // RCT_TYPE_FULL requires the true input to be in the same position within each ring. - // If each ring is a column of a matrix, the true inputs must be on the same row. - // Randomly choose a row that will contain the real inputs. - let ring_size = self.input_credentials[0].ring.len(); // ring size, e.g. 11 - let chosen_input_row: usize = rng.next_u32() as usize % ring_size; - let inputs: Vec = self .input_credentials - .iter_mut() - .map(|input_credential| { - // Swap the real input into the chosen position. - let real_index = input_credential.real_index; - input_credential.ring.swap(real_index, chosen_input_row); - input_credential - .membership_proofs - .swap(real_index, chosen_input_row); - input_credential.real_index = chosen_input_row; - - TxIn { - ring: input_credential.ring.clone(), - proofs: input_credential.membership_proofs.clone(), - } + .iter() + .map(|input_credential| TxIn { + ring: input_credential.ring.clone(), + proofs: input_credential.membership_proofs.clone(), }) .collect(); - let tx_prefix = TxPrefix::new(inputs, self.outputs.clone(), self.fee); + let tx_prefix = TxPrefix::new(inputs, self.outputs.clone(), self.fee, self.tombstone_block); + + let tx_prefix_hash = tx_prefix.hash(); + let message = tx_prefix_hash.as_bytes(); + + let mut rings: Vec> = Vec::new(); + for input in &tx_prefix.inputs { + let ring: Vec<(CompressedRistrettoPublic, CompressedCommitment)> = input + .ring + .iter() + .map(|tx_out| (tx_out.target_key, tx_out.amount.commitment)) + .collect(); + rings.push(ring); + } - // Commitment and blinding factor for each output. - let mut output_commitments_and_blindings: Vec<(Commitment, Blinding)> = tx_prefix + let real_input_indices: Vec = self + .input_credentials + .iter() + .map(|input_credential| input_credential.real_index) + .collect(); + + // One-time private key, amount value, and amount blinding for each real input. + let mut input_secrets: Vec<(RistrettoPrivate, u64, Scalar)> = Vec::new(); + for input_credential in &self.input_credentials { + let onetime_private_key = input_credential.onetime_private_key; + let amount = &input_credential.ring[input_credential.real_index].amount; + let shared_secret = compute_shared_secret( + &input_credential.real_output_public_key, + &input_credential.view_private_key, + ); + let (value, blinding) = amount.get_value(&shared_secret)?; + input_secrets.push((onetime_private_key, value, blinding.into())); + } + + let mut output_values_and_blindings: Vec<(u64, Scalar)> = tx_prefix .outputs .iter() .enumerate() .map(|(index, tx_out)| { let amount = &tx_out.amount; let shared_secret = &self.output_shared_secrets[index]; - let (_value, blinding) = amount + let (value, blinding) = amount .get_value(shared_secret) .expect("TransactionBuilder created an invalid Amount"); - (amount.commitment, blinding) + (value, blinding.into()) }) .collect(); - // Add a commitment and blinding for the fee output. - let (fee_commitment, fee_blinding) = tx_prefix.fee_commitment_and_blinding(); - output_commitments_and_blindings.push((fee_commitment, fee_blinding)); - - // Range proof - let (range_proof, _commitments) = { - let (mut output_values, mut output_blindings): (Vec, Vec) = tx_prefix - .outputs - .iter() - .enumerate() - .map(|(index, tx_out)| { - let shared_secret = &self.output_shared_secrets[index]; - tx_out - .amount - .get_value(shared_secret) - .expect("TransactionBuilder created an invalid output amount.") - }) - .unzip(); - - output_values.push(tx_prefix.fee); - output_blindings.push(fee_blinding); - - generate_range_proofs(&output_values, &output_blindings, rng) - .map_err(|_e| TxBuilderError::RangeProofFailed)? - }; - - // The one-time private key and blinding for each input. - let input_keys_and_blindings_result: Result< - Vec<(RistrettoPrivate, Blinding)>, - AmountError, - > = self - .input_credentials - .iter() - .map(|input_credential| { - let amount = &input_credential.ring[input_credential.real_index].amount; - let shared_secret = compute_shared_secret( - &input_credential.real_output_public_key, - &input_credential.view_private_key, - ); - let (_value, blinding) = amount.get_value(&shared_secret)?; - let onetime_private_key = input_credential.onetime_private_key; - Ok((onetime_private_key, blinding)) - }) - .collect(); + // The fee output is implicit in the tx_prefix. + output_values_and_blindings.push(tx_prefix.fee_value_and_blinding()); - let input_keys_and_blindings: Vec<(RistrettoPrivate, Blinding)> = - input_keys_and_blindings_result?; - - // RingCT Signature - let prefix_hash = tx_prefix.hash(); - let input_rows = get_input_rows(&tx_prefix.inputs).unwrap(); - let signature = sign( - prefix_hash.as_bytes(), - &input_rows, - &output_commitments_and_blindings, - &input_keys_and_blindings, - chosen_input_row, + let signature = SignatureRctBulletproofs::sign( + message, + &rings, + &real_input_indices, + &input_secrets, + &output_values_and_blindings, rng, )?; Ok(Tx { prefix: tx_prefix, signature, - range_proofs: range_proof.to_bytes(), - tombstone_block: self.tombstone_block, }) } } @@ -230,7 +195,7 @@ impl Default for TransactionBuilder { /// Creates a TxOut that sends `value` to `recipient`. /// /// # Arguments -/// * `value` - Value of the output. +/// * `value` - Value of the output, in picoMOB. /// * `recipient` - Recipient's address. /// * `ingest_pubkey` - The public key for the recipients fog server, if any /// * `rng` - @@ -270,49 +235,62 @@ fn create_fog_hint( } #[cfg(test)] -pub mod tests { +pub mod transaction_builder_tests { use super::*; use rand::{rngs::StdRng, SeedableRng}; use std::convert::TryFrom; use transaction::{ account_keys::{AccountKey, DEFAULT_SUBADDRESS_INDEX}, + get_tx_out_shared_secret, onetime_keys::*, - ring_signature::{get_input_rows, verify}, tx::TxOutMembershipProof, + validation::validate_transaction_signature, }; + /// Creates a ring of of TxOuts. + /// + /// # Arguments + /// * `ring_size` - Number of elements in the ring. + /// * `account` - Owner of one of the ring elements. + /// * `value` - Value of the real element. + /// * `rng` - Randomness. + /// + /// Returns (ring, real_index) + fn get_ring( + ring_size: usize, + account: &AccountKey, + value: u64, + rng: &mut RNG, + ) -> (Vec, usize) { + let mut ring: Vec = Vec::new(); + + // Create ring_size - 1 mixins. + for _i in 0..ring_size - 1 { + let address = AccountKey::random(rng).default_subaddress(); + let (tx_out, _) = create_output(value, &address, None, rng).unwrap(); + ring.push(tx_out); + } + + // Insert the real element. + let real_index = (rng.next_u64() % ring_size as u64) as usize; + let (tx_out, _) = create_output(value, &account.default_subaddress(), None, rng).unwrap(); + ring.insert(real_index, tx_out); + assert_eq!(ring.len(), ring_size); + + (ring, real_index) + } + #[test] // Spend a single input and send its full value to a single recipient. fn test_simple_transaction() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let alice = AccountKey::random(&mut rng); let bob = AccountKey::random(&mut rng); + let value = 1475; // Mint an initial collection of outputs, including one belonging to Alice. - let minted_outputs: Vec = { - let mut recipient_and_amounts: Vec<(PublicAddress, u64)> = Vec::new(); - recipient_and_amounts.push((alice.default_subaddress(), 65536)); - - // Some outputs belonging to this account will be used as mix-ins. - let other_account = AccountKey::random(&mut rng); - recipient_and_amounts.push((other_account.default_subaddress(), 65536)); - recipient_and_amounts.push((other_account.default_subaddress(), 65536)); - - recipient_and_amounts - .iter() - .map(|(recipient, amount)| { - let (tx_out, _shared_secret) = - create_output(*amount, &recipient, None, &mut rng).unwrap(); - tx_out - }) - .collect() - }; - - let ring = minted_outputs; - // Spend the first minted_output - let real_index: usize = 0; - let real_output = ring[real_index as usize].clone(); + let (ring, real_index) = get_ring(3, &alice, value, &mut rng); + let real_output = ring[real_index].clone(); let onetime_private_key = recover_onetime_private_key( &RistrettoPublic::try_from(&real_output.public_key).unwrap(), @@ -327,7 +305,7 @@ pub mod tests { .map(|_tx_out| { // TransactionBuilder does not validate membership proofs, but does require one // for each ring member. - TxOutMembershipProof::new(0, 0, Default::default()) + TxOutMembershipProof::default() }) .collect(); @@ -344,45 +322,43 @@ pub mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_input(input_credentials); transaction_builder - .add_output(65536 - BASE_FEE, &bob.default_subaddress(), None, &mut rng) + .add_output(value - BASE_FEE, &bob.default_subaddress(), None, &mut rng) .unwrap(); let tx = transaction_builder.build(&mut rng).unwrap(); - // `transaction` should have a single input. + // The transaction should have a single input. assert_eq!(tx.prefix.inputs.len(), 1); assert_eq!(tx.prefix.inputs[0].proofs.len(), membership_proofs.len()); let expected_key_images = vec![key_image]; - assert_eq!(*tx.key_images(), expected_key_images); + assert_eq!(tx.key_images(), expected_key_images); - // `transaction` should have one output. + // The transaction should have one output. assert_eq!(tx.prefix.outputs.len(), 1); + let output: &TxOut = tx.prefix.outputs.get(0).unwrap(); + // The output should belong to the correct recipient. { - let tx_out: &TxOut = tx.prefix.outputs.get(0).unwrap(); assert!(view_key_matches_output( &bob.view_key(), - &RistrettoPublic::try_from(&tx_out.target_key).unwrap(), - &RistrettoPublic::try_from(&tx_out.public_key).unwrap() + &RistrettoPublic::try_from(&output.target_key).unwrap(), + &RistrettoPublic::try_from(&output.public_key).unwrap() )); } - // `transaction` should have a valid ringct signature + // The output should have the correct value. { - let prefix_hash = tx.prefix.hash(); - let input_rows = get_input_rows(&tx.prefix.inputs).unwrap(); - let commitments = tx.prefix.output_commitments(); - - assert!(verify( - prefix_hash.as_bytes(), - &input_rows, - &commitments, - &tx.signature - )); + let public_key = RistrettoPublic::try_from(&output.public_key).unwrap(); + let shared_secret = get_tx_out_shared_secret(bob.view_private_key(), &public_key); + let (output_value, _blinding) = output.amount.get_value(&shared_secret).unwrap(); + assert_eq!(output_value, value - BASE_FEE); } + + // The transaction should have a valid signature. + assert!(validate_transaction_signature(&tx, &mut rng).is_ok()); } #[test] @@ -391,4 +367,57 @@ pub mod tests { fn test_inputs_with_different_ring_sizes() { unimplemented!() } + + #[test] + // `build` should return an error if the sum of inputs does not equal the sum of outputs and the fee. + fn test_inputs_do_not_equal_outputs() { + let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); + let alice = AccountKey::random(&mut rng); + let bob = AccountKey::random(&mut rng); + let value = 1475; + + // Mint an initial collection of outputs, including one belonging to Alice. + let (ring, real_index) = get_ring(3, &alice, value, &mut rng); + let real_output = ring[real_index].clone(); + + let onetime_private_key = recover_onetime_private_key( + &RistrettoPublic::try_from(&real_output.public_key).unwrap(), + &alice.view_private_key(), + &alice.subaddress_spend_key(DEFAULT_SUBADDRESS_INDEX), + ); + + let membership_proofs: Vec = ring + .iter() + .map(|_tx_out| { + // TransactionBuilder does not validate membership proofs, but does require one + // for each ring member. + TxOutMembershipProof::default() + }) + .collect(); + + let input_credentials = InputCredentials::new( + ring, + membership_proofs.clone(), + real_index, + onetime_private_key, + *alice.view_private_key(), + &mut rng, + ) + .unwrap(); + + let mut transaction_builder = TransactionBuilder::new(); + transaction_builder.add_input(input_credentials); + + let wrong_value = 999; + transaction_builder + .add_output(wrong_value, &bob.default_subaddress(), None, &mut rng) + .unwrap(); + + let result = transaction_builder.build(&mut rng); + // Signing should fail if value is not conserved. + match result { + Err(TxBuilderError::RingSignatureFailed) => {} // Expected. + _ => panic!("Unexpected result {:?}", result), + } + } } diff --git a/util/build-grpc/Cargo.toml b/util/build-grpc/Cargo.toml new file mode 100644 index 0000000000..8b1fbdf2a4 --- /dev/null +++ b/util/build-grpc/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "mc-build-grpc" +version = "1.0.0" +authors = ["MobileCoin"] +edition = "2018" + +[dependencies] +mcbuild-utils = { path = "../../mcbuild/utils" } + +protoc-grpcio = "0.3.1" diff --git a/util/build-grpc/README.md b/util/build-grpc/README.md new file mode 100644 index 0000000000..af6f007924 --- /dev/null +++ b/util/build-grpc/README.md @@ -0,0 +1,4 @@ +Cargo build-script utilities for automatic compilation of protobuf files. + +This crate provides a programatic API for dealing with protobuf files compilation into Rust code that can be used by GRPC clients and servers. +It relies on `protoc_grpcio` to do the actual compilation. The extra functionality provided by it is storing the auto-generated code outside of `src/`. Instead, the code is stored in `OUT_DIR` (`target/`). That functionality does not exist in `protoc_grpcio` and requires the boilterplate code provided by this crate. diff --git a/util/build-grpc/src/lib.rs b/util/build-grpc/src/lib.rs new file mode 100644 index 0000000000..c4ae26d030 --- /dev/null +++ b/util/build-grpc/src/lib.rs @@ -0,0 +1,52 @@ +// Copyright (c) 2018-2020 MobileCoin Inc. + +#![feature(external_doc)] +#![doc(include = "../README.md")] + +use mcbuild_utils::Environment; +use std::{ffi::OsStr, fs, path::PathBuf}; + +/// Compile protobuf files into Rust code, and generate a mod.rs that references all the generated +/// modules. +pub fn compile_protos_and_generate_mod_rs(proto_dirs: &[&str], proto_files: &[&str]) { + let env = Environment::default(); + + // Output directory for genereated code. + let output_destination = env.out_dir().join("protos-auto-gen"); + + // If the proto files change, we need to re-run. + for dir in proto_dirs.iter() { + mcbuild_utils::rerun_if_path_changed(&PathBuf::from(dir)); + } + + // Delete old code and create output directory. + let _ = fs::remove_dir_all(&output_destination); + fs::create_dir_all(&output_destination).expect("failed creating output destination"); + + // Generate code. + protoc_grpcio::compile_grpc_protos(proto_files, proto_dirs, &output_destination) + .expect("Failed to compile gRPC definitions!"); + + // Generate the mod.rs file that includes all the auto-generated code. + let mod_file_contents = fs::read_dir(&output_destination) + .expect("failed reading output directory") + .filter_map(|res| res.map(|e| e.path()).ok()) + .filter_map(|path| { + if path.extension() == Some(&OsStr::new("rs")) { + Some(format!( + "pub mod {};", + path.file_stem().unwrap().to_str().unwrap() + )) + } else { + None + } + }) + .collect::>() + .join("\n"); + + let mod_file_path = output_destination.join("mod.rs"); + + if fs::read_to_string(&mod_file_path).ok().as_ref() != Some(&mod_file_contents) { + fs::write(&mod_file_path, &mod_file_contents).expect("Failed writing mod.rs"); + } +} diff --git a/util/generate-sample-ledger/src/lib.rs b/util/generate-sample-ledger/src/lib.rs index 68a438d0b2..5e92e9cf99 100644 --- a/util/generate-sample-ledger/src/lib.rs +++ b/util/generate-sample-ledger/src/lib.rs @@ -8,7 +8,7 @@ use rand_hc::Hc128Rng as FixedRng; use rayon::prelude::*; use std::{path::PathBuf, vec::Vec}; use transaction::{ - account_keys::PublicAddress, constants::MAX_TINY_MOB, encrypted_fog_hint::EncryptedFogHint, + account_keys::PublicAddress, constants::TOTAL_MOB, encrypted_fog_hint::EncryptedFogHint, ring_signature::KeyImage, tx::TxOut, Block, RedactedTx, BLOCK_VERSION, }; @@ -21,6 +21,8 @@ use transaction::{ /// * `recipients` - /// * `num_outputs_per_recipient` - Number of equal-valued outputs that each recipient receives, per block. /// * `num_blocks` - Number of blocks that will be created. +/// +/// This will panic if it attempts to distribute the total value of mobilecoin into fewer than 16 outputs. pub fn bootstrap_ledger( path: &PathBuf, recipients: &[PublicAddress], @@ -34,12 +36,12 @@ pub fn bootstrap_ledger( let mut db = LedgerDB::open(path.clone()).expect("Could not open ledger_db"); let num_outputs: u64 = (recipients.len() * num_txos_per_account * num_blocks) as u64; - let initial_amount: u64 = MAX_TINY_MOB / num_outputs; + let picomob_per_output: u64 = (TOTAL_MOB / num_outputs) * 1_000_000_000_000; println!("recipients: {}", recipients.len()); println!( - "Making {:?} transactions for {:?} tiny mob", - num_outputs, initial_amount + "Making {:?} outputs of {:?} picoMOB.", + num_outputs, picomob_per_output ); let mut blocks_and_transactions: Vec<(Block, Vec)> = Vec::new(); @@ -63,7 +65,7 @@ pub fn bootstrap_ledger( .map(|_i| { create_minting_transaction( recipient, - initial_amount, + picomob_per_output, key_image_count as u64, &mut rng, ) diff --git a/util/grpc/Cargo.toml b/util/grpc/Cargo.toml index bc1e12e39a..55a86e0f0a 100644 --- a/util/grpc/Cargo.toml +++ b/util/grpc/Cargo.toml @@ -11,11 +11,11 @@ futures = "0.1" grpcio = "0.5.1" hex_fmt = "0.3" lazy_static = "1.4" -mcserial = { path = "../../util/mcserial", features = ["std"]} -metrics = { path = "../../util/metrics" } +mcserial = { path = "../mcserial", features = ["std"]} +metrics = { path = "../metrics" } prost = { version = "0.6.1", default-features = false, features = ["prost-derive"] } protobuf = "2.12" rand = "0.6.5" [build-dependencies] -protoc-grpcio = "0.3.1" +mc-build-grpc = { path = "../build-grpc" } diff --git a/util/grpc/build.rs b/util/grpc/build.rs index 424d7ca0d1..d0a6d0c334 100644 --- a/util/grpc/build.rs +++ b/util/grpc/build.rs @@ -1,20 +1,5 @@ // Copyright (c) 2018-2020 MobileCoin Inc. -extern crate protoc_grpcio; - -fn compile_protos() { - let proto_root = "./proto"; - let proto_files = ["health_api.proto"]; - let output_destination = "src"; - println!("cargo:rerun-if-changed={}", proto_root); - for file in &proto_files { - println!("cargo:rerun-if-changed={}/{}", proto_root, file); - } - - protoc_grpcio::compile_grpc_protos(&proto_files, &[proto_root], output_destination) - .expect("Failed to compile gRPC definitions!"); -} - fn main() { - compile_protos(); + mc_build_grpc::compile_protos_and_generate_mod_rs(&["./proto"], &["health_api.proto"]); } diff --git a/util/grpc/src/.gitignore b/util/grpc/src/.gitignore deleted file mode 100644 index f5b5ad8659..0000000000 --- a/util/grpc/src/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -health_api.rs -health_api_grpc.rs diff --git a/util/grpc/src/lib.rs b/util/grpc/src/lib.rs index 499b435b8f..7c06204ab3 100644 --- a/util/grpc/src/lib.rs +++ b/util/grpc/src/lib.rs @@ -1,8 +1,17 @@ // Copyright (c) 2018-2020 MobileCoin Inc. -// Auto-generatd modules (see build.rs) -mod health_api; -mod health_api_grpc; +mod autogenerated_code { + 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::*; mod health_service;