[#1113][#114] Add GetSubtreeRoots method support

* [#1113] Adopt updated gRPC proto files

* [#1114] Add GetSubtreeRoots method
This commit is contained in:
Honza Rychnovský 2023-07-17 12:51:27 +02:00 committed by GitHub
parent f1b5e3aade
commit cb85f69311
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 179 additions and 46 deletions

View File

@ -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<BlockHeightUnsafe>
): Flow<Response<RawTransactionUnsafe>>
/**
* 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<Response<SubtreeRootUnsafe>>
/**
* 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.

View File

@ -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
}

View File

@ -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<BlockHeightUnsafe>): Flow<Response<CompactBlockUnsafe>> {
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<SendResponseUnsafe> {
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<RawTransactionUnsafe> {
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<Response<GetAddressUtxosReplyUnsafe>> {
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<BlockHeightUnsafe>
): Flow<Response<RawTransactionUnsafe>> {
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<Response<SubtreeRootUnsafe>> {
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<SubtreeRootUnsafe> = Response.Success(SubtreeRootUnsafe.new(it))
response
}.catch {
val failure: Response.Failure<SubtreeRootUnsafe> = GrpcStatusResolver.resolveFailureFromStatus(it)
emit(failure)
}
} catch (e: StatusException) {
flowOf(GrpcStatusResolver.resolveFailureFromStatus(e))
}
}
override fun shutdown() {
channel.shutdown()
}

View File

@ -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
}
}

View File

@ -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),
)
}
}

View File

@ -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
}
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
}

View File

@ -131,4 +131,4 @@ service DarksideStreamer {
// Clear the list of GetTreeStates entries (can't fail)
rpc ClearAllTreeStates(Empty) returns (Empty) {}
}
}

View File

@ -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) {}
}
}