diff --git a/src/FrostFS.SDK.ClientV2/Client.cs b/src/FrostFS.SDK.ClientV2/Client.cs index 5df849a..64c16d1 100644 --- a/src/FrostFS.SDK.ClientV2/Client.cs +++ b/src/FrostFS.SDK.ClientV2/Client.cs @@ -2,6 +2,7 @@ using System.Security.Cryptography; using FrostFS.Container; using FrostFS.Netmap; using FrostFS.Object; +using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.Cryptography; using FrostFS.SDK.ModelsV2; diff --git a/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs b/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs index fbf897d..0575797 100644 --- a/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs +++ b/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs @@ -5,7 +5,7 @@ using DeleteResponse = FrostFS.Container.DeleteResponse; using GetResponse = FrostFS.Container.GetResponse; using PutResponse = FrostFS.Container.PutResponse; -namespace FrostFS.SDK.ClientV2; +namespace FrostFS.SDK.ClientV2.Interfaces; public interface IFrostFSClient { @@ -14,6 +14,7 @@ public interface IFrostFSClient Task GetContainerAsync(ContainerID containerId); Task DeleteContainerAsync(ContainerID containerId); Task GetObjectHeadAsync(ContainerID containerId, ObjectID objectId); - Task PutObjectAsync(Object.Header header, Stream payload); + Task GetObjectAsync(ContainerID containerId, ObjectID objectId); + Task PutObjectAsync(Header header, Stream payload); Task DeleteObjectAsync(ContainerID containerId, ObjectID objectId); } \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Container.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Container.cs index c880b2f..b258662 100644 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Container.cs +++ b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Container.cs @@ -19,14 +19,14 @@ public static class ContainerMapper public static ModelsV2.Container ToModel(this Container.Container container) { - var basicAclName = Enum.GetName(typeof(BasicACL), container.BasicAcl); + var basicAclName = Enum.GetName(typeof(BasicAcl), container.BasicAcl); if (basicAclName is null) { throw new ArgumentException($"Unknown BasicACL rule. Value: '{container.BasicAcl}'."); } return new ModelsV2.Container( - Enum.Parse(basicAclName), + Enum.Parse(basicAclName), container.PlacementPolicy.ToModel() ) { diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/PlacementPolicy.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/PlacementPolicy.cs index 71f66ae..d1b2409 100644 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/PlacementPolicy.cs +++ b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/PlacementPolicy.cs @@ -24,12 +24,9 @@ public static class PlacementPolicyMapper public static ModelsV2.Netmap.PlacementPolicy ToModel(this PlacementPolicy placementPolicy) { - var replicas = new List(); - foreach (var replica in placementPolicy.Replicas) - { - replicas.Add(replica.ToModel()); - } - - return new ModelsV2.Netmap.PlacementPolicy(placementPolicy.Unique, replicas.ToArray()); + return new ModelsV2.Netmap.PlacementPolicy( + placementPolicy.Unique, + placementPolicy.Replicas.Select(replica => replica.ToModel()).ToArray() + ); } } \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Object.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Object.cs index 0e0a493..6dda24b 100644 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Object.cs +++ b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Object.cs @@ -52,20 +52,27 @@ public static class ObjectHeadMapper throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'."); } - var attributes = new List(); - foreach (var attribute in header.Attributes) - { - attributes.Add(attribute.ToModel()); - } - return new ObjectHeader( ContainerId.FromHash(header.ContainerId.Value.ToByteArray()), Enum.Parse(objTypeName), - attributes.ToArray() + header.Attributes.Select(attribute => attribute.ToModel()).ToArray() ) { Size = (long)header.PayloadLength, Version = header.Version.ToModel() }; } +} + +public static class ObjectMapper +{ + public static ModelsV2.Object ToModel(this Object.Object obj) + { + return new ModelsV2.Object + { + Header = obj.Header.ToModel(), + ObjectId = ObjectId.FromHash(obj.ObjectId.Value.ToByteArray()), + Payload = obj.Payload.ToByteArray() + }; + } } \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/RequestConstructor.cs b/src/FrostFS.SDK.ClientV2/RequestConstructor.cs index fb632ee..c0682a4 100644 --- a/src/FrostFS.SDK.ClientV2/RequestConstructor.cs +++ b/src/FrostFS.SDK.ClientV2/RequestConstructor.cs @@ -3,6 +3,7 @@ using FrostFS.Refs; using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.ModelsV2; using FrostFS.Session; +using Version = FrostFS.SDK.ModelsV2.Version; namespace FrostFS.SDK.ClientV2; diff --git a/src/FrostFS.SDK.ClientV2/RequestSigner.cs b/src/FrostFS.SDK.ClientV2/RequestSigner.cs index 88c64c3..52fae36 100644 --- a/src/FrostFS.SDK.ClientV2/RequestSigner.cs +++ b/src/FrostFS.SDK.ClientV2/RequestSigner.cs @@ -18,14 +18,14 @@ namespace FrostFS.SDK.ClientV2 public static byte[] SignRFC6979(this ECDsa key, byte[] data) { var digest = new Sha256Digest(); - var secp256r1 = SecNamedCurves.GetByName("secp256r1"); - var ec_parameters = new ECDomainParameters(secp256r1.Curve, secp256r1.G, secp256r1.N); - var private_key = new ECPrivateKeyParameters(new BigInteger(1, key.PrivateKey()), ec_parameters); + var secp256R1 = SecNamedCurves.GetByName("secp256r1"); + var ecParameters = new ECDomainParameters(secp256R1.Curve, secp256R1.G, secp256R1.N); + var privateKey = new ECPrivateKeyParameters(new BigInteger(1, key.PrivateKey()), ecParameters); var signer = new ECDsaSigner(new HMacDsaKCalculator(digest)); var hash = new byte[digest.GetDigestSize()]; digest.BlockUpdate(data, 0, data.Length); digest.DoFinal(hash, 0); - signer.Init(true, private_key); + signer.Init(true, privateKey); var rs = signer.GenerateSignature(hash); var signature = new byte[RFC6979SignatureSize]; var rbytes = rs[0].ToByteArrayUnsigned(); @@ -67,11 +67,11 @@ namespace FrostFS.SDK.ClientV2 public static Signature SignMessagePart(this ECDsa key, IMessage? data) { - var data2sign = data is null ? Array.Empty() : data.ToByteArray(); + var data2Sign = data is null ? Array.Empty() : data.ToByteArray(); var sig = new Signature { Key = ByteString.CopyFrom(key.PublicKey()), - Sign = ByteString.CopyFrom(key.SignData(data2sign)), + Sign = ByteString.CopyFrom(key.SignData(data2Sign)), }; return sig; } diff --git a/src/FrostFS.SDK.ClientV2/RequestVerifier.cs b/src/FrostFS.SDK.ClientV2/RequestVerifier.cs index 5301e76..cc0e27f 100644 --- a/src/FrostFS.SDK.ClientV2/RequestVerifier.cs +++ b/src/FrostFS.SDK.ClientV2/RequestVerifier.cs @@ -24,19 +24,19 @@ namespace FrostFS.SDK.ClientV2 { return rs; } - public static bool VerifyRFC6979(this byte[] public_key, byte[] data, byte[] sig) + public static bool VerifyRFC6979(this byte[] publicKey, byte[] data, byte[] sig) { - if (public_key is null || data is null || sig is null) return false; + if (publicKey is null || data is null || sig is null) return false; var rs = DecodeSignature(sig); var digest = new Sha256Digest(); var signer = new ECDsaSigner(new HMacDsaKCalculator(digest)); - var secp256r1 = SecNamedCurves.GetByName("secp256r1"); - var ec_parameters = new ECDomainParameters(secp256r1.Curve, secp256r1.G, secp256r1.N); - var bc_public_key = new ECPublicKeyParameters(secp256r1.Curve.DecodePoint(public_key), ec_parameters); + var secp256R1 = SecNamedCurves.GetByName("secp256r1"); + var ecParameters = new ECDomainParameters(secp256R1.Curve, secp256R1.G, secp256R1.N); + var bcPublicKey = new ECPublicKeyParameters(secp256R1.Curve.DecodePoint(publicKey), ecParameters); var hash = new byte[digest.GetDigestSize()]; digest.BlockUpdate(data, 0, data.Length); digest.DoFinal(hash, 0); - signer.Init(false, bc_public_key); + signer.Init(false, bcPublicKey); return signer.VerifySignature(hash, rs[0], rs[1]); } @@ -54,8 +54,8 @@ namespace FrostFS.SDK.ClientV2 { { if (sig is null || sig.Key is null || sig.Sign is null) return false; using var key = sig.Key.ToByteArray().LoadPublicKey(); - var data2verify = data is null ? Array.Empty() : data.ToByteArray(); - return key.VerifyData(data2verify, sig.Sign.ToByteArray()); + var data2Verify = data is null ? Array.Empty() : data.ToByteArray(); + return key.VerifyData(data2Verify, sig.Sign.ToByteArray()); } public static bool VerifyMatryoskaLevel(IMessage body, IMetaHeader meta, IVerificationHeader verification) @@ -65,8 +65,7 @@ namespace FrostFS.SDK.ClientV2 { if (!verification.OriginSignature.VerifyMessagePart(origin)) return false; if (origin is null) return verification.BodySignature.VerifyMessagePart(body); - if (verification.BodySignature is not null) return false; - return VerifyMatryoskaLevel(body, meta.GetOrigin(), origin); + return verification.BodySignature is null && VerifyMatryoskaLevel(body, meta.GetOrigin(), origin); } public static bool Verify(this IVerificableMessage message) diff --git a/src/FrostFS.SDK.ClientV2/Services/Container.cs b/src/FrostFS.SDK.ClientV2/Services/Container.cs index aaa9ed9..53d37b0 100644 --- a/src/FrostFS.SDK.ClientV2/Services/Container.cs +++ b/src/FrostFS.SDK.ClientV2/Services/Container.cs @@ -65,25 +65,4 @@ public partial class Client request.Sign(_key); return await _containerServiceClient.DeleteAsync(request); } - - // private void PrepareContainerSessionToken(RequestMetaHeader meta, SessionToken sessionToken, ContainerID? cid, - // ContainerSessionContext.Types.Verb verb) - // { - // if (meta.SessionToken is not null) return; - // meta.SessionToken = sessionToken; - // var ctx = new ContainerSessionContext - // { - // Verb = verb - // }; - // if (cid is null) - // { - // ctx.Wildcard = true; - // } - // else - // { - // ctx.ContainerId = cid; - // } - // meta.SessionToken.Body.Container = ctx; - // meta.SessionToken.Signature = _key.SignMessagePart(meta.SessionToken.Body); - // } } \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Services/Object.cs b/src/FrostFS.SDK.ClientV2/Services/Object.cs index 74dfc99..636acd7 100644 --- a/src/FrostFS.SDK.ClientV2/Services/Object.cs +++ b/src/FrostFS.SDK.ClientV2/Services/Object.cs @@ -29,11 +29,64 @@ public partial class Client return await _objectServiceClient.HeadAsync(request); } - // public async Task GetObjectAsync(ContainerID cid, ObjectID oid) - // { - // - // } + public async Task GetObjectAsync(ContainerID cid, ObjectID oid) + { + var sessionToken = await CreateSessionAsync(uint.MaxValue); + var request = new GetRequest + { + Body = new GetRequest.Types.Body + { + Raw = false, + Address = new Address + { + ContainerId = cid, + ObjectId = oid + }, + } + }; + request.AddMetaHeader(); + request.AddObjectSessionToken( + sessionToken, + cid, + oid, + ObjectSessionContext.Types.Verb.Get, + _key + ); + request.Sign(_key); + + return await GetObject(request); + } + + private async Task GetObject(GetRequest request) + { + using var stream = GetObjectInit(request); + var obj = await stream.ReadHeader(); + var payload = new byte[obj.Header.PayloadLength]; + var offset = 0; + var chunk = await stream.ReadChunk(); + while (chunk is not null) + { + chunk.CopyTo(payload, offset); + offset += chunk.Length; + chunk = await stream.ReadChunk(); + } + obj.Payload = ByteString.CopyFrom(payload); + return obj; + } + private ObjectReader GetObjectInit(GetRequest initRequest) + { + if (initRequest is null) + { + throw new ArgumentNullException(nameof(initRequest)); + } + + return new ObjectReader + { + Call = _objectServiceClient.Get(initRequest) + }; + } + public async Task PutObjectAsync(Header header, Stream payload) { var sessionToken = await CreateSessionAsync(uint.MaxValue); @@ -63,7 +116,7 @@ public partial class Client ); request.Sign(_key); - using var stream = await InitObject(request); + using var stream = await PutObjectInit(request); var buffer = new byte[Constants.ObjectChunkSize]; var bufferLength = payload.Read(buffer, 0, Constants.ObjectChunkSize); while (bufferLength > 0) @@ -81,7 +134,7 @@ public partial class Client return await stream.Close(); } - private async Task InitObject(PutRequest initRequest) + private async Task PutObjectInit(PutRequest initRequest) { if (initRequest is null) { @@ -112,14 +165,47 @@ public partial class Client } } -internal class ObjectStreamer : IDisposable +internal class ObjectReader : IDisposable { - public AsyncClientStreamingCall Call { get; init; } + public AsyncServerStreamingCall Call { get; init; } + + public async Task ReadHeader() + { + if (!await Call.ResponseStream.MoveNext()) + { + throw new InvalidOperationException("unexpect end of stream"); + } + var response = Call.ResponseStream.Current; + if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Init) + throw new InvalidOperationException("unexpect message type"); + return new Object.Object + { + ObjectId = response.Body.Init.ObjectId, + Header = response.Body.Init.Header, + }; + } + + public async Task ReadChunk() + { + if (!await Call.ResponseStream.MoveNext()) + { + return null; + } + var response = Call.ResponseStream.Current; + if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Chunk) + throw new InvalidOperationException("unexpect message type"); + return response.Body.Chunk.ToByteArray(); + } public void Dispose() { Call.Dispose(); } +} + +internal class ObjectStreamer : IDisposable +{ + public AsyncClientStreamingCall Call { get; init; } public async Task Write(PutRequest request) { @@ -136,4 +222,9 @@ internal class ObjectStreamer : IDisposable await Call.RequestStream.CompleteAsync(); return await Call.ResponseAsync; } -} \ No newline at end of file + + public void Dispose() + { + Call.Dispose(); + } +} diff --git a/src/FrostFS.SDK.Cryptography/ArrayHelper.cs b/src/FrostFS.SDK.Cryptography/ArrayHelper.cs index 3a2646c..1cb43fe 100644 --- a/src/FrostFS.SDK.Cryptography/ArrayHelper.cs +++ b/src/FrostFS.SDK.Cryptography/ArrayHelper.cs @@ -7,12 +7,11 @@ namespace FrostFS.SDK.Cryptography [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] Concat(params byte[][] buffers) { - int length = 0; - for (int i = 0; i < buffers.Length; i++) - length += buffers[i].Length; - byte[] dst = new byte[length]; - int p = 0; - foreach (byte[] src in buffers) + var length = buffers.Sum(buffer => buffer.Length); + + var dst = new byte[length]; + var p = 0; + foreach (var src in buffers) { Buffer.BlockCopy(src, 0, dst, p, src.Length); p += src.Length; diff --git a/src/FrostFS.SDK.Cryptography/Key.cs b/src/FrostFS.SDK.Cryptography/Key.cs index 15b888f..0c6e7ad 100644 --- a/src/FrostFS.SDK.Cryptography/Key.cs +++ b/src/FrostFS.SDK.Cryptography/Key.cs @@ -22,8 +22,8 @@ public static class KeyExtension $"{nameof(Compress)} argument isn't uncompressed public key. " + $"expected length={UncompressedPublicKeyLength}, actual={publicKey.Length}" ); - var secp256r1 = SecNamedCurves.GetByName("secp256r1"); - var point = secp256r1.Curve.DecodePoint(publicKey); + var secp256R1 = SecNamedCurves.GetByName("secp256r1"); + var point = secp256R1.Curve.DecodePoint(publicKey); return point.GetEncoded(true); } @@ -34,8 +34,8 @@ public static class KeyExtension $"{nameof(Decompress)} argument isn't compressed public key. " + $"expected length={CompressedPublicKeyLength}, actual={publicKey.Length}" ); - var secp256r1 = SecNamedCurves.GetByName("secp256r1"); - var point = secp256r1.Curve.DecodePoint(publicKey); + var secp256R1 = SecNamedCurves.GetByName("secp256r1"); + var point = secp256R1.Curve.DecodePoint(publicKey); return point.GetEncoded(false); } @@ -115,19 +115,19 @@ public static class KeyExtension return key.ExportParameters(true).D; } - public static ECDsa LoadPrivateKey(this byte[] private_key) + public static ECDsa LoadPrivateKey(this byte[] privateKey) { - var secp256r1 = SecNamedCurves.GetByName("secp256r1"); - var public_key = - secp256r1.G.Multiply(new Org.BouncyCastle.Math.BigInteger(1, private_key)).GetEncoded(false)[1..]; + var secp256R1 = SecNamedCurves.GetByName("secp256r1"); + var publicKey = + secp256R1.G.Multiply(new Org.BouncyCastle.Math.BigInteger(1, privateKey)).GetEncoded(false)[1..]; var key = ECDsa.Create(new ECParameters { Curve = ECCurve.NamedCurves.nistP256, - D = private_key, + D = privateKey, Q = new ECPoint { - X = public_key[..32], - Y = public_key[32..] + X = publicKey[..32], + Y = publicKey[32..] } }); return key; @@ -135,20 +135,20 @@ public static class KeyExtension public static ECDsa LoadWif(this string wif) { - var private_key = GetPrivateKeyFromWIF(wif); - return LoadPrivateKey(private_key); + var privateKey = GetPrivateKeyFromWIF(wif); + return LoadPrivateKey(privateKey); } - public static ECDsa LoadPublicKey(this byte[] public_key) + public static ECDsa LoadPublicKey(this byte[] publicKey) { - var public_key_full = public_key.Decompress()[1..]; + var publicKeyFull = publicKey.Decompress()[1..]; var key = ECDsa.Create(new ECParameters { Curve = ECCurve.NamedCurves.nistP256, Q = new ECPoint { - X = public_key_full[..32], - Y = public_key_full[32..] + X = publicKeyFull[..32], + Y = publicKeyFull[32..] } }); return key; diff --git a/src/FrostFS.SDK.ModelsV2/Container.cs b/src/FrostFS.SDK.ModelsV2/Container.cs index 9588483..03f4783 100644 --- a/src/FrostFS.SDK.ModelsV2/Container.cs +++ b/src/FrostFS.SDK.ModelsV2/Container.cs @@ -6,11 +6,11 @@ namespace FrostFS.SDK.ModelsV2; public class Container { public Guid Nonce { get; set; } - public BasicACL BasicAcl { get; set; } + public BasicAcl BasicAcl { get; set; } public PlacementPolicy PlacementPolicy { get; set; } public Version Version { get; set; } - public Container(BasicACL basicAcl, PlacementPolicy placementPolicy) + public Container(BasicAcl basicAcl, PlacementPolicy placementPolicy) { Nonce = Guid.NewGuid(); BasicAcl = basicAcl; diff --git a/src/FrostFS.SDK.ModelsV2/ContainerId.cs b/src/FrostFS.SDK.ModelsV2/ContainerId.cs index ef4542e..fc2e9ee 100644 --- a/src/FrostFS.SDK.ModelsV2/ContainerId.cs +++ b/src/FrostFS.SDK.ModelsV2/ContainerId.cs @@ -4,7 +4,7 @@ namespace FrostFS.SDK.ModelsV2; public class ContainerId { - public string Value { get; } + public string Value { get; set; } public ContainerId(string id) { diff --git a/src/FrostFS.SDK.ModelsV2/Enums/BasicACL.cs b/src/FrostFS.SDK.ModelsV2/Enums/BasicAcl.cs similarity index 95% rename from src/FrostFS.SDK.ModelsV2/Enums/BasicACL.cs rename to src/FrostFS.SDK.ModelsV2/Enums/BasicAcl.cs index 2964f0b..f22dd36 100644 --- a/src/FrostFS.SDK.ModelsV2/Enums/BasicACL.cs +++ b/src/FrostFS.SDK.ModelsV2/Enums/BasicAcl.cs @@ -2,7 +2,7 @@ using System.ComponentModel; namespace FrostFS.SDK.ModelsV2.Enums; -public enum BasicACL +public enum BasicAcl { [Description("Basic ACL for private container")] Private = 0x1C8C8CCC, diff --git a/src/FrostFS.SDK.ModelsV2/Object.cs b/src/FrostFS.SDK.ModelsV2/Object.cs index 24cb6a3..6d456d9 100644 --- a/src/FrostFS.SDK.ModelsV2/Object.cs +++ b/src/FrostFS.SDK.ModelsV2/Object.cs @@ -37,5 +37,6 @@ public class ObjectHeader public class Object { public ObjectHeader Header { get; set; } - public Stream Payload { get; set; } + public ObjectId ObjectId { get; set; } + public byte[] Payload { get; set; } } \ No newline at end of file diff --git a/src/FrostFS.SDK.Service/Service.cs b/src/FrostFS.SDK.Service/Service.cs index b528424..2449e91 100644 --- a/src/FrostFS.SDK.Service/Service.cs +++ b/src/FrostFS.SDK.Service/Service.cs @@ -1,4 +1,5 @@ using FrostFS.SDK.ClientV2; +using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.ModelsV2; @@ -16,14 +17,11 @@ public class FrostFsService public async Task ListContainersAsync() { - var containersIds = new List(); var listContainersResponse = await _client.ListContainersAsync(); - foreach (var cid in listContainersResponse.Body.ContainerIds) - { - containersIds.Add(ContainerId.FromHash(cid.Value.ToByteArray())); - } - return containersIds.ToArray(); + return listContainersResponse.Body.ContainerIds.Select( + cid => ContainerId.FromHash(cid.Value.ToByteArray()) + ).ToArray(); } public async Task CreateContainerAsync(ModelsV2.Container container) @@ -52,12 +50,21 @@ public class FrostFsService return getObjectHeadResponse.Body.Header.Header.ToModel(); } + public async Task GetObjectAsync(ContainerId containerId, ObjectId objectId) + { + var obj = await _client.GetObjectAsync( + containerId.ToGrpcMessage(), + objectId.ToGrpcMessage() + ); + return obj.ToModel(); + } + public async Task PutObjectAsync(ObjectHeader header, Stream payload) { var putObjectResponse = await _client.PutObjectAsync(header.ToGrpcMessage(), payload); return ObjectId.FromHash(putObjectResponse.Body.ObjectId.Value.ToByteArray()); } - + public async Task PutObjectAsync(ObjectHeader header, byte[] payload) { var putObjectResponse = await _client.PutObjectAsync(header.ToGrpcMessage(), new MemoryStream(payload));