From cb85f69311e867e32623e86a54467240049af4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Honza=20Rychnovsk=C3=BD?= Date: Mon, 17 Jul 2023 12:51:27 +0200 Subject: [PATCH] [#1113][#114] Add GetSubtreeRoots method support * [#1113] Adopt updated gRPC proto files * [#1114] Add GetSubtreeRoots method --- .../lightwallet/client/LightWalletClient.kt | 19 +++++ .../lightwallet/client/internal/Constants.kt | 4 +- .../client/internal/LightWalletClientImpl.kt | 48 ++++++++++-- .../client/model/ShieldedProtocolEnum.kt | 13 ++++ .../client/model/SubtreeRootUnsafe.kt | 27 +++++++ .../src/main/proto/compact_formats.proto | 74 +++++++++++-------- .../src/main/proto/darkside.proto | 2 +- .../src/main/proto/service.proto | 38 ++++++++-- 8 files changed, 179 insertions(+), 46 deletions(-) create mode 100644 lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/ShieldedProtocolEnum.kt create mode 100644 lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/SubtreeRootUnsafe.kt diff --git a/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/LightWalletClient.kt b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/LightWalletClient.kt index 75277ba9..75a065fb 100644 --- a/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/LightWalletClient.kt +++ b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/LightWalletClient.kt @@ -11,6 +11,8 @@ import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe import co.electriccoin.lightwallet.client.model.Response import co.electriccoin.lightwallet.client.model.SendResponseUnsafe +import co.electriccoin.lightwallet.client.model.ShieldedProtocolEnum +import co.electriccoin.lightwallet.client.model.SubtreeRootUnsafe import kotlinx.coroutines.flow.Flow /** @@ -70,6 +72,23 @@ interface LightWalletClient { blockHeightRange: ClosedRange ): Flow> + /** + * Returns a stream of information about roots of subtrees of the Sapling and Orchard note commitment trees. + * + * @return a flow of information about roots of subtrees of the Sapling and Orchard note commitment trees. + * + * @param startIndex Index identifying where to start returning subtree roots + * @param shieldedProtocol Shielded protocol to return subtree roots for. See `ShieldedProtocolEnum` enum class. + * @param maxEntries Maximum number of entries to return, or 0 for all entries + * + * @throws IllegalArgumentException when empty argument provided + */ + fun getSubtreeRoots( + startIndex: Int, + shieldedProtocol: ShieldedProtocolEnum, + maxEntries: Int + ): Flow> + /** * Reconnect to the same or a different server. This is useful when the connection is * unrecoverable. That might be time to switch to a mirror or just reconnect. diff --git a/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/internal/Constants.kt b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/internal/Constants.kt index e5b825c9..421f02b3 100644 --- a/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/internal/Constants.kt +++ b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/internal/Constants.kt @@ -2,5 +2,7 @@ package co.electriccoin.lightwallet.client.internal internal object Constants { const val LOG_TAG = "LightWalletClient" // NON-NLS - const val ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE = "Illegal argument provided - can't be empty:" // NON-NLS + const val ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_COMMON = "Illegal argument provided:" // NON-NLS + const val ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_EMPTY = "$ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_COMMON " + + "can't be empty:" // NON-NLS } diff --git a/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/internal/LightWalletClientImpl.kt b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/internal/LightWalletClientImpl.kt index 573e905f..1b07580d 100644 --- a/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/internal/LightWalletClientImpl.kt +++ b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/internal/LightWalletClientImpl.kt @@ -13,6 +13,8 @@ import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe import co.electriccoin.lightwallet.client.model.Response import co.electriccoin.lightwallet.client.model.SendResponseUnsafe +import co.electriccoin.lightwallet.client.model.ShieldedProtocolEnum +import co.electriccoin.lightwallet.client.model.SubtreeRootUnsafe import com.google.protobuf.ByteString import io.grpc.CallOptions import io.grpc.Channel @@ -48,7 +50,7 @@ internal class LightWalletClientImpl private constructor( override fun getBlockRange(heightRange: ClosedRange): Flow> { require(!heightRange.isEmpty()) { - "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} range: $heightRange." // NON-NLS + "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_EMPTY} range: $heightRange." // NON-NLS } return try { @@ -101,8 +103,8 @@ internal class LightWalletClientImpl private constructor( override suspend fun submitTransaction(spendTransaction: ByteArray): Response { require(spendTransaction.isNotEmpty()) { - "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} Failed to submit transaction because it was empty, so " + - "this request was ignored on the client-side." // NON-NLS + "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_EMPTY} Failed to submit transaction because it was empty," + + " so this request was ignored on the client-side." // NON-NLS } val request = Service.RawTransaction.newBuilder() @@ -122,8 +124,8 @@ internal class LightWalletClientImpl private constructor( override suspend fun fetchTransaction(txId: ByteArray): Response { require(txId.isNotEmpty()) { - "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} Failed to start fetching the transaction with null " + - "transaction ID, so this request was ignored on the client-side." // NON-NLS + "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_EMPTY} Failed to start fetching the transaction with" + + " null transaction ID, so this request was ignored on the client-side." // NON-NLS } val request = Service.TxFilter.newBuilder().setHash(ByteString.copyFrom(txId)).build() @@ -144,7 +146,7 @@ internal class LightWalletClientImpl private constructor( startHeight: BlockHeightUnsafe ): Flow> { require(tAddresses.isNotEmpty() && tAddresses.all { it.isNotBlank() }) { - "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} array of addresses contains invalid item." // NON-NLS + "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_EMPTY} array of addresses contains invalid item." // NON-NLS } val getUtxosBuilder = Service.GetAddressUtxosArg.newBuilder() @@ -176,7 +178,8 @@ internal class LightWalletClientImpl private constructor( blockHeightRange: ClosedRange ): Flow> { require(!blockHeightRange.isEmpty() && tAddress.isNotBlank()) { - "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} range: $blockHeightRange, address: $tAddress." // NON-NLS + "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_EMPTY} range: $blockHeightRange, address: " + + "$tAddress." // NON-NLS } val request = Service.TransparentAddressBlockFilter.newBuilder() @@ -200,6 +203,37 @@ internal class LightWalletClientImpl private constructor( } } + override fun getSubtreeRoots( + startIndex: Int, + shieldedProtocol: ShieldedProtocolEnum, + maxEntries: Int + ): Flow> { + require(startIndex >= 0 && maxEntries >= 0) { + "${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_COMMON} startIndex: $startIndex, maxEntries: $maxEntries." + } + + val getSubtreeRootsArgBuilder = Service.GetSubtreeRootsArg.newBuilder() + getSubtreeRootsArgBuilder.startIndex = startIndex + getSubtreeRootsArgBuilder.shieldedProtocol = shieldedProtocol.toProtocol() + getSubtreeRootsArgBuilder.maxEntries = maxEntries + + val request = getSubtreeRootsArgBuilder.build() + + return try { + requireChannel().createStub(streamingRequestTimeout) + .getSubtreeRoots(request) + .map { + val response: Response = Response.Success(SubtreeRootUnsafe.new(it)) + response + }.catch { + val failure: Response.Failure = GrpcStatusResolver.resolveFailureFromStatus(it) + emit(failure) + } + } catch (e: StatusException) { + flowOf(GrpcStatusResolver.resolveFailureFromStatus(e)) + } + } + override fun shutdown() { channel.shutdown() } diff --git a/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/ShieldedProtocolEnum.kt b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/ShieldedProtocolEnum.kt new file mode 100644 index 00000000..0d6e2c78 --- /dev/null +++ b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/ShieldedProtocolEnum.kt @@ -0,0 +1,13 @@ +package co.electriccoin.lightwallet.client.model + +import cash.z.wallet.sdk.internal.rpc.Service.ShieldedProtocol + +enum class ShieldedProtocolEnum { + SAPLING, + ORCHARD; + + fun toProtocol() = when (this) { + SAPLING -> ShieldedProtocol.sapling + ORCHARD -> ShieldedProtocol.orchard + } +} diff --git a/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/SubtreeRootUnsafe.kt b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/SubtreeRootUnsafe.kt new file mode 100644 index 00000000..49dde978 --- /dev/null +++ b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/SubtreeRootUnsafe.kt @@ -0,0 +1,27 @@ +package co.electriccoin.lightwallet.client.model + +import cash.z.wallet.sdk.internal.rpc.Service.SubtreeRoot + +/** + * SubtreeRoot contains information about roots of subtrees of the Sapling and Orchard note commitment trees, which + * has come from the Light Wallet server. + * + * It is marked as "unsafe" because it is not guaranteed to be valid. + * + * @param rootHash The 32-byte Merkle root of the subtree + * @param completingBlockHash The hash of the block that completed this subtree. + * @param completingBlockHeight The height of the block that completed this subtree in the main chain. + */ +class SubtreeRootUnsafe( + val rootHash: ByteArray, + val completingBlockHash: ByteArray, + val completingBlockHeight: BlockHeightUnsafe +) { + companion object { + fun new(subtreeRoot: SubtreeRoot) = SubtreeRootUnsafe( + rootHash = subtreeRoot.rootHash.toByteArray(), + completingBlockHash = subtreeRoot.completingBlockHash.toByteArray(), + completingBlockHeight = BlockHeightUnsafe(subtreeRoot.completingBlockHeight), + ) + } +} diff --git a/lightwallet-client-lib/src/main/proto/compact_formats.proto b/lightwallet-client-lib/src/main/proto/compact_formats.proto index a8790896..394e5130 100644 --- a/lightwallet-client-lib/src/main/proto/compact_formats.proto +++ b/lightwallet-client-lib/src/main/proto/compact_formats.proto @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2020 The Zcash developers +// Copyright (c) 2019-2021 The Zcash developers // Distributed under the MIT software license, see the accompanying // file COPYING or https://www.opensource.org/licenses/mit-license.php . @@ -7,61 +7,75 @@ package cash.z.wallet.sdk.rpc; option java_package = "cash.z.wallet.sdk.internal.rpc"; option go_package = "lightwalletd/walletrpc"; option swift_prefix = ""; + // Remember that proto3 fields are all optional. A field that is not present will be set to its zero value. // bytes fields of hashes are in canonical little-endian format. +// ChainMetadata represents information about the state of the chain as of a given block. +message ChainMetadata { + uint32 saplingCommitmentTreeSize = 1; // the size of the Sapling note commitment tree as of the end of this block + uint32 orchardCommitmentTreeSize = 2; // the size of the Orchard note commitment tree as of the end of this block +} + // CompactBlock is a packaging of ONLY the data from a block that's needed to: // 1. Detect a payment to your shielded Sapling address // 2. Detect a spend of your shielded Sapling notes // 3. Update your witnesses to generate new Sapling spend proofs. message CompactBlock { - uint32 protoVersion = 1; // the version of this wire format, for storage - uint64 height = 2; // the height of this block - bytes hash = 3; // the ID (hash) of this block, same as in block explorers - bytes prevHash = 4; // the ID (hash) of this block's predecessor - uint32 time = 5; // Unix epoch time when the block was mined - bytes header = 6; // (hash, prevHash, and time) OR (full header) - repeated CompactTx vtx = 7; // zero or more compact transactions from this block + uint32 protoVersion = 1; // the version of this wire format, for storage + uint64 height = 2; // the height of this block + bytes hash = 3; // the ID (hash) of this block, same as in block explorers + bytes prevHash = 4; // the ID (hash) of this block's predecessor + uint32 time = 5; // Unix epoch time when the block was mined + bytes header = 6; // (hash, prevHash, and time) OR (full header) + repeated CompactTx vtx = 7; // zero or more compact transactions from this block + ChainMetadata chainMetadata = 8; // information about the state of the chain as of this block } // CompactTx contains the minimum information for a wallet to know if this transaction // is relevant to it (either pays to it or spends from it) via shielded elements // only. This message will not encode a transparent-to-transparent transaction. message CompactTx { - uint64 index = 1; // the index within the full block - bytes hash = 2; // the ID (hash) of this transaction, same as in block explorers + // Index and hash will allow the receiver to call out to chain + // explorers or other data structures to retrieve more information + // about this transaction. + uint64 index = 1; // the index within the full block + bytes hash = 2; // the ID (hash) of this transaction, same as in block explorers - // The transaction fee: present if server can provide. In the case of a - // stateless server and a transaction with transparent inputs, this will be - // unset because the calculation requires reference to prior transactions. - // in a pure-Sapling context, the fee will be calculable as: - // valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut)) - uint32 fee = 3; + // The transaction fee: present if server can provide. In the case of a + // stateless server and a transaction with transparent inputs, this will be + // unset because the calculation requires reference to prior transactions. + // If there are no transparent inputs, the fee will be calculable as: + // valueBalanceSapling + valueBalanceOrchard + sum(vPubNew) - sum(vPubOld) - sum(tOut) + uint32 fee = 3; - repeated CompactSaplingSpend spends = 4; // inputs - repeated CompactSaplingOutput outputs = 5; // outputs - repeated CompactOrchardAction actions = 6; + repeated CompactSaplingSpend spends = 4; + repeated CompactSaplingOutput outputs = 5; + repeated CompactOrchardAction actions = 6; } // CompactSaplingSpend is a Sapling Spend Description as described in 7.3 of the Zcash // protocol specification. message CompactSaplingSpend { - bytes nf = 1; // nullifier (see the Zcash protocol specification) + bytes nf = 1; // nullifier (see the Zcash protocol specification) } -// output is a Sapling Output Description as described in section 7.4 of the -// Zcash protocol spec. Total size is 948. +// output encodes the `cmu` field, `ephemeralKey` field, and a 52-byte prefix of the +// `encCiphertext` field of a Sapling Output Description. These fields are described in +// section 7.4 of the Zcash protocol spec: +// https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus +// Total size is 116 bytes. message CompactSaplingOutput { - bytes cmu = 1; // note commitment u-coordinate - bytes epk = 2; // ephemeral public key - bytes ciphertext = 3; // first 52 bytes of ciphertext + bytes cmu = 1; // note commitment u-coordinate + bytes ephemeralKey = 2; // ephemeral public key + bytes ciphertext = 3; // first 52 bytes of ciphertext } // https://github.com/zcash/zips/blob/main/zip-0225.rst#orchard-action-description-orchardaction // (but not all fields are needed) message CompactOrchardAction { - bytes nullifier = 1; // [32] The nullifier of the input note - bytes cmx = 2; // [32] The x-coordinate of the note commitment for the output note - bytes ephemeralKey = 3; // [32] An encoding of an ephemeral Pallas public key - bytes ciphertext = 4; // [52] The note plaintext component of the encCiphertext field -} \ No newline at end of file + bytes nullifier = 1; // [32] The nullifier of the input note + bytes cmx = 2; // [32] The x-coordinate of the note commitment for the output note + bytes ephemeralKey = 3; // [32] An encoding of an ephemeral Pallas public key + bytes ciphertext = 4; // [52] The first 52 bytes of the encCiphertext field +} diff --git a/lightwallet-client-lib/src/main/proto/darkside.proto b/lightwallet-client-lib/src/main/proto/darkside.proto index 25de5d4c..70e41caf 100644 --- a/lightwallet-client-lib/src/main/proto/darkside.proto +++ b/lightwallet-client-lib/src/main/proto/darkside.proto @@ -131,4 +131,4 @@ service DarksideStreamer { // Clear the list of GetTreeStates entries (can't fail) rpc ClearAllTreeStates(Empty) returns (Empty) {} -} \ No newline at end of file +} diff --git a/lightwallet-client-lib/src/main/proto/service.proto b/lightwallet-client-lib/src/main/proto/service.proto index 28b2617a..16174e2e 100644 --- a/lightwallet-client-lib/src/main/proto/service.proto +++ b/lightwallet-client-lib/src/main/proto/service.proto @@ -12,8 +12,8 @@ import "compact_formats.proto"; // A BlockID message contains identifiers to select a block: a height or a // hash. Specification by hash is not implemented, but may be in the future. message BlockID { - uint64 height = 1; - bytes hash = 2; + uint64 height = 1; + bytes hash = 2; } // BlockRange specifies a series of blocks from start to end inclusive. @@ -27,12 +27,12 @@ message BlockRange { // transaction: either a block and an index, or a direct transaction hash. // Currently, only specification by hash is supported. message TxFilter { - BlockID block = 1; // block identifier, height or hash - uint64 index = 2; // index within the block - bytes hash = 3; // transaction ID (hash, txid) + BlockID block = 1; // block identifier, height or hash + uint64 index = 2; // index within the block + bytes hash = 3; // transaction ID (hash, txid) } -// RawTransaction contains the complete transaction data. It also optionally includes +// RawTransaction contains the complete transaction data. It also optionally includes // the block height in which the transaction was included, or, when returned // by GetMempoolStream(), the latest block height. message RawTransaction { @@ -119,6 +119,22 @@ message TreeState { string orchardTree = 6; // orchard commitment tree state } +enum ShieldedProtocol { + sapling = 0; + orchard = 1; +} + +message GetSubtreeRootsArg { + uint32 startIndex = 1; // Index identifying where to start returning subtree roots + ShieldedProtocol shieldedProtocol = 2; // Shielded protocol to return subtree roots for + uint32 maxEntries = 3; // Maximum number of entries to return, or 0 for all entries. +} +message SubtreeRoot { + bytes rootHash = 2; // The 32-byte Merkle root of the subtree. + bytes completingBlockHash = 3; // The hash of the block that completed this subtree. + uint64 completingBlockHeight = 4; // The height of the block that completed this subtree in the main chain. +} + // Results are sorted by height, which makes it easy to issue another // request that picks up from where the previous left off. message GetAddressUtxosArg { @@ -143,8 +159,12 @@ service CompactTxStreamer { rpc GetLatestBlock(ChainSpec) returns (BlockID) {} // Return the compact block corresponding to the given block identifier rpc GetBlock(BlockID) returns (CompactBlock) {} + // Same as GetBlock except actions contain only nullifiers + rpc GetBlockNullifiers(BlockID) returns (CompactBlock) {} // Return a list of consecutive compact blocks rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {} + // Same as GetBlockRange except actions contain only nullifiers + rpc GetBlockRangeNullifiers(BlockRange) returns (stream CompactBlock) {} // Return the requested full (not compact) transaction (as from zcashd) rpc GetTransaction(TxFilter) returns (RawTransaction) {} @@ -178,6 +198,10 @@ service CompactTxStreamer { rpc GetTreeState(BlockID) returns (TreeState) {} rpc GetLatestTreeState(Empty) returns (TreeState) {} + // Returns a stream of information about roots of subtrees of the Sapling and Orchard + // note commitment trees. + rpc GetSubtreeRoots(GetSubtreeRootsArg) returns (stream SubtreeRoot) {} + rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {} rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {} @@ -185,4 +209,4 @@ service CompactTxStreamer { rpc GetLightdInfo(Empty) returns (LightdInfo) {} // Testing-only, requires lightwalletd --ping-very-insecure (do not enable in production) rpc Ping(Duration) returns (PingResponse) {} -} \ No newline at end of file +}